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
+167
View File
@@ -0,0 +1,167 @@
import { useGameStore } from '../store'
import { Character } from '../../game/types/domain'
import { describeRealm, nextRealm } from '../../game/data/realms'
import { describeRoots, ROOT_GRADE_NAMES } from '../../game/data/elements'
import { TRAITS } from '../../game/data/traits'
import { TECHNIQUES } from '../../game/data/techniques'
import { ITEMS, ARTIFACT_POWER } from '../../game/data/items'
import { combatPowerOf } from '../../game/engine/systems/combat'
import { lifespanOf } from '../../game/engine/systems/lifecycle'
export function MemberModal({ member }: { member: Character }) {
const world = useGameStore((s) => s.world)
const setSelected = useGameStore((s) => s.setSelected)
const bump = useGameStore((s) => s.bump)
if (!world) return null
const w = world
const s = w.state
const next = nextRealm(member.realm)
const nextName = next ? describeRealm(next) : '已至巅峰'
const tech = TECHNIQUES.find((t) => t.id === member.techniqueId)
const power = member.alive ? combatPowerOf(w, member) : 0
const lifespan = lifespanOf(w, member)
return (
<div className="modal-outer" onClick={() => setSelected(undefined)}>
<div className="modal member-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-title" style={{ fontSize: '1.4rem' }}>
{member.name}
{!member.alive && <span className="bad" style={{ fontSize: '1rem' }}> · </span>}
</div>
<div className="modal-cat">
{member.generation} · {member.gender === 'male' ? '男' : '女'} · {w.ageOf(member)} · 寿{lifespan} · {describeRoots(member.roots)}
</div>
<div className="mm-grid">
<div className="mm-line"><span></span><b>{describeRealm(member.realm)} {Math.floor(member.realmProgress)}%</b></div>
<div className="mm-line"><span></span><b className={member.realmProgress >= 100 ? 'gold' : ''}>{nextName}</b></div>
<div className="mm-line"><span></span><b>{Math.round(power)}</b></div>
<div className="mm-line"><span></span><b>{member.state === 'meditation' ? '闭关' : member.state === 'expedition' ? '外出' : member.state === 'wounded' ? '养伤' : '无事'}</b></div>
<div className="mm-line"><span></span><b>{Math.round(member.health)}</b></div>
<div className="mm-line"><span></span><b>{tech ? `${tech.name}(${['黄', '玄', '地', '天', '仙'][tech.grade]}阶)` : member.realm.major === 'mortal' ? '—' : '未习功法'}</b></div>
<div className="mm-line"><span></span><b>{member.equipment ? `${ITEMS[member.equipment].name}(+${Math.round((ARTIFACT_POWER[member.equipment] ?? 0) * 100)}%)` : '—'}</b></div>
<div className="mm-line"><span></span><b>{member.spouseId ? s.members[member.spouseId]?.name ?? '' : member.spouseHouse ?? '未婚'}</b></div>
</div>
<div className="bar bar-red" style={{ marginTop: 10 }} title="气血">
<div style={{ width: `${member.health}%` }} />
</div>
<div className="mm-sec">
<div className="mm-grid">
<div className="mm-line"><span></span><b>{member.perception}/10</b></div>
<div className="mm-line"><span></span><b>{member.physique}/10</b></div>
<div className="mm-line"><span></span><b>{member.mind}/10</b></div>
<div className="mm-line"><span></span><b>{member.charm}/10</b></div>
<div className="mm-line"><span></span><b>{member.fortune}/12</b></div>
<div className="mm-line"><span></span><b>{member.fortune ? (member.fortune > 7 ? '俊逸' : '端正') : '—'}</b></div>
</div>
</div>
<div className="mm-sec">
<div className="dim" style={{ marginBottom: 4 }}></div>
{member.traits.map((t) => (
<span key={t} className="tag" style={{ marginRight: 4 }}>
{TRAITS[t]?.name ?? t}
</span>
))}
{member.traits.length === 0 && <span className="dim2"></span>}
</div>
<div className="mm-sec">
<div className="dim" style={{ marginBottom: 6 }}></div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
{member.alive && member.state !== 'expedition' && (
<button
className="btn btn-sm"
onClick={() => {
w.setMeditation(member.id, member.state !== 'meditation')
bump()
}}
>
{member.state === 'meditation' ? '出关' : '闭关'}
</button>
)}
{member.alive && member.realmProgress >= 100 && member.realm.major !== 'spirit' && (
<button
className="btn btn-sm btn-primary"
onClick={() => {
w.assistedBreakthrough(member.id)
bump()
}}
>
{nextName}
</button>
)}
{member.alive && (
<>
<button
className="btn btn-sm"
onClick={() => {
w.takePill(member.id, 'pill-qiyuan')
bump()
}}
>
</button>
{member.realm.major !== 'mortal' && (
<button
className="btn btn-sm"
onClick={() => {
w.takePill(member.id, 'pill-ningyuan')
bump()
}}
>
</button>
)}
{member.realmProgress >= 100 && member.realm.major !== 'mortal' && (
<button
className="btn btn-sm"
onClick={() => {
w.takePill(member.id, 'pill-pojing')
bump()
}}
>
</button>
)}
</>
)}
</div>
</div>
{member.alive && (
<div className="mm-sec">
<div className="dim" style={{ marginBottom: 6 }}></div>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
{s.family.techniques.map((tid) => (
<button
key={tid}
className="btn btn-sm"
style={{ borderColor: member.techniqueId === tid ? 'var(--gold)' : 'var(--line)' }}
onClick={() => {
w.giveTechnique(member.id, tid)
bump()
}}
>
{TECHNIQUES.find((t) => t.id === tid)?.name ?? tid}
</button>
))}
{s.family.techniques.length === 0 && <span className="dim2"></span>}
</div>
</div>
)}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 }}>
{member.children.length > 0 && (
<span className="dim2">
{member.children.map((cid) => s.members[cid]?.name ?? '?').join('、')}
</span>
)}
<button className="btn" onClick={() => setSelected(undefined)}></button>
</div>
</div>
</div>
)
}