update treasure to weapon and auxiliary
This commit is contained in:
@@ -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", [])
|
||||
|
||||
89
src/classes/auxiliary.py
Normal file
89
src/classes/auxiliary.py
Normal file
@@ -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()
|
||||
|
||||
@@ -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"<color:{r},{g},{b}>{self.weapon.get_info()}</color>"
|
||||
add_kv(lines, "兵器", weapon_text)
|
||||
|
||||
# 辅助装备(可选,使用颜色标记等级)
|
||||
if self.auxiliary is not None:
|
||||
r, g, b = self.auxiliary.grade.color_rgb
|
||||
auxiliary_text = f"<color:{r},{g},{b}>{self.auxiliary.get_info()}</color>"
|
||||
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:
|
||||
"""
|
||||
|
||||
27
src/classes/equipment_grade.py
Normal file
27
src/classes/equipment_grade.py
Normal file
@@ -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), # 金色
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
116
src/classes/weapon.py
Normal file
116
src/classes/weapon.py
Normal file
@@ -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)
|
||||
|
||||
19
src/classes/weapon_type.py
Normal file
19
src/classes/weapon_type.py
Normal file
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
# 社交与状态
|
||||
|
||||
9
static/game_configs/auxiliary.csv
Normal file
9
static/game_configs/auxiliary.csv
Normal file
@@ -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}"
|
||||
|
||||
|
14
static/game_configs/weapon.csv
Normal file
14
static/game_configs/weapon.csv
Normal file
@@ -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,普通笛,笛,普通,,平凡无奇的笛。,"{}"
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
你是一个故事讲述者,这是一个仙侠世界,你需要把一个事件扩展为一个约150字的小故事。
|
||||
你是一个故事讲述者,这是一个仙侠世界,你需要把一个事件扩展为一个约200~250字的小故事。
|
||||
写作风格提示:{style}
|
||||
额外主题提示:{story_prompt}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user