v0.1.15: 深度审计加固(7 P0 + 20+ P1 歼灭)

【引擎层 P0】
- GameEngine.restore 双重时钟注册(回档后整月双跑)→ restore 重建内核+回置 rng 状态
- engineFromSnapshot 必崩(globalThis.__state 从未设)→ 修正带 seed 重建
- DEV 性能分支二次执行整月管线 → 只读告警(绝不重跑)
- 引擎 advance 遇待决事件死锁 → resolvePending(idx) 通道(自动化/测试推进)

【WorldSim 大修】
- NPC 换代文案垃圾(n-danxin行情降50%)→ 换代快讯专属文案
- secretQi 假闭环(纯写无读)→ 全秘境初始化+missions 统一 consumeSecretQi
- 快讯节流自锁(lastNewsMonth 恒 1 月)→ totalTicks%24
- power 月度收敛坍塌(1400±3)→ 换代/年首化+年度成长
- relation 双重拉零(4 家全 0)→ 交还外交年度漂移
- 市场基准 5 处重复 → POOL_BASE 单一权威;灾年 stale 清理;drift 各池独立噪声
- worldsim 能力卡补全 + normalize 兜底(worldSim/stats 子字段)

【UI/动效/存储】
- ModalShell 三条 CSS 规则真正落地(close 定位/body 滚动/footer 独立底排)——0.1.12 声明未实施项
- fx-canvas:resize 监听移除/mistTimer 清零/reduced 联动/soft 档无雾 + drift 走 gate(过滤器)
- 档位 density 脱钩修复;顶栏“止”钮补 onClick;版本号三处统一 0.1.15
- importAll 清 chronicle 表;自动推进季度落盘(手动逐次)
- 性能:LogFeed memo / Genealogy computeGenealogy memo / Chronicle deps 修正;死码清除(lastPhaseStats/clockFromKernel/哨兵函数等 12 处)
- 删除误入库的“p”游离文件

【测试】979 全绿(36 套件);金钟罩三档 0.1.15 基线固化;
typecheck/build 通过
This commit is contained in:
2026-08-23 13:52:51 +08:00
parent 26bd61fba0
commit c5fa2a3357
26 changed files with 220 additions and 208 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ export function EventModal() {
setSquadSel(squadPool.slice(0, 4).map((c) => c.id))
return
}
applyEventChoice(world, pendingEventId, idx, isRaid ? squadSel : undefined)
applyEventChoice(world, pendingEventId, idx, isRaid ? squadSel : undefined, undefined, isRaid ? formation : undefined)
closeModal()
const sp = useGameStore.getState().speed
if (sp > 1) setSpeed(1)
+6 -5
View File
@@ -1,12 +1,13 @@
import { memo } from 'react'
import { useGameStore } from '../store'
export function LogFeed() {
export const LogFeed = memo(function LogFeed() {
const feed = useGameStore((s) => s.logFeed)
const ref = useGameStore((s) => s.revision)
void ref
void useGameStore((s) => s.revision)
const reversed = feed.slice().reverse()
return (
<div className="feed">
{feed.slice().reverse().map((item) => (
{reversed.map((item) => (
<div key={item.id} className={`feed-item k-${item.kind}`}>
<span className="feed-date">{item.year}{item.month}</span>
{item.text}
@@ -14,4 +15,4 @@ export function LogFeed() {
))}
</div>
)
}
})
@@ -57,11 +57,6 @@ export function MemberCard({ c }: { c: Character }) {
)
}
function postLabel(c: Character): string {
const map: Record<string, string> = { head: '家主', elder: '长老', guardian: '供奉', steward: '执事', master: '掌教' }
return map[c.post ?? ''] ?? ''
}
const C_STATE: Record<string, string> = {
idle: '',
meditation: 'good',
+11 -1
View File
@@ -79,12 +79,20 @@ function spawn(kind: 'gold' | 'mist' | 'light', x?: number, y?: number): void {
if (particles.length > density * 6) particles.splice(0, particles.length - density * 6) // 竞态防胀
}
import { ensureFx } from './fx'
function frame(): void {
if (!running) return
const g = ensureFx()
if (g.preferReduced) {
// reduced-motion:粒子层静默,但保留单次清屏
raf = window.requestAnimationFrame(frame as FrameRequestCallback)
return
}
ctx?.clearRect(0, 0, window.innerWidth, window.innerHeight)
const dt = 1
mistTimer += 1
if (mistTimer % 18 === 0 && particles.length < density) spawn('mist')
if (mistTimer % 18 === 0 && particles.length < density && g.mode !== 'soft') spawn('mist')
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i]
p.life += dt
@@ -134,6 +142,8 @@ export function stopFxLayer(): void {
if (raf) cancelAnimationFrame(raf)
raf = 0
particles = []
mistTimer = 0
window.removeEventListener('resize', resize)
}
export function spawnBurst(kind: 'spark' | 'blade' | 'ripple', origin?: string): void {
+1 -2
View File
@@ -1,6 +1,5 @@
import { useGameStore } from '../store'
import { useMemo, useState } from 'react'
import { ChronicleEntry } from '../../game/types/domain'
const CAT_NAME: Record<string, string> = {
birth: '诞庆',
@@ -57,7 +56,7 @@ export default function ChroniclePanel() {
const all = [...world.state.chronicle]
const filtered = filter === 'all' ? all : all.filter((e) => e.category === filter)
return filtered.slice().sort((a, b) => (b.year - a.year) || (b.month - a.month))
}, [world, filter, revision])
}, [world, filter, world ? world.state.chronicle.length : 0])
if (!world) return null
const battles = world.state.battles.slice().reverse()
const battle = battles.find((b) => b.id === battleId)
+2 -3
View File
@@ -2,7 +2,7 @@ import { useGameStore } from '../store'
import { computeGenealogy } from '../../game/engine/narrative/genealogy'
import { describeRealm } from '../../game/data/realms'
import { Character, FamilyState } from '../../game/types/domain'
import { useState } from 'react'
import { useMemo, useState } from 'react'
import { MemberModal } from '../components/MemberModal'
export default function GenealogyPanel() {
@@ -16,7 +16,7 @@ export default function GenealogyPanel() {
if (!world) return null
const w = world
const s = w.state
const rows = computeGenealogy(w)
const rows = useMemo(() => computeGenealogy(w), [w, revision])
const deceased = !deceasedTab
? []
: Object.values(s.members).filter((c) => !c.alive).sort((a, b) => (a.deathYear ?? 0) - (b.deathYear ?? 0))
@@ -125,4 +125,3 @@ const POST_NAME: Record<string, string> = {
master: '掌教'
}
export type { FamilyState }
-2
View File
@@ -1,13 +1,11 @@
import { useState } from 'react'
import { useGameStore } from '../store'
import { computeLegacy, resolveLegacy } from '../../game/engine/narrative/legacy'
import { resolveLegacy as doesIt } from '../../game/engine/narrative/legacy'
import { ResolveModal } from '../components/ResolveModal'
import { buildBiography } from '../../game/engine/narrative/biography'
import { yearAxis, decadeLabel } from '../../game/engine/narrative/yearaxis'
import { buildReport, verdictLine } from '../../game/engine/narrative/legacyreport'
void doesIt
export default function LegacyPanel() {
const world = useGameStore((s) => s.world)
+1 -2
View File
@@ -210,10 +210,9 @@ export default function SettingsPanel() {
· <br />
·
</div>
<div className="dim2" style={{ marginTop: 8 }}> 0.1.12 · Chronicle of the Immortal Clan</div>
<div className="dim2" style={{ marginTop: 8 }}> 0.1.15 · Chronicle of the Immortal Clan</div>
</div>
</div>
)
}
void buildBiography
+1 -1
View File
@@ -74,7 +74,7 @@ export default function GameScreen() {
</span>
</div>
<div className="speed-ctl">
<button className={`speed-btn ${speed === 0 ? 'sel' : ''}`} title="暂停时间"></button>
<button className={`speed-btn ${speed === 0 ? 'sel' : ''}`} title="暂停时间" onClick={() => setSpeed(0)}></button>
<button className={`speed-btn ${speed === 1 ? 'gel' : ''}`} title="缓行(约3月/秒)" onClick={() => setSpeed(1)}></button>
<button className={`speed-btn ${speed === 2 ? 'gel' : ''}`} title="常速(约半秒/月)" onClick={() => setSpeed(2)}></button>
<button className={`speed-btn ${speed === 3 ? 'gel' : ''}`} title="疾进(约1月/秒)" onClick={() => setSpeed(3)}></button>
+13 -10
View File
@@ -7,8 +7,7 @@ import { BattleLog, ChronicleEntry, GameState, LogItem, SaveMeta, YearlyReport,
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, sGong, sClick, sTick } from './sound'
import type { PhaseStat } from '../game/engine/kernel/clock'
import { setSoundEnabled, sPaper, sGood, sBad, sWar, sBell, sTick } from './sound'
import { seasonOf } from '../game/data/season'
export type Screen = 'boot' | 'newgame' | 'game'
@@ -69,7 +68,6 @@ export interface GameStore {
}
const LOG_CAP = 260
const lastPhaseStats = new Map<World, PhaseStat[]>()
let logSeq = 1
function staddEventLog(name: string): void {
@@ -195,12 +193,20 @@ export const useGameStore = create<GameStore>((set, get) => ({
const prevMonth = w.state.month
w.advanceMonth()
const elapsed = performance.now() - t0
if (import.meta.env?.DEV && elapsed > 60) {
// 仅统计(只读 clock 快照),禁止再跑整月管线
const stats = w.clock.snapshot()
if (stats && stats.length) console.warn('[perf] tick 超预算', elapsed.toFixed(1) + 'ms')
}
try {
const nextInv = { ...w.state.family.inventory, stones: w.state.family.stones }
const FxGate = (await import('../game/engine/kernel/fxqueue')).FxGate
const deltas = FxGate.deltas(prevInv, nextInv)
const windowRef = window as unknown as { dispatchEvent: (e: Event) => void }
windowRef.dispatchEvent(new CustomEvent('fx:drift', { detail: deltas }))
const fxmod = await import('./fx')
if (!fxmod.ensureFx().preferReduced) {
const windowRef = window as unknown as { dispatchEvent: (e: Event) => void }
windowRef.dispatchEvent(new CustomEvent('fx:drift', { detail: deltas }))
}
const seasonNow = seasonOf(w.state.month)
const seasonPrev = seasonOf(prevMonth)
if (seasonNow !== seasonPrev) {
@@ -212,11 +218,8 @@ export const useGameStore = create<GameStore>((set, get) => ({
// fx 为增强项,失败不阻塞推进
}
if (import.meta.env?.DEV && elapsed > 60) {
// 性能预算:单 tick 超 60ms 提示最慢 phase
const stats = w.clock.stepMonthly(w)
void stats
const slowest = lastPhaseStats.get(w)?.sort((a, b) => b.ms - a.ms)[0]
if (slowest) console.warn('[perf] tick 超预算', elapsed.toFixed(1) + 'ms', '最慢:', slowest.phase, slowest.ms.toFixed(1) + 'ms')
// 性能预算:仅告警(绝不重跑整月管线)
console.warn('[perf] tick 超预算', elapsed.toFixed(1) + 'ms')
}
w.syncRng()
sTick()
+50
View File
@@ -1305,6 +1305,56 @@ html[data-blade] .battle-lines {
border-bottom: 1px solid rgba(120, 98, 55, 0.25);
padding-bottom: 14px;
}
.modal-close {
position: absolute;
top: 14px;
right: 16px;
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(180deg, #3a2d1a, #241a0d);
border: 1px solid #8a6d2f;
color: #e8d5a4;
font-size: 1.3rem;
cursor: pointer;
font-family: inherit;
z-index: 6;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,240,200,0.15);
transition: all 0.16s;
}
.modal-close:hover {
border-color: var(--vermilion);
color: #f4d8c0;
transform: scale(1.06);
}
.modal-body {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding-right: 4px;
width: 100%;
}
.modal-footer {
flex-shrink: 0;
margin: 14px -15px -8px;
padding: 12px 20px 10px;
border-top: 1px solid rgba(140, 100, 50, 0.45);
border-radius: 0 0 4px 4px;
display: flex;
justify-content: flex-end;
gap: 10px;
align-items: center;
flex-wrap: wrap;
background: linear-gradient(180deg, rgba(122, 96, 54, 0.12), rgba(90, 70, 40, 0.22));
box-shadow: inset 0 1px 0 rgba(255, 248, 230, 0.25);
}
.modal-title {
padding-right: 44px;
}
.modal-opts {
display: flex;
flex-direction: column;