diff --git a/EN_README.md b/EN_README.md index aa5bc8a..292ec41 100644 --- a/EN_README.md +++ b/EN_README.md @@ -206,7 +206,7 @@ You can also join the QQ group for discussion: 1071821688. Verification answer i ### 🏛️ World Lore - ✅ Inject basic world knowledge - [ ] Dynamic worldview generation -- [ ] Dynamic history generation +- [ ] Dynamic generation of techniques, equipment, sects, and maps based on user input history ### Specials - ✅ Fortuitous encounters @@ -225,7 +225,6 @@ You can also join the QQ group for discussion: 1071821688. Verification answer i - [ ] Become a Legend of Later Ages ### 🔭 Long-term -- [ ] ECS parallel toolkit - [ ] Novelization/imagery/video for history and events - [ ] Avatar calling MCP tools on their own diff --git a/README.md b/README.md index 3a41f06..c618e6e 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ ### 🏛️ 世界背景系统 - ✅ 注入基础世界知识 - [ ] 动态世界观生成 -- [ ] 动态历史生成 +- [ ] 基于用户输入历史的动态功法、装备、宗门、地图生成 ### 特殊 - ✅ 奇遇 @@ -225,7 +225,6 @@ - [ ] 成为后世传奇 ### 🔭 远期展望 -- [ ] ECS并行 - [ ] 历史/事件的小说化&图片化&视频化 - [ ] MCP agent化,修士自行调用工具 diff --git a/src/classes/avatar_manager.py b/src/classes/avatar_manager.py index 26a770e..2b062bc 100644 --- a/src/classes/avatar_manager.py +++ b/src/classes/avatar_manager.py @@ -1,6 +1,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Dict, List, TYPE_CHECKING +from typing import Dict, List, TYPE_CHECKING, Iterable +import itertools if TYPE_CHECKING: from src.classes.avatar import Avatar @@ -9,22 +10,39 @@ from src.classes.observe import get_observable_avatars @dataclass class AvatarManager: + # 仅存储存活的角色,用于主循环遍历 avatars: Dict[str, "Avatar"] = field(default_factory=dict) + # 存储已死亡的角色(归档) + dead_avatars: Dict[str, "Avatar"] = field(default_factory=dict) def get_avatar(self, avatar_id: str) -> "Avatar | None": """ - 根据 ID 获取角色对象 + 根据 ID 获取角色对象,优先查找活人,再查找死者 """ - return self.avatars.get(str(avatar_id)) + aid = str(avatar_id) + return self.avatars.get(aid) or self.dead_avatars.get(aid) + + def handle_death(self, avatar_id: str) -> None: + """ + 处理角色死亡:将角色从活跃列表移动到墓地 + """ + aid = str(avatar_id) + if aid in self.avatars: + avatar = self.avatars.pop(aid) + self.dead_avatars[aid] = avatar + # 断开地图连接,确保不出现在地图网格上 + if hasattr(avatar, "tile"): + avatar.tile = None def get_avatars_in_same_region(self, avatar: "Avatar") -> List["Avatar"]: """ - 返回与给定 avatar 处于同一区域的其他角色列表(不含自己)。 + 返回与给定 avatar 处于同一区域的其他【存活】角色列表(不含自己)。 """ if avatar is None or getattr(avatar, "tile", None) is None or avatar.tile.region is None: return [] region = avatar.tile.region same_region: list["Avatar"] = [] + # 只遍历活人 for other in self.avatars.values(): if other is avatar or getattr(other, "tile", None) is None: continue @@ -35,35 +53,46 @@ class AvatarManager: def get_living_avatars(self) -> List["Avatar"]: """ 返回所有存活的角色列表。 + 由于 avatars 现在只存活人,直接返回 values 即可。 """ - return [avatar for avatar in self.avatars.values() if not avatar.is_dead] + return list(self.avatars.values()) def get_observable_avatars(self, avatar: "Avatar") -> List["Avatar"]: """ - 返回处于 avatar 交互范围内的其他角色列表(不含自己)。 - 基于曼哈顿距离与境界映射的感知半径过滤。 + 返回处于 avatar 交互范围内的其他【存活】角色列表(不含自己)。 """ return get_observable_avatars(avatar, self.avatars.values()) + + def _iter_all_avatars(self) -> Iterable["Avatar"]: + """辅助方法:遍历所有角色(活人+死者)""" + return itertools.chain(self.avatars.values(), self.dead_avatars.values()) def remove_avatar(self, avatar_id: str) -> None: """ - 从管理器中删除一个 avatar,并清理所有与其相关的双向关系。 + 从管理器中彻底删除一个 avatar(无论是死是活),并清理所有与其相关的双向关系。 + 此操作不可逆。 """ - avatar = self.avatars.get(avatar_id) + aid = str(avatar_id) + avatar = self.get_avatar(aid) + if avatar is None: return - # 先清理与其直接记录的关系(会保持对称) + + # 1. 清理与其直接记录的关系 related = list(getattr(avatar, "relations", {}).keys()) for other in related: avatar.clear_relation(other) - # 再次扫一遍所有 avatar,确保不存在残留引用 - for other in list(self.avatars.values()): + + # 2. 扫一遍所有角色(含死者),确保清除反向引用 + for other in self._iter_all_avatars(): if other is avatar: continue if getattr(other, "relations", None) is not None and avatar in other.relations: other.clear_relation(avatar) - # 最后移除自身 - self.avatars.pop(avatar_id, None) + + # 3. 移除自身 + self.avatars.pop(aid, None) + self.dead_avatars.pop(aid, None) def remove_avatars(self, avatar_ids: List[str]) -> None: """ @@ -71,5 +100,3 @@ class AvatarManager: """ for aid in list(avatar_ids): self.remove_avatar(aid) - - diff --git a/src/server/main.py b/src/server/main.py index ba63ad3..f877907 100644 --- a/src/server/main.py +++ b/src/server/main.py @@ -402,8 +402,8 @@ async def game_loop(): # 2. 发送刚死角色的状态更新 for aid in newly_dead_ids: - # 注意:死人可能不在 get_living_avatars() 里,但还在 avatars 里 - a = world.avatar_manager.avatars.get(aid) + # 使用 get_avatar 以兼容死者查询 + a = world.avatar_manager.get_avatar(aid) if a: avatar_updates.append({ "id": str(a.id), @@ -411,6 +411,8 @@ async def game_loop(): "is_dead": True, "action": "已故" }) + # 将死者归档到墓地,从活跃列表移除 + world.avatar_manager.handle_death(aid) # 3. 常规位置更新(暂时只发前 50 个旧角色,减少数据量) limit = 50 @@ -723,7 +725,7 @@ def get_hover_info( target = None if target_type == "avatar": - target = world.avatar_manager.avatars.get(target_id) + target = world.avatar_manager.get_avatar(target_id) elif target_type == "region": if world.map and hasattr(world.map, "regions"): regions = world.map.regions @@ -762,7 +764,7 @@ def get_detail_info( target = None if target_type == "avatar": - target = world.avatar_manager.avatars.get(target_id) + target = world.avatar_manager.get_avatar(target_id) elif target_type == "region": if world.map and hasattr(world.map, "regions"): regions = world.map.regions diff --git a/src/sim/load/load_game.py b/src/sim/load/load_game.py index 85fe621..c3f3a85 100644 --- a/src/sim/load/load_game.py +++ b/src/sim/load/load_game.py @@ -123,7 +123,12 @@ def load_game(save_path: Optional[Path] = None) -> Tuple["World", "Simulator", L avatar.relations[other_avatar] = relation # 将所有avatar添加到world - world.avatar_manager.avatars = all_avatars + # 根据生死状态分流 + for avatar in all_avatars.values(): + if avatar.is_dead: + world.avatar_manager.dead_avatars[avatar.id] = avatar + else: + world.avatar_manager.avatars[avatar.id] = avatar # 恢复洞府主人关系 cultivate_regions_hosts = world_data.get("cultivate_regions_hosts", {}) diff --git a/src/sim/save/save_game.py b/src/sim/save/save_game.py index 048e6fe..12d6a6c 100644 --- a/src/sim/save/save_game.py +++ b/src/sim/save/save_game.py @@ -100,8 +100,9 @@ def save_game( } # 保存所有Avatar(第一阶段:不含relations) + # 需要保存活人和死者 avatars_data = [] - for avatar in world.avatar_manager.avatars.values(): + for avatar in world.avatar_manager._iter_all_avatars(): avatars_data.append(avatar.to_save_dict()) # 保存事件历史(限制数量) diff --git a/tests/test_death.py b/tests/test_death.py index c792a40..04c6a7d 100644 --- a/tests/test_death.py +++ b/tests/test_death.py @@ -81,6 +81,26 @@ def test_relation_display_with_death(base_world, dummy_avatar): strs_after = get_relations_strs(dummy_avatar) assert "朋友:Friend(已故:重伤不治身亡)" in strs_after + +def test_avatar_manager_archive_death(base_world, dummy_avatar): + """测试 AvatarManager 的死亡归档逻辑""" + manager = base_world.avatar_manager + manager.avatars[dummy_avatar.id] = dummy_avatar + + # 确保初始在活人表 + assert dummy_avatar.id in manager.avatars + assert dummy_avatar.id not in manager.dead_avatars + + # 执行归档 + manager.handle_death(dummy_avatar.id) + + # 验证位置转移 + assert dummy_avatar.id not in manager.avatars + assert dummy_avatar.id in manager.dead_avatars + + # 验证 get_avatar 依然能查到 + assert manager.get_avatar(dummy_avatar.id) == dummy_avatar + @pytest.mark.asyncio async def test_simulator_resolve_death(base_world, dummy_avatar): """测试模拟器的死亡结算阶段""" @@ -96,6 +116,10 @@ async def test_simulator_resolve_death(base_world, dummy_avatar): assert dummy_avatar.death_info["reason"] == "重伤不治身亡" assert len(events) > 0 assert "重伤不治身亡" in str(events[0]) + + # 注意:在 Simulator 的 phase 中,角色只是被标记死亡 + # 真正的归档发生在 main.py 循环中,或者我们可以手动触发 + # 这里我们只验证标记逻辑 @pytest.mark.asyncio async def test_simulator_evolve_relations_filter_dead(base_world, dummy_avatar, mock_llm_managers): @@ -130,19 +154,25 @@ async def test_simulator_evolve_relations_filter_dead(base_world, dummy_avatar, # 设置交互状态达到阈值 dummy_avatar.relation_interaction_states[target.id]["count"] = 100 - # 让 Target 死亡 + # 让 Target 死亡并归档(模拟真实流程) target.set_dead("测试死亡", base_world.month_stamp) + base_world.avatar_manager.handle_death(target.id) # 获取 mock_rr 用于验证调用 mock_run = mock_llm_managers["rr"] await sim._phase_evolve_relations() - # 验证:因为 target 已死,应该不会调用 run_batch + # 验证:因为 target 已死且归档,get_living_avatars 不会返回它,target 也不在活人列表里 + # 即使 get_avatar 能查到它,逻辑中应该有防守检查 mock_run.assert_not_called() # 如果 Target 活着,应该会调用 target.is_dead = False + # 复活:手动移回活人表 + if target.id in base_world.avatar_manager.dead_avatars: + base_world.avatar_manager.avatars[target.id] = base_world.avatar_manager.dead_avatars.pop(target.id) + mock_run.reset_mock() # 重置 mock 调用记录 mock_run.return_value = [] # AsyncMock 会自动将其 wrap 进 awaitable