diff --git a/src/classes/action/devour_mortals.py b/src/classes/action/devour_mortals.py index 16aabb9..eb26dc4 100644 --- a/src/classes/action/devour_mortals.py +++ b/src/classes/action/devour_mortals.py @@ -18,10 +18,11 @@ class DevourMortals(TimedAction): def _execute(self) -> None: # 若持有万魂幡:累积吞噬魂魄(10~100),上限10000 - tr = getattr(self.avatar, "treasure", None) - if tr is not None and tr.name == "万魂幡": + weapon = self.avatar.weapon + if weapon is not None and weapon.name == "万魂幡": gain = random.randint(10, 100) - tr.devoured_souls = min(10000, int(tr.devoured_souls) + gain) + current_souls = weapon.special_data.get("devoured_souls", 0) + weapon.special_data["devoured_souls"] = min(10000, int(current_souls) + gain) def can_start(self) -> tuple[bool, str]: legal = self.avatar.effects.get("legal_actions", []) diff --git a/src/classes/auxiliary.py b/src/classes/auxiliary.py new file mode 100644 index 0000000..bea1d72 --- /dev/null +++ b/src/classes/auxiliary.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Optional, Dict + +from src.utils.df import game_configs +from src.classes.effect import load_effect_from_str +from src.classes.equipment_grade import EquipmentGrade +from src.classes.sect import Sect, sects_by_id + + +@dataclass +class Auxiliary: + """ + 辅助装备类:提供各种辅助功能的装备 + 字段与 static/game_configs/auxiliary.csv 对应: + - grade: 装备等级(普通、宝物、法宝) + - sect_id: 对应宗门ID(见 sect.csv);允许为空表示无特定宗门归属 + - effects: 解析为 dict,用于与 Avatar.effects 合并 + """ + id: int + name: str + grade: EquipmentGrade + sect_id: Optional[int] + desc: str + effects: dict[str, object] = field(default_factory=dict) + sect: Optional[Sect] = None + + def get_info(self) -> str: + """获取简略信息""" + return f"{self.name}" + + def get_detailed_info(self) -> str: + """获取详细信息""" + return f"{self.name}({self.grade},{self.desc})" + + +def _load_auxiliaries() -> tuple[Dict[int, Auxiliary], Dict[str, Auxiliary], Dict[int, Auxiliary]]: + """从配表加载 auxiliary 数据。 + 返回:(按ID、按名称、按宗门ID 的映射)。 + 若同一宗门配置多个辅助装备,按首次出现保留(每门至多一个法宝级)。 + """ + auxiliaries_by_id: Dict[int, Auxiliary] = {} + auxiliaries_by_name: Dict[str, Auxiliary] = {} + auxiliaries_by_sect_id: Dict[int, Auxiliary] = {} + + df = game_configs.get("auxiliary") + if df is None: + return auxiliaries_by_id, auxiliaries_by_name, auxiliaries_by_sect_id + + for _, row in df.iterrows(): + raw_sect = row.get("sect_id") + sect_id: Optional[int] = None + if raw_sect is not None and str(raw_sect).strip() and str(raw_sect).strip() != "nan": + sect_id = int(float(raw_sect)) + + raw_effects_val = row.get("effects", "") + effects = load_effect_from_str(raw_effects_val) + + sect_obj: Optional[Sect] = sects_by_id.get(int(sect_id)) if sect_id is not None else None + + # 解析grade + grade_str = str(row.get("grade", "普通")) + grade = EquipmentGrade.COMMON + for g in EquipmentGrade: + if g.value == grade_str: + grade = g + break + + a = Auxiliary( + id=int(row["id"]), + name=str(row["name"]), + grade=grade, + sect_id=sect_id, + desc=str(row.get("desc", "")), + effects=effects, + sect=sect_obj, + ) + + auxiliaries_by_id[a.id] = a + auxiliaries_by_name[a.name] = a + if a.sect_id is not None and a.sect_id not in auxiliaries_by_sect_id: + auxiliaries_by_sect_id[a.sect_id] = a + + return auxiliaries_by_id, auxiliaries_by_name, auxiliaries_by_sect_id + + +auxiliaries_by_id, auxiliaries_by_name, auxiliaries_by_sect_id = _load_auxiliaries() + diff --git a/src/classes/avatar.py b/src/classes/avatar.py index fdd90ab..d9bf17a 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -28,7 +28,10 @@ from src.classes.effect import _merge_effects from src.classes.alignment import Alignment from src.classes.persona import Persona, personas_by_id, get_random_compatible_personas from src.classes.item import Item -from src.classes.treasure import Treasure +from src.classes.weapon import Weapon, get_common_weapon +from src.classes.auxiliary import Auxiliary +from src.classes.weapon_type import WeaponType +from src.classes.equipment_grade import EquipmentGrade from src.classes.magic_stone import MagicStone from src.classes.hp_and_mp import HP, MP, HP_MAX_BY_REALM, MP_MAX_BY_REALM from src.utils.id_generator import get_avatar_id @@ -97,8 +100,10 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): sect_rank: "SectRank | None" = None # 外貌(1~10级),创建时随机生成 appearance: Appearance = field(default_factory=get_random_appearance) - # 装备的法宝(仅一个) - treasure: Optional[Treasure] = None + # 兵器(必有,无则分配普通兵器) + weapon: Optional[Weapon] = None + # 辅助装备(可选) + auxiliary: Optional[Auxiliary] = None # 灵兽:最多一个;若再次捕捉则覆盖 spirit_animal: Optional[SpiritAnimal] = None # 当月/当步新设动作标记:在 commit_next_plan 设为 True,首次 tick_action 后清为 False @@ -139,6 +144,11 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): from src.classes.alignment import Alignment as _Alignment self.alignment = random.choice(list(_Alignment)) + # 兵器初始化:如果无兵器,分配一个随机的普通兵器 + if self.weapon is None: + weapon_type = random.choice(list(WeaponType)) + self.weapon = get_common_weapon(weapon_type) + # effects 改为实时属性,不在此初始化 @property @@ -154,9 +164,12 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): # 来自特质(persona) for persona in self.personas: merged = _merge_effects(merged, persona.effects) - # 来自法宝 - if self.treasure is not None: - merged = _merge_effects(merged, self.treasure.effects) + # 来自兵器 + if self.weapon is not None: + merged = _merge_effects(merged, self.weapon.effects) + # 来自辅助装备 + if self.auxiliary is not None: + merged = _merge_effects(merged, self.auxiliary.effects) # 来自灵兽 if self.spirit_animal is not None: merged = _merge_effects(merged, self.spirit_animal.effects) @@ -189,7 +202,8 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): from src.classes.sect import get_sect_info_with_rank if detailed: - treasure_info = self.treasure.get_detailed_info() if self.treasure is not None else "无" + weapon_info = self.weapon.get_detailed_info() if self.weapon is not None else "无" + auxiliary_info = self.auxiliary.get_detailed_info() if self.auxiliary is not None else "无" sect_info = get_sect_info_with_rank(self, detailed=True) alignment_info = self.alignment.get_detailed_info() if self.alignment is not None else "未知" region_info = region.get_detailed_info() if region is not None else "无" @@ -201,7 +215,8 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): appearance_info = self.appearance.get_detailed_info(self.gender) spirit_animal_info = self.spirit_animal.get_info() if self.spirit_animal is not None else "无" else: - treasure_info = self.treasure.get_info() if self.treasure is not None else "无" + weapon_info = self.weapon.get_info() if self.weapon is not None else "无" + auxiliary_info = self.auxiliary.get_info() if self.auxiliary is not None else "无" # 宗门信息:非详细模式下只显示"宗门名+职位" sect_info = get_sect_info_with_rank(self, detailed=False) region_info = region.get_info() if region is not None else "无" @@ -232,7 +247,8 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): "特质": personas_info, "物品": items_info, "外貌": appearance_info, - "法宝": treasure_info, + "兵器": weapon_info, + "辅助装备": auxiliary_info, } # 灵兽:仅在存在时显示 if self.spirit_animal is not None: @@ -588,11 +604,19 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): from src.utils.text_wrap import wrap_text add_section(lines, "目标", wrap_text(self.objective, 28)) - # 法宝(仅名字) - if self.treasure is not None: - add_section(lines, "法宝", [self.treasure.get_info()]) + # 兵器(必有,使用颜色标记等级) + if self.weapon is not None: + r, g, b = self.weapon.grade.color_rgb + weapon_text = f"{self.weapon.get_info()}" + add_kv(lines, "兵器", weapon_text) + + # 辅助装备(可选,使用颜色标记等级) + if self.auxiliary is not None: + r, g, b = self.auxiliary.grade.color_rgb + auxiliary_text = f"{self.auxiliary.get_info()}" + add_kv(lines, "辅助装备", auxiliary_text) else: - add_kv(lines, "法宝", "无") + add_kv(lines, "辅助装备", "无") # 灵兽:仅在存在时显示 if self.spirit_animal is not None: @@ -672,13 +696,14 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): def get_other_avatar_info(self, other_avatar: "Avatar") -> str: """ - 仅显示几个字段:名字、境界、关系、宗门、阵营、外貌。 + 仅显示几个字段:名字、境界、关系、宗门、阵营、外貌、装备。 """ relation = self.get_relation(other_avatar) relation_str = str(relation) sect_str = other_avatar.sect.name if other_avatar.sect is not None else "散修" - tr_str = other_avatar.treasure.get_info() if other_avatar.treasure is not None else "无" - return f"{other_avatar.name},境界:{other_avatar.cultivation_progress.get_info()},关系:{relation_str},阵营:{other_avatar.alignment},宗门:{sect_str},法宝:{tr_str},外貌:{other_avatar.appearance.get_info()}" + weapon_str = other_avatar.weapon.get_info() if other_avatar.weapon is not None else "无" + auxiliary_str = other_avatar.auxiliary.get_info() if other_avatar.auxiliary is not None else "无" + return f"{other_avatar.name},境界:{other_avatar.cultivation_progress.get_info()},关系:{relation_str},阵营:{other_avatar.alignment},宗门:{sect_str},兵器:{weapon_str},辅助:{auxiliary_str},外貌:{other_avatar.appearance.get_info()}" def update_time_effect(self) -> None: """ diff --git a/src/classes/equipment_grade.py b/src/classes/equipment_grade.py new file mode 100644 index 0000000..924e54a --- /dev/null +++ b/src/classes/equipment_grade.py @@ -0,0 +1,27 @@ +from enum import Enum + + +class EquipmentGrade(Enum): + """ + 装备等级枚举 + """ + COMMON = "普通" # 无限复制,作为兜底 + TREASURE = "宝物" # 可有多个,无数量限制 + ARTIFACT = "法宝" # 全世界唯一 + + def __str__(self) -> str: + return self.value + + @property + def color_rgb(self) -> tuple[int, int, int]: + """返回装备等级对应的RGB颜色值""" + return _grade_colors.get(self, (200, 200, 200)) + + +# 装备等级颜色映射 +_grade_colors = { + EquipmentGrade.COMMON: (150, 150, 150), # 灰色 + EquipmentGrade.TREASURE: (138, 43, 226), # 紫色 + EquipmentGrade.ARTIFACT: (255, 215, 0), # 金色 +} + diff --git a/src/classes/fortune.py b/src/classes/fortune.py index dd7d733..5a1862a 100644 --- a/src/classes/fortune.py +++ b/src/classes/fortune.py @@ -17,21 +17,32 @@ from src.classes.technique import ( is_attribute_compatible_with_root, TechniqueAttribute, ) -from src.classes.treasure import Treasure, treasures_by_id +from src.classes.weapon import Weapon, weapons_by_id +from src.classes.auxiliary import Auxiliary, auxiliaries_by_id +from src.classes.equipment_grade import EquipmentGrade from src.classes.relation import Relation from src.classes.alignment import Alignment class FortuneKind(Enum): """奇遇类型""" - TREASURE = "treasure" + WEAPON = "weapon" # 兵器奇遇 + AUXILIARY = "auxiliary" # 辅助装备奇遇 TECHNIQUE = "technique" FIND_MASTER = "find_master" - SPIRIT_STONE = "spirit_stone" # 灵石奇遇 + SPIRIT_STONE = "spirit_stone" # 灵石奇遇 CULTIVATION = "cultivation" # 修为奇遇 -F_TREASURE_THEMES: list[str] = [ +F_WEAPON_THEMES: list[str] = [ + "误入洞府", + "巧捡神兵", + "误入试炼", + "异象出世", + "高人赠予", +] + +F_AUXILIARY_THEMES: list[str] = [ "误入洞府", "巧捡奇物", "误入试炼", @@ -138,9 +149,18 @@ def _find_potential_master(avatar: Avatar) -> Optional[Avatar]: return None -def _can_get_treasure(avatar: Avatar) -> bool: - """检查是否可以获得法宝奇遇""" - return avatar.treasure is None +def _can_get_weapon(avatar: Avatar) -> bool: + """检查是否可以获得兵器奇遇:当前兵器是普通级时可触发""" + if avatar.weapon is None: + return True + return avatar.weapon.grade == EquipmentGrade.COMMON + + +def _can_get_auxiliary(avatar: Avatar) -> bool: + """检查是否可以获得辅助装备奇遇:无辅助装备或辅助装备非法宝级时可触发""" + if avatar.auxiliary is None: + return True + return avatar.auxiliary.grade != EquipmentGrade.ARTIFACT def _can_get_technique(avatar: Avatar) -> bool: @@ -179,9 +199,13 @@ def _choose_kind(avatar: Avatar) -> FortuneKind: """ possible_kinds: list[FortuneKind] = [] - # 法宝奇遇:任何人无法宝都可以 - if _can_get_treasure(avatar): - possible_kinds.append(FortuneKind.TREASURE) + # 兵器奇遇:当前兵器是普通级时可触发 + if _can_get_weapon(avatar): + possible_kinds.append(FortuneKind.WEAPON) + + # 辅助装备奇遇:无辅助装备或辅助装备非法宝级时可触发 + if _can_get_auxiliary(avatar): + possible_kinds.append(FortuneKind.AUXILIARY) # 功法奇遇:任何人功法非上品都可以(实际获得时会有限制) if _can_get_technique(avatar): @@ -206,8 +230,10 @@ def _choose_kind(avatar: Avatar) -> FortuneKind: def _pick_theme(kind: FortuneKind) -> str: - if kind == FortuneKind.TREASURE: - return random.choice(F_TREASURE_THEMES) + if kind == FortuneKind.WEAPON: + return random.choice(F_WEAPON_THEMES) + elif kind == FortuneKind.AUXILIARY: + return random.choice(F_AUXILIARY_THEMES) elif kind == FortuneKind.TECHNIQUE: return random.choice(F_TECHNIQUE_THEMES) elif kind == FortuneKind.FIND_MASTER: @@ -219,16 +245,58 @@ def _pick_theme(kind: FortuneKind) -> str: return "" -def _get_unique_treasure_for_world(avatar: Avatar) -> Optional[Treasure]: - """获取世界唯一法宝:从全量里挑选一个未被任何人持有的""" - owned_ids: set[int] = set() +def _get_weapon_for_avatar(avatar: Avatar) -> Optional[Weapon]: + """ + 获取兵器:优先法宝 > 宝物 > 普通 + - 法宝:检查世界唯一性 + - 宝物:可重复 + - 普通:可重复 + """ + # 尝试获取法宝级兵器 + owned_artifact_ids: set[int] = set() for other in avatar.world.avatar_manager.avatars.values(): - if other.treasure is not None: - owned_ids.add(other.treasure.id) - candidates = [t for t in treasures_by_id.values() if t.id not in owned_ids] - if not candidates: - return None - return random.choice(candidates) + if other.weapon is not None and other.weapon.grade == EquipmentGrade.ARTIFACT: + owned_artifact_ids.add(other.weapon.id) + + artifact_candidates = [w for w in weapons_by_id.values() + if w.grade == EquipmentGrade.ARTIFACT and w.id not in owned_artifact_ids] + if artifact_candidates: + return random.choice(artifact_candidates) + + # 尝试获取宝物级兵器 + treasure_candidates = [w for w in weapons_by_id.values() + if w.grade == EquipmentGrade.TREASURE] + if treasure_candidates: + return random.choice(treasure_candidates) + + # 回退到普通级兵器(理论上不应该走到这里,因为普通级兵器不应该通过奇遇获得) + return None + + +def _get_auxiliary_for_avatar(avatar: Avatar) -> Optional[Auxiliary]: + """ + 获取辅助装备:优先法宝 > 宝物 + - 法宝:检查世界唯一性 + - 宝物:可重复 + """ + # 尝试获取法宝级辅助装备 + owned_artifact_ids: set[int] = set() + for other in avatar.world.avatar_manager.avatars.values(): + if other.auxiliary is not None and other.auxiliary.grade == EquipmentGrade.ARTIFACT: + owned_artifact_ids.add(other.auxiliary.id) + + artifact_candidates = [a for a in auxiliaries_by_id.values() + if a.grade == EquipmentGrade.ARTIFACT and a.id not in owned_artifact_ids] + if artifact_candidates: + return random.choice(artifact_candidates) + + # 尝试获取宝物级辅助装备 + treasure_candidates = [a for a in auxiliaries_by_id.values() + if a.grade == EquipmentGrade.TREASURE] + if treasure_candidates: + return random.choice(treasure_candidates) + + return None def _get_fortune_technique_for_avatar(avatar: Avatar) -> Optional[Technique]: @@ -319,13 +387,15 @@ async def try_trigger_fortune(avatar: Avatar) -> list[Event]: 规则: - 奇遇不是一个 action;仅在条件满足时以概率触发。 - 触发条件: - * 法宝奇遇:无法宝(不限散修/宗门) + * 兵器奇遇:当前兵器是普通级 + * 辅助装备奇遇:无辅助装备或辅助装备非法宝级 * 功法奇遇:功法非上品(不限散修/宗门,但宗门弟子只能获得本宗门或无宗门功法) * 拜师奇遇:无师傅且世界中有合适的师傅(优先同宗门,不能拜敌对阵营) * 灵石奇遇:任何人都可以触发 * 修为奇遇:未达到瓶颈的人可以触发 - 结果: - * 法宝:世界唯一且不可重复 + * 兵器:优先法宝(世界唯一)> 宝物(可重复) + * 辅助装备:优先法宝(世界唯一)> 宝物(可重复) * 功法:可重复,优先上品,需与灵根兼容,宗门弟子受宗门限制 * 拜师:建立师徒关系 * 灵石:根据境界获得灵石(相当于一年狩猎售卖收入) @@ -351,15 +421,25 @@ async def try_trigger_fortune(avatar: Avatar) -> list[Event]: related_avatars = [avatar.id] actors_for_story = [avatar] # 用于生成故事的角色列表 - if kind == FortuneKind.TREASURE: - tr = _get_unique_treasure_for_world(avatar) - if tr is None: + if kind == FortuneKind.WEAPON: + weapon = _get_weapon_for_avatar(avatar) + if weapon is None: # 回退到功法 kind = FortuneKind.TECHNIQUE theme = _pick_theme(kind) else: - avatar.treasure = tr - res_text = f"{avatar.name} 获得法宝『{tr.name}』" + avatar.weapon = weapon + res_text = f"{avatar.name} 获得{weapon.grade}兵器『{weapon.name}』" + + if kind == FortuneKind.AUXILIARY: + auxiliary = _get_auxiliary_for_avatar(avatar) + if auxiliary is None: + # 回退到功法 + kind = FortuneKind.TECHNIQUE + theme = _pick_theme(kind) + else: + avatar.auxiliary = auxiliary + res_text = f"{avatar.name} 获得{auxiliary.grade}辅助装备『{auxiliary.name}』" if kind == FortuneKind.TECHNIQUE: tech = _get_fortune_technique_for_avatar(avatar) diff --git a/src/classes/weapon.py b/src/classes/weapon.py new file mode 100644 index 0000000..c76e077 --- /dev/null +++ b/src/classes/weapon.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Optional, Dict + +from src.utils.df import game_configs +from src.classes.effect import load_effect_from_str +from src.classes.equipment_grade import EquipmentGrade +from src.classes.weapon_type import WeaponType +from src.classes.sect import Sect, sects_by_id + + +@dataclass +class Weapon: + """ + 兵器类:用于战斗的装备 + 字段与 static/game_configs/weapon.csv 对应: + - weapon_type: 兵器类型(剑、刀、枪等) + - grade: 装备等级(普通、宝物、法宝) + - sect_id: 对应宗门ID(见 sect.csv);允许为空表示无特定宗门归属 + - effects: 解析为 dict,用于与 Avatar.effects 合并 + """ + id: int + name: str + weapon_type: WeaponType + grade: EquipmentGrade + sect_id: Optional[int] + desc: str + effects: dict[str, object] = field(default_factory=dict) + sect: Optional[Sect] = None + # 特殊属性(如万魂幡的吞噬魂魄计数) + special_data: dict = field(default_factory=dict) + + def get_info(self) -> str: + """获取简略信息""" + suffix = "" + # 万魂幡特殊显示 + if self.name == "万魂幡" and self.special_data.get("devoured_souls", 0) > 0: + suffix = f"(吞噬魂魄:{self.special_data['devoured_souls']})" + return f"{self.name}{suffix}" + + def get_detailed_info(self) -> str: + """获取详细信息""" + souls = "" + if self.name == "万魂幡" and self.special_data.get("devoured_souls", 0) > 0: + souls = f" 吞噬魂魄:{self.special_data['devoured_souls']}" + return f"{self.name}({self.weapon_type}·{self.grade},{self.desc}){souls}" + + +def _load_weapons() -> tuple[Dict[int, Weapon], Dict[str, Weapon], Dict[int, Weapon]]: + """从配表加载 weapon 数据。 + 返回:(按ID、按名称、按宗门ID 的映射)。 + 若同一宗门配置多个兵器,按首次出现保留(每门至多一个法宝级)。 + """ + weapons_by_id: Dict[int, Weapon] = {} + weapons_by_name: Dict[str, Weapon] = {} + weapons_by_sect_id: Dict[int, Weapon] = {} + + df = game_configs.get("weapon") + if df is None: + return weapons_by_id, weapons_by_name, weapons_by_sect_id + + for _, row in df.iterrows(): + raw_sect = row.get("sect_id") + sect_id: Optional[int] = None + if raw_sect is not None and str(raw_sect).strip() and str(raw_sect).strip() != "nan": + sect_id = int(float(raw_sect)) + + raw_effects_val = row.get("effects", "") + effects = load_effect_from_str(raw_effects_val) + + sect_obj: Optional[Sect] = sects_by_id.get(int(sect_id)) if sect_id is not None else None + + # 解析weapon_type + weapon_type_str = str(row.get("weapon_type", "其他")) + weapon_type = WeaponType.OTHER + for wt in WeaponType: + if wt.value == weapon_type_str: + weapon_type = wt + break + + # 解析grade + grade_str = str(row.get("grade", "普通")) + grade = EquipmentGrade.COMMON + for g in EquipmentGrade: + if g.value == grade_str: + grade = g + break + + w = Weapon( + id=int(row["id"]), + name=str(row["name"]), + weapon_type=weapon_type, + grade=grade, + sect_id=sect_id, + desc=str(row.get("desc", "")), + effects=effects, + sect=sect_obj, + ) + + weapons_by_id[w.id] = w + weapons_by_name[w.name] = w + if w.sect_id is not None and w.sect_id not in weapons_by_sect_id: + weapons_by_sect_id[w.sect_id] = w + + return weapons_by_id, weapons_by_name, weapons_by_sect_id + + +weapons_by_id, weapons_by_name, weapons_by_sect_id = _load_weapons() + + +def get_common_weapon(weapon_type: WeaponType) -> Optional[Weapon]: + """获取指定类型的普通兵器(用于兜底)""" + weapon_name = f"普通{weapon_type.value}" + return weapons_by_name.get(weapon_name) + diff --git a/src/classes/weapon_type.py b/src/classes/weapon_type.py new file mode 100644 index 0000000..3c84cab --- /dev/null +++ b/src/classes/weapon_type.py @@ -0,0 +1,19 @@ +from enum import Enum + + +class WeaponType(Enum): + """ + 兵器类型枚举 + """ + SWORD = "剑" # 包括剑匣等 + SABER = "刀" + SPEAR = "枪" # 包括矛、戟 + STAFF = "棍" # 包括杖、棒 + FAN = "扇" + WHIP = "鞭" + ZITHER = "琴" # 音律武器 + FLUTE = "笛" # 包括箫 + + def __str__(self) -> str: + return self.value + diff --git a/src/sim/load/avatar_load_mixin.py b/src/sim/load/avatar_load_mixin.py index 9b25c7d..ee9c76e 100644 --- a/src/sim/load/avatar_load_mixin.py +++ b/src/sim/load/avatar_load_mixin.py @@ -6,7 +6,7 @@ Avatar读档反序列化Mixin 读档策略: - 两阶段加载:先加载所有Avatar(relations留空),再重建relations网络 - 引用对象:通过id从全局字典获取(如techniques_by_id) -- treasure:深拷贝后恢复devoured_souls +- weapon/auxiliary:深拷贝后恢复special_data - 错误容错:缺失的引用对象会跳过而不是崩溃 """ from typing import TYPE_CHECKING @@ -41,7 +41,8 @@ class AvatarLoadMixin: from src.classes.hp_and_mp import HP, MP from src.classes.technique import techniques_by_id from src.classes.item import items_by_id - from src.classes.treasure import treasures_by_id + from src.classes.weapon import weapons_by_id + from src.classes.auxiliary import auxiliaries_by_id from src.classes.sect import sects_by_id from src.classes.sect_ranks import SectRank from src.classes.root import Root @@ -98,12 +99,19 @@ class AvatarLoadMixin: if item_id in items_by_id: avatar.items[items_by_id[item_id]] = quantity - # 重建treasure(深拷贝因为devoured_souls是实例特有的) - treasure_id = data.get("treasure_id") - if treasure_id is not None and treasure_id in treasures_by_id: + # 重建weapon(深拷贝因为special_data是实例特有的) + weapon_id = data.get("weapon_id") + if weapon_id is not None and weapon_id in weapons_by_id: import copy - avatar.treasure = copy.deepcopy(treasures_by_id[treasure_id]) - avatar.treasure.devoured_souls = data.get("treasure_devoured_souls", 0) + avatar.weapon = copy.deepcopy(weapons_by_id[weapon_id]) + avatar.weapon.special_data = data.get("weapon_special_data", {}) + + # 重建auxiliary(深拷贝因为special_data是实例特有的) + auxiliary_id = data.get("auxiliary_id") + if auxiliary_id is not None and auxiliary_id in auxiliaries_by_id: + import copy + avatar.auxiliary = copy.deepcopy(auxiliaries_by_id[auxiliary_id]) + avatar.auxiliary.special_data = data.get("auxiliary_special_data", {}) # 重建spirit_animal spirit_animal_data = data.get("spirit_animal") diff --git a/src/sim/new_avatar.py b/src/sim/new_avatar.py index 6be6088..d22cd4a 100644 --- a/src/sim/new_avatar.py +++ b/src/sim/new_avatar.py @@ -16,7 +16,8 @@ from src.classes.sect import Sect, sects_by_id, sects_by_name from src.classes.alignment import Alignment from src.classes.relation import Relation from src.classes.technique import get_technique_by_sect, attribute_to_root, Technique, techniques_by_id, techniques_by_name -from src.classes.treasure import treasures_by_sect_id, Treasure, treasures_by_id, treasures_by_name +from src.classes.weapon import Weapon, weapons_by_id, weapons_by_name +from src.classes.auxiliary import Auxiliary, auxiliaries_by_id, auxiliaries_by_name from src.classes.persona import Persona, personas_by_id, personas_by_name @@ -436,10 +437,8 @@ def build_avatars_from_plan( if sect is not None: avatar.alignment = sect.alignment avatar.technique = get_technique_by_sect(sect) - treasure = treasures_by_sect_id.get(sect.id) - if treasure is not None and not sect_treasure_assigned.get(sect.id, False): # 每宗门仅发放一次所属法宝 - avatar.treasure = treasure - sect_treasure_assigned[sect.id] = True + # 每个宗门只分配一个法宝级兵器给最强者(但不在这里分配,而是让奇遇系统处理) + # 宗门成员初始都是普通兵器 if avatar.technique is not None: mapped = attribute_to_root(avatar.technique.attribute) @@ -493,7 +492,8 @@ def make_avatars( level=int(getattr(defined, "level", 0) or 0) if str(getattr(defined, "level", "")).strip() else None, appearance=int(getattr(defined, "appearance", 0) or 0) if str(getattr(defined, "appearance", "")).strip() else None, technique=getattr(defined, "technique", None), - treasure=getattr(defined, "treasure", None), + weapon=getattr(defined, "weapon", None), + auxiliary=getattr(defined, "auxiliary", None), personas=getattr(defined, "personas", None), ) avatars[da.id] = da @@ -556,19 +556,34 @@ def _parse_technique(value: Union[str, int, Technique, None]) -> Optional[Techni return techniques_by_name.get(s) -def _parse_treasure(value: Union[str, int, Treasure, None]) -> Optional[Treasure]: +def _parse_weapon(value: Union[str, int, Weapon, None]) -> Optional[Weapon]: if value is None: return None - if isinstance(value, Treasure): + if isinstance(value, Weapon): return value if isinstance(value, int): - return treasures_by_id.get(value) + return weapons_by_id.get(value) s = str(value).strip() if not s: return None if s.isdigit(): - return treasures_by_id.get(int(s)) - return treasures_by_name.get(s) + return weapons_by_id.get(int(s)) + return weapons_by_name.get(s) + + +def _parse_auxiliary(value: Union[str, int, Auxiliary, None]) -> Optional[Auxiliary]: + if value is None: + return None + if isinstance(value, Auxiliary): + return value + if isinstance(value, int): + return auxiliaries_by_id.get(value) + s = str(value).strip() + if not s: + return None + if s.isdigit(): + return auxiliaries_by_id.get(int(s)) + return auxiliaries_by_name.get(s) def _parse_personas(value: Union[str, int, Persona, List[Union[str, int, Persona]], None]) -> Optional[List[Persona]]: @@ -643,14 +658,15 @@ def get_new_avatar_with_config( level: Optional[int] = None, pos: Optional[Tuple[int, int]] = None, technique: Union[str, int, Technique, None] = None, - treasure: Union[str, int, Treasure, None] = None, + weapon: Union[str, int, Weapon, None] = None, + auxiliary: Union[str, int, Auxiliary, None] = None, personas: Union[str, int, Persona, List[Union[str, int, Persona]], None] = None, appearance: Optional[int] = None, ) -> Avatar: """ 创建一个可配置的新角色: - 若未提供参数,则复用 get_new_avatar_from_mortal 的随机策略(通过 plan_mortal 实现)。 - - 支持字符串参数:gender 仅支持 "男/女";sect/technique/treasure/persona 可用名称或数字ID。 + - 支持字符串参数:gender 仅支持 "男/女";sect/technique/weapon/auxiliary/persona 可用名称或数字ID。 参数: - name: 角色名;为空则根据宗门与姓氏自动生成 @@ -660,7 +676,8 @@ def get_new_avatar_with_config( - level: 等级(0~120);未提供时随机 - pos: 初始坐标 (x, y);未提供时随机 - technique: 指定功法 - - treasure: 指定法宝 + - weapon: 指定兵器 + - auxiliary: 指定辅助装备 - personas: 指定个性(单个或列表) """ # 年龄(先取整数年龄,规划阶段只用到 age.age,不依赖 realm) @@ -704,7 +721,7 @@ def get_new_avatar_with_config( # 生成 avatar = build_mortal_from_plan(world, current_month_stamp, name=name or "", age=final_age, plan=plan) - # 覆盖:功法/法宝/个性 + # 覆盖:功法/兵器/辅助装备/个性 tech_obj = _parse_technique(technique) if tech_obj is not None: avatar.technique = tech_obj @@ -712,9 +729,13 @@ def get_new_avatar_with_config( if mapped is not None: avatar.root = mapped - tre_obj = _parse_treasure(treasure) - if tre_obj is not None: - avatar.treasure = tre_obj + weapon_obj = _parse_weapon(weapon) + if weapon_obj is not None: + avatar.weapon = weapon_obj + + auxiliary_obj = _parse_auxiliary(auxiliary) + if auxiliary_obj is not None: + avatar.auxiliary = auxiliary_obj pers_list = _parse_personas(personas) if pers_list is not None and len(pers_list) > 0: diff --git a/src/sim/save/avatar_save_mixin.py b/src/sim/save/avatar_save_mixin.py index 4db929e..68db0ff 100644 --- a/src/sim/save/avatar_save_mixin.py +++ b/src/sim/save/avatar_save_mixin.py @@ -8,7 +8,7 @@ Avatar存档序列化Mixin - relations:转换为dict[str, str](avatar_id -> relation_value) - items:转换为dict[int, int](item_id -> quantity) - current_action:保存动作类名和参数 -- treasure:需要深拷贝(因为devoured_souls是实例特有的) +- weapon/auxiliary:需要深拷贝(因为special_data是实例特有的) """ @@ -76,8 +76,10 @@ class AvatarSaveMixin: # 物品与资源 "magic_stone": self.magic_stone.value, "items": items_dict, - "treasure_id": self.treasure.id if self.treasure else None, - "treasure_devoured_souls": self.treasure.devoured_souls if self.treasure else 0, + "weapon_id": self.weapon.id if self.weapon else None, + "weapon_special_data": self.weapon.special_data if self.weapon else {}, + "auxiliary_id": self.auxiliary.id if self.auxiliary else None, + "auxiliary_special_data": self.auxiliary.special_data if self.auxiliary else {}, "spirit_animal": spirit_animal_dict, # 社交与状态 diff --git a/static/game_configs/auxiliary.csv b/static/game_configs/auxiliary.csv new file mode 100644 index 0000000..7362060 --- /dev/null +++ b/static/game_configs/auxiliary.csv @@ -0,0 +1,9 @@ +id,name,grade,sect_id,desc,effects +,名称,等级(普通/宝物/法宝),所属宗门ID(见sect.csv),描述/提示词,"JSON形式" +2,灵舟,法宝,9,小舟承灵气,御风渡海,千里一瞬.,"{""extra_move_step"": 1}" +3,千里镜,法宝,3,澄澈如镜,观千里之外,洞察先机.,"{""extra_observation_radius"": 2}" +5,聚灵阵盘,法宝,5,刻阵成盘,聚纳灵机,修行事半功倍.,"{""extra_cultivate_exp"": 50}" +7,万欲同心结,法宝,6,情意同心,双修之道相互映照,修为更精进.,"{""extra_dual_cultivation_exp"": 100}" +8,影遁披风,法宝,8,融身影界,来去无踪,伏击出其不意.,"{""extra_move_step"": 1, ""extra_observation_radius"": 1}" +9,百兽驭兽符,法宝,2,以兽纹灵符加持,唤引兽心,御兽更易.,"{""extra_catch_success_rate"": 0.1}" + diff --git a/static/game_configs/weapon.csv b/static/game_configs/weapon.csv new file mode 100644 index 0000000..4d37339 --- /dev/null +++ b/static/game_configs/weapon.csv @@ -0,0 +1,14 @@ +id,name,weapon_type,grade,sect_id,desc,effects +,名称,兵器类型,等级(普通/宝物/法宝),所属宗门ID(见sect.csv),描述/提示词,"JSON形式" +1,本命剑匣,剑,法宝,1,以心御剑,匣启如霆,剑意随心破万法.,"{""extra_battle_strength_points"": 3}" +4,镇魂钟,棍,法宝,7,钟鸣摄魄,定魂镇邪,护心安魂.,"{""extra_battle_strength_points"": 2, ""extra_observation_radius"": 1}" +6,万魂幡,扇,法宝,4,幡起万魂啾啾,阴风过境.,"{""legal_actions"": [""DevourMortals""], ""extra_battle_strength_points"": ""eval(avatar.weapon.special_data.get('devoured_souls', 0) // 100 * 0.1)""}" +1001,普通剑,剑,普通,,平凡无奇的剑,修仙者人手一把。,"{}" +1002,普通刀,刀,普通,,平凡无奇的刀。,"{}" +1003,普通枪,枪,普通,,平凡无奇的枪。,"{}" +1004,普通棍,棍,普通,,平凡无奇的棍。,"{}" +1005,普通扇,扇,普通,,平凡无奇的扇。,"{}" +1006,普通鞭,鞭,普通,,平凡无奇的鞭。,"{}" +1007,普通琴,琴,普通,,平凡无奇的琴。,"{}" +1008,普通笛,笛,普通,,平凡无奇的笛。,"{}" + diff --git a/static/templates/story.txt b/static/templates/story.txt index bcf2033..83565ae 100644 --- a/static/templates/story.txt +++ b/static/templates/story.txt @@ -1,4 +1,4 @@ -你是一个故事讲述者,这是一个仙侠世界,你需要把一个事件扩展为一个约150字的小故事。 +你是一个故事讲述者,这是一个仙侠世界,你需要把一个事件扩展为一个约200~250字的小故事。 写作风格提示:{style} 额外主题提示:{story_prompt}