结构(旧 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
129 lines
5.0 KiB
TypeScript
129 lines
5.0 KiB
TypeScript
import { useGameStore } from '../store'
|
||
import { computeGenealogy } from '../../game/engine/narrative/genealogy'
|
||
import { describeRealm } from '../../game/data/realms'
|
||
import { Character, FamilyState } from '../../game/types/domain'
|
||
import { useState } from 'react'
|
||
import { MemberModal } from '../components/MemberModal'
|
||
|
||
export default function GenealogyPanel() {
|
||
const world = useGameStore((s) => s.world)
|
||
const revision = useGameStore((s) => s.revision)
|
||
const selectedMemberId = useGameStore((s) => s.selectedMemberId)
|
||
const setSelected = useGameStore((s) => s.setSelected)
|
||
const [deceasedTab, setDeceasedTab] = useState(false)
|
||
void revision
|
||
|
||
if (!world) return null
|
||
const w = world
|
||
const s = w.state
|
||
const rows = computeGenealogy(w)
|
||
const deceased = !deceasedTab
|
||
? []
|
||
: Object.values(s.members).filter((c) => !c.alive).sort((a, b) => (a.deathYear ?? 0) - (b.deathYear ?? 0))
|
||
const selected = selectedMemberId ? s.members[selectedMemberId] as Character | undefined : undefined
|
||
|
||
return (
|
||
<div>
|
||
<div className="help-text">
|
||
宗族谱系:世代分带,夫妻同卷,子孙列后。点任一名字可查详情;染灰者已下世,功绩铭记。
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 6, marginBottom: 10 }}>
|
||
<button
|
||
className={`btn btn-sm ${!deceasedTab ? 'btn-primary' : ''}`}
|
||
onClick={() => setDeceasedTab(false)}
|
||
>
|
||
在世谱系
|
||
</button>
|
||
<button className={`btn btn-sm ${deceasedTab ? 'btn-primary' : ''}`} onClick={() => setDeceasedTab(true)}>
|
||
先人簿({Object.values(s.members).filter((c) => !c.alive).length}位)
|
||
</button>
|
||
</div>
|
||
|
||
{!deceasedTab &&
|
||
rows.map((row) => (
|
||
<div key={row.gen} className="genealogy-row">
|
||
<div className="genealogy-gen">第{row.gen}代</div>
|
||
<div className="genealogy-body">
|
||
{row.units.map((unit, i) => (
|
||
<div key={i} className="genealogy-unit">
|
||
<div className="genealogy-parents">
|
||
{unit.parents[0] && <GeneNode c={unit.parents[0]} w={w} />}
|
||
<div className="genealogy-matron">×</div>
|
||
{unit.parents[1] && <GeneNode c={unit.parents[1]} w={w} />}
|
||
</div>
|
||
{unit.children.length > 0 && (
|
||
<>
|
||
<div className="genealogy-drop" />
|
||
<div className="genealogy-children">
|
||
{unit.children.map((ch) => (
|
||
<GeneNode key={ch.id} c={ch} w={w} />
|
||
))}
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
))}
|
||
{row.singles.length > 0 && (
|
||
<div className="genealogy-singles">
|
||
{row.singles.map((c) => (
|
||
<GeneNode key={c.id} c={c} w={w} />
|
||
))}
|
||
</div>
|
||
)}
|
||
{row.units.length + row.singles.length === 0 && <div className="dim2">此代凋零</div>}
|
||
</div>
|
||
</div>
|
||
))}
|
||
|
||
{deceasedTab &&
|
||
deceased.map((c) => (
|
||
<div key={c.id} className="set-row">
|
||
<span>
|
||
<span className="ch-year" style={{ marginRight: 10 }}>殁于{c.deathYear}年</span>
|
||
{c.name} · {c.gender === 'male' ? '男' : '女'} · 第{c.generation}代
|
||
<span className="dim2" style={{ marginLeft: 8 }}>{c.deathCause ?? '寿终'}</span>
|
||
</span>
|
||
<button className="btn btn-sm" onClick={() => setSelected(c.id)}>阅</button>
|
||
</div>
|
||
))}
|
||
|
||
{selected && <MemberModal member={selected} />}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function GeneNode({ c, w }: { c: Character; w: NonNullable<ReturnType<typeof useGameStore.getState>['world']> }) {
|
||
const setSelected = useGameStore((s) => s.setSelected)
|
||
const isHead = w.state.family.headId === c.id
|
||
const rank = c.techniqueRank ?? 0
|
||
const post = POST_NAME[c.post ?? ''] ?? ''
|
||
return (
|
||
<div
|
||
className={`gene-node ${c.alive ? '' : 'gene-dead'} ${isHead ? 'gene-head' : ''}`}
|
||
onClick={() => setSelected(c.id)}
|
||
title={`${c.name} · ${describeRealm(c.realm)} · ${w.ageOf(c)}岁`}
|
||
>
|
||
<div className="gene-name">
|
||
{c.name}
|
||
{isHead && <span className="tag head-t" style={{ marginLeft: 4 }}>主</span>}
|
||
{post && <span className="tag" style={{ marginLeft: 4 }}>{post}</span>}
|
||
{!c.alive && <span className="tag" style={{ marginLeft: 4 }}>殁</span>}
|
||
</div>
|
||
<div className="gene-sub">
|
||
{c.alive ? `${describeRealm(c.realm)} · ${w.ageOf(c)}岁` : `${c.deathCause ?? '寿终'}${c.deathYear ? `(${c.deathYear}年)` : ''}`}
|
||
{rank > 0 && <span className="tag gold-t" style={{ marginLeft: 4 }}>悟{rank}</span>}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const POST_NAME: Record<string, string> = {
|
||
head: '家主',
|
||
elder: '长老',
|
||
guardian: '供奉',
|
||
steward: '执事',
|
||
master: '掌教'
|
||
}
|
||
|
||
export type { FamilyState }
|