(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' }
diff --git a/src/renderer/game/storage/slots.ts b/src/renderer/game/storage/slots.ts
index 8b656c1..2a6adcd 100644
--- a/src/renderer/game/storage/slots.ts
+++ b/src/renderer/game/storage/slots.ts
@@ -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
diff --git a/src/renderer/ui/panels/LegacyPanel.tsx b/src/renderer/ui/panels/LegacyPanel.tsx
index a0d0b0a..655814e 100644
--- a/src/renderer/ui/panels/LegacyPanel.tsx
+++ b/src/renderer/ui/panels/LegacyPanel.tsx
@@ -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() {
+
@@ -120,6 +122,8 @@ export default function LegacyPanel() {
{view === 'scroll' && }
+ {view === 'cscroll' && }
+
{view === 'chronicle' && (
生卒与突破年表
@@ -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 (
+
+
族史卷(天命·族事·天下 串联)
+ {scroll.length === 0 ? (
+
开卷尚浅,待岁月沉淀后再启此卷。
+ ) : (
+
+ {scroll.map((e, i) => (
+
+ {e.year}年
+ {scrollKindName(e.kind)}
+ {e.text}
+
+ ))}
+
+ )}
+
+ 族史卷将天命决策、族中大事与天下大事记串联展示——以时间为经、以事件为纬。
+
+
+ )
+}
+
function dimName(k: string): string {
const map: Record
= { renXing: '人兴', daoXing: '道兴', weiMing: '威名', xiangHuo: '香火', yinGuo: '因果' }
return map[k] ?? k
diff --git a/src/renderer/ui/panels/SettingsPanel.tsx b/src/renderer/ui/panels/SettingsPanel.tsx
index ab1354c..263b865 100644
--- a/src/renderer/ui/panels/SettingsPanel.tsx
+++ b/src/renderer/ui/panels/SettingsPanel.tsx
@@ -354,7 +354,7 @@ export default function SettingsPanel() {
· 「外交」与四邻结好联姻;仇雠之族隔岁来犯,打得赢名望大涨,打不赢蚀钱伤丁。
· 「史书」自动记述繁华与凋零——百年之后,后人翻开这一卷家族志,见代代薪火、历历雪泥。
- 版本 0.1.40 · Chronicle of the Immortal Clan
+ 版本 0.1.41 · Chronicle of the Immortal Clan
)
diff --git a/src/renderer/ui/panels/TerritoryPanel.tsx b/src/renderer/ui/panels/TerritoryPanel.tsx
index 2febf6b..55ae922 100644
--- a/src/renderer/ui/panels/TerritoryPanel.tsx
+++ b/src/renderer/ui/panels/TerritoryPanel.tsx
@@ -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() {
{built ? `${level}级` : ''}
{def.desc}
+ {/* 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 (
+
+ {bonusLabels.map((b, i) => (
+
+ {b.name}: {b.base.toFixed(2)}
+ {isThisSpecialty && →x{SPECIALTY_MULT} = {(b.base * SPECIALTY_MULT).toFixed(2)}}
+ {!isThisSpecialty && specialtyId && (设为专精可至 {(b.base * SPECIALTY_MULT).toFixed(2)})}
+
+ ))}
+
+ )
+ })()}
{produce && Object.keys(produce).length > 0 && (
{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 = {
+ craftChance: '成丹率',
+ unlockGrade: '解锁品阶',
+ expBonus: '修炼加成',
+ powerBonus: '战力加成',
+ repBonus: '声望'
+ }
+ return map[key] ?? key
+}
diff --git a/src/renderer/ui/storeHelper.ts b/src/renderer/ui/storeHelper.ts
index 2cf5d66..2a93698 100644
--- a/src/renderer/ui/storeHelper.ts
+++ b/src/renderer/ui/storeHelper.ts
@@ -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'
}
}
diff --git a/tests/audit-regression.test.ts b/tests/audit-regression.test.ts
index e6636f5..abe1e05 100644
--- a/tests/audit-regression.test.ts
+++ b/tests/audit-regression.test.ts
@@ -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')
})
})
diff --git a/tests/clock.test.ts b/tests/clock.test.ts
index b15af50..7a57148 100644
--- a/tests/clock.test.ts
+++ b/tests/clock.test.ts
@@ -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> = {
- '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> = {
- '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 = [
diff --git a/tests/facade-registry.test.ts b/tests/facade-registry.test.ts
index 8714970..7699add 100644
--- a/tests/facade-registry.test.ts
+++ b/tests/facade-registry.test.ts
@@ -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')
})
})
diff --git a/tests/storage-resilience-0.1.38.test.ts b/tests/storage-resilience-0.1.38.test.ts
index 40d384f..15787b8 100644
--- a/tests/storage-resilience-0.1.38.test.ts
+++ b/tests/storage-resilience-0.1.38.test.ts
@@ -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')
})
})