v0.1.2: 宗族气韵(职事/悟道/谱系/碑录/庆典/飞升/音效)

- 职事任命:长老(+修炼)/供奉(+战力)/执事(+坊市)/掌教(+闭关) 每人加成叠加,上限管理
- 功法悟道:修习积悟性点,小成/大成逐级+战力,大境界突破传道于直系晚辈
- 宗族谱系页:世代分带 + 夫妻同卷 + 子孙连枝,点节点开详情,先人簿可查
- 功德碑:宗主辞世即勒石,宗祠页碑录永存
- 百年庆典 + 飞升之路事件链(留世坐镇/仙风入体 或 云游飞升/宗族蒙荫)
- 合成音效(WebAudio):鼓点/钟鸣/纸韵/磬响,设置页开关
- 单测 29→38;typecheck/build 通过;GUI 冒烟受限于 WSLg 掉线(SMOKE-TIMEOUT 优雅退出)
This commit is contained in:
2026-08-23 08:28:49 +08:00
parent 6b15a97d0c
commit fefbe1f1d7
22 changed files with 864 additions and 8 deletions
+128
View File
@@ -0,0 +1,128 @@
import { useGameStore } from '../store'
import { computeGenealogy } from '../../game/core/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" style={{ maxWidth: 760 }}>
<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 }