【主线一:世界自演进深化《八方风雨》】 - NPC 战略姿态 stance:守成/扩张/隐忍/结盟四态,年首评估(era>景气>power>relation) 全行为调制:raid 概率×2/×0.4/×0、互攻阈值 -40/-70/不攻、贸量 ×1.3/×0.7、 赠礼收益 ally×1.3、结盟门槛 ally 放宽至30——NPC 从统计物理到行为战略 - 超卖潮(谷贱伤农):池>1.8×base 世界产能回调×0.7 + 年度播报(防玩家玩崩市场) - 盟约连坐:结盟令世仇(rel<-50)对玩家-20;盟者世仇 raid 概率×2 - 盟友求援:互攻失利盟友告急(distress 旗标)——WorldPanel 响应(出资200/见死不救) - 干预天下:调停(50灵石令世仇-60→-25、声望+2)/ 资助盟友;恩怨网情报视图(各家关系展开+调停钮) 【主线二:孤立系统归并引擎】 - P0 兜底修复:empty() 收敛 makeWorldSimState 单源 + normalize 补 tideTicks/calamityLeft/ era/eraStartYear/prosperity/distress/overfluxYear(旧档 NaN 级联防死) - FxGate 从 engine/kernel/fxqueue.ts 并入 ui/fx.ts(kernel 清纯引擎;deltas 顶层导出, 3 处引用 10 行改动);engine 侧零 UI 依赖再下一城 - 音效语义补充:onFx→blade/pulse/spark→sWar/sGong/sBell;onLog kind 兜底保留(不静音) - store.advance drift/season 段左移 fx.ts(fxSeason 复活接管 season 检测) - 死码清除:timesense.tideOf/tintLabel、tideDir 全链(类型/初始/写入/domain) - domain.ts worldSim 局部类型补全 0.1.18/0.1.19 全部新字段(断言类型安全) 【测试】1002 全绿(41 套件):stance-world 7 例(兜底/姿态/raid/超卖/连坐/调停/求援)
231 lines
13 KiB
TypeScript
231 lines
13 KiB
TypeScript
import { useGameStore } from '../store'
|
||
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 ?? {}
|
||
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.inventory[row.itemId] ?? 0) >= 5
|
||
const canSell = row.kind === 'quote' && row.itemId && w.state.family.stones >= marketPrice(w, row.itemId) * 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={() => {
|
||
const npc = Object.values(w.state.npcFamilies)[w.rng.int(0, 3)]
|
||
npc.relation = Math.min(100, npc.relation + 8)
|
||
w.state.family.stones -= 50
|
||
w.log('info', `开仓济世(灵石50),${npc.name} 感念恩义,关系+8。`)
|
||
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)
|
||
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>
|
||
<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>
|
||
)
|
||
}
|