add occupy action

This commit is contained in:
bridge
2025-12-10 22:34:14 +08:00
parent 2c69383112
commit 5590b83487
10 changed files with 209 additions and 6 deletions

View File

@@ -104,6 +104,7 @@ __all__ = [
"Assassinate",
"MoveToDirection",
# Talk 已移动到 mutual_action 模块
# Occupy 已移动到 mutual_action 模块
]

View File

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

View File

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

View File

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

View File

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

View File

@@ -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):
@@ -174,6 +178,9 @@ class CultivateRegion(Region):
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__()
essence_density_dict = {essence_type: 0 for essence_type in EssenceType}
@@ -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

View File

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

View File

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

View File

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