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:
@@ -22,7 +22,7 @@ export type EngineStatus = 'idle' | 'running' | 'gameover'
|
||||
* UI/测试/未来工具仅需认识 GameEngine。
|
||||
*/
|
||||
export class GameEngine {
|
||||
readonly kernel: Kernel
|
||||
kernel: Kernel
|
||||
readonly datapack: typeof PACK
|
||||
world: World
|
||||
facade: ApiFacade
|
||||
@@ -31,7 +31,7 @@ export class GameEngine {
|
||||
private tickSinceSave = 0
|
||||
|
||||
constructor(opts: GameEngineOptions = {}) {
|
||||
this._seed = opts.seed ?? `cotyc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||
this._seed = opts.seed ?? `cotyc-${Date.now().toString(36)}-${Math.floor(Math.random() * 1296).toString(36)}`
|
||||
this.kernel = makeKernel(this._seed)
|
||||
this.datapack = PACK
|
||||
if (opts.datapack) this.datapack.override(opts.datapack)
|
||||
@@ -55,8 +55,16 @@ export class GameEngine {
|
||||
|
||||
advance(): void {
|
||||
if (this.world.state.gameOver) return
|
||||
if (this.world.state.pendingEvent) return // 事件待决:停下(用户/自动化应 resolvePending)
|
||||
this.world.advanceMonth()
|
||||
this.tickSinceSave++
|
||||
}
|
||||
|
||||
/** 处理当前待决事件(默认选第一项);无待决则 no-op */
|
||||
resolvePending(idx = 0): boolean {
|
||||
const pid = this.world.state.pendingEvent
|
||||
if (!pid) return false
|
||||
// 直接走门面 apply 通路的引擎包装(不依赖 UI)
|
||||
return this.world.applyEventChoice(pid, idx)
|
||||
}
|
||||
|
||||
syncRng(): void {
|
||||
@@ -90,9 +98,12 @@ export class GameEngine {
|
||||
return JSON.parse(JSON.stringify(this.world.state)) as GameState
|
||||
}
|
||||
|
||||
/** 从快照恢复(可回放) */
|
||||
/** 从快照恢复(可回放)——重建内核以避免时钟双注册,并回置 rng 保真 */
|
||||
restore(snap: GameState): void {
|
||||
normalizeGameState(snap)
|
||||
// 新内核(同一 seed 派生)+ 回写存档 rng 快照 —— 回放保真
|
||||
this.kernel = makeKernel(snap.seed)
|
||||
this.kernel.rng.state = { ...snap.rng }
|
||||
const newWorld = new World(snap, [], this.kernel)
|
||||
newWorld.out = this.world.out
|
||||
this.world = newWorld
|
||||
@@ -110,7 +121,7 @@ export class GameEngine {
|
||||
|
||||
/** 已用旧内核时钟注册的世界快照,反序列化时公用(便捷入口) */
|
||||
export function engineFromSnapshot(state: GameState): GameEngine {
|
||||
const eng = new GameEngine({ noAutoCreate: true })
|
||||
const eng = new GameEngine({ seed: state.seed, noAutoCreate: true })
|
||||
eng.restore(state)
|
||||
return eng
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ export class GameFacade {
|
||||
about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } {
|
||||
return {
|
||||
title: '仙途家族志',
|
||||
version: '0.1.11',
|
||||
version: '0.1.15',
|
||||
modules: this.world.systemList().length,
|
||||
systems: this.world.systemList().filter((s) => s.enabled).length,
|
||||
plugins: this.world.pluginList().length,
|
||||
|
||||
@@ -421,8 +421,7 @@ export function applyEventChoice(
|
||||
}
|
||||
const opt = def.options[optionIdx]
|
||||
if (opt) {
|
||||
if (formation) opt.eff.formation = formation
|
||||
applyEffect(w, opt.eff, squad)
|
||||
applyEffect(w, opt.eff, squad, undefined, formation)
|
||||
if (def.once && !s.completedEvents.includes(def.id)) s.completedEvents.push(def.id)
|
||||
}
|
||||
s.pendingEvent = undefined
|
||||
@@ -468,7 +467,7 @@ export function rankPower(w: World, c: Character): number {
|
||||
return order.indexOf(c.realm.major) * 10 + c.realm.minor
|
||||
}
|
||||
|
||||
export function applyEffect(w: World, eff: EffectDef, squad?: string[]): void {
|
||||
export function applyEffect(w: World, eff: EffectDef, squad?: string[], _extra?: unknown, formation?: string): void {
|
||||
const s = w.state
|
||||
const fam = s.family
|
||||
|
||||
@@ -513,7 +512,7 @@ export function applyEffect(w: World, eff: EffectDef, squad?: string[]): void {
|
||||
if (!fam.techniques.includes(eff.addTech)) fam.techniques.push(eff.addTech)
|
||||
}
|
||||
if (eff.tournament) {
|
||||
runTournament(w, squad, eff.formation as never)
|
||||
runTournament(w, squad, formation as never)
|
||||
}
|
||||
if (eff.apprentice?.build) {
|
||||
buildApprentice(w)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { MissionState } from '../../../types/domain'
|
||||
import { FormationId } from '../../../data/formations'
|
||||
import { missionById, MissionDef, ENEMIES } from '../../../data/secrets'
|
||||
import { resolveEncounter, rollWarbooty } from './combat'
|
||||
import { WorldSim } from '../../sim/WorldSim'
|
||||
import { techniqueById } from '../../../data/techniques'
|
||||
import { describeRealm } from '../../../data/realms'
|
||||
|
||||
@@ -138,14 +139,9 @@ export function sendMission(w: World, defId: string, members: string[], formatio
|
||||
const c = w.memberById(id)
|
||||
c.state = 'expedition'
|
||||
})
|
||||
// 世界秘境灵气消耗(有 sim 时)
|
||||
if (w.state.worldSim?.secretQi) {
|
||||
const qi = w.state.worldSim.secretQi[def.id] as number | undefined
|
||||
if (qi !== undefined) {
|
||||
w.state.worldSim.secretQi[def.id] = Math.max(0, qi - 6)
|
||||
} else {
|
||||
w.state.worldSim.secretQi[def.id] = 44
|
||||
}
|
||||
// 世界秘境灵气消耗(统一走 WorldSim 入口)
|
||||
if (w.state.worldSim) {
|
||||
new WorldSim(w).consumeSecretQi(def.id)
|
||||
}
|
||||
w.state.missions.push(m)
|
||||
w.state.family.missionIds.push(m.id)
|
||||
|
||||
@@ -17,10 +17,6 @@ import { createWorldState, findInheritor } from './creation'
|
||||
import { SYSTEM_DEFS, SystemDef } from './capabilities'
|
||||
import { emptyClock } from './clocks'
|
||||
import { Rng } from '../kernel/rng'
|
||||
|
||||
function clockFromKernel(_target: GameClock, source: GameClock): void {
|
||||
void source
|
||||
}
|
||||
import { CotycPlugin, PluginContext, PluginStatus } from '../kernel/plugin'
|
||||
import { PluginManager } from './pluginManager'
|
||||
import { EventDef } from '../../data/events'
|
||||
@@ -79,6 +75,15 @@ export function normalizeGameState(state: GameState): GameState {
|
||||
if (!state.family.buildings) state.family.buildings = {}
|
||||
if (!state.family.techniques) state.family.techniques = []
|
||||
if (!state.family.missionIds) state.family.missionIds = []
|
||||
if (!state.stats.tourneyHistory) state.stats.tourneyHistory = []
|
||||
if (state.stats.tourneyBest === undefined) state.stats.tourneyBest = undefined
|
||||
if (state.worldSim) {
|
||||
state.worldSim.marketPool = state.worldSim.marketPool ?? {}
|
||||
state.worldSim.npcDyn = state.worldSim.npcDyn ?? {}
|
||||
state.worldSim.secretQi = state.worldSim.secretQi ?? {}
|
||||
state.worldSim.newsFeed = state.worldSim.newsFeed ?? []
|
||||
if (typeof state.worldSim.tide !== 'number') state.worldSim.tide = 0.5
|
||||
}
|
||||
if (state.family.headId && !state.members[state.family.headId]) {
|
||||
const firstAlive = Object.values(state.members).find((c) => c.alive)
|
||||
if (firstAlive) state.family.headId = firstAlive.id
|
||||
@@ -275,10 +280,6 @@ export class World {
|
||||
this.out.forEach((o) => o.onGameOver(reason, year))
|
||||
}
|
||||
|
||||
gameOver___placeholder(): void {
|
||||
void 0
|
||||
}
|
||||
|
||||
memberById(id: Id): Character {
|
||||
const c = this.state.members[id]
|
||||
if (!c) throw new Error(`member not found ${id}`)
|
||||
@@ -316,6 +317,13 @@ export class World {
|
||||
this.pruneYearFlags()
|
||||
}
|
||||
|
||||
applyEventChoice(eventId: string, idx: number): boolean {
|
||||
const pid = this.state.pendingEvent
|
||||
if (!pid) return false
|
||||
applyEventChoice(this, pid, idx)
|
||||
return true
|
||||
}
|
||||
|
||||
requeueEvent(eventId: string): void {
|
||||
const s = this.state
|
||||
if (!s.eventQueue.includes(eventId)) s.eventQueue.push(eventId)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** WorldSim —— 世界自进化引擎(game/engine/sim/WorldSim.ts) */
|
||||
import { World } from '../runtime/World'
|
||||
import { WorldSimState, NpcDynamics, WORLDSIM, CALAMITY_EFFECT, CalamityName } from './worldsim-data'
|
||||
import { WorldSimState, NpcDynamics, WORLDSIM, CALAMITY_EFFECT, CalamityName, POOL_BASE } from './worldsim-data'
|
||||
import { pack } from '../../data/registry'
|
||||
import { ITEMS } from '../../data/items'
|
||||
import { npcById } from '../../data/npcs'
|
||||
import { MAJORS, MAJOR_ORDER } from '../../data/realms'
|
||||
@@ -13,7 +14,13 @@ export class WorldSim {
|
||||
private s(): WorldSimState {
|
||||
const w = this.w
|
||||
if (!w.state.worldSim) w.state.worldSim = initSim(w) as never
|
||||
return w.state.worldSim as WorldSimState
|
||||
const ws = w.state.worldSim as WorldSimState
|
||||
if (!ws.secretQi || Object.keys(ws.secretQi).length === 0) {
|
||||
const defs = pack().missions
|
||||
ws.secretQi = {}
|
||||
for (const m of defs) ws.secretQi[m.id] = 50
|
||||
}
|
||||
return ws
|
||||
}
|
||||
|
||||
tick(): void {
|
||||
@@ -37,6 +44,8 @@ export class WorldSim {
|
||||
s.calamityYear = this.w.state.year
|
||||
applyCalamityToMarket(s, cl as CalamityName)
|
||||
this.w.log('bad', `【天下灾年】${cl}——灵植减产,市价将行。`)
|
||||
} else {
|
||||
s.calamity = undefined
|
||||
}
|
||||
|
||||
// ---- A. 资源循环市场(库存自然流向 + 再平衡 + 价格信号) ----
|
||||
@@ -46,7 +55,7 @@ export class WorldSim {
|
||||
evolveNpc(this.w, s, rng.next())
|
||||
|
||||
// ---- E. 天下快讯(节流) ----
|
||||
if (this.w.state.month !== s.lastNewsMonth && this.w.state.totalTicks % WORLDSIM.newsEvery === 0) {
|
||||
if ((this.w.state.totalTicks % WORLDSIM.newsEvery) === 0) {
|
||||
pushNews(this.w, s, MARKET_IDS)
|
||||
}
|
||||
|
||||
@@ -54,13 +63,19 @@ export class WorldSim {
|
||||
if (s.newsFeed.length > WORLDSIM.newsKeep) s.newsFeed.splice(0, s.newsFeed.length - WORLDSIM.newsKeep)
|
||||
}
|
||||
|
||||
/** 秘境探索消耗灵气(missions 调用) */
|
||||
consumeSecretQi(id: string, amount: number): void {
|
||||
/** 秘境探索消耗灵气(missions 调用;统一入口,默认 6 点) */
|
||||
consumeSecretQi(id: string, amount = 6): void {
|
||||
const s = this.s()
|
||||
if (s.secretQi[id] === undefined) s.secretQi[id] = 50
|
||||
s.secretQi[id] = clamp(s.secretQi[id] - amount, 0, 100)
|
||||
}
|
||||
|
||||
/** 秘境灵气对探索收益的门槛(低灵气收益衰减提示) */
|
||||
secretQiOf(id: string): number {
|
||||
const s = this.s()
|
||||
return s.secretQi[id] ?? 50
|
||||
}
|
||||
|
||||
/** 灵气系数(修炼/掉落市场乘子) */
|
||||
tideMult(): number {
|
||||
return this.s().tide
|
||||
@@ -99,8 +114,6 @@ function initSim(w: World): WorldSimState {
|
||||
relationsWithOthers: {}
|
||||
}
|
||||
}
|
||||
for (const sid of Object.keys(w.state.missions ?? {})) void sid
|
||||
// 秘境灵气初始(从 mission 定义 id)
|
||||
s.secretQi = {}
|
||||
return s
|
||||
}
|
||||
@@ -121,12 +134,15 @@ function empty(): WorldSimState {
|
||||
}
|
||||
|
||||
function driftMarket(s: WorldSimState, noise: number): void {
|
||||
let n = noise
|
||||
for (const id of MARKET_IDS) {
|
||||
const per = n; n += 0.13
|
||||
const base = poolBase(id)
|
||||
const cur = s.marketPool[id] ?? base
|
||||
// 向基准再平衡 + 噪声漂移(价格弹性)
|
||||
const rebalance = (base - cur) * WORLDSIM.marketRebalance
|
||||
const drift = (noise - 0.5) * WORLDSIM.marketDriftRate * base
|
||||
const drift = ((per % 1) - 0.5) * WORLDSIM.marketDriftRate * base
|
||||
|
||||
s.marketPool[id] = Math.max(base * 0.3, cur + rebalance + drift)
|
||||
}
|
||||
}
|
||||
@@ -139,9 +155,8 @@ function applyCalamityToMarket(s: WorldSimState, cl: CalamityName): void {
|
||||
}
|
||||
|
||||
function poolBase(id: string): number {
|
||||
return MARKET_BASE[id] ?? 100
|
||||
return POOL_BASE[id] ?? 100
|
||||
}
|
||||
const MARKET_BASE: Record<string, number> = { lingcao: 600, lingkuang: 300, beastcore: 80, 'pill-qiyuan': 90, 'pill-ningyuan': 40 }
|
||||
|
||||
function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
||||
void noise
|
||||
@@ -149,15 +164,18 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
||||
for (const [id, npc] of Object.entries(w.state.npcFamilies)) {
|
||||
const dyn = s.npcDyn[id] ?? initDynFor(id)
|
||||
s.npcDyn[id] = dyn
|
||||
dyn.leaderAge++
|
||||
// 宗主换代:寿终(年龄>预期)
|
||||
const def = npcById(id)
|
||||
const lifespan = MAJORS[def.leaderRealm].lifespan
|
||||
if (dyn.leaderAge > lifespan * 0.9 || w.rng.chance(0.006)) {
|
||||
if (!w.rng.chance(0.25)) {
|
||||
const curRealm = MAJOR_ORDER[dyn.leaderRealmIdx] ?? def.leaderRealm
|
||||
const lifespan = MAJORS[curRealm].lifespan
|
||||
if (w.state.month === 1) {
|
||||
dyn.leaderAge++
|
||||
// 换代评估:寿终阈值 或 年首小概率(退隐/遇害)——约 50 年一代
|
||||
const chanceNow = dyn.leaderAge > lifespan * 0.9 ? 0.5 : 0.02
|
||||
if (w.rng.chance(chanceNow) && !w.rng.chance(0.25)) {
|
||||
dyn.leaderAge = 30 + w.rng.int(0, 25)
|
||||
dyn.leaderRealmIdx = Math.min(dyn.leaderRealmIdx + 1, 5)
|
||||
dyn.leaderName = `${def.name.replace('氏', '')}氏新主`
|
||||
npc.power = Math.round(Math.max(40, npc.power + 15))
|
||||
dyn.lastEvent = '宗祧更替'
|
||||
dyn.lastEventYear = year
|
||||
s.npcSuccessions++
|
||||
@@ -165,11 +183,7 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
||||
w.log('info', `【天下】${def.name} 更易宗主,气象一新。`)
|
||||
}
|
||||
}
|
||||
// 势力聚合:境界+财力+兵员(前有 power 为基础)
|
||||
npc.power = Math.max(40, Math.round(npc.power * 0.95 + (dyn.leaderRealmIdx * 20 + 40) * 0.5))
|
||||
if (Math.abs(npc.relation) > 2) {
|
||||
npc.relation += w.rng.chance(0.5) ? 0 : (npc.relation > 0 ? -0.3 : 0.3)
|
||||
}
|
||||
void npc
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,18 +203,28 @@ function pushNews(w: World, s: WorldSimState, about: string[]): void {
|
||||
const rng = w.rng
|
||||
const idx = rng.int(0, about.length - 1)
|
||||
const id = about[idx]
|
||||
const pool = s.marketPool[id] ?? 50
|
||||
const base = poolBase(id)
|
||||
const pct = Math.round(((pool - base) / base) * 100)
|
||||
const itemName = ITEMS[id]?.name ?? id
|
||||
const tide = s.tide > 1.0 ? '灵潮上涨' : s.tide < 0.75 ? '灵潮回落' : '汐平'
|
||||
const row = {
|
||||
year: w.state.year,
|
||||
month: w.state.month,
|
||||
src: id.startsWith('n-') ? npcById(id).name : `世界·${itemName}`,
|
||||
text: id.startsWith('n-')
|
||||
? `${npcById(id).name} 世务维系(宗主更替率);${tide}。${pct !== 0 ? `${itemName}行情${pct > 0 ? '升' : '降'}${Math.abs(pct)}%` : ''}`
|
||||
: `${tide}·${itemName}行情${pct > 0 ? '升' : '降'}${Math.abs(pct)}%`
|
||||
let row: { year: number; month: number; src: string; text: string }
|
||||
if (id.startsWith('n-')) {
|
||||
const def = npcById(id)
|
||||
const dyn = s.npcDyn[id]
|
||||
row = {
|
||||
year: w.state.year,
|
||||
month: w.state.month,
|
||||
src: def.name,
|
||||
text: dyn ? `灵潮气象:${dyn.leaderName} 宗主更替,势力重排。` : `${def.name} 世务新张。`
|
||||
}
|
||||
} else {
|
||||
const pool = clamp(s.marketPool[id] ?? poolBase(id), POOL_BASE[id] * 0.5, POOL_BASE[id] * 1.8)
|
||||
const base = poolBase(id)
|
||||
const pct = Math.round(((pool - base) / base) * 100)
|
||||
const itemName = ITEMS[id]?.name ?? id
|
||||
row = {
|
||||
year: w.state.year,
|
||||
month: w.state.month,
|
||||
src: `世界·${itemName}`,
|
||||
text: pct !== 0 ? `${itemName}行情${pct > 0 ? '升' : '降'}${Math.abs(pct)}% (${tide})` : `${itemName}行情平稳 (${tide})`
|
||||
}
|
||||
}
|
||||
s.newsFeed.push(row)
|
||||
s.lastNewsMonth = w.state.month
|
||||
|
||||
@@ -50,6 +50,15 @@ export function makeWorldSimState(): WorldSimState {
|
||||
}
|
||||
}
|
||||
|
||||
/** 市场基准库存(单一权威:WorldSim 再平衡 / Market 行情 / brief 图标 三方共用) */
|
||||
export const POOL_BASE: Record<string, number> = {
|
||||
lingcao: 600,
|
||||
lingkuang: 300,
|
||||
beastcore: 80,
|
||||
'pill-qiyuan': 90,
|
||||
'pill-ningyuan': 40
|
||||
}
|
||||
|
||||
/** 世界演化参数表(全部可调) */
|
||||
export const WORLDSIM = {
|
||||
marketDriftRate: 0.03,
|
||||
|
||||
@@ -128,6 +128,7 @@ export class SaveSlot {
|
||||
state.schemaVersion = CURRENT_SCHEMA
|
||||
await this.driver.exec(`DELETE FROM snapshot`)
|
||||
await this.driver.exec(`DELETE FROM meta`)
|
||||
await this.driver.exec(`DELETE FROM chronicle`)
|
||||
await this.saveState(state, 'import')
|
||||
const aliveCount = Object.values(state.members).filter((c) => c.alive).length
|
||||
await this.setMeta(buildSimpleMeta(state, this.slot, aliveCount))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,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,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 }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user