v0.1.17: 天下为市——世界真炉膛(市场实体化+NPC博弈+灾年持续)

【A 市场实体化(真库存循环)】
- 世界呼吸:池月供给 × 潮汐span - 常驻需求(worldSupplyRate 2% vs worldDemandRate 1.5%)
- NPC 上桌:def.sells/buys 逐月入池出池——池浅采不到→power-15%,池深抛货→市场暖意
- 玩家交易回写:buy 浅池价涨/sell 盈池价跌(tradeSettle 0.35 份额粒度);池深<35% 断供拒单(12% 是枯井)
- marketPrice 统一走 marketMultFor;本地 POOL_BASE/TECH 复刻清除(worldsim-data 单源)

【B 世界格局博弈】
- 战争伤骨:劫掠胜负回写 power(败-18%/胜+6%)+ dead 字段 raidCount/warCooldownYear 启用
- 社交回响:赠礼回礼(30% 灵石/30% 灵草)+ power 微涨;联姻/和约/结盟皆涨 power
- NPC 关系网:relationsWithOthers 填实(同风格亲30~55/异风格隙-35~15)+ 年首±5 漂移;世仇<-50 年首互攻(败方-18%)
- 灾年共苦:灾年 NPC 贸易 ×0.7;tide 对供给 ±0.3 弹性
- 灾年持续 6 月渐退;灾起/灾散快讯(kind 标记)

【UI】
- 天下面板:池深百分比(含断供阈值 tooltip)/本族天下排位/灾年余月
- 快讯响应化:行情见顶抛售5/见底吃进5/灾年救济(50灵石→关系8)
- MarketPanel 法帖价格改 techniquePrice()(硬编码清除)

【测试】994 全绿(38 套件,新增世界炉膛 8 例);金钟罩 0.1.17 基线固化
This commit is contained in:
2026-08-23 14:24:23 +08:00
parent 809707408b
commit ea9f5c9c5d
17 changed files with 353 additions and 53 deletions
+2 -2
View File
@@ -2,7 +2,7 @@ import { useGameStore } from '../store'
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 { marketPrice, buyItem, sellItem, buyTechnique, techniquePrice } 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'
@@ -122,7 +122,7 @@ export default function MarketPanel() {
.map((t) => {
const owned = fam.techniques.includes(t.id)
const p = t.grade <= 1 + cangshuLv
const priceT = techniqueGradeName(t.grade) === '' ? 300 : [120, 300, 700, 1600][t.grade] ?? 300
const priceT = techniquePrice(t.id)
return (
<tr key={t.id}>
<td><b>{t.name}</b></td>
+1 -1
View File
@@ -210,7 +210,7 @@ export default function SettingsPanel() {
· <br />
·
</div>
<div className="dim2" style={{ marginTop: 8 }}> 0.1.16 · Chronicle of the Immortal Clan</div>
<div className="dim2" style={{ marginTop: 8 }}> 0.1.17 · Chronicle of the Immortal Clan</div>
</div>
</div>
)
+50 -7
View File
@@ -1,7 +1,10 @@
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 type { WorldSimState } from '../../game/engine/sim/worldsim-data'
import { POOL_BASE, WORLDSIM } 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: '兽核' }
@@ -15,7 +18,7 @@ export default function WorldPanel() {
const [view, setView] = useState<string | null>(null)
if (!world) return null
const w = world
const ws = w.state.worldSim
const ws = w.state.worldSim as WorldSimState | undefined
if (!ws) return <div className="help-text"></div>
const tide = ws.tide ?? 0.5
@@ -23,6 +26,13 @@ export default function WorldPanel() {
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
return (
<div>
@@ -33,15 +43,18 @@ export default function WorldPanel() {
<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" 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 < 85 ? 'good' : 'dim'
const cls = r > 115 ? 'bad' : r < WORLDSIM.tradeFloorPct * 100 ? '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>
<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>
@@ -50,12 +63,42 @@ export default function WorldPanel() {
<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) => (
{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>