v0.1.16: 资源闭环 × 世情入味(A+B+C 全套餐)

【A 经济闭环(引擎核心)】
- 修为经济化:灵草成修行燃料——练气以上月耗(闭关2/普通1),无草速修0.6~1.0衰减+缺草每年一告
- 炼丹等级化:bonusOf 白名单表达式求值器(extra 单源);craftChance 转正(L5=100%),丹方 PILL_RECIPES(破境丹需丹房3级),失败折半退料
- 炼器筑宝:FORGE_RECIPES(法器/灵器/法宝)灵矿兽核入炉
- 秘灵灵脉:secretQiOf→rollWarbooty 收益门槛(0.5~1.5 折算),掉落按秘境 techGrades 过滤(暗墨林不再出仙典)
- NPC 实力参战:raid 强度/风险随 npc.power 浮动(0.5~1.8)——外交成长期博弈
- 灾年落效:CALAMITY_FAMILY 月度减产乘子 + 疫病/兽潮一次性体感

【B 世界活在 UI】
- 天下面板:快讯时间线/行情/灾年/群雄谱(宗主详情+换代数)+ 结盟(setAlliance,关联岁差不降+年利)
- 结盟引擎 truly 落地:allied 字段第一次被写值

【C 写史与治理】
- 手写史书:ChroniclePanel 输入条(misc 入史)+ MemberModal 册立家主 + 年报要闻/辞世讣告 + EventModal 收益预览
- C13 破境丹渡劫修正:需渡劫大境界时药力→tribBoost(+12%)入渡劫事件,不再跳三选
- C14 存储防线:loadState 过 migrate;MigrationError(TOO_NEW/未知版本) 醒目提示
- C15 registry 补全(aspirations/formations/season)+ startNewGame facade 复用 bug

【测试】986 全绿(37 套件,新增经济闭环 7 例);金钟罩 0.1.16 基线固化(修为耗草/炼丹/灾害序列变更受控)
This commit is contained in:
2026-08-23 14:13:13 +08:00
parent de46808188
commit 809707408b
14 changed files with 266 additions and 18 deletions
+107
View File
@@ -0,0 +1,107 @@
import { useGameStore } from '../store'
import { npcById } from '../../game/data/npcs'
import { MAJOR_NAMES } from '../../game/data/realms'
import { POOL_BASE } from '../../game/engine/sim/worldsim-data'
import { useState } from 'react'
const RES_LABEL: Record<string, string> = { lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核' }
const REALM_IDX = ['mortal', 'qi', 'foundation', 'core', 'nascent', 'spirit'] as const
export default function WorldPanel() {
const world = useGameStore((s) => s.world)
const bump = useGameStore((s) => s.bump)
const revision = useGameStore((s) => s.revision)
void revision
const [view, setView] = useState<string | null>(null)
if (!world) return null
const w = world
const ws = w.state.worldSim
if (!ws) return <div className="help-text"></div>
const tide = ws.tide ?? 0.5
const tideLabel = tide > 0.9 ? '灵涨' : tide < 0.7 ? '灵衰' : '汐平'
const pool = ws.marketPool ?? {}
const news = [...(ws.newsFeed ?? [])].slice().reverse()
const calamity = ws.calamity
return (
<div>
<div className="help-text">
40
</div>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginBottom: 10 }}>
<span className="tag gold-t"> {tideLabel}{Math.round(tide * 100)}%</span>
{calamity
? <span className="tag" style={{ borderColor: '#a33', color: '#e8a08a' }}>·{calamity}</span>
: <span className="tag"></span>}
{Object.entries(pool).map(([k, v]) => {
const base = POOL_BASE[k as keyof typeof POOL_BASE] ?? 100
const r = ((v as number) / base) * 100
const cls = r > 115 ? 'bad' : r < 85 ? 'good' : 'dim'
const dir = r > 105 ? '↑' : r < 95 ? '↓' : '→'
return (
<span key={k} className="tag"><span className={cls}>{RES_LABEL[k] ?? k}{dir}{Math.round(r)}%</span></span>
)
})}
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div>
<h4 className="dim" style={{ margin: '4px 0 6px' }}></h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, maxHeight: 380, overflowY: 'auto' }}>
{news.length === 0 && <div className="dim2"></div>}
{news.map((row, i) => (
<div key={i} className="ch-item" style={{ padding: '5px 8px' }}>
<span className="ch-month">{row.year}{row.month}</span>
<span>{row.text}</span>
</div>
))}
</div>
</div>
<div>
<h4 className="dim" style={{ margin: '4px 0 6px' }}></h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{Object.values(w.state.npcFamilies).map((npc) => {
const def = npcById(npc.id)
const dyn = (ws.npcDyn ?? {})[npc.id]
const open = view === npc.id
return (
<div key={npc.id} style={{ border: '1px solid rgba(140,100,50,0.3)', borderRadius: 6, padding: '7px 10px', background: 'rgba(18,14,9,0.4)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }} onClick={() => setView(open ? null : npc.id)}>
<span>
<b>{npc.name}</b> <span className="dim">{def.style}</span>
{npc.allied && <span className="tag gold-t" style={{ marginLeft: 6 }}></span>}
</span>
<span className="dim">{npc.power} · {npc.relation}</span>
</div>
{open && (
<div style={{ marginTop: 6, fontSize: '0.86rem', color: '#c8b285', borderTop: '1px solid rgba(140,100,50,0.25)', paddingTop: 6 }}>
<div className="dim">{dyn?.leaderName ?? '未知'}{dyn ? MAJOR_NAMES[REALM_IDX[dyn.leaderRealmIdx] ?? 'qi'] : ''} · {dyn?.leaderAge ?? '?'}</div>
<div className="dim2" style={{ margin: '2px 0 6px' }}>{def.desc}</div>
<div className="dim">{dyn?.lastEvent ? `${dyn.lastEvent}${dyn.lastEventYear ?? '?'}年)` : '暂无'}</div>
{npc.alliedSinceYear && <div className="dim"> {npc.alliedSinceYear} </div>}
<div style={{ marginTop: 6, display: 'flex', gap: 6 }}>
<button
className="btn btn-sm"
disabled={npc.allied || npc.relation < 40}
title={npc.relation < 40 ? '关系不足(需40' : ''}
onClick={() => { w.setAlliance(npc.id, true); bump() }}
></button>
{npc.allied && (
<button
className="btn btn-sm"
onClick={() => { w.setAlliance(npc.id, false); bump() }}
>-20</button>
)}
</div>
</div>
)}
</div>
)
})}
</div>
</div>
</div>
</div>
)
}