refactor(0.1.14-P1): 内核归位——game/ 按引擎架构重排(零行为漂移)
结构(旧 game/core+engine → 新 engine/ 域): - engine/kernel/ 时钟/随机/插件协议/fxqueue/timesense/format/urgency/guide/names(原 core) - engine/narrative/ legacy/报告/列传/谱系/年轴(原 core 叙事族) - engine/runtime/ World/creation/pcgen/ApiFacade/capabilities/pluginManager/boot/clocks + Systems/*(12 系统) - engine/sim/ Market(未来 WorldSim 同行) - 旧 game/core、engine/systems、engine/world.ts 等路径全部废弃(无 re-export 兼容层) 验证:35 套件/967 测试全绿(金钟罩三档零漂移=纯搬迁无行为变化) typecheck 0 error
This commit is contained in:
@@ -0,0 +1,719 @@
|
||||
import {
|
||||
BattleLog,
|
||||
Character,
|
||||
ChronicleEntry,
|
||||
GameState,
|
||||
Id,
|
||||
LogItem,
|
||||
Realm,
|
||||
YearlyReport
|
||||
} from '../../types/domain'
|
||||
import { Rng } from '../kernel/rng'
|
||||
import { BUILDINGS } from '../../data/buildings'
|
||||
import { POSTS } from '../../data/posts'
|
||||
import { aspirationById as aspirationOf } from '../../data/aspirations'
|
||||
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../narrative/legacy'
|
||||
import { createWorldState, findInheritor } from './creation'
|
||||
import { SYSTEM_DEFS, SystemDef } from './capabilities'
|
||||
import { emptyClock } from './clocks'
|
||||
import { CotycPlugin, PluginContext, PluginStatus } from '../kernel/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 '../kernel/clock'
|
||||
import { SystemHook } from '../kernel/clock'
|
||||
import { resolveBreakthrough } from './Systems/cultivation'
|
||||
import { applyEventChoice } from './Systems/events'
|
||||
import { combatPowerOf } from './Systems/combat'
|
||||
|
||||
export type LogKind = LogItem['kind']
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
requeueEvent(eventId: string): void {
|
||||
const s = this.state
|
||||
if (!s.eventQueue.includes(eventId)) s.eventQueue.push(eventId)
|
||||
s.pendingEvent = undefined
|
||||
}
|
||||
|
||||
/** 年度清理:剔除 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)
|
||||
}
|
||||
Reference in New Issue
Block a user