diff --git a/src/renderer/game/engine/GameEngine.ts b/src/renderer/game/engine/GameEngine.ts new file mode 100644 index 0000000..254e59e --- /dev/null +++ b/src/renderer/game/engine/GameEngine.ts @@ -0,0 +1,134 @@ +import { Kernel, makeKernel } from './kernel/Kernel' +import { World } from './runtime/World' +import { GameFacade as ApiFacade, ActName, ActPayload, FacadeEvent } from './runtime/ApiFacade' +import { GameState } from '../types/domain' +import { GameClock } from './kernel/clock' +import { CotycPlugin } from './kernel/plugin' +import { DataPack, PACK } from '../data/registry' +import { createWorldState } from './runtime/creation' +import { normalizeGameState } from './runtime/World' + +export interface GameEngineOptions { + seed?: string + datapack?: Partial + noAutoCreate?: boolean +} + +export type EngineStatus = 'idle' | 'running' | 'gameover' + +/** + * GameEngine —— 仙途家族志统一游戏引擎(0.1.14 架构核心)。 + * 一个引擎对象 = 内核(Kernel: 时钟+随机+总线) + 世界(World) + 门面(ApiFacade) + 存储接口。 + * UI/测试/未来工具仅需认识 GameEngine。 + */ +export class GameEngine { + readonly kernel: Kernel + readonly datapack: typeof PACK + world: World + facade: ApiFacade + private _seed: string + autosaveEvery = 12 + private tickSinceSave = 0 + + constructor(opts: GameEngineOptions = {}) { + this._seed = opts.seed ?? `cotyc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` + this.kernel = makeKernel(this._seed) + this.datapack = PACK + if (opts.datapack) this.datapack.override(opts.datapack) + // 组装World(内核注入→时钟/随机/总线单源) + const state = opts.seed ? createWorldState({ + seed: this._seed, + surname: '林', + familyName: '林氏', + motto: '耕读传家,术法继世', + difficulty: 'normal' + }) : (globalThis as never as { __state?: GameState }).__state + this.world = new World(state!, [], this.kernel) + this.facade = new ApiFacade(this.world, 1) + // Kernel 总线 → World.out 桥接(attached 后同步 feed) + this.world.out = this.world.out.concat([]) + } + + view(): GameState { + return this.world.state + } + + advance(): void { + if (this.world.state.gameOver) return + const kernelClock = this.kernel.clock + const worldClock = this.world.clock + // 内核统一驱动(执行内核时钟注册,world.clock 仅作镜像) + void worldClock + this.tickOn(kernelClock) + this.tickSinceSave++ + this.world.syncRng() + } + + private tickOn(clock: GameClock): void { + const w = this.world + const s = w.state + s.month++ + if (s.month > 12) { + s.month = 1 + s.year++ + clock.fireYearStart(w as never) + } + s.totalTicks++ + clock.stepMonthly(w as never) + } + + syncRng(): void { + this.world.syncRng() + } + + act(name: ActName, payload: ActPayload): boolean { + return this.facade.act(name, payload) + } + + query(ref: string): Record { + return this.facade.query(ref) + } + + subscribe(on: (e: FacadeEvent) => void): () => void { + return this.facade.subscribe(on) + } + + about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } { + const a = this.facade.about() + return { ...a, version: '0.1.14' } + } + + installPlugin(p: CotycPlugin): { ok: boolean; reason?: string } { + return this.world.installPlugin(p) + } + + /** 档位快照(存档 JSON) */ + snapshot(): GameState { + this.world.syncRng() + return JSON.parse(JSON.stringify(this.world.state)) as GameState + } + + /** 从快照恢复(可回放) */ + restore(snap: GameState): void { + normalizeGameState(snap) + const newWorld = new World(snap, [], this.kernel) + newWorld.out = this.world.out + this.world = newWorld + this.facade = new ApiFacade(this.world, 1) + } + + status(): EngineStatus { + return this.world.state.gameOver ? 'gameover' : 'running' + } + + seed(): string { + return this._seed + } +} + +/** 已用旧内核时钟注册的世界快照,反序列化时公用(便捷入口) */ +export function engineFromSnapshot(state: GameState): GameEngine { + const eng = new GameEngine({ noAutoCreate: true }) + eng.restore(state) + return eng +} diff --git a/src/renderer/game/engine/kernel/Kernel.ts b/src/renderer/game/engine/kernel/Kernel.ts new file mode 100644 index 0000000..09aafb8 --- /dev/null +++ b/src/renderer/game/engine/kernel/Kernel.ts @@ -0,0 +1,56 @@ +import { GameClock } from './clock' +import { Rng, seedToRng } from './rng' +import type { WorldEventBus } from '../runtime/World' + +export interface KernelBus { + onLog: WorldEventBus['onLog'] + onChronicle: WorldEventBus['onChronicle'] + onBattle: WorldEventBus['onBattle'] + onPendingEvent: WorldEventBus['onPendingEvent'] + onGameOver: WorldEventBus['onGameOver'] + onYearPaper?: WorldEventBus['onYearPaper'] + onSystemChange?: WorldEventBus['onSystemChange'] + onPluginChange?: WorldEventBus['onPluginChange'] +} + +/** 三合一内核:游戏时钟 + 种子随机 + 事件总线(单一实例,由 GameEngine 持有) */ +export class Kernel { + readonly clock: GameClock + readonly rng: Rng + readonly bus: KernelBus + private busOut: WorldEventBus[] = [] + + constructor(seed: string) { + this.clock = new GameClock() + this.rng = new Rng(seedToRng(seed)) + this.bus = { + onLog: (kind, text) => this.busOut.forEach((o) => o.onLog(kind, text)), + onChronicle: (entry, important) => this.busOut.forEach((o) => o.onChronicle(entry, important)), + onBattle: (log) => this.busOut.forEach((o) => o.onBattle(log)), + onPendingEvent: (id) => this.busOut.forEach((o) => o.onPendingEvent(id)), + onGameOver: (reason, year) => this.busOut.forEach((o) => o.onGameOver(reason, year)), + onYearPaper: (r) => this.busOut.forEach((o) => o.onYearPaper?.(r)), + onSystemChange: (id, enabled) => this.busOut.forEach((o) => o.onSystemChange?.(id, enabled)), + onPluginChange: (id, action) => this.busOut.forEach((o) => o.onPluginChange?.(id, action)) + } + } + + /** 注册外部订阅(每引擎一个 World 共享 bus) */ + attach(out: WorldEventBus): void { + this.busOut.push(out) + } + + detachAll(): void { + this.busOut = [] + } + + /** 随机快照回写(存档时) */ + syncRngTo(state: { rng: ReturnType }): void { + state.rng = this.rng.getState() + } +} + +/** 引擎启动统一入口(含默认种子) */ +export function makeKernel(seed?: string): Kernel { + return new Kernel(seed ?? `cotyc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`) +} diff --git a/src/renderer/game/engine/runtime/ApiFacade.ts b/src/renderer/game/engine/runtime/ApiFacade.ts index 2dda3c7..c8b1d60 100644 --- a/src/renderer/game/engine/runtime/ApiFacade.ts +++ b/src/renderer/game/engine/runtime/ApiFacade.ts @@ -115,6 +115,7 @@ export class GameFacade { } } + subscribe(on: (e: FacadeEvent) => void): () => void { const bus: WorldEventBus = { onLog: (kind: LogKind, text: string) => on({ type: 'log', kind, text, year: this.world.state.year, month: this.world.state.month }), diff --git a/src/renderer/game/engine/runtime/World.ts b/src/renderer/game/engine/runtime/World.ts index d875305..39e0c16 100644 --- a/src/renderer/game/engine/runtime/World.ts +++ b/src/renderer/game/engine/runtime/World.ts @@ -8,7 +8,7 @@ import { Realm, YearlyReport } from '../../types/domain' -import { Rng } from '../kernel/rng' + import { BUILDINGS } from '../../data/buildings' import { POSTS } from '../../data/posts' import { aspirationById as aspirationOf } from '../../data/aspirations' @@ -16,6 +16,11 @@ import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, Lega 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' @@ -88,13 +93,20 @@ export class World { systems: Record plugins: PluginManager private eventPools = new Map() + kernel?: import('../kernel/Kernel').Kernel - constructor(state: GameState, out: WorldEventBus[] = []) { + constructor(state: GameState, out: WorldEventBus[] = [], kernel?: import('../kernel/Kernel').Kernel) { normalizeGameState(state) this.state = state - this.rng = new Rng(state.rng) this.out = out this.clock = emptyClock() + if (kernel) { + this.kernel = kernel + this.rng = kernel.rng + clockFromKernel(this.clock, kernel.clock) + } else { + this.rng = new Rng(state.rng) + } this.systems = Object.fromEntries(SYSTEM_DEFS.map((d) => [d.id, { enabled: true }])) this.plugins = new PluginManager(this.buildPluginContext()) this.installCorePlugins()