v0.1.0: 仙途家族志 · Chronicle of the Immortal Clan

家族模拟器首版:修仙/经营/战斗/外交/叙事全套系统
- Electron + React + TS + MetonaSqlark(aria+OPFS)
- 水墨中国风 UI
- 引擎种子随机、确定性可回放
- 19 项单元测试、端到端冒烟验证
This commit is contained in:
2026-08-23 00:04:16 +08:00
commit adb22f0558
69 changed files with 14305 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
import { useGameStore } from '../store'
import { useMemo, useState } from 'react'
import { ChronicleEntry } from '../../game/types/domain'
const CAT_NAME: Record<string, string> = {
birth: '诞庆',
marriage: '姻缘',
death: '祭丧',
breakthrough: '突破',
battle: '战事',
trade: '商贸',
diplomacy: '外交',
exploration: '寻宝',
building: '营造',
event: '奇遇',
misc: '家常'
}
export default function ChroniclePanel() {
const world = useGameStore((s) => s.world)
const revision = useGameStore((s) => s.revision)
void revision
const [filter, setFilter] = useState<string>('all')
const entries = useMemo(() => {
if (!world) return []
const all = [...world.state.chronicle]
const filtered = filter === 'all' ? all : all.filter((e) => e.category === filter)
return filtered.slice().sort((a, b) => (b.year - a.year) || (b.month - a.month))
}, [world, filter])
if (!world) return null
return (
<div>
<div className="help-text">
</div>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 10 }}>
<button className={`btn btn-sm ${filter === 'all' ? 'btn-primary' : ''}`} onClick={() => setFilter('all')}></button>
{Object.entries(CAT_NAME).map(([k, v]) => (
<button key={k} className={`btn btn-sm ${filter === k ? 'btn-primary' : ''}`} onClick={() => setFilter(k)}>
{v}
</button>
))}
</div>
<div className="chronicle-list">
{entries.map((e) => (
<div key={e.id} className={`ch-item ${e.important ? 'gold' : ''}`}>
<span className="ch-year">{e.year}{e.month}</span>
<span>
<span className="tag" style={{ marginRight: 8 }}>{CAT_NAME[e.category] ?? e.category}</span>
{e.text}
</span>
</div>
))}
{entries.length === 0 && <div className="dim2"></div>}
</div>
</div>
)
}