From b4fa1c1c7661333ef3e35221eba738309375e280 Mon Sep 17 00:00:00 2001 From: bridge Date: Tue, 21 Oct 2025 22:53:13 +0800 Subject: [PATCH] ad catch --- src/classes/action/__init__.py | 3 ++ src/classes/action/catch.py | 75 ++++++++++++++++++++++++++++++++ src/classes/avatar.py | 18 +++++++- src/classes/region.py | 23 ++++++++-- src/classes/sect.py | 24 ++++++++-- src/classes/spirit_animal.py | 42 ++++++++++++++++++ static/config.yml | 2 +- static/game_configs/persona.csv | 2 +- static/game_configs/sect.csv | 2 +- static/game_configs/treasure.csv | 2 + static/templates/ai.txt | 3 +- 11 files changed, 185 insertions(+), 11 deletions(-) create mode 100644 src/classes/action/catch.py create mode 100644 src/classes/spirit_animal.py diff --git a/src/classes/action/__init__.py b/src/classes/action/__init__.py index 6001f7e..0b5512f 100644 --- a/src/classes/action/__init__.py +++ b/src/classes/action/__init__.py @@ -32,6 +32,7 @@ from .help_mortals import HelpMortals from .talk import Talk from .devour_mortals import DevourMortals from .self_heal import SelfHeal +from .catch import Catch # 注册到 ActionRegistry(标注是否为实际可执行动作) register_action(actual=False)(Action) @@ -60,6 +61,7 @@ register_action(actual=True)(HelpMortals) register_action(actual=True)(Talk) register_action(actual=True)(DevourMortals) register_action(actual=True)(SelfHeal) +register_action(actual=True)(Catch) __all__ = [ # 基类 @@ -90,6 +92,7 @@ __all__ = [ "Talk", "DevourMortals", "SelfHeal", + "Catch", ] diff --git a/src/classes/action/catch.py b/src/classes/action/catch.py new file mode 100644 index 0000000..13917cf --- /dev/null +++ b/src/classes/action/catch.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +from src.classes.action import TimedAction +from src.classes.event import Event +from src.classes.region import NormalRegion +from src.classes.spirit_animal import SpiritAnimal +from src.classes.cultivation import Realm + +if TYPE_CHECKING: + from src.classes.animal import Animal + + +class Catch(TimedAction): + """ + 御兽:仅百兽宗弟子可用。 + 条件: + - 当前处于普通区域,且该区域有动物分布 + - 目标动物境界 <= Avatar 境界 + 结果: + - 按动物境界映射成功率尝试捕捉,成功则成为灵兽(覆盖旧灵兽)。 + """ + + COMMENT = "尝试驯服一只灵兽,成为自身灵兽" + DOABLES_REQUIREMENTS = "仅百兽宗;在有动物的普通区域;目标动物境界不高于角色" + PARAMS = {} + + duration_months = 4 + + def _get_available_animals(self) -> list[Animal]: + region = self.avatar.tile.region + avatar_realm = self.avatar.cultivation_progress.realm + return [animal for animal in getattr(region, "animals", []) if avatar_realm >= animal.realm] + + def _calc_success_rate_by_realm(self, animal_realm: Realm) -> float: + mapping: dict[Realm, float] = { + Realm.Qi_Refinement: 0.8, + Realm.Foundation_Establishment: 0.6, + Realm.Core_Formation: 0.4, + Realm.Nascent_Soul: 0.2, + } + return mapping.get(animal_realm, 0.1) + + def _execute(self) -> None: + animals = self._get_available_animals() + if not animals: + return + target = random.choice(animals) + base = self._calc_success_rate_by_realm(target.realm) + extra = float(self.avatar.effects.get("extra_catch_success_rate", 0) or 0) + rate = max(0.0, min(1.0, base + extra)) + if random.random() < rate: + # 覆盖为新的灵兽 + self.avatar.spirit_animal = SpiritAnimal(name=target.name, realm=target.realm) + + def can_start(self) -> bool: + # 仅百兽宗 + sect = getattr(self.avatar, "sect", None) + if sect is None or getattr(sect, "name", "") != "百兽宗": + return False + region = self.avatar.tile.region + if not isinstance(region, NormalRegion): + return False + return len(self._get_available_animals()) > 0 + + def start(self) -> Event: + region = self.avatar.tile.region + return Event(self.world.month_stamp, f"{self.avatar.name} 在 {region.name} 尝试御兽") + + def finish(self) -> list[Event]: + return [] + + diff --git a/src/classes/avatar.py b/src/classes/avatar.py index 6f6599a..40a80cb 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -35,6 +35,7 @@ from src.utils.params import filter_kwargs_for_callable from src.classes.sect import Sect from src.classes.appearance import Appearance, get_random_appearance from src.classes.battle import get_base_strength +from src.classes.spirit_animal import SpiritAnimal persona_num = CONFIG.avatar.persona_num @@ -91,6 +92,8 @@ class Avatar: appearance: Appearance = field(default_factory=get_random_appearance) # 装备的法宝(仅一个) treasure: Optional[Treasure] = None + # 灵兽:最多一个;若再次捕捉则覆盖 + spirit_animal: Optional[SpiritAnimal] = None # 当月/当步新设动作标记:在 commit_next_plan 设为 True,首次 tick_action 后清为 False _new_action_set_this_step: bool = False # 不缓存 effects;实时从宗门与功法合并 @@ -142,6 +145,9 @@ class Avatar: # 来自法宝 if self.treasure is not None: merged = _merge_effects(merged, self.treasure.effects) + # 来自灵兽 + if self.spirit_animal is not None: + merged = _merge_effects(merged, self.spirit_animal.effects) # 评估动态效果表达式:值以 "eval(...)" 形式给出 evaluated: dict[str, object] = {} for k, v in merged.items(): @@ -177,6 +183,7 @@ class Avatar: personas_info = ", ".join([p.get_detailed_info() for p in self.personas]) if self.personas else "无" items_info = ",".join([f"{item.get_detailed_info()}x{quantity}" for item, quantity in self.items.items()]) if self.items else "无" 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 "无" # personas和sect一致返回detailed,因为这俩太重要了 @@ -189,8 +196,9 @@ class Avatar: personas_info = ", ".join([p.get_detailed_info() for p in self.personas]) if self.personas else "无" items_info = ",".join([f"{item.get_info()}x{quantity}" for item, quantity in self.items.items()]) if self.items else "无" appearance_info = self.appearance.get_info() + spirit_animal_info = self.spirit_animal.get_info() if self.spirit_animal is not None else "无" - return { + info_dict = { "id": self.id, "名字": self.name, "性别": str(self.gender), @@ -210,6 +218,10 @@ class Avatar: "外貌": appearance_info, "法宝": treasure_info, } + # 灵兽:仅在存在时显示 + if self.spirit_animal is not None: + info_dict["灵兽"] = spirit_animal_info + return info_dict def __str__(self) -> str: return str(self.get_info(detailed=False)) @@ -548,6 +560,10 @@ class Avatar: else: add_kv(lines, "法宝", "无") + # 灵兽:仅在存在时显示 + if self.spirit_animal is not None: + add_kv(lines, "灵兽", self.spirit_animal.get_info()) + # 关系 relations_list = [f"{other.name}({str(relation)})" for other, relation in getattr(self, "relations", {}).items()] if relations_list: diff --git a/src/classes/region.py b/src/classes/region.py index 9f78799..3009261 100644 --- a/src/classes/region.py +++ b/src/classes/region.py @@ -249,18 +249,35 @@ class NormalRegion(Region): return "; ".join(info_parts) if info_parts else "暂无特色物种" + def _get_species_brief(self) -> str: + """ + 简要物种信息:仅名字与境界,用于在名称后括号展示。 + 例:"灵兔(练气)、青云鹿(练气)、暗影豹(筑基)" + 若无物种,返回空串。 + """ + briefs: list[str] = [] + if self.animals: + briefs.extend([f"{a.name}({a.realm.value})" for a in self.animals]) + if self.plants: + briefs.extend([f"{p.name}({p.realm.value})" for p in self.plants]) + return "、".join(briefs) + def __str__(self) -> str: species_info = self.get_species_info() return f"普通区域:{self.name} - {self.desc} | 物种分布:{species_info}" def get_info(self) -> str: - return self.name + brief = self._get_species_brief() + return f"{self.name}({brief})" if brief else self.name def get_detailed_info(self) -> str: + # 名称后追加物种简要;正文仍保留原来的详细物种描述 + brief = self._get_species_brief() + name_with_brief = f"{self.name}({brief})" if brief else self.name species_info = self.get_species_info() if not species_info or species_info == "暂无特色物种": - return f"{self.name} - {self.desc}" - return f"{self.name} - {self.desc} | 物种分布:{species_info}" + return f"{name_with_brief} - {self.desc}" + return f"{name_with_brief} - {self.desc} | 物种分布:{species_info}" def get_hover_info(self) -> list[str]: lines = super().get_hover_info() diff --git a/src/classes/sect.py b/src/classes/sect.py index 18a44c7..fdec7fe 100644 --- a/src/classes/sect.py +++ b/src/classes/sect.py @@ -68,6 +68,19 @@ def _load_sects() -> tuple[dict[int, Sect], dict[str, Sect]]: sects_by_name: dict[str, Sect] = {} df = game_configs["sect"] + # 读取宗门驻地映射(优先从 sect_region.csv 获取驻地地名/描述) + sect_region_df = game_configs.get("sect_region") + hq_by_sect_id: dict[int, tuple[str, str]] = {} + if sect_region_df is not None: + for _, sr in sect_region_df.iterrows(): + sid_str = str(sr.get("sect_id", "")).strip() + # 跳过说明行或空值 + if not sid_str.isdigit(): + continue + sid = int(sid_str) + hq_name = str(sr.get("headquarter_name", "")).strip() + hq_desc = str(sr.get("headquarter_desc", "")).strip() + hq_by_sect_id[sid] = (hq_name, hq_desc) # 可能不存在 technique 配表或未添加 sect 列,做容错 tech_df = game_configs.get("technique") assets_base = Path("assets/sects") @@ -92,6 +105,11 @@ def _load_sects() -> tuple[dict[int, Sect], dict[str, Sect]]: # 读取 effects(兼容 JSON/单引号字面量/空) effects = load_effect_from_str(row.get("effects", "")) + # 从 sect_region.csv 中优先取驻地名称/描述;否则兼容旧列或退回宗门名 + csv_hq = hq_by_sect_id.get(int(row["id"])) + hq_name_from_csv = (csv_hq[0] if csv_hq else "").strip() if csv_hq else "" + hq_desc_from_csv = (csv_hq[1] if csv_hq else "").strip() if csv_hq else "" + sect = Sect( id=int(row["id"]), name=str(row["name"]), @@ -101,10 +119,10 @@ def _load_sects() -> tuple[dict[int, Sect], dict[str, Sect]]: sect_surnames=_split_names(row["sect_surnames"]), male_sect_given_names=male_given_names, female_sect_given_names=female_given_names, - # 保留旧字段的兼容读取(如旧csv仍包含headquarter_*列则读入;否则使用宗门名与空描述) + # 驻地:优先 sect_region.csv;否则兼容旧列;最终回退宗门名 headquarter=SectHeadQuarter( - name=(str(row.get("headquarter_name", "")).strip() or str(row["name"])) , - desc=str(row.get("headquarter_desc", "")), + name=(hq_name_from_csv or str(row.get("headquarter_name", "")).strip() or str(row["name"])) , + desc=(hq_desc_from_csv or str(row.get("headquarter_desc", ""))), image=image_path, ), technique_names=technique_names, diff --git a/src/classes/spirit_animal.py b/src/classes/spirit_animal.py new file mode 100644 index 0000000..7c4cad5 --- /dev/null +++ b/src/classes/spirit_animal.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from src.classes.cultivation import Realm + + +@dataclass +class SpiritAnimal: + """ + 灵兽:附着于 Avatar 的唯一“守护灵兽”。 + 仅记录名称与境界,并据境界提供固定战斗力点数加成。 + + 规则: + - 练气/筑基/金丹/元婴 对应战斗力点数 +2/+4/+6/+8 + - Avatar 最多只能拥有一个 spirit_animal;新的会覆盖旧的。 + """ + + name: str + realm: Realm + + def get_extra_strength_points(self) -> int: + mapping = { + Realm.Qi_Refinement: 2, + Realm.Foundation_Establishment: 4, + Realm.Core_Formation: 6, + Realm.Nascent_Soul: 8, + } + return mapping.get(self.realm, 0) + + def get_info(self) -> str: + return f"{self.name}({self.realm.value})" + + @property + def effects(self) -> dict[str, object]: + """ + 灵兽提供的效果集合。当前仅包含战斗力点数,后续可扩展其他键。 + """ + pts = self.get_extra_strength_points() + return {"extra_battle_strength_points": pts} if pts else {} + + diff --git a/static/config.yml b/static/config.yml index 53c2a3c..e543ce0 100644 --- a/static/config.yml +++ b/static/config.yml @@ -18,7 +18,7 @@ game: init_npc_num: 6 sect_num: 2 # init_npc_num大于sect_num时,会随机选择sect_num个宗门 npc_birth_rate_per_month: 0.001 - fortune_probability: 0.01 + fortune_probability: 0.001 df: ids_separator: ";" diff --git a/static/game_configs/persona.csv b/static/game_configs/persona.csv index 3df10be..97cc124 100644 --- a/static/game_configs/persona.csv +++ b/static/game_configs/persona.csv @@ -7,7 +7,7 @@ id,name,exclusion_ids,desc,weight,condition 5,随性,1;24;25,你总是会随机应变,性子到哪里了就是哪里,没有一定之规。,1, 6,贪财,,你对灵石和财富有着强烈的渴望。,1, 7,采药,,喜欢在山林中寻找各种奇花异草和灵药,对植物有着敏锐的直觉和深厚的兴趣。你认为大自然的恩赐需要用心去发现和珍惜。,1, -8,猎者,,享受在野外追踪猎物的刺激感,对各种动物的习性了如指掌。你相信通过狩猎能够磨练自己的意志和技能,获得更强大的力量。,1, +8,猎者,,享受在野外追踪猎物的刺激感,对各种动物的习性了如指掌,喜欢捕猎野兽。情况允许也会御兽。,100, 9,沉思,2,你总是会深思熟虑,思考问题比较有哲理。,1, 10,惜命,4;24;25,你总是会珍惜自己的生命,不会轻易冒险。,1, 11,友爱,13;14;15;12;24;25,你重视同伴与和谐,乐于助人,倾向通过协作与沟通化解矛盾。,1, diff --git a/static/game_configs/sect.csv b/static/game_configs/sect.csv index 3bc3ea9..20253cc 100644 --- a/static/game_configs/sect.csv +++ b/static/game_configs/sect.csv @@ -1,7 +1,7 @@ id,name,desc,member_act_style,alignment,sect_surnames,male_sect_given_names,female_sect_given_names,weight,effects ,,宗门名称与描述,宗门成员行事风格,阵营(正/中/邪),宗门常用姓氏(分号分隔),男性常用名(分号分隔),女性常用名(分号分隔),权重(默认1),effects(JSON) 1,明心剑宗,通玄界东方第一宗,以无上剑道称雄于世。云纹禁制为不传心法。,清明克己,行止如一。重剑与心法并重,讲究明心见性。,正,明;心;剑;霄;玄;霁;衡;孤;徽;肃,澄川;宏石;磐岳;霆岱;寂岚;久安;宸秋;烁离;沧岳;砺锋;炎洲;远歌,采微;霏岚;韶华;绮澜;珠影;远岫;若水;凝香;雪瑶;南絮;轻萝;宛竹,1, -2,百兽宗,以驯养灵兽闻名,豢养各种妖兽灵怪为战力。,你言语直接,重视力量与血性,崇尚狩猎与搏斗。,邪,,驼王;飞熊;虎魄;狼行;熊罡;白猿;石坚;山岚;青鬃;玄爪;金瞳;裂爪;破角;狂鬃;赤鬣;苍隼;啸风;裂岩,狐绮;白貂;青翎;雪牙;赤羽;玄狸;灵爪;月狐;银鳞;霜蹄;云貉;绒尾;锦狐;轻蹄,1, +2,百兽宗,以驯养灵兽闻名,豢养各种妖兽灵怪为战力。,你言语直接,重视力量与血性,崇尚狩猎与搏斗。,邪,,驼王;飞熊;虎魄;狼行;熊罡;白猿;石坚;山岚;青鬃;玄爪;金瞳;裂爪;破角;狂鬃;赤鬣;苍隼;啸风;裂岩,狐绮;白貂;青翎;雪牙;赤羽;玄狸;灵爪;月狐;银鳞;霜蹄;云貉;绒尾;锦狐;轻蹄,100, 3,水镜宗,正道十宗之一,实则严守中立。拥有仙界异宝"彻天水镜"可预知未来。,你处事冷静圆融,喜以柔克刚,擅借力与反制。,中,水;镜;寒;霜;冰;清;沐;澜;渊;泉,涟光;沧浪;泽远;浩川;泊舟;涓石;溪原;涵舟;泠曜;漪岑;淞岳;涔雨,漫霖;洛漪;潋月;涵烟;沁波;翠波;漫葭;汀兰;潭歌;涓玥;澧宁;潇然,1, 4,冥王宗,行走幽冥之道,术法阴冷狠厉。,你言辞冷厉少情,敬畏因果而不惧杀伐,偏向效率与结果。,邪,冥;王;玄;幽;夜;白;冷;狱;魇;阴,血燎;焚魄;灰灭;殁川;绝尘;厌离;朔寒;邪风;归墟;朽骨;朔月;止戈,寒绫;霜瑶;凄歌;素鸢;祭宁;黛魂;夙梦;绫雪;凛珑;霁月;旷音;凝岚,1, 5,朱勾宗,邪宗大派。以炼器、机关、暗杀闻名于世,素来阴毒冷僻。,你直面欲望与代价,不惧黑暗,以攻伐见长。,邪,朱;绯;刃;戮;蚀;渊;钧;鸦;墨;殷,暗阑;机括;鬼匣;夜禁;幻锁;残锋;暗弦;影栅;幽钩;断线;潜匿;迷踪,玄簪;霜绡;纤罗;碎玉;影裳;轻弦;凝黛;凝烟;冷珥;素纱;凛钗;寒袖,1, diff --git a/static/game_configs/treasure.csv b/static/game_configs/treasure.csv index 904f1b2..b2e895d 100644 --- a/static/game_configs/treasure.csv +++ b/static/game_configs/treasure.csv @@ -9,4 +9,6 @@ id,name,sect_id,desc,effects 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/templates/ai.txt b/static/templates/ai.txt index 04b3d61..a5bbd7a 100644 --- a/static/templates/ai.txt +++ b/static/templates/ai.txt @@ -20,4 +20,5 @@ 要求与约束: - thought从侧面体现出角色个性、宗门信息 - 只有当前动作空间中的动作是立刻可以做的,其他动作需满足对应条件 -- 不应过分重复的做相同动作 \ No newline at end of file +- 不应过分重复的做相同动作 +- 决定动作前,注意是否可执行 \ No newline at end of file