feat(avatar): implement region ownership management in AvatarManager and Avatar classes
- Added `owned_regions` attribute to the Avatar class to track regions owned by avatars. - Introduced `occupy_region` and `release_region` methods for managing region ownership and ensuring proper relationship handling. - Updated AvatarManager to clear relationships when an avatar is released, ensuring no lingering references. - Refactored region ownership logic in the Occupy action and Simulator to utilize the new methods for better clarity and maintainability. - Enhanced game loading process to establish ownership relationships correctly during game state restoration.
This commit is contained in:
@@ -11,6 +11,7 @@ from typing import Optional, List, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.sect_ranks import SectRank
|
||||
from src.classes.region import CultivateRegion
|
||||
|
||||
from src.classes.calendar import MonthStamp
|
||||
from src.classes.world import World
|
||||
@@ -133,6 +134,39 @@ class Avatar(
|
||||
# 关系交互计数器: key=target_id, value={"count": 0, "checked_times": 0}
|
||||
relation_interaction_states: dict[str, dict[str, int]] = field(default_factory=lambda: defaultdict(lambda: {"count": 0, "checked_times": 0}))
|
||||
|
||||
# 拥有的洞府列表(不参与序列化,通过 load_game 重建)
|
||||
owned_regions: List["CultivateRegion"] = field(default_factory=list, init=False)
|
||||
|
||||
def occupy_region(self, region: "CultivateRegion") -> None:
|
||||
"""
|
||||
占据一个洞府,处理双向绑定和旧主清理。
|
||||
"""
|
||||
# 如果已经是我的,无需操作
|
||||
if region.host_avatar == self:
|
||||
if region not in self.owned_regions:
|
||||
self.owned_regions.append(region)
|
||||
return
|
||||
|
||||
# 如果有旧主,先让旧主释放
|
||||
if region.host_avatar is not None:
|
||||
region.host_avatar.release_region(region)
|
||||
|
||||
# 建立新关系
|
||||
region.host_avatar = self
|
||||
if region not in self.owned_regions:
|
||||
self.owned_regions.append(region)
|
||||
|
||||
def release_region(self, region: "CultivateRegion") -> None:
|
||||
"""
|
||||
放弃一个洞府的所有权。
|
||||
"""
|
||||
if region in self.owned_regions:
|
||||
self.owned_regions.remove(region)
|
||||
|
||||
# 只有当 region 的主人确实是自己时才置空(防止误伤新主人)
|
||||
if region.host_avatar == self:
|
||||
region.host_avatar = None
|
||||
|
||||
def add_breakthrough_rate(self, rate: float, duration: int = 1) -> None:
|
||||
"""
|
||||
增加突破成功率(临时效果)
|
||||
@@ -259,6 +293,11 @@ class Avatar(
|
||||
self.thinking = ""
|
||||
self.short_term_objective = ""
|
||||
|
||||
# 释放所有拥有的洞府
|
||||
# 复制列表进行遍历,因为 release_region 会修改列表
|
||||
for region in list(self.owned_regions):
|
||||
self.release_region(region)
|
||||
|
||||
if self.sect:
|
||||
self.sect.remove_member(self)
|
||||
|
||||
|
||||
@@ -114,10 +114,12 @@ class AvatarManager:
|
||||
avatar.clear_relation(other)
|
||||
|
||||
# 2. 清理占据的洞府
|
||||
if getattr(avatar, "world", None) and hasattr(avatar.world, "map"):
|
||||
for region in avatar.world.map.regions.values():
|
||||
if getattr(region, "host_avatar", None) == avatar:
|
||||
if hasattr(avatar, "owned_regions") and avatar.owned_regions:
|
||||
for region in list(avatar.owned_regions):
|
||||
# 仅解除关系,不触发其他逻辑
|
||||
if region.host_avatar == avatar:
|
||||
region.host_avatar = None
|
||||
avatar.owned_regions.clear()
|
||||
|
||||
# 3. 扫一遍所有角色(含死者),确保清除反向引用
|
||||
for other in self._iter_all_avatars():
|
||||
|
||||
@@ -106,7 +106,7 @@ class Occupy(MutualAction):
|
||||
if feedback_name == "Yield":
|
||||
# 对方让步:直接转移所有权
|
||||
if region:
|
||||
region.host_avatar = self.avatar
|
||||
self.avatar.occupy_region(region)
|
||||
|
||||
# 共用一个事件
|
||||
event_text = t("{initiator} forced {target} to yield {region}",
|
||||
@@ -131,7 +131,7 @@ class Occupy(MutualAction):
|
||||
# 进攻方胜利则洞府易主
|
||||
attacker_won = winner == self.avatar
|
||||
if attacker_won and region:
|
||||
region.host_avatar = self.avatar
|
||||
self.avatar.occupy_region(region)
|
||||
|
||||
self._last_result = (winner, loser, loser_dmg, winner_dmg, region_name, attacker_won)
|
||||
|
||||
|
||||
@@ -261,7 +261,9 @@ def load_game(save_path: Optional[Path] = None) -> Tuple["World", "Simulator", L
|
||||
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]
|
||||
avatar = all_avatars[avatar_id]
|
||||
# 使用 occupy_region 建立双向绑定
|
||||
avatar.occupy_region(region)
|
||||
|
||||
# 重建宗门成员关系与功法列表
|
||||
from src.classes.technique import techniques_by_name
|
||||
|
||||
@@ -82,7 +82,7 @@ class Simulator:
|
||||
if region.host_avatar is None:
|
||||
if avatar.id not in avatars_with_home:
|
||||
# 占据
|
||||
region.host_avatar = avatar
|
||||
avatar.occupy_region(region)
|
||||
avatars_with_home.add(avatar.id)
|
||||
# 记录事件
|
||||
event = Event(
|
||||
|
||||
@@ -1,177 +1,240 @@
|
||||
"""
|
||||
测试 CSV 数据加载的正确性。
|
||||
验证代码中使用的列名与 CSV 文件中的实际列名匹配。
|
||||
采用动态多语言测试方案,不再硬编码特定语言的预期字符串。
|
||||
"""
|
||||
import pytest
|
||||
from src.classes.sect import sects_by_id, sects_by_name, Sect
|
||||
from src.classes.technique import techniques_by_id, techniques_by_name, Technique
|
||||
import csv
|
||||
from pathlib import Path
|
||||
from src.classes.sect import sects_by_id, sects_by_name, Sect, reload as reload_sects
|
||||
from src.classes.technique import techniques_by_id, techniques_by_name, Technique, reload as reload_techniques
|
||||
from src.classes.elixir import elixirs_by_id
|
||||
from src.utils.config import CONFIG
|
||||
from src.i18n import t, reload_translations
|
||||
from src.classes.language import language_manager
|
||||
|
||||
# --- Helpers ---
|
||||
|
||||
def read_raw_csv_as_dict(file_path):
|
||||
"""读取原始 CSV 文件,跳过描述行"""
|
||||
if not file_path.exists():
|
||||
return []
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8-sig') as f:
|
||||
lines = list(csv.reader(f))
|
||||
|
||||
if len(lines) < 1:
|
||||
return []
|
||||
|
||||
headers = lines[0]
|
||||
data = []
|
||||
|
||||
# Start from index 2 if there's a description row
|
||||
start_index = 2 if len(lines) > 1 else 1
|
||||
|
||||
for row_values in lines[start_index:]:
|
||||
if not row_values: continue
|
||||
row_dict = {}
|
||||
for i, h in enumerate(headers):
|
||||
if i < len(row_values):
|
||||
row_dict[h] = row_values[i]
|
||||
else:
|
||||
row_dict[h] = None
|
||||
data.append(row_dict)
|
||||
|
||||
return data
|
||||
|
||||
@pytest.fixture(params=["zh-CN", "zh-TW", "en-US"])
|
||||
def game_lang(request):
|
||||
"""
|
||||
参数化 Fixture:切换语言并重载游戏数据。
|
||||
测试结束后自动恢复回 zh-CN 环境。
|
||||
"""
|
||||
lang = request.param
|
||||
|
||||
# 1. Switch Language
|
||||
language_manager.set_language(lang)
|
||||
reload_translations()
|
||||
|
||||
# 2. Force Reload Game Data
|
||||
from src.utils.config import update_paths_for_language
|
||||
update_paths_for_language(lang)
|
||||
|
||||
from src.utils.df import reload_game_configs
|
||||
reload_game_configs()
|
||||
|
||||
reload_techniques()
|
||||
reload_sects()
|
||||
|
||||
yield lang
|
||||
|
||||
# Teardown: Restore to zh-CN for other tests
|
||||
language_manager.set_language("zh-CN")
|
||||
reload_translations()
|
||||
update_paths_for_language("zh-CN")
|
||||
reload_game_configs()
|
||||
reload_techniques()
|
||||
reload_sects()
|
||||
|
||||
|
||||
class TestSectLoading:
|
||||
"""测试宗门数据加载"""
|
||||
"""测试宗门数据加载 (多语言动态验证)"""
|
||||
|
||||
def test_sect_headquarter_name_loaded(self):
|
||||
"""测试宗门驻地名称正确加载(来自 sect_region.csv 的 name 列)"""
|
||||
# 不夜城 (sect_id=12) 的驻地应该是 "大千光极城"
|
||||
sect = sects_by_id.get(12)
|
||||
assert sect is not None, "宗门 ID=12 应该存在"
|
||||
def test_sect_headquarter_name_loaded(self, game_lang):
|
||||
"""测试宗门驻地名称正确加载"""
|
||||
# Read RAW Sect CSV
|
||||
sect_csv_path = CONFIG.paths.shared_game_configs / "sect.csv"
|
||||
raw_sects = read_raw_csv_as_dict(sect_csv_path)
|
||||
|
||||
# 兼容多语言环境:检查中文或英文名称
|
||||
expected_names = {"Sleepless City", "不夜城"}
|
||||
assert sect.name in expected_names, f"宗门名称 '{sect.name}' 不在预期列表中: {expected_names}"
|
||||
# Read RAW Sect Region CSV (Source of HQ names)
|
||||
region_csv_path = CONFIG.paths.shared_game_configs / "sect_region.csv"
|
||||
raw_regions = read_raw_csv_as_dict(region_csv_path)
|
||||
sect_region_map = {int(r['sect_id']): r for r in raw_regions if r.get('sect_id')}
|
||||
|
||||
expected_hqs = {"Daqian Aurora City", "大千光极城"}
|
||||
assert sect.headquarter.name in expected_hqs, (
|
||||
f"驻地名称 '{sect.headquarter.name}' 不在预期列表中: {expected_hqs}"
|
||||
)
|
||||
|
||||
def test_sect_headquarter_desc_loaded(self):
|
||||
"""测试宗门驻地描述正确加载(来自 sect_region.csv 的 desc 列)"""
|
||||
sect = sects_by_id.get(12)
|
||||
# Verify specific Sect (ID=12, 不夜城)
|
||||
target_id = 12
|
||||
sect = sects_by_id.get(target_id)
|
||||
assert sect is not None
|
||||
# 验证描述不为空且包含关键词 (兼容中英文)
|
||||
assert sect.headquarter.desc, "驻地描述不应为空"
|
||||
|
||||
desc = sect.headquarter.desc.lower()
|
||||
# 1. Verify Sect Name
|
||||
sect_row = next((r for r in raw_sects if int(r['id']) == target_id), None)
|
||||
assert sect_row
|
||||
|
||||
# 简单宽松的检查:只要包含任一语言的关键词即可,
|
||||
# 因为测试环境加载语言的顺序可能不确定(pytest 并行或 fixture 顺序)。
|
||||
# 这样无论当前加载的是哪种语言的数据,只要数据本身正确就能通过。
|
||||
keywords = ["aurora", "极光", "不夜"]
|
||||
expected_sect_name = sect_row.get('name')
|
||||
if sect_row.get('name_id'):
|
||||
trans = t(sect_row['name_id'])
|
||||
if trans and trans != sect_row['name_id']:
|
||||
expected_sect_name = trans
|
||||
|
||||
found = any(k in desc for k in keywords)
|
||||
assert found, f"驻地描述 '{desc}' 应该包含以下关键词之一: {keywords}"
|
||||
assert sect.name == expected_sect_name, f"Sect name mismatch in {game_lang}"
|
||||
|
||||
# 2. Verify HQ Name
|
||||
region_row = sect_region_map.get(target_id)
|
||||
assert region_row
|
||||
|
||||
expected_hq_name = region_row.get('name')
|
||||
if region_row.get('name_id'):
|
||||
trans = t(region_row['name_id'])
|
||||
if trans and trans != region_row['name_id']:
|
||||
expected_hq_name = trans
|
||||
|
||||
assert sect.headquarter.name == expected_hq_name, f"HQ name mismatch in {game_lang}"
|
||||
|
||||
def test_all_sects_have_headquarters(self):
|
||||
def test_sect_headquarter_desc_loaded(self, game_lang):
|
||||
"""测试宗门驻地描述正确加载"""
|
||||
target_id = 12
|
||||
sect = sects_by_id.get(target_id)
|
||||
assert sect is not None
|
||||
|
||||
# Read RAW Sect Region CSV
|
||||
region_csv_path = CONFIG.paths.shared_game_configs / "sect_region.csv"
|
||||
raw_regions = read_raw_csv_as_dict(region_csv_path)
|
||||
region_row = next((r for r in raw_regions if int(r.get('sect_id', -1)) == target_id), None)
|
||||
assert region_row
|
||||
|
||||
expected_desc = region_row.get('desc')
|
||||
if region_row.get('desc_id'):
|
||||
trans = t(region_row['desc_id'])
|
||||
if trans and trans != region_row['desc_id']:
|
||||
expected_desc = trans
|
||||
|
||||
# Normalize newlines/spaces for comparison if needed
|
||||
assert sect.headquarter.desc == expected_desc, f"HQ desc mismatch in {game_lang}"
|
||||
|
||||
def test_all_sects_have_headquarters(self, game_lang):
|
||||
"""测试所有宗门都有驻地信息"""
|
||||
for sect_id, sect in sects_by_id.items():
|
||||
assert sect.headquarter is not None, f"宗门 {sect.name} (ID={sect_id}) 应该有驻地"
|
||||
assert sect.headquarter is not None
|
||||
assert sect.headquarter.name, f"宗门 {sect.name} 的驻地名称不应为空"
|
||||
|
||||
def test_sect_techniques_loaded(self):
|
||||
def test_sect_techniques_loaded(self, game_lang):
|
||||
"""测试宗门功法列表正确加载"""
|
||||
# 明心剑宗 (sect_id=1) 应该有功法
|
||||
sect = sects_by_id.get(1)
|
||||
assert sect is not None, "宗门 ID=1 应该存在"
|
||||
assert len(sect.technique_names) > 0, (
|
||||
f"宗门 '{sect.name}' 应该有独门功法,但 technique_names 为空"
|
||||
)
|
||||
|
||||
def test_sect_without_techniques(self):
|
||||
"""测试没有配置功法的宗门(不夜城 sect_id=12)"""
|
||||
sect = sects_by_id.get(12)
|
||||
sect = sects_by_id.get(1) # 明心剑宗
|
||||
assert sect is not None
|
||||
# 不夜城在 technique.csv 中没有配置功法,所以应该是空列表
|
||||
assert sect.technique_names == [], (
|
||||
f"宗门 '{sect.name}' 不应该有独门功法"
|
||||
)
|
||||
assert len(sect.technique_names) > 0
|
||||
|
||||
def test_sect_without_techniques(self, game_lang):
|
||||
"""测试没有配置功法的宗门"""
|
||||
sect = sects_by_id.get(12) # 不夜城
|
||||
assert sect is not None
|
||||
assert sect.technique_names == []
|
||||
|
||||
|
||||
class TestTechniqueLoading:
|
||||
"""测试功法数据加载"""
|
||||
|
||||
def test_technique_sect_id_loaded(self):
|
||||
"""测试功法的 sect_id 正确加载(来自 technique.csv 的 sect_id 列)"""
|
||||
# 草字剑诀 (id=30) 属于明心剑宗 (sect_id=1)
|
||||
technique = techniques_by_id.get(30)
|
||||
assert technique is not None, "功法 ID=30 应该存在"
|
||||
def test_technique_sect_id_loaded(self, game_lang):
|
||||
"""测试功法的 sect_id 正确加载"""
|
||||
tech_id = 30 # 草字剑诀
|
||||
technique = techniques_by_id.get(tech_id)
|
||||
assert technique is not None
|
||||
|
||||
# 兼容多语言环境
|
||||
expected_names = {"Grass Word Sword Formula", "草字剑诀"}
|
||||
assert technique.name in expected_names, f"功法名称 '{technique.name}' 不在预期列表中: {expected_names}"
|
||||
# Verify Name using Dynamic Logic
|
||||
tech_csv_path = CONFIG.paths.shared_game_configs / "technique.csv"
|
||||
raw_techs = read_raw_csv_as_dict(tech_csv_path)
|
||||
row = next((r for r in raw_techs if int(r['id']) == tech_id), None)
|
||||
|
||||
assert technique.sect_id == 1, (
|
||||
f"功法 '{technique.name}' 的 sect_id 应该是 1,而不是 {technique.sect_id}"
|
||||
)
|
||||
expected_name = row.get('name')
|
||||
if row.get('name_id'):
|
||||
trans = t(row['name_id'])
|
||||
if trans and trans != row['name_id']:
|
||||
expected_name = trans
|
||||
|
||||
assert technique.name == expected_name, f"Technique name mismatch in {game_lang}"
|
||||
assert technique.sect_id == 1
|
||||
|
||||
def test_technique_without_sect(self):
|
||||
"""测试散修功法(没有宗门限制)的 sect_id 为 None"""
|
||||
# 金刚不坏体 (id=1) 是散修功法
|
||||
def test_technique_without_sect(self, game_lang):
|
||||
"""测试散修功法"""
|
||||
technique = techniques_by_id.get(1)
|
||||
assert technique is not None, "功法 ID=1 应该存在"
|
||||
assert technique.sect_id is None, (
|
||||
f"散修功法 '{technique.name}' 的 sect_id 应该是 None,而不是 {technique.sect_id}"
|
||||
)
|
||||
assert technique is not None
|
||||
assert technique.sect_id is None
|
||||
|
||||
def test_sect_techniques_match(self):
|
||||
def test_sect_techniques_match(self, game_lang):
|
||||
"""测试宗门功法和功法的宗门ID相互匹配"""
|
||||
for sect_id, sect in sects_by_id.items():
|
||||
for tech_name in sect.technique_names:
|
||||
technique = techniques_by_name.get(tech_name)
|
||||
assert technique is not None, f"功法 '{tech_name}' 应该存在"
|
||||
assert technique.sect_id == sect_id, (
|
||||
f"功法 '{tech_name}' 的 sect_id ({technique.sect_id}) "
|
||||
f"应该匹配宗门 '{sect.name}' 的 ID ({sect_id})"
|
||||
)
|
||||
# 注意:technique_names 是 string list,如果 names 不匹配(翻译问题)这里会取不到
|
||||
# 但我们的系统设计是:sect.technique_names 是直接从 technique.csv 加载的
|
||||
# 所以只要 reload 顺序正确(先 technique 后 sect),名字应该是一致的
|
||||
assert technique is not None, f"功法 '{tech_name}' 应该存在 (Lang: {game_lang})"
|
||||
assert technique.sect_id == sect_id
|
||||
|
||||
|
||||
class TestElixirLoading:
|
||||
"""测试丹药数据加载"""
|
||||
"""丹药加载测试 (ID check, less dependent on lang but good to verify integrity)"""
|
||||
|
||||
def test_elixir_loaded_with_item_id(self):
|
||||
"""测试丹药使用 item_id 列正确加载"""
|
||||
from src.classes.elixir import elixirs_by_id
|
||||
|
||||
# 验证丹药已加载且 ID 不为 0(如果用错误的列名会得到默认值 0)
|
||||
assert len(elixirs_by_id) > 0, "应该加载到丹药数据"
|
||||
|
||||
# 丹药目前没有专门的 reload 和 translation key 绑定逻辑验证需求
|
||||
# 保持原样即可,不需要 parametrizing unless needed
|
||||
assert len(elixirs_by_id) > 0
|
||||
for elixir_id, elixir in elixirs_by_id.items():
|
||||
assert elixir_id > 0, f"丹药 '{elixir.name}' 的 ID 应该大于 0"
|
||||
assert elixir.id == elixir_id, f"丹药 ID 不匹配: {elixir.id} != {elixir_id}"
|
||||
assert elixir_id > 0
|
||||
assert elixir.id == elixir_id
|
||||
|
||||
|
||||
class TestGameDataAPI:
|
||||
"""测试 /api/meta/game_data API 返回正确的数据结构"""
|
||||
"""测试 API (API 测试通常在固定环境下运行,这里不使用多语言参数化以免影响 Server 状态)"""
|
||||
|
||||
@pytest.fixture
|
||||
def client(self):
|
||||
"""创建测试客户端"""
|
||||
from fastapi.testclient import TestClient
|
||||
from src.server.main import app
|
||||
return TestClient(app)
|
||||
|
||||
def test_game_data_techniques_have_sect_id(self, client):
|
||||
"""测试 /api/meta/game_data 返回的功法包含 sect_id 字段(而非 sect)"""
|
||||
response = client.get("/api/meta/game_data")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "techniques" in data, "响应应该包含 techniques 字段"
|
||||
|
||||
techniques = data["techniques"]
|
||||
assert len(techniques) > 0, "应该有功法数据"
|
||||
|
||||
for tech in techniques:
|
||||
# 确保使用 sect_id 而非 sect
|
||||
assert "sect_id" in tech, (
|
||||
f"功法 '{tech.get('name', 'unknown')}' 应该有 sect_id 字段"
|
||||
)
|
||||
assert "sect" not in tech, (
|
||||
f"功法 '{tech.get('name', 'unknown')}' 不应该有 sect 字段(应使用 sect_id)"
|
||||
)
|
||||
|
||||
# 验证 sect_id 的值类型正确
|
||||
sect_id = tech["sect_id"]
|
||||
assert sect_id is None or isinstance(sect_id, int), (
|
||||
f"功法 '{tech.get('name')}' 的 sect_id 应该是 None 或 int,而不是 {type(sect_id)}"
|
||||
)
|
||||
assert len(data["techniques"]) > 0
|
||||
assert "sect_id" in data["techniques"][0]
|
||||
|
||||
def test_game_data_sects_structure(self, client):
|
||||
"""测试 /api/meta/game_data 返回的宗门数据结构正确"""
|
||||
response = client.get("/api/meta/game_data")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "sects" in data, "响应应该包含 sects 字段"
|
||||
|
||||
sects = data["sects"]
|
||||
assert len(sects) > 0, "应该有宗门数据"
|
||||
|
||||
for sect in sects:
|
||||
assert "id" in sect, "宗门应该有 id 字段"
|
||||
assert "name" in sect, "宗门应该有 name 字段"
|
||||
assert sect["id"] > 0, f"宗门 '{sect.get('name')}' 的 ID 应该大于 0"
|
||||
|
||||
assert len(data["sects"]) > 0
|
||||
assert "id" in data["sects"][0]
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
|
||||
107
tests/test_death_region_release.py
Normal file
107
tests/test_death_region_release.py
Normal file
@@ -0,0 +1,107 @@
|
||||
|
||||
import pytest
|
||||
from src.classes.death_reason import DeathReason, DeathType
|
||||
from src.classes.death import handle_death
|
||||
from src.classes.region import CultivateRegion, EssenceType
|
||||
|
||||
def test_death_releases_region(base_world, dummy_avatar):
|
||||
"""测试死亡时释放占领的洞府"""
|
||||
# 1. 创建一个修炼区域
|
||||
region = CultivateRegion(
|
||||
id=1001,
|
||||
name="Test Cave",
|
||||
desc="A test cave",
|
||||
essence_type=EssenceType.GOLD,
|
||||
essence_density=10
|
||||
)
|
||||
# 将区域添加到地图
|
||||
base_world.map.regions[region.id] = region
|
||||
|
||||
# 2. 让角色占领该区域
|
||||
dummy_avatar.occupy_region(region)
|
||||
|
||||
# 验证占领成功
|
||||
assert region.host_avatar == dummy_avatar
|
||||
assert region in dummy_avatar.owned_regions
|
||||
|
||||
# 3. 角色死亡
|
||||
reason = DeathReason(DeathType.OLD_AGE)
|
||||
handle_death(base_world, dummy_avatar, reason)
|
||||
|
||||
# 4. 验证洞府已被释放
|
||||
assert region.host_avatar is None
|
||||
assert len(dummy_avatar.owned_regions) == 0
|
||||
assert dummy_avatar.is_dead is True
|
||||
|
||||
def test_occupy_region_logic(base_world, dummy_avatar):
|
||||
"""测试占领逻辑的双向绑定和抢夺"""
|
||||
from src.classes.avatar import Avatar, Gender
|
||||
from src.classes.age import Age
|
||||
from src.classes.cultivation import Realm
|
||||
from src.utils.id_generator import get_avatar_id
|
||||
from src.classes.root import Root
|
||||
from src.classes.alignment import Alignment
|
||||
from src.classes.calendar import create_month_stamp, Year, Month
|
||||
|
||||
# 创建第二个角色
|
||||
other_avatar = Avatar(
|
||||
world=base_world,
|
||||
name="Other",
|
||||
id=get_avatar_id(),
|
||||
birth_month_stamp=create_month_stamp(Year(2000), Month.JANUARY),
|
||||
age=Age(20, Realm.Qi_Refinement),
|
||||
gender=Gender.MALE,
|
||||
pos_x=0, pos_y=0,
|
||||
root=Root.WOOD,
|
||||
alignment=Alignment.RIGHTEOUS
|
||||
)
|
||||
|
||||
region = CultivateRegion(
|
||||
id=1002,
|
||||
name="Test Cave 2",
|
||||
desc="Another test cave",
|
||||
essence_type=EssenceType.WOOD,
|
||||
essence_density=10
|
||||
)
|
||||
|
||||
# 1. dummy_avatar 占领
|
||||
dummy_avatar.occupy_region(region)
|
||||
assert region.host_avatar == dummy_avatar
|
||||
assert region in dummy_avatar.owned_regions
|
||||
|
||||
# 2. other_avatar 抢夺
|
||||
other_avatar.occupy_region(region)
|
||||
|
||||
# 验证所有权转移
|
||||
assert region.host_avatar == other_avatar
|
||||
assert region in other_avatar.owned_regions
|
||||
|
||||
# 验证旧主已释放
|
||||
assert region not in dummy_avatar.owned_regions
|
||||
|
||||
def test_remove_avatar_releases_region(base_world, dummy_avatar):
|
||||
"""测试彻底删除角色时释放占领的洞府"""
|
||||
# 1. 创建一个修炼区域
|
||||
region = CultivateRegion(
|
||||
id=1003,
|
||||
name="Test Cave 3",
|
||||
desc="Yet another test cave",
|
||||
essence_type=EssenceType.WATER,
|
||||
essence_density=10
|
||||
)
|
||||
base_world.map.regions[region.id] = region
|
||||
|
||||
# 2. 注册并占领
|
||||
base_world.avatar_manager.register_avatar(dummy_avatar)
|
||||
dummy_avatar.occupy_region(region)
|
||||
|
||||
assert region.host_avatar == dummy_avatar
|
||||
|
||||
# 3. 彻底删除角色
|
||||
base_world.avatar_manager.remove_avatar(dummy_avatar.id)
|
||||
|
||||
# 4. 验证洞府已被释放
|
||||
assert region.host_avatar is None
|
||||
# 注意:此时 dummy_avatar 对象可能还在内存中,但已经从管理器移除
|
||||
# 它的 owned_regions 应该被清空了
|
||||
assert len(dummy_avatar.owned_regions) == 0
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { NButton, NSelect } from 'naive-ui'
|
||||
import { NButton, NSelect, NIcon } from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSettingStore } from '../stores/setting'
|
||||
import SaveLoadPanel from './game/panels/system/SaveLoadPanel.vue'
|
||||
@@ -149,7 +149,14 @@ watch(() => props.visible, (val) => {
|
||||
|
||||
<div class="settings-form">
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">{{ t('ui.language') }}</span>
|
||||
<div class="setting-label-group">
|
||||
<n-icon size="24" color="#eee" class="setting-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path fill="currentColor" d="M256 48C141.13 48 48 141.13 48 256s93.13 208 208 208s208-93.13 208-208S370.87 48 256 48m0 46.54c33.4 0 63.87 11.23 88.63 30.17c-22.56 31.84-51.48 59.7-88.63 80.64c-37.15-20.94-66.07-48.8-88.63-80.64C192.13 105.77 222.6 94.54 256 94.54m-80 32.14c22.76 27.65 49.77 52.37 80 71.95c-30.23 19.58-57.24 44.3-80 71.95c-20.78-22.38-38.35-46.73-52.09-72c13.73-25.26 31.31-49.61 52.09-71.9ZM256 417.46c-33.4 0-63.87-11.23-88.63-30.17c22.56-31.84 51.48-59.7 88.63-80.64c37.15 20.94 66.07 48.8 88.63 80.64c-24.76 18.94-55.23 30.17-88.63 30.17m80-32.14c-22.76-27.65-49.77-52.37-80-71.95c30.23-19.58 57.24-44.3 80-71.95c20.78 22.38 38.35 46.73 52.09 72c-13.74 25.26-31.31 49.61-52.09 71.95M256 244.64c-25.68-18.3-48.46-41.22-67.45-66.52c19.64-18.79 42.41-32.58 67.45-39.72c25.04 7.14 47.81 20.93 67.45 39.72c-18.99 25.3-41.77 48.22-67.45 66.52m0 109.24c-25.04-7.14-47.81-20.93-67.45-39.72c18.99-25.3 41.77-48.22 67.45-66.52c25.68 18.3 48.46 41.22 67.45 66.52c-19.64 18.79-42.41 32.58-67.45 39.72M81.56 238.15c13.29 27.23 30.76 52.92 51.64 76.54c-15.65-17.65-28.77-37.15-38.74-58.12c-5.18-10.9-9.17-22.03-11.96-33.37c3.15 5.06 6.13 10.05 9.06 14.95m24.16-52.53c9.97-20.97 23.09-40.47 38.74-58.12c-20.88 23.62-38.35 49.31-51.64 76.54c-2.93 4.9-5.91 9.89-9.06 14.95c2.79-11.34 6.78-22.47 11.96-33.37M406.28 273.85c-9.97 20.97-23.09 40.47-38.74 58.12c20.88-23.62 38.35-49.31 51.64-76.54c2.93-4.9 5.91-9.89 9.06-14.95c-2.79 11.34-6.78 22.47-11.96 33.37m-24.16 52.53c-13.29-27.23-30.76-52.92-51.64-76.54c15.65 17.65 28.77 37.15 38.74 58.12c5.18 10.9 9.17 22.03 11.96 33.37c-3.15-5.06-6.13-10.05-9.06-14.95"/>
|
||||
</svg>
|
||||
</n-icon>
|
||||
<span class="setting-label">{{ t('ui.language') }}</span>
|
||||
</div>
|
||||
<n-select
|
||||
v-model:value="settingStore.locale"
|
||||
:options="languageOptions"
|
||||
@@ -212,6 +219,19 @@ watch(() => props.visible, (val) => {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.setting-label-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.setting-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-size: 1.1em;
|
||||
color: #eee;
|
||||
|
||||
Reference in New Issue
Block a user