v0.1.20: 万世无谬(第二轮全量审计歼灭,1011 测试+双金钟罩)

【事件闸(机制重锤)】
- fire(w,id,priority):单闸+优先级——高优(raid/渡劫/大比)顶替被占者、被顶替入 eventQueue 下月兑现不丢失;普通被占返回 false 调用方冷却(raid 被吞也写 CD)
- pending 唯一写入入口:渡劫/破境丹改走 fire(防覆盖丢弃);名宿传薪年锚 flag(防同年重复 12 次)
- applyEventChoice 前置校验 pendingEvent===id(陈旧 Modal 二次结算防线)

【世界模型四修(真·活世界)】
- era 延续档:flow 权重和<1(留白=续任)——盛世能撑 30 年而非固定 25
- 潮汐供给符号修正:灵涨万物丰(原实现反了)+tide 双界 clamp
- driftMarket 弱锚定:rebalance 0.1→0.025——供需/era/灾年真实撬动价格(原五池被锚死±5%)
- 断供/池底单常量 poolFloorPct=0.35(消灭禁买暗雷带)

【知识防线(审计虫洞修复)】
- 指纹扩展:era/stance/prosperity/relationsWithOthers/calamityLeft/pendingEvent 全入 hash
- 第二金钟罩 GOLDEN_RESOLVED:自动 resolve 长跑锁“现实”世界(原指纹锁的只是“事件流冻结”世界)

【性能与存储】
- 存档节流(疾12/常6/缓4,手动即时)+ saveChronicle O(N)→O(1) 单查增量写(2160 月档告别每 tick 数千 OPFS)
- battles≤220/chronicle≤520 截断(stringify 线性膨胀止血);startNewGame/openState 停 timer 摘旧 bus

【杂项】actDirect 对齐 ACT_CATALOG 全 24 项;about 三版本统一 0.1.20;kernel.bus 死总线删;seed 改 RngHub.rollSeed;normalize 补 eraStartYear/overflux/distress;WorldPanel 买卖条件反转修复;音效与视效同闸

【测试】1011 全绿(42 套件):audit-0.1.20 9 例(事件闸/传薪/二次结算/era延续/潮汐符号/断供一致/弱锚定/指纹敏感)
This commit is contained in:
2026-08-23 15:14:28 +08:00
parent e853fbdb71
commit be6f2f5fe7
22 changed files with 286 additions and 97 deletions
+1 -1
View File
@@ -210,7 +210,7 @@ export default function SettingsPanel() {
· <br />
·
</div>
<div className="dim2" style={{ marginTop: 8 }}> 0.1.19 · Chronicle of the Immortal Clan</div>
<div className="dim2" style={{ marginTop: 8 }}> 0.1.20 · Chronicle of the Immortal Clan</div>
</div>
</div>
)
+2 -2
View File
@@ -122,8 +122,8 @@ export default function WorldPanel() {
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, maxHeight: 380, overflowY: 'auto' }}>
{news.length === 0 && <div className="dim2"></div>}
{news.map((row, i) => {
const canBuy = row.kind === 'quote' && row.itemId && (w.state.family.inventory[row.itemId] ?? 0) >= 5
const canSell = row.kind === 'quote' && row.itemId && w.state.family.stones >= marketPrice(w, row.itemId) * 5
const canBuy = row.kind === 'quote' && row.itemId && w.state.family.stones >= marketPrice(w, row.itemId) * 5
const canSell = row.kind === 'quote' && row.itemId && (w.state.family.inventory[row.itemId] ?? 0) >= 5
const isCrisis = row.kind === 'calamity'
return (
<div key={i} className="ch-item" style={{ padding: '5px 8px' }}>
+39 -10
View File
@@ -8,6 +8,9 @@ import { getSlotManager, getSaveSlot } from '../game/storage/db'
import { metaFromState, updateSlotMeta } from './storeHelper'
import { GameFacade, ActName } from '../game/engine/runtime/ApiFacade'
import { setSoundEnabled, sPaper, sGood, sBad, sWar, sBell, sTick } from './sound'
import { buyItem as marketBuy, sellItem as marketSell, buyTechnique as marketTechnique } from '../game/engine/sim/Market'
import { giftNpc as dipGift, makePeace as dipPeace, marryNpcFamily as dipMarry } from '../game/engine/runtime/Systems/diplomacy'
import { sendMission as sendMissionDirect, recallAll as recallAllDirect } from '../game/engine/runtime/Systems/missions'
import { seasonOf } from '../game/data/season'
export type Screen = 'boot' | 'newgame' | 'game'
@@ -145,6 +148,7 @@ export const useGameStore = create<GameStore>((set, get) => ({
go: (screen) => set({ screen }),
startNewGame: async (opts, slot) => {
stopTimer()
const engine = new GameEngine({ seed: opts.seed })
const world = engine.world
set({ world, engine, slot, screen: 'game', panel: 'family', logFeed: [], battleView: undefined, pendingEventId: undefined, pendingEventDef: undefined, revision: 1, speed: 0, gameOverReason: undefined, paperReport: undefined, toast: undefined, selectedMemberId: undefined, selectedMissionDef: undefined })
@@ -225,7 +229,11 @@ export const useGameStore = create<GameStore>((set, get) => ({
w.syncRng()
sTick()
set((s) => ({ revision: s.revision + 1 }))
await Stash.save(st, '自动')
// 自动速度节流落盘:疾进 12 tick/常速 6 tick/缓行 4 tick;手动单步即时落盘
const saveEvery = st.speed >= 3 ? 12 : st.speed === 2 ? 6 : 4
if (st.speed === 0 || w.state.totalTicks % saveEvery === 0) {
await Stash.save(st, '自动')
}
} catch (e) {
// 出险保护:先落一档"出险前"现场再提示回档
console.error('[advance-crash]', e)
@@ -449,7 +457,10 @@ function makeBus(st: GameStore): WorldEventBus {
onFx: (em) => {
void import(/* @vite-ignore */ './fx').then((m) => m.ensureFx().emit(em.kind as never, em.source ? { src: em.source } : undefined))
// 音效语义补充:引擎语义点 → 声响(onLog kind 兜底仍保留)
void import(/* @vite-ignore */ './sound').then((snd) => {
void Promise.all([import('./sound'), import('./fx')]).then(([snd, fxM]) => {
// 声音与视效同闸:gate 屏蔽(std 无 blade)时无声;门内再播
const g = fxM.ensureFx()
if (!g.allowed(em.kind as never)) return
if (em.kind === 'blade') snd.sWar()
else if (em.kind === 'pulse') snd.sGong()
else if (em.kind === 'spark') snd.sBell()
@@ -467,14 +478,32 @@ function makeBus(st: GameStore): WorldEventBus {
}
function actDirect(w: World, name: ActName, payload: import('../game/engine/runtime/ApiFacade').ActPayload): boolean {
if (name === 'legacy.resolve') return (w.resolveLegacyNow(), true)
if (name === 'estate.rite') return w.ancestralRite()
if (name === 'estate.sutra') return w.seekSutra()
if (name === 'estate.build' && payload.building) return w.build(payload.building)
if (name === 'estate.upgrade' && payload.building) return w.upgrade(payload.building)
if (name === 'member.post' && payload.memberId) return w.assignPost(payload.memberId, payload.post)
if (name === 'member.marry' && payload.memberId && payload.targetId) return w.marryTo(payload.memberId, payload.targetId)
return false
switch (name) {
case 'legacy.resolve': return (w.resolveLegacyNow(), true)
case 'estate.rite': return w.ancestralRite()
case 'estate.sutra': return w.seekSutra()
case 'estate.build': return payload.building ? w.build(payload.building) : false
case 'estate.upgrade': return payload.building ? w.upgrade(payload.building) : false
case 'head.set': return payload.memberId ? (w.assignHead(payload.memberId), true) : false
case 'member.meditate': return payload.memberId ? (w.setMeditation(payload.memberId, !!payload.count), true) : false
case 'member.technique': return payload.memberId && payload.tech ? (w.giveTechnique(payload.memberId, payload.tech), true) : false
case 'member.equip': return payload.memberId && payload.item ? (w.equip(payload.memberId, payload.item), true) : false
case 'member.pill': return payload.memberId && payload.pill ? (w.takePill(payload.memberId, payload.pill), true) : false
case 'member.advance': return payload.memberId ? (w.assistedBreakthrough(payload.memberId), true) : false
case 'member.marry': return payload.memberId && payload.targetId ? w.marryTo(payload.memberId, payload.targetId) : false
case 'member.post': return payload.memberId ? w.assignPost(payload.memberId, payload.post) : false
case 'market.buy': return payload.item ? marketBuy(w, payload.item, payload.count ?? 1) : false
case 'market.sell': return payload.item ? marketSell(w, payload.item, payload.count ?? 1) : false
case 'market.tech': return payload.tech ? marketTechnique(w, payload.tech, payload.stones ?? 0) : false
case 'craft.pill': return payload.item === 'ningyuan' ? w.craftPill('ningyuan') : payload.item === 'pojing' ? w.craftPill('pojing') : payload.item === 'qiyuan' ? w.craftPill('qiyuan') : false
case 'diplomacy.gift': return payload.npcId ? dipGift(w, payload.npcId, payload.stones ?? 0) : false
case 'diplomacy.peace': return payload.npcId ? dipPeace(w, payload.npcId) : false
case 'diplomacy.taunt': return payload.npcId ? w.tauntNpc(payload.npcId) : false
case 'diplomacy.marry': return payload.npcId ? dipMarry(w, payload.npcId) : false
case 'expedition.send': return payload.mission ? sendMissionDirect(w, payload.mission, payload.squad ?? []) : false
case 'expedition.recall': return (recallAllDirect(w, payload.memberId ?? ''), true)
default: return false
}
}
const Stash = {