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
@@ -82,7 +82,7 @@ describe('审计回归:P0 修复固化', () => {
})
it('防御性修补后金钟罩不变(行为等价确认)', () => {
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('9531969e')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('aeefac96')
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('c923fdd1')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('5edde2cd')
})
})
+5 -4
View File
@@ -7,11 +7,12 @@ import { World } from '../src/renderer/game/engine/runtime/World'
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
*/
// 0.1.16 资源闭环基线:修为耗草/炼丹概率化/铸器/灵脉折算/NPC战力/灾变落效/渡劫药力/结盟后固化。
// 0.1.17 世界真炉膛基线:市场实体化(供给/需求呼吸+NPC上桌+玩家买卖回写)
// + NPC博弈(互攻/关系网/战争伤骨)+ 灾年持续化后固化。
const GOLDEN: Record<string, Record<number, string>> = {
'bell-seed-1': { 560: '9531969e', 1200: 'cdd70056', 2160: '7e33a89d' },
'bell-seed-2': { 560: 'f2ecc3e1', 1200: 'dab62832', 2160: '03c02233' },
'bell-seed-3': { 560: 'aeefac96', 1200: '9eda1d0f', 2160: 'b3bcf822' }
'bell-seed-1': { 560: 'c923fdd1', 1200: '2cb58265', 2160: 'a367ba40' },
'bell-seed-2': { 560: 'e98eb9fa', 1200: '3dd495e9', 2160: '17a588c2' },
'bell-seed-3': { 560: '5edde2cd', 1200: '476512f7', 2160: 'bc2d55d8' }
}
const TIERS = [
+3 -3
View File
@@ -162,7 +162,7 @@ describe('GameFacade 门面', () => {
const f = new GameFacade(w, 1)
const info = f.about()
expect(info.title).toBe('仙途家族志')
expect(info.version).toContain('0.1.16')
expect(info.version).toContain('0.1.17')
expect(info.modules).toBeGreaterThanOrEqual(11)
expect(info.systems).toBeGreaterThan(0)
expect(info.plugins).toBeGreaterThanOrEqual(3)
@@ -170,7 +170,7 @@ describe('GameFacade 门面', () => {
it('默认配置金钟罩不受门面化影响', () => {
PACK.reset()
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('9531969e')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('aeefac96')
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('c923fdd1')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('5edde2cd')
})
})
+3 -1
View File
@@ -105,7 +105,9 @@ describe('diplomacy 外交', () => {
const before = npc.relation
expect(giftNpc(w, npc.id, 120)).toBe(true)
expect(npc.relation).toBe(100)
expect(w.state.family.stones).toBe(800 - 120)
// 0.1.17 回礼机制:30% 概率回灵石(≤+15)、30% 概率回灵草(不破灵石断言)
expect(w.state.family.stones).toBeGreaterThanOrEqual(800 - 120)
expect(w.state.family.stones).toBeLessThanOrEqual(800 - 120 + 15)
})
it('赠礼不足预算时拒绝且分文不动', () => {
+3 -3
View File
@@ -80,9 +80,9 @@ describe('PluginCore 插件协议', () => {
})
it('默认管线金钟罩不受插件层影响', () => {
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('9531969e')
expect(stateFingerprint(longRun('bell-seed-2').state)).toBe('f2ecc3e1')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('aeefac96')
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('c923fdd1')
expect(stateFingerprint(longRun('bell-seed-2').state)).toBe('e98eb9fa')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('5edde2cd')
})
it('facade 插件查询与 about.plugins', () => {
+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)
})
})