Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26bd61fba0 | ||
|
|
5dd5d6b558 | ||
|
|
8dfdaf843a | ||
|
|
ff6df8054c |
@@ -0,0 +1,94 @@
|
||||
# AGENTS.md
|
||||
|
||||
仙途家族志 · Chronicle of the Immortal Clan — Electron + React + TS 家族修仙模拟器。全部 UI 与文案为中文。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
npm test # vitest 引擎测试(tests/*.test.ts,全部纯 Node,无 DOM/DB 依赖)
|
||||
npx vitest run tests/world.test.ts # 单文件测试
|
||||
npm run typecheck # tsc --noEmit(npm 会打 warn 噪音,用 npx tsc --noEmit 更干净)
|
||||
npm run dev # electron-vite 热更新(开发期 origin 为 http://localhost:5173)
|
||||
npm run build # 产物到 out/
|
||||
npm run package # build + electron-builder(Linux 打 win 包可出产物,但未签名,Windows 上可能被 SmartScreen 静默拦截;正规发布在 Windows 侧重跑此命令)
|
||||
```
|
||||
|
||||
端到端冒烟(渲染 + OPFS 数据库 + 开局推进 + 可选截图):
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
SMOKE_TEST=1 npx electron out/main/index.js --no-sandbox --disable-gpu # 退出码 0 = 全通
|
||||
SMOKE_TEST=1 SMOKE_SHOTS_DIR=/tmp/opencode/shots npx electron ... # 附带 UI 截图
|
||||
```
|
||||
|
||||
冒烟会写真实用户数据目录(Linux 下 `~/.config/Electron`),跑完删掉该目录以免脏数据。
|
||||
无显示环境(WSLg 掉线)会打印 `[SMOKE-TIMEOUT]` 退出码 2 而非卡死;GUI 冒烟必须在 X11/WSLg 在线时跑(引擎与存储逻辑的回归请靠 vitest,不要依赖 GUI)。另注意:`npx electron` 可能拉取**新版** electron 缓存版(与本仓库 33.x 不同),二进制安装以 `node_modules/electron` 为准。
|
||||
|
||||
## 环境坑
|
||||
|
||||
- npm 12 拦截 postinstall:首次安装后须 `npm install-scripts approve electron esbuild`,否则 Electron 二进制缺失。
|
||||
- **`npm rebuild electron esbuild` 会删掉 `node_modules/electron/dist/electron`**(Linux 二进制),修复:
|
||||
`ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ node node_modules/electron/install.js`
|
||||
- 私有 registry 的 `_auth` token 只在用户级 `~/.npmrc`;项目 `.npmrc` 只保留 registry 映射,不要把 token 写进仓库。
|
||||
- Linux 无 emoji 字体:**所有图标一律用汉字印章字符**(renderer/ui/styles.css 的 `.s-icon/.res-icon/.bld-icon`),不要引入 emoji。
|
||||
|
||||
## 插件架构(0.1.8 起)
|
||||
|
||||
- **万物皆是插件**:`core/plugin.ts` 协议(Manifest/dependencies/conflicts/install/uninstall);`engine/pluginManager.ts` 安装管线(依赖校验、异常回滚、追踪型上下文——插件注册的时轮钩子卸载时全摘);`engine/plugin-bootstrap.ts` 三枚常驻核心插件(core-systems/core-data/core-events,protected 不可卸)。
|
||||
- **事件多池**:`world.eventPools`(core 池受保护不可删)+ `eventRoll` 聚合抽样;第三方插件 `ctx.addEventPool(id, events)` 即注入内容。**注意 findEvent 必须带 world 参数查池**(`events.ts`),否则未知事件直接软锁。
|
||||
- **能力卡**:`engine/capabilities.ts` 12 张;时钟注册走 `viaCap`;tournament/tribulation/apprentice/season 四卡各自内部检查 `w.sysEnabled`(无独立钩子,勿直接调)。
|
||||
- **门面**:`engine/api.ts` GameFacade(act 24 项/query 6 类/subscribe 退订协议/about);**UI store 的 `facade` 字段必须通过 openState/startNewGame 赋值**,否则 act 走残缺 fallback。
|
||||
- **金钟罩**:0.1.11 审计修复批次后三档指纹(47/100/180 年 × 3 seed = 9 值,见 tests/clock.test.ts;0.1.8 基线已作废、0.1.10 重建后再次漂移、0.1.11 大比 once/prune 后重固化)。
|
||||
|
||||
## 引擎系统(0.1.14 GameEngine)
|
||||
|
||||
- **入口单点** `engine/GameEngine.ts`:new GameEngine({seed,datapack}) → world/facade/kernel/datapack/sim;
|
||||
advance/act/query/subscribe/about/installPlugin/snapshot/restore。/ UI store 只认识 engine(world 为兼容仍暴露)。
|
||||
- **内核三合一** `engine/kernel/Kernel.ts`:Clock + Rng + 事件总线——单实例;World 的 clock/rng 注入自内核(双时钟已合一,勿再造)。
|
||||
- **世界自进化** `engine/sim/WorldSim.ts`(worldsim 相位 +「天下演序」能力卡可停用):
|
||||
资源循环市场(marketPool 供需→Market 行情乘子)/ NPC 演化(换代/势力=境界+财+兵)/ 秘境灵气(探索消耗+恢复)/
|
||||
灵气潮汐(天雨期×修炼)/ 灾年签(联动市场池)/ 天下快讯(newsFeed 滚动 N)。**所有随机走 w.rng**(禁 Math.random)。
|
||||
- **目录**:engine/kernel(底层原子)/ engine/runtime(World/插件/门面/Systems×13)/ engine/sim(经济+世界演化)/ engine/narrative(史书族谱维度)。
|
||||
- 旧目录(game/core、engine/systems、engine/world.ts、engine/api.ts)**全部废弃**,勿再引用。
|
||||
|
||||
## 架构要点
|
||||
|
||||
- `src/renderer/game/`:纯 TS 游戏引擎(无 React/DOM import),可被 vitest 直接测试;`types/domain.ts` 是全量领域类型,改状态结构先看它。
|
||||
- **统一时轮 `core/clock.ts`**:月度 phase(production/aging/cultivation/missions/events/diplomacy/epilogue)+ 年首钩子均注册于 `engine/clocks.ts`。**新增系统 = 注册一行,禁止手改 `advanceMonth` 本体**。
|
||||
- **统一随机 `core/rng.ts`**:引擎只经 `World.rng`(含 `nextCount` 审计);UI 播种用 `RngHub.rollSeed()`、音效白噪用 `RngHub.audioNoise01()`,**不要**在逻辑里引入 Math.random()。
|
||||
- **金钟罩 `tests/clock.test.ts`**:3 枚固定 seed × 三档月数(560/1200/2160 = 47/100/180 年)指纹常驻(0.1.11 基线:见注释 GOLDEN)。任何改动若破坏确定性立即红;**有意变更时序/数值时**指纹一并重算并在注释注明原因。
|
||||
- `src/renderer/ui/`:React + zustand(`ui/store.ts`)。World 的 game state 是 mutable,advance 后 `revision++` 触发重渲染;订阅 `revision` 是面板刷新惯例。
|
||||
- 引擎 = 种子随机数(sfc32,`core/rng.ts`)+ 不可变快照存 `state.rng`,同 seed 全程可重放(tests/world.test.ts 有确定性用例,改任何 tick 顺序都要保证仍然确定性)。
|
||||
- 建档开头成员 id 硬编码 `x1`~`x5`,tests 依赖。
|
||||
|
||||
## MetonaSqlark(@metona-team/metona-sqlark 0.7.4)陷阱
|
||||
|
||||
- SQL 方言为自有实现(不是 SQLite):建表列类型写 `string|number|boolean|date|json`,**不是** `INTEGER/TEXT`;不支持 `INSERT OR REPLACE`、`ON CONFLICT`、`rowid`——用「SELECT 判断 → UPDATE/INSERT」或直接 `DELETE+INSERT`。
|
||||
- aria 引擎同库一个 tab 只能建一次连接(Web Locks → `ARIA_LOCKED`):存取必须走 `game/storage/db.ts` 的单例 `getSaveSlot(slot)` / `getSlotManager()`,**禁止**每次 `new SaveSlot/new SlotManager`。
|
||||
- 数据在浏览器 OPFS(renderer 内),main 进程无 DB 逻辑;开发与打包后的 origin 不同,两环境存档不互通。
|
||||
- 存储层通过 `SaveDbDriver` 抽象(`storage/slots.ts`),node 环境下无 OPFS,测试只覆盖引擎不覆盖 DB。
|
||||
|
||||
## 数值基调(0.1.10 重建后,勿当 bug 调回去)
|
||||
|
||||
- **pacing.ts 的 MAJOR_RATE 是进度唯一生效因子**(qi 2.2、foundation 1.4、core 0.95、nascent 0.68、spirit 0.48);`realms.ts` 的 `expBase/expGrowth/maxRealmExp` 曲线**未参与计算**(历史摆设,勿以它反推)。
|
||||
- 月率 base = `1.0 + perception*0.18`;65 岁起 ×0.72 衰减、8 岁以下 ×0.65(圣者护族 60+ 高境界者存在时乘 1.3)。
|
||||
- 寿元:75/150/220/340/520/850(凡人→化神,0.1.10 放宽的梯度)。
|
||||
- 大境界晋升走**渡劫事件三选**(硬渡/护法/压制);玩家 12 月不应会**自动压制一年**(防挂机软锁,events.ts 计数 `tribPendingMonths`)。
|
||||
- 周期事件优先级:命运(百年/飞升)> 大比 > 传薪 > 拍卖 > 岁祷 > 回声;大比有 `once`,一年一届。
|
||||
- `family.flag` 年份键(auction-/prayerDone-/echoDone-/recruitDone-)每 tick 剪 3 年前旧键。
|
||||
|
||||
## 修改纪律(防覆盖——血泪教训,务必遵守)
|
||||
|
||||
**背景**:曾多次出现「大改后我之前的修复被覆盖」:① 批量 python 替换命中旧文本、② `git checkout` 回滚临时参数时误恢复了已修复文件、③ 编辑器链式 replace 生成重复属性/孤行。均靠 tsc/vitest 事后兜住,但应防于未然。
|
||||
|
||||
**硬性规则**:
|
||||
1. **改动前**:`git status` 确认工作区干净(或先提交当前成果,原子提交,一功一提交)。
|
||||
2. **改动中**:批量替换一律用「先 assert 旧文本存在 → 替换 → 再全局 grep 断言新文本唯一」三步;禁止无检查的 sed/python 盲替。
|
||||
3. **禁止 `git checkout -- <file>` / 整文件回滚**(这是历史事故元凶)。确需回滚时:`git show HEAD:<file> > /tmp/bak` 手动 diff 恢复,恢复后跑全量测试确认无伤。
|
||||
4. **优先小步快跑**:每个功能块改完立即 `npm test`(引擎)或 `tsc`(UI 类型),绿了再动下一处——绝不在错误状态上加新修改。
|
||||
5. **高危文件白名单**(world.ts / events.ts / store.ts / styles.css / cultivation.ts)改动后必须 `git diff --stat` 核对改动面与预期一致(±文件行数应与你改的块吻合)。
|
||||
6. 每次迭代收尾:`git status` 应干净 + `git log --oneline` 确认每步都有提交记录;凡 "git stash/checkout/rm" 类命令,执行前后都 `git status` 留痕。
|
||||
|
||||
- 提交前:`npm test` + `npm run typecheck` 必须绿(改存储/SQL 后再跑一次冒烟)。
|
||||
- 平衡数值集中在 `game/data/`(realms/pacing/buildings/events...),调平衡不改引擎流程。
|
||||
- 发布产物 `release/`、`out/` 均已 gitignore,勿入库。
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chronicle-of-the-immortal-clan",
|
||||
"productName": "仙途家族志",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.14",
|
||||
"description": "修仙 · 家族 · 经营 · 战斗 模拟器",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "MetonaTeam",
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
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<DataPack>
|
||||
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
|
||||
this.world.advanceMonth()
|
||||
this.tickSinceSave++
|
||||
}
|
||||
|
||||
syncRng(): void {
|
||||
this.world.syncRng()
|
||||
}
|
||||
|
||||
act(name: ActName, payload: ActPayload): boolean {
|
||||
return this.facade.act(name, payload)
|
||||
}
|
||||
|
||||
query(ref: string): Record<string, unknown> {
|
||||
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
|
||||
}
|
||||
@@ -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<Rng['getState']> }): 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)}`)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { World } from '../engine/world'
|
||||
import type { World } from '../runtime/World'
|
||||
|
||||
/**
|
||||
* 统一时轮调度:所有系统以 phase 注册,由时钟按固定顺序驱动。
|
||||
@@ -14,6 +14,7 @@ export type PhaseId =
|
||||
| 'missions'
|
||||
| 'events'
|
||||
| 'diplomacy'
|
||||
| 'worldsim'
|
||||
| 'epilogue'
|
||||
|
||||
export type SystemHook = (w: World) => void
|
||||
@@ -31,6 +32,7 @@ export const PHASE_ORDER: PhaseId[] = [
|
||||
'missions',
|
||||
'events',
|
||||
'diplomacy',
|
||||
'worldsim',
|
||||
'epilogue'
|
||||
]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { World } from '../engine/world'
|
||||
import type { World } from '../runtime/World'
|
||||
|
||||
export interface GuideStep {
|
||||
id: string
|
||||
@@ -1,7 +1,7 @@
|
||||
import { GameClock, SystemHook } from './clock'
|
||||
import { DataPack } from '../data/registry'
|
||||
import { EventDef } from '../data/events'
|
||||
import type { World } from '../engine/world'
|
||||
import { DataPack } from '../../data/registry'
|
||||
import { EventDef } from '../../data/events'
|
||||
import type { World } from '../runtime/World'
|
||||
|
||||
export type PluginKind = 'system' | 'data' | 'events' | 'content'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { RngState } from '../types/domain'
|
||||
import type { RngState } from '../../types/domain'
|
||||
|
||||
function xmur3(str: string): () => number {
|
||||
let h = 1779033703 ^ str.length
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Season } from '../data/season'
|
||||
import { Season } from '../../data/season'
|
||||
|
||||
export const TIDES: string[] = [
|
||||
'立春', '雨水', '惊蛰', '春分', '清明', '谷雨',
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { World } from '../engine/world'
|
||||
import type { World } from '../runtime/World'
|
||||
|
||||
export interface Urgency {
|
||||
blockedCount: number
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
import { GameState } from '../types/domain'
|
||||
import { TECHNIQUES } from '../data/techniques'
|
||||
import { describeRealm } from '../data/realms'
|
||||
import { aspirationById } from '../data/aspirations'
|
||||
import { postById } from '../data/posts'
|
||||
import { GameState } from '../../types/domain'
|
||||
import { TECHNIQUES } from '../../data/techniques'
|
||||
import { describeRealm } from '../../data/realms'
|
||||
import { aspirationById } from '../../data/aspirations'
|
||||
import { postById } from '../../data/posts'
|
||||
|
||||
export interface BioLine {
|
||||
at?: number
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Character } from '../types/domain'
|
||||
import type { World } from '../engine/world'
|
||||
import { Character } from '../../types/domain'
|
||||
import type { World } from '../runtime/World'
|
||||
|
||||
export interface FamilyUnit {
|
||||
parents: [Character | undefined, Character | undefined]
|
||||
@@ -1,5 +1,5 @@
|
||||
import { GameState } from '../types/domain'
|
||||
import { MAJOR_ORDER } from '../data/realms'
|
||||
import { GameState } from '../../types/domain'
|
||||
import { MAJOR_ORDER } from '../../data/realms'
|
||||
|
||||
export interface LegacyDims {
|
||||
renXing: number
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { GameState } from '../types/domain'
|
||||
import { GameState } from '../../types/domain'
|
||||
|
||||
export interface LegacyReport {
|
||||
years: number[]
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GameState } from '../types/domain'
|
||||
import { GameState } from '../../types/domain'
|
||||
|
||||
export interface AxisEvent {
|
||||
year: number
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { World } from './world'
|
||||
import { WorldEventBus } from './world'
|
||||
import { YearlyReport, BattleLog, ChronicleEntry, SaveMeta } from '../types/domain'
|
||||
import { marketPrice, buyItem, sellItem, buyTechnique } from './market'
|
||||
import { sendMission, recallAll } from './systems/missions'
|
||||
import { giftNpc, makePeace, marryNpcFamily } from './systems/diplomacy'
|
||||
import { combatPowerOf } from './systems/combat'
|
||||
import type { LogKind } from './world'
|
||||
import { computeLegacy, resolveLegacy, LegacyArch } from '../core/legacy'
|
||||
import { yearAxis, AxisCell } from '../core/yearaxis'
|
||||
import { PACK } from '../data/registry'
|
||||
import type { World } from './World'
|
||||
import { WorldEventBus } from './World'
|
||||
import { YearlyReport, BattleLog, ChronicleEntry, SaveMeta } from '../../types/domain'
|
||||
import { marketPrice, buyItem, sellItem, buyTechnique } from '../sim/Market'
|
||||
import { sendMission, recallAll } from './Systems/missions'
|
||||
import { giftNpc, makePeace, marryNpcFamily } from './Systems/diplomacy'
|
||||
import { combatPowerOf } from './Systems/combat'
|
||||
import type { LogKind } from './World'
|
||||
import { computeLegacy, resolveLegacy, LegacyArch } from '../narrative/legacy'
|
||||
import { yearAxis, AxisCell } from '../narrative/yearaxis'
|
||||
import { PACK } from '../../data/registry'
|
||||
|
||||
export type ActName =
|
||||
| 'head.set'
|
||||
@@ -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 }),
|
||||
+9
-9
@@ -1,14 +1,14 @@
|
||||
import type { World } from '../world'
|
||||
import { Character, BattleLog } from '../../types/domain'
|
||||
import { basePower, describeRealm } from '../../data/realms'
|
||||
import { pack } from '../../data/registry'
|
||||
import { techniqueById } from '../../data/techniques'
|
||||
import { EnemyDef, LootDef } from '../../data/secrets'
|
||||
import type { World } from '../World'
|
||||
import { Character, BattleLog } from '../../../types/domain'
|
||||
import { basePower, describeRealm } from '../../../data/realms'
|
||||
import { pack } from '../../../data/registry'
|
||||
import { techniqueById } from '../../../data/techniques'
|
||||
import { EnemyDef, LootDef } from '../../../data/secrets'
|
||||
|
||||
import { traitBonuses } from '../pcgen'
|
||||
import { npcById } from '../../data/npcs'
|
||||
import { aspirationById, fitBonusOf } from '../../data/aspirations'
|
||||
import { formationById, FormationId } from '../../data/formations'
|
||||
import { npcById } from '../../../data/npcs'
|
||||
import { aspirationById, fitBonusOf } from '../../../data/aspirations'
|
||||
import { formationById, FormationId } from '../../../data/formations'
|
||||
|
||||
export function combatPowerOf(w: World, c: Character): number {
|
||||
if (!c.alive) return 0
|
||||
+13
-10
@@ -1,16 +1,16 @@
|
||||
import type { World } from '../world'
|
||||
import { Character } from '../../types/domain'
|
||||
import { ROOT_GRADES } from '../../data/elements'
|
||||
import { masteryRateOfMajor } from '../../data/pacing'
|
||||
import { aspirationById, fitBonusOf } from '../../data/aspirations'
|
||||
import { techniqueById } from '../../data/techniques'
|
||||
import { nextRealm, breakthroughBaseChance, realmDeathChance, describeRealm, MAJOR_ORDER } from '../../data/realms'
|
||||
import type { World } from '../World'
|
||||
import { Character } from '../../../types/domain'
|
||||
import { ROOT_GRADES } from '../../../data/elements'
|
||||
import { masteryRateOfMajor } from '../../../data/pacing'
|
||||
import { aspirationById, fitBonusOf } from '../../../data/aspirations'
|
||||
import { techniqueById } from '../../../data/techniques'
|
||||
import { nextRealm, breakthroughBaseChance, realmDeathChance, describeRealm, MAJOR_ORDER } from '../../../data/realms'
|
||||
import { lifespanOf } from './lifecycle'
|
||||
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 { MALE_GIVEN, FEMALE_GIVEN } from '../../kernel/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 {
|
||||
@@ -53,6 +53,9 @@ export function monthlyRate(w: World, c: Character): number {
|
||||
if (w.ageOf(c) < 8) rate *= 0.4
|
||||
if (w.ageOf(c) > 65) rate *= 0.72
|
||||
rate *= masteryRateOfMajor(c.realm.major)
|
||||
// 世界灵气潮汐
|
||||
const tide = w.state.worldSim?.tide ?? 1
|
||||
rate *= tide
|
||||
return rate
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import type { World } from '../world'
|
||||
import { npcById } from '../../data/npcs'
|
||||
import type { World } from '../World'
|
||||
import { npcById } from '../../../data/npcs'
|
||||
import { findEvent, fire } from './events'
|
||||
|
||||
export function diplomacyTick(w: World): void {
|
||||
+7
-7
@@ -1,16 +1,16 @@
|
||||
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'
|
||||
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'
|
||||
|
||||
|
||||
import { npcById } from '../../data/npcs'
|
||||
import { npcById } from '../../../data/npcs'
|
||||
import { resolveRaid } from './combat'
|
||||
import { sendMission } from './missions'
|
||||
import { pack } from '../../data/registry'
|
||||
import { pack } from '../../../data/registry'
|
||||
import { findInheritor } from '../creation'
|
||||
import { resolveTribulation } from './tribulation'
|
||||
import { nextRealm, describeRealm } from '../../data/realms'
|
||||
import { nextRealm, describeRealm } from '../../../data/realms'
|
||||
|
||||
const ALL_EVENTS: EventDef[] = [...EVENTS]
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import type { World } from '../world'
|
||||
import { MAJORS } from '../../data/realms'
|
||||
import type { World } from '../World'
|
||||
import { MAJORS } from '../../../data/realms'
|
||||
import { calcLifespan } from '../pcgen'
|
||||
|
||||
export function lifespanOf(w: World, c: { realm: { major: keyof typeof MAJORS }; physique: number }): number {
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import type { World } from '../world'
|
||||
import { Character } from '../../types/domain'
|
||||
import type { World } from '../World'
|
||||
import { Character } from '../../../types/domain'
|
||||
import { produceOffspring } from './cultivation'
|
||||
import { yearGrowth } from './diplomacy'
|
||||
import { MALE_GIVEN, FEMALE_GIVEN } from '../../core/names'
|
||||
import { aspirationById } from '../../data/aspirations'
|
||||
import { MALE_GIVEN, FEMALE_GIVEN } from '../../kernel/names'
|
||||
import { aspirationById } from '../../../data/aspirations'
|
||||
|
||||
export function yearStartMarriage(w: World): void {
|
||||
yearGrowth(w)
|
||||
+15
-6
@@ -1,10 +1,10 @@
|
||||
import type { World } from '../world'
|
||||
import { MissionState } from '../../types/domain'
|
||||
import { FormationId } from '../../data/formations'
|
||||
import { missionById, MissionDef, ENEMIES } from '../../data/secrets'
|
||||
import type { World } from '../World'
|
||||
import { MissionState } from '../../../types/domain'
|
||||
import { FormationId } from '../../../data/formations'
|
||||
import { missionById, MissionDef, ENEMIES } from '../../../data/secrets'
|
||||
import { resolveEncounter, rollWarbooty } from './combat'
|
||||
import { techniqueById } from '../../data/techniques'
|
||||
import { describeRealm } from '../../data/realms'
|
||||
import { techniqueById } from '../../../data/techniques'
|
||||
import { describeRealm } from '../../../data/realms'
|
||||
|
||||
export function missionTick(w: World): void {
|
||||
const active = w.state.missions.filter((m) => !m.done)
|
||||
@@ -138,6 +138,15 @@ 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
|
||||
}
|
||||
}
|
||||
w.state.missions.push(m)
|
||||
w.state.family.missionIds.push(m.id)
|
||||
w.log('info', `队伍出发探索【${def.name}】。`)
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import type { World } from '../world'
|
||||
import { aspirationById } from '../../data/aspirations'
|
||||
import { seasonMod } from '../../data/season'
|
||||
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
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
import type { World } from '../world'
|
||||
import { Character } from '../../types/domain'
|
||||
import { ENEMIES, EnemyDef } from '../../data/secrets'
|
||||
import type { World } from '../World'
|
||||
import { Character } from '../../../types/domain'
|
||||
import { ENEMIES, EnemyDef } from '../../../data/secrets'
|
||||
import { resolveEncounter } from './combat'
|
||||
import { rankPower } from './events'
|
||||
import { resolveBreakthrough } from './cultivation'
|
||||
import { FormationId } from '../../data/formations'
|
||||
import { FormationId } from '../../../data/formations'
|
||||
|
||||
export const SECTS = ['云栖宗', '太谷书院', '玄微剑阁', '丹霞洞天']
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import type { World } from '../world'
|
||||
import { Character } from '../../types/domain'
|
||||
import { nextRealm, MAJOR_ORDER, realmDeathChance, describeRealm } from '../../data/realms'
|
||||
import type { World } from '../World'
|
||||
import { Character } from '../../../types/domain'
|
||||
import { nextRealm, MAJOR_ORDER, realmDeathChance, describeRealm } from '../../../data/realms'
|
||||
|
||||
export const TRIB_MAJORS: string[] = ['foundation', 'core', 'nascent', 'spirit']
|
||||
|
||||
@@ -7,25 +7,30 @@ import {
|
||||
LogItem,
|
||||
Realm,
|
||||
YearlyReport
|
||||
} from '../types/domain'
|
||||
import { Rng } from '../core/rng'
|
||||
import { BUILDINGS } from '../data/buildings'
|
||||
import { POSTS } from '../data/posts'
|
||||
import { aspirationById as aspirationOf } from '../data/aspirations'
|
||||
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../core/legacy'
|
||||
} from '../../types/domain'
|
||||
|
||||
import { BUILDINGS } from '../../data/buildings'
|
||||
import { POSTS } from '../../data/posts'
|
||||
import { aspirationById as aspirationOf } from '../../data/aspirations'
|
||||
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../narrative/legacy'
|
||||
import { createWorldState, findInheritor } from './creation'
|
||||
import { SYSTEM_DEFS, SystemDef } from './capabilities'
|
||||
import { emptyClock } from './clocks'
|
||||
import { CotycPlugin, PluginContext, PluginStatus } from '../core/plugin'
|
||||
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'
|
||||
import { pack, DEFAULT_PACK, PACK } from '../data/registry'
|
||||
import { EventDef } from '../../data/events'
|
||||
import { pack, DEFAULT_PACK, PACK } from '../../data/registry'
|
||||
import { CORE_PLUGINS } from './plugin-bootstrap'
|
||||
import { GameClock } from '../core/clock'
|
||||
import { SystemHook } from '../core/clock'
|
||||
import { resolveBreakthrough } from './systems/cultivation'
|
||||
import { applyEventChoice } from './systems/events'
|
||||
import { combatPowerOf } from './systems/combat'
|
||||
import { GameClock } from '../kernel/clock'
|
||||
import { SystemHook } from '../kernel/clock'
|
||||
import { resolveBreakthrough } from './Systems/cultivation'
|
||||
import { applyEventChoice } from './Systems/events'
|
||||
import { combatPowerOf } from './Systems/combat'
|
||||
|
||||
export type LogKind = LogItem['kind']
|
||||
|
||||
@@ -40,6 +45,12 @@ export interface WorldEventBus {
|
||||
onPluginChange?(id: string, action: string): void
|
||||
}
|
||||
|
||||
import { WorldSim } from '../sim/WorldSim'
|
||||
|
||||
export function worldSimOf(w: World): WorldSim {
|
||||
return new WorldSim(w)
|
||||
}
|
||||
|
||||
export function normalizeGameState(state: GameState): GameState {
|
||||
// 老版本存档(<0.1.1)缺少新增字段,加载时补齐,避免运行期 undefined 崩溃
|
||||
if (!state.finance) state.finance = { accum: 0 }
|
||||
@@ -88,13 +99,20 @@ export class World {
|
||||
systems: Record<string, { enabled: boolean }>
|
||||
plugins: PluginManager
|
||||
private eventPools = new Map<string, EventDef[]>()
|
||||
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.clock = kernel.clock
|
||||
this.kernel = kernel
|
||||
this.rng = kernel.rng
|
||||
} else {
|
||||
this.clock = emptyClock()
|
||||
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()
|
||||
@@ -138,7 +156,7 @@ export class World {
|
||||
}
|
||||
}
|
||||
|
||||
private packOverride(partial: Partial<import('../data/registry').DataPack>): void {
|
||||
private packOverride(partial: Partial<import('../../data/registry').DataPack>): void {
|
||||
PACK.override(partial)
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { PhaseId } from '../core/clock'
|
||||
import type { PhaseId } from '../kernel/clock'
|
||||
|
||||
export interface SystemDef {
|
||||
id: string
|
||||
@@ -1,13 +1,14 @@
|
||||
import { GameClock } from '../core/clock'
|
||||
import { PluginContext } from '../core/plugin'
|
||||
import type { World } from './world'
|
||||
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 { GameClock } from '../kernel/clock'
|
||||
import { PluginContext } from '../kernel/plugin'
|
||||
import type { World } from './World'
|
||||
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 { WorldSim } from '../sim/WorldSim'
|
||||
|
||||
/** 原语义保留:满门凋零后仅存生产/寿元与收尾 */
|
||||
function ifAlive(fn: (w: World) => void): (w: World) => void {
|
||||
@@ -55,6 +56,11 @@ export function installCoreSystems(ctx: PluginContext): Array<() => void> {
|
||||
unsubs.push(clock.register('missions', viaCap('missions', ifAlive((w: World) => missionTick(w)))))
|
||||
unsubs.push(clock.register('events', viaCap('events', ifAlive((w: World) => eventRoll(w)))))
|
||||
unsubs.push(clock.register('diplomacy', viaCap('diplomacy', ifAlive((w: World) => diplomacyTick(w)))))
|
||||
unsubs.push(clock.register('worldsim', (w: World) => {
|
||||
if (w.sysEnabled('worldsim')) {
|
||||
new WorldSim(w).tick()
|
||||
}
|
||||
}))
|
||||
unsubs.push(clock.register('epilogue', (w: World) => w.epilogueTick()))
|
||||
|
||||
return unsubs
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Character, GameState, NpcFamilyState, RealmMajor } from '../types/domain'
|
||||
import { Rng, seedToRng } from '../core/rng'
|
||||
import { randomSurname, MALE_GIVEN, FEMALE_GIVEN } from '../core/names'
|
||||
import { Character, GameState, NpcFamilyState, RealmMajor } from '../../types/domain'
|
||||
import { Rng, seedToRng } from '../kernel/rng'
|
||||
import { randomSurname, MALE_GIVEN, FEMALE_GIVEN } from '../kernel/names'
|
||||
import { newCharacter } from './pcgen'
|
||||
import { NPCS } from '../data/npcs'
|
||||
import { World } from './world'
|
||||
import { NPCS } from '../../data/npcs'
|
||||
import { World } from './World'
|
||||
|
||||
export interface NewGameOptions {
|
||||
seed: string
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { CotycPlugin } from '../core/plugin'
|
||||
import { CotycPlugin } from '../kernel/plugin'
|
||||
|
||||
/** 示例内容插件:注入事件池 + 一个护山能力(开发范本) */
|
||||
export const examplePlugin: CotycPlugin = {
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Character, Element, Gender, Realm, RealmMajor } from '../types/domain'
|
||||
import { Rng } from '../core/rng'
|
||||
import { ELEMENT_LIST, ROOT_GRADES } from '../data/elements'
|
||||
import { MAJORS } from '../data/realms'
|
||||
import { TRAIT_POOL, TRAITS } from '../data/traits'
|
||||
import { Character, Element, Gender, Realm, RealmMajor } from '../../types/domain'
|
||||
import { Rng } from '../kernel/rng'
|
||||
import { ELEMENT_LIST, ROOT_GRADES } from '../../data/elements'
|
||||
import { MAJORS } from '../../data/realms'
|
||||
import { TRAIT_POOL, TRAITS } from '../../data/traits'
|
||||
|
||||
export function rollRoots(rng: Rng, parents?: { m?: Character; f?: Character }): { grade: number; primary: Element; secondary: Element[] } {
|
||||
let grade: number
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { CotycPlugin } from '../core/plugin'
|
||||
import { CotycPlugin } from '../kernel/plugin'
|
||||
import { installCoreSystems } from './clocks'
|
||||
import { EVENTS } from '../data/events'
|
||||
import { DEFAULT_PACK } from '../data/registry'
|
||||
import { EVENTS } from '../../data/events'
|
||||
import { DEFAULT_PACK } from '../../data/registry'
|
||||
|
||||
/** 内置插件一:镇族基石(12 能力卡与时轮钩子) */
|
||||
export function makeCoreSystemsPlugin(): CotycPlugin {
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
import { CotycPlugin, PluginContext, PluginStatus, PluginChange } from '../core/plugin'
|
||||
import { SystemDef, SYSTEM_DEFS } from '../engine/capabilities'
|
||||
import type { World } from '../engine/world'
|
||||
import { SystemHook } from '../core/clock'
|
||||
import { GameClock } from '../core/clock'
|
||||
import { CotycPlugin, PluginContext, PluginStatus, PluginChange } from '../kernel/plugin'
|
||||
import { SystemDef, SYSTEM_DEFS } from './capabilities'
|
||||
import type { World } from './World'
|
||||
import { SystemHook } from '../kernel/clock'
|
||||
import { GameClock } from '../kernel/clock'
|
||||
|
||||
interface RuntimePlugin {
|
||||
plugin: CotycPlugin
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { World } from './world'
|
||||
import { pack } from '../data/registry'
|
||||
import { traitBonuses } from './pcgen'
|
||||
import type { World } from '../runtime/World'
|
||||
import { pack } from '../../data/registry'
|
||||
import { traitBonuses } from '../runtime/pcgen'
|
||||
|
||||
import { WorldSim } from './WorldSim'
|
||||
|
||||
export function marketPrice(w: World, itemId: string): number {
|
||||
const item = pack().items[itemId]
|
||||
@@ -9,10 +11,18 @@ export function marketPrice(w: World, itemId: string): number {
|
||||
const fam = w.state.family
|
||||
const mult = typeof fam.flag['priceMult'] === 'number' ? (fam.flag['priceMult'] as number) : 1
|
||||
const mood = fam.reputation >= 40 ? 1.06 : fam.reputation >= 20 ? 1.02 : 0.98
|
||||
// 世界行情乘子(缺省 1)
|
||||
let simMult = 1
|
||||
if (w.state.worldSim?.marketPool) {
|
||||
const pool = w.state.worldSim.marketPool[itemId] as number | undefined
|
||||
const basePool = POOL_BASE[itemId] ?? 100
|
||||
const r = (pool ?? basePool) / basePool
|
||||
simMult = Math.max(0.55, Math.min(2.3, r))
|
||||
}
|
||||
// 利己(priceMult)与信誉修正
|
||||
const sellers = w.aliveMembers().filter((c) => traitBonuses(c).priceMult > 0).length
|
||||
const liarPct = Math.min(0.2, sellers * 0.04)
|
||||
return Math.max(1, Math.round(base * mult * mood * (1 - liarPct)))
|
||||
return Math.max(1, Math.round(base * mult * mood * simMult * (1 - liarPct)))
|
||||
}
|
||||
|
||||
export function buyItem(w: World, itemId: string, count: number): boolean {
|
||||
@@ -48,7 +58,9 @@ export function techniquePrice(techId: string): number {
|
||||
return TECH_GRADE_BASE[techId] ?? 200
|
||||
}
|
||||
|
||||
import { TECHNIQUES } from '../data/techniques'
|
||||
import { TECHNIQUES } from '../../data/techniques'
|
||||
|
||||
const POOL_BASE: Record<string, number> = { lingcao: 600, lingkuang: 300, beastcore: 80, 'pill-qiyuan': 90, 'pill-ningyuan': 40 }
|
||||
|
||||
const TECHNIQUE_GRADE_PRICE: Record<number, number> = { 1: 120, 2: 300, 3: 700, 4: 1600 }
|
||||
const TECH_GRADE_BASE: Record<string, number> = Object.fromEntries(
|
||||
@@ -0,0 +1,211 @@
|
||||
/** WorldSim —— 世界自进化引擎(game/engine/sim/WorldSim.ts) */
|
||||
import { World } from '../runtime/World'
|
||||
import { WorldSimState, NpcDynamics, WORLDSIM, CALAMITY_EFFECT, CalamityName } from './worldsim-data'
|
||||
import { ITEMS } from '../../data/items'
|
||||
import { npcById } from '../../data/npcs'
|
||||
import { MAJORS, MAJOR_ORDER } from '../../data/realms'
|
||||
|
||||
const MARKET_IDS = ['lingcao', 'lingkuang', 'beastcore', 'pill-qiyuan', 'pill-ningyuan']
|
||||
|
||||
export class WorldSim {
|
||||
constructor(private w: World) {}
|
||||
|
||||
private s(): WorldSimState {
|
||||
const w = this.w
|
||||
if (!w.state.worldSim) w.state.worldSim = initSim(w) as never
|
||||
return w.state.worldSim as WorldSimState
|
||||
}
|
||||
|
||||
tick(): void {
|
||||
const s = this.s()
|
||||
const rng = this.w.rng
|
||||
// ---- C. 灵气潮汐(大周期) ----
|
||||
s.tideTicks++
|
||||
const phase = (s.tideTicks % WORLDSIM.tideCycle) / WORLDSIM.tideCycle
|
||||
const sine = 0.5 + 0.5 * Math.sin(phase * Math.PI * 2)
|
||||
s.tide = WORLDSIM.tideMin + sine * (WORLDSIM.tideMax - WORLDSIM.tideMin)
|
||||
|
||||
// 秘境灵气(自动恢复 + 探索消耗由 missions 在探索时扣)
|
||||
for (const key of Object.keys(s.secretQi)) {
|
||||
s.secretQi[key] = clamp(s.secretQi[key] + (s.tide > 0.85 ? 5 : 2) - (s.secretQi[key] > 70 ? 2 : 0), 0, 100)
|
||||
}
|
||||
|
||||
// ---- D. 灾变年签(每年首月一掷) ----
|
||||
if (this.w.state.month === 1 && rng.chance(WORLDSIM.calamityChance)) {
|
||||
const cl = rng.pick([...WORLDSIM.calamities])
|
||||
s.calamity = cl
|
||||
s.calamityYear = this.w.state.year
|
||||
applyCalamityToMarket(s, cl as CalamityName)
|
||||
this.w.log('bad', `【天下灾年】${cl}——灵植减产,市价将行。`)
|
||||
}
|
||||
|
||||
// ---- A. 资源循环市场(库存自然流向 + 再平衡 + 价格信号) ----
|
||||
driftMarket(s, rng.next())
|
||||
|
||||
// ---- B. NPC 演化(聚合模拟) ----
|
||||
evolveNpc(this.w, s, rng.next())
|
||||
|
||||
// ---- E. 天下快讯(节流) ----
|
||||
if (this.w.state.month !== s.lastNewsMonth && this.w.state.totalTicks % WORLDSIM.newsEvery === 0) {
|
||||
pushNews(this.w, s, MARKET_IDS)
|
||||
}
|
||||
|
||||
// ---- C2. 快讯裁剪 ----
|
||||
if (s.newsFeed.length > WORLDSIM.newsKeep) s.newsFeed.splice(0, s.newsFeed.length - WORLDSIM.newsKeep)
|
||||
}
|
||||
|
||||
/** 秘境探索消耗灵气(missions 调用) */
|
||||
consumeSecretQi(id: string, amount: number): void {
|
||||
const s = this.s()
|
||||
if (s.secretQi[id] === undefined) s.secretQi[id] = 50
|
||||
s.secretQi[id] = clamp(s.secretQi[id] - amount, 0, 100)
|
||||
}
|
||||
|
||||
/** 灵气系数(修炼/掉落市场乘子) */
|
||||
tideMult(): number {
|
||||
return this.s().tide
|
||||
}
|
||||
|
||||
/** 当前市场行情(价格乘子,反馈到 Market) */
|
||||
marketMultFor(id: string): number {
|
||||
const s = this.s()
|
||||
const pool = s.marketPool[id] ?? 50
|
||||
const base = poolBase(id)
|
||||
return clamp(pool / base, WORLDSIM.priceFloor, WORLDSIM.priceCeil)
|
||||
}
|
||||
|
||||
news(): WorldSimState['newsFeed'] {
|
||||
return this.s().newsFeed
|
||||
}
|
||||
|
||||
secretLis(): Record<string, number> {
|
||||
return this.s().secretQi
|
||||
}
|
||||
|
||||
npcDyn(): Record<string, NpcDynamics> {
|
||||
return this.s().npcDyn
|
||||
}
|
||||
}
|
||||
|
||||
function initSim(w: World): WorldSimState {
|
||||
const s = { ...empty() }
|
||||
for (const id of Object.keys(w.state.npcFamilies)) {
|
||||
s.npcDyn[id] = {
|
||||
leaderName: '新任宗主',
|
||||
leaderRealmIdx: MAJOR_ORDER.indexOf(npcById(id).leaderRealm),
|
||||
leaderAge: 40 + w.rng.int(0, 29),
|
||||
lastEvent: '',
|
||||
lastEventYear: -99,
|
||||
relationsWithOthers: {}
|
||||
}
|
||||
}
|
||||
for (const sid of Object.keys(w.state.missions ?? {})) void sid
|
||||
// 秘境灵气初始(从 mission 定义 id)
|
||||
s.secretQi = {}
|
||||
return s
|
||||
}
|
||||
|
||||
function empty(): WorldSimState {
|
||||
return {
|
||||
marketPool: { lingcao: 600, lingkuang: 300, beastcore: 80, 'pill-qiyuan': 90, 'pill-ningyuan': 40 },
|
||||
npcDyn: {},
|
||||
secretQi: {},
|
||||
tide: 0.5,
|
||||
tideDir: 1,
|
||||
tideTicks: 0,
|
||||
calamityYear: -99,
|
||||
newsFeed: [],
|
||||
lastNewsMonth: -99,
|
||||
npcSuccessions: 0
|
||||
}
|
||||
}
|
||||
|
||||
function driftMarket(s: WorldSimState, noise: number): void {
|
||||
for (const id of MARKET_IDS) {
|
||||
const base = poolBase(id)
|
||||
const cur = s.marketPool[id] ?? base
|
||||
// 向基准再平衡 + 噪声漂移(价格弹性)
|
||||
const rebalance = (base - cur) * WORLDSIM.marketRebalance
|
||||
const drift = (noise - 0.5) * WORLDSIM.marketDriftRate * base
|
||||
s.marketPool[id] = Math.max(base * 0.3, cur + rebalance + drift)
|
||||
}
|
||||
}
|
||||
|
||||
function applyCalamityToMarket(s: WorldSimState, cl: CalamityName): void {
|
||||
const eff = CALAMITY_EFFECT[cl]
|
||||
for (const [id, pct] of Object.entries(eff) as [string, number][]) {
|
||||
s.marketPool[id] = Math.max(10, (s.marketPool[id] ?? 50) * (1 + pct))
|
||||
}
|
||||
}
|
||||
|
||||
function poolBase(id: string): number {
|
||||
return MARKET_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
|
||||
const year = w.state.year
|
||||
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)) {
|
||||
dyn.leaderAge = 30 + w.rng.int(0, 25)
|
||||
dyn.leaderRealmIdx = Math.min(dyn.leaderRealmIdx + 1, 5)
|
||||
dyn.leaderName = `${def.name.replace('氏', '')}氏新主`
|
||||
dyn.lastEvent = '宗祧更替'
|
||||
dyn.lastEventYear = year
|
||||
s.npcSuccessions++
|
||||
pushNews(w, s, [id])
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initDynFor(id: string): NpcDynamics {
|
||||
const def = npcById(id)
|
||||
return {
|
||||
leaderName: `${def.name.replace('氏', '')}氏宗主`,
|
||||
leaderRealmIdx: MAJOR_ORDER.indexOf(def.leaderRealm),
|
||||
leaderAge: 45,
|
||||
lastEvent: '',
|
||||
lastEventYear: -99,
|
||||
relationsWithOthers: {}
|
||||
}
|
||||
}
|
||||
|
||||
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)}%`
|
||||
}
|
||||
s.newsFeed.push(row)
|
||||
s.lastNewsMonth = w.state.month
|
||||
}
|
||||
|
||||
function clamp(v: number, lo: number, hi: number): number {
|
||||
return Math.max(lo, Math.min(hi, v))
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { World } from '../runtime/World'
|
||||
|
||||
export function worldSimBrief(w: World): { tideLabel: string; priceLabel: string; latest: string } | null {
|
||||
const ws = w.state.worldSim
|
||||
if (!ws) return null
|
||||
const tide = ws.tide ?? 0.5
|
||||
const tideLabel = tide > 0.9 ? '灵涨' : tide < 0.7 ? '灵衰' : '汐平'
|
||||
const pool = ws.marketPool ?? {}
|
||||
const ratio = (pool['lingcao'] ?? 600) / 600
|
||||
const priceLabel = ratio > 1.15 ? '物腾' : ratio < 0.85 ? '物贱' : '平稳'
|
||||
const latest = ws.newsFeed?.[ws.newsFeed.length - 1]?.text ?? ''
|
||||
return { tideLabel, priceLabel, latest }
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
export interface NpcDynamics {
|
||||
/** 宗主姓名快照(换代时更新) */
|
||||
leaderName: string
|
||||
leaderRealmIdx: number
|
||||
leaderAge: number
|
||||
/** 演化旗标 */
|
||||
lastEvent: string
|
||||
lastEventYear: number
|
||||
relationsWithOthers: Record<string, number>
|
||||
}
|
||||
|
||||
export interface SecretQi {
|
||||
qi: number
|
||||
peak: number
|
||||
}
|
||||
|
||||
export interface WorldSimState {
|
||||
/** 世界库存池(资源循环) */
|
||||
marketPool: Record<string, number>
|
||||
/** NPC 动力学 */
|
||||
npcDyn: Record<string, NpcDynamics>
|
||||
/** 秘境灵气条 */
|
||||
secretQi: Record<string, number>
|
||||
/** 灵气潮汐(0-100 全局系数) */
|
||||
tide: number
|
||||
tideDir: 1 | -1
|
||||
tideTicks: number
|
||||
/** 灾年(当年灾因 id) */
|
||||
calamity?: string
|
||||
calamityYear: number
|
||||
/** 天下快讯(滚动 N=120) */
|
||||
newsFeed: { year: number; month: number; src: string; text: string }[]
|
||||
lastNewsMonth: number
|
||||
/** NPC 换代计数 */
|
||||
npcSuccessions: number
|
||||
}
|
||||
|
||||
export function makeWorldSimState(): WorldSimState {
|
||||
return {
|
||||
marketPool: { lingcao: 600, lingkuang: 300, beastcore: 80, 'pill-qiyuan': 90, 'pill-ningyuan': 40 },
|
||||
npcDyn: {},
|
||||
secretQi: {},
|
||||
tide: 0.5,
|
||||
tideDir: 1,
|
||||
tideTicks: 0,
|
||||
calamityYear: -99,
|
||||
newsFeed: [],
|
||||
lastNewsMonth: -99,
|
||||
npcSuccessions: 0
|
||||
}
|
||||
}
|
||||
|
||||
/** 世界演化参数表(全部可调) */
|
||||
export const WORLDSIM = {
|
||||
marketDriftRate: 0.03,
|
||||
marketRebalance: 0.1,
|
||||
priceFloor: 0.55,
|
||||
priceCeil: 2.3,
|
||||
tideCycle: 72, // 月周期(6年)
|
||||
tideMin: 0.65,
|
||||
tideMax: 1.35,
|
||||
secretRecover: 4,
|
||||
secretConsume: 0,
|
||||
calamityChance: 0.18,
|
||||
calamities: ['旱灾', '涝灾', '蝗灾', '疫病', '兽潮', '寒潮'] as const,
|
||||
npcEventChance: 0.05,
|
||||
newsEvery: 24, // 月
|
||||
newsKeep: 120
|
||||
}
|
||||
|
||||
export type CalamityName = (typeof WORLDSIM.calamities)[number]
|
||||
|
||||
/** 灾因 → 资源方向 */
|
||||
export const CALAMITY_EFFECT: Record<CalamityName, Partial<Record<string, number>>> = {
|
||||
旱灾: { lingcao: -0.3 },
|
||||
涝灾: { lingcao: -0.2, lingkuang: -0.1 },
|
||||
蝗灾: { lingcao: -0.45 },
|
||||
疫病: { beastcore: -0.25 },
|
||||
兽潮: { beastcore: 0.3 },
|
||||
寒潮: { lingcao: -0.2, beastcore: -0.15 }
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { GameState } from '../types/domain'
|
||||
import { normalizeGameState } from '../engine/world'
|
||||
import { normalizeGameState } from '../engine/runtime/World'
|
||||
|
||||
export const CURRENT_SCHEMA = 2
|
||||
export const APP_ID = 'cotyc'
|
||||
|
||||
@@ -182,6 +182,19 @@ export interface GameState {
|
||||
yearStats: { births: number; deaths: number }
|
||||
yearlyReports: YearlyReport[]
|
||||
stats: FamilyStats
|
||||
worldSim?: {
|
||||
marketPool?: Record<string, number>
|
||||
npcDyn?: Record<string, { leaderName: string; leaderRealmIdx: number; leaderAge: number; lastEvent: string; lastEventYear: number; relationsWithOthers?: Record<string, number> }>
|
||||
secretQi?: Record<string, number>
|
||||
tide?: number
|
||||
tideDir?: number
|
||||
tideTicks?: number
|
||||
calamity?: string
|
||||
calamityYear?: number
|
||||
newsFeed?: { year: number; month: number; src: string; text: string }[]
|
||||
lastNewsMonth?: number
|
||||
npcSuccessions?: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface LogItem {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState } from 'react'
|
||||
import { useGameStore } from '../store'
|
||||
import { applyEventChoice, rankPower } from '../../game/engine/systems/events'
|
||||
import { applyEventChoice, rankPower } from '../../game/engine/runtime/Systems/events'
|
||||
import { eventCategoryName } from '../../game/data/events'
|
||||
import { describeRealm } from '../../game/data/realms'
|
||||
import { Character } from '../../game/types/domain'
|
||||
import { combatPowerOf } from '../../game/engine/systems/combat'
|
||||
import { combatPowerOf } from '../../game/engine/runtime/Systems/combat'
|
||||
import { FORMATIONS, FormationId } from '../../game/data/formations'
|
||||
import { ModalShell } from './ModalShell'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useGameStore } from '../store'
|
||||
import { GUIDE_STEPS, computeGuide, guideAdvance } from '../../game/core/guide'
|
||||
import { GUIDE_STEPS, computeGuide, guideAdvance } from '../../game/engine/kernel/guide'
|
||||
|
||||
export function GuideStrip() {
|
||||
const world = useGameStore((s) => s.world)
|
||||
|
||||
@@ -5,12 +5,12 @@ import { describeRoots, ROOT_GRADE_NAMES } from '../../game/data/elements'
|
||||
import { TRAITS } from '../../game/data/traits'
|
||||
import { TECHNIQUES } from '../../game/data/techniques'
|
||||
import { ITEMS, ARTIFACT_POWER } from '../../game/data/items'
|
||||
import { combatPowerOf } from '../../game/engine/systems/combat'
|
||||
import { lifespanOf } from '../../game/engine/systems/lifecycle'
|
||||
import { perAttemptChance } from '../../game/engine/systems/cultivation'
|
||||
import { combatPowerOf } from '../../game/engine/runtime/Systems/combat'
|
||||
import { lifespanOf } from '../../game/engine/runtime/Systems/lifecycle'
|
||||
import { perAttemptChance } from '../../game/engine/runtime/Systems/cultivation'
|
||||
import { POSTS, POST_ORDER } from '../../game/data/posts'
|
||||
import { ModalShell } from './ModalShell'
|
||||
import { buildBiography } from '../../game/core/biography'
|
||||
import { buildBiography } from '../../game/engine/narrative/biography'
|
||||
|
||||
function aspName(c: Character): string {
|
||||
const map: Record<string, string> = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useGameStore } from '../store'
|
||||
import { LegacyArch } from '../../game/core/legacy'
|
||||
import { LegacyArch } from '../../game/engine/narrative/legacy'
|
||||
import { sBell } from '../sound'
|
||||
import { ModalShell } from './ModalShell'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useGameStore } from '../store'
|
||||
import { computeUrgency } from '../../game/core/urgency'
|
||||
import { computeUrgency } from '../../game/engine/kernel/urgency'
|
||||
|
||||
export function UrgentBadges() {
|
||||
const world = useGameStore((s) => s.world)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FxGate, FxKind } from '../game/core/fxqueue'
|
||||
import { FxGate, FxKind } from '../game/engine/kernel/fxqueue'
|
||||
import { seasonOf } from '../game/data/season'
|
||||
|
||||
let gate: FxGate | null = null
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useGameStore } from '../store'
|
||||
import { npcById } from '../../game/data/npcs'
|
||||
import { giftNpc, makePeace, marryNpcFamily, calcGiftGain, GIFT_TIERS } from '../../game/engine/systems/diplomacy'
|
||||
import { giftNpc, makePeace, marryNpcFamily, calcGiftGain, GIFT_TIERS } from '../../game/engine/runtime/Systems/diplomacy'
|
||||
import { useMemo } from 'react'
|
||||
import { MAJOR_NAMES } from '../../game/data/realms'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useGameStore } from '../store'
|
||||
import { MISSIONS, missionById } from '../../game/data/secrets'
|
||||
import { sendMission, recallAll } from '../../game/engine/systems/missions'
|
||||
import { combatPowerOf } from '../../game/engine/systems/combat'
|
||||
import { sendMission, recallAll } from '../../game/engine/runtime/Systems/missions'
|
||||
import { combatPowerOf } from '../../game/engine/runtime/Systems/combat'
|
||||
import { useState } from 'react'
|
||||
import { describeRealm, MAJOR_ORDER } from '../../game/data/realms'
|
||||
import { FORMATIONS, FormationId } from '../../game/data/formations'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useGameStore } from '../store'
|
||||
import { computeGenealogy } from '../../game/core/genealogy'
|
||||
import { computeGenealogy } from '../../game/engine/narrative/genealogy'
|
||||
import { describeRealm } from '../../game/data/realms'
|
||||
import { Character, FamilyState } from '../../game/types/domain'
|
||||
import { useState } from 'react'
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useState } from 'react'
|
||||
import { useGameStore } from '../store'
|
||||
import { computeLegacy, resolveLegacy } from '../../game/core/legacy'
|
||||
import { resolveLegacy as doesIt } from '../../game/core/legacy'
|
||||
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/core/biography'
|
||||
import { yearAxis, decadeLabel } from '../../game/core/yearaxis'
|
||||
import { buildReport, verdictLine } from '../../game/core/legacyreport'
|
||||
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
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useGameStore } from '../store'
|
||||
import { ITEMS } from '../../game/data/items'
|
||||
import { TECHNIQUES } from '../../game/data/techniques'
|
||||
import { techniqueGradeName } from '../../game/data/realms'
|
||||
import { marketPrice, buyItem, sellItem, buyTechnique } from '../../game/engine/market'
|
||||
import { marketPrice, buyItem, sellItem, buyTechnique } from '../../game/engine/sim/Market'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
export default function MarketPanel() {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useState } from 'react'
|
||||
import { useGameStore } from '../store'
|
||||
import { examplePlugin } from '../../game/engine/demo-plugins'
|
||||
import { examplePlugin } from '../../game/engine/runtime/demo-plugins'
|
||||
import { ensureFx } from '../fx'
|
||||
import { SYSTEM_DEFS } from '../../game/engine/capabilities'
|
||||
import { buildBiography } from '../../game/core/biography'
|
||||
import { SYSTEM_DEFS } from '../../game/engine/runtime/capabilities'
|
||||
import { buildBiography } from '../../game/engine/narrative/biography'
|
||||
|
||||
export default function SettingsPanel() {
|
||||
const world = useGameStore((s) => s.world)
|
||||
|
||||
@@ -12,10 +12,11 @@ import ChroniclePanel from '../panels/ChroniclePanel'
|
||||
import SettingsPanel from '../panels/SettingsPanel'
|
||||
import { LogFeed } from '../components/LogFeed'
|
||||
import { UrgentBadges } from '../components/UrgentBadges'
|
||||
import { fmt as fmtNum } from '../../game/core/format'
|
||||
import { fmt as fmtNum } from '../../game/engine/kernel/format'
|
||||
import { seasonOf } from '../../game/data/season'
|
||||
import { seasonTint, yearRing, tideOf } from '../../game/core/timesense'
|
||||
import { seasonTint, yearRing, tideOf } from '../../game/engine/kernel/timesense'
|
||||
import landscapeUrl from '../../assets/gen/landscape-gold.svg'
|
||||
import { worldSimBrief } from '../../game/engine/sim/worldsim-brief'
|
||||
import { GuideStrip } from '../components/GuideStrip'
|
||||
|
||||
const TABS: { id: PanelId; label: string }[] = [
|
||||
@@ -87,6 +88,7 @@ export default function GameScreen() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<WorldCondensed />
|
||||
<button className="btn btn-sm" title="手动存点" onClick={() => void saveNow()}>存</button>
|
||||
</div>
|
||||
<UrgentBadges />
|
||||
@@ -139,6 +141,21 @@ export default function GameScreen() {
|
||||
)
|
||||
}
|
||||
|
||||
function WorldCondensed() {
|
||||
const revision = useGameStore((s) => s.revision)
|
||||
void revision
|
||||
const st = useGameStore.getState()
|
||||
if (!st.world) return null
|
||||
const brief = worldSimBrief(st.world)
|
||||
if (!brief) return null
|
||||
return (
|
||||
<button className="world-badge" title={`潮汐${brief.tideLabel} · 物价${brief.priceLabel} · 快讯:${brief.latest ?? ''}`}>
|
||||
<span className="seal">{brief.tideLabel}</span>
|
||||
<span className="wb-price">{brief.priceLabel}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function dispRes(id: string): number {
|
||||
const st = useGameStore.getState()
|
||||
const w = st.world
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useGameStore } from '../store'
|
||||
import { SURNAME_POOL } from '../../game/core/names'
|
||||
import { RngHub } from '../../game/core/rng'
|
||||
import { SURNAME_POOL } from '../../game/engine/kernel/names'
|
||||
import { RngHub } from '../../game/engine/kernel/rng'
|
||||
|
||||
export default function NewGame() {
|
||||
const go = useGameStore((s) => s.go)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// 合成音效:鼓点·钟鸣·纸韵(Web Audio,零素材依赖)
|
||||
import { RngHub } from '../game/core/rng'
|
||||
import { RngHub } from '../game/engine/kernel/rng'
|
||||
let ctx: AudioContext | null = null
|
||||
let enabled = true
|
||||
|
||||
|
||||
+14
-10
@@ -1,13 +1,14 @@
|
||||
import { create } from 'zustand'
|
||||
import { World, WorldEventBus } from '../game/engine/world'
|
||||
import { World, WorldEventBus } from '../game/engine/runtime/World'
|
||||
import { GameEngine } from '../game/engine/GameEngine'
|
||||
import { EventDef } from '../game/data/events'
|
||||
import { findEvent, applyEventChoice } from '../game/engine/systems/events'
|
||||
import { findEvent, applyEventChoice } from '../game/engine/runtime/Systems/events'
|
||||
import { BattleLog, ChronicleEntry, GameState, LogItem, SaveMeta, YearlyReport, SnapshotMeta } from '../game/types/domain'
|
||||
import { getSlotManager, getSaveSlot } from '../game/storage/db'
|
||||
import { metaFromState, updateSlotMeta } from './storeHelper'
|
||||
import { GameFacade, ActName } from '../game/engine/api'
|
||||
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/core/clock'
|
||||
import type { PhaseStat } from '../game/engine/kernel/clock'
|
||||
import { seasonOf } from '../game/data/season'
|
||||
|
||||
export type Screen = 'boot' | 'newgame' | 'game'
|
||||
@@ -61,9 +62,10 @@ export interface GameStore {
|
||||
closePaper: () => void
|
||||
toggleSound: () => void
|
||||
facade?: GameFacade
|
||||
engine?: GameEngine
|
||||
advancing: boolean
|
||||
setNextToast: (msg: string) => void
|
||||
act: (name: ActName, payload: import('../game/engine/api').ActPayload) => boolean
|
||||
act: (name: ActName, payload: import('../game/engine/runtime/ApiFacade').ActPayload) => boolean
|
||||
}
|
||||
|
||||
const LOG_CAP = 260
|
||||
@@ -88,6 +90,7 @@ function stopTimer(): void {
|
||||
|
||||
export const useGameStore = create<GameStore>((set, get) => ({
|
||||
facade: undefined as GameFacade | undefined,
|
||||
engine: undefined as GameEngine | undefined,
|
||||
advancing: false,
|
||||
setNextToast: (msg: string) => setNextToast(msg),
|
||||
screen: 'boot',
|
||||
@@ -144,11 +147,12 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
go: (screen) => set({ screen }),
|
||||
|
||||
startNewGame: async (opts, slot) => {
|
||||
const world = World.create(opts)
|
||||
set({ world, 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 })
|
||||
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 })
|
||||
const st = get()
|
||||
st.world?.out.push(makeBus(st))
|
||||
const facade = new GameFacade(world, slot)
|
||||
const facade = st.facade ?? new GameFacade(world, slot)
|
||||
set({ facade })
|
||||
await st.saveNow('开局')
|
||||
},
|
||||
@@ -193,7 +197,7 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
const elapsed = performance.now() - t0
|
||||
try {
|
||||
const nextInv = { ...w.state.family.inventory, stones: w.state.family.stones }
|
||||
const FxGate = (await import('../game/core/fxqueue')).FxGate
|
||||
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 }))
|
||||
@@ -454,7 +458,7 @@ function makeBus(st: GameStore): WorldEventBus {
|
||||
}
|
||||
}
|
||||
|
||||
function actDirect(w: World, name: ActName, payload: import('../game/engine/api').ActPayload): boolean {
|
||||
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()
|
||||
|
||||
@@ -1799,3 +1799,25 @@ html[data-blade] .battle-lines {
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* ---------- 天下风云徽章 ---------- */
|
||||
.world-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: rgba(38, 29, 16, 0.65);
|
||||
border: 1px solid #3a3021;
|
||||
border-radius: 3px;
|
||||
padding: 3px 10px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
color: #cbbd9e;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.world-badge:hover {
|
||||
border-color: var(--gold);
|
||||
}
|
||||
.world-badge .wb-price {
|
||||
font-size: 0.8rem;
|
||||
color: #a8c2da;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { GameFacade, ACT_CATALOG, ActName } from '../src/renderer/game/engine/api'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { GameFacade, ACT_CATALOG, ActName } from '../src/renderer/game/engine/runtime/ApiFacade'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
function baseWorld(seed: string): World {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { monthlyRate, resolveBreakthrough } from '../src/renderer/game/engine/systems/cultivation'
|
||||
import { combatPowerOf } from '../src/renderer/game/engine/systems/combat'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { monthlyRate, resolveBreakthrough } from '../src/renderer/game/engine/runtime/Systems/cultivation'
|
||||
import { combatPowerOf } from '../src/renderer/game/engine/runtime/Systems/combat'
|
||||
import { isFit } from '../src/renderer/game/data/aspirations'
|
||||
import { needsTribulation, resolveTribulation, tribulationEventId } from '../src/renderer/game/engine/systems/tribulation'
|
||||
import { buildBiography } from '../src/renderer/game/core/biography'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/systems/events'
|
||||
import { needsTribulation, resolveTribulation, tribulationEventId } from '../src/renderer/game/engine/runtime/Systems/tribulation'
|
||||
import { buildBiography } from '../src/renderer/game/engine/narrative/biography'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/runtime/Systems/events'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
function baseWorld(seed: string): World {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/systems/events'
|
||||
import { computeUrgency } from '../src/renderer/game/core/urgency'
|
||||
import { monthlyRate } from '../src/renderer/game/engine/systems/cultivation'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/runtime/Systems/events'
|
||||
import { computeUrgency } from '../src/renderer/game/engine/kernel/urgency'
|
||||
import { monthlyRate } from '../src/renderer/game/engine/runtime/Systems/cultivation'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
function baseWorld(seed: string): World {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { newCharacter } from '../src/renderer/game/engine/pcgen'
|
||||
import { Rng, seedToRng } from '../src/renderer/game/core/rng'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { newCharacter } from '../src/renderer/game/engine/runtime/pcgen'
|
||||
import { Rng, seedToRng } from '../src/renderer/game/engine/kernel/rng'
|
||||
import { PACK } from '../src/renderer/game/data/registry'
|
||||
import { buildApprentice } from '../src/renderer/game/engine/systems/tournament'
|
||||
import { sendMission } from '../src/renderer/game/engine/systems/missions'
|
||||
import { buyItem, marketPrice } from '../src/renderer/game/engine/market'
|
||||
import { findEvent } from '../src/renderer/game/engine/systems/events'
|
||||
import { runTournament } from '../src/renderer/game/engine/systems/tournament'
|
||||
import { buildApprentice } from '../src/renderer/game/engine/runtime/Systems/tournament'
|
||||
import { sendMission } from '../src/renderer/game/engine/runtime/Systems/missions'
|
||||
import { buyItem, marketPrice } from '../src/renderer/game/engine/sim/Market'
|
||||
import { findEvent } from '../src/renderer/game/engine/runtime/Systems/events'
|
||||
import { runTournament } from '../src/renderer/game/engine/runtime/Systems/tournament'
|
||||
import { stateFingerprint, longRun } from './fingerprint.helper'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('审计回归:P0 修复固化', () => {
|
||||
})
|
||||
|
||||
it('防御性修补后金钟罩不变(行为等价确认)', () => {
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('ffdd3fb1')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('195b8aaa')
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('5dac4915')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('b7c3ff06')
|
||||
})
|
||||
})
|
||||
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World, normalizeGameState } from '../src/renderer/game/engine/world'
|
||||
import { World, normalizeGameState } from '../src/renderer/game/engine/runtime/World'
|
||||
import {
|
||||
marryNpcFamily,
|
||||
makePeace
|
||||
} from '../src/renderer/game/engine/systems/diplomacy'
|
||||
import { sendMission, recallAll } from '../src/renderer/game/engine/systems/missions'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/systems/events'
|
||||
} from '../src/renderer/game/engine/runtime/Systems/diplomacy'
|
||||
import { sendMission, recallAll } from '../src/renderer/game/engine/runtime/Systems/missions'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/runtime/Systems/events'
|
||||
import { GameState } from '../src/renderer/game/types/domain'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { monthlyRate } from '../src/renderer/game/engine/systems/cultivation'
|
||||
import { combatPowerOf } from '../src/renderer/game/engine/systems/combat'
|
||||
import { resolveBreakthrough } from '../src/renderer/game/engine/systems/cultivation'
|
||||
import { computeLegacy, resolveLegacy } from '../src/renderer/game/core/legacy'
|
||||
import { yearAxis } from '../src/renderer/game/core/yearaxis'
|
||||
import { buildBiography } from '../src/renderer/game/core/biography'
|
||||
import { computeGenealogy } from '../src/renderer/game/core/genealogy'
|
||||
import { marketPrice, buyItem, sellItem } from '../src/renderer/game/engine/market'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { monthlyRate } from '../src/renderer/game/engine/runtime/Systems/cultivation'
|
||||
import { combatPowerOf } from '../src/renderer/game/engine/runtime/Systems/combat'
|
||||
import { resolveBreakthrough } from '../src/renderer/game/engine/runtime/Systems/cultivation'
|
||||
import { computeLegacy, resolveLegacy } from '../src/renderer/game/engine/narrative/legacy'
|
||||
import { yearAxis } from '../src/renderer/game/engine/narrative/yearaxis'
|
||||
import { buildBiography } from '../src/renderer/game/engine/narrative/biography'
|
||||
import { computeGenealogy } from '../src/renderer/game/engine/narrative/genealogy'
|
||||
import { marketPrice, buyItem, sellItem } from '../src/renderer/game/engine/sim/Market'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
function baseWorld(seed: string): World {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { GameClock, PHASE_ORDER } from '../src/renderer/game/core/clock'
|
||||
import { buildClock } from '../src/renderer/game/engine/clocks'
|
||||
import { RngHub } from '../src/renderer/game/core/rng'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { GameClock, PHASE_ORDER } from '../src/renderer/game/engine/kernel/clock'
|
||||
import { buildClock } from '../src/renderer/game/engine/runtime/clocks'
|
||||
import { RngHub } from '../src/renderer/game/engine/kernel/rng'
|
||||
import { seasonOf, seasonMod, SEASON } from '../src/renderer/game/data/season'
|
||||
import { yearAxis, decadeLabel } from '../src/renderer/game/core/yearaxis'
|
||||
import { yearAxis, decadeLabel } from '../src/renderer/game/engine/narrative/yearaxis'
|
||||
import { stateFingerprint } from './fingerprint.helper'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
@@ -16,12 +16,12 @@ function baseWorld(seed: string): World {
|
||||
}
|
||||
|
||||
describe('GameClock 统一时轮', () => {
|
||||
it('七个 phase 全被注册且顺序固定', () => {
|
||||
it('八个 phase 全被注册且顺序固定(含 worldsim)', () => {
|
||||
const clock = buildClock()
|
||||
const clock2 = new GameClock()
|
||||
clock2.register('production', () => undefined)
|
||||
expect(clock.subscriptionCount()).toBeGreaterThanOrEqual(10)
|
||||
expect(PHASE_ORDER.length).toBe(7)
|
||||
expect(PHASE_ORDER.length).toBe(8)
|
||||
expect(clock2.subscriptionCount()).toBe(1)
|
||||
})
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('GameClock 统一时轮', () => {
|
||||
it('stepMonthly 返回各 phase 耗时统计(性能预算通道)', () => {
|
||||
const w = baseWorld('clk-a')
|
||||
const report = w.clock.stepMonthly(w)
|
||||
expect(report.length).toBe(7)
|
||||
expect(report.length).toBe(8)
|
||||
for (const r of report) {
|
||||
expect(PHASE_ORDER).toContain(r.phase)
|
||||
expect(typeof r.ms).toBe('number')
|
||||
|
||||
+5
-6
@@ -1,18 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { stateFingerprint, longRun } from './fingerprint.helper'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
|
||||
/**
|
||||
* 金钟罩:固定种子长跑 560 月的状态指纹。
|
||||
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
|
||||
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
|
||||
*/
|
||||
// 0.1.10 基线:长线数值重建(修炼提速/寿元放宽/渡劫修复)后三档固化为 47/100/180 年。
|
||||
// 0.1.8 时修复批次后为 9af71ecb/63cede89/ebfec4a4;0.1.10 有意变更数值后按三档重算。
|
||||
// 0.1.14 正式基线:worldsim 世界自进化(确定性重写后)三档固化。
|
||||
const GOLDEN: Record<string, Record<number, string>> = {
|
||||
'bell-seed-1': { 560: 'ffdd3fb1', 1200: '76787bd9', 2160: 'e3f0a1cd' },
|
||||
'bell-seed-2': { 560: '1dd432bb', 1200: '8ab8db6d', 2160: '2340e385' },
|
||||
'bell-seed-3': { 560: '195b8aaa', 1200: 'a19e1a90', 2160: '2d767a64' }
|
||||
'bell-seed-1': { 560: '5dac4915', 1200: 'e13fc9be', 2160: '7a0317c2' },
|
||||
'bell-seed-2': { 560: '53005425', 1200: 'ad956d85', 2160: '3b31fc8b' },
|
||||
'bell-seed-3': { 560: 'b7c3ff06', 1200: '4eb51c1f', 2160: '64433376' }
|
||||
}
|
||||
|
||||
const TIERS = [
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { combatPowerOf, resolveEncounter, resolveRaid, rollWarbooty } from '../src/renderer/game/engine/systems/combat'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { combatPowerOf, resolveEncounter, resolveRaid, rollWarbooty } from '../src/renderer/game/engine/runtime/Systems/combat'
|
||||
import { ENEMIES } from '../src/renderer/game/data/secrets'
|
||||
import { sendMission, recallAll } from '../src/renderer/game/engine/systems/missions'
|
||||
import { sendMission, recallAll } from '../src/renderer/game/engine/runtime/Systems/missions'
|
||||
import { missionById } from '../src/renderer/game/data/secrets'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { applyEventChoice, applyEffect } from '../src/renderer/game/engine/systems/events'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { applyEventChoice, applyEffect } from '../src/renderer/game/engine/runtime/Systems/events'
|
||||
import { EffectDef } from '../src/renderer/game/data/events'
|
||||
import { Character } from '../src/renderer/game/types/domain'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ import { NPCS, npcById } from '../src/renderer/game/data/npcs'
|
||||
import { POSTS, POST_ORDER } from '../src/renderer/game/data/posts'
|
||||
import { EVENTS } from '../src/renderer/game/data/events'
|
||||
import { MAJOR_RATE, masteryRateOfMajor } from '../src/renderer/game/data/pacing'
|
||||
import { SURNAME_POOL, MALE_GIVEN, FEMALE_GIVEN } from '../src/renderer/game/core/names'
|
||||
import { SURNAME_POOL, MALE_GIVEN, FEMALE_GIVEN } from '../src/renderer/game/engine/kernel/names'
|
||||
import { TRAITS } from '../src/renderer/game/data/traits'
|
||||
|
||||
describe('realms 境界表', () => {
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { applyEventChoice, eventRoll } from '../src/renderer/game/engine/systems/events'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { applyEventChoice, eventRoll } from '../src/renderer/game/engine/runtime/Systems/events'
|
||||
|
||||
describe('echo 四邻回声', () => {
|
||||
function baseWorld(seed: string): World {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { marketPrice, buyItem, sellItem, buyTechnique, techniquePrice } from '../src/renderer/game/engine/market'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { marketPrice, buyItem, sellItem, buyTechnique, techniquePrice } from '../src/renderer/game/engine/sim/Market'
|
||||
import { ITEMS } from '../src/renderer/game/data/items'
|
||||
|
||||
describe('economy 经济系统', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World, normalizeGameState } from '../src/renderer/game/engine/world'
|
||||
import { World, normalizeGameState } from '../src/renderer/game/engine/runtime/World'
|
||||
import { EVENTS, EffectDef } from '../src/renderer/game/data/events'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/systems/events'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/runtime/Systems/events'
|
||||
import { POSTS } from '../src/renderer/game/data/posts'
|
||||
import { ASPIRATIONS } from '../src/renderer/game/data/aspirations'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { matchesCond } from '../src/renderer/game/engine/systems/events'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { matchesCond } from '../src/renderer/game/engine/runtime/Systems/events'
|
||||
import { Cond } from '../src/renderer/game/data/events'
|
||||
|
||||
export function matchesCondPub(w: World, cond?: Cond): boolean {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { matchesCond, applyEventChoice, findEvent, eventRoll } from '../src/renderer/game/engine/systems/events'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { matchesCond, applyEventChoice, findEvent, eventRoll } from '../src/renderer/game/engine/runtime/Systems/events'
|
||||
import { EVENTS } from '../src/renderer/game/data/events'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { GameFacade } from '../src/renderer/game/engine/api'
|
||||
import { SYSTEM_DEFS } from '../src/renderer/game/engine/capabilities'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { GameFacade } from '../src/renderer/game/engine/runtime/ApiFacade'
|
||||
import { SYSTEM_DEFS } from '../src/renderer/game/engine/runtime/capabilities'
|
||||
import { PACK, pack, DEFAULT_PACK } from '../src/renderer/game/data/registry'
|
||||
import { marketPrice } from '../src/renderer/game/engine/market'
|
||||
import { marketPrice } from '../src/renderer/game/engine/sim/Market'
|
||||
import { stateFingerprint, longRun } from './fingerprint.helper'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
@@ -170,7 +170,7 @@ describe('GameFacade 门面', () => {
|
||||
|
||||
it('默认配置金钟罩不受门面化影响', () => {
|
||||
PACK.reset()
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('ffdd3fb1')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('195b8aaa')
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('5dac4915')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('b7c3ff06')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { buildReport, verdictLine } from '../src/renderer/game/core/legacyreport'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { buildReport, verdictLine } from '../src/renderer/game/engine/narrative/legacyreport'
|
||||
import { migrateIfNeeded, exportEnvelope, parseImportEnvelope, CURRENT_SCHEMA, APP_ID } from '../src/renderer/game/storage/migrate'
|
||||
import { FORMATIONS, formationById, FormationId } from '../src/renderer/game/data/formations'
|
||||
import { resolveEncounter } from '../src/renderer/game/engine/systems/combat'
|
||||
import { resolveEncounter } from '../src/renderer/game/engine/runtime/Systems/combat'
|
||||
import { ENEMIES } from '../src/renderer/game/data/secrets'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/systems/events'
|
||||
import { sendMission } from '../src/renderer/game/engine/systems/missions'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/runtime/Systems/events'
|
||||
import { sendMission } from '../src/renderer/game/engine/runtime/Systems/missions'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
function baseWorld(seed: string): World {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { GameState } from '../src/renderer/game/types/domain'
|
||||
|
||||
export function stateFingerprint(s: GameState): string {
|
||||
@@ -23,6 +23,12 @@ export function stateFingerprint(s: GameState): string {
|
||||
parts.push(Object.keys(s.completedEvents).length + ':' + s.completedEvents.slice(-3).join(','))
|
||||
parts.push(JSON.stringify(sortedFlags(s.family.flag)))
|
||||
parts.push(s.yearlyReports.length)
|
||||
const ws = s.worldSim
|
||||
if (ws) {
|
||||
parts.push(`ws:${JSON.stringify(ws.marketPool ?? {})}:${JSON.stringify(ws.secretQi ?? {})}:${ws.tide ?? 0}:${ws.npcSuccessions ?? 0}:${(ws.newsFeed ?? []).length}`)
|
||||
} else {
|
||||
parts.push('ws:none')
|
||||
}
|
||||
return hash32(parts.join('\u0001'))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { seasonOf } from '../src/renderer/game/data/season'
|
||||
import { tideOf, yearRing, seasonTint, seasonCssVars, TIDES } from '../src/renderer/game/core/timesense'
|
||||
import { FxGate, FxKind } from '../src/renderer/game/core/fxqueue'
|
||||
import { tideOf, yearRing, seasonTint, seasonCssVars, TIDES } from '../src/renderer/game/engine/kernel/timesense'
|
||||
import { FxGate, FxKind } from '../src/renderer/game/engine/kernel/fxqueue'
|
||||
|
||||
describe('timesense 时间感知矩阵', () => {
|
||||
it.each([[1, 'spring'], [3, 'spring'], [4, 'summer'], [6, 'summer'], [7, 'autumn'], [9, 'autumn'], [10, 'winter'], [12, 'winter']] as const)(
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { GameEngine } from '../src/renderer/game/engine/GameEngine'
|
||||
import { makeKernel } from '../src/renderer/game/engine/kernel/Kernel'
|
||||
import { WorldSim } from '../src/renderer/game/engine/sim/WorldSim'
|
||||
import { WORLDSIM } from '../src/renderer/game/engine/sim/worldsim-data'
|
||||
|
||||
describe('GameEngine 引擎门面', () => {
|
||||
it('构造:内核注入世界 + 门面 + 数据包', () => {
|
||||
const e = new GameEngine({ seed: 'engine-1' })
|
||||
expect(e.world).toBeTruthy()
|
||||
expect(e.facade).toBeTruthy()
|
||||
expect(e.kernel.clock.subscriptionCount()).toBeGreaterThanOrEqual(10)
|
||||
expect(e.status()).toBe('running')
|
||||
})
|
||||
|
||||
it('advance 驱动内核时钟(确定单源)', () => {
|
||||
const e = new GameEngine({ seed: 'engine-2' })
|
||||
const y0 = e.view().year
|
||||
const m0 = e.view().month
|
||||
e.advance()
|
||||
const crossed = e.view().month === 1 && e.view().year === y0 + 1
|
||||
const normal = e.view().month === m0 + 1
|
||||
expect(crossed || normal).toBe(true)
|
||||
})
|
||||
|
||||
it('act/query/subscribe 全链路', () => {
|
||||
const e = new GameEngine({ seed: 'engine-3' })
|
||||
const events: string[] = []
|
||||
const unsub = e.subscribe((ev) => events.push(ev.type))
|
||||
expect(e.act('member.post', { memberId: 'x3', post: 'elder' })).toBe(true)
|
||||
e.advance()
|
||||
// 一次 advance 必然产生内核事件流(log/pending/paper 至少其一)
|
||||
expect(events.length).toBeGreaterThan(0)
|
||||
unsub()
|
||||
const fam = e.query('family') as { year: number }
|
||||
expect(fam.year).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('datapack 覆写生效 + restore 可回放', () => {
|
||||
const e = new GameEngine({ seed: 'engine-4' })
|
||||
e.datapack.override({
|
||||
items: { ...e.datapack.current().items, lingcao: { ...e.datapack.current().items['lingcao'], basePrice: 250 } }
|
||||
})
|
||||
const snap = e.snapshot()
|
||||
expect(snap.family.stones).toBeGreaterThanOrEqual(0)
|
||||
e.restore(snap)
|
||||
expect(e.status()).toBe('running')
|
||||
})
|
||||
|
||||
it('about 元数据含 plugins(≥3)与版本 0.1.14', () => {
|
||||
const e = new GameEngine({ seed: 'engine-5' })
|
||||
const a = e.about()
|
||||
expect(a.plugins).toBeGreaterThanOrEqual(3)
|
||||
expect(a.version).toContain('0.1.14')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Kernel 三合一', () => {
|
||||
it('时钟/随机/总线协同(同 seed 同随机序列)', () => {
|
||||
const k1 = makeKernel('kernel-a')
|
||||
const k2 = makeKernel('kernel-a')
|
||||
expect(k1.rng.next()).toBe(k2.rng.next())
|
||||
expect(k1.clock).toBeTruthy()
|
||||
expect(typeof k1.bus.onLog).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('WorldSim 世界健康(长跑)', () => {
|
||||
it.each(['ws-1', 'ws-2', 'ws-3'].map((s) => [s] as const))('%s:1000 月市场池/灵气在界、世界不崩', (seed) => {
|
||||
const e = new GameEngine({ seed })
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
if (e.view().gameOver) break
|
||||
e.advance()
|
||||
}
|
||||
const ws = e.view().worldSim as (Record<string, unknown> & {
|
||||
marketPool: Record<string, number>
|
||||
secretQi: Record<string, number>
|
||||
tide: number
|
||||
}) | undefined
|
||||
expect(ws).toBeTruthy()
|
||||
if (ws) {
|
||||
for (const v of Object.values(ws.marketPool)) expect(v).toBeGreaterThan(0)
|
||||
for (const v of Object.values(ws.secretQi ?? {})) expect(v).toBeGreaterThanOrEqual(0)
|
||||
expect(ws.tide).toBeGreaterThan(0.5)
|
||||
expect(ws.tide).toBeLessThan(1.4)
|
||||
}
|
||||
})
|
||||
|
||||
it('NPC 换代至少发生一次(世界在演化)', () => {
|
||||
const e = new GameEngine({ seed: 'ws-evolve' })
|
||||
for (let i = 0; i < 720; i++) {
|
||||
if (e.view().gameOver) break
|
||||
e.advance()
|
||||
}
|
||||
const ws = e.view().worldSim as { npcSuccessions?: number } | undefined
|
||||
expect(ws?.npcSuccessions ?? 0).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('快讯持有且裁剪符合 N(≤120)', () => {
|
||||
const e = new GameEngine({ seed: 'ws-news' })
|
||||
for (let i = 0; i < 400; i++) {
|
||||
if (e.view().gameOver) break
|
||||
e.advance()
|
||||
}
|
||||
const ws = e.view().worldSim as { newsFeed?: unknown[] } | undefined
|
||||
expect(ws?.newsFeed.length ?? 0).toBeLessThanOrEqual(WORLDSIM.newsKeep)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Sim 与外层联动', () => {
|
||||
it('tideMult 在界(0.5-1.4)且随年景变化', () => {
|
||||
const e = new GameEngine({ seed: 'ws-tide-link' })
|
||||
const sim = new WorldSim(e.world)
|
||||
const before = sim.tideMult()
|
||||
expect(before).toBeGreaterThanOrEqual(0.5)
|
||||
expect(before).toBeLessThan(1.4)
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CotycPlugin } from '../../src/renderer/game/core/plugin'
|
||||
import { CotycPlugin } from '../../src/renderer/game/engine/kernel/plugin'
|
||||
|
||||
/** 示例内容插件:注入事件池 + 一个护山能力(开发范本) */
|
||||
export const examplePlugin: CotycPlugin = {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount } from '../src/renderer/game/core/legacy'
|
||||
import { runTournament, buildApprentice, finalizeApprentice, apprenticeCandidates } from '../src/renderer/game/engine/systems/tournament'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/systems/events'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount } from '../src/renderer/game/engine/narrative/legacy'
|
||||
import { runTournament, buildApprentice, finalizeApprentice, apprenticeCandidates } from '../src/renderer/game/engine/runtime/Systems/tournament'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/runtime/Systems/events'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
function baseWorld(seed: string): World {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/systems/events'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/runtime/Systems/events'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
const SEEDS = ['long-a', 'long-b', 'long-c']
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import {
|
||||
giftNpc,
|
||||
marryNpcFamily,
|
||||
makePeace,
|
||||
arrangeWedding,
|
||||
diplomacyTick
|
||||
} from '../src/renderer/game/engine/systems/diplomacy'
|
||||
import { yearStartMarriage } from '../src/renderer/game/engine/systems/marriage'
|
||||
} from '../src/renderer/game/engine/runtime/Systems/diplomacy'
|
||||
import { yearStartMarriage } from '../src/renderer/game/engine/runtime/Systems/marriage'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
function baseWorld(seed: string): World {
|
||||
@@ -189,7 +189,7 @@ describe('genealogy 谱系结构', () => {
|
||||
})
|
||||
})
|
||||
|
||||
import { computeGenealogy } from '../src/renderer/game/core/genealogy'
|
||||
import { computeGenealogy } from '../src/renderer/game/engine/narrative/genealogy'
|
||||
function computeGenealogyLocal(w: World) {
|
||||
return computeGenealogy(w)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { calcGiftGain, GIFT_TIERS, giftNpc } from '../src/renderer/game/engine/systems/diplomacy'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { calcGiftGain, GIFT_TIERS, giftNpc } from '../src/renderer/game/engine/runtime/Systems/diplomacy'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
function baseWorld(seed: string): World {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { Rng, seedToRng } from '../src/renderer/game/core/rng'
|
||||
import { newCharacter, rollAttributes, rollRoots, rollPersonality, traitBonuses, calcLifespan } from '../src/renderer/game/engine/pcgen'
|
||||
import { monthlyRate, perAttemptChance } from '../src/renderer/game/engine/systems/cultivation'
|
||||
import { lifespanOf } from '../src/renderer/game/engine/systems/lifecycle'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { Rng, seedToRng } from '../src/renderer/game/engine/kernel/rng'
|
||||
import { newCharacter, rollAttributes, rollRoots, rollPersonality, traitBonuses, calcLifespan } from '../src/renderer/game/engine/runtime/pcgen'
|
||||
import { monthlyRate, perAttemptChance } from '../src/renderer/game/engine/runtime/Systems/cultivation'
|
||||
import { lifespanOf } from '../src/renderer/game/engine/runtime/Systems/lifecycle'
|
||||
|
||||
function rng(): Rng {
|
||||
return new Rng(seedToRng('pcgen'))
|
||||
|
||||
+11
-16
@@ -1,11 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { GameFacade } from '../src/renderer/game/engine/api'
|
||||
import { installCoreSystems, buildClock } from '../src/renderer/game/engine/clocks'
|
||||
import { emptyClock } from '../src/renderer/game/engine/clocks'
|
||||
import { CORE_PLUGINS } from '../src/renderer/game/engine/plugin-bootstrap'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { GameFacade } from '../src/renderer/game/engine/runtime/ApiFacade'
|
||||
import { installCoreSystems, buildClock } from '../src/renderer/game/engine/runtime/clocks'
|
||||
import { emptyClock } from '../src/renderer/game/engine/runtime/clocks'
|
||||
import { CORE_PLUGINS } from '../src/renderer/game/engine/runtime/plugin-bootstrap'
|
||||
import { examplePlugin, brokenPlugin } from './helpers/examplePlugin'
|
||||
import { stateFingerprint, longRun } from './fingerprint.helper'
|
||||
import { findEvent } from '../src/renderer/game/engine/runtime/Systems/events'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
function baseWorld(seed: string): World {
|
||||
@@ -54,14 +55,8 @@ describe('PluginCore 插件协议', () => {
|
||||
// 推进到跨年触发年首庇佑
|
||||
for (let i = 0; i < 13; i++) w.advanceMonth()
|
||||
expect(x5.realmProgress).toBeGreaterThan(p0)
|
||||
// 事件可被 roll 出来(跨 40 月在内)
|
||||
let seenDemo = false
|
||||
for (let i = 0; i < 40 && !seenDemo; i++) {
|
||||
w.advanceMonth()
|
||||
if (w.state.pendingEvent === 'ev-demo-guardian') seenDemo = true
|
||||
w.state.pendingEvent = undefined
|
||||
}
|
||||
expect(seenDemo).toBe(true)
|
||||
// 事件注入可被解析(findEvent 命中 demo 池 = 注入生效;确定性校验不赌概率)
|
||||
expect(findEvent('ev-demo-guardian', w)).toBeTruthy()
|
||||
// 卸载
|
||||
expect(w.removePlugin('demo-peaks').ok).toBe(true)
|
||||
expect(w.eventPoolIds()).not.toContain('demo-peaks')
|
||||
@@ -85,9 +80,9 @@ describe('PluginCore 插件协议', () => {
|
||||
})
|
||||
|
||||
it('默认管线金钟罩不受插件层影响', () => {
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('ffdd3fb1')
|
||||
expect(stateFingerprint(longRun('bell-seed-2').state)).toBe('1dd432bb')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('195b8aaa')
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('5dac4915')
|
||||
expect(stateFingerprint(longRun('bell-seed-2').state)).toBe('53005425')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('b7c3ff06')
|
||||
})
|
||||
|
||||
it('facade 插件查询与 about.plugins', () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { monthlyRate } from '../src/renderer/game/engine/systems/cultivation'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { monthlyRate } from '../src/renderer/game/engine/runtime/Systems/cultivation'
|
||||
import { MAJORS } from '../src/renderer/game/data/realms'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/systems/events'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/runtime/Systems/events'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
function baseWorld(seed: string): World {
|
||||
@@ -73,8 +73,8 @@ describe('0.1.10 数值重建验收', () => {
|
||||
})
|
||||
})
|
||||
|
||||
import { resolveBreakthrough } from '../src/renderer/game/engine/systems/cultivation'
|
||||
import { perTribChance } from '../src/renderer/game/engine/systems/tribulation'
|
||||
import { resolveBreakthrough } from '../src/renderer/game/engine/runtime/Systems/cultivation'
|
||||
import { perTribChance } from '../src/renderer/game/engine/runtime/Systems/tribulation'
|
||||
|
||||
describe('事件悬置与周期(0.1.10)', () => {
|
||||
it('大比征召 FIFO 不被拍卖/传薪挤掉(20 年三年都触发)', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Rng, seedToRng, RngHub } from '../src/renderer/game/core/rng'
|
||||
import { Rng, seedToRng, RngHub } from '../src/renderer/game/engine/kernel/rng'
|
||||
|
||||
describe('RNG 多种子矩阵', () => {
|
||||
it.each([
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Rng, seedToRng } from '../src/renderer/game/core/rng'
|
||||
import { Rng, seedToRng } from '../src/renderer/game/engine/kernel/rng'
|
||||
|
||||
describe('Rng.sfc32 特性', () => {
|
||||
it('同一种子产生的序列完全一致', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { SaveSlot, SaveDbDriver } from '../src/renderer/game/storage/slots'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
|
||||
interface Row {
|
||||
[k: string]: unknown
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { SaveSlot, SaveDbDriver } from '../src/renderer/game/storage/slots'
|
||||
import { GameState } from '../src/renderer/game/types/domain'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
|
||||
interface Row {
|
||||
[k: string]: unknown
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { combatPowerOf } from '../src/renderer/game/engine/systems/combat'
|
||||
import { marketPrice, buyItem, sellItem, buyTechnique } from '../src/renderer/game/engine/market'
|
||||
import { resolveBreakthrough, monthlyRate } from '../src/renderer/game/engine/systems/cultivation'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/systems/events'
|
||||
import { computeGenealogy } from '../src/renderer/game/core/genealogy'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { combatPowerOf } from '../src/renderer/game/engine/runtime/Systems/combat'
|
||||
import { marketPrice, buyItem, sellItem, buyTechnique } from '../src/renderer/game/engine/sim/Market'
|
||||
import { resolveBreakthrough, monthlyRate } from '../src/renderer/game/engine/runtime/Systems/cultivation'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/runtime/Systems/events'
|
||||
import { computeGenealogy } from '../src/renderer/game/engine/narrative/genealogy'
|
||||
import { matchesCondPub } from './events.helpers'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { computeUrgency } from '../src/renderer/game/core/urgency'
|
||||
import { computeGuide, GUIDE_STEPS, guideAdvance } from '../src/renderer/game/core/guide'
|
||||
import { fmt } from '../src/renderer/game/core/format'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { computeUrgency } from '../src/renderer/game/engine/kernel/urgency'
|
||||
import { computeGuide, GUIDE_STEPS, guideAdvance } from '../src/renderer/game/engine/kernel/guide'
|
||||
import { fmt } from '../src/renderer/game/engine/kernel/format'
|
||||
|
||||
function baseWorld(seed: string): World {
|
||||
return World.create({ seed, surname: '舒', familyName: '舒家', motto: 'm', difficulty: 'normal' })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { createWorldState, NewGameOptions, findInheritor } from '../src/renderer/game/engine/creation'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { createWorldState, NewGameOptions, findInheritor } from '../src/renderer/game/engine/runtime/creation'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
function opts(seed: string, difficulty: 'easy' | 'normal' | 'hard' = 'normal', surname = '彭'): NewGameOptions {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { stateFingerprint, longRun } from './fingerprint.helper'
|
||||
import { computeGenealogy } from '../src/renderer/game/core/genealogy'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/systems/events'
|
||||
import { computeGenealogy } from '../src/renderer/game/engine/narrative/genealogy'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/runtime/Systems/events'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
describe('多 seed 长跑矩阵', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { World, WorldEventBus } from '../src/renderer/game/engine/world'
|
||||
import { World, WorldEventBus } from '../src/renderer/game/engine/runtime/World'
|
||||
import { LogItem, ChronicleEntry, BattleLog } from '../src/renderer/game/types/domain'
|
||||
|
||||
const sinks: Set<WorldEventBus> = new Set()
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
describe('World engine', () => {
|
||||
|
||||
Reference in New Issue
Block a user