Files
ChronicleOfTheImmortalClan/src/renderer/ui/panels/WorldPanel.tsx
T
thzxx da4fb9f635 v0.1.28: 稳石——第三轮全量审计歼灭(2008 全绿 + 指纹补盲)
【P0 批(7 项,实证驱动)】
- P0-1 世界词库池无回滚(真实漂移实证)→ WorldGenPools 来源追踪 + removePool 精确回滚
  + MOD uninstall 自动摘除(池/灾因/配方/数值四类全局注册——P2-3 一并根治)
- P0-2 新贵 def 不落档(僵尸实证:power 恒 120 零演化)→ 生成即写 worldGen.npcs
  + normalize 占位 def 双保险 + 新贵 id 掺 seed 指纹(P1-10 防跨档碰撞)
- P0-3 校验死代码化 → installModText 装前 modPreflight(冲突拒+警告 toast)
- P0-4 MOD 读档静默消失 → state.pluginsMissing 收集提示(与 MOD_GUIDE 承诺对齐)
- P0-5 救济按钮乌龙(哈希序首个+直改 state)→ World.reliefCalamity(景气最低+双检)
- P0-6 天下快讯永久冻结(memo 依赖恒 120 length)→ 去 memo 每次 reverse
- P0-7 MOD 导出单行道 → modParse 识别 {app:'cotymod',bundle} 逐包递归(roundtrip 闭环)

【P1 批】
- P1-8 指纹补盲(worldGen/sagaAnnals/distress/declineYears 入 hash——0.1.27 成果真实锁定)+ 双基线重算
- P1-9 重放同步 worldGen.npcs(自捕获);P1-11 worldNum 字段接线(install/uninstall 复位)
- P1-13 落盘兜底(beforeunload/visibilitychange 突发 save,堵 12-tick 节流窗口)
- P1-14 onGameOver 停 timer + IPC mods 路径白名单(扩展名正则防穿越)
- P1-15 DataPack 摘除 npcs(覆写死路删除,与五口宣言对齐);P1-4 techniques 双源统一(techniquePrice 读包)

【P2 扫尾】tauntCD 清理/sagaAnnals 上限 40/worldGenMods 落空写入/validateMod 深度校验随 P0-3 提交

【测试】69 套件 2008 全绿(audit-0.1.28 八例:池回滚/僵尸/preflight/bundle/freeze/指纹敏感/worldNum/救济);
金钟罩 0.1.28 基线重算(指纹补盲+受控时序变更);build 通过
2026-08-23 18:07:31 +08:00

234 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useGameStore } from '../store'
import { useMemo } from 'react'
import { npcById } from '../../game/data/npcs'
import { MAJOR_NAMES } from '../../game/data/realms'
import type { WorldSimState } from '../../game/engine/sim/worldsim-data'
import { POOL_BASE, WORLDSIM, ERA_CONF } from '../../game/engine/sim/worldsim-data'
import { combatPowerOf } from '../../game/engine/runtime/Systems/combat'
import { sellItem, buyItem, marketPrice } from '../../game/engine/sim/Market'
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 as WorldSimState | undefined
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 ?? {}
// P0-6 修复:newsFeed 恒定 120 时 length 依赖 memo 永冻结——直接每次 reverse(120 项零成本)
const news = [...(ws.newsFeed ?? [])].slice().reverse()
const calamity = ws.calamity
const calamityLeft = ws.calamityLeft ?? 0
const myPower = Object.values(w.state.members)
.filter((c) => c.alive)
.reduce((a, c) => a + combatPowerOf(w, c), 0)
const allPowers = Object.entries(w.state.npcFamilies).map(([id, n]) => ({ id, name: n.name, power: n.power }))
const myRank = allPowers.filter((x) => x.power > myPower).length + 1
void myPower
const eraName = ws.era ? ERA_CONF[ws.era].name : '平世'
const eraDesc = ws.era ? ERA_CONF[ws.era].desc : ''
const annals = news.filter((r) => r.kind === 'annal').slice(0, 3)
const maxPower = Math.max(myPower, ...allPowers.map((x) => x.power)) || 1
return (
<div>
<div className="help-text">
天下时局尽览:行市起落、宗主更替、灾年吉凶——皆随世界自演化而来。
结盟后岁差不降,每年另有盟利奉上(需关系≥40)。
</div>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginBottom: 10 }}>
<span className="tag" style={{ borderColor: ws.era === 'shengshi' ? '#6a8a2e' : ws.era === 'luanshi' || ws.era === 'mofa' ? '#a33' : '#8a6d2f', color: ws.era === 'shengshi' ? '#b6d17a' : ws.era === 'luanshi' || ws.era === 'mofa' ? '#e8a08a' : undefined, cursor: 'help' }} title={eraDesc}>
{eraName}
</span>
<span className="tag gold-t">灵潮 {tideLabel}{Math.round(tide * 100)}%</span>
{calamity
? <span className="tag" style={{ borderColor: '#a33', color: '#e8a08a' }}>灾年·{calamity}(约{calamityLeft}月后散去)</span>
: <span className="tag">风调雨顺</span>}
<span className="tag">本族雄基 {myRank}/{Object.keys(w.state.npcFamilies).length + 1}</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 < WORLDSIM.tradeFloorPct * 100 ? 'good' : 'dim'
const dir = r > 105 ? '↑' : r < 95 ? '↓' : '→'
return (
<span key={k} className="tag" title={`池深 ${Math.round(r)}%(低于${Math.round(WORLDSIM.tradeFloorPct * 100)}%坊市断供告急)`}>
<span className={cls}>{RES_LABEL[k] ?? k}{dir}{Math.round(r)}%</span>
</span>
)
})}
</div>
{ws.distress && w.state.npcFamilies[ws.distress.id] && (
<div className="ch-item" style={{ padding: '7px 10px', marginBottom: 10, border: '1px solid rgba(160,60,40,0.5)' }}>
<span className="bad"></span> 盟友 <b>{w.state.npcFamilies[ws.distress.id].name}</b> 遣使求援:
<span style={{ marginLeft: 8 }}>
<button
className="btn btn-sm"
disabled={w.state.family.stones < 200}
title={w.state.family.stones < 200 ? '灵石不足(需200' : '捐 200 灵石助其重整武备'}
onClick={() => { w.assistAlly(ws.distress!.id); bump() }}
>出资200</button>
<button
className="btn btn-sm"
style={{ marginLeft: 6 }}
title="置之不理(盟谊受损)"
onClick={() => {
const npc = w.state.npcFamilies[ws.distress!.id]
if (npc) npc.relation = Math.max(0, npc.relation - 10)
ws.distress = undefined
w.log('info', `${npc?.name ?? '盟友'} 见吾族坐视,失望而返。`)
bump()
}}
>见死不救</button>
</span>
</div>
)}
{annals.length > 0 && (
<div style={{ marginBottom: 10 }}>
<h4 className="dim" style={{ margin: '4px 0 6px' }}>史官十年鉴</h4>
{annals.map((r, i) => (
<div key={i} className="ch-item" style={{ padding: '5px 8px' }}>
<span className="ch-month">{r.year}</span><span className="gold-t">{r.text}</span>
</div>
))}
</div>
)}
<div style={{ marginBottom: 12 }}>
<h4 className="dim" style={{ margin: '4px 0 6px' }}>群雄势衡(本族 vs 四方)</h4>
<div style={{ display: 'flex', gap: 10, alignItems: 'flex-end' }}>
{[{ name: '我族', power: myPower, me: true }, ...allPowers.map((x) => ({ ...x, me: false }))].map((x) => (
<div key={x.name} style={{ textAlign: 'center', flex: 1 }}>
<div className="dim2" style={{ fontSize: '0.72rem' }}>{x.name}</div>
<div className="dim" style={{ fontSize: '0.78rem' }}>{Math.round(x.power)}{x.power < 45 && !x.me ? '·衰' : ''}</div>
<div style={{
height: Math.max(6, (x.power / maxPower) * 90),
background: x.me ? 'linear-gradient(180deg,#c9a227,#8a5a20)' : x.power < 45 ? '#5a4444' : 'linear-gradient(180deg,#4a6a3a,#2e4a2e)',
borderRadius: '4px 4px 0 0', border: '1px solid rgba(180,140,80,0.4)'
}} />
</div>
))}
</div>
</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) => {
const canBuy = row.kind === 'quote' && row.itemId && w.state.family.stones >= marketPrice(w, row.itemId) * 5
const canSell = row.kind === 'quote' && row.itemId && (w.state.family.inventory[row.itemId] ?? 0) >= 5
const isCrisis = row.kind === 'calamity'
return (
<div key={i} className="ch-item" style={{ padding: '5px 8px' }}>
<span className="ch-month">{row.year}{row.month}</span>
<span>{row.text}</span>
{isCrisis && w.state.family.stones >= 50 && (
<span style={{ marginLeft: 8 }}>
<button
className="btn btn-sm"
title="引粥施药,换两家之谊"
onClick={() => {
w.reliefCalamity(50)
bump()
}}
>救济</button>
</span>
)}
{canBuy && (
<span style={{ marginLeft: 8 }}>
<button className="btn btn-sm" title="行情见底,出手吃进(买5)" onClick={() => { buyItem(w, row.itemId!, 5); bump() }}>吃进5</button>
</span>
)}
{canSell && !canBuy && (
<span style={{ marginLeft: 8 }}>
<button className="btn btn-sm" title="行情见顶,集市抛售(卖5)" onClick={() => { sellItem(w, row.itemId!, 5); bump() }}>抛售5</button>
</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)
if (!def) return null
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}{' '}
{dyn ? <span className="dim2" style={{ marginLeft: 6 }}>{dyn.prosperity}</span> : null}
{(dyn?.declineYears ?? 0) > 0 ? <span className="bad" style={{ marginLeft: 6 }}>{dyn.declineYears}/8</span> : null}
</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>
<div style={{ marginTop: 4 }}>
<div className="dim2" style={{ marginBottom: 3 }}>恩怨网(此家眼中各家)</div>
{Object.entries((dyn as { relationsWithOthers?: Record<string, number> })?.relationsWithOthers ?? {}).map(([oid, rel]) => {
const od = w.state.npcFamilies[oid]
return od ? (
<div key={oid} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.8rem', margin: '2px 0' }}>
<span className="dim">{od.name}</span>
<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span className={rel < -50 ? 'bad' : rel > 50 ? 'good' : 'dim'}>{rel}</span>
{rel < -50 && (
<button
className="btn btn-sm"
disabled={w.state.family.stones < 50}
title="耗 50 灵石劝和两家"
onClick={() => { w.mediateNpcs(npc.id, oid); bump() }}
>调停</button>
)}
</span>
</div>
) : null
})}
</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>
)
}