v0.1.7: 仙门之钥(门面化/能力注册表/数据包/开发者面板)
- GameFacade 统一门面:act(23 种动作白名单)/query(6 类只读快照)/subscribe(七大事件协议)/ about(版本+模块清单+数据包指纹);UI store 接入门面(act 直达,散装调用收口) - CapabilityRegistry:12 张能力卡(id/name/version/desc/hooks),world.systems 启停即拔插, 时钟按能力开关过滤(禁修炼则无人精进…沙盒玩法);sysChanged 广播 - DataPackRegistry:默认包与静态数据同引用(金钟罩零漂移),override/reset/fingerprint, 高频读口 6 处迁移 pack()——未来 MOD=注入数据包覆写 - 开发者面板:设置页系统仪表盘(清单+启停开关+警示文案) - 测试 225→239(能力清单/拔插分殊四路/广播幂等/包覆盖物价/act 目录/query/协议退订/about/ 金钟罩复验零漂移)
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "chronicle-of-the-immortal-clan",
|
"name": "chronicle-of-the-immortal-clan",
|
||||||
"productName": "仙途家族志",
|
"productName": "仙途家族志",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"description": "修仙 · 家族 · 经营 · 战斗 模拟器",
|
"description": "修仙 · 家族 · 经营 · 战斗 模拟器",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "MetonaTeam",
|
"author": "MetonaTeam",
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { ITEMS, ARTIFACT_POWER } from './items'
|
||||||
|
import { TECHNIQUES } from './techniques'
|
||||||
|
import { BUILDINGS } from './buildings'
|
||||||
|
import { MISSIONS, ENEMIES } from './secrets'
|
||||||
|
import { NPCS } from './npcs'
|
||||||
|
import { EVENTS } from './events'
|
||||||
|
import { POSTS } from './posts'
|
||||||
|
import { TRAITS } from './traits'
|
||||||
|
import { ROOT_GRADES, ROOT_GRADE_NAMES, ELEMENT_LIST } from './elements'
|
||||||
|
import { MAJORS, MAJOR_ORDER } from './realms'
|
||||||
|
|
||||||
|
export interface DataPack {
|
||||||
|
items: typeof ITEMS
|
||||||
|
artifacts: typeof ARTIFACT_POWER
|
||||||
|
techniques: typeof TECHNIQUES
|
||||||
|
buildings: typeof BUILDINGS
|
||||||
|
missions: typeof MISSIONS
|
||||||
|
enemies: typeof ENEMIES
|
||||||
|
npcs: typeof NPCS
|
||||||
|
events: typeof EVENTS
|
||||||
|
posts: typeof POSTS
|
||||||
|
traits: typeof TRAITS
|
||||||
|
rootGrades: typeof ROOT_GRADES
|
||||||
|
rootGradeNames: typeof ROOT_GRADE_NAMES
|
||||||
|
elements: typeof ELEMENT_LIST
|
||||||
|
majors: typeof MAJORS
|
||||||
|
majorOrder: typeof MAJOR_ORDER
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 默认包:与库内静态数据**同引用**(保证确定性指纹不变);未来 MOD 通过注入覆盖 */
|
||||||
|
export const DEFAULT_PACK: DataPack = {
|
||||||
|
items: ITEMS,
|
||||||
|
artifacts: ARTIFACT_POWER,
|
||||||
|
techniques: TECHNIQUES,
|
||||||
|
buildings: BUILDINGS,
|
||||||
|
missions: MISSIONS,
|
||||||
|
enemies: ENEMIES,
|
||||||
|
npcs: NPCS,
|
||||||
|
events: EVENTS,
|
||||||
|
posts: POSTS,
|
||||||
|
traits: TRAITS,
|
||||||
|
rootGrades: ROOT_GRADES,
|
||||||
|
rootGradeNames: ROOT_GRADE_NAMES,
|
||||||
|
elements: ELEMENT_LIST,
|
||||||
|
majors: MAJORS,
|
||||||
|
majorOrder: MAJOR_ORDER
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据包注册表:默认 `pack()` 返回 DEFAULT_PACK(引用共享)。
|
||||||
|
* setPack(partial) 覆写后立即生效;resetPack() 回到默认。返回的数据包指纹可用于版本校验。
|
||||||
|
*/
|
||||||
|
export class DataPackRegistry {
|
||||||
|
private data: DataPack = DEFAULT_PACK
|
||||||
|
|
||||||
|
current(): DataPack {
|
||||||
|
return this.data
|
||||||
|
}
|
||||||
|
|
||||||
|
override(partial: Partial<DataPack>): DataPack {
|
||||||
|
this.data = { ...DEFAULT_PACK, ...partial }
|
||||||
|
return this.data
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): DataPack {
|
||||||
|
this.data = DEFAULT_PACK
|
||||||
|
return this.data
|
||||||
|
}
|
||||||
|
|
||||||
|
fingerprint(): string {
|
||||||
|
const key = ['items', 'techniques', 'buildings', 'missions', 'events', 'npcs'] as const
|
||||||
|
let h = 7
|
||||||
|
for (const k of key) {
|
||||||
|
const o = this.data[k] as unknown as Record<string, unknown>
|
||||||
|
const len = Object.keys(o ?? {}).length
|
||||||
|
h = Math.imul(h ^ len, 31)
|
||||||
|
}
|
||||||
|
return (h >>> 0).toString(16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PACK = new DataPackRegistry()
|
||||||
|
|
||||||
|
/** 便捷读口(引擎唯一访问方式) */
|
||||||
|
export function pack(): DataPack {
|
||||||
|
return PACK.current()
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
export type ActName =
|
||||||
|
| 'head.set'
|
||||||
|
| 'member.meditate'
|
||||||
|
| 'member.technique'
|
||||||
|
| 'member.equip'
|
||||||
|
| 'member.pill'
|
||||||
|
| 'member.advance'
|
||||||
|
| 'member.marry'
|
||||||
|
| 'member.post'
|
||||||
|
| 'estate.build'
|
||||||
|
| 'estate.upgrade'
|
||||||
|
| 'estate.rite'
|
||||||
|
| 'estate.sutra'
|
||||||
|
| 'market.buy'
|
||||||
|
| 'market.sell'
|
||||||
|
| 'market.tech'
|
||||||
|
| 'craft.pill'
|
||||||
|
| 'diplomacy.gift'
|
||||||
|
| 'diplomacy.peace'
|
||||||
|
| 'diplomacy.taunt'
|
||||||
|
| 'diplomacy.marry'
|
||||||
|
| 'expedition.send'
|
||||||
|
| 'expedition.recall'
|
||||||
|
| 'legacy.resolve'
|
||||||
|
|
||||||
|
export interface ActPayload {
|
||||||
|
memberId?: string
|
||||||
|
targetId?: string
|
||||||
|
post?: string
|
||||||
|
item?: string
|
||||||
|
pill?: string
|
||||||
|
tech?: string
|
||||||
|
building?: string
|
||||||
|
npcId?: string
|
||||||
|
stones?: number
|
||||||
|
mission?: string
|
||||||
|
squad?: string[]
|
||||||
|
count?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ACT_CATALOG: Record<ActName, { desc: string; fn: (w: World, p: ActPayload) => boolean }> = {
|
||||||
|
'head.set': { desc: '册立家主', fn: (w, p) => (p.memberId ? (w.assignHead(p.memberId), true) : false) },
|
||||||
|
'member.meditate': { desc: '闭关/出关', fn: (w, p) => (p.memberId ? (w.setMeditation(p.memberId, !!p.count), true) : false) },
|
||||||
|
'member.technique': { desc: '传功', fn: (w, p) => (p.memberId && p.tech ? (w.giveTechnique(p.memberId, p.tech), true) : false) },
|
||||||
|
'member.equip': { desc: '装备法宝', fn: (w, p) => (p.memberId && p.item ? (w.equip(p.memberId, p.item), true) : false) },
|
||||||
|
'member.pill': { desc: '服丹', fn: (w, p) => (p.memberId && p.pill ? (w.takePill(p.memberId, p.pill), true) : false) },
|
||||||
|
'member.advance': { desc: '冲关', fn: (w, p) => (p.memberId ? (w.assistedBreakthrough(p.memberId), true) : false) },
|
||||||
|
'member.marry': { desc: '指婚', fn: (w, p) => (p.memberId && p.targetId ? w.marryTo(p.memberId, p.targetId) : false) },
|
||||||
|
'member.post': { desc: '任职/卸任', fn: (w, p) => (p.memberId ? w.assignPost(p.memberId, p.post) : false) },
|
||||||
|
'estate.build': { desc: '营建', fn: (w, p) => (p.building ? w.build(p.building) : false) },
|
||||||
|
'estate.upgrade': { desc: '升级', fn: (w, p) => (p.building ? w.upgrade(p.building) : false) },
|
||||||
|
'estate.rite': { desc: '祭祖', fn: (w) => w.ancestralRite() },
|
||||||
|
'estate.sutra': { desc: '求经', fn: (w) => w.seekSutra() },
|
||||||
|
'market.buy': { desc: '购货', fn: (w, p) => (p.item ? buyItem(w, p.item, p.count ?? 1) : false) },
|
||||||
|
'market.sell': { desc: '售货', fn: (w, p) => (p.item ? sellItem(w, p.item, p.count ?? 1) : false) },
|
||||||
|
'market.tech': { desc: '购法帖', fn: (w, p) => (p.tech ? buyTechnique(w, p.tech, p.stones ?? 0) : false) },
|
||||||
|
'craft.pill': { desc: '炼丹', fn: (w, p) => (p.item === 'ningyuan' ? w.craftPill('ningyuan') : w.craftPill('qiyuan')) },
|
||||||
|
'diplomacy.gift': { desc: '赠礼', fn: (w, p) => (p.npcId ? giftNpc(w, p.npcId, p.stones ?? 0) : false) },
|
||||||
|
'diplomacy.peace': { desc: '议和', fn: (w, p) => (p.npcId ? makePeace(w, p.npcId) : false) },
|
||||||
|
'diplomacy.taunt': { desc: '寻衅', fn: (w, p) => (p.npcId ? w.tauntNpc(p.npcId) : false) },
|
||||||
|
'diplomacy.marry': { desc: '联姻', fn: (w, p) => (p.npcId ? marryNpcFamily(w, p.npcId) : false) },
|
||||||
|
'expedition.send': { desc: '遣队出征', fn: (w, p) => (p.mission ? sendMission(w, p.mission, p.squad ?? []) : false) },
|
||||||
|
'expedition.recall': { desc: '召回', fn: (w, p) => (p.memberId ? (recallAll(w, p.memberId), true) : false) },
|
||||||
|
'legacy.resolve': { desc: '落印定鼎', fn: (w) => (w.resolveLegacyNow(), true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryResult {
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GameFacade {
|
||||||
|
constructor(
|
||||||
|
public readonly world: World,
|
||||||
|
public readonly slot: number,
|
||||||
|
private bus?: WorldEventBus
|
||||||
|
) {}
|
||||||
|
|
||||||
|
act(name: ActName, payload: ActPayload = {}): boolean {
|
||||||
|
const c = ACT_CATALOG[name]
|
||||||
|
if (!c) return false
|
||||||
|
return c.fn(this.world, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
query(ref: string): QueryResult {
|
||||||
|
const w = this.world
|
||||||
|
switch (ref) {
|
||||||
|
case 'family':
|
||||||
|
return { name: w.state.family.name, estate: w.state.family.estate, year: w.state.year, month: w.state.month, reputation: w.state.family.reputation, generation: w.state.family.generation }
|
||||||
|
case 'members':
|
||||||
|
return { list: w.aliveMembers().map((c) => ({ id: c.id, name: c.name, realm: `${c.realm.major}/${c.realm.minor}`, post: c.post ?? '', state: c.state, power: Math.round(combatPowerOf(w, c)) })) }
|
||||||
|
case 'legacy':
|
||||||
|
return resolveLegacy(w.state) as unknown as QueryResult
|
||||||
|
case 'yearAxis':
|
||||||
|
return { cells: yearAxis(w.state) as unknown as AxisCell[] }
|
||||||
|
case 'systems':
|
||||||
|
return { list: w.systemList() }
|
||||||
|
case 'finance':
|
||||||
|
return { stones: w.state.family.stones, accum: w.state.finance.accum, yearStats: w.state.yearStats }
|
||||||
|
default:
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }),
|
||||||
|
onChronicle: (entry: ChronicleEntry, important: boolean) => on({ type: 'chronicle', entry, important }),
|
||||||
|
onBattle: (log: BattleLog) => on({ type: 'battle', log }),
|
||||||
|
onPendingEvent: (id: string) => on({ type: 'pending', id }),
|
||||||
|
onGameOver: (reason: string) => on({ type: 'gameover', reason }),
|
||||||
|
onYearPaper: (report: YearlyReport) => on({ type: 'paper', report }),
|
||||||
|
onSystemChange: (id: string, enabled: boolean) => on({ type: 'sysChanged', id, enabled })
|
||||||
|
}
|
||||||
|
this.world.out.push(bus)
|
||||||
|
return () => {
|
||||||
|
const i = this.world.out.indexOf(bus)
|
||||||
|
if (i >= 0) this.world.out.splice(i, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
about(): { title: string; version: string; modules: number; systems: number; packFingerprint: string } {
|
||||||
|
return {
|
||||||
|
title: '仙途家族志',
|
||||||
|
version: '0.1.7',
|
||||||
|
modules: this.world.systemList().length,
|
||||||
|
systems: this.world.systemList().filter((s) => s.enabled).length,
|
||||||
|
packFingerprint: PACK.fingerprint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FacadeEvent =
|
||||||
|
| { type: 'log'; kind: LogKind; text: string; year: number; month: number }
|
||||||
|
| { type: 'chronicle'; entry: ChronicleEntry; important: boolean }
|
||||||
|
| { type: 'battle'; log: BattleLog }
|
||||||
|
| { type: 'pending'; id: string }
|
||||||
|
| { type: 'gameover'; reason: string }
|
||||||
|
| { type: 'paper'; report: YearlyReport }
|
||||||
|
| { type: 'sysChanged'; id: string; enabled: boolean }
|
||||||
|
|
||||||
|
export { marketPrice, computeLegacy }
|
||||||
|
export type { SaveMeta, LegacyArch }
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { PhaseId } from '../core/clock'
|
||||||
|
|
||||||
|
export interface SystemDef {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
version: string
|
||||||
|
desc: string
|
||||||
|
phase?: PhaseId
|
||||||
|
yearly?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SYSTEM_DEFS: SystemDef[] = [
|
||||||
|
{ id: 'production', name: '生产', version: '1.0', phase: 'production', desc: '灵田药丹坊市之产出与物价漂移。' },
|
||||||
|
{ id: 'aging', name: '寿元', version: '1.0', phase: 'aging', desc: '年岁衰减、伤病滋养与辞世。' },
|
||||||
|
{ id: 'cultivation', name: '修炼', version: '1.0', phase: 'cultivation', desc: '修为积累、突破试炼与悟道。' },
|
||||||
|
{ id: 'missions', name: '探秘', version: '1.0', phase: 'missions', desc: '秘境远征与遭遇战。' },
|
||||||
|
{ id: 'events', name: '事件', version: '1.0', phase: 'events', desc: '日常/家国/际遇之机与天劫。' },
|
||||||
|
{ id: 'diplomacy', name: '外交', version: '1.0', phase: 'diplomacy', desc: '四邻聚落之好恶与劫掠。' },
|
||||||
|
{ id: 'marriage', name: '婚育', version: '1.0', yearly: true, desc: '媒娶联姻、添丁与代为祭。' },
|
||||||
|
{ id: 'annals', name: '岁簿', version: '1.0', yearly: true, desc: '岁末族簿与开年报数。' },
|
||||||
|
{ id: 'tournament', name: '太虚大比', version: '1.0', desc: '五年一会的太虚仙盟争锋。' },
|
||||||
|
{ id: 'tribulation', name: '渡劫', version: '1.0', desc: '大境界天劫与护法之仪。' },
|
||||||
|
{ id: 'apprentice', name: '寄读', version: '1.0', desc: '宗门子弟外修与求经台。' },
|
||||||
|
{ id: 'season', name: '时节', version: '1.0', desc: '春夏秋冬之乘气(春耕夏修秋市冬闭)。' }
|
||||||
|
]
|
||||||
@@ -15,6 +15,13 @@ function ifAlive(fn: (w: World) => void): (w: World) => void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 能力开关:禁用即拔插(对应 capability id) */
|
||||||
|
function viaCap(capId: string, fn: (w: World) => void): (w: World) => void {
|
||||||
|
return (w: World) => {
|
||||||
|
if (w.sysEnabled(capId)) fn(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 装备时钟:年度钩子与月度 phase 的注册顺序即执行顺序(确定性)。
|
* 装备时钟:年度钩子与月度 phase 的注册顺序即执行顺序(确定性)。
|
||||||
* 时间线:婚配养育 → 声望岁贡 → 岁末族簿 —— 再逐月:生产→寿元→修炼→任务→事件→外交→收尾
|
* 时间线:婚配养育 → 声望岁贡 → 岁末族簿 —— 再逐月:生产→寿元→修炼→任务→事件→外交→收尾
|
||||||
@@ -22,23 +29,25 @@ function ifAlive(fn: (w: World) => void): (w: World) => void {
|
|||||||
export function buildClock(): GameClock {
|
export function buildClock(): GameClock {
|
||||||
const clock = new GameClock()
|
const clock = new GameClock()
|
||||||
|
|
||||||
clock.onYearStart((w) => yearStartMarriage(w))
|
clock.onYearStart(viaCap('marriage', (w) => yearStartMarriage(w)))
|
||||||
clock.onYearStart((w) => {
|
clock.onYearStart(
|
||||||
|
viaCap('marriage', (w) => {
|
||||||
w.state.family.reputation += w.postBonus('familyRep')
|
w.state.family.reputation += w.postBonus('familyRep')
|
||||||
const zhenCount = w
|
const zhenCount = w
|
||||||
.aliveMembers()
|
.aliveMembers()
|
||||||
.filter((c) => c.aspiration === 'zhen').length
|
.filter((c) => c.aspiration === 'zhen').length
|
||||||
w.state.family.reputation += Math.round(zhenCount * 0.3 * 100) / 100
|
w.state.family.reputation += Math.round(zhenCount * 0.3 * 100) / 100
|
||||||
})
|
})
|
||||||
clock.onYearStart((w) => w.publishYearReport())
|
)
|
||||||
|
clock.onYearStart(viaCap('annals', (w) => w.publishYearReport()))
|
||||||
|
|
||||||
clock.register('production', (w: World) => productionTick(w))
|
clock.register('production', viaCap('production', (w: World) => productionTick(w)))
|
||||||
clock.register('aging', (w: World) => deathTick(w))
|
clock.register('aging', viaCap('aging', (w: World) => deathTick(w)))
|
||||||
clock.register('aging', (w: World) => woundHealTick(w))
|
clock.register('aging', viaCap('aging', (w: World) => woundHealTick(w)))
|
||||||
clock.register('cultivation', ifAlive((w: World) => cultivationTick(w)))
|
clock.register('cultivation', viaCap('cultivation', ifAlive((w: World) => cultivationTick(w))))
|
||||||
clock.register('missions', ifAlive((w: World) => missionTick(w)))
|
clock.register('missions', viaCap('missions', ifAlive((w: World) => missionTick(w))))
|
||||||
clock.register('events', ifAlive((w: World) => eventRoll(w)))
|
clock.register('events', viaCap('events', ifAlive((w: World) => eventRoll(w))))
|
||||||
clock.register('diplomacy', ifAlive((w: World) => diplomacyTick(w)))
|
clock.register('diplomacy', viaCap('diplomacy', ifAlive((w: World) => diplomacyTick(w))))
|
||||||
clock.register('epilogue', (w: World) => w.epilogueTick())
|
clock.register('epilogue', (w: World) => w.epilogueTick())
|
||||||
|
|
||||||
return clock
|
return clock
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { World } from './world'
|
import type { World } from './world'
|
||||||
import { ITEMS } from '../data/items'
|
import { pack } from '../data/registry'
|
||||||
|
|
||||||
export function marketPrice(w: World, itemId: string): number {
|
export function marketPrice(w: World, itemId: string): number {
|
||||||
const base = ITEMS[itemId]?.basePrice ?? 1
|
const base = pack().items[itemId]?.basePrice ?? 1
|
||||||
const fam = w.state.family
|
const fam = w.state.family
|
||||||
const mult = typeof fam.flag['priceMult'] === 'number' ? (fam.flag['priceMult'] as number) : 1
|
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
|
const mood = fam.reputation >= 40 ? 1.06 : fam.reputation >= 20 ? 1.02 : 0.98
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { World } from '../world'
|
import type { World } from '../world'
|
||||||
import { Character, BattleLog } from '../../types/domain'
|
import { Character, BattleLog } from '../../types/domain'
|
||||||
import { basePower, describeRealm } from '../../data/realms'
|
import { basePower, describeRealm } from '../../data/realms'
|
||||||
import { ARTIFACT_POWER } from '../../data/items'
|
import { pack } from '../../data/registry'
|
||||||
import { techniqueById } from '../../data/techniques'
|
import { techniqueById } from '../../data/techniques'
|
||||||
import { EnemyDef, LootDef } from '../../data/secrets'
|
import { EnemyDef, LootDef } from '../../data/secrets'
|
||||||
import { TECHNIQUES } from '../../data/techniques'
|
|
||||||
import { traitBonuses } from '../pcgen'
|
import { traitBonuses } from '../pcgen'
|
||||||
import { npcById } from '../../data/npcs'
|
import { npcById } from '../../data/npcs'
|
||||||
import { aspirationById, fitBonusOf } from '../../data/aspirations'
|
import { aspirationById, fitBonusOf } from '../../data/aspirations'
|
||||||
@@ -16,7 +16,7 @@ export function combatPowerOf(w: World, c: Character): number {
|
|||||||
const tech = techniqueById(c.techniqueId)
|
const tech = techniqueById(c.techniqueId)
|
||||||
const wuRank = (c.techniqueRank ?? 0) > 1 ? 0.2 : (c.techniqueRank ?? 0) === 1 ? 0.08 : 0
|
const wuRank = (c.techniqueRank ?? 0) > 1 ? 0.2 : (c.techniqueRank ?? 0) === 1 ? 0.08 : 0
|
||||||
const techBonus = tech ? 1 + tech.powerBonus + wuRank : 1
|
const techBonus = tech ? 1 + tech.powerBonus + wuRank : 1
|
||||||
const equip = c.equipment ? 1 + (ARTIFACT_POWER[c.equipment] ?? 0) : 1
|
const equip = c.equipment ? 1 + (pack().artifacts[c.equipment] ?? 0) : 1
|
||||||
const aspiration = aspirationById(c.aspiration)
|
const aspiration = aspirationById(c.aspiration)
|
||||||
const aspi = aspiration?.effect.type === 'battle' ? 1 + aspiration.effect.value : 1
|
const aspi = aspiration?.effect.type === 'battle' ? 1 + aspiration.effect.value : 1
|
||||||
const fit = fitBonusOf(c).battle ? 1.04 : 1
|
const fit = fitBonusOf(c).battle ? 1.04 : 1
|
||||||
@@ -206,7 +206,7 @@ export function rollWarbooty(w: World, loot: LootDef): Record<string, number> {
|
|||||||
result[a] = 1
|
result[a] = 1
|
||||||
}
|
}
|
||||||
if (loot.techniqueChance && w.rng.chance(loot.techniqueChance)) {
|
if (loot.techniqueChance && w.rng.chance(loot.techniqueChance)) {
|
||||||
const t = w.rng.pick(TECHNIQUES)
|
const t = w.rng.pick(pack().techniques)
|
||||||
w.state.family.techniques.push(t.id)
|
w.state.family.techniques.push(t.id)
|
||||||
result['tech'] = 1
|
result['tech'] = 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import type { World } from '../world'
|
|||||||
import { Character } from '../../types/domain'
|
import { Character } from '../../types/domain'
|
||||||
import { Cond, EffectDef, EventDef, EVENTS, MemberEffect } from '../../data/events'
|
import { Cond, EffectDef, EventDef, EVENTS, MemberEffect } from '../../data/events'
|
||||||
import { MAJOR_ORDER } from '../../data/realms'
|
import { MAJOR_ORDER } from '../../data/realms'
|
||||||
import { TECHNIQUES } from '../../data/techniques'
|
|
||||||
import { MISSIONS } from '../../data/secrets'
|
|
||||||
import { npcById } from '../../data/npcs'
|
import { npcById } from '../../data/npcs'
|
||||||
import { resolveRaid } from './combat'
|
import { resolveRaid } from './combat'
|
||||||
import { sendMission } from './missions'
|
import { sendMission } from './missions'
|
||||||
|
import { pack } from '../../data/registry'
|
||||||
import { findInheritor } from '../creation'
|
import { findInheritor } from '../creation'
|
||||||
import { resolveTribulation } from './tribulation'
|
import { resolveTribulation } from './tribulation'
|
||||||
import { nextRealm, describeRealm } from '../../data/realms'
|
import { nextRealm, describeRealm } from '../../data/realms'
|
||||||
@@ -378,7 +379,7 @@ export function applyEffect(w: World, eff: EffectDef, squad?: string[]): void {
|
|||||||
Object.assign(fam.flag, eff.flag)
|
Object.assign(fam.flag, eff.flag)
|
||||||
}
|
}
|
||||||
if (eff.techniqueChance && w.rng.chance(eff.techniqueChance)) {
|
if (eff.techniqueChance && w.rng.chance(eff.techniqueChance)) {
|
||||||
const t = w.rng.pick(TECHNIQUES)
|
const t = w.rng.pick(pack().techniques)
|
||||||
if (!fam.techniques.includes(t.id)) {
|
if (!fam.techniques.includes(t.id)) {
|
||||||
fam.techniques.push(t.id)
|
fam.techniques.push(t.id)
|
||||||
w.log('good', `得《${t.name}》残篇,录入藏书阁。`)
|
w.log('good', `得《${t.name}》残篇,录入藏书阁。`)
|
||||||
@@ -495,7 +496,7 @@ export function applyEffect(w: World, eff: EffectDef, squad?: string[]): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (eff.mission) {
|
if (eff.mission) {
|
||||||
const def = MISSIONS.find((m) => m.id === eff.mission)
|
const def = pack().missions.find((m) => m.id === eff.mission)
|
||||||
if (def) {
|
if (def) {
|
||||||
const squad = w
|
const squad = w
|
||||||
.aliveMembers()
|
.aliveMembers()
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ import {
|
|||||||
} from '../types/domain'
|
} from '../types/domain'
|
||||||
import { Rng } from '../core/rng'
|
import { Rng } from '../core/rng'
|
||||||
import { BUILDINGS } from '../data/buildings'
|
import { BUILDINGS } from '../data/buildings'
|
||||||
|
import { pack } from '../data/registry'
|
||||||
import { POSTS } from '../data/posts'
|
import { POSTS } from '../data/posts'
|
||||||
import { TECHNIQUES } from '../data/techniques'
|
|
||||||
import { aspirationById as aspirationOf } from '../data/aspirations'
|
import { aspirationById as aspirationOf } from '../data/aspirations'
|
||||||
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../core/legacy'
|
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../core/legacy'
|
||||||
import { createWorldState, findInheritor } from './creation'
|
import { createWorldState, findInheritor } from './creation'
|
||||||
import { buildClock } from './clocks'
|
import { buildClock } from './clocks'
|
||||||
|
import { SYSTEM_DEFS, SystemDef } from './capabilities'
|
||||||
import { GameClock } from '../core/clock'
|
import { GameClock } from '../core/clock'
|
||||||
import { resolveBreakthrough } from './systems/cultivation'
|
import { resolveBreakthrough } from './systems/cultivation'
|
||||||
import { applyEventChoice } from './systems/events'
|
import { applyEventChoice } from './systems/events'
|
||||||
@@ -30,6 +32,7 @@ export interface WorldEventBus {
|
|||||||
onPendingEvent(id: string): void
|
onPendingEvent(id: string): void
|
||||||
onGameOver(reason: string, year: number): void
|
onGameOver(reason: string, year: number): void
|
||||||
onYearPaper?(entry: YearlyReport): void
|
onYearPaper?(entry: YearlyReport): void
|
||||||
|
onSystemChange?(id: string, enabled: boolean): void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeGameState(state: GameState): GameState {
|
export function normalizeGameState(state: GameState): GameState {
|
||||||
@@ -65,6 +68,7 @@ export class World {
|
|||||||
rng: Rng
|
rng: Rng
|
||||||
out: WorldEventBus[]
|
out: WorldEventBus[]
|
||||||
clock: GameClock
|
clock: GameClock
|
||||||
|
systems: Record<string, { enabled: boolean }>
|
||||||
|
|
||||||
constructor(state: GameState, out: WorldEventBus[] = []) {
|
constructor(state: GameState, out: WorldEventBus[] = []) {
|
||||||
normalizeGameState(state)
|
normalizeGameState(state)
|
||||||
@@ -72,6 +76,23 @@ export class World {
|
|||||||
this.rng = new Rng(state.rng)
|
this.rng = new Rng(state.rng)
|
||||||
this.out = out
|
this.out = out
|
||||||
this.clock = buildClock()
|
this.clock = buildClock()
|
||||||
|
this.systems = Object.fromEntries(SYSTEM_DEFS.map((d) => [d.id, { enabled: true }]))
|
||||||
|
}
|
||||||
|
|
||||||
|
sysEnabled(id: string): boolean {
|
||||||
|
return this.systems[id]?.enabled ?? true
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSystem(id: string): boolean {
|
||||||
|
const s = this.systems[id]
|
||||||
|
if (!s) return false
|
||||||
|
s.enabled = !s.enabled
|
||||||
|
this.out.forEach((o) => o.onSystemChange?.(id, s.enabled))
|
||||||
|
return s.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
systemList(): { id: string; name: string; version: string; desc: string; enabled: boolean }[] {
|
||||||
|
return SYSTEM_DEFS.map((d) => ({ id: d.id, name: d.name, version: d.version, desc: d.desc, enabled: this.sysEnabled(d.id) }))
|
||||||
}
|
}
|
||||||
|
|
||||||
seq(): Id {
|
seq(): Id {
|
||||||
@@ -115,6 +136,10 @@ export class World {
|
|||||||
this.out.forEach((o) => o.onGameOver(reason, year))
|
this.out.forEach((o) => o.onGameOver(reason, year))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gameOver___placeholder(): void {
|
||||||
|
void 0
|
||||||
|
}
|
||||||
|
|
||||||
memberById(id: Id): Character {
|
memberById(id: Id): Character {
|
||||||
const c = this.state.members[id]
|
const c = this.state.members[id]
|
||||||
if (!c) throw new Error(`member not found ${id}`)
|
if (!c) throw new Error(`member not found ${id}`)
|
||||||
@@ -444,7 +469,7 @@ export class World {
|
|||||||
if (fam.stones < 150) return false
|
if (fam.stones < 150) return false
|
||||||
fam.stones -= 150
|
fam.stones -= 150
|
||||||
fam.flag['sutraCD'] = this.state.year
|
fam.flag['sutraCD'] = this.state.year
|
||||||
const pool = TECHNIQUES.filter((t) => t.grade >= 2 && !fam.techniques.includes(t.id))
|
const pool = pack().techniques.filter((t) => t.grade >= 2 && !fam.techniques.includes(t.id))
|
||||||
if (pool.length === 0) {
|
if (pool.length === 0) {
|
||||||
this.log('info', '求经访道:天下典籍已入庶几,无可再得。')
|
this.log('info', '求经访道:天下典籍已入庶几,无可再得。')
|
||||||
this.chronicle('event', '求经台广搜天下,经卷已穷。', undefined, false)
|
this.chronicle('event', '求经台广搜天下,经卷已穷。', undefined, false)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { findEvent, applyEventChoice } from '../game/engine/systems/events'
|
|||||||
import { BattleLog, ChronicleEntry, GameState, LogItem, SaveMeta, YearlyReport, SnapshotMeta } from '../game/types/domain'
|
import { BattleLog, ChronicleEntry, GameState, LogItem, SaveMeta, YearlyReport, SnapshotMeta } from '../game/types/domain'
|
||||||
import { getSlotManager, getSaveSlot } from '../game/storage/db'
|
import { getSlotManager, getSaveSlot } from '../game/storage/db'
|
||||||
import { metaFromState, updateSlotMeta } from './storeHelper'
|
import { metaFromState, updateSlotMeta } from './storeHelper'
|
||||||
|
import { GameFacade, ActName } from '../game/engine/api'
|
||||||
import { setSoundEnabled, sPaper, sGood, sBad, sWar, sBell, sGong, sClick, sTick } from './sound'
|
import { setSoundEnabled, sPaper, sGood, sBad, sWar, sBell, sGong, sClick, sTick } from './sound'
|
||||||
import type { PhaseStat } from '../game/core/clock'
|
import type { PhaseStat } from '../game/core/clock'
|
||||||
|
|
||||||
@@ -58,6 +59,8 @@ export interface GameStore {
|
|||||||
onYearPaper: (report: YearlyReport) => void
|
onYearPaper: (report: YearlyReport) => void
|
||||||
closePaper: () => void
|
closePaper: () => void
|
||||||
toggleSound: () => void
|
toggleSound: () => void
|
||||||
|
facade?: GameFacade
|
||||||
|
act: (name: ActName, payload: import('../game/engine/api').ActPayload) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const LOG_CAP = 260
|
const LOG_CAP = 260
|
||||||
@@ -65,6 +68,7 @@ const lastPhaseStats = new Map<World, PhaseStat[]>()
|
|||||||
let logSeq = 1
|
let logSeq = 1
|
||||||
|
|
||||||
export const useGameStore = create<GameStore>((set, get) => ({
|
export const useGameStore = create<GameStore>((set, get) => ({
|
||||||
|
facade: undefined as GameFacade | undefined,
|
||||||
screen: 'boot',
|
screen: 'boot',
|
||||||
world: null,
|
world: null,
|
||||||
slot: 1,
|
slot: 1,
|
||||||
@@ -236,6 +240,16 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
|||||||
|
|
||||||
bump: () => set((s) => ({ revision: s.revision + 1 })),
|
bump: () => set((s) => ({ revision: s.revision + 1 })),
|
||||||
|
|
||||||
|
act: (name, payload) => {
|
||||||
|
const st = get()
|
||||||
|
const w = st.world
|
||||||
|
if (!w) return false
|
||||||
|
const ok = st.facade ? st.facade.act(name, payload) : actDirect(w, name, payload)
|
||||||
|
if (ok) set((s) => ({ revision: s.revision + 1 }))
|
||||||
|
return ok
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
setSpeed: (v) => {
|
setSpeed: (v) => {
|
||||||
const st = get()
|
const st = get()
|
||||||
if (st.timer) clearInterval(st.timer)
|
if (st.timer) clearInterval(st.timer)
|
||||||
@@ -311,6 +325,8 @@ function openState(state: GameState, slot: number): void {
|
|||||||
const world = new World(state)
|
const world = new World(state)
|
||||||
const st = useGameStore.getState()
|
const st = useGameStore.getState()
|
||||||
world.out.push(makeBus(st))
|
world.out.push(makeBus(st))
|
||||||
|
const facade = new GameFacade(world, slot)
|
||||||
|
world.out.push(facadebus(facade))
|
||||||
useGameStore.setState({
|
useGameStore.setState({
|
||||||
world,
|
world,
|
||||||
slot,
|
slot,
|
||||||
@@ -347,6 +363,28 @@ function makeBus(st: GameStore): WorldEventBus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function facadebus(facade: GameFacade): WorldEventBus {
|
||||||
|
return {
|
||||||
|
onLog: () => undefined,
|
||||||
|
onChronicle: () => undefined,
|
||||||
|
onBattle: () => undefined,
|
||||||
|
onPendingEvent: () => undefined,
|
||||||
|
onGameOver: () => undefined,
|
||||||
|
onSystemChange: () => undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function actDirect(w: World, name: ActName, payload: import('../game/engine/api').ActPayload): boolean {
|
||||||
|
if (name === 'legacy.resolve') return (w.resolveLegacyNow(), true)
|
||||||
|
if (name === 'estate.rite') return w.ancestralRite()
|
||||||
|
if (name === 'estate.sutra') return w.seekSutra()
|
||||||
|
if (name === 'estate.build' && payload.building) return w.build(payload.building)
|
||||||
|
if (name === 'estate.upgrade' && payload.building) return w.upgrade(payload.building)
|
||||||
|
if (name === 'member.post' && payload.memberId) return w.assignPost(payload.memberId, payload.post)
|
||||||
|
if (name === 'member.marry' && payload.memberId && payload.targetId) return w.marryTo(payload.memberId, payload.targetId)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const Stash = {
|
const Stash = {
|
||||||
async save(st: GameStore, label: string): Promise<void> {
|
async save(st: GameStore, label: string): Promise<void> {
|
||||||
if (!st.world) return
|
if (!st.world) return
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
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 { PACK, pack, DEFAULT_PACK } from '../src/renderer/game/data/registry'
|
||||||
|
import { marketPrice } from '../src/renderer/game/engine/market'
|
||||||
|
import { stateFingerprint, longRun } from './fingerprint.helper'
|
||||||
|
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||||
|
|
||||||
|
function baseWorld(seed: string): World {
|
||||||
|
const w = World.create({ seed, surname: '阮', familyName: '阮家', motto: 'm', difficulty: 'normal' })
|
||||||
|
resetSaveBus()
|
||||||
|
attachLogSink(w)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CapabilityRegistry 能力卡', () => {
|
||||||
|
it('系统清单齐全(id 唯一、版本与说明在位)', () => {
|
||||||
|
const ids = new Set(SYSTEM_DEFS.map((d) => d.id))
|
||||||
|
expect(ids.size).toBe(SYSTEM_DEFS.length)
|
||||||
|
expect(SYSTEM_DEFS.length).toBeGreaterThanOrEqual(11)
|
||||||
|
for (const d of SYSTEM_DEFS) {
|
||||||
|
expect(d.version).toMatch(/^\d+\.\d+$/)
|
||||||
|
expect(d.name.length).toBeGreaterThan(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('禁用修炼 → 12 月无人精进;恢复后回归', () => {
|
||||||
|
const w = baseWorld('cap-a')
|
||||||
|
const x5 = w.state.members['x5']
|
||||||
|
x5.realm = { major: 'qi', minor: 1 }
|
||||||
|
const p0 = x5.realmProgress
|
||||||
|
w.toggleSystem('cultivation')
|
||||||
|
for (let i = 0; i < 12; i++) w.advanceMonth()
|
||||||
|
expect(x5.realmProgress).toBe(p0)
|
||||||
|
w.toggleSystem('cultivation')
|
||||||
|
for (let i = 0; i < 12; i++) w.advanceMonth()
|
||||||
|
expect(x5.realmProgress).toBeGreaterThan(p0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('禁用生产 → 库藏零涨;恢复后产出回归', () => {
|
||||||
|
const w = baseWorld('cap-b')
|
||||||
|
const c0 = w.state.family.inventory['lingcao'] ?? 0
|
||||||
|
w.toggleSystem('production')
|
||||||
|
for (let i = 0; i < 6; i++) w.advanceMonth()
|
||||||
|
expect(w.state.family.inventory['lingcao'] ?? 0).toBe(c0)
|
||||||
|
w.toggleSystem('production')
|
||||||
|
for (let i = 0; i < 6; i++) w.advanceMonth()
|
||||||
|
expect(w.state.family.inventory['lingcao'] ?? 0).toBeGreaterThan(c0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('禁用婚育 → 全年无诞丁与媒觅', () => {
|
||||||
|
const w = baseWorld('cap-c')
|
||||||
|
const n0 = Object.values(w.state.members).filter((c) => c.bornYear > 1).length
|
||||||
|
w.toggleSystem('marriage')
|
||||||
|
for (let i = 0; i < 48; i++) w.advanceMonth()
|
||||||
|
const n1 = Object.values(w.state.members).filter((c) => c.bornYear > 1).length
|
||||||
|
expect(n1).toBe(n0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('禁用事件 → 迟迟无事,天空澄澈', () => {
|
||||||
|
const w = baseWorld('cap-d')
|
||||||
|
w.toggleSystem('events')
|
||||||
|
for (let i = 0; i < 40; i++) w.advanceMonth()
|
||||||
|
expect(w.state.pendingEvent).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggle 幂等与 sysChanged 广播', () => {
|
||||||
|
const w = baseWorld('cap-e')
|
||||||
|
const changes: [string, boolean][] = []
|
||||||
|
w.out.push({
|
||||||
|
onLog: () => undefined,
|
||||||
|
onChronicle: () => undefined,
|
||||||
|
onBattle: () => undefined,
|
||||||
|
onPendingEvent: () => undefined,
|
||||||
|
onGameOver: () => undefined,
|
||||||
|
onSystemChange: (id, enabled) => changes.push([id, enabled])
|
||||||
|
})
|
||||||
|
const r1 = w.toggleSystem('season')
|
||||||
|
expect(r1).toBe(false)
|
||||||
|
expect(changes).toEqual([['season', false]])
|
||||||
|
const r2 = w.toggleSystem('season')
|
||||||
|
expect(r2).toBe(true)
|
||||||
|
expect(w.toggleSystem('no-such-system')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('DataPackRegistry 数据包', () => {
|
||||||
|
it('默认包与静态数据同引用(金钟罩不受影响)', () => {
|
||||||
|
expect(pack().items).toBe(DEFAULT_PACK.items)
|
||||||
|
expect(DEFAULT_PACK.items['lingcao'].name).toBe('灵草')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('覆写物价 → 行情立即反映;重置还原', () => {
|
||||||
|
const w = baseWorld('dp-a')
|
||||||
|
const p0 = marketPrice(w, 'lingcao')
|
||||||
|
PACK.override({ items: { ...DEFAULT_PACK.items, lingcao: { ...DEFAULT_PACK.items['lingcao'], basePrice: 999 } } })
|
||||||
|
const p1 = marketPrice(w, 'lingcao')
|
||||||
|
expect(p1).toBeGreaterThan(p0)
|
||||||
|
PACK.reset()
|
||||||
|
const p2 = marketPrice(w, 'lingcao')
|
||||||
|
expect(p2).toBe(p0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fingerprint 稳定且对覆写敏感', () => {
|
||||||
|
const f0 = PACK.fingerprint()
|
||||||
|
PACK.override({})
|
||||||
|
const f1 = PACK.fingerprint()
|
||||||
|
expect(f1).toBe(f0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('GameFacade 门面', () => {
|
||||||
|
it('act 全目录跑通(每类至少一条不抛且有副作用或无副作用返回)', () => {
|
||||||
|
const w = baseWorld('fa-a')
|
||||||
|
const f = new GameFacade(w, 1)
|
||||||
|
f.act('head.set', { memberId: 'x3' })
|
||||||
|
expect(w.state.family.headId).toBe('x3')
|
||||||
|
f.act('member.post', { memberId: 'x4', post: 'guardian' })
|
||||||
|
expect(w.state.members['x4'].post).toBe('guardian')
|
||||||
|
f.act('estate.build', { building: 'fangshi' })
|
||||||
|
expect(w.state.family.buildings['fangshi']).toBe(1)
|
||||||
|
f.act('estate.rite', {})
|
||||||
|
expect(w.state.family.flag['lastRiteYear']).toBe(1)
|
||||||
|
const afterRite = w.state.family.stones
|
||||||
|
f.act('market.sell', { item: 'lingcao', count: 5 })
|
||||||
|
expect(w.state.family.stones).toBeGreaterThan(afterRite)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('query 家族/成员/系统/年轴/财务快照', () => {
|
||||||
|
const w = baseWorld('fa-b')
|
||||||
|
const f = new GameFacade(w, 1)
|
||||||
|
const fam = f.query('family') as { year: number; reputation: number }
|
||||||
|
expect(fam.year).toBe(1)
|
||||||
|
const mem = f.query('members') as { list: { id: string }[] }
|
||||||
|
expect(mem.list.length).toBe(5)
|
||||||
|
const sys = f.query('systems') as { list: { id: string }[] }
|
||||||
|
expect(sys.list.length).toBe(SYSTEM_DEFS.length)
|
||||||
|
const fin = f.query('finance') as { stones: number }
|
||||||
|
expect(fin.stones).toBe(800)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('subscribe 协议全套捕获(log/paper/sysChanged)并可退订', () => {
|
||||||
|
const w = baseWorld('fa-c')
|
||||||
|
const f = new GameFacade(w, 1)
|
||||||
|
const seen: string[] = []
|
||||||
|
const unsub = f.subscribe((e) => seen.push(e.type))
|
||||||
|
for (let i = 0; i < 14; i++) w.advanceMonth()
|
||||||
|
f.act('estate.rite', {})
|
||||||
|
w.toggleSystem('season')
|
||||||
|
expect(seen).toContain('log')
|
||||||
|
expect(seen).toContain('paper')
|
||||||
|
expect(seen).toContain('sysChanged')
|
||||||
|
unsub()
|
||||||
|
const before = seen.length
|
||||||
|
w.advanceMonth()
|
||||||
|
expect(seen.length).toBe(before)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('about 元数据完整', () => {
|
||||||
|
const w = baseWorld('fa-d')
|
||||||
|
const f = new GameFacade(w, 1)
|
||||||
|
const info = f.about()
|
||||||
|
expect(info.title).toBe('仙途家族志')
|
||||||
|
expect(info.version).toContain('0.1.7')
|
||||||
|
expect(info.modules).toBeGreaterThanOrEqual(11)
|
||||||
|
expect(info.systems).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('默认配置金钟罩不受门面化影响', () => {
|
||||||
|
PACK.reset()
|
||||||
|
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('9af71ecb')
|
||||||
|
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('753aca71')
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user