From 5590b834875f1511d52ba95821c10845c7bc9eb8 Mon Sep 17 00:00:00 2001 From: bridge Date: Wed, 10 Dec 2025 22:34:14 +0800 Subject: [PATCH] add occupy action --- src/classes/action/__init__.py | 1 + src/classes/action/cultivate.py | 5 ++ src/classes/action/move_to_direction.py | 2 +- src/classes/avatar.py | 4 -- src/classes/mutual_action/__init__.py | 3 + src/classes/mutual_action/occupy.py | 86 +++++++++++++++++++++++++ src/classes/region.py | 22 ++++++- src/sim/load/load_game.py | 10 +++ src/sim/save/save_game.py | 9 +++ src/sim/simulator.py | 73 +++++++++++++++++++++ 10 files changed, 209 insertions(+), 6 deletions(-) create mode 100644 src/classes/mutual_action/occupy.py diff --git a/src/classes/action/__init__.py b/src/classes/action/__init__.py index 864b036..960cf11 100644 --- a/src/classes/action/__init__.py +++ b/src/classes/action/__init__.py @@ -104,6 +104,7 @@ __all__ = [ "Assassinate", "MoveToDirection", # Talk 已移动到 mutual_action 模块 + # Occupy 已移动到 mutual_action 模块 ] diff --git a/src/classes/action/cultivate.py b/src/classes/action/cultivate.py index 45dd931..278b1c3 100644 --- a/src/classes/action/cultivate.py +++ b/src/classes/action/cultivate.py @@ -53,6 +53,11 @@ class Cultivate(TimedAction): return False, "修为已达瓶颈,无法继续修炼" if not isinstance(region, CultivateRegion): return False, "当前不在修炼区域" + + # 检查洞府所有权 + if region.host_avatar is not None and region.host_avatar != self.avatar: + return False, f"该洞府已被 {region.host_avatar.name} 占据,无法修炼" + if all(region.essence.get_density(et) == 0 for et in essence_types): return False, "当前区域无与灵根相符的灵气" return True, "" diff --git a/src/classes/action/move_to_direction.py b/src/classes/action/move_to_direction.py index d3b3d1e..223fbf2 100644 --- a/src/classes/action/move_to_direction.py +++ b/src/classes/action/move_to_direction.py @@ -111,4 +111,4 @@ class MoveToDirection(DefineAction, ActualActionMixin): return [Event(self.world.month_stamp, f"{self.avatar.name} 结束了向{direction_cn}方的探索", related_avatars=[self.avatar.id])] def _execute(self, *args, **kwargs): - pass + pass \ No newline at end of file diff --git a/src/classes/avatar.py b/src/classes/avatar.py index d632b54..d3b6b57 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -593,10 +593,6 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): # 当前无动作时才清除标记,避免清除新提交动作的标记 self._new_action_set_this_step = False - # 每次执行动作后,更新已知区域 - if self.tile and self.tile.region: - self.known_regions.add(self.tile.region.id) - return events def update_cultivation(self, new_level: int): diff --git a/src/classes/mutual_action/__init__.py b/src/classes/mutual_action/__init__.py index c9ad0de..2e6da2c 100644 --- a/src/classes/mutual_action/__init__.py +++ b/src/classes/mutual_action/__init__.py @@ -9,6 +9,7 @@ from .talk import Talk from .impart import Impart from .gift_spirit_stone import GiftSpiritStone from .spar import Spar +from .occupy import Occupy from src.classes.action.registry import register_action __all__ = [ @@ -21,6 +22,7 @@ __all__ = [ "Impart", "GiftSpiritStone", "Spar", + "Occupy", ] # 注册 mutual actions(均为实际动作) @@ -32,5 +34,6 @@ register_action(actual=True)(Talk) register_action(actual=True)(Impart) register_action(actual=True)(GiftSpiritStone) register_action(actual=True)(Spar) +register_action(actual=True)(Occupy) diff --git a/src/classes/mutual_action/occupy.py b/src/classes/mutual_action/occupy.py new file mode 100644 index 0000000..946218f --- /dev/null +++ b/src/classes/mutual_action/occupy.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +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.region import resolve_region, CultivateRegion +from src.classes.action_runtime import ActionResult, ActionStatus + +if TYPE_CHECKING: + from src.classes.avatar import Avatar + from src.classes.world import World + +@register_action(actual=True) +class Occupy(MutualAction): + """ + 占据动作(互动版): + 占据指定的洞府。如果是无主洞府直接占据;如果是有主洞府,则发起抢夺。 + """ + ACTION_NAME = "Occupy" + COMMENT = "占据或抢夺洞府" + + # 参数:洞府名称 + PARAMS = {"region_name": "str"} + + # 对方的反馈选项(仅在抢夺时有效) + FEEDBACK_ACTIONS = ["Yield", "Reject"] + + # 反馈对应的中文描述 + FEEDBACK_LABELS = { + "Yield": "让步", + "Reject": "拒绝", + } + + # 是大事 + IS_MAJOR = True + + def _get_region_and_host(self, region_name: str) -> tuple[CultivateRegion | None, Avatar | None, str]: + """ + 解析区域并获取主人 + """ + try: + region = resolve_region(self.world, region_name) + except Exception as e: + return None, None, f"无法找到区域:{region_name}" + + if not isinstance(region, CultivateRegion): + return None, None, f"{region.name} 不是修炼区域,无法占据" + + return region, region.host_avatar, "" + + def can_start(self, region_name: str) -> tuple[bool, str]: + region, host, err = self._get_region_and_host(region_name) + if err: + return False, err + + if region.host_avatar == self.avatar: + return False, "已经是该洞府的主人了" + + return super().can_start(target_avatar=host) + + def start(self, region_name: str) -> Event: + region, host, _ = self._get_region_and_host(region_name) + return super().start(target_avatar=host) + + def step(self, region_name: str) -> ActionResult: + region, host, _ = self._get_region_and_host(region_name) + return super().step(target_avatar=host) + + def _settle_feedback(self, target_avatar: "Avatar", feedback_name: str) -> None: + """ + 处理反馈结果 + """ + region = self.avatar.tile.region + if feedback_name == "Yield": + # 对方让步:转移所有权 + region.host_avatar = self.avatar + + # 记录事件 + self.avatar.add_event(self.create_event(f"成功从 {target_avatar.name} 手中夺取了 {region.name}", related_avatars=[target_avatar.id])) + target_avatar.add_event(Event(self.world.month_stamp, f"面对 {self.avatar.name} 的逼迫,不得不让出了 {region.name}", related_avatars=[self.avatar.id], is_major=True)) + + elif feedback_name == "Reject": + # 对方拒绝:所有权不变 + self.avatar.add_event(self.create_event(f"试图抢夺 {region.name},但被 {target_avatar.name} 拒绝", related_avatars=[target_avatar.id])) + target_avatar.add_event(Event(self.world.month_stamp, f"拒绝了 {self.avatar.name} 对 {region.name} 的抢夺要求", related_avatars=[self.avatar.id], is_major=True)) diff --git a/src/classes/region.py b/src/classes/region.py index e78b6b4..61c03c8 100644 --- a/src/classes/region.py +++ b/src/classes/region.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field -from typing import Union, TypeVar, Type, Optional +from typing import Union, TypeVar, Type, Optional, TYPE_CHECKING from enum import Enum from abc import ABC, abstractmethod @@ -11,6 +11,10 @@ from src.classes.animal import Animal, animals_by_id from src.classes.plant import Plant, plants_by_id from src.classes.sect import sects_by_name +if TYPE_CHECKING: + from src.classes.avatar import Avatar + + @dataclass class Region(ABC): @@ -173,6 +177,9 @@ class CultivateRegion(Region): essence_type: EssenceType = EssenceType.GOLD # 默认值避免 dataclass 继承错误 essence_density: int = 0 essence: Essence = field(init=False) + + # 洞府主人:默认为空(无主) + host_avatar: Optional["Avatar"] = field(default=None, init=False) def __post_init__(self): super().__post_init__() @@ -193,6 +200,10 @@ class CultivateRegion(Region): lines = super().get_hover_info() stars = "★" * self.essence_density + "☆" * (10 - self.essence_density) lines.append(f"主要灵气: {self.essence_type} {stars}") + if self.host_avatar: + lines.append(f"主人: {self.host_avatar.name}") + else: + lines.append("主人: 无(可占据)") return lines def get_structured_info(self) -> dict: @@ -202,6 +213,15 @@ class CultivateRegion(Region): "type": str(self.essence_type), "density": self.essence_density } + + if self.host_avatar: + info["host"] = { + "id": self.host_avatar.id, + "name": self.host_avatar.name + } + else: + info["host"] = None + return info diff --git a/src/sim/load/load_game.py b/src/sim/load/load_game.py index 8c38e28..469384f 100644 --- a/src/sim/load/load_game.py +++ b/src/sim/load/load_game.py @@ -124,6 +124,16 @@ def load_game(save_path: Optional[Path] = None) -> Tuple["World", "Simulator", L # 将所有avatar添加到world world.avatar_manager.avatars = all_avatars + # 恢复洞府主人关系 + cultivate_regions_hosts = world_data.get("cultivate_regions_hosts", {}) + from src.classes.region import CultivateRegion + for rid_str, avatar_id in cultivate_regions_hosts.items(): + rid = int(rid_str) + if rid in game_map.regions: + region = game_map.regions[rid] + if isinstance(region, CultivateRegion) and avatar_id in all_avatars: + region.host_avatar = all_avatars[avatar_id] + # 重建宗门成员关系与功法列表 from src.classes.technique import techniques_by_name diff --git a/src/sim/save/save_game.py b/src/sim/save/save_game.py index a3b1fb2..b6ec530 100644 --- a/src/sim/save/save_game.py +++ b/src/sim/save/save_game.py @@ -80,12 +80,21 @@ def save_game( } # 构建世界数据 + # 收集有主洞府信息 + from src.classes.region import CultivateRegion + cultivate_regions_hosts = {} + if hasattr(world.map, 'regions'): + for rid, region in world.map.regions.items(): + if isinstance(region, CultivateRegion) and region.host_avatar: + cultivate_regions_hosts[str(rid)] = region.host_avatar.id + world_data = { "month_stamp": int(world.month_stamp), "existed_sect_ids": [sect.id for sect in existed_sects], # 天地灵机 "current_phenomenon_id": world.current_phenomenon.id if world.current_phenomenon else None, "phenomenon_start_year": world.phenomenon_start_year if hasattr(world, 'phenomenon_start_year') else 0, + "cultivate_regions_hosts": cultivate_regions_hosts, } # 保存所有Avatar(第一阶段:不含relations) diff --git a/src/sim/simulator.py b/src/sim/simulator.py index 0525752..0d7270a 100644 --- a/src/sim/simulator.py +++ b/src/sim/simulator.py @@ -23,6 +23,75 @@ class Simulator: self.world = world self.birth_rate = CONFIG.game.npc_birth_rate_per_month # 从配置文件读取NPC每月出生率 + def _phase_update_perception_and_knowledge(self): + """ + 感知更新阶段: + 1. 基于感知范围更新 known_regions + 2. 自动占据无主洞府(如果自己没有洞府) + """ + from src.classes.observe import get_avatar_observation_radius + from src.classes.region import CultivateRegion + + # 1. 缓存当前有洞府的角色ID + avatars_with_home = set() + # 注意:这里我们只关心 CultivateRegion 的 host + # map.cultivate_regions 可能需要确保被正确初始化,如果没有,可以回退到遍历所有 regions + # 为了稳妥,遍历所有 Region 筛选 + cultivate_regions = [ + r for r in self.world.map.regions.values() + if isinstance(r, CultivateRegion) + ] + + for r in cultivate_regions: + if r.host_avatar: + avatars_with_home.add(r.host_avatar.id) + + # 2. 遍历所有存活角色 + for avatar in self.world.avatar_manager.get_living_avatars(): + # 计算感知半径(曼哈顿距离) + radius = get_avatar_observation_radius(avatar) + + # 扫描范围内的坐标 + # 优化:只扫描半径内的坐标可能比遍历所有region快,也可能慢,取决于地图大小和半径 + # 地图可能很大,半径通常很小(<10),所以基于坐标扫描更优 + + # 获取范围内的有效坐标 + start_x = max(0, avatar.pos_x - radius) + end_x = min(self.world.map.width - 1, avatar.pos_x + radius) + start_y = max(0, avatar.pos_y - radius) + end_y = min(self.world.map.height - 1, avatar.pos_y + radius) + + # 收集感知到的区域 + observed_regions = set() + for x in range(start_x, end_x + 1): + for y in range(start_y, end_y + 1): + # 距离判定:曼哈顿距离 + if abs(x - avatar.pos_x) + abs(y - avatar.pos_y) <= radius: + tile = self.world.map.get_tile(x, y) + if tile.region: + observed_regions.add(tile.region) + + # 更新认知与自动占据 + for region in observed_regions: + # 更新 known_regions + avatar.known_regions.add(region.id) + + # 自动占据逻辑 + # 只有当:是修炼区域 + 无主 + 自己无洞府 时触发 + if isinstance(region, CultivateRegion): + if region.host_avatar is None: + if avatar.id not in avatars_with_home: + # 占据 + region.host_avatar = avatar + avatars_with_home.add(avatar.id) + # 记录事件 + event = Event( + self.world.month_stamp, + f"路过 {region.name},发现无主,将其占据。", + related_avatars=[avatar.id] + ) + avatar.add_event(event) + async def _phase_decide_actions(self): """ 决策阶段:仅对需要新计划的角色调用 AI(当前无动作且无计划), @@ -242,6 +311,10 @@ class Simulator: """ events = [] # list of Event + # 0. 感知与认知更新阶段(包括自动占据洞府) + # 在思考和决策之前,先让角色感知世界 + self._phase_update_perception_and_knowledge() + # 0.5 长期目标思考阶段(在决策之前) events.extend(await self._phase_long_term_objective_thinking())