Files
ChronicleOfTheImmortalClan/src/renderer/game/engine/world.ts
T
thzxx 9bcb5d6af8 v0.1.11: 全源码审计 + 缺陷歼灭 + UI/UX 打磨
【引擎审计修复】
- P0:大比 once(原整年刷 12 场、奖励通胀);flag 年份键 3 年剪枝(长线膨胀)
- P1:飞升离世累加 feishengCount(飞升之资称号可达);圣者护族接消费(原死代码);
  大比/传薪挂能力卡门控;pending 抢渡劫时不再重设
- P2:version 硬编码统一 0.1.11;yearaxis 飞升归类;MonumentList 挂载

【UI 审计修复】
- P0:已故成员卡显示「已殁」(原秀活动 status);空队迎战禁用+提示(原静默失效)
- P1:指婚候选去截断(滚动列表);终局出口(回卷/主菜单按钮);saveNow 反馈;
  事件来临写入 feed(k-event 使用);遣队「最强 N 人/清空」一键;寻衅确认+联姻原因提示;
  未建建筑视觉 uc 类
- P2:speed 死代码;Modal 分层 z-index;FamilyPanel 在世/先人分隔

【UX 三件套】
- 警讯徽章(瓶颈/重伤/在外/媒缘/孤嗣之忧)顶栏一屏秒答「我该干嘛」
- 首启引导任务条(四步目标 + 完成推进,可跳不强制)
- 顶栏丹药瘦身(4 资源);数字千/万化;瓶颈卡红框高亮

【测试】905→923(urgency/guide/format 三模块 + 审计修复回归 6 项)
金钟罩三档重固化(0.1.11),typecheck/build 通过
2026-08-23 11:30:16 +08:00

714 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
BattleLog,
Character,
ChronicleEntry,
GameState,
Id,
LogItem,
Realm,
YearlyReport
} from '../types/domain'
import { Rng } from '../core/rng'
import { BUILDINGS } from '../data/buildings'
import { POSTS } from '../data/posts'
import { aspirationById as aspirationOf } from '../data/aspirations'
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../core/legacy'
import { createWorldState, findInheritor } from './creation'
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'
export type LogKind = LogItem['kind']
export interface WorldEventBus {
onLog(kind: LogKind, text: string): void
onChronicle(entry: ChronicleEntry, important: boolean): void
onBattle(log: BattleLog): void
onPendingEvent(id: string): void
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 {
// 老版本存档(<0.1.1)缺少新增字段,加载时补齐,避免运行期 undefined 崩溃
if (!state.finance) state.finance = { accum: 0 }
if (!state.yearStats) state.yearStats = { births: 0, deaths: 0 }
if (!state.yearlyReports) state.yearlyReports = []
if (!state.stats) {
state.stats = {
repPeak: state.family?.reputation ?? 0,
popPeak: Object.values(state.members).filter((c) => c.alive).length,
maxRealmIdx: peakRealmIndex(state.members ?? {}),
techniqueGrand: grandTechniqueCount(state.members ?? {}),
tourneyHistory: [],
feishengCount: 0
}
}
if (typeof state.totalTicks !== 'number') state.totalTicks = 0
if (typeof state.seq !== 'number') state.seq = 10
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
if (typeof c.health !== 'number') c.health = 100
}
return state
}
export class World {
state: GameState
rng: Rng
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 = 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 {
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 {
this.state.seq++
return `x${this.state.seq.toString(36)}`
}
syncRng(): void {
this.state.rng = this.rng.getState()
}
log(kind: LogKind, text: string): void {
this.out.forEach((o) => o.onLog(kind, text))
}
chronicle(cat: ChronicleEntry['category'], text: string, memberId?: Id, important = false): void {
const entry: ChronicleEntry = {
id: this.seq(),
year: this.state.year,
month: this.state.month,
category: cat,
text,
memberId,
important
}
this.state.chronicle.push(entry)
this.out.forEach((o) => o.onChronicle(entry, important))
}
battle(log: BattleLog): void {
this.state.battles.push(log)
this.out.forEach((o) => o.onBattle(log))
}
pendingEvent(id: string): void {
this.out.forEach((o) => o.onPendingEvent(id))
}
gameOver(reason: string, year: number): void {
this.state.gameOver = { year, reason }
this.out.forEach((o) => o.onGameOver(reason, year))
}
gameOver___placeholder(): void {
void 0
}
memberById(id: Id): Character {
const c = this.state.members[id]
if (!c) throw new Error(`member not found ${id}`)
return c
}
aliveMembers(): Character[] {
return Object.values(this.state.members).filter((c) => c.alive)
}
ageOf(c: Character): number {
return this.state.year - c.bornYear
}
head(): Character {
return this.memberById(this.state.family.headId)
}
advanceMonth(): void {
const s = this.state
const stonesStart = s.family.stones
s.month++
if (s.month > 12) {
s.month = 1
s.year++
this.clock.fireYearStart(this)
}
s.totalTicks++
this.clock.stepMonthly(this)
const stonesEnd = s.family.stones
s.finance.accum += stonesEnd - stonesStart
this.trackStats()
this.clampState()
this.pruneYearFlags()
}
/** 年度清理:剔除 3 年以前的年份前缀 flag 键(防长线膨胀) */
private pruneYearFlags(): void {
const fam = this.state.family
const cutoff = this.state.year - 3
for (const key of Object.keys(fam.flag)) {
const m = /^(auction-|prayerDone-|echoDone-|recruitDone-)(\d+)$/.exec(key)
if (m && Number(m[2]) < cutoff) delete fam.flag[key]
}
}
/** 月底统一数值钳制:修为/气血/库存/金钱永不越界 */
private clampState(): void {
for (const c of Object.values(this.state.members)) {
if (c.realmProgress < 0) c.realmProgress = 0
if (c.realmProgress > 100) c.realmProgress = 100
if (c.health < 0) c.health = 0
if (c.health > 100) c.health = 100
}
if (this.state.family.stones < 0) this.state.family.stones = 0
for (const [k, v] of Object.entries(this.state.family.inventory)) {
if (typeof v === 'number' && v < 0) this.state.family.inventory[k] = 0
}
}
private trackStats(): void {
const s = this.state
const st = s.stats
if (s.family.reputation > st.repPeak) st.repPeak = s.family.reputation
const pop = this.aliveMembers().length
if (pop > st.popPeak) st.popPeak = pop
const idx = peakRealmIndex(s.members)
if (idx > st.maxRealmIdx) st.maxRealmIdx = idx
const g = grandTechniqueCount(s.members)
if (g > st.techniqueGrand) st.techniqueGrand = g
}
legacyPreview() {
return computeLegacy(this.state)
}
resolveLegacyNow(): LegacyArch {
const arch = resolveLegacy(this.state)
this.state.stats.resolvedYear = this.state.year
this.state.stats.resolveTitle = arch.title
this.state.family.flag['resolved'] = true
this.chronicle('event', `望气观澜,本族百年气数终有定论——「${arch.title}」。开卷盖印,史入青册。`, undefined, true)
this.log('good', `定鼎:${arch.title}。`)
return arch
}
publishYearReport(): void {
const rep = this.state.family.reputation
const power = this.familyPower()
const report: YearlyReport = {
year: this.state.year - 1,
nets: this.state.finance.accum,
births: this.state.yearStats.births,
deaths: this.state.yearStats.deaths,
rep,
power
}
this.state.yearlyReports.push(report)
if (this.state.yearlyReports.length > 80) {
this.state.yearlyReports.shift()
}
this.state.finance.accum = 0
this.state.yearStats = { births: 0, deaths: 0 }
this.out.forEach((o) => o.onYearPaper?.(report))
}
epilogueTick(): void {
if (this.state.gameOver) return
this.checkHead()
}
reputationDrift(): void {
const cur = this.state.family.reputation
const drift = cur > 0 ? -1.5 : cur < 0 ? 1.2 : 0
if (drift !== 0) this.state.family.reputation = Math.round(cur + drift)
}
totalFamilyReputation(): number {
return this.state.family.reputation
}
private checkHead(): void {
const s = this.state
if (s.gameOver) return
const headId = s.family.headId
if (!headId) return
const head = this.memberById(headId)
if (head.alive) return
// 功德碑:宗主薨,勒石纪功
const reignStart = (s.family.flag['reignStart'] as number | undefined) ?? 1
const reignYears = Math.max(1, s.year - reignStart)
const peak = s.family.reputation
const top = this.aliveMembers().length
this.chronicle(
'misc',
`功德碑:先主${head.name}承宗${reignYears}载,宗族声望达「${peak}」、丁口${top}。族人勒石铭功,立于宗祠。`,
head.id,
true
)
const heir = findInheritor(this)
if (heir) {
this.assignHead(heir.id, true)
s.family.flag['reignStart'] = s.year
} else if (this.aliveMembers().length === 0) {
this.gameOver('满门凋零,香火断绝', s.year)
}
}
// ==================== player actions ====================
assignHead(id: Id, silent = false): void {
const c = this.memberById(id)
if (!c.alive) return
if (this.state.family.headId && !silent) {
const old = this.memberById(this.state.family.headId)
old.isHead = false
} else {
const oldId = this.state.family.headId
if (oldId && this.state.members[oldId]) this.state.members[oldId].isHead = false
}
c.isHead = true
this.state.family.headId = id
this.state.family.flag['reignStart'] = this.state.year
if (!silent) {
this.chronicle('misc', `${c.name} 继任为家主。`, c.id, true)
this.log('info', `${c.name} 继任为家主。`)
}
}
setMeditation(id: Id, on: boolean): void {
const c = this.memberById(id)
if (!c.alive || c.state === 'expedition') return
c.state = on ? 'meditation' : 'idle'
}
giveTechnique(memberId: Id, techId: string): void {
const c = this.memberById(memberId)
c.techniqueId = techId
}
teachTechnique(techId: string, cost: number): boolean {
const fam = this.state.family
if (fam.techniques.includes(techId)) return false
if (fam.stones < cost) return false
fam.stones -= cost
fam.techniques.push(techId)
this.log('info', `藏书阁续得《${techId}》,译作一名。`)
return true
}
equip(memberId: Id, artifact: string): void {
const c = this.memberById(memberId)
if (!c.alive) return
c.equipment = artifact
}
takePill(memberId: Id, pill: string): void {
const c = this.memberById(memberId)
const inv = this.state.family.inventory
if (!c.alive || (inv[pill] ?? 0) <= 0) return
inv[pill] = inv[pill]! - 1
if (pill === 'pill-pojing') {
if (c.realmProgress >= 100) {
this.resolveBottleneck(c, 0.22)
} else {
this.memberById(memberId).realmProgress = Math.min(100, c.realmProgress + 20)
this.log('info', `${c.name} 服下破境丹,灵力充盈。`)
}
} else {
const pct = pill === 'pill-qiyuan' ? 18 : 30
c.realmProgress = Math.min(100, c.realmProgress + pct)
this.log('info', `${c.name} 服下丹药,修为精进。`)
}
}
assistedBreakthrough(id: Id): void {
const c = this.memberById(id)
if (!c.alive || c.realmProgress < 100) return
this.resolveBottleneck(c, 0.06 + this.head().mind * 0.005)
}
private resolveBottleneck(c: Character, boost: number): void {
resolveBreakthrough(this, c, boost)
}
build(id: string): boolean {
const fam = this.state.family
const def = BUILDINGS[id]
if (!def) return false
if (fam.buildings[id]) return false
const cost = def.upgradeCost(1)
if (fam.stones < cost.stones) return false
fam.stones -= cost.stones
fam.buildings[id] = 1
this.chronicle('building', `建成「${def.name}」。`, undefined, true)
this.log('info', `建成「${def.name}」。`)
return true
}
upgrade(id: string): boolean {
const fam = this.state.family
const def = BUILDINGS[id]
const lvl = fam.buildings[id]
if (!def || !lvl || lvl >= def.maxLevel) return false
const cost = def.upgradeCost(lvl + 1)
if (fam.stones < cost.stones || (fam.inventory['lingkuang'] ?? 0) < cost.lingkuang) return false
fam.stones -= cost.stones
fam.inventory['lingkuang'] -= cost.lingkuang
fam.buildings[id] = lvl + 1
this.log('info', `「${def.name}」升至 ${lvl + 1} 级。`)
return true
}
craftPill(kind: 'qiyuan' | 'ningyuan'): boolean {
const fam = this.state.family
const lvl = fam.buildings['danfang']
if (!lvl) return false
const cost = kind === 'qiyuan'
? { lingcao: 15, beastcore: 0, stones: 20 }
: { lingcao: 25, beastcore: 4, stones: 60 }
if ((fam.inventory['lingcao'] ?? 0) < cost.lingcao) return false
if ((fam.inventory['beastcore'] ?? 0) < cost.beastcore) return false
if (fam.stones < cost.stones) return false
fam.inventory['lingcao'] -= cost.lingcao
fam.inventory['beastcore'] -= cost.beastcore
fam.stones -= cost.stones
fam.inventory[kind === 'qiyuan' ? 'pill-qiyuan' : 'pill-ningyuan'] =
(fam.inventory[kind === 'qiyuan' ? 'pill-qiyuan' : 'pill-ningyuan'] ?? 0) + 1
this.log('info', `丹房炼成一枚${kind === 'qiyuan' ? '聚气丹' : '凝元丹'}。`)
return true
}
addMember(c: Character, father?: Character, mother?: Character): void {
const s = this.state
if (father || mother) {
if (father) {
c.fatherId = father.id
father.children.push(c.id)
}
if (mother) mother.children.push(c.id)
}
c.id = c.id || this.seq()
s.members[c.id] = c
}
postCount(def: string): number {
return this.aliveMembers().filter((c) => c.post === def && c.state !== 'apprentice').length
}
assignPost(memberId: Id, postId: string | undefined): boolean {
const c = this.memberById(memberId)
if (!c.alive || c.state === 'apprentice') return false
if (postId === undefined || postId === '') {
c.post = undefined
return true
}
const def = POSTS[postId]
if (!def || def.id === 'head') return false
if (this.postCount(postId) >= def.max) return false
c.post = postId
return true
}
postBonus(type: string): number {
let sum = 0
for (const c of this.aliveMembers()) {
const def = POSTS[c.post ?? '']
if (def && def.effect.type === type) sum += def.effect.value
}
return sum
}
familyPower(): number {
const fam = this.state.family
const bonus =
1 +
(fam.buildings['yanwu'] ?? 0) * 0.04 +
(fam.buildings['lingshou'] ?? 0) * 0.05 +
this.postBonus('battlePower')
const top = this.aliveMembers()
.map((c) => combatPowerOf(this, c))
.sort((a, b) => b - a)
.slice(0, 4)
.reduce((a, b) => a + b, 0)
return Math.round(top * bonus)
}
ancestralRite(): boolean {
const fam = this.state.family
const last = (fam.flag['lastRiteYear'] as number | undefined) ?? -999
if (this.state.year - last < 2) return false
if (fam.stones < 150) return false
fam.stones -= 150
fam.flag['lastRiteYear'] = this.state.year
fam.reputation += 6
for (const c of this.aliveMembers()) {
c.realmProgress = Math.min(100, c.realmProgress + 3)
c.health = Math.min(100, c.health + 5)
}
const headName = this.head()?.name ?? '家主'
this.chronicle('event', `${headName} 斋戒三日后开祠祭祖,先祖显灵赐福。`, undefined, true)
this.log('good', `祭祖!族中众人灵力温润,族人受益。`)
return true
}
seekSutra(): boolean {
const fam = this.state.family
if ((fam.buildings['cangshu'] ?? 0) < 3) return false
const last = (fam.flag['sutraCD'] as number | undefined) ?? -999
if (this.state.year - last < 2) return false
if (fam.stones < 150) return false
fam.stones -= 150
fam.flag['sutraCD'] = this.state.year
const pool = pack().techniques.filter((t) => t.grade >= 2 && !fam.techniques.includes(t.id))
if (pool.length === 0) {
this.log('info', '求经访道:天下典籍已入庶几,无可再得。')
this.chronicle('event', '求经台广搜天下,经卷已穷。', undefined, false)
return true
}
const t = this.rng.pick(pool)
fam.techniques.push(t.id)
this.chronicle('event', `遣人下江南求经,携回《${t.name}》。`, undefined, true)
this.log('good', `求经台访得《${t.name}》!`)
return true
}
tauntNpc(npcId: string): boolean {
const fam = this.state.family
const npc = this.state.npcFamilies[npcId]
if (!npc) return false
const last = (fam.flag[`tauntCD-${npcId}`] as number | undefined) ?? 0
if (this.state.year - last < 1) return false
fam.flag[`tauntCD-${npcId}`] = this.state.year
npc.relation = Math.max(-100, npc.relation - 20)
this.log('bad', `指桑骂槐,${npc.name}记恨于心。`)
return true
}
isWidowed(member: Character | Id): boolean {
const m = typeof member === 'string' ? this.state.members[member] : member
if (!m || !m.spouseId) return false
const sp = this.state.members[m.spouseId]
return !!sp && !sp.alive
}
marriageCandidatesOf(id: Id): Character[] {
const me = this.memberById(id)
const meG = me.gender
return this.aliveMembers()
.filter((c) => c.gender !== meG && c.state !== 'expedition' && c.state !== 'apprentice')
.filter((c) => w2age(this, c) >= 16 && w2age(this, c) <= 46)
.filter((c) => !c.spouseId || this.isWidowed(c))
.filter(
(c) =>
!(me.fatherId && me.fatherId === c.fatherId) &&
!(me.motherId && me.motherId === c.motherId) &&
!me.children.includes(c.id) &&
!c.children.includes(me.id) &&
me.id !== c.id
)
}
canMarry(memberId: Id): boolean {
const me = this.memberById(memberId)
if (!me.alive) return false
return !me.spouseId || this.isWidowed(me)
}
marryTo(aId: Id, bId: Id): boolean {
return arrangeWeddingBridge(this, aId, bId)
}
static create(opts: { seed: string; surname: string; familyName: string; motto: string; difficulty: 'easy' | 'normal' | 'hard' }): World {
const state = createWorldState(opts)
return new World(state)
}
}
export function makeWorldFromSave(state: GameState): World {
return new World(state, [])
}
function w2age(w: World, c: Character): number {
return w.ageOf(c)
}
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
a.spouseId = b.id
b.spouseId = a.id
const aAge = w.ageOf(a)
const bAge = w.ageOf(b)
w.chronicle('marriage', `${a.name}${aAge})与${b.name}${bAge})拜堂成亲。`, a.id, true)
w.log('good', `${a.name}${b.name} 结为连理。`)
return true
}
export function applyChoice(world: World, eventId: string, optionIdx: number): void {
applyEventChoice(world, eventId, optionIdx)
}