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,77 @@
|
||||
import { GameState } from '../../types/domain'
|
||||
import { TECHNIQUES } from '../../data/techniques'
|
||||
import { describeRealm } from '../../data/realms'
|
||||
import { aspirationById } from '../../data/aspirations'
|
||||
import { postById } from '../../data/posts'
|
||||
|
||||
export interface BioLine {
|
||||
at?: number
|
||||
label: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export function buildBiography(s: GameState, memberId: string): BioLine[] {
|
||||
const c = s.members[memberId]
|
||||
if (!c) return []
|
||||
const lines: BioLine[] = []
|
||||
const tech = TECHNIQUES.find((t) => t.id === c.techniqueId)
|
||||
const asp = aspirationById(c.aspiration)
|
||||
const post = postById(c.post)
|
||||
|
||||
lines.push({
|
||||
label: '生平',
|
||||
text: `${c.name},第${c.generation}代子孙${c.gender === 'male' ? '男' : '女'},生于${c.bornYear}年${
|
||||
c.deathYear ? `,殁于${c.deathYear}年(${c.deathCause ?? '寿终'})` : ',今尚在世'
|
||||
}。${post ? `曾居族中「${post.name}」之位。` : ''}${asp ? `平生志向:${asp.name}。` : ''}${
|
||||
tech ? `主修《${tech.name}》。` : ''
|
||||
}`
|
||||
})
|
||||
|
||||
// 突破脉络
|
||||
const breaks = s.chronicle.filter((e) => e.category === 'breakthrough' && e.memberId === c.id)
|
||||
const trials = s.chronicle.filter((e) => e.text.includes(c.name) && (e.category === 'battle' || e.category === 'exploration'))
|
||||
for (const e of breaks) {
|
||||
lines.push({ at: e.year, label: '修行', text: `${e.year}年${e.month}月,${e.text.replace(new RegExp(`^${c.name}\\s*`), '')}`})
|
||||
}
|
||||
if (trials.length > 0) {
|
||||
lines.push({ at: trials[0].year, label: '历险', text: `${trials[0].year}年·${trials[0].text}` })
|
||||
}
|
||||
|
||||
// 婚育
|
||||
const marriages = s.chronicle.filter((e) => e.category === 'marriage' && e.memberId === c.id)
|
||||
for (const m of marriages) {
|
||||
lines.push({ at: m.year, label: '姻缘', text: m.text })
|
||||
}
|
||||
if (c.children.length > 0) {
|
||||
const kids = c.children.map((id) => s.members[id]?.name ?? '?').join('、')
|
||||
lines.push({ label: '子嗣', text: `抚育子女${c.children.length}人:${kids}。` })
|
||||
}
|
||||
|
||||
// 大比
|
||||
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))
|
||||
})
|
||||
for (const t of hisTourneys) {
|
||||
lines.push({ at: t.year, label: '大比', text: `${t.year}年,随队参加太虚大比,列第${t.rank}名。` })
|
||||
}
|
||||
|
||||
// 墓碑铭文
|
||||
if (!c.alive) {
|
||||
const epitaph = EPITAPH[c.deathCause ?? '寿终'] ?? '一尘一土,终归青山。'
|
||||
lines.push({ label: '墓志', text: `${c.name}之墓。${epitaph}` })
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
const EPITAPH: Record<string, string> = {
|
||||
寿终: '一尘一土,终归青山。子孙焚香,岁岁在此。',
|
||||
'寿元将尽': '一尘一土,终归青山。子孙焚香,岁岁在此。',
|
||||
幼夭: '稚子长眠,天地垂爱。家人悬灯,常照归路。',
|
||||
'伤势不治': '壮士折戟,魂兮归来。家祠之下,与祖同列。',
|
||||
'突破走火': '一念求道,九死未悔。灵前法灯,为君长明。',
|
||||
'渡劫陨落': '雷霆战天,其志未竟。遗骨归山,英风尚在。',
|
||||
'战殁': '马革裹尸,归葬桑梓。长剑挂壁,子孙相传。',
|
||||
'云游飞升': '此去云深不知处,魂同列宿游太虚。'
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Character } from '../../types/domain'
|
||||
import type { World } from '../runtime/World'
|
||||
|
||||
export interface FamilyUnit {
|
||||
parents: [Character | undefined, Character | undefined]
|
||||
children: Character[]
|
||||
gen: number
|
||||
}
|
||||
|
||||
export interface GenRow {
|
||||
gen: number
|
||||
singles: Character[]
|
||||
units: FamilyUnit[]
|
||||
}
|
||||
|
||||
export function computeGenealogy(w: World): GenRow[] {
|
||||
const alive = w.aliveMembers()
|
||||
const byGen = new Map<number, Character[]>()
|
||||
for (const c of alive) {
|
||||
const g = byGen.get(c.generation) ?? []
|
||||
g.push(c)
|
||||
byGen.set(c.generation, g)
|
||||
}
|
||||
const gens = [...byGen.keys()].sort((a, b) => a - b)
|
||||
const rows: GenRow[] = []
|
||||
|
||||
for (const gen of gens) {
|
||||
const members = byGen.get(gen)!
|
||||
const units: FamilyUnit[] = []
|
||||
const used = new Set<string>()
|
||||
|
||||
for (const c of members) {
|
||||
if (used.has(c.id)) continue
|
||||
const spouse = c.spouseId ? w.state.members[c.spouseId] : undefined
|
||||
if (spouse && spouse.alive && !used.has(spouse.id)) {
|
||||
used.add(c.id)
|
||||
used.add(spouse.id)
|
||||
const children = [...c.children, ...spouse.children]
|
||||
.filter((id, i, arr) => arr.indexOf(id) === i)
|
||||
.map((id) => w.state.members[id])
|
||||
.filter((ch): ch is Character => !!ch && ch.alive)
|
||||
.sort((a, b) => a.bornYear - b.bornYear)
|
||||
units.push({ parents: [c, spouse], children, gen })
|
||||
}
|
||||
}
|
||||
|
||||
const singles = members.filter((c) => !used.has(c.id))
|
||||
rows.push({ gen, singles, units })
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
export function countDead(w: World): number {
|
||||
return Object.values(w.state.members).filter((c) => !c.alive).length
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { GameState } from '../../types/domain'
|
||||
import { MAJOR_ORDER } from '../../data/realms'
|
||||
|
||||
export interface LegacyDims {
|
||||
renXing: number
|
||||
daoXing: number
|
||||
weiMing: number
|
||||
xiangHuo: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface LegacyArch {
|
||||
title: string
|
||||
poem: string[]
|
||||
dims: LegacyDims
|
||||
rank: number
|
||||
verdict: string
|
||||
}
|
||||
|
||||
export const VERDICTS: Record<string, { title: string; poem: string[] }> = {
|
||||
feisheng: {
|
||||
title: '飞升之资',
|
||||
poem: ['海内升龙真气象,仙班亦录旧香名。', '遥看青山埋骨处,一脉烟霞送飞鸿。']
|
||||
},
|
||||
celestial: {
|
||||
title: '仙朝遗脉',
|
||||
poem: ['祖庭灯火三千里,曾照中天玉斗垂。', '后世儿孙开卷处,犹闻钟鼎旧清音。']
|
||||
},
|
||||
noble: {
|
||||
title: '中州望族',
|
||||
poem: ['钟鸣鼎食三百年,莫道朱门无圣贤。', '一炷心香传世绪,青灯犹自照芸编。']
|
||||
},
|
||||
warlord: {
|
||||
title: '一方枭雄',
|
||||
poem: ['剑气纵横镇八荒,家声不堕尽金汤。', '而今若问兴亡事,半壁江山问晚霜。']
|
||||
},
|
||||
recluse: {
|
||||
title: '隐世大族',
|
||||
poem: ['不向尘沙争利名,青山深处课桑耕。', '子孙自有莲舟意,一棹烟波到洞庭。']
|
||||
},
|
||||
modest: {
|
||||
title: '积余之家',
|
||||
poem: ['檐下燕飞春复秋,园蔬新雨补梁楸。', '粗茶淡饭家声在,犹胜浮云逐海流。']
|
||||
},
|
||||
dust: {
|
||||
title: '冢中枯骨',
|
||||
poem: ['一度花开一度尘,纸灰飞作旧人魂。', '若教重理当年事,且把残篇问故园。']
|
||||
}
|
||||
}
|
||||
|
||||
const RANK_MAP: [number, string][] = [
|
||||
[160, 'celestial'],
|
||||
[120, 'noble'],
|
||||
[85, 'warlord'],
|
||||
[55, 'recluse'],
|
||||
[30, 'modest']
|
||||
]
|
||||
|
||||
export function computeLegacy(s: GameState): LegacyDims {
|
||||
const st = s.stats
|
||||
const gens = Math.max(
|
||||
...Object.values(s.members).map((c) => c.generation).concat(1)
|
||||
)
|
||||
const renXing = Math.round(gens * 18 + Math.min(st.popPeak, 40) * 1.6)
|
||||
const daoXing = Math.round(st.maxRealmIdx * 26 + st.techniqueGrand * 4)
|
||||
const weiMing = Math.round(st.repPeak * 1.4 + (st.tourneyBest ? (6 - st.tourneyBest) * 12 : 0))
|
||||
const xiangHuo = Math.round(Math.min(s.year, 300) * 0.9 + st.feishengCount * 60)
|
||||
return { renXing, daoXing, weiMing, xiangHuo, total: renXing + daoXing + weiMing + xiangHuo }
|
||||
}
|
||||
|
||||
export function resolveLegacy(s: GameState): LegacyArch {
|
||||
const dims = computeLegacy(s)
|
||||
let key = 'dust'
|
||||
if (s.stats.feishengCount > 0) key = 'feisheng'
|
||||
else {
|
||||
for (const [need, k] of RANK_MAP) {
|
||||
if (dims.total >= need) {
|
||||
key = k
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
const verdict = VERDICTS[key]
|
||||
return { title: verdict.title, poem: verdict.poem, dims, rank: dims.total, verdict: key }
|
||||
}
|
||||
|
||||
export function peakRealmIndex(members: GameState['members']): number {
|
||||
let max = -1
|
||||
for (const c of Object.values(members)) {
|
||||
const idx = MAJOR_ORDER.indexOf(c.realm.major) * 10 + c.realm.minor
|
||||
if (idx > max) max = idx
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
export function grandTechniqueCount(members: GameState['members']): number {
|
||||
let n = 0
|
||||
for (const c of Object.values(members)) {
|
||||
if ((c.techniqueRank ?? 0) >= 2) n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { GameState } from '../../types/domain'
|
||||
|
||||
export interface LegacyReport {
|
||||
years: number[]
|
||||
population: number[]
|
||||
reputation: number[]
|
||||
power: number[]
|
||||
deathCauses: Record<string, number>
|
||||
breakthroughs: { year: number; count: number }[]
|
||||
tourneys: { year: number; rank: number }[]
|
||||
genSpan: { gen: number; from: number; to: number }[]
|
||||
totalYears: number
|
||||
}
|
||||
|
||||
export function buildReport(s: GameState): LegacyReport {
|
||||
const reports = s.yearlyReports.slice().sort((a, b) => a.year - b.year)
|
||||
const years = reports.map((r) => r.year)
|
||||
const population = reports.map((r) => r.births - r.deaths)
|
||||
const reputation = reports.map((r) => r.rep)
|
||||
const power = reports.map((r) => r.power)
|
||||
|
||||
const deathCauses: Record<string, number> = {}
|
||||
for (const e of s.chronicle) {
|
||||
if (e.category !== 'death') continue
|
||||
const cause = (e.text.match(/(寿元将尽|幼夭|伤势不治|突破走火|渡劫陨落|战殁|云游飞升)/)?.[0] ?? e.text.split(',')[0] ?? '其他').slice(0, 14)
|
||||
deathCauses[cause] = (deathCauses[cause] ?? 0) + 1
|
||||
}
|
||||
|
||||
const btCount = new Map<number, number>()
|
||||
for (const e of s.chronicle) {
|
||||
if (e.category === 'breakthrough') btCount.set(e.year, (btCount.get(e.year) ?? 0) + 1)
|
||||
}
|
||||
const breakthroughs = [...btCount.entries()].map(([year, count]) => ({ year, count })).sort((a, b) => a.year - b.year)
|
||||
|
||||
const members = Object.values(s.members)
|
||||
const genSpan: LegacyReport['genSpan'] = []
|
||||
const byGen = new Map<number, { min: number; max: number }>()
|
||||
for (const c of members) {
|
||||
const span = byGen.get(c.generation) ?? { min: c.bornYear, max: c.bornYear }
|
||||
span.min = Math.min(span.min, c.bornYear)
|
||||
span.max = Math.max(span.max, c.deathYear ?? s.year)
|
||||
byGen.set(c.generation, span)
|
||||
}
|
||||
for (const [gen, span] of [...byGen.entries()].sort((a, b) => a[0] - b[0])) {
|
||||
genSpan.push({ gen, from: span.min, to: span.max })
|
||||
}
|
||||
|
||||
return {
|
||||
years,
|
||||
population,
|
||||
reputation,
|
||||
power,
|
||||
deathCauses,
|
||||
breakthroughs,
|
||||
tourneys: s.stats.tourneyHistory.slice(),
|
||||
genSpan,
|
||||
totalYears: s.year
|
||||
}
|
||||
}
|
||||
|
||||
/** 依据数据自动措辞的族史结语 */
|
||||
export function verdictLine(r: LegacyReport): string {
|
||||
if (r.years.length < 4) return '岁月尚浅,家族仍在晨雾中前行。'
|
||||
const finalRep = r.reputation[r.reputation.length - 1]
|
||||
const peakRep = Math.max(...r.reputation)
|
||||
const peakIndex = r.reputation.indexOf(peakRep)
|
||||
const peakPower = Math.max(...r.power)
|
||||
const btSum = r.breakthroughs.reduce((a, b) => a + b.count, 0)
|
||||
const mainCause = Object.entries(r.deathCauses).sort((a, b) => b[1] - a[1])[0]
|
||||
|
||||
const lines: string[] = []
|
||||
if (peakRep >= 60) lines.push(`家族声望曾至 ${peakRep} 之巅(第${peakIndex + 1}年),四方来贺。`)
|
||||
else if (finalRep < 0) lines.push('晚境声名凋零,门可罗雀。')
|
||||
else lines.push('不显山露水,然香火未冷。')
|
||||
lines.push(`战力峰值 ${peakPower},共记突破 ${btSum} 次。`)
|
||||
if (mainCause && mainCause[1] > 0) lines.push(`族中弃世者多缘「${mainCause[0]}」。`)
|
||||
const best = r.tourneys.reduce((m, t) => (m === null || t.rank < m.rank ? t : m), null as { year: number; rank: number } | null)
|
||||
if (best) lines.push(`太虚大比最佳战果:第${best.rank}名(${best.year}年)。`)
|
||||
return lines.join('')
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { GameState } from '../../types/domain'
|
||||
|
||||
export interface AxisEvent {
|
||||
year: number
|
||||
kind: '突破' | '大比' | '渡劫' | '婚' | '殇' | '战' | '飞升'
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface AxisCell {
|
||||
from: number
|
||||
to: number
|
||||
events: AxisEvent[]
|
||||
births: number
|
||||
deaths: number
|
||||
}
|
||||
|
||||
/** 横轴年表:十年一格,把族簿大事压缩进一根时间轴 */
|
||||
export function yearAxis(s: GameState, decadeSize = 10): AxisCell[] {
|
||||
const startYear = Math.max(1, Math.min(...s.chronicle.map((e) => e.year).concat(1)))
|
||||
const endYear = Math.max(startYear, s.year)
|
||||
const cells = new Map<number, AxisCell>()
|
||||
const cellOf = (year: number): AxisCell => {
|
||||
const from = Math.floor((year - 1) / decadeSize) * decadeSize + 1
|
||||
if (!cells.has(from)) cells.set(from, { from, to: Math.min(from + decadeSize - 1, endYear), events: [], births: 0, deaths: 0 })
|
||||
return cells.get(from)!
|
||||
}
|
||||
|
||||
for (const e of s.chronicle) {
|
||||
if (e.year < startYear) continue
|
||||
const cell = cellOf(e.year)
|
||||
const kind: AxisEvent['kind'] | null =
|
||||
e.category === 'breakthrough'
|
||||
? /渡劫|天劫/.test(e.text)
|
||||
? '渡劫'
|
||||
: '突破'
|
||||
: e.category === 'birth'
|
||||
? null
|
||||
: e.category === 'death'
|
||||
? /飞升/.test(e.text) ? '飞升' : '殇'
|
||||
: e.category === 'marriage'
|
||||
? '婚'
|
||||
: e.category === 'battle'
|
||||
? '战'
|
||||
: null
|
||||
if (kind) cell.events.push({ year: e.year, kind, label: e.text.slice(0, 26) })
|
||||
if (e.category === 'birth') cell.births++
|
||||
if (e.category === 'death') cell.deaths++
|
||||
}
|
||||
|
||||
return [...cells.entries()].sort((a, b) => a[0] - b[0]).map(([, c]) => c)
|
||||
}
|
||||
|
||||
export function decadeLabel(from: number, to: number): string {
|
||||
return from === to ? `${from}年` : `${from}-${to}年`
|
||||
}
|
||||
Reference in New Issue
Block a user