v0.1.8: 万物皆是插件 + 全应用审计修复(60+ 缺陷歼灭)
插件协议:
- CotycPlugin Manifest(dependencies/conflicts/protected/install/uninstall)
- PluginManager:安装序确定性/依赖拒绝/异常回滚/追踪型上下文(卸载全摘钩子)
- 三核心插件常驻(Systems/Data/Events);插件清单/安装移除/启停广播
- EventPoolRegistry 多池合并;facade about/query('plugins')/plugin 订阅
- 设置页插件清单(含卸载按钮、核心保护);examplePlugin 开发范本入仓
全应用审计修复(引擎 39 + UI/主进程 40 双路清单,P0→P2 歼灭):
- P0:单亲 rollRoots 崩/插件事件软锁(findEvent 查池)/fire 覆盖幸存事件/
facade 未赋值(act 目录 6/24 生效→全量接通)/packOverride 死实现真接管/
4 能力卡无接线(trib/apprentice/tournament/season)/读档 rowid 方言崩溃/
buildApprentice 空候选崩/插件安装无回滚/trait 加成从未消费/DEV 双跑破坏时序
- P1:定时器幸存/advance in-flight 闸/battle 暂停/tournament 点将真传/
史书 memo 依赖/面板 hooks 顺序违规/远征 squad 残留 id/跨档状态残留/
toast 自清除/读档错误提示/导入后重载世界/pendingEvent 读档回填
- P2:版本三处统一/NewGame 随机收编/学费错误文案/姓氏池去重/
功法品阶错位(凡阶哨兵)/功法定价对齐/market 白名单校验/
战利功法去重/回声漏封缄/clock 迭代快照/core 事件池保护/
渡劫陨落走真突破/寄读 desc 诚实化/doas 死代码清理等 30 项
测试 239→256;金钟罩复验(防御批零漂移)
This commit is contained in:
@@ -48,13 +48,12 @@ export function buildBiography(s: GameState, memberId: string): BioLine[] {
|
||||
}
|
||||
|
||||
// 大比
|
||||
const tourneys = s.stats.tourneyHistory.filter((t) => true).slice(-1)
|
||||
const hisTourney = tourneys.filter((t) => {
|
||||
const hisTourneys = s.stats.tourneyHistory.filter((t) => {
|
||||
const battle = s.battles.find((b) => b.year === t.year && b.title.includes('大比'))
|
||||
return battle?.lines.some((l) => l.includes(c.name))
|
||||
})
|
||||
if (hisTourney.length > 0) {
|
||||
lines.push({ at: hisTourney[0].year, label: '大比', text: `${hisTourney[0].year}年,随队参加太虚大比,列第${hisTourney[0].rank}名。` })
|
||||
for (const t of hisTourneys) {
|
||||
lines.push({ at: t.year, label: '大比', text: `${t.year}年,随队参加太虚大比,列第${t.rank}名。` })
|
||||
}
|
||||
|
||||
// 墓碑铭文
|
||||
|
||||
@@ -38,14 +38,22 @@ export class GameClock {
|
||||
private monthly = new Map<PhaseId, SystemHook[]>()
|
||||
private yearly: SystemHook[] = []
|
||||
|
||||
register(phase: PhaseId, fn: SystemHook): void {
|
||||
register(phase: PhaseId, fn: SystemHook): () => void {
|
||||
const list = this.monthly.get(phase) ?? []
|
||||
list.push(fn)
|
||||
this.monthly.set(phase, list)
|
||||
return () => {
|
||||
const li = list.indexOf(fn)
|
||||
if (li >= 0) list.splice(li, 1)
|
||||
}
|
||||
}
|
||||
|
||||
onYearStart(fn: SystemHook): void {
|
||||
onYearStart(fn: SystemHook): () => void {
|
||||
this.yearly.push(fn)
|
||||
return () => {
|
||||
const i = this.yearly.indexOf(fn)
|
||||
if (i >= 0) this.yearly.splice(i, 1)
|
||||
}
|
||||
}
|
||||
|
||||
fireYearStart(w: World): void {
|
||||
@@ -56,7 +64,7 @@ export class GameClock {
|
||||
const report: PhaseStat[] = []
|
||||
for (const phase of PHASE_ORDER) {
|
||||
const t0 = performance.now()
|
||||
const fns = this.monthly.get(phase) ?? []
|
||||
const fns = [...(this.monthly.get(phase) ?? [])]
|
||||
for (const fn of fns) fn(w)
|
||||
const ms = performance.now() - t0
|
||||
report.push({ phase, ms, count: fns.length })
|
||||
@@ -69,4 +77,8 @@ export class GameClock {
|
||||
for (const list of this.monthly.values()) n += list.length
|
||||
return n + this.yearly.length
|
||||
}
|
||||
|
||||
snapshot(): Array<{ phase: PhaseId; count: number }> {
|
||||
return PHASE_ORDER.map((phase) => ({ phase, count: (this.monthly.get(phase) ?? []).length }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export const SURNAME_POOL = [
|
||||
'林', '苏', '沈', '谢', '顾', '萧', '叶', '江', '秦', '裴',
|
||||
'柳', '陆', '云', '姜', '晏', '楚', '洛', '许', '宋', '薛',
|
||||
'韩', '白', '纪', '容', '卫', '柳', '燕', '温', '孟', '阮',
|
||||
'洛', '池', '顾', '岑', '傅', '虞', '尹', '霍', '曲', '齐'
|
||||
'韩', '白', '纪', '容', '卫', '燕', '温', '孟', '阮',
|
||||
'池', '岑', '傅', '虞', '尹', '霍', '曲', '齐'
|
||||
]
|
||||
|
||||
export const MALE_GIVEN = [
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { GameClock, SystemHook } from './clock'
|
||||
import { DataPack } from '../data/registry'
|
||||
import { EventDef } from '../data/events'
|
||||
import type { World } from '../engine/world'
|
||||
|
||||
export type PluginKind = 'system' | 'data' | 'events' | 'content'
|
||||
|
||||
export interface PluginHookGuard {
|
||||
onLoad?(): void
|
||||
onDisable?(): void
|
||||
onEnable?(): void
|
||||
onUninstall?(): void
|
||||
}
|
||||
|
||||
export interface CotycPlugin {
|
||||
id: string
|
||||
name: string
|
||||
version: string
|
||||
author?: string
|
||||
description: string
|
||||
kind: PluginKind
|
||||
dependencies?: string[]
|
||||
conflicts?: string[]
|
||||
install(ctx: PluginContext): void
|
||||
uninstall?(ctx: PluginContext): void
|
||||
hooks?: PluginHookGuard
|
||||
protected?: boolean
|
||||
}
|
||||
|
||||
export interface PluginContext {
|
||||
world: World
|
||||
clock: GameClock
|
||||
register: (phase: Parameters<GameClock['register']>[0], fn: SystemHook) => () => void
|
||||
onYearStart: (fn: SystemHook) => () => void
|
||||
addCapability: (cap: { id: string; name: string; version: string; desc: string }) => void
|
||||
removeCapability: (id: string) => void
|
||||
enableCapability: (id: string, enabled: boolean) => void
|
||||
overridePack: (partial: Partial<DataPack>) => void
|
||||
resetPack: () => void
|
||||
addEventPool: (id: string, events: EventDef[]) => void
|
||||
removeEventPool: (id: string) => void
|
||||
}
|
||||
|
||||
export interface PluginStatus {
|
||||
id: string
|
||||
name: string
|
||||
version: string
|
||||
kind: PluginKind
|
||||
installed: boolean
|
||||
enabled: boolean
|
||||
protected: boolean
|
||||
dependencies?: string[]
|
||||
}
|
||||
|
||||
export type PluginChange = { id: string; action: 'install' | 'remove' | 'enable' | 'disable' }
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Element } from '../types/domain'
|
||||
import { Character } from '../types/domain'
|
||||
import { Element, type Character } from '../types/domain'
|
||||
import { techniqueById } from './techniques'
|
||||
|
||||
export type AspirationEffectType = 'cult' | 'battle' | 'market' | 'field' | 'offspring' | 'rep'
|
||||
@@ -58,7 +57,7 @@ export const ASPIRATIONS: Record<string, AspirationDef> = {
|
||||
yun: {
|
||||
id: 'yun',
|
||||
name: '云游',
|
||||
desc: '天地为庐:修炼 -3%,但多见奇景(际遇更频)。',
|
||||
desc: '天地为庐:修炼 -3%,所见所闻略广。',
|
||||
icon: '云',
|
||||
effect: { type: 'cult', value: -0.03 }
|
||||
}
|
||||
@@ -71,15 +70,6 @@ export function aspirationById(id: string | undefined): AspirationDef | null {
|
||||
return ASPIRATIONS[id] ?? null
|
||||
}
|
||||
|
||||
export function countAspiration(type: AspirationEffectType, members: Character[]): number {
|
||||
let n = 0
|
||||
for (const c of members) {
|
||||
const def = aspirationById(c.aspiration)
|
||||
if (def?.effect.type === type) n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
export function fitBonusOf(c: Character): { cult: boolean; battle: boolean } {
|
||||
const tech = techniqueById(c.techniqueId)
|
||||
if (!tech) return { cult: false, battle: false }
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface NpcFamilyDef {
|
||||
|
||||
export const NPCS: NpcFamilyDef[] = [
|
||||
{
|
||||
id: 'n-xuanying', name: '玄影沈氏', region: '北岳玄影峰', style: '剑修世家', desc: '隐于北岳的剑修吕氏,剑意凛冽,最为孤傲。',
|
||||
id: 'n-xuanying', name: '玄影沈氏', region: '北岳玄影峰', style: '剑修世家', desc: '隐于北岳的剑修沈氏,剑意凛冽,最为孤傲。',
|
||||
leaderRealm: 'foundation', initialPower: 240, powerGrowth: [4, 10],
|
||||
sells: ['weapon-qi', 'weapon-ling'], buys: ['lingcao', 'lingkuang']
|
||||
},
|
||||
|
||||
@@ -116,4 +116,8 @@ export interface TechniqueDef {
|
||||
desc: string
|
||||
}
|
||||
|
||||
export const TECHNIQUE_GRADE_NAMES = ['黄阶', '玄阶', '地阶', '天阶', '仙阶']
|
||||
export const TECHNIQUE_GRADE_NAMES = ['凡阶', '黄阶', '玄阶', '地阶', '天阶', '仙阶']
|
||||
|
||||
export function techniqueGradeName(grade: number): string {
|
||||
return TECHNIQUE_GRADE_NAMES[grade] ?? '?'
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export const ACT_CATALOG: Record<ActName, { desc: string; fn: (w: World, p: ActP
|
||||
'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')) },
|
||||
'craft.pill': { desc: '炼丹', fn: (w, p) => (p.item === 'ningyuan' ? w.craftPill('ningyuan') : Boolean(p.item === 'qiyuan') && 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) },
|
||||
@@ -108,6 +108,8 @@ export class GameFacade {
|
||||
return { list: w.systemList() }
|
||||
case 'finance':
|
||||
return { stones: w.state.family.stones, accum: w.state.finance.accum, yearStats: w.state.yearStats }
|
||||
case 'plugins':
|
||||
return { list: w.pluginList() }
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
@@ -121,7 +123,8 @@ export class GameFacade {
|
||||
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 })
|
||||
onSystemChange: (id: string, enabled: boolean) => on({ type: 'sysChanged', id, enabled }),
|
||||
onPluginChange: (id: string, action: string) => on({ type: 'plugin', id, action })
|
||||
}
|
||||
this.world.out.push(bus)
|
||||
return () => {
|
||||
@@ -130,12 +133,13 @@ export class GameFacade {
|
||||
}
|
||||
}
|
||||
|
||||
about(): { title: string; version: string; modules: number; systems: number; packFingerprint: string } {
|
||||
about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } {
|
||||
return {
|
||||
title: '仙途家族志',
|
||||
version: '0.1.7',
|
||||
version: '0.1.8',
|
||||
modules: this.world.systemList().length,
|
||||
systems: this.world.systemList().filter((s) => s.enabled).length,
|
||||
plugins: this.world.pluginList().length,
|
||||
packFingerprint: PACK.fingerprint()
|
||||
}
|
||||
}
|
||||
@@ -149,6 +153,7 @@ export type FacadeEvent =
|
||||
| { type: 'gameover'; reason: string }
|
||||
| { type: 'paper'; report: YearlyReport }
|
||||
| { type: 'sysChanged'; id: string; enabled: boolean }
|
||||
| { type: 'plugin'; id: string; action: string }
|
||||
|
||||
export { marketPrice, computeLegacy }
|
||||
export type { SaveMeta, LegacyArch }
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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'
|
||||
@@ -6,7 +8,6 @@ import { missionTick } from './systems/missions'
|
||||
import { eventRoll } from './systems/events'
|
||||
import { diplomacyTick } from './systems/diplomacy'
|
||||
import { yearStartMarriage } from './systems/marriage'
|
||||
import type { World } from './world'
|
||||
|
||||
/** 原语义保留:满门凋零后仅存生产/寿元与收尾 */
|
||||
function ifAlive(fn: (w: World) => void): (w: World) => void {
|
||||
@@ -22,33 +23,59 @@ function viaCap(capId: string, fn: (w: World) => void): (w: World) => void {
|
||||
}
|
||||
}
|
||||
|
||||
export function emptyClock(): GameClock {
|
||||
return new GameClock()
|
||||
}
|
||||
|
||||
/**
|
||||
* 装备时钟:年度钩子与月度 phase 的注册顺序即执行顺序(确定性)。
|
||||
* 内建系统注册(core-systems 插件入口)。
|
||||
* 注册顺序 = 执行顺序(确定性红线,经由金钟罩校验)。
|
||||
* 时间线:婚配养育 → 声望岁贡 → 岁末族簿 —— 再逐月:生产→寿元→修炼→任务→事件→外交→收尾
|
||||
*/
|
||||
export function buildClock(): GameClock {
|
||||
const clock = new GameClock()
|
||||
export function installCoreSystems(ctx: PluginContext): Array<() => void> {
|
||||
const unsubs: Array<() => void> = []
|
||||
const clock = ctx.clock
|
||||
|
||||
clock.onYearStart(viaCap('marriage', (w) => yearStartMarriage(w)))
|
||||
clock.onYearStart(
|
||||
viaCap('marriage', (w) => {
|
||||
w.state.family.reputation += w.postBonus('familyRep')
|
||||
const zhenCount = w
|
||||
.aliveMembers()
|
||||
.filter((c) => c.aspiration === 'zhen').length
|
||||
w.state.family.reputation += Math.round(zhenCount * 0.3 * 100) / 100
|
||||
})
|
||||
unsubs.push(clock.onYearStart(viaCap('marriage', (w) => yearStartMarriage(w))))
|
||||
unsubs.push(
|
||||
clock.onYearStart(
|
||||
viaCap('marriage', (w) => {
|
||||
w.state.family.reputation += w.postBonus('familyRep')
|
||||
const zhenCount = w.aliveMembers().filter((c) => c.aspiration === 'zhen').length
|
||||
w.state.family.reputation += Math.round(zhenCount * 0.3 * 100) / 100
|
||||
})
|
||||
)
|
||||
)
|
||||
clock.onYearStart(viaCap('annals', (w) => w.publishYearReport()))
|
||||
unsubs.push(clock.onYearStart(viaCap('annals', (w) => w.publishYearReport())))
|
||||
|
||||
clock.register('production', viaCap('production', (w: World) => productionTick(w)))
|
||||
clock.register('aging', viaCap('aging', (w: World) => deathTick(w)))
|
||||
clock.register('aging', viaCap('aging', (w: World) => woundHealTick(w)))
|
||||
clock.register('cultivation', viaCap('cultivation', ifAlive((w: World) => cultivationTick(w))))
|
||||
clock.register('missions', viaCap('missions', ifAlive((w: World) => missionTick(w))))
|
||||
clock.register('events', viaCap('events', ifAlive((w: World) => eventRoll(w))))
|
||||
clock.register('diplomacy', viaCap('diplomacy', ifAlive((w: World) => diplomacyTick(w))))
|
||||
clock.register('epilogue', (w: World) => w.epilogueTick())
|
||||
unsubs.push(clock.register('production', viaCap('production', (w: World) => productionTick(w))))
|
||||
unsubs.push(clock.register('aging', viaCap('aging', (w: World) => deathTick(w))))
|
||||
unsubs.push(clock.register('aging', viaCap('aging', (w: World) => woundHealTick(w))))
|
||||
unsubs.push(clock.register('cultivation', viaCap('cultivation', ifAlive((w: World) => cultivationTick(w)))))
|
||||
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('epilogue', (w: World) => w.epilogueTick()))
|
||||
|
||||
return unsubs
|
||||
}
|
||||
|
||||
/** 兼容旧接口:完整安装一套核心系统(测试/工具用) */
|
||||
export function buildClock(): GameClock {
|
||||
const clock = emptyClock()
|
||||
const ctx = {
|
||||
world: undefined as never,
|
||||
clock,
|
||||
register: (phase: Parameters<GameClock['register']>[0], fn: (w: World) => void) => clock.register(phase, fn),
|
||||
onYearStart: (fn: (w: World) => void) => clock.onYearStart(fn),
|
||||
addCapability: () => undefined,
|
||||
removeCapability: () => undefined,
|
||||
enableCapability: () => undefined,
|
||||
overridePack: () => undefined,
|
||||
resetPack: () => undefined,
|
||||
addEventPool: () => undefined,
|
||||
removeEventPool: () => undefined
|
||||
}
|
||||
installCoreSystems(ctx as never)
|
||||
return clock
|
||||
}
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import type { World } from './world'
|
||||
import { pack } from '../data/registry'
|
||||
import { traitBonuses } from './pcgen'
|
||||
|
||||
export function marketPrice(w: World, itemId: string): number {
|
||||
const base = pack().items[itemId]?.basePrice ?? 1
|
||||
const item = pack().items[itemId]
|
||||
if (!item) return 0
|
||||
const base = item.basePrice
|
||||
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
|
||||
return Math.max(1, Math.round(base * mult * mood))
|
||||
// 利己(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)))
|
||||
}
|
||||
|
||||
export function buyItem(w: World, itemId: string, count: number): boolean {
|
||||
const fam = w.state.family
|
||||
if (!pack().items[itemId]) return false
|
||||
const total = marketPrice(w, itemId) * count
|
||||
if (total > fam.stones) return false
|
||||
fam.stones -= total
|
||||
@@ -20,6 +27,7 @@ export function buyItem(w: World, itemId: string, count: number): boolean {
|
||||
|
||||
export function sellItem(w: World, itemId: string, count: number): boolean {
|
||||
const fam = w.state.family
|
||||
if (!pack().items[itemId]) return false
|
||||
const have = fam.inventory[itemId] ?? 0
|
||||
if (have < count) return false
|
||||
fam.inventory[itemId] = have - count
|
||||
@@ -37,12 +45,12 @@ export function buyTechnique(w: World, techId: string, price: number): boolean {
|
||||
}
|
||||
|
||||
export function techniquePrice(techId: string): number {
|
||||
const grade = TECH_GRADE_BASE[techId] ?? 200
|
||||
return grade
|
||||
return TECH_GRADE_BASE[techId] ?? 200
|
||||
}
|
||||
|
||||
import { TECHNIQUES } from '../data/techniques'
|
||||
|
||||
const TECHNIQUE_GRADE_PRICE: Record<number, number> = { 1: 120, 2: 300, 3: 700, 4: 1600 }
|
||||
const TECH_GRADE_BASE: Record<string, number> = Object.fromEntries(
|
||||
TECHNIQUES.map((t) => [t.id, [120, 300, 700, 1600, 3600][t.grade] ?? 300])
|
||||
TECHNIQUES.map((t) => [t.id, TECHNIQUE_GRADE_PRICE[t.grade] ?? 300])
|
||||
)
|
||||
|
||||
@@ -27,9 +27,8 @@ export function rollRoots(rng: Rng, parents?: { m?: Character; f?: Character }):
|
||||
}
|
||||
grade = Math.max(0, Math.min(5, grade))
|
||||
|
||||
const primaryCandidate = parents && (parents.m || parents.f)
|
||||
? [parents.m!.roots.primary, parents.f!.roots.primary]
|
||||
: ELEMENT_LIST
|
||||
const single = parents ? (parents.m ?? parents.f) : undefined
|
||||
const primaryCandidate = single ? [single.roots.primary] : ELEMENT_LIST
|
||||
const primary = rng.pick(primaryCandidate)
|
||||
const secondaryCount = grade >= 3 ? rng.int(1, 2) : grade === 2 ? rng.int(0, 1) : 0
|
||||
const rest = ELEMENT_LIST.filter((e) => e !== primary)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { CotycPlugin } from '../core/plugin'
|
||||
import { installCoreSystems } from './clocks'
|
||||
import { EVENTS } from '../data/events'
|
||||
import { DEFAULT_PACK } from '../data/registry'
|
||||
|
||||
/** 内置插件一:镇族基石(12 能力卡与时轮钩子) */
|
||||
export function makeCoreSystemsPlugin(): CotycPlugin {
|
||||
let unsubs: Array<() => void> = []
|
||||
return {
|
||||
id: 'core-systems',
|
||||
name: '镇族基石',
|
||||
version: '0.1.8',
|
||||
kind: 'system',
|
||||
protected: true,
|
||||
description: '十二能力卡与时轮钩子:生产/寿元/修炼/探秘/事件/外交/婚育/岁簿/大比/渡劫/寄读/时节。',
|
||||
install(ctx) {
|
||||
unsubs = installCoreSystems(ctx)
|
||||
},
|
||||
uninstall(ctx) {
|
||||
void ctx
|
||||
unsubs.forEach((u) => u())
|
||||
unsubs = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 内置插件二:经典数据包(默认平衡表) */
|
||||
export function makeCoreDataPlugin(): CotycPlugin {
|
||||
return {
|
||||
id: 'core-data',
|
||||
name: '经典数据包',
|
||||
version: '0.1.8',
|
||||
kind: 'data',
|
||||
protected: true,
|
||||
description: '平衡表默认包:境界/物品/功法/建筑/秘境/势力/性格/职事。',
|
||||
install(ctx) {
|
||||
ctx.resetPack()
|
||||
},
|
||||
uninstall() {
|
||||
void DEFAULT_PACK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 内置插件三:内建事件池(日常/家国/际遇) */
|
||||
export function makeCoreEventsPlugin(): CotycPlugin {
|
||||
return {
|
||||
id: 'core-events',
|
||||
name: '内建事件池',
|
||||
version: '0.1.8',
|
||||
kind: 'events',
|
||||
protected: true,
|
||||
description: '命运事件主池:日常/重大/乾坤三层选支事件。',
|
||||
install(ctx) {
|
||||
ctx.addEventPool('core', EVENTS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const CORE_PLUGINS: CotycPlugin[] = [
|
||||
makeCoreSystemsPlugin(),
|
||||
makeCoreDataPlugin(),
|
||||
makeCoreEventsPlugin()
|
||||
]
|
||||
@@ -0,0 +1,131 @@
|
||||
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'
|
||||
|
||||
interface RuntimePlugin {
|
||||
plugin: CotycPlugin
|
||||
status: { installed: boolean; enabled: boolean }
|
||||
unsubscribers: Array<() => void>
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件管理器:统一安装/卸载/启停管线。
|
||||
* 安装序即确定性序;依赖缺失或冲突 → 拒绝安装。
|
||||
*/
|
||||
export class PluginManager {
|
||||
private runtime = new Map<string, RuntimePlugin>()
|
||||
private order: string[] = []
|
||||
private changes: PluginChange[] = []
|
||||
|
||||
constructor(private ctx: PluginContext) {}
|
||||
|
||||
private onChange(c: PluginChange): void {
|
||||
this.changes.push(c)
|
||||
}
|
||||
|
||||
listChanges(): PluginChange[] {
|
||||
return this.changes.slice()
|
||||
}
|
||||
|
||||
clearChanges(): void {
|
||||
this.changes = []
|
||||
}
|
||||
|
||||
install(plugin: CotycPlugin): { ok: boolean; reason?: string } {
|
||||
if (this.runtime.has(plugin.id)) return { ok: false, reason: `插件已存在:${plugin.id}` }
|
||||
for (const dep of plugin.dependencies ?? []) {
|
||||
if (!this.runtime.has(dep)) return { ok: false, reason: `缺少依赖:${dep}` }
|
||||
}
|
||||
for (const c of plugin.conflicts ?? []) {
|
||||
if (this.runtime.has(c)) return { ok: false, reason: `与 ${c} 冲突` }
|
||||
}
|
||||
for (const def of SYSTEM_DEFS) {
|
||||
if (def.id === plugin.id) return { ok: false, reason: `id 冲突:${plugin.id}` }
|
||||
}
|
||||
|
||||
const rt: RuntimePlugin = { plugin, status: { installed: true, enabled: true }, unsubscribers: [] }
|
||||
this.runtime.set(plugin.id, rt)
|
||||
this.order.push(plugin.id)
|
||||
// 追踪型上下文:插件在时轮上的注册(phase/年首)一律归属该插件,卸载时全摘
|
||||
const tracked = new Proxy(this.ctx, {
|
||||
get(target, key) {
|
||||
const prop = key as keyof PluginContext
|
||||
if (prop === 'register' || prop === 'onYearStart') {
|
||||
return (phase: Parameters<GameClock['register']>[0], fn: SystemHook) => {
|
||||
const unsub = (target[prop] as (phase: Parameters<GameClock['register']>[0], fn: SystemHook) => () => void)(phase, fn)
|
||||
rt.unsubscribers.push(unsub)
|
||||
return unsub
|
||||
}
|
||||
}
|
||||
return (target as unknown as Record<string, unknown>)[key as string]
|
||||
}
|
||||
})
|
||||
try {
|
||||
plugin.install(tracked as PluginContext)
|
||||
plugin.hooks?.onLoad?.()
|
||||
} catch (e) {
|
||||
rt.unsubscribers.forEach((u) => u())
|
||||
this.runtime.delete(plugin.id)
|
||||
const i = this.order.indexOf(plugin.id)
|
||||
if (i >= 0) this.order.splice(i, 1)
|
||||
return { ok: false, reason: `安装异常:${String(e)}` }
|
||||
}
|
||||
this.onChange({ id: plugin.id, action: 'install' })
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
remove(pluginId: string): { ok: boolean; reason?: string } {
|
||||
const rt = this.runtime.get(pluginId)
|
||||
if (!rt) return { ok: false, reason: '未安装' }
|
||||
if (rt.plugin.protected) return { ok: false, reason: '核心插件受保护' }
|
||||
rt.unsubscribers.forEach((u) => u())
|
||||
rt.plugin.uninstall?.(this.ctx)
|
||||
this.runtime.delete(pluginId)
|
||||
const i = this.order.indexOf(pluginId)
|
||||
if (i >= 0) this.order.splice(i, 1)
|
||||
this.onChange({ id: pluginId, action: 'remove' })
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
setEnabled(pluginId: string, enabled: boolean): { ok: boolean; reason?: string } {
|
||||
const rt = this.runtime.get(pluginId)
|
||||
if (!rt) return { ok: false, reason: '未安装' }
|
||||
if (enabled && !rt.plugin.hooks?.onEnable) {
|
||||
// 无 enable 钩子的插件视为直接切换 enabled 状态
|
||||
}
|
||||
rt.status.enabled = enabled
|
||||
if (enabled) rt.plugin.hooks?.onEnable?.()
|
||||
else rt.plugin.hooks?.onDisable?.()
|
||||
this.onChange({ id: pluginId, action: enabled ? 'enable' : 'disable' })
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
list(): PluginStatus[] {
|
||||
return this.order
|
||||
.filter((id) => this.runtime.has(id))
|
||||
.map((id) => {
|
||||
const rt = this.runtime.get(id)!
|
||||
const p = rt.plugin
|
||||
return {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
version: p.version,
|
||||
kind: p.kind,
|
||||
installed: rt.status.installed,
|
||||
enabled: rt.status.enabled,
|
||||
protected: !!p.protected,
|
||||
dependencies: p.dependencies
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
has(id: string): boolean {
|
||||
return this.runtime.has(id)
|
||||
}
|
||||
|
||||
get(id: string): CotycPlugin | null {
|
||||
return this.runtime.get(id)?.plugin ?? null
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export function monthlyRate(w: World, c: Character): number {
|
||||
const buildings = st.family.buildings
|
||||
const juling = buildings['juling'] ?? 0
|
||||
rate *= 1 + juling * 0.05
|
||||
rate *= 1 + seasonMod(st.month, 'cult')
|
||||
if (w.sysEnabled('season')) rate *= 1 + seasonMod(st.month, 'cult')
|
||||
rate *= 1 + w.postBonus('expAll')
|
||||
if (st.family.flag['fengFeiBless']) rate *= 1.05
|
||||
if (c.traits.includes('fengxian')) rate *= 1.3
|
||||
@@ -36,7 +36,7 @@ export function monthlyRate(w: World, c: Character): number {
|
||||
if (fitBonusOf(c).cult) rate *= 1.06
|
||||
if (c.state === 'meditation') {
|
||||
rate *= 1.35
|
||||
rate *= 1 + w.postBonus('meditation') + seasonMod(st.month, 'meditation')
|
||||
rate *= 1 + w.postBonus('meditation') + (w.sysEnabled('season') ? seasonMod(st.month, 'meditation') : 0)
|
||||
const dongfu = buildings['dongfu'] ?? 0
|
||||
rate *= 1 + dongfu * 0.08
|
||||
} else if (c.state === 'expedition') {
|
||||
@@ -49,6 +49,7 @@ export function monthlyRate(w: World, c: Character): number {
|
||||
if (c.health <= 40) rate *= 0.45
|
||||
else if (c.health <= 70) rate *= 0.8
|
||||
}
|
||||
rate *= traitBonuses(c).exp
|
||||
if (w.ageOf(c) < 8) rate *= 0.4
|
||||
if (w.ageOf(c) > 55) rate *= 0.7
|
||||
rate *= masteryRateOfMajor(c.realm.major)
|
||||
@@ -89,7 +90,7 @@ export function cultivationTick(w: World): void {
|
||||
if (c.realmProgress >= 100) {
|
||||
const months = (w.state.year * 12 + w.state.month) - (c.lastBreakthroughAttempt ?? -999)
|
||||
if (months >= 6 && w.rng.chance(perAttemptChance(w, c))) {
|
||||
if (needsTribulation(c)) {
|
||||
if (needsTribulation(c) && w.sysEnabled('tribulation')) {
|
||||
// 大境界晋升改走渡劫事件(玩家三选);压制者待来年
|
||||
if (!c.tribDelayYear || w.state.year >= c.tribDelayYear) {
|
||||
c.tribDelayYear = undefined
|
||||
|
||||
@@ -15,7 +15,13 @@ import { nextRealm, describeRealm } from '../../data/realms'
|
||||
const ALL_EVENTS: EventDef[] = [...EVENTS]
|
||||
|
||||
export function findEvent(id: string, world?: World): EventDef | undefined {
|
||||
return ALL_EVENTS.find((e) => e.id === id) ?? dynamicEventFor(id, world)
|
||||
const found = ALL_EVENTS.find((e) => e.id === id)
|
||||
if (found) return found
|
||||
if (world) {
|
||||
const pooled = world.allEvents().find((e) => e.id === id)
|
||||
if (pooled) return pooled
|
||||
}
|
||||
return dynamicEventFor(id, world)
|
||||
}
|
||||
|
||||
export function dynamicEventFor(id: string, w?: World): EventDef | undefined {
|
||||
@@ -57,7 +63,7 @@ export function dynamicEventFor(id: string, w?: World): EventDef | undefined {
|
||||
name: '太虚大比',
|
||||
category: 'major',
|
||||
weight: 0,
|
||||
text: `太虚仙盟十年一会,新一期大比于祖庭开擂。观礼之众云集,百族竞锋——可遣嫡系赴赛,亦可称病让席。`,
|
||||
text: `太虚仙盟五年一会,新一期大比于祖庭开擂。观礼之众云集,百族竞锋——可遣嫡系赴赛,亦可称病让席。`,
|
||||
options: [
|
||||
{ label: '点将赴赛', hint: '战三关,位次定声望', eff: { tournament: true, flag: { [`tournamentYear-${year}`]: true } } },
|
||||
{ label: '献礼买名', hint: '灵石-200,名次垫底,各族好感略升', eff: { res: { stones: -200 }, relation: { 'n-xuanying': 5, 'n-danxin': 5, 'n-sihai': 5, 'n-nulei': 5 } } },
|
||||
@@ -243,7 +249,7 @@ export function eventRoll(w: World): void {
|
||||
return
|
||||
}
|
||||
const firstSpirit = s.flags['firstSpirit'] as number | undefined
|
||||
if (firstSpirit && s.year - firstSpirit >= 2 && s.year - firstSpirit <= 3 && !s.completedEvents.includes('ev-feisheng')) {
|
||||
if (firstSpirit && s.year - firstSpirit >= 2 && !s.completedEvents.includes('ev-feisheng')) {
|
||||
fire(w, 'ev-feisheng')
|
||||
return
|
||||
}
|
||||
@@ -256,14 +262,16 @@ export function eventRoll(w: World): void {
|
||||
fire(w, `ev-tournament-${tourneyNext}`)
|
||||
return
|
||||
}
|
||||
// 四邻回声(每两年左右一桩)
|
||||
// 四邻回声(偶数年一掷,不论成败封缄)
|
||||
if (s.year % 2 === 0) {
|
||||
const key = `echoDone-${s.year}`
|
||||
if (!s.family.flag[key] && w.rng.chance(0.7)) {
|
||||
const npc = w.rng.pick(Object.values(s.npcFamilies))
|
||||
if (!s.family.flag[key]) {
|
||||
s.family.flag[key] = true
|
||||
fire(w, `ev-echo-${npc.id}-${s.year}`)
|
||||
return
|
||||
if (w.rng.chance(0.7)) {
|
||||
const npc = w.rng.pick(Object.values(s.npcFamilies))
|
||||
fire(w, `ev-echo-${npc.id}-${s.year}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +279,8 @@ export function eventRoll(w: World): void {
|
||||
const roll = w.rng.next()
|
||||
const category: 'daily' | 'major' | 'fate' | undefined = roll < 0.5 ? 'daily' : roll < 0.78 ? 'major' : roll < 0.86 ? 'fate' : undefined
|
||||
if (!category) return
|
||||
const candidates = ALL_EVENTS.filter(
|
||||
const pool = w.allEvents()
|
||||
const candidates = pool.filter(
|
||||
(e) =>
|
||||
e.category === category &&
|
||||
!(e.once && s.completedEvents.includes(e.id)) &&
|
||||
@@ -290,6 +299,7 @@ export function eventRoll(w: World): void {
|
||||
}
|
||||
|
||||
export function fire(w: World, id: string): void {
|
||||
if (w.state.pendingEvent) return
|
||||
w.state.pendingEvent = id
|
||||
w.pendingEvent(id)
|
||||
}
|
||||
@@ -394,7 +404,7 @@ export function applyEffect(w: World, eff: EffectDef, squad?: string[]): void {
|
||||
if (!fam.techniques.includes(eff.addTech)) fam.techniques.push(eff.addTech)
|
||||
}
|
||||
if (eff.tournament) {
|
||||
runTournament(w)
|
||||
runTournament(w, squad)
|
||||
}
|
||||
if (eff.apprentice?.build) {
|
||||
buildApprentice(w)
|
||||
|
||||
@@ -111,8 +111,8 @@ function resultText(m: MissionState): string {
|
||||
export function canSendMission(w: World, def: MissionDef, members: string[]): boolean {
|
||||
if (members.length < def.minMembers || members.length > def.maxMembers) return false
|
||||
for (const id of members) {
|
||||
const c = w.memberById(id)
|
||||
if (!c.alive || c.state === 'expedition' || c.state === 'apprentice') return false
|
||||
const c = w.state.members[id]
|
||||
if (!c || !c.alive || c.state === 'expedition' || c.state === 'apprentice') return false
|
||||
}
|
||||
return w.state.missions.filter((m) => !m.done).length < 3
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export function productionTick(w: World): void {
|
||||
|
||||
const fielders = w.aliveMembers().filter((c) => aspirationById(c.aspiration)?.effect.type === 'field').length
|
||||
const merchants = w.aliveMembers().filter((c) => aspirationById(c.aspiration)?.effect.type === 'market').length
|
||||
const springMod = seasonMod(w.state.month, 'field')
|
||||
const springMod = w.sysEnabled('season') ? seasonMod(w.state.month, 'field') : 0
|
||||
if (lingtian > 0) {
|
||||
const v = Math.round(10 * lingtian * (1 + fielders * 0.05 + springMod))
|
||||
inv.lingcao = (inv.lingcao ?? 0) + v
|
||||
@@ -36,7 +36,7 @@ export function productionTick(w: World): void {
|
||||
inv.lingkuang = (inv.lingkuang ?? 0) + v
|
||||
parts.push(`灵矿+${v}灵矿`)
|
||||
}
|
||||
const autumnMod = seasonMod(w.state.month, 'market')
|
||||
const autumnMod = w.sysEnabled('season') ? seasonMod(w.state.month, 'market') : 0
|
||||
if (fangshi > 0) {
|
||||
const v = Math.round(55 * fangshi * (1 + w.postBonus('marketIncome') + merchants * 0.03 + autumnMod))
|
||||
fam.stones += v
|
||||
|
||||
@@ -20,6 +20,10 @@ export function gateEnemy(idx: number, year: number): EnemyDef {
|
||||
}
|
||||
|
||||
export function runTournament(w: World, squad?: string[]): void {
|
||||
if (!w.sysEnabled('tournament')) {
|
||||
w.log('info', '太虚大比:赛事未启。')
|
||||
return
|
||||
}
|
||||
const s = w.state
|
||||
const eligible = w
|
||||
.aliveMembers()
|
||||
@@ -83,12 +87,16 @@ export function apprenticeCandidates(w: World): Character[] {
|
||||
}
|
||||
|
||||
export function buildApprentice(w: World): void {
|
||||
if (!w.sysEnabled('apprentice')) {
|
||||
w.log('bad', '宗门来使失望而归:族中暂拒通学。')
|
||||
return
|
||||
}
|
||||
const cand = apprenticeCandidates(w)
|
||||
const c = w.rng.pick(cand)
|
||||
if (!c) {
|
||||
if (cand.length === 0) {
|
||||
w.log('bad', '宗门来使失望而归:族中无适龄儿郎。')
|
||||
return
|
||||
}
|
||||
const c = w.rng.pick(cand)
|
||||
const sect = w.rng.pick(SECTS)
|
||||
const years = w.rng.int(2, 4)
|
||||
c.state = 'apprentice'
|
||||
@@ -125,6 +133,7 @@ export function finalizeApprentice(w: World, memberId: string, mode: 'return' |
|
||||
}
|
||||
|
||||
export function checkApprenticeExpiry(w: World): void {
|
||||
if (!w.sysEnabled('apprentice')) return
|
||||
for (const c of Object.values(w.state.members)) {
|
||||
if (!c.alive || !c.apprentice || c.apprentice.quiet) continue
|
||||
if (w.state.year >= c.apprentice.untilYear) {
|
||||
@@ -138,6 +147,7 @@ export function checkApprenticeExpiry(w: World): void {
|
||||
}
|
||||
|
||||
export function recruitCheck(w: World): void {
|
||||
if (!w.sysEnabled('apprentice')) return
|
||||
const s = w.state
|
||||
if (s.year % 4 !== 0) return
|
||||
const key = `recruitDone-${s.year}`
|
||||
|
||||
@@ -21,7 +21,6 @@ export function resolveTribulation(w: World, c: Character, mode: 'rash' | 'guard
|
||||
|
||||
if (mode === 'delay') {
|
||||
c.tribDelayYear = w.state.year + 1
|
||||
w.state.family.stones = w.state.family.stones
|
||||
w.log('info', `${c.name} 按兵不动,引而不发,待来年再渡。`)
|
||||
return 'delayed'
|
||||
}
|
||||
|
||||
@@ -10,15 +10,19 @@ import {
|
||||
} from '../types/domain'
|
||||
import { Rng } from '../core/rng'
|
||||
import { BUILDINGS } from '../data/buildings'
|
||||
import { pack } from '../data/registry'
|
||||
import { POSTS } from '../data/posts'
|
||||
|
||||
import { aspirationById as aspirationOf } from '../data/aspirations'
|
||||
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../core/legacy'
|
||||
import { createWorldState, findInheritor } from './creation'
|
||||
import { buildClock } from './clocks'
|
||||
import { SYSTEM_DEFS, SystemDef } from './capabilities'
|
||||
import { emptyClock } from './clocks'
|
||||
import { CotycPlugin, PluginContext, PluginStatus } from '../core/plugin'
|
||||
import { PluginManager } from './pluginManager'
|
||||
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'
|
||||
@@ -33,6 +37,7 @@ export interface WorldEventBus {
|
||||
onGameOver(reason: string, year: number): void
|
||||
onYearPaper?(entry: YearlyReport): void
|
||||
onSystemChange?(id: string, enabled: boolean): void
|
||||
onPluginChange?(id: string, action: string): void
|
||||
}
|
||||
|
||||
export function normalizeGameState(state: GameState): GameState {
|
||||
@@ -55,6 +60,18 @@ export function normalizeGameState(state: GameState): GameState {
|
||||
if (!state.battles) state.battles = []
|
||||
if (!state.eventQueue) state.eventQueue = []
|
||||
if (!state.completedEvents) state.completedEvents = []
|
||||
if (!state.missions) state.missions = []
|
||||
if (!state.flags) state.flags = {}
|
||||
if (!state.npcFamilies) state.npcFamilies = {}
|
||||
if (!state.family.flag) state.family.flag = {}
|
||||
if (!state.family.inventory) state.family.inventory = {}
|
||||
if (!state.family.buildings) state.family.buildings = {}
|
||||
if (!state.family.techniques) state.family.techniques = []
|
||||
if (!state.family.missionIds) state.family.missionIds = []
|
||||
if (state.family.headId && !state.members[state.family.headId]) {
|
||||
const firstAlive = Object.values(state.members).find((c) => c.alive)
|
||||
if (firstAlive) state.family.headId = firstAlive.id
|
||||
}
|
||||
for (const c of Object.values(state.members)) {
|
||||
if (typeof c.techniqueRank !== 'number') c.techniqueRank = 0
|
||||
if (typeof c.techniqueProgress !== 'number') c.techniqueProgress = 0
|
||||
@@ -69,14 +86,118 @@ export class World {
|
||||
out: WorldEventBus[]
|
||||
clock: GameClock
|
||||
systems: Record<string, { enabled: boolean }>
|
||||
plugins: PluginManager
|
||||
private eventPools = new Map<string, EventDef[]>()
|
||||
|
||||
constructor(state: GameState, out: WorldEventBus[] = []) {
|
||||
normalizeGameState(state)
|
||||
this.state = state
|
||||
this.rng = new Rng(state.rng)
|
||||
this.out = out
|
||||
this.clock = buildClock()
|
||||
this.clock = emptyClock()
|
||||
this.systems = Object.fromEntries(SYSTEM_DEFS.map((d) => [d.id, { enabled: true }]))
|
||||
this.plugins = new PluginManager(this.buildPluginContext())
|
||||
this.installCorePlugins()
|
||||
}
|
||||
|
||||
|
||||
/** 事件池聚合(含动态事件回看) */
|
||||
|
||||
buildPluginContext(): PluginContext {
|
||||
const self = this
|
||||
return {
|
||||
world: self,
|
||||
clock: self.clock,
|
||||
register: (phase, fn: SystemHook) => self.clock.register(phase, fn),
|
||||
onYearStart: (fn: SystemHook) => self.clock.onYearStart(fn),
|
||||
addCapability: (cap) => {
|
||||
if (!SYSTEM_DEFS.find((d) => d.id === cap.id)) {
|
||||
SYSTEM_DEFS.push({ id: cap.id, name: cap.name, version: cap.version, desc: cap.desc })
|
||||
}
|
||||
self.systems[cap.id] = { enabled: true }
|
||||
},
|
||||
removeCapability: (id) => {
|
||||
delete self.systems[id]
|
||||
},
|
||||
enableCapability: (id, enabled) => {
|
||||
if (self.systems[id]) self.systems[id].enabled = enabled
|
||||
},
|
||||
overridePack: (partial) => {
|
||||
void pack
|
||||
self.packOverride(partial)
|
||||
},
|
||||
resetPack: () => {
|
||||
self.packReset()
|
||||
},
|
||||
addEventPool: (id, events) => {
|
||||
self.eventPools.set(id, events)
|
||||
},
|
||||
removeEventPool: (id) => {
|
||||
self.eventPools.delete(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private packOverride(partial: Partial<import('../data/registry').DataPack>): void {
|
||||
PACK.override(partial)
|
||||
}
|
||||
|
||||
private packReset(): void {
|
||||
PACK.reset()
|
||||
}
|
||||
|
||||
installCorePlugins(): void {
|
||||
// 内置三插件:系统/数据/事件(受保护常驻,统一走管线)
|
||||
for (const p of CORE_PLUGINS) {
|
||||
this.plugins.install(p)
|
||||
}
|
||||
}
|
||||
|
||||
pluginList(): PluginStatus[] {
|
||||
return this.plugins.list()
|
||||
}
|
||||
|
||||
pluginChanges(): { id: string; action: string }[] {
|
||||
return this.plugins.listChanges()
|
||||
}
|
||||
|
||||
installPlugin(p: CotycPlugin): { ok: boolean; reason?: string } {
|
||||
const r = this.plugins.install(p)
|
||||
if (r.ok) this.out.forEach((o) => o.onPluginChange?.(p.id, 'install'))
|
||||
return r
|
||||
}
|
||||
|
||||
removePlugin(id: string): { ok: boolean; reason?: string } {
|
||||
const r = this.plugins.remove(id)
|
||||
if (r.ok) {
|
||||
this.rebuildEventPoolsAfterRemoval(id)
|
||||
this.out.forEach((o) => o.onPluginChange?.(id, 'remove'))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
setPluginEnabled(id: string, enabled: boolean): { ok: boolean; reason?: string } {
|
||||
const r = this.plugins.setEnabled(id, enabled)
|
||||
if (r.ok) this.out.forEach((o) => o.onPluginChange?.(id, enabled ? 'enable' : 'disable'))
|
||||
return r
|
||||
}
|
||||
|
||||
allEvents(): EventDef[] {
|
||||
const list: EventDef[] = []
|
||||
for (const pool of this.eventPools.values()) {
|
||||
for (const e of pool) list.push(e)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
eventPoolIds(): string[] {
|
||||
return [...this.eventPools.keys()]
|
||||
}
|
||||
|
||||
private rebuildEventPoolsAfterRemoval(id: string): void {
|
||||
void id
|
||||
// 事件池卸载暂由插件 uninstall 自行处理;此处在 remove 后重置 core 保证可用
|
||||
if (!this.eventPools.has('core')) this.eventPools.set('core', [])
|
||||
}
|
||||
|
||||
sysEnabled(id: string): boolean {
|
||||
@@ -525,7 +646,7 @@ export class World {
|
||||
}
|
||||
|
||||
marryTo(aId: Id, bId: Id): boolean {
|
||||
return arrangeWeddingPublic(this, aId, bId)
|
||||
return arrangeWeddingBridge(this, aId, bId)
|
||||
}
|
||||
|
||||
static create(opts: { seed: string; surname: string; familyName: string; motto: string; difficulty: 'easy' | 'normal' | 'hard' }): World {
|
||||
@@ -542,18 +663,13 @@ function w2age(w: World, c: Character): number {
|
||||
return w.ageOf(c)
|
||||
}
|
||||
|
||||
function widowedOf(w: World, m: Character): boolean {
|
||||
if (!m.spouseId) return false
|
||||
const sp = w.state.members[m.spouseId]
|
||||
return !!sp && !sp.alive
|
||||
}
|
||||
|
||||
function arrangeWeddingPublic(w: World, aId: Id, bId: Id): boolean {
|
||||
const a = w.memberById(aId)
|
||||
const b = w.memberById(bId)
|
||||
if (!a.alive || !b.alive) return false
|
||||
if (a.spouseId && !widowedOf(w, a)) return false
|
||||
if (b.spouseId && !widowedOf(w, b)) return false
|
||||
function arrangeWeddingBridge(w: World, aId: Id, bId: Id): boolean {
|
||||
const a = w.state.members[aId]
|
||||
const b = w.state.members[bId]
|
||||
if (!a || !b || !a.alive || !b.alive) return false
|
||||
if (w.ageOf(a) < 16 || w.ageOf(b) < 16) return false
|
||||
if (a.spouseId && !w.isWidowed(a)) return false
|
||||
if (b.spouseId && !w.isWidowed(b)) return false
|
||||
if (a.gender === b.gender) return false
|
||||
if (a.fatherId && a.fatherId === b.fatherId) return false
|
||||
if (a.motherId && a.motherId === b.motherId) return false
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { GameState, SaveMeta, SnapshotMeta, Id } from '../types/domain'
|
||||
|
||||
const DB_PREFIX = 'cotyc-save-'
|
||||
let seqCounter = 0
|
||||
|
||||
export interface SaveDbDriver {
|
||||
open(name: string): Promise<void>
|
||||
@@ -38,7 +39,7 @@ export class SaveSlot {
|
||||
}
|
||||
|
||||
async saveState(state: GameState, label: string): Promise<string> {
|
||||
const id = `${state.year}.${state.month}.${Date.now().toString(36)}`
|
||||
const id = `${state.year}.${state.month}.${Date.now().toString(36)}-${(seqCounter++ % 1296).toString(36)}`
|
||||
const json = JSON.stringify(state)
|
||||
await this.driver.run(
|
||||
`INSERT INTO snapshot (id, year, month, savedAt, label, data) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
@@ -80,8 +81,11 @@ export class SaveSlot {
|
||||
|
||||
async loadState(id?: string): Promise<GameState | null> {
|
||||
const rows = await this.driver.all<{ data: string }>(
|
||||
id ? `SELECT data FROM snapshot WHERE id = ?` : `SELECT data FROM snapshot ORDER BY rowid DESC LIMIT 1`
|
||||
, id ? [id] : [])
|
||||
id
|
||||
? `SELECT data FROM snapshot WHERE id = ?`
|
||||
: `SELECT data FROM snapshot ORDER BY year DESC, month DESC, savedAt DESC LIMIT 1`,
|
||||
id ? [id] : []
|
||||
)
|
||||
if (!rows || rows.length === 0) return null
|
||||
return JSON.parse(rows[0]!.data) as GameState
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ const STATE_LABEL: Record<string, { label: string; cls: string }> = {
|
||||
idle: { label: '无事', cls: '' },
|
||||
meditation: { label: '闭关', cls: 'good' },
|
||||
expedition: { label: '出探', cls: 'warn' },
|
||||
wounded: { label: '养伤', cls: 'bad' }
|
||||
wounded: { label: '养伤', cls: 'bad' },
|
||||
apprentice: { label: '寄读', cls: 'gold' },
|
||||
closed: { label: '养息', cls: '' }
|
||||
}
|
||||
|
||||
export function MemberCard({ c }: { c: Character }) {
|
||||
|
||||
@@ -27,6 +27,11 @@ function deadish(c: Character): boolean {
|
||||
return !c.alive
|
||||
}
|
||||
|
||||
function gradeShort(g: number): string {
|
||||
const map: Record<number, string> = { 0: '凡', 1: '黄', 2: '玄', 3: '地', 4: '天', 5: '仙' }
|
||||
return map[g] ?? '?'
|
||||
}
|
||||
|
||||
function pillCount(s: GameState, id: string): number {
|
||||
return s.family.inventory[id] ?? 0
|
||||
}
|
||||
@@ -65,9 +70,19 @@ export function MemberModal({ member }: { member: Character }) {
|
||||
<div className="mm-line"><span>境界进度</span><b>{describeRealm(member.realm)} {Math.floor(member.realmProgress)}%</b></div>
|
||||
<div className="mm-line"><span>下一关</span><b className={member.realmProgress >= 100 ? 'gold' : ''}>{nextName}</b></div>
|
||||
<div className="mm-line"><span>战力</span><b>{Math.round(power)}</b></div>
|
||||
<div className="mm-line"><span>状态</span><b>{member.state === 'meditation' ? '闭关' : member.state === 'expedition' ? '外出' : member.state === 'wounded' ? '养伤' : '无事'}</b></div>
|
||||
<div className="mm-line"><span>状态</span><b>
|
||||
{member.state === 'meditation'
|
||||
? '闭关'
|
||||
: member.state === 'expedition'
|
||||
? '外出'
|
||||
: member.state === 'wounded'
|
||||
? '养伤'
|
||||
: member.state === 'apprentice'
|
||||
? '寄读'
|
||||
: '无事'}
|
||||
</b></div>
|
||||
<div className="mm-line"><span>气血</span><b>{Math.round(member.health)}</b></div>
|
||||
<div className="mm-line"><span>功法</span><b>{tech ? `${tech.name}(${['黄', '玄', '地', '天', '仙'][tech.grade]}阶)` : member.realm.major === 'mortal' ? '—' : '未习功法'}</b></div>
|
||||
<div className="mm-line"><span>功法</span><b>{tech ? `${tech.name}(${gradeShort(tech.grade)}阶)` : member.realm.major === 'mortal' ? '—' : '未习功法'}</b></div>
|
||||
<div className="mm-line"><span>法宝</span><b>{member.equipment ? `${ITEMS[member.equipment].name}(+${Math.round((ARTIFACT_POWER[member.equipment] ?? 0) * 100)}%)` : '—'}</b></div>
|
||||
<div className="mm-line"><span>姻亲</span><b>{member.spouseId ? s.members[member.spouseId]?.name ?? '' : member.spouseHouse ?? '未婚'}</b></div>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function ChroniclePanel() {
|
||||
const all = [...world.state.chronicle]
|
||||
const filtered = filter === 'all' ? all : all.filter((e) => e.category === filter)
|
||||
return filtered.slice().sort((a, b) => (b.year - a.year) || (b.month - a.month))
|
||||
}, [world, filter])
|
||||
}, [world, filter, revision])
|
||||
if (!world) return null
|
||||
const battles = world.state.battles.slice().reverse()
|
||||
const battle = battles.find((b) => b.id === battleId)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useGameStore } from '../store'
|
||||
import { ITEMS } from '../../game/data/items'
|
||||
import { TECHNIQUES } from '../../game/data/techniques'
|
||||
import { TECHNIQUE_GRADE_NAMES } from '../../game/data/realms'
|
||||
import { techniqueGradeName } from '../../game/data/realms'
|
||||
import { marketPrice, buyItem, sellItem, buyTechnique } from '../../game/engine/market'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
@@ -10,11 +10,11 @@ export default function MarketPanel() {
|
||||
const bump = useGameStore((s) => s.bump)
|
||||
const revision = useGameStore((s) => s.revision)
|
||||
void revision
|
||||
const [tab, setTab] = useState<'goods' | 'tech'>('goods')
|
||||
if (!world) return null
|
||||
const w = world
|
||||
const fam = w.state.family
|
||||
const inv = fam.inventory
|
||||
const [tab, setTab] = useState<'goods' | 'tech'>('goods')
|
||||
|
||||
const goods = useMemo(() => Object.values(ITEMS).filter((i) => i.kind === 'resource' || i.kind === 'pill'), [])
|
||||
const artifacts = useMemo(() => Object.values(ITEMS).filter((i) => i.kind === 'artifact'), [])
|
||||
@@ -110,7 +110,7 @@ export default function MarketPanel() {
|
||||
{tab === 'tech' && (
|
||||
<>
|
||||
<div className="help-text">
|
||||
藏书阁录存法帖,族人可修习。藏书阁等级决定可在坊市搜购的品阶(当前 {cangshuLv} 级,可购至{TECHNIQUE_GRADE_NAMES[Math.min(3, Math.max(0, cangshuLv))]})。
|
||||
藏书阁录存法帖,族人可修习。藏书阁等级决定可在坊市搜购的品阶(当前 {cangshuLv} 级,可购至{techniqueGradeName(Math.min(4, Math.max(0, cangshuLv + 1)))})。
|
||||
若藏书阁未建或等级不足,只可见基础法帖。
|
||||
</div>
|
||||
<table className="market-table">
|
||||
@@ -131,11 +131,11 @@ export default function MarketPanel() {
|
||||
.map((t) => {
|
||||
const owned = fam.techniques.includes(t.id)
|
||||
const p = t.grade <= 1 + cangshuLv
|
||||
const priceT = [120, 300, 700, 1600, 3600][t.grade] ?? 300
|
||||
const priceT = techniqueGradeName(t.grade) === '?' ? 300 : [120, 300, 700, 1600][t.grade] ?? 300
|
||||
return (
|
||||
<tr key={t.id}>
|
||||
<td><b>《{t.name}》</b></td>
|
||||
<td className="dim">{TECHNIQUE_GRADE_NAMES[t.grade]} · {t.path}</td>
|
||||
<td className="dim">{techniqueGradeName(t.grade)} · {t.path}</td>
|
||||
<td className="dim">{t.desc}</td>
|
||||
<td>+{Math.round((t.expBonus) * 100)}%</td>
|
||||
<td>+{Math.round((t.powerBonus) * 100)}%</td>
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function SettingsPanel() {
|
||||
· 「外交」与四邻结好联姻;仇雠之族隔岁来犯,打得赢名望大涨,打不赢蚀钱伤丁。<br />
|
||||
· 「史书」自动记述繁华与凋零——百年之后,后人翻开这一卷家族志,见代代薪火、历历雪泥。
|
||||
</div>
|
||||
<div className="dim2">版本 0.1.0 · Chronicle of the Immortal Clan</div>
|
||||
<div className="dim2">版本 0.1.8 · Chronicle of the Immortal Clan</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ export default function TerritoryPanel() {
|
||||
const bump = useGameStore((s) => s.bump)
|
||||
const revision = useGameStore((s) => s.revision)
|
||||
void revision
|
||||
const ids = useMemo(() => Object.keys(BUILDINGS), [])
|
||||
if (!world) return null
|
||||
const fam = world.state.family
|
||||
const ids = useMemo(() => Object.keys(BUILDINGS), [])
|
||||
|
||||
const levelLabel = (l: number) => '·'.repeat(l) + '。'.repeat(5 - l)
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ export default function NewGame() {
|
||||
|
||||
const rollNames = () => {
|
||||
setRandoming((r) => !r)
|
||||
const s = SURNAME_POOL[Math.floor(Math.random() * SURNAME_POOL.length)]
|
||||
const idx = Math.floor(RngHub.audioNoise01() * SURNAME_POOL.length)
|
||||
const s = SURNAME_POOL[Math.min(SURNAME_POOL.length - 1, idx)]
|
||||
setSurname(s)
|
||||
setFamilyName(`${s}氏`)
|
||||
}
|
||||
|
||||
@@ -110,7 +110,11 @@ export function sGong(): void {
|
||||
tone(660, 0.7, 0.03, 'sine', 0.06)
|
||||
}
|
||||
|
||||
/** 按钮:纸面轻叩 */
|
||||
let lastClick = 0
|
||||
/** 按钮:纸面轻叩(80ms 节流防连点爆 buffer) */
|
||||
export function sClick(): void {
|
||||
const now = performance.now()
|
||||
if (now - lastClick < 80) return
|
||||
lastClick = now
|
||||
noise(0.045, 0.03, 0, 4200)
|
||||
}
|
||||
|
||||
+85
-36
@@ -60,6 +60,7 @@ export interface GameStore {
|
||||
closePaper: () => void
|
||||
toggleSound: () => void
|
||||
facade?: GameFacade
|
||||
advancing: boolean
|
||||
act: (name: ActName, payload: import('../game/engine/api').ActPayload) => boolean
|
||||
}
|
||||
|
||||
@@ -67,8 +68,21 @@ const LOG_CAP = 260
|
||||
const lastPhaseStats = new Map<World, PhaseStat[]>()
|
||||
let logSeq = 1
|
||||
|
||||
let toastTimer: ReturnType<typeof setTimeout> | null = null
|
||||
function setNextToast(msg: string): void {
|
||||
if (toastTimer) clearTimeout(toastTimer)
|
||||
useGameStore.setState({ toast: msg })
|
||||
toastTimer = setTimeout(() => useGameStore.setState({ toast: undefined }), 4200)
|
||||
}
|
||||
function stopTimer(): void {
|
||||
const st = useGameStore.getState()
|
||||
if (st.timer) clearInterval(st.timer)
|
||||
useGameStore.setState({ speed: 0, timer: null })
|
||||
}
|
||||
|
||||
export const useGameStore = create<GameStore>((set, get) => ({
|
||||
facade: undefined as GameFacade | undefined,
|
||||
advancing: false,
|
||||
screen: 'boot',
|
||||
world: null,
|
||||
slot: 1,
|
||||
@@ -124,34 +138,46 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
|
||||
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 })
|
||||
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 st = get()
|
||||
st.world?.out.push(makeBus(st))
|
||||
const facade = new GameFacade(world, slot)
|
||||
set({ facade })
|
||||
await st.saveNow('开局')
|
||||
},
|
||||
|
||||
continueGame: async (slot) => {
|
||||
const manager = getSlotManager()
|
||||
const file = await getSaveSlot(slot)
|
||||
await file.open()
|
||||
const state = await file.loadState()
|
||||
if (!state) throw new Error('找不到存档数据')
|
||||
openState(state, slot)
|
||||
await manager.updateSlotMeta(slot, metaFromState(state, slot))
|
||||
try {
|
||||
const manager = getSlotManager()
|
||||
const file = await getSaveSlot(slot)
|
||||
await file.open()
|
||||
const state = await file.loadState()
|
||||
if (!state) throw new Error('找不到存档数据')
|
||||
openState(state, slot)
|
||||
await manager.updateSlotMeta(slot, metaFromState(state, slot))
|
||||
} catch (e) {
|
||||
setNextToast(`读档失败:${String(e)}`)
|
||||
useGameStore.setState({ screen: 'boot' })
|
||||
}
|
||||
},
|
||||
|
||||
loadSnapshot: async (slot, snapshotId) => {
|
||||
const file = await getSaveSlot(slot)
|
||||
await file.open()
|
||||
const state = await file.loadState(snapshotId)
|
||||
if (!state) throw new Error('找不到快照')
|
||||
openState(state, slot)
|
||||
try {
|
||||
const file = await getSaveSlot(slot)
|
||||
await file.open()
|
||||
const state = await file.loadState(snapshotId)
|
||||
if (!state) throw new Error('找不到快照')
|
||||
openState(state, slot)
|
||||
} catch (e) {
|
||||
setNextToast(`读取快照失败:${String(e)}`)
|
||||
}
|
||||
},
|
||||
|
||||
advance: async () => {
|
||||
const st = get()
|
||||
const w = st.world
|
||||
if (!w || st.pendingEventId || w.state.gameOver) return
|
||||
if (!w || st.pendingEventId || w.state.gameOver || st.advancing) return
|
||||
set({ advancing: true })
|
||||
try {
|
||||
const t0 = performance.now()
|
||||
w.advanceMonth()
|
||||
@@ -175,7 +201,10 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
} catch {
|
||||
// 保存失败时不再叠加错误
|
||||
}
|
||||
set({ toast: '时日流转出现异常,已保存出险现场,可回档重来。', speed: 0 })
|
||||
setNextToast('时日流转出现异常,已保存出险现场,可回档重来。')
|
||||
stopTimer()
|
||||
} finally {
|
||||
set({ advancing: false })
|
||||
}
|
||||
},
|
||||
|
||||
@@ -192,10 +221,21 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
onBattle: (log) => set({ battleView: log, revision: get().revision + 1 }),
|
||||
onBattle: (log) => {
|
||||
stopTimer()
|
||||
set({ battleView: log, revision: get().revision + 1 })
|
||||
},
|
||||
|
||||
onPendingEvent: (id) => {
|
||||
const def = findEvent(id, get().world ?? undefined)
|
||||
if (!def) {
|
||||
// 未知事件(插件卸载/旧档漂移):跳过并解除软锁
|
||||
const w = get().world
|
||||
if (w?.state.pendingEvent === id) w.state.pendingEvent = undefined
|
||||
get().addLog({ id: logSeq++, kind: 'bad', text: `事件「${id}」未知,已跳过。`, year: get().world?.state.year ?? 0, month: get().world?.state.month ?? 0 })
|
||||
set({ pendingEventId: undefined, pendingEventDef: undefined })
|
||||
return
|
||||
}
|
||||
set({ pendingEventId: id, pendingEventDef: def })
|
||||
},
|
||||
|
||||
@@ -253,6 +293,9 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
setSpeed: (v) => {
|
||||
const st = get()
|
||||
if (st.timer) clearInterval(st.timer)
|
||||
if (st.speed === v && v === 0 && false) {
|
||||
// noop
|
||||
}
|
||||
if (v <= 0) {
|
||||
set({ speed: v, timer: null })
|
||||
return
|
||||
@@ -261,9 +304,12 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
if (v >= 3) v = 3
|
||||
const timer = setInterval(() => {
|
||||
const cur = get()
|
||||
if (!cur.pendingEventId && cur.world && !cur.world.state.gameOver) {
|
||||
void cur.advance()
|
||||
const w = cur.world
|
||||
if (cur.speed <= 0 || !w || cur.pendingEventId || cur.battleView || cur.advancing || w.state.gameOver) {
|
||||
// 速度被暂停/事件待决/战报展开/在途推进时静默轮空
|
||||
return
|
||||
}
|
||||
void cur.advance()
|
||||
}, ms)
|
||||
set({ speed: v, timer })
|
||||
},
|
||||
@@ -282,25 +328,27 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
const json = await file.exportAll()
|
||||
const meta = metaFromState(st.world.state, st.slot)
|
||||
const r = await window.api.exportSave(json, `${meta.name}-年${meta.year}年`)
|
||||
if (r.ok) set({ toast: '存档已导出。' })
|
||||
else set({ toast: `导出失败:${r.error ?? ''}` })
|
||||
if (r.ok) setNextToast('存档已导出。')
|
||||
else setNextToast(`导出失败:${r.error ?? ''}`)
|
||||
},
|
||||
|
||||
importSave: async (slot) => {
|
||||
if (!window.api) return
|
||||
const r = await window.api.importSave()
|
||||
if (!r.ok || !r.text) {
|
||||
set({ toast: `导入失败:${r.error ?? ''}` })
|
||||
setNextToast(`导入失败:${r.error ?? ''}`)
|
||||
return
|
||||
}
|
||||
const file = await getSaveSlot(slot)
|
||||
await file.open()
|
||||
const ok = await file.importAll(r.text)
|
||||
if (ok) {
|
||||
set({ toast: '导入成功,重新读取存档槽。' })
|
||||
setNextToast('导入成功,重新读取存档槽。')
|
||||
await get().refreshSlots()
|
||||
const state = await file.loadState()
|
||||
if (state) openState(state, slot)
|
||||
} else {
|
||||
set({ toast: '导入失败:文件格式不正确。' })
|
||||
setNextToast('导入失败:文件格式不正确。')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -326,8 +374,8 @@ function openState(state: GameState, slot: number): void {
|
||||
const st = useGameStore.getState()
|
||||
world.out.push(makeBus(st))
|
||||
const facade = new GameFacade(world, slot)
|
||||
world.out.push(facadebus(facade))
|
||||
useGameStore.setState({
|
||||
facade,
|
||||
world,
|
||||
slot,
|
||||
screen: 'game',
|
||||
@@ -338,8 +386,20 @@ function openState(state: GameState, slot: number): void {
|
||||
pendingEventDef: undefined,
|
||||
revision: 1,
|
||||
speed: 0,
|
||||
gameOverReason: state.gameOver?.reason
|
||||
gameOverReason: state.gameOver?.reason || undefined,
|
||||
paperReport: undefined,
|
||||
toast: undefined,
|
||||
selectedMemberId: undefined,
|
||||
selectedMissionDef: undefined
|
||||
})
|
||||
if (state.pendingEvent && state.members) {
|
||||
const def = findEvent(state.pendingEvent, world)
|
||||
if (def) {
|
||||
useGameStore.setState({ pendingEventId: state.pendingEvent, pendingEventDef: def })
|
||||
} else {
|
||||
world.state.pendingEvent = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeBus(st: GameStore): WorldEventBus {
|
||||
@@ -363,17 +423,6 @@ 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()
|
||||
|
||||
Reference in New Issue
Block a user