This commit is contained in:
bridge
2025-10-21 22:53:13 +08:00
parent f7a2e377a1
commit b4fa1c1c76
11 changed files with 185 additions and 11 deletions

View File

@@ -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",
]

View File

@@ -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 []

View File

@@ -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:

View File

@@ -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()

View File

@@ -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,

View File

@@ -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 {}

View File

@@ -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: ";"

View File

@@ -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,
1 id name exclusion_ids desc weight condition
7 5 随性 1;24;25 你总是会随机应变,性子到哪里了就是哪里,没有一定之规。 1
8 6 贪财 你对灵石和财富有着强烈的渴望。 1
9 7 采药 喜欢在山林中寻找各种奇花异草和灵药,对植物有着敏锐的直觉和深厚的兴趣。你认为大自然的恩赐需要用心去发现和珍惜。 1
10 8 猎者 享受在野外追踪猎物的刺激感,对各种动物的习性了如指掌。你相信通过狩猎能够磨练自己的意志和技能,获得更强大的力量。 享受在野外追踪猎物的刺激感,对各种动物的习性了如指掌,喜欢捕猎野兽。情况允许也会御兽。 1 100
11 9 沉思 2 你总是会深思熟虑,思考问题比较有哲理。 1
12 10 惜命 4;24;25 你总是会珍惜自己的生命,不会轻易冒险。 1
13 11 友爱 13;14;15;12;24;25 你重视同伴与和谐,乐于助人,倾向通过协作与沟通化解矛盾。 1

View File

@@ -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,
Can't render this file because it contains an unexpected character in line 5 and column 73.

View File

@@ -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}"
1 id name sect_id desc effects
9 7 万欲同心结 6 情意同心,双修之道相互映照,修为更精进. {"extra_dual_cultivation_exp": 100}
10 8 影遁披风 8 融身影界,来去无踪,伏击出其不意. {"extra_move_step": 1, "extra_observation_radius": 1}
11 9 百兽驭兽符 2 以兽纹灵符加持,唤引兽心,御兽更易. {"extra_catch_success_rate": 0.1}
12
13
14

View File

@@ -20,4 +20,5 @@
要求与约束:
- thought从侧面体现出角色个性、宗门信息
- 只有当前动作空间中的动作是立刻可以做的,其他动作需满足对应条件
- 不应过分重复的做相同动作
- 不应过分重复的做相同动作
- 决定动作前,注意是否可执行