From ebd4f8be18f3f6ff670296748515d7020aeb4af4 Mon Sep 17 00:00:00 2001 From: bridge Date: Tue, 25 Nov 2025 00:17:12 +0800 Subject: [PATCH] add new avatar button and func --- src/classes/avatar.py | 2 + src/server/main.py | 250 ++++++++- src/sim/new_avatar.py | 7 - static/config.yml | 16 +- web/src/api/game.ts | 56 +- web/src/components/SystemMenu.vue | 495 +++++++++++++++++- .../game/panels/info/AvatarDetail.vue | 1 + web/src/stores/world.ts | 44 +- 8 files changed, 803 insertions(+), 68 deletions(-) diff --git a/src/classes/avatar.py b/src/classes/avatar.py index 93ccb4f..a77b404 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -111,6 +111,8 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): spirit_animal: Optional[SpiritAnimal] = None # 绰号:江湖中对该角色的称谓,满足条件后生成,永久不变 nickname: Optional[str] = None + # 自定义头像ID:如果设置,优先使用此ID显示头像 + custom_pic_id: Optional[int] = None # 当月/当步新设动作标记:在 commit_next_plan 设为 True,首次 tick_action 后清为 False _new_action_set_this_step: bool = False # 动作冷却:记录动作类名 -> 上次完成月戳 diff --git a/src/server/main.py b/src/server/main.py index f680c20..c1cf525 100644 --- a/src/server/main.py +++ b/src/server/main.py @@ -18,9 +18,16 @@ from src.sim.simulator import Simulator from src.classes.world import World from src.classes.calendar import Month, Year, create_month_stamp from src.run.create_map import create_cultivation_world_map, add_sect_headquarters -from src.sim.new_avatar import make_avatars as _new_make +from src.sim.new_avatar import make_avatars as _new_make, get_new_avatar_with_config from src.utils.config import CONFIG from src.classes.sect import sects_by_id +from src.classes.technique import techniques_by_id +from src.classes.weapon import weapons_by_id +from src.classes.auxiliary import auxiliaries_by_id +from src.classes.appearance import get_appearance_by_level +from src.classes.persona import personas_by_id +from src.classes.cultivation import REALM_ORDER +from src.classes.alignment import Alignment from src.classes.color import serialize_hover_lines from src.classes.event import Event from src.classes.long_term_objective import set_user_long_term_objective, clear_user_long_term_objective @@ -76,6 +83,17 @@ def get_avatar_pic_id(avatar_id: str, gender_val: str) -> int: idx = abs(hash(str(avatar_id))) % len(available) return available[idx] + +def resolve_avatar_pic_id(avatar) -> int: + """Return the actual avatar portrait ID, respecting custom overrides.""" + if avatar is None: + return 1 + custom_pic_id = getattr(avatar, "custom_pic_id", None) + if custom_pic_id is not None: + return custom_pic_id + gender_val = getattr(getattr(avatar, "gender", None), "value", "male") + return get_avatar_pic_id(str(getattr(avatar, "id", "")), gender_val or "male") + # 触发配置重载的标记 (technique.csv updated) # 简易的命令行参数检查 (不使用 argparse 以避免冲突和时序问题) @@ -258,7 +276,7 @@ async def game_loop(): "x": int(getattr(a, "pos_x", 0)), "y": int(getattr(a, "pos_y", 0)), "gender": a.gender.value, # 使用 value (male/female) 而不是 str (中文) - "pic_id": get_avatar_pic_id(str(a.id), a.gender.value), + "pic_id": resolve_avatar_pic_id(a), "action": getattr(a, "current_action", {}).get("name", "发呆") if hasattr(a, "current_action") and a.current_action else "发呆" }) @@ -450,7 +468,7 @@ def get_state(): "y": ay, "action": str(aaction), "gender": str(a.gender.value), - "pic_id": get_avatar_pic_id(aid, str(a.gender.value)) + "pic_id": resolve_avatar_pic_id(a) }) except Exception as e: return {"step": 3, "error": str(e)} @@ -650,6 +668,232 @@ def clear_long_term_objective(req: ClearObjectiveRequest): "message": "Objective cleared" if cleared else "No user objective to clear" } +# --- 角色管理 API --- + +class CreateAvatarRequest(BaseModel): + surname: Optional[str] = None + given_name: Optional[str] = None + gender: Optional[str] = None + age: Optional[int] = None + level: Optional[int] = None + sect_id: Optional[int] = None + persona_ids: Optional[List[int]] = None + pic_id: Optional[int] = None + technique_id: Optional[int] = None + weapon_id: Optional[int] = None + auxiliary_id: Optional[int] = None + alignment: Optional[str] = None + appearance: Optional[int] = None + +class DeleteAvatarRequest(BaseModel): + avatar_id: str + +@app.get("/api/meta/game_data") +def get_game_data(): + """获取游戏元数据(宗门、个性、境界等),供前端选择""" + # 1. 宗门列表 + sects_list = [] + for s in sects_by_id.values(): + sects_list.append({ + "id": s.id, + "name": s.name, + "alignment": s.alignment.value + }) + + # 2. 个性列表 + personas_list = [] + for p in personas_by_id.values(): + personas_list.append({ + "id": p.id, + "name": p.name, + "desc": p.desc, + "rarity": p.rarity.level.name if hasattr(p.rarity, 'level') else "N" + }) + + # 3. 境界列表 + realms_list = [r.value for r in REALM_ORDER] + + # 4. 功法 / 兵器 / 辅助装备 + techniques_list = [ + { + "id": t.id, + "name": t.name, + "grade": t.grade.value, + "attribute": t.attribute.value, + "sect": t.sect + } + for t in techniques_by_id.values() + ] + + weapons_list = [ + { + "id": w.id, + "name": w.name, + "type": w.weapon_type.value, + "grade": w.grade.value, + "sect_id": w.sect_id + } + for w in weapons_by_id.values() + ] + + auxiliaries_list = [ + { + "id": a.id, + "name": a.name, + "grade": a.grade.value, + "sect_id": a.sect_id + } + for a in auxiliaries_by_id.values() + ] + + alignments_list = [ + { + "value": align.value, + "label": str(align) + } + for align in Alignment + ] + + return { + "sects": sects_list, + "personas": personas_list, + "realms": realms_list, + "techniques": techniques_list, + "weapons": weapons_list, + "auxiliaries": auxiliaries_list, + "alignments": alignments_list + } + +@app.get("/api/meta/avatar_list") +def get_avatar_list_simple(): + """获取简略的角色列表,用于管理界面""" + world = game_instance.get("world") + if not world: + return {"avatars": []} + + result = [] + for a in world.avatar_manager.avatars.values(): + sect_name = a.sect.name if a.sect else "散修" + realm_str = a.cultivation_progress.realm.value if hasattr(a, 'cultivation_progress') else "未知" + + result.append({ + "id": str(a.id), + "name": a.name, + "sect_name": sect_name, + "realm": realm_str, + "gender": a.gender.value, + "age": a.age.age + }) + + # 按名字排序 + result.sort(key=lambda x: x["name"]) + return {"avatars": result} + +@app.post("/api/action/create_avatar") +def create_avatar(req: CreateAvatarRequest): + """创建新角色""" + world = game_instance.get("world") + if not world: + raise HTTPException(status_code=503, detail="World not initialized") + + try: + # 准备参数 + sect = None + if req.sect_id is not None: + sect = sects_by_id.get(req.sect_id) + + personas = None + if req.persona_ids: + personas = req.persona_ids # get_new_avatar_with_config 支持 int 列表 + + have_name = False + final_name = None + surname = (req.surname or "").strip() + given_name = (req.given_name or "").strip() + if surname or given_name: + if surname and given_name: + final_name = f"{surname}{given_name}" + have_name = True + elif surname: + final_name = f"{surname}某" + have_name = True + else: + final_name = given_name + have_name = True + if not have_name: + final_name = None + + # 创建角色 + # 注意:level 如果是境界枚举值对应的等级范围,前端可能传的是 realm index,后端需要转换吗? + # 简单起见,我们假设 level 传的是具体等级 (1-120) 或者 realm index * 30 + 1 + # get_new_avatar_with_config 接收 level (int) + + avatar = get_new_avatar_with_config( + world, + world.month_stamp, + name=final_name, + gender=req.gender, # "男"/"女" + age=req.age, + level=req.level, + sect=sect, + personas=personas, + technique=req.technique_id, + weapon=req.weapon_id, + auxiliary=req.auxiliary_id, + appearance=req.appearance + ) + + if req.pic_id is not None: + gender_key = "females" if getattr(avatar.gender, "value", "male") == "female" else "males" + available_ids = set(AVATAR_ASSETS.get(gender_key, [])) + if available_ids and req.pic_id not in available_ids: + raise HTTPException(status_code=400, detail="Invalid pic_id for selected gender") + avatar.custom_pic_id = req.pic_id + + if req.alignment: + avatar.alignment = Alignment.from_str(req.alignment) + + if req.appearance is not None: + avatar.appearance = get_appearance_by_level(req.appearance) + + # 清空系统自动生成的关系,保持用户自定义角色独立 + existing_relations = list(getattr(avatar, "relations", {}).keys()) + for other in existing_relations: + avatar.clear_relation(other) + + if req.alignment: + avatar.alignment = Alignment.from_str(req.alignment) + + # 注册到管理器 + world.avatar_manager.avatars[avatar.id] = avatar + + return { + "status": "ok", + "message": f"Created avatar {avatar.name}", + "avatar_id": str(avatar.id) + } + except Exception as e: + import traceback + traceback.print_exc() + raise HTTPException(status_code=500, detail=str(e)) + +@app.post("/api/action/delete_avatar") +def delete_avatar(req: DeleteAvatarRequest): + """删除角色""" + world = game_instance.get("world") + if not world: + raise HTTPException(status_code=503, detail="World not initialized") + + if req.avatar_id not in world.avatar_manager.avatars: + raise HTTPException(status_code=404, detail="Avatar not found") + + try: + world.avatar_manager.remove_avatar(req.avatar_id) + return {"status": "ok", "message": "Avatar deleted"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + # --- 存档系统 API --- class SaveGameRequest(BaseModel): diff --git a/src/sim/new_avatar.py b/src/sim/new_avatar.py index 11b2eef..aa9855c 100644 --- a/src/sim/new_avatar.py +++ b/src/sim/new_avatar.py @@ -263,13 +263,6 @@ class MortalPlan: self.pos_x: int = 0 self.pos_y: int = 0 - -def _pick_any_sect(existed_sects: Optional[List[Sect]]) -> Optional[Sect]: - if not existed_sects: - return None - return random.choice(existed_sects) - - def _pick_sects_balanced(existed_sects: List[Sect], k: int) -> list[Optional[Sect]]: """ 从宗门列表中“均衡”挑选 k 个位置的宗门引用: diff --git a/static/config.yml b/static/config.yml index 7029926..41de6c5 100644 --- a/static/config.yml +++ b/static/config.yml @@ -38,18 +38,4 @@ nickname: minor_event_threshold: 20 # 获得绰号需要的短期事件数量 save: - max_events_to_save: 1000 - -# defined_avatar: -# surname: 丰川 -# given_name: 祥子 -# level: 31 -# age: 18 -# gender: 女 -# sect: 百兽宗 -# appearance: 10 -# personas: -# - 穿越者 -# - 随性 -# - 外向 -# treasure: 百兽驭兽符 + max_events_to_save: 1000 \ No newline at end of file diff --git a/web/src/api/game.ts b/web/src/api/game.ts index a6ef572..51afad7 100644 --- a/web/src/api/game.ts +++ b/web/src/api/game.ts @@ -12,6 +12,43 @@ export interface HoverParams { id: string; } +// --- New Types --- + +export interface GameDataDTO { + sects: Array<{ id: number; name: string; alignment: string }>; + personas: Array<{ id: number; name: string; desc: string; rarity: string }>; + realms: string[]; + techniques: Array<{ id: number; name: string; grade: string; attribute: string; sect: string | null }>; + weapons: Array<{ id: number; name: string; grade: string; type: string; sect_id: number | null }>; + auxiliaries: Array<{ id: number; name: string; grade: string; sect_id: number | null }>; + alignments: Array<{ value: string; label: string }>; +} + +export interface SimpleAvatarDTO { + id: string; + name: string; + sect_name: string; + realm: string; + gender: string; + age: number; +} + +export interface CreateAvatarParams { + surname?: string; + given_name?: string; + gender?: string; + age?: number; + level?: number; + sect_id?: number; + persona_ids?: number[]; + pic_id?: number; + technique_id?: number; + weapon_id?: number; + auxiliary_id?: number; + alignment?: string; + appearance?: number; +} + export const gameApi = { // --- World State --- @@ -76,6 +113,23 @@ export const gameApi = { loadGame(filename: string) { return httpClient.post<{ status: string; message: string }>('/api/game/load', { filename }); + }, + + // --- Avatar Management --- + + fetchGameData() { + return httpClient.get('/api/meta/game_data'); + }, + + fetchAvatarList() { + return httpClient.get<{ avatars: SimpleAvatarDTO[] }>('/api/meta/avatar_list'); + }, + + createAvatar(params: CreateAvatarParams) { + return httpClient.post<{ status: string; message: string; avatar_id: string }>('/api/action/create_avatar', params); + }, + + deleteAvatar(avatarId: string) { + return httpClient.post<{ status: string; message: string }>('/api/action/delete_avatar', { avatar_id: avatarId }); } }; - diff --git a/web/src/components/SystemMenu.vue b/web/src/components/SystemMenu.vue index 5bab554..ff20f5d 100644 --- a/web/src/components/SystemMenu.vue +++ b/web/src/components/SystemMenu.vue @@ -1,9 +1,9 @@