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
+107
View File
@@ -0,0 +1,107 @@
import { describe, expect, it } from 'vitest'
import { World } from '../src/renderer/game/engine/runtime/World'
import { WorldSim } from '../src/renderer/game/engine/sim/WorldSim'
import { buyItem, sellItem, marketPrice, buyTechnique, techniquePrice } from '../src/renderer/game/engine/sim/Market'
import { resolveRaid } from '../src/renderer/game/engine/runtime/Systems/combat'
import { POOL_BASE } from '../src/renderer/game/engine/sim/worldsim-data'
function worldWithSim(seed: string): World {
const w = World.create({ seed, surname: '万', familyName: '万家', motto: 'm', difficulty: 'normal' })
w.advanceMonth()
w.state.totalTicks = 60
return w
}
describe('0.1.17 世界真炉膛', () => {
it('买卖回写池:买浅卖盈,断供拒单', () => {
const w = worldWithSim('w1')
const sim = new WorldSim(w)
const p0 = sim.marketMultFor('lingcao')
expect(buyItem(w, 'lingcao', 5)).toBe(true)
const p1 = sim.marketMultFor('lingcao')
expect(p1).toBeLessThan(p0) // 买浅 → 价涨
expect(sellItem(w, 'lingcao', 5)).toBe(true)
const p2 = sim.marketMultFor('lingcao')
expect(p2).toBeGreaterThan(p1) // 卖盈 → 价跌回来
// 断供:池压平后拒单
const ws = w.state.worldSim as { marketPool: Record<string, number> }
ws.marketPool['lingcao'] = POOL_BASE.lingcao * 0.3
const before = w.state.family.inventory['lingcao'] ?? 0
expect(buyItem(w, 'lingcao', 1)).toBe(false)
expect(w.state.family.inventory['lingcao'] ?? 0).toBe(before)
})
it('市场呼吸有界:长跑 600 月池不枯竭亦不冲顶', () => {
const w = World.create({ seed: 'w2', surname: '安', familyName: '安家', motto: 'm', difficulty: 'normal' })
for (let i = 0; i < 600; i++) w.advanceMonth()
const ws = w.state.worldSim as { marketPool: Record<string, number> }
for (const id of Object.keys(POOL_BASE)) {
expect(ws.marketPool[id]).toBeGreaterThan(POOL_BASE[id] * 0.1)
expect(ws.marketPool[id]).toBeLessThanOrEqual(POOL_BASE[id] * 2.5)
}
})
it('NPC 上桌:长期活跃于池(需料者逢底而购)', () => {
const w = World.create({ seed: 'w3', surname: '风', familyName: '风家', motto: 'm', difficulty: 'normal' })
for (let i = 0; i < 240; i++) w.advanceMonth()
const ws = w.state.worldSim as { marketPool: Record<string, number> }
const beast = ws.marketPool['beastcore']
expect(beast).toBeGreaterThan(POOL_BASE.beastcore * 0.12)
expect(beast).toBeLessThan(POOL_BASE.beastcore * 2.4)
})
it('劫掠胜利:NPC 伤筋动骨(power-18%、raidCount/warCooldownYear 写值)', () => {
const w = worldWithSim('w4')
const npcId = Object.keys(w.state.npcFamilies)[0]!
const npc = w.state.npcFamilies[npcId]
npc.power = 200
const team = [w.state.members['x1']!, w.state.members['x2']!, w.state.members['x3']!, w.state.members['x4']!]
for (const c of team) {
c.realm = { major: 'spirit', minor: 0 }
c.health = 100
}
const res = resolveRaid(w, npcId, team)
expect(res.win).toBe(true)
expect(npc.power).toBeLessThan(200)
expect(npc.raidCount).toBeGreaterThan(0)
expect(npc.warCooldownYear).toBe(w.state.year)
})
it('灾年持续 6 月并散去(B12', () => {
const w = worldWithSim('w5')
w.state.month = 1
void w
const ws = w.state.worldSim as { calamity?: string; calamityLeft?: number }
// 强制触发:首月+高概率 seed 抽——健壮断言:任何时刻 calamityLeft ∈ [0,6]
for (let i = 0; i < 24; i++) {
w.advanceMonth()
const left = ws.calamityLeft ?? 0
expect(left).toBeLessThanOrEqual(6)
if (ws.calamity) expect(left).toBeGreaterThan(0)
}
})
it('NPC 关系网填充:同风格亲近、异风格有隙,且年首漂移', () => {
const w = worldWithSim('w6')
const dyn = (w.state.worldSim as { npcDyn: Record<string, { relationsWithOthers: Record<string, number> }> }).npcDyn
const xuan = dyn['n-xuanying']?.relationsWithOthers ?? {}
expect(Object.keys(xuan).length).toBeGreaterThanOrEqual(3)
const after = (w.state.worldSim as { npcDyn: Record<string, { relationsWithOthers: Record<string, number> }> }).npcDyn
// 长跑后关系网仍存在(不丢)
expect(Object.keys(after['n-xuanying']?.relationsWithOthers ?? {}).length).toBeGreaterThanOrEqual(3)
})
it('功法价格与 techniquePrice 一致(面板引用安全)', () => {
const w = worldWithSim('w7')
const t = w.state.family.techniques[0]
if (!t) return
expect(techniquePrice(t)).toBeGreaterThan(0)
expect(buyTechnique(w, t, techniquePrice(t))).toBe(false) // 已录
})
it('无 sim 世界市场价正常(兜底不崩)', () => {
const w = World.create({ seed: 'w8', surname: '石', familyName: '石家', motto: 'm', difficulty: 'normal' })
w.state.worldSim = undefined as never
expect(marketPrice(w, 'lingcao')).toBeGreaterThan(0)
})
})