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

家族模拟器首版:修仙/经营/战斗/外交/叙事全套系统
- Electron + React + TS + MetonaSqlark(aria+OPFS)
- 水墨中国风 UI
- 引擎种子随机、确定性可回放
- 19 项单元测试、端到端冒烟验证
This commit is contained in:
Hermes Agent
2026-08-23 00:04:16 +08:00
commit 2f81b29878
69 changed files with 14305 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
import { useMemo, useState } from 'react'
import { useGameStore } from '../store'
import { SURNAME_POOL } from '../../game/core/names'
export default function NewGame() {
const go = useGameStore((s) => s.go)
const startNewGame = useGameStore((s) => s.startNewGame)
const [surname, setSurname] = useState('林')
const [familyName, setFamilyName] = useState('林家')
const [motto, setMotto] = useState('耕读传家,术法继世')
const [difficulty, setDifficulty] = useState<'easy' | 'normal' | 'hard'>('normal')
const [slot, setSlot] = useState(1)
const [randoming, setRandoming] = useState(false)
const seed = useMemo(
() => `${Date.now()}-${Math.floor(Math.random() * 1e9)}-${Math.floor(Math.random() * 1e9)}`,
[randoming]
)
const rollNames = () => {
setRandoming((r) => !r)
const s = SURNAME_POOL[Math.floor(Math.random() * SURNAME_POOL.length)]
setSurname(s)
setFamilyName(`${s}`)
}
return (
<div className="newgame">
<div className="newgame-box">
<div className="card-title" style={{ textAlign: 'center', fontSize: '1.4rem' }}>
</div>
<div className="field">
<label></label>
<input value={surname} onChange={(e) => setSurname(e.target.value)} maxLength={2} />
<button className="btn btn-sm" onClick={rollNames}></button>
</div>
<div className="field">
<label></label>
<input value={familyName} onChange={(e) => setFamilyName(e.target.value)} maxLength={10} placeholder="例如:林氏" />
</div>
<div className="field">
<label></label>
<input value={motto} onChange={(e) => setMotto(e.target.value)} maxLength={20} />
</div>
<div className="field">
<label></label>
<div className="diff-grid">
{(
[
['easy', '顺境', '灵石丰足,时来运转'],
['normal', '中庸', '中规中矩,全靠经营'],
['hard', '逆境', '部曲拮据,波折不断']
] as const
).map(([k, label, desc]) => (
<div
key={k}
className={`diff-opt ${difficulty === k ? 'sel' : ''}`}
onClick={() => setDifficulty(k)}
>
<div>{label}</div>
<div className="dim2" style={{ fontSize: '0.78rem', marginTop: 2 }}>{desc}</div>
</div>
))}
</div>
</div>
<div className="field">
<label></label>
<select value={slot} onChange={(e) => setSlot(Number(e.target.value))}>
<option value={1}></option>
<option value={2}></option>
<option value={3}></option>
<option value={4}></option>
</select>
</div>
<div style={{ display: 'flex', gap: 12, justifyContent: 'center', marginTop: 8 }}>
<button className="btn" onClick={() => go('boot')}></button>
<button
className="btn btn-primary btn-lg"
onClick={() => {
void startNewGame({ seed, surname, familyName, motto, difficulty }, slot)
}}
>
</button>
</div>
<div className="dim2" style={{ textAlign: 'center' }}>
</div>
</div>
</div>
)
}