feat(0.1.16-P1): 资源闭环引擎(修为耗草/炼丹概率化/铸器/灵脉收益/NPC战力/灾变落效)+ C13C14

【A 经济闭环】
- 修为经济化:练气以上修行者月耗灵草(闭关2普通1),无草速修0.6~1.0衰减+缺草年告警
- 炼丹等级化:bonusOf 表达式引擎(白名单安全求值),craftChance 转正;丹方单源 PILL_RECIPES(聚气L1/凝元L2/破境L3)+ 失败折半退料
- 炼器筑宝:FORGE_RECIPES(法器/灵器/法宝,灵矿+兽核+灵石)
- 秘境灵脉:secretQiOf→rollWarbooty 收益门槛(0.5~1.3折算);掉落按秘境 techGrades 过滤
- NPC 实力参战:raid 强度/风险随 npc.power 浮动(0.5~1.8)
- 灾年落效:疫病/兽潮一次性袭击 + CALAMITY_FAMILY 月度减产乘子

【C 治理】
- C13 破境丹渡劫修正:大境界时药力→tribBoost 加成(+12%)入渡劫事件,不再跳过三选
- C14 存储防线:loadState 补 migrate;MigrationError(TOO_NEW/未知版本)提示升级

【UI 半批】
- 坊市新增 定制tab(丹方/铸器);ChroniclePanel 手写史书输入条;MemberModal 册立家主钮
- 年报摘要(要闻5条+辞世讣告);EventModal 选项收益预览
- 结盟引擎:setAlliance(relation≥40)/盟友岁差不降/年利15灵石
This commit is contained in:
2026-08-23 14:10:42 +08:00
parent c5fa2a3357
commit de46808188
23 changed files with 409 additions and 81 deletions
+71 -25
View File
@@ -3,6 +3,8 @@ import { ITEMS } from '../../game/data/items'
import { TECHNIQUES } from '../../game/data/techniques'
import { techniqueGradeName } from '../../game/data/realms'
import { marketPrice, buyItem, sellItem, buyTechnique } from '../../game/engine/sim/Market'
import { PILL_RECIPES, FORGE_RECIPES } from '../../game/data/items'
import { bonusOf } from '../../game/data/buildings'
import { useMemo, useState } from 'react'
export default function MarketPanel() {
@@ -10,7 +12,7 @@ export default function MarketPanel() {
const bump = useGameStore((s) => s.bump)
const revision = useGameStore((s) => s.revision)
void revision
const [tab, setTab] = useState<'goods' | 'tech'>('goods')
const [tab, setTab] = useState<'goods' | 'tech' | 'craft'>('goods')
if (!world) return null
const w = world
const fam = w.state.family
@@ -28,6 +30,7 @@ export default function MarketPanel() {
<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 className={`tab ${tab === 'craft' ? 'sel' : ''}`} onClick={() => setTab('craft')}></div>
</div>
{tab === 'goods' && (
<>
@@ -90,30 +93,7 @@ export default function MarketPanel() {
</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"
disabled={(inv['lingcao'] ?? 0) < 15 || fam.stones < 20}
title="需灵草15+灵石20"
onClick={() => { w.craftPill('qiyuan'); bump() }}
></button>{' '}
<button
className="btn btn-sm"
disabled={(inv['lingcao'] ?? 0) < 25 || (inv['beastcore'] ?? 0) < 4 || fam.stones < 60}
title="需灵草25+兽核4+灵石60"
onClick={() => { w.craftPill('ningyuan'); bump() }}
></button>
</>
)}
</td>
</tr>
</tbody>
</table>
</>
@@ -173,6 +153,72 @@ export default function MarketPanel() {
</table>
</>
)}
{tab === 'craft' && (
<>
<div className="help-text">
+++
</div>
<h4 className="dim" style={{ margin: '8px 0 4px' }}> {fam.buildings['danfang'] ?? 0} · {Math.round(bonusOf('danfang', 'craftChance', fam.buildings['danfang'] ?? 0) * 100)}%</h4>
<table className="market-table">
<thead>
<tr><th></th><th></th><th></th><th></th><th></th></tr>
</thead>
<tbody>
{PILL_RECIPES.map((r) => {
const lvl = fam.buildings['danfang'] ?? 0
const enough = (inv['lingcao'] ?? 0) >= r.lingcao && (inv['beastcore'] ?? 0) >= r.beastcore && fam.stones >= r.stones
return (
<tr key={r.output}>
<td><b><span className="s-icon"></span> {r.name}</b></td>
<td className="dim">{r.desc}</td>
<td className="dim">{r.lingcao}·{r.beastcore}·{r.stones}</td>
<td className="dim">{r.danfangLevel}</td>
<td>
<button
className="btn btn-sm"
disabled={lvl < r.danfangLevel || !enough}
title={lvl < r.danfangLevel ? `丹房需升到 ${r.danfangLevel}` : enough ? '炼制(失败折半退料)' : '材料不足'}
onClick={() => { w.craftPill(r.output.replace('pill-', '') as 'qiyuan' | 'ningyuan' | 'pojing'); bump() }}
></button>
</td>
</tr>
)
})}
</tbody>
</table>
<h4 className="dim" style={{ margin: '14px 0 4px' }}>++ </h4>
<table className="market-table">
<thead>
<tr><th></th><th></th><th></th><th></th></tr>
</thead>
<tbody>
{FORGE_RECIPES.map((r) => {
const enough = (inv['lingkuang'] ?? 0) >= r.lingkuang && (inv['beastcore'] ?? 0) >= r.beastcore && fam.stones >= r.stones
return (
<tr key={r.output}>
<td><b><span className="s-icon"></span> {r.name}</b></td>
<td className="dim">{r.desc}</td>
<td className="dim">{r.lingkuang}·{r.beastcore}·{r.stones}</td>
<td>
<button
className="btn btn-sm"
disabled={!enough}
title={enough ? '淬火铸成' : '材料不足'}
onClick={() => { w.forgeArtifact(r.output as 'weapon-qi' | 'weapon-ling' | 'weapon-fa'); bump() }}
></button>
</td>
</tr>
)
})}
</tbody>
</table>
{(fam.buildings['danfang'] ?? 0) > 0 && (
<div className="dim2" style={{ marginTop: 8 }}>
</div>
)}
</>
)}
<div className="dim2" style={{ marginTop: 10 }}>
</div>