0.1.41 天命·兑现:天命flag全量接线15+项即时兑现 + NPC演化种子派生零主rng消耗 + pushNews/purgeNpc确定性修复 + 族史卷叙事层chronicle-scroll.ts + 时效flag自动清理 + UI建筑专精加成预览 + LegacyPanel族史卷视图 + 金钟罩重算
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# AGENTS.md
|
||||
|
||||
仙途家族志 · Chronicle of the Immortal Clan — Electron + React + TS 家族修仙模拟器。全部 UI 与文案为中文。
|
||||
当前版本 **0.1.40**(《天命·裂痕》:天命议题系统 20 条 5 类种子派生选取 + NPC 宗主更替四模式 + NPC 势力分裂 + 灰烬重生新贵补位 + 建筑专精系统 extra×1.5 + UI memo 化 4 处 + 天下大事记叙事层 saga.ts + WorldPanel/LegacyPanel 大事记展示)。
|
||||
当前版本 **0.1.41**(《天命·兑现》:天命 flag 全量接线 15+ 项即时兑现 + NPC 演化种子派生零主 rng 消耗 + pushNews/purgeNpc 确定性修复 + 族史卷叙事层 chronicle-scroll.ts + 时效 flag 自动清理 + UI 建筑专精加成预览 + LegacyPanel 族史卷视图)。
|
||||
|
||||
## 命令
|
||||
|
||||
@@ -45,7 +45,7 @@ src/renderer/game/ 纯 TS 引擎(无 React/DOM,vitest 直
|
||||
Systems/(production/aging/cultivation/missions/events/diplomacy/marriage/tribulation/tournament/lifecycle/combat)
|
||||
capabilities.ts(12 能力卡)/pluginManager.ts/plugin-bootstrap.ts/modSchema.ts(MOD 范式)/modManager.ts(MOD→插件)/creation.ts/pcgen.ts
|
||||
engine/sim/ 世界自进化: WorldSim.ts / worldsim-data.ts(数值与常量唯一权威)/worldgen.ts(世界种子生成器)/Market.ts / worldsim-brief.ts
|
||||
engine/narrative/ 族谱/传记/定鼎/年轴/百年报告
|
||||
engine/narrative/ 族谱/传记/定鼎/年轴/百年报告/族史卷(chronicle-scroll)
|
||||
data/ balance 表 + DataPackRegistry(registry.ts pack() 单源;PILL_RECIPES/FORGE_RECIPES/ERA_CONF 等)
|
||||
storage/ db(MetonaSqlark 单例)/slots(槽+快照+exportEnvelope)/migrate(CURRENT_SCHEMA=2,即 normalize 即迁移)
|
||||
types/domain.ts 全量领域类型(改状态结构先看它)
|
||||
@@ -64,7 +64,7 @@ src/main/ Electron 主进程(app:// 协议 + IPC 存
|
||||
- **插件架构**(0.1.8 起,路径已迁 engine/runtime|kernel):`kernel/plugin.ts` 协议、`runtime/pluginManager.ts` 安装管线(依赖校验/异常回滚/追踪型上下文——注销时钩子全摘)、`plugin-bootstrap.ts` 三核心插件(core-systems/core-data/core-events protected)。事件多池 `world.eventPools`;**findEvent 必须带 world 参数查池**。能力卡 12 张经 `viaCap` 注册;tournament/tribulation/apprentice/season 四卡各自内部检查 `w.sysEnabled`(无独立钩子)。
|
||||
- **MOD 红线(0.1.26 规范)**:MOD 只含 JSON 零脚本(`docs/MOD_GUIDE.md`);**禁止新增 rng 消耗**(全局随机序列);不改核心时序(扩展走五口:事件池/模板/词库/灾因/配方);`modPreflight`+`validateMod` 先行(id 冲突用 `findEvent` 权威检测);MOD 只影响**新开档**世界(老档世界变形成档记录);MOD 状态随 `state.plugins` 持久化(对应 `worldGen.npcs` 落档保证读档永稳)。
|
||||
- **门面**:`runtime/ApiFacade.ts` GameFacade(act 24 项/query 6 类/subscribe 退订协议/about);**UI 面板事实走 world 直调**(act 为测试/自动化通道),`actDirect` 与 ACT_CATALOG 语义对齐(0.1.20 补齐全 24 项);store 的 `facade` 必须在 openState/startNewGame 赋值。
|
||||
- **金钟罩(双基线)**:`tests/clock.test.ts` 的 `GOLDEN`(直调冻结世界)+ `GOLDEN_RESOLVED`(自动 resolve 长跑"现实"世界),各 3 seed × 三档月数(560/1200/2160)共 18 值。当前为 **0.1.40 基线**(0.1.25/0.1.26/0.1.31 零漂;0.1.27/0.1.28/0.1.29/0.1.30/0.1.32/0.1.33 受控变更重算;0.1.39 purgeNpc 清理 worldGen.npcs/relations 僵尸定义——bell-seed-2 三档受控漂移;0.1.40 天命系统+NPC演化增强+建筑专精——金钟罩重算)。指纹 `tests/fingerprint.helper.ts` 含全世界轴(era/stance/prosperity/relationsWithOthers/calamity/pendingEvent/eventQueue/worldAnnals/**worldGen 摘要/sagaAnnals/distress/declineYears 汇总**——0.1.27 生灭/世鉴路径真实锁定)。**有意变更时序/数值时**:指纹一并重算并在注释注明原因。
|
||||
- **金钟罩(双基线)**:`tests/clock.test.ts` 的 `GOLDEN`(直调冻结世界)+ `GOLDEN_RESOLVED`(自动 resolve 长跑"现实"世界),各 3 seed × 三档月数(560/1200/2160)共 18 值。当前为 **0.1.41 基线**(0.1.25/0.1.26/0.1.31 零漂;0.1.27/0.1.28/0.1.29/0.1.30/0.1.32/0.1.33 受控变更重算;0.1.39 purgeNpc 清理 worldGen.npcs/relations 僵尸定义——bell-seed-2 三档受控漂移;0.1.40 天命系统+NPC演化增强+建筑专精——金钟罩重算;0.1.41 NPC演化种子派生+pushNews确定性修复+天命flag全量接线——金钟罩重算)。指纹 `tests/fingerprint.helper.ts` 含全世界轴(era/stance/prosperity/relationsWithOthers/calamity/pendingEvent/eventQueue/worldAnnals/**worldGen 摘要/sagaAnnals/distress/declineYears 汇总**——0.1.27 生灭/世鉴路径真实锁定)。**有意变更时序/数值时**:指纹一并重算并在注释注明原因。
|
||||
|
||||
## 世界自演进模型(0.1.14 sim/ 层,改数值勿动流程)
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ src/main/ Electron 主进程(app:// 协议 + IPC 存
|
||||
|
||||
## 守护机制
|
||||
|
||||
- **双金钟罩**:`tests/clock.test.ts` 的 `GOLDEN`(直调冻结世界)+ `GOLDEN_RESOLVED`(自动 resolve 长跑“现实”世界),各 3 固定 seed × 560/1200/2160 月(47/100/180 年)= 18 指纹;指纹含全世界轴(era/stance/prosperity/关系网/事件闸);**0.1.40 基线**。有意变更时序需受控更新并注明。
|
||||
- **双金钟罩**:`tests/clock.test.ts` 的 `GOLDEN`(直调冻结世界)+ `GOLDEN_RESOLVED`(自动 resolve 长跑“现实”世界),各 3 固定 seed × 560/1200/2160 月(47/100/180 年)= 18 指纹;指纹含全世界轴(era/stance/prosperity/关系网/事件闸);**0.1.41 基线**。有意变更时序需受控更新并注明。
|
||||
- **事件闸**:pending 唯一入口 `fire(w,id,priority)`——高优(raid/渡劫/大比)可顶替被占者,被顶替者入队下月兑现;`applyEventChoice` 前置校验防陈旧 Modal 二次结算。
|
||||
- **插件化**:万物皆是插件(系统/数据包/事件池),三核心插件 protected 常驻;示例见 tests/helpers/examplePlugin.ts。
|
||||
- **可回放**:种子随机(sfc32,引擎域零 Math.random)+ 快照回放保真(restore 重建内核+回置 rng)。
|
||||
@@ -55,7 +55,7 @@ src/main/ Electron 主进程(app:// 协议 + IPC 存
|
||||
- 图标资源 resources/icon.png|ico —— electron-builder 自动使用
|
||||
- 注意:WSL/Linux 交叉打 win 包可行但未签名,正规发布请用 Windows 侧重跑
|
||||
|
||||
## 版本沿革(0.1.0 → 0.1.40)
|
||||
## 版本沿革(0.1.0 → 0.1.41)
|
||||
|
||||
| 版本 | 主题 |
|
||||
|---|---|
|
||||
@@ -89,5 +89,6 @@ src/main/ Electron 主进程(app:// 协议 + IPC 存
|
||||
| 0.1.38 | 存档根治·石碑重刻(OPFS 探测+自动降级 IndexedDB + 回读校验 + 诊断面板 + driver.open 锁死修复) |
|
||||
| 0.1.39 | 淬体·洗髓(missionIds 根治 + chronicle 智能裁剪 + worldsim 双闸守卫 + purgeNpc 僵尸定义清理 + MemberCard memo 化 + 飞升视觉强化 + 测试 7058) |
|
||||
| 0.1.40 | 天命·裂痕(天命议题系统 20 条 5 类种子派生选取 + NPC 宗主更替四模式 + NPC 势力分裂 + 灰烬重生新贵补位 + 建筑专精系统 extra×1.5 + UI memo 化 4 处 + 天下大事记叙事层 saga.ts + WorldPanel/LegacyPanel 大事记展示) |
|
||||
| 0.1.41 | 天命·兑现(天命 flag 全量接线 15+ 项即时兑现 + NPC 演化种子派生零主 rng 消耗 + pushNews/purgeNpc 确定性修复 + 族史卷叙事层 chronicle-scroll.ts + 时效 flag 自动清理 + UI 建筑专精加成预览 + LegacyPanel 族史卷视图) |
|
||||
|
||||
详见:`docs/PROJECT_MEMORY.md`(设计哲学与每个迭代的"为什么")。
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# MOD 开发教程 · Chronicle of the Immortal Clan
|
||||
|
||||
> 版本 **0.1.40·《天命·裂痕》** · MOD_SCHEMA_VERSION = 1 · 全口纯 JSON(零脚本)
|
||||
> 版本 **0.1.41·《天命·兑现》** · MOD_SCHEMA_VERSION = 1 · 全口纯 JSON(零脚本)
|
||||
> **本教程即全部所需**:你不需要读任何源码或类型文件——所有 MOD 能用的字段、取值、默认值、坑点、示例都写在下面。
|
||||
> 照例动手即可:最快 5 分钟做出你的第一个 MOD。
|
||||
|
||||
@@ -989,4 +989,4 @@ MOD 可以注册**自定义奇观类型**,加入年首种子派生选取池:
|
||||
> 0.1.35 涤荡注:无存档迁移(游戏未发布——旧档随版本演进失效属预期);`patch` 层按插件归属回滚;validate 覆盖 §12 全部规则。
|
||||
> 0.1.37 因果奇观注:奇观零 rng(种子派生选取池含 MOD 扩展);`eff.karma` 天然支持;`wonderChance`/`karmaDecayRate`/`karmaHighThreshold`/`karmaLowThreshold` 可经 worldNum 覆写;飞升事件 `ev-ascension-*` 前缀保护防劫持。
|
||||
|
||||
<sub>⚔️ 本文档随游戏版本更新(0.1.40)。字段若有新增,此处先行。</sub>
|
||||
<sub>⚔️ 本文档随游戏版本更新(0.1.41)。字段若有新增,此处先行。</sub>
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
| 0.1.38 | 存档根治·石碑重刻 | OPFS 可用性探测 + 自动降级 IndexedDB + 错误上浮可见 + 回读校验 + 持久化请求 + 诊断面板 + driver.open 永久锁死修复 + exportSave open 补全 |
|
||||
| 0.1.39 | 淬体·洗髓 | missionIds 只增不减根治(releaseSquad 显式 splice)+ chronicle 智能裁剪保留 important 里程碑 + worldsim tick 入口双闸守卫 + purgeNpc 清理 worldGen.npcs/relations 僵尸定义 + MemberCard memo 化脱离 store 订阅 + 飞升叙事视觉强化(金色辉光标题/仙韵边框/金边选项);**测试 7058** |
|
||||
| 0.1.40 | 天命·裂痕 | 天命议题系统(20 条 5 类种子派生选取 + epilogue 高优入闸 + 紫金样式不可回避)+ NPC 宗主更替四模式(退隐传贤/遇害暴毙/夺位篡权/寿终正寝)+ NPC 势力分裂(power>500 景气<25 年首 3% 概率)+ 灰烬重生新贵补位(35% 概率在灭亡家族原区域重建 + 8 风格 6 区域)+ 建筑专精系统(bonusOf specialtyId 参数 extra×1.5 + TerritoryPanel 专精选择 UI)+ UI memo 化 4 处(NpcRow/NewsList/groupedEntries/specialtyId)+ 天下大事记叙事层(saga.ts buildSaga 8 类标签色系 + WorldPanel 展示区 + LegacyPanel 百年报告引用)|
|
||||
| 0.1.41 | 天命·兑现 | 天命 flag 全量接线 15+ 项即时兑现(tideHarvest 灵草×1.5/mercenaryHired 战力×1.15/banditRecruit 入族/treasureExplored 古宝探索/artisanHired 丹房+10%/prodigyAdopted 天骄/envoyToAlliance 仙盟/coalitionFormed 联盟/successionElder 立长/successionWise 立贤/elderCouncil 长老会/advisoryBoard 参议等)+ NPC 演化种子派生(FNV1a 哈希 npcDerive 系列函数替代 evolveNpc/splitNpcDynasty/spawnNewbornDynasty/assessStance/pushNews 中所有 w.rng 调用——零主 rng 消耗)+ purgeNpc 确定性修复(不再 unregisterNpcDef——并行 World 实例隔离)+ 族史卷叙事层(chronicle-scroll.ts 编年史+大事记+天命决策串联)+ 时效 flag 自动清理(tideHarvestUntil/mercenaryUntil/envoyReturnUntil 到期自动删除+使者归来逻辑)+ UI 建筑专精加成预览(TerritoryPanel bonusOf 预览)+ LegacyPanel 族史卷视图(cscroll)+ 金钟罩重算(18 值全量更新)|
|
||||
|
||||
## 三、关键架构决策(及为什么)
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chronicle-of-the-immortal-clan",
|
||||
"productName": "仙途家族志",
|
||||
"version": "0.1.40",
|
||||
"version": "0.1.41",
|
||||
"description": "修仙 · 家族 · 经营 · 战斗 模拟器",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "MetonaTeam",
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 0.1.41 族史卷叙事层
|
||||
*
|
||||
* 将 GameState.chronicle(编年史原始条目)+ WorldSim 大事记 + 天命决策记录
|
||||
* 串联成一个按时间轴排列的叙事性"族史卷"序列。
|
||||
*
|
||||
* 设计原则:
|
||||
* - 纯函数(不修改 state)
|
||||
* - 去重 + 裁剪(上限 80 条——族史长卷不无限膨胀)
|
||||
* - 叙事化:原始 chronicle 条目经文本润色后输出
|
||||
* - 天命串联:天命决策(mandateLastId)标记在时间轴上,与同年的族中事件并列展示
|
||||
*
|
||||
* 数据来源:
|
||||
* 1. state.chronicle — 家族编年史(生卒/突破/外交/建筑/事件/战役/死亡)
|
||||
* 2. state.worldSim.newsFeed — 天下快讯中的里程碑事件
|
||||
* 3. state.family.flag['mandateYear'] / mandateLastId — 天命决策记录
|
||||
* 4. state.family.flag['mandateCount'] — 天命累计次数
|
||||
*/
|
||||
|
||||
import type { GameState, ChronicleEntry } from '../../types/domain'
|
||||
import type { WorldSimState } from '../sim/worldsim-data'
|
||||
import { mandateById, mandateCategoryName } from '../../data/mandate'
|
||||
|
||||
/** 族史卷条目 */
|
||||
export interface ChronicleScrollEntry {
|
||||
year: number
|
||||
/** 事件类型 */
|
||||
kind: 'mandate' | 'breakthrough' | 'death' | 'birth' | 'marriage' | 'diplomacy' | 'building' | 'event' | 'battle' | 'misc' | 'world'
|
||||
/** 叙事文本 */
|
||||
text: string
|
||||
/** 是否重要里程碑(飞升/大境界突破/天命等) */
|
||||
important: boolean
|
||||
/** 相关成员 id(如有) */
|
||||
memberId?: string
|
||||
}
|
||||
|
||||
/** 从 GameState + WorldSimState 中提取族史卷 */
|
||||
export function buildChronicleScroll(state: GameState, ws?: WorldSimState): ChronicleScrollEntry[] {
|
||||
const entries: ChronicleScrollEntry[] = []
|
||||
|
||||
// 1. 从 chronicle 提取全部条目
|
||||
for (const e of state.chronicle) {
|
||||
const entry = chronicleToScroll(e)
|
||||
if (entry) entries.push(entry)
|
||||
}
|
||||
|
||||
// 2. 从 worldSim.newsFeed 提取天下里程碑
|
||||
if (ws) {
|
||||
const seen = new Set<string>()
|
||||
for (const n of ws.newsFeed ?? []) {
|
||||
if (n.kind === 'annal' || n.kind === 'calamity') {
|
||||
const key = `${n.year}:${n.text.slice(0, 20)}`
|
||||
if (seen.has(key)) continue
|
||||
seen.add(key)
|
||||
entries.push({
|
||||
year: n.year,
|
||||
kind: 'world',
|
||||
text: n.text,
|
||||
important: n.kind === 'calamity'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 天命决策记录——从 mandateYear/mandateLastId 提取
|
||||
const mandateYear = state.family.flag['mandateYear'] as number | undefined
|
||||
const mandateLastId = state.family.flag['mandateLastId'] as string | undefined
|
||||
if (mandateYear && mandateLastId) {
|
||||
const def = mandateById(mandateLastId)
|
||||
if (def) {
|
||||
entries.push({
|
||||
year: mandateYear,
|
||||
kind: 'mandate',
|
||||
text: `【天命·${mandateCategoryName(def.category)}】${def.name}:${def.text.slice(0, 60)}${def.text.length > 60 ? '……' : ''}`,
|
||||
important: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 按年份排序(同年按 important 优先)
|
||||
entries.sort((a, b) => a.year - b.year || (b.important ? 1 : 0) - (a.important ? 1 : 0))
|
||||
|
||||
// 去重:同年同 kind 同文本前 30 字符只保留一条
|
||||
const dedupMap = new Map<string, ChronicleScrollEntry>()
|
||||
for (const e of entries) {
|
||||
const key = `${e.year}:${e.kind}:${e.text.slice(0, 30)}`
|
||||
if (!dedupMap.has(key)) dedupMap.set(key, e)
|
||||
}
|
||||
|
||||
const result = [...dedupMap.values()].sort((a, b) => b.year - a.year || (b.important ? 1 : 0) - (a.important ? 1 : 0))
|
||||
|
||||
// 裁剪:上限 80 条
|
||||
if (result.length > 80) result.length = 80
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/** 将 ChronicleEntry 转换为族史卷条目 */
|
||||
function chronicleToScroll(e: ChronicleEntry): ChronicleScrollEntry | null {
|
||||
const kindMap: Record<string, ChronicleScrollEntry['kind']> = {
|
||||
breakthrough: 'breakthrough',
|
||||
death: 'death',
|
||||
marriage: 'marriage',
|
||||
diplomacy: 'diplomacy',
|
||||
building: 'building',
|
||||
event: 'event',
|
||||
battle: 'battle',
|
||||
misc: 'misc'
|
||||
}
|
||||
const kind = kindMap[e.category] ?? 'misc'
|
||||
return {
|
||||
year: e.year,
|
||||
kind,
|
||||
text: e.text,
|
||||
important: e.important,
|
||||
memberId: e.memberId
|
||||
}
|
||||
}
|
||||
|
||||
/** 族史卷条目类型中文名 */
|
||||
export function scrollKindName(kind: ChronicleScrollEntry['kind']): string {
|
||||
return kind === 'mandate' ? '天命'
|
||||
: kind === 'breakthrough' ? '突破'
|
||||
: kind === 'death' ? '殒落'
|
||||
: kind === 'birth' ? '诞辰'
|
||||
: kind === 'marriage' ? '姻缘'
|
||||
: kind === 'diplomacy' ? '外交'
|
||||
: kind === 'building' ? '建筑'
|
||||
: kind === 'event' ? '事件'
|
||||
: kind === 'battle' ? '战役'
|
||||
: kind === 'world' ? '天下'
|
||||
: '杂录'
|
||||
}
|
||||
|
||||
/** 族史卷条目 CSS 类名(供 UI 着色) */
|
||||
export function scrollKindClass(kind: ChronicleScrollEntry['kind']): string {
|
||||
return kind === 'mandate' ? 'scroll-mandate'
|
||||
: kind === 'breakthrough' ? 'scroll-breakthrough'
|
||||
: kind === 'death' ? 'scroll-death'
|
||||
: kind === 'birth' ? 'scroll-birth'
|
||||
: kind === 'marriage' ? 'scroll-marriage'
|
||||
: kind === 'diplomacy' ? 'scroll-diplomacy'
|
||||
: kind === 'building' ? 'scroll-building'
|
||||
: kind === 'event' ? 'scroll-event'
|
||||
: kind === 'battle' ? 'scroll-battle'
|
||||
: kind === 'world' ? 'scroll-world'
|
||||
: 'scroll-misc'
|
||||
}
|
||||
@@ -23,7 +23,11 @@ export function combatPowerOf(w: World, c: Character): number {
|
||||
const fit = fitBonusOf(c).battle ? 1.04 : 1
|
||||
const trait = 1 + traitBonuses(c).windBonus
|
||||
const health = 0.5 + 0.5 * (c.health / 100)
|
||||
return round1(base * stat * techBonus * equip * trait * aspi * fit * health)
|
||||
// 0.1.41 天命接线:mercenaryHired — 佣兵入族战力加成 +15%(时效 flag)
|
||||
const nowMonth = w.state.year * 12 + w.state.month
|
||||
const mercActive = typeof w.state.family.flag['mercenaryUntil'] === 'number' && (w.state.family.flag['mercenaryUntil'] as number) > nowMonth
|
||||
const mercMult = mercActive ? 1.15 : 1
|
||||
return round1(base * stat * techBonus * equip * trait * aspi * fit * health * mercMult)
|
||||
}
|
||||
|
||||
function round1(n: number): number {
|
||||
|
||||
@@ -43,6 +43,12 @@ export function monthlyRate(w: World, c: Character, herbFactor = 1): number {
|
||||
rate *= 1 + w.postBonus('expAll')
|
||||
if (st.family.flag['fengFeiBless']) rate *= 1.05
|
||||
if (c.traits.includes('fengxian')) rate *= 1.3
|
||||
// 0.1.41 天命接线:elderCouncil — 长老会设立,全族心境稳定(修炼+3%)
|
||||
if (st.family.flag['elderCouncil']) rate *= 1.03
|
||||
// 0.1.41 天命接线:advisoryBoard — 参议设立,微弱心境加成(修炼+1.5%)
|
||||
if (st.family.flag['advisoryBoard']) rate *= 1.015
|
||||
// 0.1.41 天命接线:successionWise — 立贤储君,悟性最高者额外修炼加成(+5%)
|
||||
if (st.family.flag['successionWise'] && c.perception >= 7) rate *= 1.05
|
||||
const aspiration = aspirationById(c.aspiration)
|
||||
if (aspiration?.effect.type === 'cult') rate *= 1 + aspiration.effect.value
|
||||
if (fitBonusOf(c).cult) rate *= 1.06
|
||||
|
||||
@@ -6,13 +6,17 @@ import { MAJOR_ORDER } from '../../../data/realms'
|
||||
|
||||
|
||||
import { npcById } from '../../../data/npcs'
|
||||
import { resolveRaid } from './combat'
|
||||
import { resolveRaid, resolveEncounter } from './combat'
|
||||
import { sendMission } from './missions'
|
||||
import { pack } from '../../../data/registry'
|
||||
import { findInheritor } from '../creation'
|
||||
import { resolveTribulation, resolveAscension } from './tribulation'
|
||||
import { nextRealm, describeRealm } from '../../../data/realms'
|
||||
import { findMandateEvent } from './mandate'
|
||||
import { newCharacter } from '../pcgen'
|
||||
import { MALE_GIVEN, FEMALE_GIVEN } from '../../kernel/names'
|
||||
import type { EnemyDef } from '../../../data/secrets'
|
||||
import type { FormationId } from '../../../data/formations'
|
||||
|
||||
const ALL_EVENTS: EventDef[] = [...EVENTS]
|
||||
|
||||
@@ -810,4 +814,318 @@ export function applyEffect(w: World, eff: EffectDef, squad?: string[], _extra?:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────── 0.1.41 天命 flag 全量接线 ────────────────
|
||||
// 即时兑现:flag 设置后立即在引擎层产生真实效果
|
||||
// 持续效果:设置时效 flag(以到期绝对月数存储),由各系统每月读取
|
||||
|
||||
// tideHarvest — 灵草产出 ×1.5 持续 24 月
|
||||
if (eff.flag?.['tideHarvest'] !== undefined) {
|
||||
const expireMonth = s.year * 12 + s.month + 24
|
||||
fam.flag['tideHarvestUntil'] = expireMonth
|
||||
delete fam.flag['tideHarvest']
|
||||
w.log('good', '趁灵气充沛广植灵草,来年丰收——灵草产出将提升 24 月。')
|
||||
}
|
||||
|
||||
// mercenaryHired — 佣兵入族,战力加成 3 年(36 月)
|
||||
if (eff.flag?.['mercenaryHired'] !== undefined) {
|
||||
const expireMonth = s.year * 12 + s.month + 36
|
||||
fam.flag['mercenaryUntil'] = expireMonth
|
||||
delete fam.flag['mercenaryHired']
|
||||
w.log('good', '佣兵入族效力——家族战力大增,三年后离去。')
|
||||
}
|
||||
|
||||
// banditRecruit — 流寇归顺入族(人丁+2)
|
||||
if (eff.flag?.['banditRecruit'] !== undefined) {
|
||||
const surname = fam.surname
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const isMale = w.rng.chance(0.6) // 流寇多为男性
|
||||
const name = `${surname}${w.rng.pick(isMale ? MALE_GIVEN : FEMALE_GIVEN)}`
|
||||
const recruit = newCharacter(w.rng, {
|
||||
name,
|
||||
gender: isMale ? 'male' : 'female',
|
||||
generation: fam.generation,
|
||||
bornYear: s.year - (20 + w.rng.int(0, 15)),
|
||||
age: 0,
|
||||
realm: { major: 'qi', minor: w.rng.int(0, 2) },
|
||||
})
|
||||
recruit.health = 70 + w.rng.int(0, 20) // 流寇体质尚可
|
||||
recruit.state = 'idle'
|
||||
w.addMember(recruit)
|
||||
w.log('info', `流寇归顺——${name} 入族充丁。`)
|
||||
}
|
||||
delete fam.flag['banditRecruit']
|
||||
}
|
||||
|
||||
// borderWarPicked — 选边站队:根据选择调整 NPC 关系
|
||||
if (eff.flag?.['borderWarPicked'] !== undefined) {
|
||||
const side = eff.flag['borderWarPicked'] as string
|
||||
const npcs = Object.entries(s.npcFamilies).sort(([, a], [, b]) => b.power - a.power)
|
||||
if (npcs.length >= 2) {
|
||||
if (side === 'strong') {
|
||||
const strongNpc = npcs[0]![1]
|
||||
strongNpc.relation = Math.min(100, strongNpc.relation + 25)
|
||||
for (let i = 1; i < npcs.length; i++) {
|
||||
npcs[i]![1].relation = Math.max(-100, npcs[i]![1].relation - 25)
|
||||
}
|
||||
w.log('info', `支持${strongNpc.name}——胜方关系提升,败方关系恶化。`)
|
||||
} else if (side === 'weak') {
|
||||
const [strongId] = npcs[0]!
|
||||
const weakNpc = npcs[npcs.length - 1]![1]
|
||||
weakNpc.relation = Math.min(100, weakNpc.relation + 30)
|
||||
s.npcFamilies[strongId]!.relation = Math.max(-100, s.npcFamilies[strongId]!.relation - 15)
|
||||
w.log('info', `支持${weakNpc.name}——雪中送炭,弱方铭记在心。`)
|
||||
}
|
||||
}
|
||||
delete fam.flag['borderWarPicked']
|
||||
}
|
||||
|
||||
// siegeFought — 誓死抵抗:执行围庄战斗判定
|
||||
if (eff.flag?.['siegeFought'] !== undefined) {
|
||||
const team = w.aliveMembers()
|
||||
.filter((c) => w.ageOf(c) >= 16 && c.state !== 'expedition')
|
||||
.sort((a, b) => rankPower(w, b) - rankPower(w, a))
|
||||
.slice(0, 5)
|
||||
if (team.length > 0) {
|
||||
const enemy: EnemyDef = {
|
||||
id: 'siege-attacker',
|
||||
name: '围庄敌军',
|
||||
realm: 'foundation',
|
||||
strength: 1.8,
|
||||
icon: '围',
|
||||
desc: '来犯之敌兵临城下。'
|
||||
}
|
||||
const res = resolveEncounter(w, {
|
||||
title: '围庄之战',
|
||||
enemy,
|
||||
risk: 0.6,
|
||||
team,
|
||||
kind: 'war',
|
||||
year: s.year,
|
||||
month: s.month,
|
||||
formation: (formation as FormationId) ?? undefined
|
||||
})
|
||||
if (res.win) {
|
||||
w.log('good', '族人同仇敌忾,击退围敌!声威大振。')
|
||||
} else if (res.draw) {
|
||||
w.log('info', '围敌暂退,但伤亡不小。')
|
||||
} else {
|
||||
w.log('bad', '围庄之战惨败——族中损失惨重。')
|
||||
fam.stones = Math.max(0, fam.stones - 200)
|
||||
}
|
||||
}
|
||||
delete fam.flag['siegeFought']
|
||||
}
|
||||
|
||||
// siegeAlliance — 求援盟友:检查是否有盟友(关系≥40 的 NPC)
|
||||
if (eff.flag?.['siegeAlliance'] !== undefined) {
|
||||
const allies = Object.values(s.npcFamilies).filter((n) => n.relation >= 40 || n.allied)
|
||||
if (allies.length > 0) {
|
||||
const ally = allies[0]!
|
||||
ally.relation = Math.min(100, ally.relation + 15)
|
||||
w.log('good', `${ally.name} 出兵来援,围敌退去——同盟之谊更深。`)
|
||||
w.chronicle('diplomacy', `${ally.name} 遣兵驰援围庄之危,同盟益固。`, undefined, true)
|
||||
} else {
|
||||
// 无盟友——效果落空,补偿灵石损失
|
||||
w.log('bad', '求援无门——四方无一肯来,只得独自面对。')
|
||||
fam.stones = Math.max(0, fam.stones - 100) // 额外赔偿
|
||||
}
|
||||
delete fam.flag['siegeAlliance']
|
||||
}
|
||||
|
||||
// treasureExplored — 古宝探索:灵石已扣,按概率获得功法/宝器/丹药
|
||||
if (eff.flag?.['treasureExplored'] !== undefined) {
|
||||
const roll = w.rng.next()
|
||||
if (roll < 0.30) {
|
||||
const t = w.rng.pick(pack().techniques.filter((t) => t.grade >= 2))
|
||||
if (t && !fam.techniques.includes(t.id)) {
|
||||
fam.techniques.push(t.id)
|
||||
w.log('good', `古洞探索——寻得《${t.name}》残篇!`)
|
||||
w.chronicle('event', `遣队深入古洞,寻得《${t.name}》残篇。`, undefined, true)
|
||||
} else {
|
||||
fam.inventory['pill-pojing'] = (fam.inventory['pill-pojing'] ?? 0) + 1
|
||||
w.log('good', '古洞探索——寻得一枚破境丹!')
|
||||
}
|
||||
} else if (roll < 0.55) {
|
||||
const a = w.rng.pick(['weapon-fan', 'weapon-qi', 'weapon-ling'])
|
||||
fam.inventory[a] = (fam.inventory[a] ?? 0) + 1
|
||||
w.log('good', '古洞探索——寻得一件法器!')
|
||||
} else if (roll < 0.75) {
|
||||
fam.inventory['pill-pojing'] = (fam.inventory['pill-pojing'] ?? 0) + 1
|
||||
w.log('good', '古洞探索——寻得一枚破境丹!')
|
||||
} else {
|
||||
// 受伤但无所得
|
||||
for (const c of w.aliveMembers().slice(0, 3)) {
|
||||
c.health = Math.max(1, c.health - w.rng.int(10, 30))
|
||||
if (c.health < 35) c.state = 'wounded'
|
||||
}
|
||||
w.log('bad', '古洞探索——禁制反噬,队员负伤而归。')
|
||||
}
|
||||
delete fam.flag['treasureExplored']
|
||||
}
|
||||
|
||||
// artisanHired — 匠师入族,丹房效率永久+10%
|
||||
if (eff.flag?.['artisanHired'] !== undefined) {
|
||||
fam.flag['artisanBonus'] = true
|
||||
delete fam.flag['artisanHired']
|
||||
w.log('good', '匠师入族——炼丹炼器效率永久提升。')
|
||||
}
|
||||
|
||||
// prodigyAdopted — 收养天骄幼童
|
||||
if (eff.flag?.['prodigyAdopted'] !== undefined) {
|
||||
const surname = fam.surname
|
||||
const isMale = w.rng.chance(0.5)
|
||||
const name = `${surname}${w.rng.pick(isMale ? MALE_GIVEN : FEMALE_GIVEN)}`
|
||||
const child = newCharacter(w.rng, {
|
||||
name,
|
||||
gender: isMale ? 'male' : 'female',
|
||||
generation: fam.generation + 1,
|
||||
bornYear: s.year - 3, // 3 岁幼童
|
||||
age: 0,
|
||||
realm: { major: 'mortal', minor: 0 },
|
||||
})
|
||||
// 天骄:高悟性+高根骨
|
||||
child.perception = Math.min(10, 7 + w.rng.int(0, 3))
|
||||
child.mind = Math.min(10, 7 + w.rng.int(0, 3))
|
||||
child.fortune = Math.min(12, 8 + w.rng.int(0, 4))
|
||||
w.addMember(child)
|
||||
w.log('good', `天纵之资——${name} 入族,灵根奇佳,悟性超群。`)
|
||||
w.chronicle('event', `族中收养一名天骄幼童${name},灵根奇佳。`, child.id, true)
|
||||
delete fam.flag['prodigyAdopted']
|
||||
}
|
||||
|
||||
// prodigyTested — 试根骨:概率天才或伤根基
|
||||
if (eff.flag?.['prodigyTested'] !== undefined) {
|
||||
const roll = w.rng.next()
|
||||
const surname = fam.surname
|
||||
const isMale = w.rng.chance(0.5)
|
||||
const name = `${surname}${w.rng.pick(isMale ? MALE_GIVEN : FEMALE_GIVEN)}`
|
||||
if (roll < 0.45) {
|
||||
// 天才
|
||||
const child = newCharacter(w.rng, {
|
||||
name,
|
||||
gender: isMale ? 'male' : 'female',
|
||||
generation: fam.generation + 1,
|
||||
bornYear: s.year - 3,
|
||||
age: 0,
|
||||
realm: { major: 'mortal', minor: 0 },
|
||||
})
|
||||
child.perception = Math.min(10, 8 + w.rng.int(0, 2))
|
||||
child.mind = Math.min(10, 8 + w.rng.int(0, 2))
|
||||
child.fortune = Math.min(12, 10)
|
||||
// 但秘法伤了一分根基
|
||||
child.health = 70
|
||||
w.addMember(child)
|
||||
w.log('good', `秘法试根骨——${name} 确为天才,但根基微损。`)
|
||||
w.chronicle('event', `以秘法探得${name}乃天才,然根基微损。`, child.id, true)
|
||||
} else if (roll < 0.75) {
|
||||
// 普通幼童入族
|
||||
const child = newCharacter(w.rng, {
|
||||
name,
|
||||
gender: isMale ? 'male' : 'female',
|
||||
generation: fam.generation + 1,
|
||||
bornYear: s.year - 3,
|
||||
age: 0,
|
||||
realm: { major: 'mortal', minor: 0 },
|
||||
})
|
||||
w.addMember(child)
|
||||
w.log('info', `秘法试根骨——${name} 灵根尚可,收入族中。`)
|
||||
} else {
|
||||
// 伤其根基——幼童未能入族
|
||||
w.log('bad', '秘法反噬——幼童根基受损,其家含泪带走,再不来投。')
|
||||
fam.karma -= 2
|
||||
}
|
||||
delete fam.flag['prodigyTested']
|
||||
}
|
||||
|
||||
// envoyToAlliance — 遣送入盟,三年后归
|
||||
if (eff.flag?.['envoyToAlliance'] !== undefined) {
|
||||
const best = w.aliveMembers()
|
||||
.filter((c) => c.perception >= 6 && w.ageOf(c) >= 16)
|
||||
.sort((a, b) => b.perception - a.perception)[0]
|
||||
if (best) {
|
||||
const expireMonth = s.year * 12 + s.month + 36
|
||||
fam.flag['envoyReturnUntil'] = expireMonth
|
||||
fam.flag['envoyMemberId'] = best.id
|
||||
best.state = 'apprentice'
|
||||
best.apprentice = { sect: '太虚仙盟', untilYear: s.year + 3, quiet: false }
|
||||
w.log('info', `${best.name} 入太虚仙盟修行三年——悟性大开。`)
|
||||
w.chronicle('event', `${best.name} 遣入太虚仙盟修行三年。`, best.id, true)
|
||||
}
|
||||
delete fam.flag['envoyToAlliance']
|
||||
}
|
||||
|
||||
// coalitionFormed — 组建中立联盟:与关系≥40 的 NPC 建立同盟
|
||||
if (eff.flag?.['coalitionFormed'] !== undefined) {
|
||||
let count = 0
|
||||
for (const npc of Object.values(s.npcFamilies)) {
|
||||
if (npc.relation >= 40 && !npc.allied) {
|
||||
npc.allied = true
|
||||
npc.alliedSinceYear = s.year
|
||||
npc.relation = Math.min(100, npc.relation + 10)
|
||||
count++
|
||||
}
|
||||
}
|
||||
if (count >= 2) {
|
||||
w.log('good', `中立联盟成立——${count} 家响应,第三条路已开。`)
|
||||
w.chronicle('event', `牵头组建中立联盟,${count} 家响应。`, undefined, true)
|
||||
} else if (count === 1) {
|
||||
w.log('info', '仅有一家响应联盟——势单力薄,但聊胜于无。')
|
||||
} else {
|
||||
w.log('bad', '无人响应联盟——中立之路难行。')
|
||||
}
|
||||
delete fam.flag['coalitionFormed']
|
||||
}
|
||||
|
||||
// choseStrongSide — 择强而从:与最强 NPC 关系+30,其余-20
|
||||
if (eff.flag?.['choseStrongSide'] !== undefined) {
|
||||
const npcs = Object.values(s.npcFamilies).sort((a, b) => b.power - a.power)
|
||||
if (npcs.length > 0) {
|
||||
npcs[0]!.relation = Math.min(100, npcs[0]!.relation + 30)
|
||||
for (let i = 1; i < npcs.length; i++) {
|
||||
npcs[i]!.relation = Math.max(-100, npcs[i]!.relation - 20)
|
||||
}
|
||||
w.log('info', `择强而从——与${npcs[0]!.name}关系大增,其余家族转冷。`)
|
||||
}
|
||||
delete fam.flag['choseStrongSide']
|
||||
}
|
||||
|
||||
// rebelPunished — 大义灭亲:移除一名最高修为的叛逆成员
|
||||
if (eff.flag?.['rebelPunished'] !== undefined) {
|
||||
const rebel = w.aliveMembers()
|
||||
.filter((c) => c.id !== fam.headId && c.realm.major !== 'mortal')
|
||||
.sort((a, b) => rankPower(w, b) - rankPower(w, a))[0]
|
||||
if (rebel) {
|
||||
rebel.alive = false
|
||||
rebel.deathYear = s.year
|
||||
rebel.deathCause = '族规处决'
|
||||
w.log('bad', `${rebel.name} 以叛逆之罪处决——人心震慑。`)
|
||||
w.chronicle('death', `${rebel.name} 因通敌叛逆被公审处决。`, rebel.id, true)
|
||||
}
|
||||
delete fam.flag['rebelPunished']
|
||||
}
|
||||
|
||||
// elderCouncil — 长老会设立(永久 flag,不删除)
|
||||
if (eff.flag?.['elderCouncil'] !== undefined) {
|
||||
// elderCouncil 作为永久 flag 保留——cultivation.ts 持续读取
|
||||
w.log('good', '长老会成立——族务共议,人心凝聚。')
|
||||
}
|
||||
|
||||
// advisoryBoard — 参议设立(永久 flag,不删除)
|
||||
if (eff.flag?.['advisoryBoard'] !== undefined) {
|
||||
// advisoryBoard 作为永久 flag 保留——cultivation.ts 持续读取
|
||||
w.log('info', '参议之位设立——长老有言权无决权。')
|
||||
}
|
||||
|
||||
// successionElder — 立长(永久 flag,影响 findInheritor)
|
||||
if (eff.flag?.['successionElder'] !== undefined) {
|
||||
// successionElder 作为永久 flag 保留——creation.ts findInheritor 读取
|
||||
w.log('good', '储位已定——以长幼为序,纲常有序。')
|
||||
}
|
||||
|
||||
// successionWise — 立贤(永久 flag,影响 findInheritor + 修炼加成)
|
||||
if (eff.flag?.['successionWise'] !== undefined) {
|
||||
// successionWise 作为永久 flag 保留
|
||||
w.log('good', '储位已定——以才德为先,来日可期。')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,15 @@ export function productionTick(w: World): void {
|
||||
parts.push(`${def!.name}+${prod}${itemNameOf(k)}`)
|
||||
}
|
||||
}
|
||||
// 0.1.41 天命接线:tideHarvest — 灵草产出 ×1.5 持续 24 月
|
||||
const nowMonth = w.state.year * 12 + w.state.month
|
||||
const tideHarvestActive = typeof fam.flag['tideHarvestUntil'] === 'number' && (fam.flag['tideHarvestUntil'] as number) > nowMonth
|
||||
const tideHarvestMult = tideHarvestActive ? 1.5 : 1
|
||||
// 0.1.41 天命接线:artisanHired — 丹房/炼器效率永久+10%(体现在灵田药园产出微增)
|
||||
const artisanMult = fam.flag['artisanBonus'] ? 1.1 : 1
|
||||
// 0.1.33 灾年真正生效(此前 farmFactor/mineFactor 算而未用——生产零体感)
|
||||
produceAll('lingtian', lingtian, (1 + fielders * 0.05 + springMod) * farmFactor)
|
||||
produceAll('yaoyuan', yaoyuan, farmFactor)
|
||||
produceAll('lingtian', lingtian, (1 + fielders * 0.05 + springMod) * farmFactor * tideHarvestMult)
|
||||
produceAll('yaoyuan', yaoyuan, farmFactor * tideHarvestMult * artisanMult)
|
||||
produceAll('lingkuang', lingkuang, mineFactor)
|
||||
produceAll('lingguoyuan', fam.buildings['lingguoyuan'] ?? 0, farmFactor)
|
||||
produceAll('lingmufang', fam.buildings['lingmufang'] ?? 0, farmFactor)
|
||||
|
||||
@@ -435,15 +435,49 @@ export class World {
|
||||
s.pendingEvent = undefined
|
||||
}
|
||||
|
||||
/** 年度清理:剔除 3 年以前的年份前缀 flag 键(防长线膨胀) */
|
||||
/**
|
||||
* 年度清理:剔除过期的 flag 键(防长线膨胀)
|
||||
* 0.1.41 扩展:新增时效 flag 自动清理(tideHarvestUntil/mercenaryUntil/envoyReturnUntil)
|
||||
* 以及 tourneyPending 年份键清理
|
||||
*/
|
||||
private pruneYearFlags(): void {
|
||||
const fam = this.state.family
|
||||
const cutoff = this.state.year - 3
|
||||
const nowMonth = this.state.year * 12 + this.state.month
|
||||
for (const key of Object.keys(fam.flag)) {
|
||||
const m = /^(auction-|prayerDone-|echoDone-|recruitDone-|legacyDone-|tournamentYear-)(\d+)$/.exec(key)
|
||||
if (m && Number(m[2]) < cutoff) delete fam.flag[key]
|
||||
// tauntCD-<npcId> 非年份键:随目标家族消亡删除(防长线膨胀)
|
||||
if (key.startsWith('tauntCD-') && !this.state.npcFamilies[key.slice('tauntCD-'.length)]) delete fam.flag[key]
|
||||
// tourneyPending-<year> 年份键:过期清理
|
||||
const tm = /^tourneyPending-(\d+)$/.exec(key)
|
||||
if (tm && Number(tm[1]) < cutoff) delete fam.flag[key]
|
||||
// raidCD-<npcId> 随目标家族消亡删除
|
||||
if (key.startsWith('raidCD-') && !this.state.npcFamilies[key.slice('raidCD-'.length)]) delete fam.flag[key]
|
||||
// 0.1.41 时效 flag 清理:到期自动删除
|
||||
if (key === 'tideHarvestUntil' || key === 'mercenaryUntil' || key === 'envoyReturnUntil') {
|
||||
const expire = fam.flag[key]
|
||||
if (typeof expire === 'number' && expire <= nowMonth) {
|
||||
delete fam.flag[key]
|
||||
if (key === 'envoyReturnUntil') {
|
||||
// 使者归来:恢复成员状态
|
||||
const memberId = fam.flag['envoyMemberId'] as string | undefined
|
||||
if (memberId) {
|
||||
const c = this.state.members[memberId]
|
||||
if (c && c.alive && c.state === 'apprentice') {
|
||||
c.state = 'idle'
|
||||
c.apprentice = undefined
|
||||
c.perception = Math.min(10, c.perception + 2)
|
||||
c.mind = Math.min(10, c.mind + 1)
|
||||
c.realmProgress = Math.min(100, c.realmProgress + 20)
|
||||
this.log('good', `${c.name} 从太虚仙盟归来,脱胎换骨——悟性大增。`)
|
||||
this.chronicle('event', `${c.name} 自仙盟归来,修为大进。`, c.id, true)
|
||||
}
|
||||
delete fam.flag['envoyMemberId']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -202,6 +202,27 @@ export function findInheritor(world: World): Character | undefined {
|
||||
const head = world.state.members[world.state.family.headId]
|
||||
const candidates = alive.filter((c) => c.id !== head?.id)
|
||||
if (candidates.length === 0) return undefined
|
||||
// 0.1.41 天命接线:successionElder — 立长不立幼(优先年长者)
|
||||
const flag = world.state.family.flag
|
||||
if (flag['successionElder']) {
|
||||
const byBlood = candidates
|
||||
.filter((c) => (head && head.children.includes(c.id)) || (c.fatherId === head?.id))
|
||||
.sort((a, b) => world.ageOf(b) - world.ageOf(a))
|
||||
if (byBlood.length > 0) return byBlood[0]
|
||||
// 无血亲时也优先年长
|
||||
const byAge = [...candidates].sort((a, b) => world.ageOf(b) - world.ageOf(a))
|
||||
return byAge[0]
|
||||
}
|
||||
// 0.1.41 天命接线:successionWise — 立贤不立长(优先悟性最高者)
|
||||
if (flag['successionWise']) {
|
||||
const byBlood = candidates
|
||||
.filter((c) => (head && head.children.includes(c.id)) || (c.fatherId === head?.id))
|
||||
.sort((a, b) => b.perception - a.perception || realmRank(b.realm) - realmRank(a.realm))
|
||||
if (byBlood.length > 0) return byBlood[0]
|
||||
const byWisdom = [...candidates].sort((a, b) => b.perception - a.perception || realmRank(b.realm) - realmRank(a.realm))
|
||||
return byWisdom[0]
|
||||
}
|
||||
// 默认继承:血亲优先(年长),无血亲按境界
|
||||
const byBlood = candidates
|
||||
.filter((c) => (head && head.children.includes(c.id)) || (c.fatherId === head?.id))
|
||||
.sort((a, b) => world.ageOf(b) - world.ageOf(a))
|
||||
|
||||
@@ -3,12 +3,46 @@ import { World } from '../runtime/World'
|
||||
import { WorldSimState, NpcDynamics, WORLDSIM, ERA_CONF, makeWorldSimState, NpcStance, REGION_TIDE, eraDuraOf, regionTideOf, calamityNames, calamityFamilyOf, calamityEffectOf, CalamityName, POOL_BASE, worldNum, WONDER_CHANCE, WONDER_NAMES, WONDER_DESCS, WONDER_DURATION, WorldWonderKind, WorldWonder, allWonderKinds, wonderDefOf } from './worldsim-data'
|
||||
import { pack } from '../../data/registry'
|
||||
import { ITEMS } from '../../data/items'
|
||||
import { npcById, getNpcDefs, registerNpcDef, unregisterNpcDef } from '../../data/npcs'
|
||||
import { npcById, getNpcDefs, registerNpcDef } from '../../data/npcs'
|
||||
import { allMissions, WONDER_TEMPLATES, type MissionDef } from '../../data/secrets'
|
||||
import { MAJORS, MAJOR_ORDER } from '../../data/realms'
|
||||
|
||||
const MARKET_IDS = ['lingcao', 'lingkuang', 'beastcore', 'lingyu', 'lingmu', 'shoupi', 'lingguo', 'pill-qiyuan', 'pill-ningyuan']
|
||||
|
||||
/**
|
||||
* 0.1.41 NPC 演化种子派生哈希(零主 rng 消耗)
|
||||
*
|
||||
* 基于 seed + year + npcId + salt 生成确定性 [0,1) 浮点数。
|
||||
* 用于 evolveNpc/splitNpcDynasty/spawnNewbornDynasty/assessStance 中
|
||||
* 替代 w.rng 调用——使 NPC 演化不消耗主随机序列,
|
||||
* 玩家行为不间接引发 rng 漂移。
|
||||
*/
|
||||
function npcDerive(seed: string, year: number, npcId: string, salt: number): number {
|
||||
const str = `${seed}:${year}:${npcId}:${salt}`
|
||||
let h = 2166136261
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
h ^= str.charCodeAt(i)
|
||||
h = Math.imul(h, 16777619)
|
||||
h = (h << 13) | (h >>> 19)
|
||||
}
|
||||
return (h >>> 0) / 4294967296
|
||||
}
|
||||
|
||||
/** 种子派生整数 [min, max](零主 rng 消耗) */
|
||||
function npcDeriveInt(seed: string, year: number, npcId: string, salt: number, min: number, max: number): number {
|
||||
return min + Math.floor(npcDerive(seed, year, npcId, salt) * (max - min + 1))
|
||||
}
|
||||
|
||||
/** 种子派生概率判定(零主 rng 消耗) */
|
||||
function npcDeriveChance(seed: string, year: number, npcId: string, salt: number, p: number): boolean {
|
||||
return npcDerive(seed, year, npcId, salt) < p
|
||||
}
|
||||
|
||||
/** 种子派生数组选取(零主 rng 消耗) */
|
||||
function npcDerivePick<T>(seed: string, year: number, npcId: string, salt: number, arr: readonly T[]): T {
|
||||
return arr[Math.floor(npcDerive(seed, year, npcId, salt) * arr.length)]!
|
||||
}
|
||||
|
||||
export class WorldSim {
|
||||
constructor(private w: World) {}
|
||||
|
||||
@@ -595,23 +629,27 @@ function poolBase(id: string): number {
|
||||
function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
||||
void noise
|
||||
const year = w.state.year
|
||||
const seed = w.state.seed
|
||||
const ids = Object.keys(w.state.npcFamilies)
|
||||
for (const [id, npc] of Object.entries(w.state.npcFamilies)) {
|
||||
const dyn = s.npcDyn[id] ?? initDynFor(id)
|
||||
s.npcDyn[id] = dyn
|
||||
const defFirst = npcById(id)
|
||||
// 0.1.41 修复:不依赖全局 DYNAMIC_NPCS(purgeNpc 会 unregister 导致并行 World 实例干扰)
|
||||
// 优先从全局查,fallback 从 worldGen.npcs 查(世界自有的 NPC 定义)
|
||||
const defFirst = npcById(id) ?? w.state.worldGen?.npcs?.find((n) => n.id === id)
|
||||
if (!defFirst) continue
|
||||
if (!dyn.relationsWithOthers || Object.keys(dyn.relationsWithOthers).length === 0) {
|
||||
// 关系网初始化:同风格亲近(剑修→剑修 +30~55),异风格事仇(-35~+15)
|
||||
const mine = npcById(id)
|
||||
// 0.1.41 种子派生(零主 rng 消耗)
|
||||
const mine = npcById(id) ?? w.state.worldGen?.npcs?.find((n) => n.id === id)
|
||||
if (!mine) continue
|
||||
for (const oid of ids) {
|
||||
if (oid === id) continue
|
||||
const other = npcById(oid)
|
||||
const other = npcById(oid) ?? w.state.worldGen?.npcs?.find((n) => n.id === oid)
|
||||
const sameStyle = mine.style === other?.style
|
||||
dyn.relationsWithOthers[oid] = sameStyle
|
||||
? 30 + w.rng.int(0, 25)
|
||||
: -35 + w.rng.int(0, 50)
|
||||
? 30 + npcDeriveInt(seed, year, id + oid, 0xA001, 0, 25)
|
||||
: -35 + npcDeriveInt(seed, year, id + oid, 0xA002, 0, 50)
|
||||
}
|
||||
}
|
||||
const def = defFirst
|
||||
@@ -657,44 +695,49 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
||||
dyn.declineYears = 0
|
||||
}
|
||||
// 新贵补位:家数 < 开局目标(4~8 的种子格)且几率(乱世更频)——世界会新生
|
||||
// 0.1.41 种子派生(零主 rng 消耗)
|
||||
const aliveCount = Object.keys(w.state.npcFamilies).length
|
||||
const cap = Math.min(8, w.state.worldGen?.npcCount ?? 4)
|
||||
if (aliveCount < cap && w.rng.chance(worldNum('greatNewbornChance') * (s.era === 'luanshi' ? 2 : 1))) {
|
||||
if (aliveCount < cap && npcDeriveChance(seed, year, id, 0xB001, worldNum('greatNewbornChance') * (s.era === 'luanshi' ? 2 : 1))) {
|
||||
spawnNewbornDynasty(w, s)
|
||||
}
|
||||
}
|
||||
// 顶峰衰(0.1.27):绝顶不可久踞——power>=700 年首回落(豪强被制衡)
|
||||
if (w.state.month === 1 && npc.power >= 700 && w.rng.chance(0.08)) {
|
||||
// 0.1.41 种子派生(零主 rng 消耗)
|
||||
if (w.state.month === 1 && npc.power >= 700 && npcDeriveChance(seed, year, id, 0xC001, 0.08)) {
|
||||
npc.power = Math.round(npc.power * 0.9)
|
||||
dyn.lastEvent = '顶峰回落'
|
||||
dyn.lastEventYear = w.state.year
|
||||
s.newsFeed.push({ year: w.state.year, month: 1, src: '史官', text: `${def.name} 盛极而衰,锋芒微敛。`, kind: 'annal' })
|
||||
}
|
||||
// 关系漂移:年首各 ±5(邻近的讲合、世仇的愈深)
|
||||
// 0.1.41 种子派生(零主 rng 消耗)
|
||||
let relSalt = 0
|
||||
for (const oid of ids) {
|
||||
if (oid === id) continue
|
||||
const cur = dyn.relationsWithOthers[oid] ?? 0
|
||||
dyn.relationsWithOthers[oid] = clamp(cur + (w.rng.chance(0.5) ? 1 : -1) * 5, -100, 100)
|
||||
dyn.relationsWithOthers[oid] = clamp(cur + (npcDeriveChance(seed, year, id + oid, 0xD001 + relSalt++, 0.5) ? 1 : -1) * 5, -100, 100)
|
||||
}
|
||||
// 0.1.40 换代评估:四模式(退隐传贤/遇害暴毙/夺位篡权/寿终正寝)
|
||||
// 0.1.41 种子派生(零主 rng 消耗)
|
||||
const chanceNow = dyn.leaderAge > lifespan * 0.9 ? 0.5 : 0.02
|
||||
if (w.rng.chance(chanceNow) && !w.rng.chance(0.25)) {
|
||||
if (npcDeriveChance(seed, year, id, 0xE001, chanceNow) && !npcDeriveChance(seed, year, id, 0xE002, 0.25)) {
|
||||
const eraNow = (s.era ?? 'pingshi') as string
|
||||
const prosperityNow = dyn.prosperity ?? 50
|
||||
let mode: 'retire' | 'death' | 'usurp' | 'normal'
|
||||
if (eraNow === 'luanshi' || eraNow === 'mofa') {
|
||||
if (prosperityNow < 30 && w.rng.chance(0.4)) mode = 'death'
|
||||
else if (npc.power > 400 && w.rng.chance(0.25)) mode = 'usurp'
|
||||
if (prosperityNow < 30 && npcDeriveChance(seed, year, id, 0xE003, 0.4)) mode = 'death'
|
||||
else if (npc.power > 400 && npcDeriveChance(seed, year, id, 0xE004, 0.25)) mode = 'usurp'
|
||||
else mode = 'normal'
|
||||
} else {
|
||||
if (prosperityNow > 70 && w.rng.chance(0.45)) mode = 'retire'
|
||||
else if (npc.power > 400 && w.rng.chance(0.15)) mode = 'usurp'
|
||||
if (prosperityNow > 70 && npcDeriveChance(seed, year, id, 0xE005, 0.45)) mode = 'retire'
|
||||
else if (npc.power > 400 && npcDeriveChance(seed, year, id, 0xE006, 0.15)) mode = 'usurp'
|
||||
else mode = 'normal'
|
||||
}
|
||||
switch (mode) {
|
||||
case 'retire': {
|
||||
// 退隐传贤:平盛世高景气——宗主功成身退,传位后辈
|
||||
dyn.leaderAge = 30 + w.rng.int(0, 20)
|
||||
dyn.leaderAge = 30 + npcDeriveInt(seed, year, id, 0xE010, 0, 20)
|
||||
dyn.leaderRealmIdx = Math.min(dyn.leaderRealmIdx + 1, 5)
|
||||
dyn.leaderName = `${def.name.replace('氏', '')}氏新主`
|
||||
dyn.prosperity = clamp(prosperityNow - 5, 8, 100)
|
||||
@@ -704,7 +747,7 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
||||
}
|
||||
case 'death': {
|
||||
// 遇害暴毙:乱世末法低景气——宗主陨落,群龙无首
|
||||
dyn.leaderAge = 25 + w.rng.int(0, 20)
|
||||
dyn.leaderAge = 25 + npcDeriveInt(seed, year, id, 0xE011, 0, 20)
|
||||
dyn.leaderRealmIdx = Math.max(0, dyn.leaderRealmIdx) // 境界不变或降
|
||||
dyn.leaderName = `${def.name.replace('氏', '')}氏少主`
|
||||
dyn.prosperity = clamp(prosperityNow - 20, 8, 100)
|
||||
@@ -714,8 +757,8 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
||||
}
|
||||
case 'usurp': {
|
||||
// 夺位篡权:高压家族——强人夺位,势力激增但人心不稳
|
||||
dyn.leaderAge = 35 + w.rng.int(0, 15)
|
||||
dyn.leaderRealmIdx = Math.min(dyn.leaderRealmIdx + (w.rng.chance(0.4) ? 2 : 1), 5)
|
||||
dyn.leaderAge = 35 + npcDeriveInt(seed, year, id, 0xE012, 0, 15)
|
||||
dyn.leaderRealmIdx = Math.min(dyn.leaderRealmIdx + (npcDeriveChance(seed, year, id, 0xE013, 0.4) ? 2 : 1), 5)
|
||||
dyn.leaderName = `${def.name.replace('氏', '')}氏篡主`
|
||||
dyn.prosperity = clamp(prosperityNow + 8, 8, 100)
|
||||
npc.power = Math.round(Math.max(40, npc.power + 40))
|
||||
@@ -730,10 +773,10 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
||||
}
|
||||
default: {
|
||||
// 寿终正寝:常规换代
|
||||
dyn.leaderAge = 30 + w.rng.int(0, 25)
|
||||
dyn.leaderAge = 30 + npcDeriveInt(seed, year, id, 0xE014, 0, 25)
|
||||
dyn.leaderRealmIdx = Math.min(dyn.leaderRealmIdx + 1, 5)
|
||||
dyn.leaderName = `${def.name.replace('氏', '')}氏新主`
|
||||
dyn.prosperity = 55 + w.rng.int(0, 20)
|
||||
dyn.prosperity = 55 + npcDeriveInt(seed, year, id, 0xE015, 0, 20)
|
||||
npc.power = Math.round(Math.max(40, npc.power + 15))
|
||||
dyn.lastEvent = '宗祧更替'
|
||||
}
|
||||
@@ -749,7 +792,8 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
||||
w.emitFx('ripple', `succession:${id}`)
|
||||
}
|
||||
// 0.1.40 NPC 势力分裂:power>500 且景气<25 的家族,年首 3% 概率分裂
|
||||
if (w.rng.chance(0.03) && npc.power > 500 && (dyn.prosperity ?? 50) < 25 && !npc.allied) {
|
||||
// 0.1.41 种子派生(零主 rng 消耗)
|
||||
if (npcDeriveChance(seed, year, id, 0xF001, 0.03) && npc.power > 500 && (dyn.prosperity ?? 50) < 25 && !npc.allied) {
|
||||
splitNpcDynasty(w, s, id)
|
||||
}
|
||||
// B7 互攻:与世仇(关系<-50)年首相搏——败方伤筋动骨
|
||||
@@ -762,11 +806,12 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
||||
const hateBar = foeStance === 'expand' ? -40 : foeStance === 'endure' ? -70 : -50
|
||||
if (foeStance === 'ally') void hateBar
|
||||
// W4 灾年全环:天下凶年群雄相噬——互攻概率 ×1.5
|
||||
// 0.1.41 种子派生(零主 rng 消耗)
|
||||
const foeEventChance = (s.calamity ? WORLDSIM.npcEventChance * 1.5 : WORLDSIM.npcEventChance)
|
||||
if (rel < hateBar && foeStance !== 'ally' && w.rng.chance(foeEventChance)) {
|
||||
if (rel < hateBar && foeStance !== 'ally' && npcDeriveChance(seed, year, id + oid, 0xF101 + ids.indexOf(oid), foeEventChance)) {
|
||||
// 1-3 蝴蝶效应:互攻按实力加权(强者越可能胜,弱者一败再败)
|
||||
const wSum = npc.power + foe.power
|
||||
const winner = w.rng.chance(wSum > 0 ? npc.power / wSum : 0.5) ? npc : foe
|
||||
const winner = npcDeriveChance(seed, year, id + oid, 0xF201 + ids.indexOf(oid), wSum > 0 ? npc.power / wSum : 0.5) ? npc : foe
|
||||
const loser = winner === npc ? foe : npc
|
||||
winner.power = Math.min(900, Math.round(winner.power * 1.06))
|
||||
loser.power = Math.max(52, Math.round(loser.power * 0.82))
|
||||
@@ -818,7 +863,10 @@ function purgeNpc(w: World, s: WorldSimState, id: string): void {
|
||||
delete w.state.family.flag[`tauntCD-${id}`]
|
||||
w.state.eventQueue = (w.state.eventQueue ?? []).filter((e) => !e.startsWith('ev-raid-') || !e.endsWith(id))
|
||||
if (s.distress?.id === id) s.distress = undefined
|
||||
unregisterNpcDef(id)
|
||||
// 0.1.41 修复:不再调用 unregisterNpcDef——全局 DYNAMIC_NPCS 是跨 World 共享的,
|
||||
// 并行 World 实例(如测试中的 det-replay)会因 a 的 purge 导致 b 的 npcById 返回 undefined。
|
||||
// 已灭亡 NPC 的 def 留在 DYNAMIC_NPCS 中不影响逻辑(npcFamilies 中已删除,不会被迭代到)。
|
||||
// unregisterNpcDef(id)
|
||||
// 0.1.39:从 worldGen.npcs 移除已灭亡 NPC 的定义(存档体积精简——僵尸 def 不再落盘)
|
||||
if (w.state.worldGen?.npcs) {
|
||||
const idx = w.state.worldGen.npcs.findIndex((n) => n.id === id)
|
||||
@@ -853,7 +901,9 @@ function splitNpcDynasty(w: World, s: WorldSimState, parentId: string): void {
|
||||
const parentDyn = s.npcDyn[parentId]
|
||||
if (!parent || !parentDef || !parentDyn) return
|
||||
|
||||
const rng = w.rng
|
||||
// 0.1.41 种子派生(零主 rng 消耗)
|
||||
const seed = w.state.seed
|
||||
const year = w.state.year
|
||||
const splitPower = Math.round(parent.power * 0.35)
|
||||
parent.power = Math.round(parent.power * 0.6)
|
||||
parentDyn.prosperity = 15
|
||||
@@ -862,11 +912,11 @@ function splitNpcDynasty(w: World, s: WorldSimState, parentId: string): void {
|
||||
|
||||
// 分裂出的新家族
|
||||
let seedHash = 0
|
||||
for (let i = 0; i < w.state.seed.length; i++) seedHash = (seedHash * 31 + w.state.seed.charCodeAt(i)) >>> 0
|
||||
const newId = `n-split-${(seedHash % 4096).toString(36)}-${rng.int(1, 9999999)}`
|
||||
for (let i = 0; i < seed.length; i++) seedHash = (seedHash * 31 + seed.charCodeAt(i)) >>> 0
|
||||
const newId = `n-split-${(seedHash % 4096).toString(36)}-${npcDeriveInt(seed, year, parentId, 0xF010, 1, 9999999)}`
|
||||
const newName = `${parent.name.slice(0, 2)}分支`
|
||||
const newStyle = rng.pick([...NEWBORN_STYLES])
|
||||
const newRegion = parent.region || rng.pick([...NEWBORN_REGIONS])
|
||||
const newStyle = npcDerivePick(seed, year, parentId, 0xF011, NEWBORN_STYLES)
|
||||
const newRegion = parent.region || npcDerivePick(seed, year, parentId, 0xF012, NEWBORN_REGIONS)
|
||||
const newDef = {
|
||||
id: newId,
|
||||
name: newName,
|
||||
@@ -895,7 +945,7 @@ function splitNpcDynasty(w: World, s: WorldSimState, parentId: string): void {
|
||||
}
|
||||
const newDyn = initDynFor(newId)
|
||||
newDyn.leaderName = `${newName.slice(0, 2)}氏少主`
|
||||
newDyn.leaderAge = 25 + rng.int(0, 15)
|
||||
newDyn.leaderAge = 25 + npcDeriveInt(seed, year, parentId, 0xF013, 0, 15)
|
||||
newDyn.leaderRealmIdx = Math.max(0, parentDyn.leaderRealmIdx - 1)
|
||||
newDyn.prosperity = 40
|
||||
newDyn.lastEvent = '分裂自立'
|
||||
@@ -932,27 +982,29 @@ function splitNpcDynasty(w: World, s: WorldSimState, parentId: string): void {
|
||||
* - 65% 全新初立:原逻辑(随机风格/区域)
|
||||
*/
|
||||
function spawnNewbornDynasty(w: World, s: WorldSimState): void {
|
||||
const rng = w.rng
|
||||
// 0.1.41 种子派生(零主 rng 消耗)
|
||||
const seed = w.state.seed
|
||||
const year = w.state.year
|
||||
// 0.1.40 灰烬重生模式:检查 worldGen.npcs 中已被 purge 的家族区域
|
||||
let ashesRegion: string | undefined
|
||||
let ashesStyle: string | undefined
|
||||
if (w.rng.chance(0.35) && w.state.worldGen?.npcs) {
|
||||
if (npcDeriveChance(seed, year, 'newborn', 0xF020, 0.35) && w.state.worldGen?.npcs) {
|
||||
// 从 worldGen.npcs 找到已不在 npcFamilies 中的灭亡家族
|
||||
const fallen = w.state.worldGen.npcs.filter((n) => !w.state.npcFamilies[n.id])
|
||||
if (fallen.length > 0) {
|
||||
const pick = fallen[rng.int(0, fallen.length - 1)]!
|
||||
const pick = fallen[npcDeriveInt(seed, year, 'newborn', 0xF021, 0, fallen.length - 1)]!
|
||||
ashesRegion = pick.region
|
||||
// 不继承原风格——灰烬中重生的新风格
|
||||
ashesStyle = rng.pick([...NEWBORN_STYLES])
|
||||
ashesStyle = npcDerivePick(seed, year, 'newborn', 0xF022, NEWBORN_STYLES)
|
||||
}
|
||||
}
|
||||
const style = ashesStyle ?? rng.pick([...NEWBORN_STYLES])
|
||||
const region = ashesRegion ?? rng.pick([...NEWBORN_REGIONS])
|
||||
const style = ashesStyle ?? npcDerivePick(seed, year, 'newborn', 0xF023, NEWBORN_STYLES)
|
||||
const region = ashesRegion ?? npcDerivePick(seed, year, 'newborn', 0xF024, NEWBORN_REGIONS)
|
||||
// P1-10 id 掺 seed 指纹(防跨档碰撞)
|
||||
let seedHash = 0
|
||||
for (let i = 0; i < w.state.seed.length; i++) seedHash = (seedHash * 31 + w.state.seed.charCodeAt(i)) >>> 0
|
||||
const id = `n-new-${(seedHash % 4096).toString(36)}-${rng.int(1, 9999999)}` // 0.1.35 A-10 碰撞窗口 9999→9999999
|
||||
const name = `${region.slice(0, 2)}${NEWBORN_NAME_SUFFIX[rng.int(0, NEWBORN_NAME_SUFFIX.length - 1)]}`
|
||||
for (let i = 0; i < seed.length; i++) seedHash = (seedHash * 31 + seed.charCodeAt(i)) >>> 0
|
||||
const id = `n-new-${(seedHash % 4096).toString(36)}-${npcDeriveInt(seed, year, 'newborn', 0xF025, 1, 9999999)}` // 0.1.35 A-10 碰撞窗口 9999→9999999
|
||||
const name = `${region.slice(0, 2)}${NEWBORN_NAME_SUFFIX[npcDeriveInt(seed, year, 'newborn', 0xF026, 0, NEWBORN_NAME_SUFFIX.length - 1)]}`
|
||||
const isAshes = !!ashesRegion
|
||||
const def = {
|
||||
id,
|
||||
@@ -963,7 +1015,7 @@ function spawnNewbornDynasty(w: World, s: WorldSimState): void {
|
||||
: `${style}初立,闷头搞了十年发展,如今渐攒起一份家业。`,
|
||||
style,
|
||||
leaderRealm: 'foundation' as const,
|
||||
initialPower: isAshes ? 90 + rng.int(0, 50) : 100 + rng.int(0, 60), // 灰烬重生起步略低
|
||||
initialPower: isAshes ? 90 + npcDeriveInt(seed, year, 'newborn', 0xF027, 0, 50) : 100 + npcDeriveInt(seed, year, 'newborn', 0xF028, 0, 60), // 灰烬重生起步略低
|
||||
powerGrowth: [3, 9] as [number, number],
|
||||
sells: [],
|
||||
buys: ['lingcao', 'lingkuang']
|
||||
@@ -1032,8 +1084,9 @@ function assessStance(w: World, id: string, dyn: NpcDynamics): NpcStance {
|
||||
}
|
||||
const pick = (Object.keys(W) as NpcStance[]).sort((a, b) => W[b] - W[a])
|
||||
// 用幂等 rng 抽取(先取最高权)+ 30% 软翻转
|
||||
// 0.1.41 种子派生(零主 rng 消耗)
|
||||
const top = pick[0]!
|
||||
if (w.rng.chance(0.7)) return top
|
||||
if (npcDeriveChance(w.state.seed, w.state.year, id, 0xA100, 0.7)) return top
|
||||
return pick[1]!
|
||||
}
|
||||
|
||||
@@ -1054,8 +1107,9 @@ function initDynFor(id: string): NpcDynamics {
|
||||
}
|
||||
|
||||
function pushNews(w: World, s: WorldSimState, about: string[]): void {
|
||||
const rng = w.rng
|
||||
const idx = rng.int(0, about.length - 1)
|
||||
// 0.1.41 种子派生(零主 rng 消耗)——pushNews 被 evolveNpc 调用,
|
||||
// 若用 w.rng 会因 NPC 数量变化导致 rng 消耗不同,破坏确定性。
|
||||
const idx = npcDeriveInt(w.state.seed, w.state.year, about.join(','), 0xA200, 0, about.length - 1)
|
||||
const id = about[idx]
|
||||
const tide = s.tide > 1.0 ? '灵潮上涨' : s.tide < 0.75 ? '灵潮回落' : '汐平'
|
||||
let row: { year: number; month: number; src: string; text: string; itemId?: string; kind?: 'quote' | 'npc' | 'calamity' | 'annal' }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { GameState, SaveMeta, SnapshotMeta, Id } from '../types/domain'
|
||||
import { validateLoadedState, exportEnvelope, parseImportEnvelope } from './migrate'
|
||||
|
||||
const GAME_VERSION = '0.1.40'
|
||||
const GAME_VERSION = '0.1.41'
|
||||
|
||||
const DB_PREFIX = 'cotyc-save-'
|
||||
let seqCounter = 0
|
||||
|
||||
@@ -6,6 +6,7 @@ import { buildBiography } from '../../game/engine/narrative/biography'
|
||||
import { yearAxis, decadeLabel } from '../../game/engine/narrative/yearaxis'
|
||||
import { buildReport, verdictLine } from '../../game/engine/narrative/legacyreport'
|
||||
import { buildSaga, sagaKindName, sagaKindClass } from '../../game/engine/narrative/saga'
|
||||
import { buildChronicleScroll, scrollKindName, scrollKindClass } from '../../game/engine/narrative/chronicle-scroll'
|
||||
import type { WorldSimState } from '../../game/engine/sim/worldsim-data'
|
||||
|
||||
|
||||
@@ -14,7 +15,7 @@ export default function LegacyPanel() {
|
||||
const revision = useGameStore((s) => s.revision)
|
||||
void revision
|
||||
const [showResolve, setShowResolve] = useState(false)
|
||||
const [view, setView] = useState<'dims' | 'chronicle' | 'report' | 'scroll' | 'axis' | 'legacy'>('dims')
|
||||
const [view, setView] = useState<'dims' | 'chronicle' | 'report' | 'scroll' | 'axis' | 'legacy' | 'cscroll'>('dims')
|
||||
if (!world) return null
|
||||
const w = world
|
||||
const s = w.state
|
||||
@@ -56,6 +57,7 @@ export default function LegacyPanel() {
|
||||
<button className="btn" onClick={() => setView('report')}>年报</button>
|
||||
<button className="btn" onClick={() => setView('chronicle')}>年表</button>
|
||||
<button className="btn" onClick={() => setView('scroll')}>长卷</button>
|
||||
<button className="btn" onClick={() => setView('cscroll')}>族史卷</button>
|
||||
<button className="btn" onClick={() => setView('axis')}>年轴</button>
|
||||
<button className="btn" onClick={() => setView('legacy')}>百年报告</button>
|
||||
</div>
|
||||
@@ -120,6 +122,8 @@ export default function LegacyPanel() {
|
||||
|
||||
{view === 'scroll' && <ScrollScroll />}
|
||||
|
||||
{view === 'cscroll' && <ChronicleScrollView />}
|
||||
|
||||
{view === 'chronicle' && (
|
||||
<div className="card">
|
||||
<div className="card-title">生卒与突破年表</div>
|
||||
@@ -261,6 +265,38 @@ function ScrollScroll() {
|
||||
)
|
||||
}
|
||||
|
||||
function ChronicleScrollView() {
|
||||
const world = useGameStore((s) => s.world)
|
||||
if (!world) return null
|
||||
const ws = world.state.worldSim as WorldSimState | undefined
|
||||
const scroll = buildChronicleScroll(world.state, ws)
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-title">族史卷(天命·族事·天下 串联)</div>
|
||||
{scroll.length === 0 ? (
|
||||
<div className="dim">开卷尚浅,待岁月沉淀后再启此卷。</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, maxHeight: 500, overflowY: 'auto' }}>
|
||||
{scroll.map((e, i) => (
|
||||
<div key={i} className="ch-item" style={{
|
||||
padding: '5px 8px',
|
||||
borderLeft: e.important ? '3px solid #c9a227' : undefined,
|
||||
paddingLeft: e.important ? 10 : 8
|
||||
}}>
|
||||
<span className="ch-month">{e.year}年</span>
|
||||
<span className={`tag ${scrollKindClass(e.kind)}`} style={{ marginRight: 6, fontSize: '0.72rem' }}>{scrollKindName(e.kind)}</span>
|
||||
<span className={e.important ? 'gold-t' : ''}>{e.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="dim2" style={{ marginTop: 8 }}>
|
||||
族史卷将天命决策、族中大事与天下大事记串联展示——以时间为经、以事件为纬。
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function dimName(k: string): string {
|
||||
const map: Record<string, string> = { renXing: '人兴', daoXing: '道兴', weiMing: '威名', xiangHuo: '香火', yinGuo: '因果' }
|
||||
return map[k] ?? k
|
||||
|
||||
@@ -354,7 +354,7 @@ export default function SettingsPanel() {
|
||||
· 「外交」与四邻结好联姻;仇雠之族隔岁来犯,打得赢名望大涨,打不赢蚀钱伤丁。<br />
|
||||
· 「史书」自动记述繁华与凋零——百年之后,后人翻开这一卷家族志,见代代薪火、历历雪泥。
|
||||
</div>
|
||||
<div className="dim2" style={{ marginTop: 8 }}>版本 0.1.40 · Chronicle of the Immortal Clan</div>
|
||||
<div className="dim2" style={{ marginTop: 8 }}>版本 0.1.41 · Chronicle of the Immortal Clan</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useGameStore } from '../store'
|
||||
import { BUILDINGS, buildingById, listBuildingDefs, SPECIALTY_MULT } from '../../game/data/buildings'
|
||||
import { BUILDINGS, buildingById, listBuildingDefs, SPECIALTY_MULT, bonusOf, isSpecialty } from '../../game/data/buildings'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
export default function TerritoryPanel() {
|
||||
@@ -44,6 +44,30 @@ export default function TerritoryPanel() {
|
||||
<span className="bld-lv">{built ? `${level}级` : ''}</span>
|
||||
</div>
|
||||
<div className="bld-desc">{def.desc}</div>
|
||||
{/* 0.1.41 建筑专精加成预览 */}
|
||||
{built && def.extra && Object.keys(def.extra).length > 0 && (() => {
|
||||
const specialtyId = fam.flag['buildingSpecialty'] as string | undefined
|
||||
const currentLevel = level
|
||||
const isThisSpecialty = isSpecialty(id, specialtyId)
|
||||
const bonusLabels: { name: string; base: number; specialty: number }[] = []
|
||||
for (const [key, expr] of Object.entries(def.extra)) {
|
||||
void expr
|
||||
const base = bonusOf(id, key, currentLevel, undefined)
|
||||
const spec = bonusOf(id, key, currentLevel, id)
|
||||
bonusLabels.push({ name: extraLabel(key), base, specialty: spec })
|
||||
}
|
||||
return (
|
||||
<div className="bld-prod" style={{ fontSize: '0.72rem', color: '#a89878' }}>
|
||||
{bonusLabels.map((b, i) => (
|
||||
<span key={i} style={{ marginRight: 8 }}>
|
||||
{b.name}: {b.base.toFixed(2)}
|
||||
{isThisSpecialty && <span className="gold"> →x{SPECIALTY_MULT} = {(b.base * SPECIALTY_MULT).toFixed(2)}</span>}
|
||||
{!isThisSpecialty && specialtyId && <span className="dim2"> (设为专精可至 {(b.base * SPECIALTY_MULT).toFixed(2)})</span>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
{produce && Object.keys(produce).length > 0 && (
|
||||
<div className="bld-prod">
|
||||
{Object.entries(produce)
|
||||
@@ -168,3 +192,15 @@ function itemLabel(k: string): string {
|
||||
}
|
||||
return map[k] ?? k
|
||||
}
|
||||
|
||||
/** 0.1.41 建筑加成属性中文名 */
|
||||
function extraLabel(key: string): string {
|
||||
const map: Record<string, string> = {
|
||||
craftChance: '成丹率',
|
||||
unlockGrade: '解锁品阶',
|
||||
expBonus: '修炼加成',
|
||||
powerBonus: '战力加成',
|
||||
repBonus: '声望'
|
||||
}
|
||||
return map[key] ?? key
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export function metaFromState(state: GameState, slot: number): SaveMeta {
|
||||
members: alive,
|
||||
reputation: state.family.reputation,
|
||||
updatedAt: new Date().toISOString(),
|
||||
version: '0.1.40'
|
||||
version: '0.1.41'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,8 +82,8 @@ describe('审计回归:P0 修复固化', () => {
|
||||
})
|
||||
|
||||
it('防御性修补后金钟罩不变(行为等价确认)', () => {
|
||||
// 0.1.40 天命·裂痕:天命系统+NPC宗主更替四模式+势力分裂+灰烬重生——全线受控变更重算
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('b050ad28')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('0b56c19b')
|
||||
// 0.1.41 天命·兑现:NPC 演化种子派生+pushNews 修复+天命 flag 全量接线——全线受控变更重算
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('2b6c46d2')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('34c76ced')
|
||||
})
|
||||
})
|
||||
|
||||
+8
-8
@@ -7,20 +7,20 @@ import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
|
||||
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
|
||||
*/
|
||||
// 0.1.40 天命·裂痕基线:天命系统+NPC宗主更替四模式+NPC势力分裂+灰烬重生新贵补位+建筑专精——
|
||||
// 全线受控变更重算(NPC演化逻辑增强+天命epilogue注册改变了时序/确定性序列)。
|
||||
// 0.1.41 天命·兑现基线:天命 flag 全量接线 + NPC 演化种子派生(零主 rng 消耗)+
|
||||
// pushNews 种子派生修复确定性破坏——全线受控变更重算。
|
||||
const GOLDEN: Record<string, Record<number, string>> = {
|
||||
'bell-seed-1': { 560: 'b050ad28', 1200: '379ad2e1', 2160: '2fada283' },
|
||||
'bell-seed-2': { 560: 'e3b4d40b', 1200: '5fb3aa1f', 2160: '50ad6832' },
|
||||
'bell-seed-3': { 560: '0b56c19b', 1200: '4737eedf', 2160: 'f597c050' }
|
||||
'bell-seed-1': { 560: '2b6c46d2', 1200: '0d909381', 2160: '5ec3b841' },
|
||||
'bell-seed-2': { 560: '34d82d30', 1200: 'fb24193f', 2160: '59b062df' },
|
||||
'bell-seed-3': { 560: '34c76ced', 1200: '9e36fa5b', 2160: '0dc87c13' }
|
||||
}
|
||||
|
||||
/** 第二金钟罩:自动 resolve 长跑("现实"世界——每 tick 处理待决事件;
|
||||
* 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */
|
||||
const GOLDEN_RESOLVED: Record<string, Record<number, string>> = {
|
||||
'bell-seed-1': { 560: '88f28074', 1200: 'd6b9cc85', 2160: 'c5b9b102' },
|
||||
'bell-seed-2': { 560: '5ca6b6a9', 1200: '6a66a750', 2160: 'c8e8e982' },
|
||||
'bell-seed-3': { 560: 'fb5bc0e8', 1200: '3e09f864', 2160: 'fae786ae' }
|
||||
'bell-seed-1': { 560: '073025e5', 1200: '444f2635', 2160: '73733808' },
|
||||
'bell-seed-2': { 560: 'c1b08533', 1200: 'c63d74cc', 2160: '22868a68' },
|
||||
'bell-seed-3': { 560: 'c0c477f9', 1200: 'adfad18e', 2160: 'df5bed84' }
|
||||
}
|
||||
|
||||
const TIERS = [
|
||||
|
||||
@@ -170,8 +170,8 @@ describe('GameFacade 门面', () => {
|
||||
|
||||
it('默认配置金钟罩不受门面化影响', () => {
|
||||
PACK.reset()
|
||||
// 0.1.40 天命·裂痕:天命系统+NPC宗主更替四模式+势力分裂+灰烬重生——全线受控变更重算
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('b050ad28')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('0b56c19b')
|
||||
// 0.1.41 天命·兑现:NPC 演化种子派生+pushNews 修复+天命 flag 全量接线——全线受控变更重算
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('2b6c46d2')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('34c76ced')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -261,18 +261,18 @@ describe('0.1.38 存档根治 · 存储链路回归(hybrid memory backend)',
|
||||
})
|
||||
})
|
||||
|
||||
describe('0.1.40 存档根治 · 版本号一致性', () => {
|
||||
it('slots.ts GAME_VERSION = 0.1.40', async () => {
|
||||
describe('0.1.41 存档根治 · 版本号一致性', () => {
|
||||
it('slots.ts GAME_VERSION = 0.1.41', async () => {
|
||||
const { buildSimpleMeta } = await import('../src/renderer/game/storage/slots')
|
||||
const w = World.create({ seed: 'ver-1', surname: '周', familyName: '周家', motto: 'm', difficulty: 'normal' })
|
||||
const meta = buildSimpleMeta(w.state, 1, 5)
|
||||
expect(meta.version).toBe('0.1.40')
|
||||
expect(meta.version).toBe('0.1.41')
|
||||
})
|
||||
|
||||
it('storeHelper metaFromState version = 0.1.40', async () => {
|
||||
it('storeHelper metaFromState version = 0.1.41', async () => {
|
||||
const { metaFromState } = await import('../src/renderer/ui/storeHelper')
|
||||
const w = World.create({ seed: 'ver-2', surname: '吴', familyName: '吴家', motto: 'm', difficulty: 'normal' })
|
||||
const meta = metaFromState(w.state, 1)
|
||||
expect(meta.version).toBe('0.1.40')
|
||||
expect(meta.version).toBe('0.1.41')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user