Feat/hidden domain (#113)

Summary
新增秘境探索,属于多人活动,每N年触发一次
Closes #105
This commit is contained in:
4thfever
2026-01-31 20:43:42 +08:00
committed by GitHub
parent efa663febe
commit 0315dca6e6
34 changed files with 843 additions and 29 deletions

View File

@@ -38,11 +38,11 @@ class Animal:
"""
from src.i18n import t
# 使用格式化字符串 msgid
base_info = t("[{name}] ({realm})", name=t(self.name), realm=str(self.realm))
info_parts = [base_info, t(self.desc)]
base_info = t("[{name}] ({realm})", name=self.name, realm=str(self.realm))
info_parts = [base_info, self.desc]
if self.materials:
material_names = [t(material.name) for material in self.materials]
material_names = [material.name for material in self.materials]
materials_str = t("comma_separator").join(material_names)
info_parts.append(t("Drops: {materials}", materials=materials_str))

View File

@@ -7,6 +7,7 @@ class DeathType(Enum):
OLD_AGE = "old_age"
BATTLE = "battle"
SERIOUS_INJURY = "serious_injury"
HIDDEN_DOMAIN = "hidden_domain"
@dataclass
class DeathReason:
@@ -20,6 +21,8 @@ class DeathReason:
return t("Killed by {killer}", killer=killer)
elif self.death_type == DeathType.SERIOUS_INJURY:
return t("Died from severe injuries")
elif self.death_type == DeathType.HIDDEN_DOMAIN:
return t("Perished in a Hidden Domain")
elif self.death_type == DeathType.OLD_AGE:
return t("Died of old age")
return t(self.death_type.value)

View File

@@ -364,6 +364,30 @@ EXTRA_PLUNDER_MULTIPLIER = "extra_plunder_multiplier"
- 大量: 2
"""
# 秘境相关
EXTRA_HIDDEN_DOMAIN_DROP_PROB = "extra_hidden_domain_drop_prob"
"""
额外秘境掉落概率
类型: float
结算: src/classes/gathering/hidden_domain.py
说明: 增加在秘境中获得宝物的概率。
数值参考:
- 微量: 0.05
- 中量: 0.1
- 大量: 0.2
"""
EXTRA_HIDDEN_DOMAIN_DANGER_PROB = "extra_hidden_domain_danger_prob"
"""
额外秘境危险概率
类型: float
结算: src/classes/gathering/hidden_domain.py
说明: 增加(或减少,负值)在秘境中遇到危险的概率。
数值参考:
- 降低危险: -0.1 (降低10%危险率)
- 增加危险: 0.1
"""
# --- 特殊权限 ---
LEGAL_ACTIONS = "legal_actions"
"""
@@ -479,6 +503,10 @@ ALL_EFFECTS = [
"shop_buy_price_reduction", # float - 商铺购买价格倍率减免
"extra_plunder_multiplier", # float - 额外搜刮收益倍率
# 秘境相关
"extra_hidden_domain_drop_prob", # float - 额外秘境掉落概率
"extra_hidden_domain_danger_prob", # float - 额外秘境危险概率
# 特殊权限
"legal_actions", # list[str] - 合法动作列表
]

View File

@@ -37,6 +37,8 @@ def get_effect_desc(effect_key: str) -> str:
"cultivate_duration_reduction": "effect_cultivate_duration_reduction",
"extra_cast_success_rate": "effect_extra_cast_success_rate",
"extra_refine_success_rate": "effect_extra_refine_success_rate",
"extra_hidden_domain_drop_prob": "effect_extra_hidden_domain_drop_prob",
"extra_hidden_domain_danger_prob": "effect_extra_hidden_domain_danger_prob",
}
msgid = msgid_map.get(effect_key, effect_key)
@@ -189,7 +191,7 @@ def format_effects_to_text(effects: dict[str, Any] | list[dict[str, Any]]) -> st
# 如果有条件,添加条件描述
if effects.get("when"):
cond = translate_condition(str(effects["when"]))
return t("[{condition}] {effects}", condition=cond, effects=text)
return f"[{cond}] {text}"
return text

View File

@@ -1,2 +1,3 @@
from .gathering import Gathering, GatheringManager
from .auction import Auction
from .hidden_domain import HiddenDomain

View File

@@ -0,0 +1,328 @@
from typing import List, Dict, Optional, Any, TYPE_CHECKING
import random
import asyncio
from dataclasses import dataclass
from src.classes.gathering.gathering import Gathering, register_gathering
from src.classes.event import Event
if TYPE_CHECKING:
from src.classes.world import World
from src.classes.avatar import Avatar
from src.classes.item import Item
from src.utils.df import game_configs, get_str, get_float, get_int
from src.classes.cultivation import Realm, REALM_ORDER, REALM_RANK
from src.classes.death_reason import DeathReason, DeathType
from src.classes.death import handle_death
from src.classes.weapon import get_random_weapon_by_realm
from src.classes.auxiliary import get_random_auxiliary_by_realm
from src.classes.technique import get_random_technique_for_avatar
from src.i18n import t
from src.run.log import get_logger
logger = get_logger().logger
@dataclass
class DomainConfig:
id: str
name: str
desc: str
max_realm: Realm
danger_prob: float
hp_loss_percent: float
drop_prob: float
cd_years: int
open_prob: float
@register_gathering
class HiddenDomain(Gathering):
"""
秘境系统 (Hidden Domain)
定期开启,符合境界条件的修士可进入探索,面临凶险或获得机缘。
"""
# 记录每个秘境上次开启的年份 {domain_id: last_open_year}
_domain_states: Dict[str, int] = {}
# 临时存储本轮开启的秘境
_active_domains: List[DomainConfig] = []
# LLM Prompt ID
STORY_PROMPT_ID = "hidden_domain_story_prompt"
@classmethod
def get_story_prompt(cls) -> str:
return t(cls.STORY_PROMPT_ID)
def _load_configs(self) -> List[DomainConfig]:
"""从配置表加载秘境配置"""
configs = []
df = game_configs.get("hidden_domain")
if df is None:
return []
for row in df:
try:
# 必须字段
conf = DomainConfig(
id=get_str(row, "id"),
name=get_str(row, "name"),
desc=get_str(row, "desc"),
max_realm=Realm.from_str(get_str(row, "max_realm")),
danger_prob=get_float(row, "danger_prob"),
hp_loss_percent=get_float(row, "hp_loss_percent"),
drop_prob=get_float(row, "drop_prob"),
cd_years=get_int(row, "cd_years"),
open_prob=get_float(row, "open_prob"),
)
configs.append(conf)
except Exception as e:
logger.error(f"Failed to load hidden domain config: {e}")
continue
return configs
def is_start(self, world: "World") -> bool:
"""
判断是否有秘境开启
"""
self._active_domains = []
current_year = world.month_stamp.get_year()
configs = self._load_configs()
for conf in configs:
last_open = self._domain_states.get(conf.id, -999)
# 只有 CD 转好才进行概率判定
if current_year - last_open >= conf.cd_years:
if random.random() < conf.open_prob:
self._active_domains.append(conf)
# 立即更新状态防止同一step多次调用导致状态不一致虽然后续execute才算正式执行
# 但GatheringManager是 is_start -> execute 顺序执行,所以这里更新没问题
# 如果需要execute失败回滚可以把更新移到execute里。这里简单起见放在这里。
self._domain_states[conf.id] = current_year
return len(self._active_domains) > 0
def get_related_avatars(self, world: "World") -> List[int]:
"""
获取所有可能参与的角色(即所有存活角色,具体筛选在 execute 中按秘境条件进行)
"""
return [av.id for av in world.avatar_manager.get_living_avatars()]
def get_info(self, world: "World") -> str:
details = []
for conf in self._active_domains:
detail = t("Hidden Domain {name} opened! Entry restricted to {realm} and below.",
name=conf.name,
realm=str(conf.max_realm))
details.append(detail)
return t("Hidden Domains opened: {names}", names="\n".join(details))
def _get_next_realm(self, realm: Realm) -> Optional[Realm]:
"""获取下一个大境界"""
current_idx = REALM_RANK.get(realm)
if current_idx is not None and current_idx + 1 < len(REALM_ORDER):
return REALM_ORDER[current_idx + 1]
return None
def _generate_loot(self, avatar: "Avatar", next_realm: Realm) -> Optional[Item]:
"""生成掉落物:优先给予高一阶的物品"""
# 掉落类型权重:兵器(40%), 防具(40%), 功法(20%)
roll = random.random()
loot = None
if roll < 0.4:
# 兵器
loot = get_random_weapon_by_realm(next_realm)
elif roll < 0.8:
# 防具
loot = get_random_auxiliary_by_realm(next_realm)
else:
# 功法:尝试获取更高级的功法
# get_random_technique_for_avatar 根据灵根匹配,但这里我们希望给一点“机缘”
# 复用该函数,但可能获取到同阶的。为了体现“机缘”,我们允许多试几次取最好的,或者直接给
# 这里简单调用现有接口
loot = get_random_technique_for_avatar(avatar)
return loot
async def execute(self, world: "World") -> List[Event]:
events = []
for domain in self._active_domains:
domain_events = await self._process_single_domain(world, domain)
events.extend(domain_events)
return events
async def _process_single_domain(self, world: "World", domain: DomainConfig) -> List[Event]:
"""处理单个秘境的逻辑"""
events = []
month_stamp = world.month_stamp
# 1. 筛选进入秘境的角色
entrants: List["Avatar"] = []
for av in world.avatar_manager.get_living_avatars():
# 境界判定realm <= max (取消最低限制,允许越阶挑战)
if av.cultivation_progress.realm <= domain.max_realm:
entrants.append(av)
# 添加开启事件
entrants_names = [av.name for av in entrants]
if entrants_names:
entrants_str = ", ".join(entrants_names)
open_event_content = t("Hidden Domain {name} opened! Entry restricted to {realm} and below. Entrants: {entrants}",
name=domain.name,
realm=str(domain.max_realm),
entrants=entrants_str)
else:
open_event_content = t("Hidden Domain {name} opened! Entry restricted to {realm} and below. No one entered.",
name=domain.name,
realm=str(domain.max_realm))
events.append(Event(month_stamp, open_event_content))
if not entrants:
return events
# 记录本次秘境的事件文本和相关角色
event_texts: List[str] = [open_event_content]
related_avatars_set: set["Avatar"] = set()
# 2. 遍历角色执行逻辑
for av in entrants:
# --- 效果结算 ---
extra_drop = float(av.effects.get("extra_hidden_domain_drop_prob", 0.0))
extra_danger = float(av.effects.get("extra_hidden_domain_danger_prob", 0.0))
drop_prob = domain.drop_prob + extra_drop
danger_prob = domain.danger_prob + extra_danger
# 确保概率合理
danger_prob = max(0.0, danger_prob)
# --- 凶险判定 ---
if random.random() < danger_prob:
loss_percent = domain.hp_loss_percent
damage = int(av.hp.max * loss_percent)
av.hp.cur -= damage
if av.hp.cur <= 0:
# 死亡结算
reason = DeathReason(DeathType.HIDDEN_DOMAIN)
handle_death(world, av, reason)
event_content = t("{name} perished in the hidden domain {domain}.", name=av.name, domain=domain.name)
event = Event(
month_stamp,
event_content,
related_avatars=[av.id]
)
events.append(event)
event_texts.append(event_content)
related_avatars_set.add(av)
continue # 死了就不能拿奖励了
# --- 机缘判定 ---
if random.random() < drop_prob:
# 获取奖励阶位(高一阶)
target_realm = self._get_next_realm(av.cultivation_progress.realm)
# 如果已经是最高阶,则维持当前阶位
if not target_realm:
target_realm = av.cultivation_progress.realm
loot = self._generate_loot(av, target_realm)
if loot:
# 发放奖励
from src.classes.weapon import Weapon
from src.classes.auxiliary import Auxiliary
from src.classes.technique import Technique
from src.classes.prices import prices
loot_name = loot.name
if isinstance(loot, Weapon):
old = av.weapon
av.change_weapon(loot)
if old: # 回收旧物
av.magic_stone += prices.get_selling_price(old, av)
elif isinstance(loot, Auxiliary):
old = av.auxiliary
av.change_auxiliary(loot)
if old:
av.magic_stone += prices.get_selling_price(old, av)
elif isinstance(loot, Technique):
# 只有当比当前功法好,或者还没功法时才更换?
# 或者直接放入背包(如果有)?目前 Avatar 没有通用背包,通常直接修习
# 简化逻辑:直接修习
av.technique = loot
# 记录事件
event_content = t("{name} found a treasure {loot} in {domain}!", name=av.name, loot=loot_name, domain=domain.name)
event = Event(
month_stamp,
event_content,
related_avatars=[av.id]
)
events.append(event)
event_texts.append(event_content)
related_avatars_set.add(av)
# 3. 生成故事 (StoryTeller)
# 只有当发生了一些值得记录的事情(死人、或者有人获得重宝)才生成故事,避免刷屏
if event_texts:
story_event = await self._generate_story(world, domain, event_texts, list(related_avatars_set))
if story_event:
events.append(story_event)
return events
async def _generate_story(
self,
world: "World",
domain: DomainConfig,
event_texts: List[str],
related_avatars: List["Avatar"]
) -> Optional[Event]:
"""调用 LLM 生成秘境探索故事"""
if not related_avatars:
return None
# 1. 场景描述
gathering_info = t(
"Event: Hidden Domain Opening\nName: {name}\nDescription: {desc}",
name=domain.name, desc=domain.desc
)
# 2. 事件列表
events_str = "\n".join(event_texts)
# 3. 角色信息 (可选,增加故事细节)
details_list = []
details_list.append(t("【Related Avatars Information】"))
for av in related_avatars:
info = av.get_info(detailed=True)
details_list.append(f"- {av.name}: {info}")
details_text = "\n".join(details_list)
# 4. 调用 StoryTeller
from src.classes.story_teller import StoryTeller
story = await StoryTeller.tell_gathering_story(
gathering_info=gathering_info,
events_text=events_str,
details_text=details_text,
related_avatars=related_avatars,
prompt=self.get_story_prompt()
)
return Event(
month_stamp=world.month_stamp,
content=story,
related_avatars=[av.id for av in related_avatars],
is_major=True
)

View File

@@ -37,11 +37,11 @@ class Lode:
"""
from src.i18n import t
# 使用格式化字符串 msgid
base_info = t("[{name}] ({realm})", name=t(self.name), realm=str(self.realm))
info_parts = [base_info, t(self.desc)]
base_info = t("[{name}] ({realm})", name=self.name, realm=str(self.realm))
info_parts = [base_info, self.desc]
if self.materials:
material_names = [t(material.name) for material in self.materials]
material_names = [material.name for material in self.materials]
materials_str = t("comma_separator").join(material_names)
info_parts.append(t("Drops: {materials}", materials=materials_str))

View File

@@ -25,11 +25,11 @@ class Material(Item):
def get_info(self) -> str:
from src.i18n import t
return t("{name} ({realm})", name=t(self.name), realm=str(self.realm))
return t("{name} ({realm})", name=self.name, realm=str(self.realm))
def get_detailed_info(self) -> str:
from src.i18n import t
return t("{name}: {desc} ({realm})", name=t(self.name), desc=t(self.desc), realm=str(self.realm))
return t("{name}: {desc} ({realm})", name=self.name, desc=self.desc, realm=str(self.realm))
def get_structured_info(self) -> dict:
return {

View File

@@ -38,11 +38,11 @@ class Plant:
"""
from src.i18n import t
# 使用格式化字符串 msgid
base_info = t("[{name}] ({realm})", name=t(self.name), realm=str(self.realm))
info_parts = [base_info, t(self.desc)]
base_info = t("[{name}] ({realm})", name=self.name, realm=str(self.realm))
info_parts = [base_info, self.desc]
if self.materials:
material_names = [t(material.name) for material in self.materials]
material_names = [material.name for material in self.materials]
materials_str = t("comma_separator").join(material_names)
info_parts.append(t("Drops: {materials}", materials=materials_str))

View File

@@ -327,8 +327,8 @@ def get_sect_info_with_rank(avatar: "Avatar", detailed: bool = False) -> str:
# 构造详细信息,使用标准空格和括号
detail_content = t("(Alignment: {alignment}, Style: {style}, Headquarters: {hq_name}){effect}",
alignment=avatar.sect.alignment,
style=t(avatar.sect.member_act_style),
hq_name=t(hq.name),
style=avatar.sect.member_act_style,
hq_name=hq.name,
effect=effect_part)
return f"{sect_rank_str} {detail_content}"

View File

@@ -116,7 +116,7 @@ def get_rank_display_name(rank: SectRank, sect: Optional["Sect"] = None) -> str:
if sect is not None:
custom_name = sect.get_rank_name(rank)
if custom_name:
return t(custom_name)
return custom_name
val = DEFAULT_RANK_NAMES.get(rank, "弟子")
return t(val)

View File

@@ -106,21 +106,21 @@ class Technique:
if detailed:
return self.get_detailed_info()
from src.i18n import t
return t("{name} ({attribute}) {grade}", name=t(self.name), attribute=str(self.attribute), grade=str(self.grade))
return t("{name} ({attribute}) {grade}", name=self.name, attribute=str(self.attribute), grade=str(self.grade))
def get_detailed_info(self) -> str:
from src.i18n import t
effect_part = t(" Effect: {effect_desc}", effect_desc=self.effect_desc) if self.effect_desc else ""
return t("{name} ({attribute}) {grade} {desc}{effect}",
name=t(self.name), attribute=str(self.attribute), grade=str(self.grade),
desc=t(self.desc), effect=effect_part)
name=self.name, attribute=str(self.attribute), grade=str(self.grade),
desc=self.desc, effect=effect_part)
def get_colored_info(self) -> str:
"""获取带颜色标记的信息,供前端渲染使用"""
from src.i18n import t
r, g, b = self.grade.color_rgb
# 使用与 get_info 相同的格式,但带有颜色标签
info = t("{name} ({attribute}·{grade})", name=t(self.name), attribute=str(self.attribute), grade=str(self.grade))
info = t("{name} ({attribute}·{grade})", name=self.name, attribute=str(self.attribute), grade=str(self.grade))
return f"<color:{r},{g},{b}>{info}</color>"
def get_structured_info(self) -> dict:

View File

@@ -44,8 +44,8 @@ class Weapon(Item):
from src.i18n import t
effect_part = t(" Effect: {effect_desc}", effect_desc=self.effect_desc) if self.effect_desc else ""
return t("{name} ({type}·{realm}, {desc}){effect}",
name=t(self.name), type=str(self.weapon_type), realm=str(self.realm),
desc=t(self.desc), effect=effect_part)
name=self.name, type=str(self.weapon_type), realm=str(self.realm),
desc=self.desc, effect=effect_part)
def get_colored_info(self) -> str:
"""获取带颜色标记的信息,供前端渲染使用"""

View File

@@ -35,6 +35,8 @@ class World():
gathering_manager: GatheringManager = field(default_factory=GatheringManager)
# 世界历史
history: "History" = field(default_factory=lambda: History())
# 世界开始年份
start_year: int = 0
def get_info(self, detailed: bool = False, avatar: Optional["Avatar"] = None) -> dict:
"""
@@ -106,6 +108,7 @@ class World():
map: "Map",
month_stamp: MonthStamp,
events_db_path: Path,
start_year: int = 0,
) -> "World":
"""
工厂方法:创建使用 SQLite 持久化事件的 World 实例。
@@ -114,6 +117,7 @@ class World():
map: 地图对象。
month_stamp: 时间戳。
events_db_path: 事件数据库文件路径。
start_year: 世界开始年份。
Returns:
配置好的 World 实例。
@@ -123,4 +127,5 @@ class World():
map=map,
month_stamp=month_stamp,
event_manager=event_manager,
start_year=start_year,
)

View File

@@ -1626,6 +1626,18 @@ msgstr "Refine Success Rate"
msgid "effect_extra_hidden_domain_drop_prob"
msgstr "Hidden Domain Drop Rate"
msgid "effect_extra_hidden_domain_danger_prob"
msgstr "Hidden Domain Danger Rate"
# Action Short Names (for legal_actions)
msgid "action_dualcultivation_short_name"
@@ -2086,7 +2098,7 @@ msgstr "\n【Related Avatars Information】"
msgid "auction_story_prompt"
msgstr "Select the most interesting aspect or bidding moment to describe, no need to cover everything."
msgstr "Select the most interesting aspect or a single bid to describe, without needing to cover everything. Focus on describing the tense atmosphere during the bidding process."
@@ -3376,3 +3388,37 @@ msgstr "Two people are holding an elegant tea party."
msgid "action_chess_story_prompt"
msgstr "Two people are playing chess."
# ============================================================================
# Hidden Domain
# ============================================================================
msgid "hidden_domain_story_prompt"
msgstr "Describe the exploration of the hidden domain, focusing on the dangers encountered or the opportunities seized, creating a mysterious and tense atmosphere."
msgid "Hidden Domains opened: {names}"
msgstr "{names}"
msgid "Hidden Domain {name} opened! Entry restricted to {realm} and below."
msgstr "Hidden Domain {name} opened! Entry restricted to {realm} and below."
msgid "{name} perished in the hidden domain {domain}."
msgstr "{name} perished in the hidden domain {domain}."
msgid "{name} found a treasure {loot} in {domain}!"
msgstr "{name} found a treasure {loot} in {domain}!"
msgid "Perished in a Hidden Domain"
msgstr "Perished in a Hidden Domain"
msgid "Event: Hidden Domain Opening\nName: {name}\nDescription: {desc}"
msgstr "Event: Hidden Domain Opening\nName: {name}\nDescription: {desc}"
msgid "Hidden Domain {name} opened! Entry restricted to {realm} and below. Entrants: {entrants}"
msgstr "Hidden Domain {name} opened! Entry restricted to {realm} and below. Entrants: {entrants}"
msgid "Hidden Domain {name} opened! Entry restricted to {realm} and below. No one entered."
msgstr "Hidden Domain {name} opened! Entry restricted to {realm} and below. No one entered."
msgid "【Related Avatars Information】"
msgstr "【Related Avatars Information】"

View File

@@ -1191,6 +1191,12 @@ msgstr "铸造成功率"
msgid "effect_extra_refine_success_rate"
msgstr "炼丹成功率"
msgid "effect_extra_hidden_domain_drop_prob"
msgstr "秘境掉落概率"
msgid "effect_extra_hidden_domain_danger_prob"
msgstr "秘境凶险概率"
# Action 简短名称(用于 legal_actions
msgid "action_dualcultivation_short_name"
msgstr "双修"
@@ -1422,7 +1428,7 @@ msgstr "\n【相关角色信息】"
# LLM Prompt
msgid "auction_story_prompt"
msgstr "选取其中最有趣的一个侧面或一次竞价进行描写,无需面面俱到。"
msgstr "选取其中最有趣的一个侧面或一次竞价进行描写,无需面面俱到。重点描写竞价过程中的紧张气氛。"
# ============================================================================
# Fortune 系统 - 奇遇
@@ -2222,3 +2228,38 @@ msgstr "两人正在举行一场雅致的茶会。"
msgid "action_chess_story_prompt"
msgstr "两人正在对弈下棋。"
# ============================================================================
# Hidden Domain (秘境)
# ============================================================================
msgid "hidden_domain_story_prompt"
msgstr "描述探索秘境的过程,侧重于遭遇的凶险或获得的机缘,营造神秘和紧张的氛围。"
msgid "Hidden Domains opened: {names}"
msgstr "{names}"
msgid "Hidden Domain {name} opened! Entry restricted to {realm} and below."
msgstr "秘境【{name}】现世!限制{realm}及以下修士进入。"
msgid "{name} perished in the hidden domain {domain}."
msgstr "{name} 葬身于秘境【{domain}】。"
msgid "{name} found a treasure {loot} in {domain}!"
msgstr "{name} 在秘境【{domain}】中觅得宝物【{loot}】!"
msgid "Perished in a Hidden Domain"
msgstr "在秘境中陨落"
msgid "Event: Hidden Domain Opening\nName: {name}\nDescription: {desc}"
msgstr "事件:秘境开启\n名称{name}\n描述{desc}"
msgid "Hidden Domain {name} opened! Entry restricted to {realm} and below. Entrants: {entrants}"
msgstr "秘境【{name}】现世!限制{realm}及以下修士进入。进入者:{entrants}。"
msgid "Hidden Domain {name} opened! Entry restricted to {realm} and below. No one entered."
msgstr "秘境【{name}】现世!限制{realm}及以下修士进入。无修士进入。"
msgid "【Related Avatars Information】"
msgstr "【相关角色信息】"

View File

@@ -342,10 +342,12 @@ async def init_game_async():
game_instance["current_save_path"] = save_path
print(f"事件数据库: {events_db_path}")
start_year = getattr(CONFIG.game, "start_year", 100)
world = World.create_with_db(
map=game_map,
month_stamp=create_month_stamp(Year(100), Month.JANUARY),
month_stamp=create_month_stamp(Year(start_year), Month.JANUARY),
events_db_path=events_db_path,
start_year=start_year,
)
sim = Simulator(world)

View File

@@ -194,6 +194,7 @@ def load_game(save_path: Optional[Path] = None) -> Tuple["World", "Simulator", L
# 读取世界数据
world_data = save_data.get("world", {})
month_stamp = MonthStamp(world_data["month_stamp"])
start_year = world_data.get("start_year", 100)
# 计算事件数据库路径。
events_db_path = get_events_db_path(save_path)
@@ -203,6 +204,7 @@ def load_game(save_path: Optional[Path] = None) -> Tuple["World", "Simulator", L
map=game_map,
month_stamp=month_stamp,
events_db_path=events_db_path,
start_year=start_year,
)
# 恢复世界历史

View File

@@ -117,6 +117,7 @@ def save_game(
world_data = {
"month_stamp": int(world.month_stamp),
"start_year": world.start_year,
"existed_sect_ids": [sect.id for sect in existed_sects],
# 天地灵机
"current_phenomenon_id": world.current_phenomenon.id if world.current_phenomenon else None,

View File

@@ -267,6 +267,10 @@ class Simulator:
Gathering 结算阶段:
检查并执行注册的多人聚集事件(如拍卖会、大比等)。
"""
# 第一年不触发聚集事件,给予发育缓冲
if self.world.month_stamp.get_year() <= self.world.start_year:
return []
return await self.world.gathering_manager.check_and_run_all(self.world)
def _phase_update_celestial_phenomenon(self):