v0.1.2: 宗族气韵(职事/悟道/谱系/碑录/庆典/飞升/音效)
- 职事任命:长老(+修炼)/供奉(+战力)/执事(+坊市)/掌教(+闭关) 每人加成叠加,上限管理 - 功法悟道:修习积悟性点,小成/大成逐级+战力,大境界突破传道于直系晚辈 - 宗族谱系页:世代分带 + 夫妻同卷 + 子孙连枝,点节点开详情,先人簿可查 - 功德碑:宗主辞世即勒石,宗祠页碑录永存 - 百年庆典 + 飞升之路事件链(留世坐镇/仙风入体 或 云游飞升/宗族蒙荫) - 合成音效(WebAudio):鼓点/钟鸣/纸韵/磬响,设置页开关 - 单测 29→38;typecheck/build 通过;GUI 冒烟受限于 WSLg 掉线(SMOKE-TIMEOUT 优雅退出)
This commit is contained in:
@@ -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 }
|
||||
@@ -13,6 +13,8 @@ export default function SettingsPanel() {
|
||||
const snapshots = useGameStore((s) => s.snapshots)
|
||||
const go = useGameStore((s) => s.go)
|
||||
const setSpeed = useGameStore((s) => s.setSpeed)
|
||||
const soundOn = useGameStore((s) => s.soundOn)
|
||||
const toggleSound = useGameStore((s) => s.toggleSound)
|
||||
if (!world) return null
|
||||
|
||||
return (
|
||||
@@ -63,6 +65,12 @@ export default function SettingsPanel() {
|
||||
<span>刷新存档槽</span>
|
||||
<button className="btn btn-sm" onClick={() => void refreshSlots()}>刷新</button>
|
||||
</div>
|
||||
<div className="set-row">
|
||||
<span>音效(合成音:鼓点·钟鸣·纸韵)</span>
|
||||
<button className="btn btn-sm" onClick={toggleSound}>
|
||||
{soundOn ? '开(点击关)' : '关(点击开)'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="set-row">
|
||||
<span>暂停时间</span>
|
||||
<button className="btn btn-sm" onClick={() => setSpeed(0)}>停止流转</button>
|
||||
|
||||
@@ -104,6 +104,26 @@ export default function TerritoryPanel() {
|
||||
)
|
||||
}
|
||||
|
||||
function MonumentList() {
|
||||
const world = useGameStore((s) => s.world)
|
||||
const revision = useGameStore((s) => s.revision)
|
||||
void revision
|
||||
if (!world) return null
|
||||
const monuments = world.state.chronicle.filter((e) => e.text.startsWith('功德碑')).slice().reverse()
|
||||
if (monuments.length === 0) return null
|
||||
return (
|
||||
<div className="monument-strip">
|
||||
<div className="card-title" style={{ fontSize: '0.98rem' }}>宗祠碑录</div>
|
||||
{monuments.slice(0, 4).map((m) => (
|
||||
<div key={m.id} className="dim" style={{ lineHeight: 1.7 }}>
|
||||
<span className="ch-year" style={{ marginRight: 8 }}>{m.year}年</span>
|
||||
{m.text.slice(3)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function itemLabel(k: string): string {
|
||||
const map: Record<string, string> = {
|
||||
stone: '灵石',
|
||||
|
||||
Reference in New Issue
Block a user