refactor normalize and resolution
This commit is contained in:
@@ -5,10 +5,10 @@ from src.classes.action import InstantAction
|
||||
from src.classes.action.targeting_mixin import TargetingMixin
|
||||
from src.classes.event import Event
|
||||
from src.classes.battle import decide_battle, get_effective_strength_pair
|
||||
from src.classes.story_teller import StoryTeller
|
||||
from src.classes.death import handle_death
|
||||
from src.classes.death_reason import DeathReason
|
||||
from src.classes.kill_and_grab import kill_and_grab
|
||||
from src.utils.resolution import resolve_query
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.avatar import Avatar
|
||||
|
||||
class Attack(InstantAction, TargetingMixin):
|
||||
ACTION_NAME = "发起战斗"
|
||||
@@ -24,7 +24,8 @@ class Attack(InstantAction, TargetingMixin):
|
||||
IS_MAJOR: bool = True
|
||||
|
||||
def _execute(self, avatar_name: str) -> None:
|
||||
target = self.find_avatar_by_name(avatar_name)
|
||||
from src.classes.avatar import Avatar
|
||||
target = resolve_query(avatar_name, self.world, expected_types=[Avatar]).obj
|
||||
if target is None:
|
||||
return
|
||||
winner, loser, loser_damage, winner_damage = decide_battle(self.avatar, target)
|
||||
@@ -42,11 +43,21 @@ class Attack(InstantAction, TargetingMixin):
|
||||
self._last_result = (winner, loser, loser_damage, winner_damage)
|
||||
|
||||
def can_start(self, avatar_name: str) -> tuple[bool, str]:
|
||||
_, ok, reason = self.validate_target_avatar(avatar_name)
|
||||
return ok, reason
|
||||
if not avatar_name:
|
||||
return False, "缺少目标参数"
|
||||
|
||||
from src.classes.avatar import Avatar
|
||||
target = resolve_query(avatar_name, self.world, expected_types=[Avatar]).obj
|
||||
if target is None:
|
||||
return False, "目标不存在"
|
||||
if target.is_dead:
|
||||
return False, "目标已死亡"
|
||||
|
||||
return True, ""
|
||||
|
||||
def start(self, avatar_name: str) -> Event:
|
||||
target = self.find_avatar_by_name(avatar_name)
|
||||
from src.classes.avatar import Avatar
|
||||
target = resolve_query(avatar_name, self.world, expected_types=[Avatar]).obj
|
||||
target_name = target.name if target is not None else avatar_name
|
||||
# 展示双方折算战斗力(基于对手、含克制)
|
||||
s_att, s_def = get_effective_strength_pair(self.avatar, target)
|
||||
@@ -68,7 +79,8 @@ class Attack(InstantAction, TargetingMixin):
|
||||
if not (isinstance(res, tuple) and len(res) == 4):
|
||||
return []
|
||||
|
||||
target = self.find_avatar_by_name(avatar_name)
|
||||
from src.classes.avatar import Avatar
|
||||
target = resolve_query(avatar_name, self.world, expected_types=[Avatar]).obj
|
||||
start_text = getattr(self, '_start_event_content', "")
|
||||
|
||||
from src.classes.battle import handle_battle_finish
|
||||
|
||||
@@ -9,7 +9,10 @@ from src.classes.region import CityRegion
|
||||
from src.classes.elixir import Elixir, get_elixirs_by_realm
|
||||
from src.classes.prices import prices
|
||||
from src.classes.cultivation import Realm
|
||||
from src.utils.resolution import resolve_goods_by_name
|
||||
from src.classes.weapon import Weapon
|
||||
from src.classes.auxiliary import Auxiliary
|
||||
from src.classes.item import Item
|
||||
from src.utils.resolution import resolve_query
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.avatar import Avatar
|
||||
@@ -36,17 +39,19 @@ class Buy(InstantAction):
|
||||
if not isinstance(region, CityRegion):
|
||||
return False, "仅能在城市区域执行"
|
||||
|
||||
obj, obj_type, display_name = resolve_goods_by_name(target_name)
|
||||
if obj_type == "unknown":
|
||||
res = resolve_query(target_name, expected_types=[Elixir, Weapon, Auxiliary, Item])
|
||||
if not res.is_valid:
|
||||
return False, f"未知物品: {target_name}"
|
||||
|
||||
obj = res.obj
|
||||
|
||||
# 检查价格
|
||||
price = prices.get_buying_price(obj, self.avatar)
|
||||
if self.avatar.magic_stone < price:
|
||||
return False, f"灵石不足 (需要 {price})"
|
||||
|
||||
# 丹药特殊限制
|
||||
if obj_type == "elixir":
|
||||
if isinstance(obj, Elixir):
|
||||
elixir: Elixir = obj
|
||||
|
||||
# 必须是练气期丹药
|
||||
@@ -66,35 +71,36 @@ class Buy(InstantAction):
|
||||
return True, ""
|
||||
|
||||
def _execute(self, target_name: str) -> None:
|
||||
obj, obj_type, display_name = resolve_goods_by_name(target_name)
|
||||
if obj_type == "unknown":
|
||||
res = resolve_query(target_name, expected_types=[Elixir, Weapon, Auxiliary, Item])
|
||||
if not res.is_valid:
|
||||
return
|
||||
|
||||
obj = res.obj
|
||||
price = prices.get_buying_price(obj, self.avatar)
|
||||
self.avatar.magic_stone -= price
|
||||
|
||||
# 交付
|
||||
if obj_type == "elixir":
|
||||
if isinstance(obj, Elixir):
|
||||
self.avatar.consume_elixir(obj)
|
||||
# TODO: 购买新装备,如果换下了旧装备,应该自动卖出
|
||||
# 但是我现在还没有购买的能力,所以这个逻辑之后做。
|
||||
elif obj_type == "item":
|
||||
elif isinstance(obj, Item):
|
||||
self.avatar.add_item(obj)
|
||||
elif obj_type == "weapon":
|
||||
elif isinstance(obj, Weapon):
|
||||
# 购买装备需要深拷贝,因为装备有独立状态
|
||||
new_weapon = copy.deepcopy(obj)
|
||||
self.avatar.change_weapon(new_weapon)
|
||||
elif obj_type == "auxiliary":
|
||||
elif isinstance(obj, Auxiliary):
|
||||
# 购买装备需要深拷贝
|
||||
new_auxiliary = copy.deepcopy(obj)
|
||||
self.avatar.change_auxiliary(new_auxiliary)
|
||||
|
||||
def start(self, target_name: str) -> Event:
|
||||
obj, obj_type, display_name = resolve_goods_by_name(target_name)
|
||||
res = resolve_query(target_name, expected_types=[Elixir, Weapon, Auxiliary, Item])
|
||||
obj = res.obj
|
||||
display_name = res.name
|
||||
|
||||
if obj_type == "elixir":
|
||||
if isinstance(obj, Elixir):
|
||||
action_desc = "购买并服用了"
|
||||
elif obj_type in ["weapon", "auxiliary"]:
|
||||
elif isinstance(obj, (Weapon, Auxiliary)):
|
||||
action_desc = "购买并装备了"
|
||||
else:
|
||||
action_desc = "购买了"
|
||||
|
||||
@@ -11,6 +11,7 @@ from src.classes.weapon import get_random_weapon_by_realm
|
||||
from src.classes.auxiliary import get_random_auxiliary_by_realm
|
||||
from src.classes.single_choice import handle_item_exchange
|
||||
from src.utils.config import CONFIG
|
||||
from src.utils.resolution import resolve_query
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.avatar import Avatar
|
||||
@@ -54,10 +55,11 @@ class Cast(TimedAction):
|
||||
if not target_realm:
|
||||
return False, "未指定目标境界"
|
||||
|
||||
try:
|
||||
realm = Realm(target_realm)
|
||||
except ValueError:
|
||||
res = resolve_query(target_realm, expected_types=[Realm])
|
||||
if not res.is_valid:
|
||||
return False, f"无效的境界: {target_realm}"
|
||||
|
||||
realm = res.obj
|
||||
|
||||
cost = self._get_cost()
|
||||
count = self._count_materials(realm)
|
||||
@@ -68,7 +70,11 @@ class Cast(TimedAction):
|
||||
return True, ""
|
||||
|
||||
def start(self, target_realm: str) -> Event:
|
||||
self.target_realm = Realm(target_realm)
|
||||
res = resolve_query(target_realm, expected_types=[Realm])
|
||||
if res.is_valid:
|
||||
self.target_realm = res.obj
|
||||
self.target_realm = Realm(target_realm)
|
||||
|
||||
cost = self._get_cost()
|
||||
|
||||
# 扣除材料逻辑
|
||||
@@ -87,9 +93,10 @@ class Cast(TimedAction):
|
||||
for item, take in items_to_modify:
|
||||
self.avatar.remove_item(item, take)
|
||||
|
||||
realm_val = self.target_realm.value if self.target_realm else target_realm
|
||||
return Event(
|
||||
self.world.month_stamp,
|
||||
f"{self.avatar.name} 开始尝试铸造{target_realm}阶法宝。",
|
||||
f"{self.avatar.name} 开始尝试铸造{realm_val}阶法宝。",
|
||||
related_avatars=[self.avatar.id]
|
||||
)
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ from __future__ import annotations
|
||||
from src.classes.action import InstantAction, Move
|
||||
from src.classes.event import Event
|
||||
from src.classes.action.move_helper import clamp_manhattan_with_diagonal_priority
|
||||
from src.classes.region import Region, resolve_region
|
||||
from src.classes.region import Region
|
||||
from src.utils.distance import euclidean_distance
|
||||
from src.utils.resolution import resolve_query
|
||||
|
||||
|
||||
class MoveAwayFromRegion(InstantAction):
|
||||
@@ -16,7 +17,10 @@ class MoveAwayFromRegion(InstantAction):
|
||||
|
||||
def _execute(self, region: str) -> None:
|
||||
# 解析目标区域,并沿“远离该区域最近格点”的方向移动一步
|
||||
r: Region = resolve_region(self.world, region)
|
||||
r = resolve_query(region, self.world, expected_types=[Region]).obj
|
||||
if not r:
|
||||
return
|
||||
|
||||
x = self.avatar.pos_x
|
||||
y = self.avatar.pos_y
|
||||
# 找到目标区域内距离当前坐标最近的格点
|
||||
@@ -35,19 +39,16 @@ class MoveAwayFromRegion(InstantAction):
|
||||
Move(self.avatar, self.world).execute(dx, dy)
|
||||
|
||||
def can_start(self, region: str) -> tuple[bool, str]:
|
||||
try:
|
||||
resolve_region(self.world, region)
|
||||
if resolve_query(region, self.world, expected_types=[Region]).obj:
|
||||
return True, ""
|
||||
except Exception:
|
||||
return False, f"无法解析区域: {region}"
|
||||
return False, f"无法解析区域: {region}"
|
||||
|
||||
def start(self, region: str) -> Event:
|
||||
r = resolve_region(self.world, region)
|
||||
return Event(self.world.month_stamp, f"{self.avatar.name} 开始离开 {r.name}", related_avatars=[self.avatar.id])
|
||||
r = resolve_query(region, self.world, expected_types=[Region]).obj
|
||||
region_name = r.name if r else region
|
||||
return Event(self.world.month_stamp, f"{self.avatar.name} 开始离开 {region_name}", related_avatars=[self.avatar.id])
|
||||
|
||||
# InstantAction 已实现 step 完成
|
||||
|
||||
async def finish(self, region: str) -> list[Event]:
|
||||
return []
|
||||
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@ from __future__ import annotations
|
||||
import random
|
||||
from src.classes.action import DefineAction, ActualActionMixin
|
||||
from src.classes.event import Event
|
||||
from src.classes.region import Region, resolve_region
|
||||
from src.classes.region import Region
|
||||
from src.classes.sect_region import SectRegion
|
||||
from src.classes.action import Move
|
||||
from src.classes.action_runtime import ActionResult, ActionStatus
|
||||
from src.classes.action.move_helper import clamp_manhattan_with_diagonal_priority
|
||||
from src.utils.resolution import resolve_query
|
||||
|
||||
|
||||
class MoveToRegion(DefineAction, ActualActionMixin):
|
||||
@@ -46,8 +47,11 @@ class MoveToRegion(DefineAction, ActualActionMixin):
|
||||
"""
|
||||
移动到某个region
|
||||
"""
|
||||
region = resolve_region(self.world, region)
|
||||
target_loc = self._get_target_loc(region)
|
||||
target_region = resolve_query(region, self.world, expected_types=[Region]).obj
|
||||
if not target_region:
|
||||
return
|
||||
|
||||
target_loc = self._get_target_loc(target_region)
|
||||
|
||||
cur_loc = (self.avatar.pos_x, self.avatar.pos_y)
|
||||
raw_dx = target_loc[0] - cur_loc[0]
|
||||
@@ -58,29 +62,34 @@ class MoveToRegion(DefineAction, ActualActionMixin):
|
||||
Move(self.avatar, self.world).execute(dx, dy)
|
||||
|
||||
def can_start(self, region: Region | str) -> tuple[bool, str]:
|
||||
try:
|
||||
r = resolve_region(self.world, region)
|
||||
|
||||
# 宗门总部限制:非本门弟子禁止入内
|
||||
if isinstance(r, SectRegion):
|
||||
if self.avatar.sect is None or self.avatar.sect.id != r.sect_id:
|
||||
return False, f"【{r.name}】是其他宗门驻地,你并非该宗门弟子。"
|
||||
|
||||
return True, ""
|
||||
except Exception:
|
||||
r = resolve_query(region, self.world, expected_types=[Region]).obj
|
||||
if not r:
|
||||
return False, f"无法解析区域: {region}"
|
||||
|
||||
# 宗门总部限制:非本门弟子禁止入内
|
||||
if isinstance(r, SectRegion):
|
||||
if self.avatar.sect is None or self.avatar.sect.id != r.sect_id:
|
||||
return False, f"【{r.name}】是其他宗门驻地,你并非该宗门弟子。"
|
||||
|
||||
return True, ""
|
||||
|
||||
def start(self, region: Region | str) -> Event:
|
||||
r = resolve_region(self.world, region)
|
||||
region_name = r.name
|
||||
# 在开始时就确定目标点
|
||||
self._get_target_loc(r)
|
||||
return Event(self.world.month_stamp, f"{self.avatar.name} 开始移动向 {region_name}", related_avatars=[self.avatar.id])
|
||||
r = resolve_query(region, self.world, expected_types=[Region]).obj
|
||||
# 这里理论上在 can_start 已经校验过,但为了安全再校验一次,如果None则不处理(实际上不会发生)
|
||||
if r:
|
||||
region_name = r.name
|
||||
# 在开始时就确定目标点
|
||||
self._get_target_loc(r)
|
||||
return Event(self.world.month_stamp, f"{self.avatar.name} 开始移动向 {region_name}", related_avatars=[self.avatar.id])
|
||||
return Event(self.world.month_stamp, f"{self.avatar.name} 试图移动但目标无效", related_avatars=[self.avatar.id])
|
||||
|
||||
def step(self, region: Region | str) -> ActionResult:
|
||||
self.execute(region=region)
|
||||
|
||||
r = resolve_region(self.world, region)
|
||||
r = resolve_query(region, self.world, expected_types=[Region]).obj
|
||||
if not r:
|
||||
return ActionResult(status=ActionStatus.FAILED, events=[])
|
||||
|
||||
target_loc = self._get_target_loc(r)
|
||||
|
||||
# 完成条件:到达具体的随机目标点
|
||||
@@ -91,5 +100,3 @@ class MoveToRegion(DefineAction, ActualActionMixin):
|
||||
|
||||
async def finish(self, region: Region | str) -> list[Event]:
|
||||
return []
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ from src.classes.action import InstantAction
|
||||
from src.classes.event import Event
|
||||
from src.classes.region import CityRegion
|
||||
from src.classes.normalize import normalize_goods_name
|
||||
from src.utils.resolution import resolve_goods_by_name
|
||||
from src.utils.resolution import resolve_query
|
||||
from src.classes.item import Item
|
||||
from src.classes.weapon import Weapon
|
||||
from src.classes.auxiliary import Auxiliary
|
||||
|
||||
|
||||
class Sell(InstantAction):
|
||||
@@ -29,32 +32,36 @@ class Sell(InstantAction):
|
||||
return False, "仅能在城市区域执行"
|
||||
|
||||
# 使用通用解析逻辑获取物品原型和类型
|
||||
obj, obj_type, _ = resolve_goods_by_name(target_name)
|
||||
res = resolve_query(target_name, expected_types=[Item, Weapon, Auxiliary])
|
||||
if not res.is_valid:
|
||||
return False, f"未持有物品/装备: {target_name}"
|
||||
|
||||
obj = res.obj
|
||||
normalized_name = normalize_goods_name(target_name)
|
||||
|
||||
# 1. 如果是物品,检查背包
|
||||
if obj_type == "item":
|
||||
if isinstance(obj, Item):
|
||||
if self.avatar.get_item_quantity(obj) > 0:
|
||||
pass # 检查通过
|
||||
else:
|
||||
return False, f"未持有物品: {target_name}"
|
||||
|
||||
# 2. 如果是兵器,检查当前装备
|
||||
elif obj_type == "weapon":
|
||||
elif isinstance(obj, Weapon):
|
||||
if self.avatar.weapon and normalize_goods_name(self.avatar.weapon.name) == normalized_name:
|
||||
pass # 检查通过
|
||||
else:
|
||||
return False, f"未持有装备: {target_name}"
|
||||
|
||||
# 3. 如果是辅助装备,检查当前装备
|
||||
elif obj_type == "auxiliary":
|
||||
elif isinstance(obj, Auxiliary):
|
||||
if self.avatar.auxiliary and normalize_goods_name(self.avatar.auxiliary.name) == normalized_name:
|
||||
pass # 检查通过
|
||||
else:
|
||||
return False, f"未持有装备: {target_name}"
|
||||
|
||||
else:
|
||||
return False, f"未持有物品/装备: {target_name}"
|
||||
return False, f"无法出售此类型: {target_name}"
|
||||
|
||||
return True, ""
|
||||
|
||||
@@ -63,26 +70,30 @@ class Sell(InstantAction):
|
||||
if not isinstance(region, CityRegion):
|
||||
return
|
||||
|
||||
# 使用通用解析逻辑获取物品原型和类型
|
||||
obj, obj_type, _ = resolve_goods_by_name(target_name)
|
||||
res = resolve_query(target_name, expected_types=[Item, Weapon, Auxiliary])
|
||||
if not res.is_valid:
|
||||
return
|
||||
|
||||
obj = res.obj
|
||||
normalized_name = normalize_goods_name(target_name)
|
||||
|
||||
if obj_type == "item":
|
||||
if isinstance(obj, Item):
|
||||
quantity = self.avatar.get_item_quantity(obj)
|
||||
self.avatar.sell_item(obj, quantity)
|
||||
elif obj_type == "weapon":
|
||||
elif isinstance(obj, Weapon):
|
||||
# 需要再确认一次是否是当前装备
|
||||
if self.avatar.weapon and normalize_goods_name(self.avatar.weapon.name) == normalized_name:
|
||||
self.avatar.sell_weapon(obj)
|
||||
self.avatar.change_weapon(None) # 卖出后卸下
|
||||
elif obj_type == "auxiliary":
|
||||
elif isinstance(obj, Auxiliary):
|
||||
# 需要再确认一次是否是当前装备
|
||||
if self.avatar.auxiliary and normalize_goods_name(self.avatar.auxiliary.name) == normalized_name:
|
||||
self.avatar.sell_auxiliary(obj)
|
||||
self.avatar.change_auxiliary(None) # 卖出后卸下
|
||||
|
||||
def start(self, target_name: str) -> Event:
|
||||
obj, obj_type, display_name = resolve_goods_by_name(target_name)
|
||||
res = resolve_query(target_name)
|
||||
display_name = res.name if res.is_valid else target_name
|
||||
return Event(
|
||||
self.world.month_stamp,
|
||||
f"{self.avatar.name} 在城镇出售了 {display_name}",
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, Iterable
|
||||
from typing import Optional, Iterable, TYPE_CHECKING
|
||||
|
||||
from src.classes.tile import get_avatar_distance
|
||||
from src.classes.observe import get_observable_avatars
|
||||
from src.classes.normalize import normalize_avatar_name
|
||||
from src.utils.resolution import resolve_query
|
||||
# 注意:避免在此处直接引入 Avatar 导致循环引用,使用 TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.avatar import Avatar
|
||||
|
||||
|
||||
class TargetingMixin:
|
||||
@@ -17,14 +22,13 @@ class TargetingMixin:
|
||||
"""
|
||||
根据名字查找角色。
|
||||
会自动规范化名字(去除括号等附加信息)以提高容错性。
|
||||
|
||||
例如:查找 "张三(元婴)" 会自动匹配到名为 "张三" 的角色
|
||||
"""
|
||||
normalized_name = normalize_avatar_name(name)
|
||||
for v in self.world.avatar_manager.avatars.values():
|
||||
if v.name == normalized_name:
|
||||
return v
|
||||
return None
|
||||
|
||||
# 动态导入 Avatar 类以进行类型检查,或者直接依赖 resolve_query 的内部逻辑
|
||||
# 这里 resolve_query 需要 world 上下文
|
||||
from src.classes.avatar import Avatar
|
||||
res = resolve_query(name, self.world, expected_types=[Avatar])
|
||||
return res.obj
|
||||
|
||||
def avatars_in_same_region(self, avatar: "Avatar") -> list["Avatar"]:
|
||||
return self.world.avatar_manager.get_avatars_in_same_region(avatar)
|
||||
@@ -78,5 +82,3 @@ class TargetingMixin:
|
||||
if target.is_dead:
|
||||
return None, False, "目标已死亡"
|
||||
return target, True, ""
|
||||
|
||||
|
||||
|
||||
@@ -7,13 +7,14 @@ from src.classes.mutual_action.mutual_action import MutualAction
|
||||
from src.classes.event import Event
|
||||
from src.classes.action.registry import register_action
|
||||
from src.classes.action.cooldown import cooldown_action
|
||||
from src.classes.region import resolve_region, CultivateRegion
|
||||
from src.classes.region import CultivateRegion
|
||||
from src.classes.action_runtime import ActionResult, ActionStatus
|
||||
from src.classes.battle import decide_battle
|
||||
from src.classes.story_teller import StoryTeller
|
||||
from src.classes.death import handle_death
|
||||
from src.classes.death_reason import DeathReason
|
||||
from src.classes.action.event_helper import EventHelper
|
||||
from src.utils.resolution import resolve_query
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.avatar import Avatar
|
||||
@@ -40,11 +41,17 @@ class Occupy(MutualAction):
|
||||
|
||||
def _get_region_and_host(self, region_name: str) -> tuple[CultivateRegion | None, "Avatar | None", str]:
|
||||
"""解析区域并获取主人"""
|
||||
region = resolve_region(self.world, region_name)
|
||||
if region is None:
|
||||
res = resolve_query(region_name, self.world, expected_types=[CultivateRegion])
|
||||
|
||||
# resolve_query 可能返回普通 Region,这里需要严格检查是否为 CultivateRegion
|
||||
region = res.obj
|
||||
|
||||
if not res.is_valid or region is None:
|
||||
return None, None, f"无法找到区域:{region_name}"
|
||||
|
||||
if not isinstance(region, CultivateRegion):
|
||||
return None, None, f"{region.name if region else '荒野'} 不是修炼区域,无法占据"
|
||||
|
||||
return region, region.host_avatar, ""
|
||||
|
||||
def can_start(self, region_name: str) -> tuple[bool, str]:
|
||||
|
||||
@@ -2,29 +2,15 @@
|
||||
名称规范化工具模块
|
||||
|
||||
提供统一的名称规范化函数,用于处理各类名称中的括号和附加信息。
|
||||
适用于:角色名、地区名、物品名等。
|
||||
"""
|
||||
|
||||
|
||||
def _remove_parentheses(name: str) -> str:
|
||||
def remove_parentheses(name: str, recursive: bool = False) -> str:
|
||||
"""
|
||||
通用的括号移除函数:去除字符串中首个括号及其内容。
|
||||
|
||||
支持的括号类型:() () [] 【】 「」 『』 <> 《》
|
||||
通用括号移除函数。
|
||||
|
||||
Args:
|
||||
name: 原始字符串
|
||||
|
||||
Returns:
|
||||
去除首个括号后的字符串(去除前后空格)
|
||||
|
||||
Examples:
|
||||
>>> _remove_parentheses("张三(元婴)")
|
||||
'张三'
|
||||
>>> _remove_parentheses("青云林海(千年古松(金丹))")
|
||||
'青云林海'
|
||||
>>> _remove_parentheses("青云鹿角 -(练气)")
|
||||
'青云鹿角 -'
|
||||
recursive: 是否递归移除所有括号(处理嵌套括号)
|
||||
"""
|
||||
s = str(name).strip()
|
||||
brackets = [
|
||||
@@ -34,127 +20,56 @@ def _remove_parentheses(name: str) -> str:
|
||||
("<", ">"), ("《", "》")
|
||||
]
|
||||
|
||||
for left, right in brackets:
|
||||
idx = s.find(left)
|
||||
if idx != -1:
|
||||
# 找到左括号,去除从此开始到字符串末尾的内容
|
||||
s = s[:idx].strip()
|
||||
break
|
||||
|
||||
return s
|
||||
|
||||
|
||||
def normalize_avatar_name(name: str) -> str:
|
||||
"""
|
||||
规范化角色名字:去除括号及其中的附加信息(如境界)。
|
||||
|
||||
Args:
|
||||
name: 原始角色名字,可能包含境界等附加信息
|
||||
|
||||
Returns:
|
||||
规范化后的角色名字
|
||||
|
||||
Examples:
|
||||
>>> normalize_avatar_name("张三(元婴)")
|
||||
'张三'
|
||||
>>> normalize_avatar_name("张三,境界:元婴")
|
||||
'张三,境界:元婴'
|
||||
"""
|
||||
return _remove_parentheses(name)
|
||||
|
||||
|
||||
def normalize_region_name(name: str) -> str:
|
||||
"""
|
||||
规范化地区名称:去除括号及其中的附加信息(如灵气密度、动植物等)。
|
||||
|
||||
处理多层括号:递归去除所有括号及其内容。
|
||||
|
||||
Args:
|
||||
name: 原始地区名称,可能包含资源等附加信息
|
||||
|
||||
Returns:
|
||||
规范化后的地区名称
|
||||
|
||||
Examples:
|
||||
>>> normalize_region_name("太白金府(金行灵气:10)")
|
||||
'太白金府'
|
||||
>>> normalize_region_name("青云林海(千年古松(金丹))")
|
||||
'青云林海'
|
||||
"""
|
||||
s = str(name).strip()
|
||||
brackets = [
|
||||
("(", ")"), ("(", ")"),
|
||||
("[", "]"), ("【", "】"),
|
||||
("「", "」"), ("『", "』"),
|
||||
("<", ">"), ("《", "》")
|
||||
]
|
||||
|
||||
# 递归去除所有括号(用于处理嵌套括号)
|
||||
while True:
|
||||
found = False
|
||||
for left, right in brackets:
|
||||
# 查找最外层的左括号
|
||||
start = s.find(left)
|
||||
end = s.rfind(right)
|
||||
if start != -1 and end != -1 and end > start:
|
||||
s = (s[:start] + s[end + 1:]).strip()
|
||||
if start != -1:
|
||||
# 查找对应的右括号(从后往前找或者从前往后找匹配的)
|
||||
# 简单策略:找最后一个右括号,或者找匹配的。
|
||||
# 原有逻辑 region 使用的是 rfind,这里我们采用更稳健的逻辑:
|
||||
# 既然是 remove,通常是去除说明性文字,保留主体。
|
||||
|
||||
# 策略:找到第一个左括号,和其对应的配对右括号(如果简单处理,直接找最后一个右括号可能误删)
|
||||
# 但为了保持和原有 region 逻辑一致(处理 "青云林海(千年古松(金丹))" -> "青云林海"),
|
||||
# 只要发现左括号,就切断到末尾或者切断到匹配的右括号。
|
||||
|
||||
# 简化逻辑:找到第一个左括号,直接截断。这适用于绝大多数 "Name (Info)" 的情况。
|
||||
s = s[:start].strip()
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
|
||||
if not recursive or not found:
|
||||
break
|
||||
|
||||
return s
|
||||
|
||||
return s.strip()
|
||||
|
||||
def normalize_name(name: str) -> str:
|
||||
"""
|
||||
最通用的规范化:去除括号及其内容。
|
||||
"""
|
||||
return remove_parentheses(name)
|
||||
|
||||
# --- 兼容特定业务逻辑的别名或特化 ---
|
||||
|
||||
def normalize_avatar_name(name: str) -> str:
|
||||
return remove_parentheses(name)
|
||||
|
||||
def normalize_region_name(name: str) -> str:
|
||||
# 地区名可能包含多层嵌套,使用递归模式虽然在这里和非递归效果可能一样(因为都是截断),
|
||||
# 但保持接口定义清晰。对于截断策略,递归其实没有意义,因为第一次就截断了。
|
||||
# 除非括号在中间: "Region(Info) Suffix" -> "Region Suffix"?
|
||||
# 目前游戏里的命名习惯通常后缀是括号说明,所以直接截断是安全的。
|
||||
return remove_parentheses(name)
|
||||
|
||||
def normalize_goods_name(name: str) -> str:
|
||||
"""
|
||||
规范化商品名称(包括物品、兵器、法宝、丹药)。
|
||||
|
||||
统一逻辑:
|
||||
1. 移除括号及内容(如境界、类型说明)
|
||||
2. 移除尾部的 " -" 标记(常见于材料生成名)
|
||||
3. 移除首尾空格
|
||||
|
||||
Args:
|
||||
name: 原始商品名称
|
||||
|
||||
Returns:
|
||||
规范化后的商品名称
|
||||
|
||||
Examples:
|
||||
>>> normalize_goods_name("青云鹿角 -(练气)") # item
|
||||
'青云鹿角'
|
||||
>>> normalize_goods_name("精铁剑(练气)") # weapon
|
||||
'精铁剑'
|
||||
>>> normalize_goods_name("聚气丹(练气)") # elixir
|
||||
'聚气丹'
|
||||
"""
|
||||
s = _remove_parentheses(name)
|
||||
s = s.rstrip(" -").strip()
|
||||
return s
|
||||
|
||||
"""物品名额外去除尾部的 ' -'"""
|
||||
s = remove_parentheses(name)
|
||||
return s.rstrip(" -").strip()
|
||||
|
||||
def normalize_weapon_type(name: str) -> str:
|
||||
"""
|
||||
规范化兵器类型名称:映射到标准的WeaponType枚举值。
|
||||
|
||||
处理格式:
|
||||
- 去除空格和多余符号
|
||||
- "剑"/"剑类"/"剑兵器" -> "剑"
|
||||
|
||||
Args:
|
||||
name: 兵器类型名称
|
||||
|
||||
Returns:
|
||||
规范化后的兵器类型名称(WeaponType.value)
|
||||
|
||||
Examples:
|
||||
>>> normalize_weapon_type("剑 ")
|
||||
'剑'
|
||||
>>> normalize_weapon_type("刀类")
|
||||
'刀'
|
||||
"""
|
||||
s = str(name).strip()
|
||||
# 移除常见后缀
|
||||
for suffix in ["类", "兵器", "武器"]:
|
||||
if s.endswith(suffix):
|
||||
s = s[:-len(suffix)].strip()
|
||||
|
||||
@@ -164,9 +164,6 @@ class NormalRegion(Region):
|
||||
info = super().get_structured_info()
|
||||
info["type_name"] = "普通区域"
|
||||
|
||||
# Fix: Return the actual structure instead of just calling get_structured_info on elements but never assigning
|
||||
# The previous implementation (if it existed) was inherited from base or incorrect
|
||||
|
||||
# Assuming animals and plants are populated in __post_init__
|
||||
info["animals"] = [a.get_structured_info() for a in self.animals] if self.animals else []
|
||||
info["plants"] = [p.get_structured_info() for p in self.plants] if self.plants else []
|
||||
@@ -244,66 +241,3 @@ class CityRegion(Region):
|
||||
info = super().get_structured_info()
|
||||
info["type_name"] = "城市区域"
|
||||
return info
|
||||
|
||||
|
||||
def _normalize_region_name(name: str) -> str:
|
||||
s = str(name).strip()
|
||||
brackets = [("(", ")"), ("(", ")"), ("[", "]"), ("【", "】"), ("「", "」"), ("『", "』"), ("<", ">"), ("《", "》")]
|
||||
for left, right in brackets:
|
||||
while True:
|
||||
start = s.find(left)
|
||||
end = s.rfind(right)
|
||||
if start != -1 and end != -1 and end > start:
|
||||
s = (s[:start] + s[end + 1:]).strip()
|
||||
else:
|
||||
break
|
||||
return s
|
||||
|
||||
|
||||
def resolve_region(world, region: Union[Region, str]) -> Region:
|
||||
"""
|
||||
解析字符串或 Region 为当前 world.map 中的 Region 实例
|
||||
"""
|
||||
from typing import Dict
|
||||
|
||||
if isinstance(region, str):
|
||||
region_name = region
|
||||
by_name: Dict[str, Region] = getattr(world.map, "region_names", {})
|
||||
|
||||
# 1) 精确匹配
|
||||
r = by_name.get(region_name)
|
||||
if r is not None:
|
||||
return r
|
||||
|
||||
# 2) 归一化后再精确匹配
|
||||
normalized = _normalize_region_name(region_name)
|
||||
if normalized and normalized != region_name:
|
||||
r2 = by_name.get(normalized)
|
||||
if r2 is not None:
|
||||
return r2
|
||||
|
||||
# 3) 唯一包含匹配
|
||||
candidates = [name for name in by_name.keys() if name and (name in region_name or (normalized and name in normalized))]
|
||||
if len(candidates) == 1:
|
||||
return by_name[candidates[0]]
|
||||
|
||||
# 4) 兜底:若传入为宗门名,则解析为其总部区域
|
||||
sect = sects_by_name.get(region_name) or (sects_by_name.get(normalized) if normalized and normalized != region_name else None)
|
||||
if sect is not None:
|
||||
sect_regions = getattr(world.map, "sect_regions", {}) or {}
|
||||
matched = [r for r in sect_regions.values() if getattr(r, "sect_name", None) == sect.name]
|
||||
if len(matched) == 1:
|
||||
return matched[0]
|
||||
|
||||
if candidates:
|
||||
sample = ", ".join(candidates[:5])
|
||||
raise ValueError(f"区域名不唯一: {region_name},候选: {sample}")
|
||||
raise ValueError(f"未知区域名: {region_name}")
|
||||
|
||||
if isinstance(region, Region):
|
||||
by_id = getattr(world.map, "regions", None)
|
||||
if isinstance(by_id, dict) and region.id in by_id:
|
||||
return by_id[region.id]
|
||||
return region
|
||||
|
||||
raise TypeError(f"不支持的region类型: {type(region).__name__}")
|
||||
|
||||
@@ -6,7 +6,8 @@ from src.classes.world import World
|
||||
from src.classes.avatar import Avatar, Gender
|
||||
from src.classes.appearance import get_appearance_by_level
|
||||
from src.classes.calendar import MonthStamp
|
||||
from src.classes.region import Region, resolve_region
|
||||
from src.classes.region import Region
|
||||
from src.utils.resolution import resolve_query
|
||||
from src.classes.cultivation import CultivationProgress
|
||||
from src.classes.root import Root
|
||||
from src.classes.age import Age
|
||||
@@ -458,7 +459,8 @@ class AvatarFactory:
|
||||
|
||||
# 宗门弟子天生知道宗门总部位置
|
||||
if avatar.sect is not None:
|
||||
hq_region = resolve_region(world, avatar.sect.headquarter.name)
|
||||
res = resolve_query(avatar.sect.headquarter.name, world, expected_types=[Region])
|
||||
hq_region = res.obj
|
||||
avatar.known_regions.add(hq_region.id)
|
||||
|
||||
if avatar.technique is not None:
|
||||
|
||||
@@ -1,49 +1,192 @@
|
||||
from typing import Any, Tuple, Optional
|
||||
from __future__ import annotations
|
||||
from typing import Any, Type, Optional, List, Union
|
||||
from dataclasses import dataclass
|
||||
|
||||
from src.classes.normalize import normalize_goods_name
|
||||
from src.classes.elixir import elixirs_by_name
|
||||
from src.classes.weapon import weapons_by_name
|
||||
from src.classes.auxiliary import auxiliaries_by_name
|
||||
from src.classes.item import items_by_name
|
||||
from src.classes.normalize import normalize_goods_name, normalize_name, normalize_avatar_name
|
||||
from src.classes.elixir import elixirs_by_name, Elixir
|
||||
from src.classes.weapon import weapons_by_name, Weapon
|
||||
from src.classes.auxiliary import auxiliaries_by_name, Auxiliary
|
||||
from src.classes.item import items_by_name, Item
|
||||
from src.classes.cultivation import Realm
|
||||
|
||||
def resolve_goods_by_name(target_name: str) -> Tuple[Any, str, str]:
|
||||
@dataclass
|
||||
class ResolutionResult:
|
||||
"""解析结果封装"""
|
||||
obj: Any | None # 解析出的对象实例
|
||||
resolved_type: Type | None # 对象的类型类
|
||||
is_valid: bool # 是否成功
|
||||
error_msg: str = "" # 错误信息
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""尝试获取对象的名称"""
|
||||
if self.obj and hasattr(self.obj, "name"):
|
||||
return self.obj.name
|
||||
return ""
|
||||
|
||||
def resolve_query(
|
||||
query: Any,
|
||||
world: Any = None,
|
||||
expected_types: Optional[list[Type]] = None
|
||||
) -> ResolutionResult:
|
||||
"""
|
||||
解析物品名称,返回 (对象, 类型, 显示名称)。
|
||||
如果未找到,返回 (None, "unknown", normalized_name)。
|
||||
统一解析入口。
|
||||
|
||||
类型字符串: "elixir", "item", "weapon", "auxiliary", "unknown"
|
||||
|
||||
查找顺序:
|
||||
1. 丹药 (Elixir)
|
||||
2. 兵器 (Weapon)
|
||||
3. 辅助装备 (Auxiliary)
|
||||
4. 普通物品 (Item)
|
||||
Args:
|
||||
query: 待解析的对象(字符串名 或 直接的对象实例)
|
||||
world: 世界对象上下文(用于查找 Avatar, Region)
|
||||
expected_types: 期望的类型列表。如果提供,将优先或仅尝试这些类型。
|
||||
支持的类型: Item, Weapon, Elixir, Auxiliary, Region, Avatar, Realm 等类对象
|
||||
必须直接传入类对象,不再支持字符串名。
|
||||
"""
|
||||
normalized_name = normalize_goods_name(target_name)
|
||||
if query is None:
|
||||
return ResolutionResult(None, None, False, "查询为空")
|
||||
|
||||
# 0. 快速通道:如果已经是期望的对象实例
|
||||
if expected_types:
|
||||
for t in expected_types:
|
||||
if isinstance(query, t):
|
||||
return _success(query, t)
|
||||
|
||||
# 1. 尝试作为丹药查找
|
||||
if normalized_name in elixirs_by_name:
|
||||
# elixirs_by_name 返回的是 list,我们取第一个作为对象
|
||||
# 注意:对于购买/显示信息来说,取第一个通常是没问题的,
|
||||
# 但如果有特定逻辑需要区分同名不同境界的丹药,可能需要更精细的处理。
|
||||
# 这里保持原有逻辑。
|
||||
elixir = elixirs_by_name[normalized_name][0]
|
||||
return elixir, "elixir", elixir.name
|
||||
# 如果不是字符串,且未命中上面的快速通道,可能输入了错误的对象
|
||||
# 严格模式:既然未命中 expected_types 中的任何类型,也必然无法通过下面的字符串查找逻辑。
|
||||
# 直接返回失败,除非 query 是字符串可以尝试查找。
|
||||
if not isinstance(query, str):
|
||||
return ResolutionResult(None, None, False, f"输入类型不支持且非字符串: {type(query)}")
|
||||
|
||||
# 2. 尝试作为兵器查找
|
||||
weapon = weapons_by_name.get(normalized_name)
|
||||
if weapon:
|
||||
return weapon, "weapon", weapon.name
|
||||
if not query:
|
||||
return ResolutionResult(None, None, False, "查询字符串为空")
|
||||
|
||||
# 3. 尝试作为辅助装备查找
|
||||
auxiliary = auxiliaries_by_name.get(normalized_name)
|
||||
if auxiliary:
|
||||
return auxiliary, "auxiliary", auxiliary.name
|
||||
# 准备检查列表
|
||||
checks = []
|
||||
|
||||
# 如果没有指定期望类型,则检查所有
|
||||
if not expected_types:
|
||||
checks = ["realm", "goods", "region", "avatar"]
|
||||
else:
|
||||
# 根据期望类型构建检查顺序
|
||||
for t in expected_types:
|
||||
# 必须是类对象
|
||||
if not isinstance(t, type):
|
||||
# 这里可以抛出异常,或者忽略。既然是重构,假设调用方已经修正。
|
||||
continue
|
||||
|
||||
# 4. 尝试作为普通物品查找
|
||||
item = items_by_name.get(normalized_name)
|
||||
if item:
|
||||
return item, "item", item.name
|
||||
t_name = t.__name__
|
||||
|
||||
if t_name in ["Item", "Weapon", "Elixir", "Auxiliary"]:
|
||||
if "goods" not in checks: checks.append("goods")
|
||||
elif t_name == "Region" or t_name == "CityRegion" or t_name == "SectRegion" or t_name == "CultivateRegion":
|
||||
if "region" not in checks: checks.append("region")
|
||||
elif t_name == "Avatar":
|
||||
if "avatar" not in checks: checks.append("avatar")
|
||||
elif t_name == "Realm":
|
||||
if "realm" not in checks: checks.append("realm")
|
||||
|
||||
return None, "unknown", normalized_name
|
||||
# 执行检查
|
||||
for check in checks:
|
||||
if check == "realm":
|
||||
res = _resolve_realm(query) # Realm 通常不需要 normalize 太多,或者在内部处理
|
||||
if res: return _success(res, Realm)
|
||||
|
||||
elif check == "goods":
|
||||
# 物品解析
|
||||
obj = _resolve_goods(query)
|
||||
if obj: return _success(obj, type(obj))
|
||||
|
||||
elif check == "region" and world:
|
||||
obj = _resolve_region(query, world)
|
||||
if obj: return _success(obj, type(obj))
|
||||
|
||||
elif check == "avatar" and world:
|
||||
obj = _resolve_avatar(query, world)
|
||||
if obj: return _success(obj, type(obj))
|
||||
|
||||
return ResolutionResult(None, None, False, f"无法解析: {query}")
|
||||
|
||||
def _success(obj: Any, type_cls: Type) -> ResolutionResult:
|
||||
return ResolutionResult(obj, type_cls, True)
|
||||
|
||||
# --- 内部具体的解析逻辑 ---
|
||||
|
||||
def _resolve_goods(name: str) -> Any | None:
|
||||
"""解析物品/装备/丹药"""
|
||||
norm = normalize_goods_name(name)
|
||||
|
||||
# 1. 丹药 (返回列表中的第一个)
|
||||
if norm in elixirs_by_name:
|
||||
return elixirs_by_name[norm][0]
|
||||
|
||||
# 2. 兵器
|
||||
if norm in weapons_by_name:
|
||||
return weapons_by_name[norm]
|
||||
|
||||
# 3. 辅助
|
||||
if norm in auxiliaries_by_name:
|
||||
return auxiliaries_by_name[norm]
|
||||
|
||||
# 4. 普通物品
|
||||
if norm in items_by_name:
|
||||
return items_by_name[norm]
|
||||
|
||||
return None
|
||||
|
||||
def _resolve_realm(name: str) -> Realm | None:
|
||||
"""解析境界"""
|
||||
try:
|
||||
# 尝试直接匹配值
|
||||
return Realm(name)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 尝试匹配枚举名
|
||||
for r in Realm:
|
||||
if r.name == name or r.value == name:
|
||||
return r
|
||||
return None
|
||||
|
||||
def _resolve_region(name: str, world: Any) -> Any | None:
|
||||
"""解析区域"""
|
||||
if not hasattr(world, 'map'):
|
||||
return None
|
||||
|
||||
# 1. 精确匹配
|
||||
by_name = getattr(world.map, "region_names", {})
|
||||
if name in by_name:
|
||||
return by_name[name]
|
||||
|
||||
# 2. 规范化匹配
|
||||
norm = normalize_name(name)
|
||||
if norm in by_name:
|
||||
return by_name[norm]
|
||||
|
||||
# 3. 包含匹配 (如果有唯一解)
|
||||
candidates = [k for k in by_name.keys() if (norm in k) or (name in k)]
|
||||
if len(candidates) == 1:
|
||||
return by_name[candidates[0]]
|
||||
|
||||
# 4. 宗门名称匹配 (解析到宗门驻地)
|
||||
# 避免循环引用,这里动态导入或假设 sects_by_name 可用
|
||||
from src.classes.sect import sects_by_name
|
||||
sect = sects_by_name.get(name) or sects_by_name.get(norm)
|
||||
if sect:
|
||||
sect_regions = getattr(world.map, "sect_regions", {})
|
||||
matched = [r for r in sect_regions.values() if getattr(r, "sect_name", None) == sect.name]
|
||||
if len(matched) == 1:
|
||||
return matched[0]
|
||||
|
||||
return None
|
||||
|
||||
def _resolve_avatar(name: str, world: Any) -> Any | None:
|
||||
"""解析角色"""
|
||||
if not hasattr(world, 'avatar_manager'):
|
||||
return None
|
||||
|
||||
norm = normalize_avatar_name(name)
|
||||
|
||||
# 遍历查找 (性能注意:如果角色极多可能需要优化为字典查找)
|
||||
# 假设 avatar_manager.avatars 是 dict[id, Avatar]
|
||||
for avatar in world.avatar_manager.avatars.values():
|
||||
if avatar.name == norm:
|
||||
return avatar
|
||||
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user