v0.1.34a(修正): 新秘境改种子派生伪随机——零 rng 消耗(修复跨世界/跨跑分叉)

B0 原用 World.rng.chance/pick/int——序列漂移疑虑(实际为模块级全局 EXTRA_MISSIONS 残留污染);
现改为 seed 哈希派生(year/npcSuccessions 异或哈希——纯确定性零 rng),金钟罩序列零伤;
SecretQi/mission def 全走世界状态 s.wonders(每档独立),确定性复跑矩阵恢复绿;
金钟罩 0.1.34 基线固化(功法流派/时节共鸣/warFatigue/换代/断供受控变更——值变序列受控重算 62 例绿)
This commit is contained in:
2026-08-23 22:05:53 +08:00
parent a491b9c54a
commit 3fb64f6e78
7 changed files with 33 additions and 23 deletions
@@ -15,7 +15,8 @@ export function missionTick(w: World): void {
m.stageMonth++ m.stageMonth++
if (m.stageMonth < 2) continue if (m.stageMonth < 2) continue
const def = missionById(m.defId) const wonderDef = w.state.worldSim?.wonders?.[m.defId] as MissionDef | undefined
const def = wonderDef ?? missionById(m.defId)
const stage = def.stages[m.stage] const stage = def.stages[m.stage]
if (!stage || m.stageMonth < stage.months) continue if (!stage || m.stageMonth < stage.months) continue
+15 -10
View File
@@ -4,7 +4,7 @@ import { WorldSimState, NpcDynamics, WORLDSIM, ERA_CONF, makeWorldSimState, NpcS
import { pack } from '../../data/registry' import { pack } from '../../data/registry'
import { ITEMS } from '../../data/items' import { ITEMS } from '../../data/items'
import { npcById, getNpcDefs, registerNpcDef, unregisterNpcDef } from '../../data/npcs' import { npcById, getNpcDefs, registerNpcDef, unregisterNpcDef } from '../../data/npcs'
import { allMissions, addMission, WONDER_TEMPLATES, type MissionDef } from '../../data/secrets' import { allMissions, WONDER_TEMPLATES, type MissionDef } from '../../data/secrets'
import { MAJORS, MAJOR_ORDER } from '../../data/realms' import { MAJORS, MAJOR_ORDER } from '../../data/realms'
const MARKET_IDS = ['lingcao', 'lingkuang', 'beastcore', 'lingyu', 'lingmu', 'shoupi', 'lingguo', 'pill-qiyuan', 'pill-ningyuan'] const MARKET_IDS = ['lingcao', 'lingkuang', 'beastcore', 'lingyu', 'lingmu', 'shoupi', 'lingguo', 'pill-qiyuan', 'pill-ningyuan']
@@ -17,9 +17,9 @@ export class WorldSim {
if (!w.state.worldSim) w.state.worldSim = initSim(w) if (!w.state.worldSim) w.state.worldSim = initSim(w)
const ws = w.state.worldSim as WorldSimState const ws = w.state.worldSim as WorldSimState
if (!ws.secretQi || Object.keys(ws.secretQi).length === 0) { if (!ws.secretQi || Object.keys(ws.secretQi).length === 0) {
const defs = allMissions() const defs = [...allMissions(), ...Object.values(ws.wonders ?? {})]
ws.secretQi = {} ws.secretQi = {}
for (const m of defs) ws.secretQi[m.id] = 50 for (const m of defs) ws.secretQi[(m as { id: string }).id] = 50
} }
return ws return ws
} }
@@ -130,18 +130,23 @@ export class WorldSim {
npcTrade(this.w, s, rng.next()) // A4NPC 按 sells/buys 与池交易 npcTrade(this.w, s, rng.next()) // A4NPC 按 sells/buys 与池交易
driftMarket(s, rng.next(), this.w) // 波动项(保留噪声弹性) driftMarket(s, rng.next(), this.w) // 波动项(保留噪声弹性)
// ---- B0. 新秘境灵机(0.1.34 万象归元:年首机率——乱世×2) ----
// ---- B0. 新秘境灵机(0.1.34 万象归元:种子派生伪随机——零 rng 消耗,确定性天生) ----
if (this.w.state.month === 1) { if (this.w.state.month === 1) {
const era2 = s.era === 'luanshi' ? 2 : 1 const era2 = s.era === 'luanshi' ? 2 : 1
if (rng.chance(worldNum('newSecretChance') * era2)) { const ch = worldNum('newSecretChance') * era2
const tpl = rng.pick(WONDER_TEMPLATES) let sh = 0
const seedS = rng.int(1000, 9999) for (let i = 0; i < this.w.state.seed.length; i++) sh = (sh * 31 + this.w.state.seed.charCodeAt(i)) >>> 0
const pick = ((sh ^ (this.w.state.year * 2654435761) ^ (s.npcSuccessions * 40503)) >>> 0) % 1000 / 1000
if (pick < ch) {
const tpl = WONDER_TEMPLATES[(sh + this.w.state.year) % WONDER_TEMPLATES.length]
const def: MissionDef = { const def: MissionDef = {
...tpl, ...tpl,
id: `m-wonder-${seedS}`, id: `m-wonder-${(sh + this.w.state.year * 7919) % 9000 + 1000}`,
name: `${tpl.icon}地灵诏——${'古幽玄凌'}${rng.int(1, 9)}` name: `${tpl.icon}地灵诏——${['古', '幽', '玄', '凌'][(sh + this.w.state.year) % 4]}`
} }
addMission(def) s.wonders = s.wonders ?? {}
s.wonders[def.id] = def as unknown as Record<string, unknown>
s.secretQi[def.id] = Math.max(50, s.secretQi[def.id] ?? 50) s.secretQi[def.id] = Math.max(50, s.secretQi[def.id] ?? 50)
s.newsFeed.push({ year: this.w.state.year, month: 1, src: '天机', text: `${def.name} 现世——灵光冲霄,远近皆见。`, kind: 'annal' }) s.newsFeed.push({ year: this.w.state.year, month: 1, src: '天机', text: `${def.name} 现世——灵光冲霄,远近皆见。`, kind: 'annal' })
this.w.log('info', `【天下】有异宝出世:${def.name}`) this.w.log('info', `【天下】有异宝出世:${def.name}`)
@@ -58,6 +58,8 @@ export interface WorldSimState {
shortage?: Record<string, number> shortage?: Record<string, number>
/** 断供已播年(id→年——仅始发广播,持续断供不刷屏) */ /** 断供已播年(id→年——仅始发广播,持续断供不刷屏) */
shortageWarned?: Record<string, number> shortageWarned?: Record<string, number>
/** 0.1.34 新秘境定义(世界状态内——每档独立,绝无全局跨世界污染) */
wonders?: Record<string, unknown>
/** 天下史书(十年一鉴独立留档,免 newsFeed 裁剪;上限 80 页) */ /** 天下史书(十年一鉴独立留档,免 newsFeed 裁剪;上限 80 页) */
worldAnnals?: { year: number; text: string }[] worldAnnals?: { year: number; text: string }[]
/** 世鉴(50/100 年宏观:开局 vs 现在/era 路径/最强家更替——独立数组零漂) */ /** 世鉴(50/100 年宏观:开局 vs 现在/era 路径/最强家更替——独立数组零漂) */
+2
View File
@@ -229,6 +229,8 @@ export interface GameState {
overfluxYear?: number overfluxYear?: number
warFatigue?: number warFatigue?: number
shortage?: Record<string, number> shortage?: Record<string, number>
shortageWarned?: Record<string, number>
wonders?: Record<string, unknown>
distress?: { id: string; year: number; month: number } distress?: { id: string; year: number; month: number }
worldAnnals?: { year: number; text: string }[] worldAnnals?: { year: number; text: string }[]
newsFeed?: { year: number; month: number; src: string; text: string; itemId?: string; kind?: string }[] newsFeed?: { year: number; month: number; src: string; text: string; itemId?: string; kind?: string }[]
+2 -2
View File
@@ -82,7 +82,7 @@ describe('审计回归:P0 修复固化', () => {
}) })
it('防御性修补后金钟罩不变(行为等价确认)', () => { it('防御性修补后金钟罩不变(行为等价确认)', () => {
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('0a76a33f') expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('70f0223f')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('03184a16') expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('1788581c')
}) })
}) })
+8 -8
View File
@@ -7,20 +7,20 @@ import { World } from '../src/renderer/game/engine/runtime/World'
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。 * 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。 * 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
*/ */
// 0.1.33 闭环·止血基线:符箓开战生效(战力乘量+山符减伤)/灾年因子真正接入/ // 0.1.34 万象归元基线:功法流派效果(crit/guard/trib/secret/元素时节共鸣/
// 幽灵物品归位(新丹方/玉澜刃)+ 符箓坊(消耗式产符)后固化 // warFatigue/换代恩义清/断供/新秘境(零 rng 派生)后固化(受控变更重算)
const GOLDEN: Record<string, Record<number, string>> = { const GOLDEN: Record<string, Record<number, string>> = {
'bell-seed-1': { 560: '0a76a33f', 1200: 'c9407e76', 2160: '4bc550bd' }, 'bell-seed-1': { 560: '70f0223f', 1200: 'da91696b', 2160: '952c55c5' },
'bell-seed-2': { 560: 'b1b4d35b', 1200: 'fcc6e262', 2160: '1af450b7' }, 'bell-seed-2': { 560: '700fd9dd', 1200: 'd04b0989', 2160: 'c8aae012' },
'bell-seed-3': { 560: '03184a16', 1200: 'c73bb4b1', 2160: 'fca9a59f' } 'bell-seed-3': { 560: '1788581c', 1200: 'cc2ed227', 2160: '991d2684' }
} }
/** 第二金钟罩:自动 resolve 长跑("现实"世界——每 tick 处理待决事件; /** 第二金钟罩:自动 resolve 长跑("现实"世界——每 tick 处理待决事件;
* 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */ * 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */
const GOLDEN_RESOLVED: Record<string, Record<number, string>> = { const GOLDEN_RESOLVED: Record<string, Record<number, string>> = {
'bell-seed-1': { 560: '4b0f1f81', 1200: '430d6281', 2160: 'ba0d3816' }, 'bell-seed-1': { 560: 'd03824ce', 1200: '237e93f3', 2160: 'cee2dc9c' },
'bell-seed-2': { 560: '8865b178', 1200: 'bba0d797', 2160: '6205c6f1' }, 'bell-seed-2': { 560: 'db32bc97', 1200: '7b8177d8', 2160: '8257b4e1' },
'bell-seed-3': { 560: 'fa736e3c', 1200: '819b9def', 2160: 'a624ced8' } 'bell-seed-3': { 560: '4c8b66c5', 1200: '39fc71ea', 2160: '384b1586' }
} }
const TIERS = [ const TIERS = [
+2 -2
View File
@@ -170,7 +170,7 @@ describe('GameFacade 门面', () => {
it('默认配置金钟罩不受门面化影响', () => { it('默认配置金钟罩不受门面化影响', () => {
PACK.reset() PACK.reset()
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('0a76a33f') expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('70f0223f')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('03184a16') expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('1788581c')
}) })
}) })