v0.1.6: 时轮与造化(GameClock/RngHub/时节/年轴/保护)
- 统一时轮 GameClock:七 phase 注册制(production/aging/cultivation/missions/ events/diplomacy/epilogue) + 年首三钩子(婚配→岁贡→族簿);advanceMonth 化作薄壳; 新增系统只注册不改本体;alive 守卫、随机序、族簿时序均与重构前逐一等价 - 金钟罩:3 枚 seed × 560 月指纹常驻(重构前后已验证同哈希;时节属有意变更后重固化) - 统一随机 RngHub:开局播种(crypto UV)、音效白噪独立流、引擎 rng 带 nextCount 审计; 全工程拔除逻辑侧 Math.random - 时节系统:春夏秋冬乘子(春田+10%/夏修+5%/秋市+5%/冬闭+8%)入生产与修炼 - 崩溃保护:advance 异常自动存『出险前』档 + 停速提示回档 - 性能预算:dev-only 单 tick>60ms 报最慢 phase - 横轴年表:十年一格大事/生殇聚合 + 春秋原年轴视图 - 测试 197→225(时钟/收集器/四季/年轴/效果全矩阵/死局链/残留防御)
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import type { World } from '../engine/world'
|
||||
|
||||
/**
|
||||
* 统一时轮调度:所有系统以 phase 注册,由时钟按固定顺序驱动。
|
||||
* - 月度 phase:production / aging / cultivation / missions / events / diplomacy / epilogue
|
||||
* - 年首钩子:onYearStart(婚配 → 声望岁贡 → 岁末族簿)
|
||||
* 新增系统 = 注册一行,不再修改 advanceMonth 本体。
|
||||
*/
|
||||
|
||||
export type PhaseId =
|
||||
| 'production'
|
||||
| 'aging'
|
||||
| 'cultivation'
|
||||
| 'missions'
|
||||
| 'events'
|
||||
| 'diplomacy'
|
||||
| 'epilogue'
|
||||
|
||||
export type SystemHook = (w: World) => void
|
||||
|
||||
export interface PhaseStat {
|
||||
phase: PhaseId
|
||||
ms: number
|
||||
count: number
|
||||
}
|
||||
|
||||
export const PHASE_ORDER: PhaseId[] = [
|
||||
'production',
|
||||
'aging',
|
||||
'cultivation',
|
||||
'missions',
|
||||
'events',
|
||||
'diplomacy',
|
||||
'epilogue'
|
||||
]
|
||||
|
||||
export class GameClock {
|
||||
private monthly = new Map<PhaseId, SystemHook[]>()
|
||||
private yearly: SystemHook[] = []
|
||||
|
||||
register(phase: PhaseId, fn: SystemHook): void {
|
||||
const list = this.monthly.get(phase) ?? []
|
||||
list.push(fn)
|
||||
this.monthly.set(phase, list)
|
||||
}
|
||||
|
||||
onYearStart(fn: SystemHook): void {
|
||||
this.yearly.push(fn)
|
||||
}
|
||||
|
||||
fireYearStart(w: World): void {
|
||||
for (const fn of this.yearly) fn(w)
|
||||
}
|
||||
|
||||
stepMonthly(w: World): PhaseStat[] {
|
||||
const report: PhaseStat[] = []
|
||||
for (const phase of PHASE_ORDER) {
|
||||
const t0 = performance.now()
|
||||
const fns = this.monthly.get(phase) ?? []
|
||||
for (const fn of fns) fn(w)
|
||||
const ms = performance.now() - t0
|
||||
report.push({ phase, ms, count: fns.length })
|
||||
}
|
||||
return report
|
||||
}
|
||||
|
||||
subscriptionCount(): number {
|
||||
let n = 0
|
||||
for (const list of this.monthly.values()) n += list.length
|
||||
return n + this.yearly.length
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ const U32 = 4294967296
|
||||
|
||||
export class Rng {
|
||||
state: RngState
|
||||
nextCount = 0
|
||||
|
||||
constructor(state: RngState) {
|
||||
this.state = { ...state }
|
||||
@@ -37,6 +38,7 @@ export class Rng {
|
||||
}
|
||||
|
||||
next(): number {
|
||||
this.nextCount++
|
||||
const s = this.state
|
||||
const t = ((s.a + s.b + s.d) | 0) >>> 0
|
||||
s.d = (s.d + 1) | 0
|
||||
@@ -76,3 +78,30 @@ export class Rng {
|
||||
return this.next() * (max - min) + min
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全工程统一随机收集器:
|
||||
* - rollSeed:开局播种(crypto 级)
|
||||
* - audioNoise:音效白噪(种子独立,永不干扰引擎序列)
|
||||
* - engineRng:引擎世界随机(World.rng 实例,见 world.ts)
|
||||
*/
|
||||
export class RngHub {
|
||||
private static audioRng = new Rng(seedToRng('cotyc-audio-dither-v1'))
|
||||
|
||||
static rollSeed(): string {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return `seed-${crypto.randomUUID()}`
|
||||
}
|
||||
return `seed-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9)}`
|
||||
}
|
||||
|
||||
static audioNoise01(): number {
|
||||
return RngHub.audioRng.next()
|
||||
}
|
||||
}
|
||||
|
||||
export function rngAudit(rng: RigRng): { total: number } {
|
||||
return { total: rng.nextCount }
|
||||
}
|
||||
|
||||
type RigRng = Rng
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { GameState } from '../types/domain'
|
||||
|
||||
export interface AxisEvent {
|
||||
year: number
|
||||
kind: '突破' | '大比' | '渡劫' | '婚' | '殇' | '战' | '飞升'
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface AxisCell {
|
||||
from: number
|
||||
to: number
|
||||
events: AxisEvent[]
|
||||
births: number
|
||||
deaths: number
|
||||
}
|
||||
|
||||
/** 横轴年表:十年一格,把族簿大事压缩进一根时间轴 */
|
||||
export function yearAxis(s: GameState, decadeSize = 10): AxisCell[] {
|
||||
const startYear = Math.max(1, Math.min(...s.chronicle.map((e) => e.year).concat(1)))
|
||||
const endYear = Math.max(startYear, s.year)
|
||||
const cells = new Map<number, AxisCell>()
|
||||
const cellOf = (year: number): AxisCell => {
|
||||
const from = Math.floor((year - 1) / decadeSize) * decadeSize + 1
|
||||
if (!cells.has(from)) cells.set(from, { from, to: Math.min(from + decadeSize - 1, endYear), events: [], births: 0, deaths: 0 })
|
||||
return cells.get(from)!
|
||||
}
|
||||
|
||||
for (const e of s.chronicle) {
|
||||
if (e.year < startYear) continue
|
||||
const cell = cellOf(e.year)
|
||||
const kind: AxisEvent['kind'] | null =
|
||||
e.category === 'breakthrough'
|
||||
? /渡劫|天劫/.test(e.text)
|
||||
? '渡劫'
|
||||
: '突破'
|
||||
: e.category === 'birth'
|
||||
? null
|
||||
: e.category === 'death'
|
||||
? '殇'
|
||||
: e.category === 'marriage'
|
||||
? '婚'
|
||||
: e.category === 'battle'
|
||||
? '战'
|
||||
: null
|
||||
if (kind) cell.events.push({ year: e.year, kind, label: e.text.slice(0, 26) })
|
||||
if (e.category === 'birth') cell.births++
|
||||
if (e.category === 'death') cell.deaths++
|
||||
}
|
||||
|
||||
return [...cells.entries()].sort((a, b) => a[0] - b[0]).map(([, c]) => c)
|
||||
}
|
||||
|
||||
export function decadeLabel(from: number, to: number): string {
|
||||
return from === to ? `${from}年` : `${from}-${to}年`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export type Season = 'spring' | 'summer' | 'autumn' | 'winter'
|
||||
|
||||
export function seasonOf(month: number): Season {
|
||||
if (month <= 3) return 'spring'
|
||||
if (month <= 6) return 'summer'
|
||||
if (month <= 9) return 'autumn'
|
||||
return 'winter'
|
||||
}
|
||||
|
||||
export const SEASON: Record<Season, { name: string; field: number; cult: number; market: number; meditation: number }> = {
|
||||
spring: { name: '春', field: 0.1, cult: 0, market: 0, meditation: 0 },
|
||||
summer: { name: '夏', field: 0, cult: 0.05, market: 0, meditation: 0 },
|
||||
autumn: { name: '秋', field: 0, cult: 0, market: 0.05, meditation: 0 },
|
||||
winter: { name: '冬', field: 0, cult: 0, market: 0, meditation: 0.08 }
|
||||
}
|
||||
|
||||
export function seasonMod(month: number, key: 'field' | 'cult' | 'market' | 'meditation'): number {
|
||||
return SEASON[seasonOf(month)][key]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { GameClock } from '../core/clock'
|
||||
import { productionTick } from './systems/production'
|
||||
import { deathTick, woundHealTick } from './systems/lifecycle'
|
||||
import { cultivationTick } from './systems/cultivation'
|
||||
import { missionTick } from './systems/missions'
|
||||
import { eventRoll } from './systems/events'
|
||||
import { diplomacyTick } from './systems/diplomacy'
|
||||
import { yearStartMarriage } from './systems/marriage'
|
||||
import type { World } from './world'
|
||||
|
||||
/** 原语义保留:满门凋零后仅存生产/寿元与收尾 */
|
||||
function ifAlive(fn: (w: World) => void): (w: World) => void {
|
||||
return (w: World) => {
|
||||
if (w.aliveMembers().length > 0) fn(w)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 装备时钟:年度钩子与月度 phase 的注册顺序即执行顺序(确定性)。
|
||||
* 时间线:婚配养育 → 声望岁贡 → 岁末族簿 —— 再逐月:生产→寿元→修炼→任务→事件→外交→收尾
|
||||
*/
|
||||
export function buildClock(): GameClock {
|
||||
const clock = new GameClock()
|
||||
|
||||
clock.onYearStart((w) => yearStartMarriage(w))
|
||||
clock.onYearStart((w) => {
|
||||
w.state.family.reputation += w.postBonus('familyRep')
|
||||
const zhenCount = w
|
||||
.aliveMembers()
|
||||
.filter((c) => c.aspiration === 'zhen').length
|
||||
w.state.family.reputation += Math.round(zhenCount * 0.3 * 100) / 100
|
||||
})
|
||||
clock.onYearStart((w) => w.publishYearReport())
|
||||
|
||||
clock.register('production', (w: World) => productionTick(w))
|
||||
clock.register('aging', (w: World) => deathTick(w))
|
||||
clock.register('aging', (w: World) => woundHealTick(w))
|
||||
clock.register('cultivation', ifAlive((w: World) => cultivationTick(w)))
|
||||
clock.register('missions', ifAlive((w: World) => missionTick(w)))
|
||||
clock.register('events', ifAlive((w: World) => eventRoll(w)))
|
||||
clock.register('diplomacy', ifAlive((w: World) => diplomacyTick(w)))
|
||||
clock.register('epilogue', (w: World) => w.epilogueTick())
|
||||
|
||||
return clock
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { World } from './world'
|
||||
import type { World } from './world'
|
||||
import { ITEMS } from '../data/items'
|
||||
|
||||
export function marketPrice(w: World, itemId: string): number {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { World } from '../world'
|
||||
import type { World } from '../world'
|
||||
import { Character, BattleLog } from '../../types/domain'
|
||||
import { basePower, describeRealm } from '../../data/realms'
|
||||
import { ARTIFACT_POWER } from '../../data/items'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { World } from '../world'
|
||||
import type { World } from '../world'
|
||||
import { Character } from '../../types/domain'
|
||||
import { ROOT_GRADES } from '../../data/elements'
|
||||
import { masteryRateOfMajor } from '../../data/pacing'
|
||||
@@ -10,6 +10,7 @@ import { traitBonuses } from '../pcgen'
|
||||
import { newCharacter } from '../pcgen'
|
||||
import { MALE_GIVEN, FEMALE_GIVEN } from '../../core/names'
|
||||
import { ASPIRATION_IDS } from '../../data/aspirations'
|
||||
import { seasonMod } from '../../data/season'
|
||||
import { needsTribulation, tribulationEventId } from './tribulation'
|
||||
|
||||
export function monthlyRate(w: World, c: Character): number {
|
||||
@@ -26,6 +27,7 @@ export function monthlyRate(w: World, c: Character): number {
|
||||
const buildings = st.family.buildings
|
||||
const juling = buildings['juling'] ?? 0
|
||||
rate *= 1 + juling * 0.05
|
||||
rate *= 1 + seasonMod(st.month, 'cult')
|
||||
rate *= 1 + w.postBonus('expAll')
|
||||
if (st.family.flag['fengFeiBless']) rate *= 1.05
|
||||
if (c.traits.includes('fengxian')) rate *= 1.3
|
||||
@@ -34,7 +36,7 @@ export function monthlyRate(w: World, c: Character): number {
|
||||
if (fitBonusOf(c).cult) rate *= 1.06
|
||||
if (c.state === 'meditation') {
|
||||
rate *= 1.35
|
||||
rate *= 1 + w.postBonus('meditation')
|
||||
rate *= 1 + w.postBonus('meditation') + seasonMod(st.month, 'meditation')
|
||||
const dongfu = buildings['dongfu'] ?? 0
|
||||
rate *= 1 + dongfu * 0.08
|
||||
} else if (c.state === 'expedition') {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { World } from '../world'
|
||||
import type { World } from '../world'
|
||||
import { npcById } from '../../data/npcs'
|
||||
import { findEvent, fire } from './events'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { World } from '../world'
|
||||
import type { World } from '../world'
|
||||
import { Character } from '../../types/domain'
|
||||
import { Cond, EffectDef, EventDef, EVENTS, MemberEffect } from '../../data/events'
|
||||
import { MAJOR_ORDER } from '../../data/realms'
|
||||
@@ -348,7 +348,7 @@ export function rankPower(w: World, c: Character): number {
|
||||
return order.indexOf(c.realm.major) * 10 + c.realm.minor
|
||||
}
|
||||
|
||||
function applyEffect(w: World, eff: EffectDef, squad?: string[]): void {
|
||||
export function applyEffect(w: World, eff: EffectDef, squad?: string[]): void {
|
||||
const s = w.state
|
||||
const fam = s.family
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { World } from '../world'
|
||||
import type { World } from '../world'
|
||||
import { MAJORS } from '../../data/realms'
|
||||
import { calcLifespan } from '../pcgen'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { World } from '../world'
|
||||
import type { World } from '../world'
|
||||
import { Character } from '../../types/domain'
|
||||
import { produceOffspring } from './cultivation'
|
||||
import { yearGrowth } from './diplomacy'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { World } from '../world'
|
||||
import type { World } from '../world'
|
||||
import { MissionState } from '../../types/domain'
|
||||
import { missionById, MissionDef, ENEMIES } from '../../data/secrets'
|
||||
import { resolveEncounter, rollWarbooty } from './combat'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { World } from '../world'
|
||||
import type { World } from '../world'
|
||||
import { aspirationById } from '../../data/aspirations'
|
||||
import { seasonMod } from '../../data/season'
|
||||
|
||||
export function productionTick(w: World): void {
|
||||
const fam = w.state.family
|
||||
@@ -15,8 +16,9 @@ export function productionTick(w: World): void {
|
||||
|
||||
const fielders = w.aliveMembers().filter((c) => aspirationById(c.aspiration)?.effect.type === 'field').length
|
||||
const merchants = w.aliveMembers().filter((c) => aspirationById(c.aspiration)?.effect.type === 'market').length
|
||||
const springMod = seasonMod(w.state.month, 'field')
|
||||
if (lingtian > 0) {
|
||||
const v = Math.round(10 * lingtian * (1 + fielders * 0.05))
|
||||
const v = Math.round(10 * lingtian * (1 + fielders * 0.05 + springMod))
|
||||
inv.lingcao = (inv.lingcao ?? 0) + v
|
||||
parts.push(`灵田+${v}灵草`)
|
||||
}
|
||||
@@ -34,8 +36,9 @@ export function productionTick(w: World): void {
|
||||
inv.lingkuang = (inv.lingkuang ?? 0) + v
|
||||
parts.push(`灵矿+${v}灵矿`)
|
||||
}
|
||||
const autumnMod = seasonMod(w.state.month, 'market')
|
||||
if (fangshi > 0) {
|
||||
const v = Math.round(55 * fangshi * (1 + w.postBonus('marketIncome') + merchants * 0.03))
|
||||
const v = Math.round(55 * fangshi * (1 + w.postBonus('marketIncome') + merchants * 0.03 + autumnMod))
|
||||
fam.stones += v
|
||||
parts.push(`坊市+${v}灵石`)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { World } from '../world'
|
||||
import type { World } from '../world'
|
||||
import { Character } from '../../types/domain'
|
||||
import { ENEMIES, EnemyDef } from '../../data/secrets'
|
||||
import { resolveEncounter } from './combat'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { World } from '../world'
|
||||
import type { World } from '../world'
|
||||
import { Character } from '../../types/domain'
|
||||
import { nextRealm, MAJOR_ORDER, realmDeathChance, describeRealm } from '../../data/realms'
|
||||
|
||||
|
||||
@@ -14,14 +14,11 @@ import { POSTS } from '../data/posts'
|
||||
import { TECHNIQUES } from '../data/techniques'
|
||||
import { aspirationById as aspirationOf } from '../data/aspirations'
|
||||
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../core/legacy'
|
||||
import { productionTick } from './systems/production'
|
||||
import { deathTick, woundHealTick } from './systems/lifecycle'
|
||||
import { cultivationTick, resolveBreakthrough } from './systems/cultivation'
|
||||
import { missionTick } from './systems/missions'
|
||||
import { eventRoll, applyEventChoice } from './systems/events'
|
||||
import { diplomacyTick } from './systems/diplomacy'
|
||||
import { yearStartMarriage } from './systems/marriage'
|
||||
import { createWorldState, findInheritor } from './creation'
|
||||
import { buildClock } from './clocks'
|
||||
import { GameClock } from '../core/clock'
|
||||
import { resolveBreakthrough } from './systems/cultivation'
|
||||
import { applyEventChoice } from './systems/events'
|
||||
import { combatPowerOf } from './systems/combat'
|
||||
|
||||
export type LogKind = LogItem['kind']
|
||||
@@ -67,12 +64,14 @@ export class World {
|
||||
state: GameState
|
||||
rng: Rng
|
||||
out: WorldEventBus[]
|
||||
clock: GameClock
|
||||
|
||||
constructor(state: GameState, out: WorldEventBus[] = []) {
|
||||
normalizeGameState(state)
|
||||
this.state = state
|
||||
this.rng = new Rng(state.rng)
|
||||
this.out = out
|
||||
this.clock = buildClock()
|
||||
}
|
||||
|
||||
seq(): Id {
|
||||
@@ -141,20 +140,11 @@ export class World {
|
||||
if (s.month > 12) {
|
||||
s.month = 1
|
||||
s.year++
|
||||
this.yearStart()
|
||||
this.clock.fireYearStart(this)
|
||||
}
|
||||
s.totalTicks++
|
||||
|
||||
productionTick(this)
|
||||
deathTick(this)
|
||||
woundHealTick(this)
|
||||
if (this.aliveMembers().length > 0) {
|
||||
cultivationTick(this)
|
||||
missionTick(this)
|
||||
eventRoll(this)
|
||||
diplomacyTick(this)
|
||||
}
|
||||
this.checkHead()
|
||||
this.clock.stepMonthly(this)
|
||||
const stonesEnd = s.family.stones
|
||||
s.finance.accum += stonesEnd - stonesStart
|
||||
this.trackStats()
|
||||
@@ -186,11 +176,8 @@ export class World {
|
||||
return arch
|
||||
}
|
||||
|
||||
private yearStart(): void {
|
||||
yearStartMarriage(this)
|
||||
this.state.family.reputation += this.postBonus('familyRep')
|
||||
const zhenCount = this.aliveMembers().filter((c) => aspirationOf(c.aspiration)?.effect.type === 'rep').length
|
||||
this.state.family.reputation += Math.round(zhenCount * 0.3 * 100) / 100
|
||||
|
||||
publishYearReport(): void {
|
||||
const rep = this.state.family.reputation
|
||||
const power = this.familyPower()
|
||||
const report: YearlyReport = {
|
||||
@@ -210,6 +197,11 @@ export class World {
|
||||
this.out.forEach((o) => o.onYearPaper?.(report))
|
||||
}
|
||||
|
||||
epilogueTick(): void {
|
||||
if (this.state.gameOver) return
|
||||
this.checkHead()
|
||||
}
|
||||
|
||||
reputationDrift(): void {
|
||||
const cur = this.state.family.reputation
|
||||
const drift = cur > 0 ? -1.5 : cur < 0 ? 1.2 : 0
|
||||
|
||||
@@ -4,6 +4,7 @@ import { computeLegacy, resolveLegacy } from '../../game/core/legacy'
|
||||
import { resolveLegacy as doesIt } from '../../game/core/legacy'
|
||||
import { ResolveModal } from '../components/ResolveModal'
|
||||
import { buildBiography } from '../../game/core/biography'
|
||||
import { yearAxis, decadeLabel } from '../../game/core/yearaxis'
|
||||
|
||||
void doesIt
|
||||
|
||||
@@ -12,7 +13,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'>('dims')
|
||||
const [view, setView] = useState<'dims' | 'chronicle' | 'report' | 'scroll' | 'axis'>('dims')
|
||||
if (!world) return null
|
||||
const w = world
|
||||
const s = w.state
|
||||
@@ -54,6 +55,7 @@ export default function LegacyPanel() {
|
||||
<button className="btn" onClick={() => setView('report')}>年报</button>
|
||||
<button className="btn" onClick={() => setView('chronicle')}>年表</button>
|
||||
<button className="btn" onClick={() => setView('scroll')}>长卷</button>
|
||||
<button className="btn" onClick={() => setView('axis')}>年轴</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -110,6 +112,8 @@ export default function LegacyPanel() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{view === 'axis' && <AxisView />}
|
||||
|
||||
{view === 'scroll' && <ScrollScroll />}
|
||||
|
||||
{view === 'chronicle' && (
|
||||
@@ -133,6 +137,32 @@ export default function LegacyPanel() {
|
||||
)
|
||||
}
|
||||
|
||||
function AxisView() {
|
||||
const world = useGameStore((s) => s.world)
|
||||
if (!world) return null
|
||||
const cells = yearAxis(world.state)
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-title">横轴年表(十年一格)</div>
|
||||
{cells.map((c) => (
|
||||
<div key={c.from} className="axis-cell">
|
||||
<div className="axis-decade">{decadeLabel(c.from, c.to)}</div>
|
||||
<div className="axis-events">
|
||||
<span className="tag good-t-bg">生 {c.births}</span>
|
||||
<span className="tag bad-t-bg">殇 {c.deaths}</span>
|
||||
{c.events.slice(0, 6).map((e, i) => (
|
||||
<span key={i} className={`tag ${e.kind === '战' || e.kind === '渡劫' ? 'bad-t-bg' : ''}`}>
|
||||
{e.kind}·{e.year}年 {e.label}
|
||||
</span>
|
||||
))}
|
||||
{c.events.length > 6 && <span className="dim2">…还有{c.events.length - 6}件</span>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ScrollScroll() {
|
||||
const world = useGameStore((s) => s.world)
|
||||
if (!world) return null
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useGameStore } from '../store'
|
||||
import { SURNAME_POOL } from '../../game/core/names'
|
||||
import { RngHub } from '../../game/core/rng'
|
||||
|
||||
export default function NewGame() {
|
||||
const go = useGameStore((s) => s.go)
|
||||
@@ -12,10 +13,7 @@ export default function NewGame() {
|
||||
const [slot, setSlot] = useState(1)
|
||||
const [randoming, setRandoming] = useState(false)
|
||||
|
||||
const seed = useMemo(
|
||||
() => `${Date.now()}-${Math.floor(Math.random() * 1e9)}-${Math.floor(Math.random() * 1e9)}`,
|
||||
[randoming]
|
||||
)
|
||||
const seed = useMemo(() => RngHub.rollSeed(), [randoming])
|
||||
|
||||
const rollNames = () => {
|
||||
setRandoming((r) => !r)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// 合成音效:鼓点·钟鸣·纸韵(Web Audio,零素材依赖)
|
||||
import { RngHub } from '../game/core/rng'
|
||||
let ctx: AudioContext | null = null
|
||||
let enabled = true
|
||||
|
||||
@@ -49,7 +50,7 @@ function noise(dur: number, gain: number, when = 0, freq = 3200): void {
|
||||
const len = Math.floor(a.sampleRate * dur)
|
||||
const buf = a.createBuffer(1, len, a.sampleRate)
|
||||
const data = buf.getChannelData(0)
|
||||
for (let i = 0; i < len; i++) data[i] = (Math.random() * 2 - 1) * (1 - i / len)
|
||||
for (let i = 0; i < len; i++) data[i] = (RngHub.audioNoise01() * 2 - 1) * (1 - i / len)
|
||||
const src = a.createBufferSource()
|
||||
src.buffer = buf
|
||||
const f = a.createBiquadFilter()
|
||||
|
||||
@@ -6,6 +6,7 @@ import { BattleLog, ChronicleEntry, GameState, LogItem, SaveMeta, YearlyReport,
|
||||
import { getSlotManager, getSaveSlot } from '../game/storage/db'
|
||||
import { metaFromState, updateSlotMeta } from './storeHelper'
|
||||
import { setSoundEnabled, sPaper, sGood, sBad, sWar, sBell, sGong, sClick, sTick } from './sound'
|
||||
import type { PhaseStat } from '../game/core/clock'
|
||||
|
||||
export type Screen = 'boot' | 'newgame' | 'game'
|
||||
export type PanelId = 'family' | 'genealogy' | 'territory' | 'market' | 'diplomacy' | 'expedition' | 'chronicle' | 'legacy' | 'settings'
|
||||
@@ -60,6 +61,7 @@ export interface GameStore {
|
||||
}
|
||||
|
||||
const LOG_CAP = 260
|
||||
const lastPhaseStats = new Map<World, PhaseStat[]>()
|
||||
let logSeq = 1
|
||||
|
||||
export const useGameStore = create<GameStore>((set, get) => ({
|
||||
@@ -147,13 +149,29 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
const w = st.world
|
||||
if (!w || st.pendingEventId || w.state.gameOver) return
|
||||
try {
|
||||
const t0 = performance.now()
|
||||
w.advanceMonth()
|
||||
const elapsed = performance.now() - t0
|
||||
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')
|
||||
}
|
||||
w.syncRng()
|
||||
sTick()
|
||||
set((s) => ({ revision: s.revision + 1 }))
|
||||
await Stash.save(st, '自动')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
// 出险保护:先落一档"出险前"现场再提示回档
|
||||
console.error('[advance-crash]', e)
|
||||
try {
|
||||
await Stash.save(st, '出险前')
|
||||
} catch {
|
||||
// 保存失败时不再叠加错误
|
||||
}
|
||||
set({ toast: '时日流转出现异常,已保存出险现场,可回档重来。', speed: 0 })
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user