From 99e4871a5d5b041ede7dba2d8921ec3c440401bb Mon Sep 17 00:00:00 2001 From: bridge Date: Sat, 22 Nov 2025 15:07:09 +0800 Subject: [PATCH] update web, add clickable parts --- src/classes/animal.py | 10 + src/classes/auxiliary.py | 10 + src/classes/avatar.py | 113 ++++++ src/classes/item.py | 8 + src/classes/persona.py | 10 + src/classes/plant.py | 10 + src/classes/region.py | 30 ++ src/classes/sect.py | 13 + src/classes/sect_region.py | 6 +- src/classes/spirit_animal.py | 9 + src/classes/technique.py | 11 + src/classes/weapon.py | 22 ++ src/server/main.py | 39 +++ src/utils/effect_desc.py | 82 +++++ web/src/components/InfoPanel.vue | 570 ++++++++++++++++++++++++++++++- web/src/services/gameApi.ts | 16 +- web/src/stores/game.ts | 55 ++- web/src/types/game.ts | 84 +++++ 18 files changed, 1085 insertions(+), 13 deletions(-) create mode 100644 src/utils/effect_desc.py diff --git a/src/classes/animal.py b/src/classes/animal.py index c3bde47..b6136d7 100644 --- a/src/classes/animal.py +++ b/src/classes/animal.py @@ -44,6 +44,16 @@ class Animal: return " - ".join(info_parts) + def get_structured_info(self) -> dict: + items_info = [item.get_structured_info() for item in self.items] + return { + "name": self.name, + "desc": self.desc, + "grade": self.realm.value, + "drops": items_info, + "type": "animal" + } + def _load_animals() -> tuple[dict[int, Animal], dict[str, Animal]]: """从配表加载animal数据""" animals_by_id: dict[int, Animal] = {} diff --git a/src/classes/auxiliary.py b/src/classes/auxiliary.py index f796335..46f2f76 100644 --- a/src/classes/auxiliary.py +++ b/src/classes/auxiliary.py @@ -41,6 +41,16 @@ class Auxiliary: r, g, b = self.grade.color_rgb return f"{self.get_info()}" + def get_structured_info(self) -> dict: + from src.utils.effect_desc import format_effects_to_text + return { + "name": self.name, + "desc": self.desc, + "grade": self.grade.value, + "color": self.grade.color_rgb, + "effect_desc": format_effects_to_text(self.effects), + } + def _load_auxiliaries() -> tuple[Dict[int, Auxiliary], Dict[str, Auxiliary], Dict[int, Auxiliary]]: """从配表加载 auxiliary 数据。 diff --git a/src/classes/avatar.py b/src/classes/avatar.py index 074c2d7..1e03944 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -278,6 +278,119 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): info_dict["短期目标"] = self.short_term_objective return info_dict + def get_structured_info(self) -> dict: + """ + 获取结构化的角色信息,用于前端展示和交互。 + """ + # 基础信息 + info = { + "id": self.id, + "name": self.name, + "gender": str(self.gender), + "age": self.age.age, + "lifespan": self.age.max_lifespan, + "realm": self.cultivation_progress.realm.value, + "level": self.cultivation_progress.level, + "hp": {"cur": self.hp.cur, "max": self.hp.max}, + "mp": {"cur": self.mp.cur, "max": self.mp.max}, + "alignment": str(self.alignment) if self.alignment else "未知", + "magic_stone": self.magic_stone.value, + "thinking": self.thinking, + "short_term_objective": self.short_term_objective, + "long_term_objective": self.long_term_objective.content if self.long_term_objective else "", + "nickname": self.nickname, + } + + # 复杂对象结构化 + + # 1. 特质 (Personas) + info["personas"] = [p.get_structured_info() for p in self.personas] + + # 2. 功法 (Technique) + if self.technique: + info["technique"] = self.technique.get_structured_info() + else: + info["technique"] = None + + # 3. 宗门 (Sect) + if self.sect: + sect_info = self.sect.get_structured_info() + # 补充职位信息 + if self.sect_rank: + from src.classes.sect_ranks import get_rank_display_name + sect_info["rank"] = get_rank_display_name(self.sect_rank, self.sect) + else: + sect_info["rank"] = "弟子" + info["sect"] = sect_info + else: + info["sect"] = None + + # 补充:阵营详情 + from src.classes.alignment import alignment_infos, alignment_strs + # 保持 alignment 字段为 string (value) 兼容现有逻辑 + info["alignment"] = str(self.alignment) if self.alignment else "未知" + if self.alignment: + cn_name = alignment_strs.get(self.alignment, self.alignment.value) + desc = alignment_infos.get(self.alignment, "") + info["alignment_detail"] = { + "name": cn_name, + "desc": desc, + } + + # 4. 装备 (Weapon & Auxiliary) + if self.weapon: + w_info = self.weapon.get_structured_info() + w_info["proficiency"] = f"{self.weapon_proficiency:.1f}%" + info["weapon"] = w_info + else: + info["weapon"] = None + + if self.auxiliary: + info["auxiliary"] = self.auxiliary.get_structured_info() + else: + info["auxiliary"] = None + + # 5. 物品 (Items) + items_list = [] + for item, count in self.items.items(): + i_info = item.get_structured_info() + i_info["count"] = count + items_list.append(i_info) + info["items"] = items_list + + # 6. 关系 (Relations) + relations_list = [] + for other, relation in self.relations.items(): + relations_list.append({ + "target_id": other.id, + "name": other.name, + "relation": str(relation), + # 可以加更多 info,比如境界,用于列表中展示 + "realm": other.cultivation_progress.realm.value, + "sect": other.sect.name if other.sect else "散修" + }) + info["relations"] = relations_list + + # 7. 外貌 + info["appearance"] = self.appearance.get_info() + + # 8. 灵根 + from src.classes.root import format_root_cn + from src.utils.effect_desc import format_effects_to_text + root_str = format_root_cn(self.root) + info["root"] = root_str + info["root_detail"] = { + "name": root_str, + "desc": f"包含元素:{'、'.join(str(e) for e in self.root.elements)}", + "effect_desc": format_effects_to_text(self.root.effects) + } + + # 9. 灵兽 + if self.spirit_animal: + info["spirit_animal"] = self.spirit_animal.get_structured_info() + + return info + def __str__(self) -> str: return str(self.get_info(detailed=False)) diff --git a/src/classes/item.py b/src/classes/item.py index 4762e41..7a128e2 100644 --- a/src/classes/item.py +++ b/src/classes/item.py @@ -25,6 +25,14 @@ class Item: def get_detailed_info(self) -> str: return f"{self.name} - {self.desc}({self.realm.value})" + def get_structured_info(self) -> dict: + return { + "name": self.name, + "desc": self.desc, + "grade": self.realm.value, + "effect_desc": "" # 物品暂时没有效果字段 + } + def _load_items() -> tuple[dict[int, Item], dict[str, Item]]: """从配表加载item数据""" items_by_id: dict[int, Item] = {} diff --git a/src/classes/persona.py b/src/classes/persona.py index 8dd4af1..b136d8a 100644 --- a/src/classes/persona.py +++ b/src/classes/persona.py @@ -41,6 +41,16 @@ class Persona: r, g, b = self.rarity.color_rgb return f"{self.name}" + def get_structured_info(self) -> dict: + from src.utils.effect_desc import format_effects_to_text + return { + "name": self.name, + "desc": self.desc, + "rarity": self.rarity.level.value, + "color": self.rarity.color_rgb, + "effect_desc": format_effects_to_text(self.effects), + } + def _load_personas() -> tuple[dict[int, Persona], dict[str, Persona]]: """从配表加载persona数据""" personas_by_id: dict[int, Persona] = {} diff --git a/src/classes/plant.py b/src/classes/plant.py index 929a573..a2895b9 100644 --- a/src/classes/plant.py +++ b/src/classes/plant.py @@ -44,6 +44,16 @@ class Plant: return " - ".join(info_parts) + def get_structured_info(self) -> dict: + items_info = [item.get_structured_info() for item in self.items] + return { + "name": self.name, + "desc": self.desc, + "grade": self.realm.value, + "drops": items_info, + "type": "plant" + } + def _load_plants() -> tuple[dict[int, Plant], dict[str, Plant]]: """从配表加载plant数据""" plants_by_id: dict[int, Plant] = {} diff --git a/src/classes/region.py b/src/classes/region.py index 175d062..f754dd2 100644 --- a/src/classes/region.py +++ b/src/classes/region.py @@ -177,6 +177,15 @@ class Region(ABC): # 基类暂无更多结构化信息,详细版返回名称+描述 return f"{self.name} - {self.desc}" + def get_structured_info(self) -> dict: + return { + "id": self.id, + "name": self.name, + "desc": self.desc, + "type": self.get_region_type(), + "type_name": "区域" + } + class Shape(Enum): """ @@ -301,6 +310,13 @@ class NormalRegion(Region): # 如果该区域有植物,则可以采集 return len(self.plants) > 0 + def get_structured_info(self) -> dict: + info = super().get_structured_info() + info["type_name"] = "普通区域" + info["animals"] = [a.get_structured_info() for a in self.animals] + info["plants"] = [p.get_structured_info() for p in self.plants] + return info + @dataclass class CultivateRegion(Region): @@ -338,6 +354,15 @@ class CultivateRegion(Region): lines.append(f"主要灵气: {self.essence_type} {stars}") return lines + def get_structured_info(self) -> dict: + info = super().get_structured_info() + info["type_name"] = "修炼区域" + info["essence"] = { + "type": self.essence_type.value, + "density": self.essence_density + } + return info + @dataclass class CityRegion(Region): @@ -361,6 +386,11 @@ class CityRegion(Region): def get_detailed_info(self) -> str: return f"{self.name} - {self.desc}" + def get_structured_info(self) -> dict: + info = super().get_structured_info() + info["type_name"] = "城市区域" + return info + def _normalize_region_name(name: str) -> str: """ diff --git a/src/classes/sect.py b/src/classes/sect.py index f3c50e9..7d11ef7 100644 --- a/src/classes/sect.py +++ b/src/classes/sect.py @@ -69,6 +69,19 @@ class Sect: # 优先使用自定义名称,否则使用默认名称 return self.rank_names.get(rank.value, DEFAULT_RANK_NAMES.get(rank, "弟子")) + def get_structured_info(self) -> dict: + from src.utils.effect_desc import format_effects_to_text + hq = self.headquarter + return { + "name": self.name, + "desc": self.desc, + "alignment": self.alignment.value, + "style": self.member_act_style, + "hq_name": hq.name, + "hq_desc": hq.desc, + "effect_desc": format_effects_to_text(self.effects), + } + def _split_names(value: object) -> list[str]: raw = "" if value is None or str(value) == "nan" else str(value) sep = CONFIG.df.ids_separator diff --git a/src/classes/sect_region.py b/src/classes/sect_region.py index 73f5d7c..25d6135 100644 --- a/src/classes/sect_region.py +++ b/src/classes/sect_region.py @@ -24,4 +24,8 @@ class SectRegion(Region): f"描述: {self.desc}", ] - + def get_structured_info(self) -> dict: + info = super().get_structured_info() + info["type_name"] = "宗门驻地" + info["sect_name"] = self.sect_name + return info diff --git a/src/classes/spirit_animal.py b/src/classes/spirit_animal.py index 7c4cad5..e2e44a7 100644 --- a/src/classes/spirit_animal.py +++ b/src/classes/spirit_animal.py @@ -39,4 +39,13 @@ class SpiritAnimal: pts = self.get_extra_strength_points() return {"extra_battle_strength_points": pts} if pts else {} + def get_structured_info(self) -> dict: + from src.utils.effect_desc import format_effects_to_text + return { + "name": self.name, + "desc": f"境界:{self.realm.value}", + "grade": self.realm.value, + "effect_desc": format_effects_to_text(self.effects), + } + diff --git a/src/classes/technique.py b/src/classes/technique.py index 8f81a38..96bd807 100644 --- a/src/classes/technique.py +++ b/src/classes/technique.py @@ -83,6 +83,17 @@ class Technique: r, g, b = self.grade.color_rgb return f"{self.name}({self.attribute}·{self.grade.value})" + def get_structured_info(self) -> dict: + from src.utils.effect_desc import format_effects_to_text + return { + "name": self.name, + "desc": self.desc, + "grade": self.grade.value, + "color": self.grade.color_rgb, + "attribute": self.attribute.value, + "effect_desc": format_effects_to_text(self.effects), + } + # 五行与扩展属性的克制关系 # - 五行:金克木,木克土,土克水,水克火,火克金 # - 雷克邪;邪、冰、风、暗不克任何人 diff --git a/src/classes/weapon.py b/src/classes/weapon.py index 1b3631b..c864fca 100644 --- a/src/classes/weapon.py +++ b/src/classes/weapon.py @@ -51,6 +51,28 @@ class Weapon: r, g, b = self.grade.color_rgb return f"{self.get_info()}" + def get_structured_info(self) -> dict: + from src.utils.effect_desc import format_effects_to_text + + # 基础描述 + full_desc = self.desc + + # 特殊数据处理 + souls = 0 + if self.name == "万魂幡": + souls = self.special_data.get("devoured_souls", 0) + if souls > 0: + full_desc = f"{full_desc} (已吞噬魂魄:{souls})" + + return { + "name": self.name, + "desc": full_desc, + "grade": self.grade.value, + "color": self.grade.color_rgb, + "type": self.weapon_type.value, + "effect_desc": format_effects_to_text(self.effects), + } + def _load_weapons() -> tuple[Dict[int, Weapon], Dict[str, Weapon], Dict[int, Weapon]]: """从配表加载 weapon 数据。 diff --git a/src/server/main.py b/src/server/main.py index 3607098..82cd20e 100644 --- a/src/server/main.py +++ b/src/server/main.py @@ -475,6 +475,45 @@ def get_hover_info( "lines": serialize_hover_lines([str(line) for line in lines]), } +@app.get("/api/detail") +def get_detail_info( + target_type: str = Query(alias="type"), + target_id: str = Query(alias="id") +): + """获取结构化详情信息,替代/增强 hover info""" + world = game_instance.get("world") + if world is None: + raise HTTPException(status_code=503, detail="World not initialized") + + target = None + if target_type == "avatar": + target = world.avatar_manager.avatars.get(target_id) + elif target_type == "region": + if world.map and hasattr(world.map, "regions"): + regions = world.map.regions + target = regions.get(target_id) + if target is None: + try: + target = regions.get(int(target_id)) + except (ValueError, TypeError): + target = None + + if target is None: + raise HTTPException(status_code=404, detail="Target not found") + + if hasattr(target, "get_structured_info"): + return target.get_structured_info() + else: + # 回退到 hover info 如果没有结构化信息 + if hasattr(target, "get_hover_info"): + lines = target.get_hover_info() or [] + return { + "fallback": True, + "name": getattr(target, "name", target_id), + "lines": serialize_hover_lines([str(line) for line in lines]) + } + return {"error": "No info available"} + class SetObjectiveRequest(BaseModel): avatar_id: str content: str diff --git a/src/utils/effect_desc.py b/src/utils/effect_desc.py new file mode 100644 index 0000000..4fc04e9 --- /dev/null +++ b/src/utils/effect_desc.py @@ -0,0 +1,82 @@ +from typing import Any + +EFFECT_DESC_MAP = { + "extra_hp_recovery_rate": "生命恢复速率", + "extra_max_hp": "最大生命值", + "extra_max_mp": "最大灵力值", + "extra_max_lifespan": "最大寿元", + "extra_weapon_proficiency_gain": "兵器熟练度获取", + "extra_dual_cultivation_exp": "双修经验", + "extra_breakthrough_success_rate": "突破成功率", + "extra_fortune_probability": "奇遇概率", + "extra_harvest_items": "采集获取物品", + "extra_hunt_items": "狩猎获取物品", + "extra_item_sell_price_multiplier": "物品出售价格", + "extra_weapon_upgrade_chance": "兵器升级概率", + "extra_plunder_multiplier": "掠夺收益", + "extra_catch_success_rate": "捕捉灵兽成功率", + "extra_cultivate_exp": "修炼经验", + "extra_battle_strength_points": "战力点数", + "extra_escape_success_rate": "逃跑成功率", + "extra_observation_radius": "感知范围", + "extra_move_step": "移动步长", +} + +def format_value(key: str, value: Any) -> str: + """ + 格式化效果数值 + """ + if isinstance(value, (int, float)): + # 处理百分比类型的字段 + if "rate" in key or "probability" in key or "chance" in key or "multiplier" in key or "gain" in key: + # 如果是小数,转为百分比。通常 0.1 表示 +10% + # 但有些可能是直接的倍率?代码里 1.0 + value,所以 value 是增量 + if isinstance(value, float): + percent = value * 100 + sign = "+" if percent > 0 else "" + return f"{sign}{percent:.1f}%" + + # 处理数值类型的字段 + sign = "+" if value > 0 else "" + return f"{sign}{value}" + + return str(value) + +def format_effects_to_text(effects: dict[str, Any] | list[dict[str, Any]]) -> str: + """ + 将 effects 字典转换为易读的文本描述。 + 例如:{"extra_max_hp": 100} -> "最大生命值 +100" + """ + if not effects: + return "" + + if isinstance(effects, list): + parts = [] + for eff in effects: + text = format_effects_to_text(eff) + if text: + if eff.get("when"): + parts.append(f"[条件触发] {text}") + else: + parts.append(text) + return "\n".join(parts) + + desc_list = [] + for k, v in effects.items(): + if k == "when": + continue + + # 跳过 eval 表达式或者无法解析的 key,或者直接显示 key + name = EFFECT_DESC_MAP.get(k, k) + + # 如果是 eval 表达式(字符串形式) + if isinstance(v, str) and v.startswith("eval("): + # 尝试提取简单的描述,或者显示"特殊效果" + val_str = "特殊效果" + else: + val_str = format_value(k, v) + + desc_list.append(f"{name} {val_str}") + + return ";".join(desc_list) + diff --git a/web/src/components/InfoPanel.vue b/web/src/components/InfoPanel.vue index 7dca2a4..7cf4579 100644 --- a/web/src/components/InfoPanel.vue +++ b/web/src/components/InfoPanel.vue @@ -1,6 +1,7 @@