This commit is contained in:
bridge
2025-10-16 00:43:31 +08:00
parent 4295145934
commit 9d9d9737e5
9 changed files with 117 additions and 21 deletions

View File

@@ -2,6 +2,7 @@ import random
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional, List
from collections import defaultdict
import json
from src.classes.calendar import MonthStamp
@@ -18,7 +19,7 @@ from src.classes.age import Age
from src.classes.event import NULL_EVENT, Event
from src.classes.typings import ACTION_NAME, ACTION_PARAMS, ACTION_NAME_PARAMS_PAIRS, ACTION_NAME_PARAMS_PAIR
from src.classes.action_runtime import ActionPlan, ActionInstance
from src.classes.effect import _merge_effects
from src.classes.persona import Persona, personas_by_id, get_random_compatible_personas
from src.classes.item import Item
from src.classes.magic_stone import MagicStone
@@ -87,6 +88,7 @@ class Avatar:
appearance: Appearance = field(default_factory=get_random_appearance)
# 当月/当步新设动作标记:在 commit_next_plan 设为 True首次 tick_action 后清为 False
_new_action_set_this_step: bool = False
# 不缓存 effects实时从宗门与功法合并
def __post_init__(self):
"""
@@ -120,6 +122,18 @@ class Avatar:
from src.classes.alignment import Alignment as _Alignment
self.alignment = random.choice(list(_Alignment))
# effects 改为实时属性,不在此初始化
@property
def effects(self) -> dict[str, object]:
merged: dict[str, object] = defaultdict(str)
if self.sect is not None and getattr(self.sect, "effects", None):
merged = _merge_effects(merged, self.sect.effects)
if self.technique is not None and getattr(self.technique, "effects", None):
merged = _merge_effects(merged, self.technique.effects)
return merged
def __hash__(self) -> int:
return hash(self.id)

64
src/classes/effect.py Normal file
View File

@@ -0,0 +1,64 @@
from __future__ import annotations
import json
import ast
from typing import Any
def load_effect_from_str(value: object) -> dict[str, Any]:
"""
解析 effects 字符串为 dict
- 支持 JSON 格式(双引号)
- 支持 Python 字面量(单引号,如 {'k': ['v']})
- value 为 None/空字符串/'nan' 时返回 {}
- 解析非 dict 则返回 {}
"""
if value is None:
return {}
if isinstance(value, dict):
return value
s = str(value).strip()
if not s or s == "nan":
return {}
try:
obj = json.loads(s)
return obj if isinstance(obj, dict) else {}
except Exception:
try:
obj = ast.literal_eval(s)
return obj if isinstance(obj, dict) else {}
except Exception:
return {}
def _merge_effects(base: dict[str, object], addition: dict[str, object]) -> dict[str, object]:
"""
合并两个 effects 字典:
- list 型(如 legal_actions做去重并集
- 数值型:相加
- 其他:后者覆盖前者
返回新字典,不修改原对象。
"""
if not base and not addition:
return {}
merged: dict[str, object] = dict(base) if base else {}
for key, val in (addition or {}).items():
if key in merged:
old = merged[key]
if isinstance(old, list) and isinstance(val, list):
# 去重并集,保持相对顺序
seen: set[object] = set()
result: list[object] = []
for x in old + val:
if x in seen:
continue
seen.add(x)
result.append(x)
merged[key] = result
elif isinstance(old, (int, float)) and isinstance(val, (int, float)):
merged[key] = old + val
else:
merged[key] = val
else:
merged[key] = val
return merged

View File

@@ -38,9 +38,10 @@ class DualCultivation(MutualAction):
def can_start(self, target_avatar: "Avatar|str|None" = None) -> bool:
if target_avatar is None:
return False
# 必须为合欢宗
sect = self.avatar.sect
if sect is None or sect.name != "合欢宗":
# 基于 effects 判断是否允许
effects = self.avatar.effects
legal_actions = effects["legal_actions"]
if not isinstance(legal_actions, list) or "DualCultivation" not in legal_actions:
return False
target = self._get_target_avatar(target_avatar)
if target is None:

View File

@@ -1,8 +1,10 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from pathlib import Path
import json
from src.classes.alignment import Alignment
from src.utils.df import game_configs
from src.classes.effect import load_effect_from_str
from src.utils.config import CONFIG
@@ -40,6 +42,8 @@ class Sect:
technique_names: list[str]
# 随机选择宗门时使用的权重默认1
weight: float = 1.0
# 影响角色或系统的效果
effects: dict[str, object] = field(default_factory=dict)
# 功法在technique.csv中配置
# TODO法宝
# TODO宗内等级和称谓
@@ -86,6 +90,9 @@ def _load_sects() -> tuple[dict[int, Sect], dict[str, Sect]]:
weight_val = row.get("weight", 1)
weight = float(str(weight_val)) if str(weight_val) != "nan" else 1.0
# 读取 effects兼容 JSON/单引号字面量/空)
effects = load_effect_from_str(row.get("effects", ""))
sect = Sect(
id=int(row["id"]),
name=str(row["name"]),
@@ -103,6 +110,7 @@ def _load_sects() -> tuple[dict[int, Sect], dict[str, Sect]]:
),
technique_names=technique_names,
weight=weight,
effects=effects,
)
sects_by_id[sect.id] = sect
sects_by_name[sect.name] = sect

View File

@@ -1,8 +1,10 @@
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional, Dict, List
import json
from src.classes.effect import load_effect_from_str
from src.utils.df import game_configs
from src.classes.alignment import Alignment
@@ -51,6 +53,8 @@ class Technique:
condition: str
# 归属宗门名称None/空表示无宗门要求(散修可修)
sect: Optional[str] = None
# 影响角色或系统的效果
effects: dict[str, object] = field(default_factory=dict)
def is_allowed_for(self, avatar) -> bool:
if not self.condition:
@@ -96,6 +100,9 @@ def loads() -> tuple[dict[int, Technique], dict[str, Technique]]:
weight = float(str(weight_val)) if str(weight_val) != "nan" else 1.0
sect_val = row.get("sect", "")
sect = None if str(sect_val) == "nan" or str(sect_val).strip() == "" else str(sect_val).strip()
# 读取 effects兼容 JSON/单引号字面量/空)
effects = load_effect_from_str(row.get("effects", ""))
t = Technique(
id=int(row["id"]),
name=name,
@@ -105,6 +112,7 @@ def loads() -> tuple[dict[int, Technique], dict[str, Technique]]:
weight=weight,
condition=condition,
sect=sect,
effects=effects,
)
techniques_by_id[t.id] = t
techniques_by_name[t.name] = t