update death

This commit is contained in:
bridge
2025-12-01 02:05:11 +08:00
parent f047251c0d
commit 39f158bbe8
18 changed files with 185 additions and 66 deletions

View File

@@ -9,6 +9,7 @@ from src.classes.battle import decide_battle, get_assassination_success_rate
from src.classes.story_teller import StoryTeller
from src.classes.normalize import normalize_avatar_name
from src.classes.death import handle_death
from src.classes.death_reason import DeathReason
from src.classes.kill_and_grab import kill_and_grab
if TYPE_CHECKING:
@@ -116,7 +117,7 @@ class Assassinate(InstantAction):
story_event = Event(self.world.month_stamp, story, related_avatars=rel_ids, is_story=True)
# 死亡清理
handle_death(self.world, target)
handle_death(self.world, target, DeathReason.BATTLE)
return [result_event, story_event]
@@ -153,7 +154,7 @@ class Assassinate(InstantAction):
story_event = Event(self.world.month_stamp, story, related_avatars=rel_ids, is_story=True)
if is_fatal:
handle_death(self.world, loser)
handle_death(self.world, loser, DeathReason.BATTLE)
return [result_event, story_event]

View File

@@ -7,6 +7,7 @@ from src.classes.battle import decide_battle, get_effective_strength_pair
from src.classes.story_teller import StoryTeller
from src.classes.normalize import normalize_avatar_name
from src.classes.death import handle_death
from src.classes.death_reason import DeathReason
from src.classes.kill_and_grab import kill_and_grab
class Attack(InstantAction):
@@ -109,6 +110,6 @@ class Attack(InstantAction):
# 如果死亡,执行死亡清理(在故事生成后,保证关系数据可用)
if is_fatal:
handle_death(self.world, loser)
handle_death(self.world, loser, DeathReason.BATTLE)
return [result_event, story_event]

View File

@@ -84,7 +84,8 @@ class Age:
years_over_lifespan = self.age - expected
# 基础概率每超过1年增加0.01的概率
death_probability = min(years_over_lifespan * 0.01, 0.1)
prob_add = 0.01
death_probability = min(years_over_lifespan * prob_add, 0.1)
return death_probability

View File

@@ -114,6 +114,12 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin):
nickname: Optional[Nickname] = None
# 自定义头像ID如果设置优先使用此ID显示头像
custom_pic_id: Optional[int] = None
# 死亡状态
is_dead: bool = False
# 死亡信息:{ "time": MonthStamp, "reason": str, "location": (x, y) }
death_info: Optional[dict] = None
# 当月/当步新设动作标记:在 commit_next_plan 设为 True首次 tick_action 后清为 False
_new_action_set_this_step: bool = False
# 动作冷却:记录动作类名 -> 上次完成月戳
@@ -122,6 +128,8 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin):
def join_sect(self, sect: Sect, rank: "SectRank") -> None:
"""加入宗门"""
if self.is_dead:
return
if self.sect:
self.leave_sect()
self.sect = sect
@@ -137,6 +145,38 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin):
self.sect = None
self.sect_rank = None
def set_dead(self, reason: str, time: MonthStamp) -> None:
"""
设置角色死亡状态。
Args:
reason: 死亡原因
time: 死亡时间
"""
if self.is_dead:
return
self.is_dead = True
self.death_info = {
"time": int(time),
"reason": reason,
"location": (self.pos_x, self.pos_y)
}
# 清空所有计划和当前动作
self.planned_actions.clear()
self.current_action = None
self._pending_events.clear()
self.thinking = ""
self.short_term_objective = ""
# 退出宗门(保留职位记录还是清除?通常死人不再担任职位)
# 但为了历史记录,也许可以保留 sect 字段,但从宗门成员列表中移除
if self.sect:
self.sect.remove_member(self)
# 不清除 self.sect 和 self.sect_rank作为生平记录保留
def __post_init__(self):
"""
在Avatar创建后自动初始化tile和HP
@@ -319,6 +359,8 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin):
"long_term_objective": self.long_term_objective.content if self.long_term_objective else "",
"nickname": self.nickname.value if self.nickname else None,
"nickname_reason": self.nickname.reason if self.nickname else None,
"is_dead": self.is_dead,
"death_info": self.death_info,
}
# 复杂对象结构化

View File

@@ -26,6 +26,12 @@ class AvatarManager:
same_region.append(other)
return same_region
def get_living_avatars(self) -> List["Avatar"]:
"""
返回所有存活的角色列表。
"""
return [avatar for avatar in self.avatars.values() if not avatar.is_dead]
def get_observable_avatars(self, avatar: "Avatar") -> List["Avatar"]:
"""
返回处于 avatar 交互范围内的其他角色列表(不含自己)。

View File

@@ -1,16 +1,27 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Union
from src.classes.death_reason import DeathReason
if TYPE_CHECKING:
from src.classes.world import World
from src.classes.avatar import Avatar
def handle_death(world: World, avatar: Avatar) -> None:
def handle_death(world: World, avatar: Avatar, reason: Union[str, DeathReason] = DeathReason.UNKNOWN) -> None:
"""
处理角色死亡的统一入口。
负责将角色从世界管理器中移除,并处理相关的清理工作(如关系解除已在 remove_avatar 中实现)
注意:本函数不负责生成死亡事件文本,调用者应在调用前生成相应的 Event。
负责将角色标记为死亡,清理行动队列,但保留角色数据
Args:
world: 世界对象
avatar: 死亡的角色
reason: 死亡原因DeathReason枚举或字符串
"""
# 从管理器中移除角色remove_avatar 内部会自动清理双向关系)
world.avatar_manager.remove_avatar(avatar.id)
# 如果传入的是枚举,转为字符串值
reason_str = reason.value if isinstance(reason, DeathReason) else str(reason)
# 标记为死亡(软删除)
avatar.set_dead(reason_str, world.month_stamp)
# 可以在这里触发其他逻辑,比如检查是否有继承人等

View File

@@ -0,0 +1,11 @@
from enum import Enum
class DeathReason(Enum):
OLD_AGE = "老死"
BATTLE = "战死"
SERIOUS_INJURY = "重伤"
UNKNOWN = "未知"
def __str__(self) -> str:
return self.value

View File

@@ -255,16 +255,19 @@ async def game_loop():
# 执行一步
events = await sim.step()
# 找出新诞生的角色 ID
# 找出新诞生的角色 ID 和 刚死亡的角色 ID
newly_born_ids = set()
newly_dead_ids = set()
for e in events:
if "晋升为修士" in e.content and e.related_avatars:
newly_born_ids.update(e.related_avatars)
if ("身亡" in e.content or "老死" in e.content) and e.related_avatars:
newly_dead_ids.update(e.related_avatars)
avatar_updates = []
# 为了避免重复发送大量数据,我们区分处理:
# - 新角色:发送完整数据
# - 新角色/刚死角色:发送完整数据(或关键状态更新)
# - 旧角色:只发送位置 (x, y)(限制数量)
# 1. 发送新角色的完整信息
@@ -276,15 +279,29 @@ async def game_loop():
"name": a.name,
"x": int(getattr(a, "pos_x", 0)),
"y": int(getattr(a, "pos_y", 0)),
"gender": a.gender.value, # 使用 value (male/female) 而不是 str (中文)
"gender": a.gender.value,
"pic_id": resolve_avatar_pic_id(a),
"action": getattr(a, "current_action", {}).get("name", "发呆") if hasattr(a, "current_action") and a.current_action else "发呆"
"action": getattr(a, "current_action", {}).get("name", "发呆") if hasattr(a, "current_action") and a.current_action else "发呆",
"is_dead": False
})
# 2. 常规位置更新(暂时只发前 50 个旧角色,减少数据量)
# 2. 发送刚死角色的状态更新
for aid in newly_dead_ids:
# 注意:死人可能不在 get_living_avatars() 里,但还在 avatars 里
a = world.avatar_manager.avatars.get(aid)
if a:
avatar_updates.append({
"id": str(a.id),
"name": a.name, # 名字也带上,防止前端没数据
"is_dead": True,
"action": "已故"
})
# 3. 常规位置更新(暂时只发前 50 个旧角色,减少数据量)
limit = 50
count = 0
for a in world.avatar_manager.avatars.values():
# 只遍历活人更新位置
for a in world.avatar_manager.get_living_avatars():
# 如果是新角色,已经在上面处理过了,跳过
if a.id in newly_born_ids:
continue

View File

@@ -148,6 +148,10 @@ class AvatarLoadMixin:
# 恢复绰号
from src.classes.nickname_data import Nickname
avatar.nickname = Nickname.from_dict(data.get("nickname"))
# 恢复死亡状态
avatar.is_dead = data.get("is_dead", False)
avatar.death_info = data.get("death_info")
# 设置行动与AI
avatar.thinking = data.get("thinking", "")

View File

@@ -251,7 +251,10 @@ class MortalPlanner:
plan.pos_y = random.randint(0, world.map.height - 1)
if existing_avatars is None:
existing_avatars = list(world.avatar_manager.avatars.values())
existing_avatars = world.avatar_manager.get_living_avatars()
else:
existing_avatars = [av for av in existing_avatars if not av.is_dead]
if existed_sects is None:
try:
from src.classes.sect import sects_by_id as _sects_by_id

View File

@@ -90,6 +90,8 @@ class AvatarSaveMixin:
"persona_ids": [p.id for p in self.personas] if self.personas else [],
"appearance": self.appearance.level,
"nickname": self.nickname.to_dict() if self.nickname else None,
"is_dead": self.is_dead,
"death_info": self.death_info,
# 行动与AI
"current_action": current_action_dict,

View File

@@ -16,6 +16,7 @@ from src.classes.fortune import try_trigger_fortune
from src.classes.celestial_phenomenon import get_random_celestial_phenomenon
from src.classes.long_term_objective import process_avatar_long_term_objective
from src.classes.death import handle_death
from src.classes.death_reason import DeathReason
class Simulator:
def __init__(self, world: World):
@@ -28,7 +29,7 @@ class Simulator:
将 AI 的决策结果加载为角色的计划链。
"""
avatars_to_decide = []
for avatar in list(self.world.avatar_manager.avatars.values()):
for avatar in self.world.avatar_manager.get_living_avatars():
if avatar.current_action is None and not avatar.has_plans():
avatars_to_decide.append(avatar)
if not avatars_to_decide:
@@ -45,7 +46,7 @@ class Simulator:
提交阶段:为空闲角色提交计划中的下一个可执行动作,返回开始事件集合。
"""
events = []
for avatar in list(self.world.avatar_manager.avatars.values()):
for avatar in self.world.avatar_manager.get_living_avatars():
if avatar.current_action is None:
start_event = avatar.commit_next_plan()
if start_event is not None and not is_null_event(start_event):
@@ -60,7 +61,7 @@ class Simulator:
MAX_LOCAL_ROUNDS = 3
for _ in range(MAX_LOCAL_ROUNDS):
new_action_happened = False
for avatar_id, avatar in list(self.world.avatar_manager.avatars.items()):
for avatar in self.world.avatar_manager.get_living_avatars():
# 本轮执行前若标记为新设,则清理,执行后由 Avatar 再统一清除
if getattr(avatar, "_new_action_set_this_step", False):
new_action_happened = True
@@ -78,28 +79,30 @@ class Simulator:
def _phase_resolve_death(self):
"""
结算死亡:
- 战斗死亡已在 Action 中结算,此处不再重复(因为已从 avatars 中移除)
- 战斗死亡已在 Action 中结算
- 此时剩下的 avatars 都是存活的,只需检查非战斗因素(如老死、被动掉血)
"""
events = []
# 遍历时可能修改字典,使用 list() 复制
for avatar_id, avatar in list(self.world.avatar_manager.avatars.items()):
for avatar in self.world.avatar_manager.get_living_avatars():
is_dead = False
reason = ""
reason_str = ""
death_reason = DeathReason.UNKNOWN
# 优先判定重伤(可能是被动效果导致)
if avatar.hp <= 0:
if avatar.hp.cur <= 0: # 注意:这里应该是 avatar.hp.cur 或者 avatar.hp <= 0 取决于 HP 类的实现,原代码是 avatar.hp <= 0
is_dead = True
reason = f"{avatar.name} 因重伤不治身亡"
reason_str = f"{avatar.name} 因重伤不治身亡"
death_reason = DeathReason.SERIOUS_INJURY
# 其次判定寿元
elif avatar.death_by_old_age():
is_dead = True
reason = f"{avatar.name} 老死了,时年{avatar.age.get_age()}"
reason_str = f"{avatar.name} 老死了,时年{avatar.age.get_age()}"
death_reason = DeathReason.OLD_AGE
if is_dead:
event = Event(self.world.month_stamp, reason, related_avatars=[avatar.id])
event = Event(self.world.month_stamp, reason_str, related_avatars=[avatar.id])
events.append(event)
handle_death(self.world, avatar)
handle_death(self.world, avatar, death_reason)
return events
@@ -108,12 +111,13 @@ class Simulator:
更新存活角色年龄,并以一定概率生成新修士,返回期间产生的事件集合。
"""
events = []
for avatar_id, avatar in self.world.avatar_manager.avatars.items():
for avatar in self.world.avatar_manager.get_living_avatars():
avatar.update_age(self.world.month_stamp)
if random.random() < self.birth_rate:
age = random.randint(16, 60)
gender = random.choice(list(Gender))
name = get_random_name(gender)
# create_random_mortal 内部会获取 existing_avatars需要确保它处理活人
new_avatar = create_random_mortal(self.world, self.world.month_stamp, name, Age(age, Realm.Qi_Refinement))
self.world.avatar_manager.avatars[new_avatar.id] = new_avatar
event = Event(self.world.month_stamp, f"{new_avatar.name}晋升为修士了。", related_avatars=[new_avatar.id])
@@ -127,11 +131,12 @@ class Simulator:
- 触发奇遇(非动作)
"""
events = []
for avatar in self.world.avatar_manager.avatars.values():
living_avatars = self.world.avatar_manager.get_living_avatars()
for avatar in living_avatars:
avatar.update_time_effect()
# 使用 gather 并行触发奇遇
tasks = [try_trigger_fortune(avatar) for avatar in self.world.avatar_manager.avatars.values()]
tasks = [try_trigger_fortune(avatar) for avatar in living_avatars]
results = await asyncio.gather(*tasks)
for res in results:
if res:
@@ -146,7 +151,8 @@ class Simulator:
from src.classes.nickname import process_avatar_nickname
# 并发执行
tasks = [process_avatar_nickname(avatar) for avatar in self.world.avatar_manager.avatars.values()]
living_avatars = self.world.avatar_manager.get_living_avatars()
tasks = [process_avatar_nickname(avatar) for avatar in living_avatars]
results = await asyncio.gather(*tasks)
events = [e for e in results if e]
@@ -158,7 +164,8 @@ class Simulator:
检查角色是否需要生成/更新长期目标
"""
# 并发执行
tasks = [process_avatar_long_term_objective(avatar) for avatar in self.world.avatar_manager.avatars.values()]
living_avatars = self.world.avatar_manager.get_living_avatars()
tasks = [process_avatar_long_term_objective(avatar) for avatar in living_avatars]
results = await asyncio.gather(*tasks)
events = [e for e in results if e]