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
+170
View File
@@ -0,0 +1,170 @@
import { useGameStore } from '../store'
import { ITEMS } from '../../game/data/items'
import { TECHNIQUES } from '../../game/data/techniques'
import { TECHNIQUE_GRADE_NAMES } from '../../game/data/realms'
import { marketPrice, buyItem, sellItem, buyTechnique } from '../../game/engine/market'
import { useMemo, useState } from 'react'
export default function MarketPanel() {
const world = useGameStore((s) => s.world)
const bump = useGameStore((s) => s.bump)
const revision = useGameStore((s) => s.revision)
void revision
if (!world) return null
const w = world
const fam = w.state.family
const inv = fam.inventory
const [tab, setTab] = useState<'goods' | 'tech'>('goods')
const goods = useMemo(() => Object.values(ITEMS).filter((i) => i.kind === 'resource' || i.kind === 'pill'), [])
const artifacts = useMemo(() => Object.values(ITEMS).filter((i) => i.kind === 'artifact'), [])
const marketTech = TECHNIQUES.filter((t) => t.grade <= 3)
const cangshuLv = fam.buildings['cangshu'] ?? 0
const price = (id: string) => marketPrice(w, id)
return (
<div>
<div className="tabs" style={{ padding: 0, background: 'none', border: 'none', marginBottom: 10 }}>
<div className={`tab ${tab === 'goods' ? 'sel' : ''}`} onClick={() => setTab('goods')}></div>
<div className={`tab ${tab === 'tech' ? 'sel' : ''}`} onClick={() => setTab('tech')}></div>
</div>
{tab === 'goods' && (
<>
<div className="help-text">
{Math.round(((fam.flag['priceMult'] as number) ?? 1) * 100)}%
</div>
<table className="market-table">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th>/</th>
<th></th>
</tr>
</thead>
<tbody>
{[...goods, ...artifacts].map((item) => {
const have = item.id === 'lingcao' || item.id === 'lingkuang' || item.id === 'beastcore' || item.kind === 'pill' || item.kind === 'artifact'
? inv[item.id] ?? 0
: 0
const stock = item.kind === 'artifact' ? 3 : item.kind === 'pill' ? 5 : 9999
const count = item.kind === 'artifact' ? 1 : 5
return (
<tr key={item.id}>
<td>
<b><span className="s-icon">{item.icon}</span> {item.name}</b>
</td>
<td className="dim">{item.desc}</td>
<td>{price(item.id)} </td>
<td className="dim">{have}</td>
<td>
<button
className="btn btn-sm"
disabled={fam.stones < price(item.id) * count}
onClick={() => {
buyItem(w, item.id, count)
bump()
}}
>
{count}
</button>{' '}
{((item.kind === 'resource' || item.kind === 'pill') && have >= count) && (
<button
className="btn btn-sm"
onClick={() => {
sellItem(w, item.id, count)
bump()
}}
>
{count}
</button>
)}
{item.kind === 'artifact' && have > 0 && (
<span className="dim2"> </span>
)}
</td>
</tr>
)
})}
<tr>
<td><b><span className="s-icon"></span> </b></td>
<td className="dim">+ </td>
<td className="dim">{fam.buildings['danfang'] ? `${fam.buildings['danfang']}级丹房` : '未建丹房'}</td>
<td className="dim">{inv['lingcao'] ?? 0}</td>
<td>
{fam.buildings['danfang'] && (
<>
<button className="btn btn-sm" onClick={() => { w.craftPill('qiyuan'); bump() }}></button>{' '}
<button className="btn btn-sm" onClick={() => { w.craftPill('ningyuan'); bump() }}></button>
</>
)}
</td>
</tr>
</tbody>
</table>
</>
)}
{tab === 'tech' && (
<>
<div className="help-text">
{cangshuLv} {TECHNIQUE_GRADE_NAMES[Math.min(3, Math.max(0, cangshuLv))]}
</div>
<table className="market-table">
<thead>
<tr>
<th></th>
<th>/</th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{marketTech
.filter((t) => t.grade <= 1 + cangshuLv)
.filter((t) => t.grade <= 3)
.map((t) => {
const owned = fam.techniques.includes(t.id)
const p = t.grade <= 1 + cangshuLv
const priceT = [120, 300, 700, 1600, 3600][t.grade] ?? 300
return (
<tr key={t.id}>
<td><b>{t.name}</b></td>
<td className="dim">{TECHNIQUE_GRADE_NAMES[t.grade]} · {t.path}</td>
<td className="dim">{t.desc}</td>
<td>+{Math.round((t.expBonus) * 100)}%</td>
<td>+{Math.round((t.powerBonus) * 100)}%</td>
<td>
{owned ? (
<span className="good"></span>
) : (
<button
className="btn btn-sm"
disabled={!p || fam.stones < priceT}
onClick={() => {
buyTechnique(w, t.id, priceT)
bump()
}}
>
{priceT}
</button>
)}
</td>
</tr>
)
})}
</tbody>
</table>
</>
)}
<div className="dim2" style={{ marginTop: 10 }}>
</div>
</div>
)
}