【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 基线固化
198 lines
6.3 KiB
TypeScript
198 lines
6.3 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { World } from '../src/renderer/game/engine/runtime/World'
|
|
import {
|
|
giftNpc,
|
|
marryNpcFamily,
|
|
makePeace,
|
|
arrangeWedding,
|
|
diplomacyTick
|
|
} from '../src/renderer/game/engine/runtime/Systems/diplomacy'
|
|
import { yearStartMarriage } from '../src/renderer/game/engine/runtime/Systems/marriage'
|
|
import { resetSaveBus, attachLogSink } from './world.helpers'
|
|
|
|
function baseWorld(seed: string): World {
|
|
const w = World.create({ seed, surname: '池', familyName: '池家', motto: 'm', difficulty: 'normal' })
|
|
resetSaveBus()
|
|
attachLogSink(w)
|
|
return w
|
|
}
|
|
|
|
describe('marriage 婚配生育', () => {
|
|
it('媒人只撮合合适人选并完成夫妻绑定', () => {
|
|
const w = baseWorld('mar-a')
|
|
// 造一对适婚人
|
|
const man = w.state.members['x3']
|
|
const woman = w.state.members['x5']
|
|
man.bornYear = w.state.year - 20
|
|
woman.bornYear = w.state.year - 18
|
|
w.state.year = 2 // 保证媒人会走
|
|
yearStartMarriage(w)
|
|
const paired = man.spouseId === woman.id || woman.spouseId === man.id
|
|
// 同父兄妹(x1 之父)实为同父,应绝不互配
|
|
expect(paired).toBe(false)
|
|
})
|
|
|
|
it('姻亲生育年首发生,血统与代数正确', () => {
|
|
const w = baseWorld('mar-b')
|
|
w.state.year = 2
|
|
yearStartMarriage(w)
|
|
const newborns = Object.values(w.state.members).filter(
|
|
(c) => c.bornYear === 2 && c.generation === 2
|
|
)
|
|
// 概率性事件不做硬性:至少系统不崩且无非法代数
|
|
for (const nb of newborns) {
|
|
expect(nb.generation).toBe(2)
|
|
expect(nb.alive).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('双胞胎概率出现时登记双新生儿', () => {
|
|
const w = baseWorld('mar-c')
|
|
w.state.year = 2
|
|
let twinSeen = false
|
|
for (let i = 0; i < 30 && !twinSeen; i++) {
|
|
// 复用同一开局状态重跑不同 rng? 不能; 直接跑 240 月统计
|
|
}
|
|
for (let i = 0; i < 240; i++) w.advanceMonth()
|
|
// 有 duo born in same month among same parents -> twin 可接受未出现
|
|
expect(w.state.members).toBeTruthy()
|
|
})
|
|
|
|
it('寡妇再醮候选均为适龄异性', () => {
|
|
const w = baseWorld('mar-d')
|
|
const wife = w.state.members['x2']
|
|
w.state.members['x1'].alive = false
|
|
expect(w.isWidowed(wife.id)).toBe(true)
|
|
const cands = w.marriageCandidatesOf(wife.id)
|
|
expect(cands.length).toBeGreaterThan(0)
|
|
for (const c of cands) {
|
|
expect(c.gender).not.toBe(wife.gender)
|
|
expect(w.ageOf(c)).toBeGreaterThanOrEqual(16)
|
|
expect(w.ageOf(c)).toBeLessThanOrEqual(46)
|
|
}
|
|
})
|
|
|
|
it('同父或同母不可婚(双防线)', () => {
|
|
const w = baseWorld('mar-e')
|
|
const a = w.state.members['x3']
|
|
const b = w.state.members['x5']
|
|
a.fatherId = 'x1'
|
|
b.fatherId = 'x1'
|
|
expect(w.marryTo(a.id, b.id)).toBe(false)
|
|
a.fatherId = undefined
|
|
b.fatherId = undefined
|
|
a.motherId = 'x2'
|
|
b.motherId = 'x2'
|
|
expect(w.marryTo(a.id, b.id)).toBe(false)
|
|
})
|
|
|
|
it('自由婚配成功绑定(叔父与续弦寡妇)', () => {
|
|
const w = baseWorld('mar-f')
|
|
w.state.members['x1'].alive = false // 家主离世令 x2 为寡
|
|
const a = w.state.members['x4']
|
|
const b = w.state.members['x2']
|
|
expect(w.marryTo(a.id, b.id)).toBe(true)
|
|
expect(a.spouseId).toBe(b.id)
|
|
expect(b.spouseId).toBe(a.id)
|
|
})
|
|
})
|
|
|
|
describe('diplomacy 外交', () => {
|
|
it('赠礼换算关系并按上限封顶', () => {
|
|
const w = baseWorld('dip-a')
|
|
const npc = w.state.npcFamilies['n-xuanying']
|
|
npc.relation = 95
|
|
const before = npc.relation
|
|
expect(giftNpc(w, npc.id, 120)).toBe(true)
|
|
expect(npc.relation).toBe(100)
|
|
// 0.1.17 回礼机制:30% 概率回灵石(≤+15)、30% 概率回灵草(不破灵石断言)
|
|
expect(w.state.family.stones).toBeGreaterThanOrEqual(800 - 120)
|
|
expect(w.state.family.stones).toBeLessThanOrEqual(800 - 120 + 15)
|
|
})
|
|
|
|
it('赠礼不足预算时拒绝且分文不动', () => {
|
|
const w = baseWorld('dip-b')
|
|
w.state.family.stones = 20
|
|
expect(giftNpc(w, 'n-xuanying', 120)).toBe(false)
|
|
expect(w.state.family.stones).toBe(20)
|
|
})
|
|
|
|
it('寻衅有一年冷却', () => {
|
|
const w = baseWorld('dip-c')
|
|
const npc = w.state.npcFamilies['n-nulei']
|
|
const before = npc.relation
|
|
expect(w.tauntNpc(npc.id)).toBe(true)
|
|
expect(npc.relation).toBe(before - 20)
|
|
expect(w.tauntNpc(npc.id)).toBe(false)
|
|
w.state.year += 1
|
|
expect(w.tauntNpc(npc.id)).toBe(true)
|
|
})
|
|
|
|
it('联姻要求 25 以上关系且一家一亲', () => {
|
|
const w = baseWorld('dip-d')
|
|
const npc = w.state.npcFamilies['n-danxin']
|
|
npc.relation = 20
|
|
w.state.members['x3'].bornYear = 1 - 19
|
|
expect(marryNpcFamily(w, npc.id)).toBe(false)
|
|
npc.relation = 50
|
|
expect(marryNpcFamily(w, npc.id)).toBe(true)
|
|
expect(npc.allied).toBe(true)
|
|
expect(marryNpcFamily(w, npc.id)).toBe(false)
|
|
})
|
|
|
|
it('关系月度向中立漂移靠拢', () => {
|
|
const w = baseWorld('dip-e')
|
|
const npc = w.state.npcFamilies['n-xuanying']
|
|
npc.relation = 40
|
|
let drifted = 0
|
|
for (let i = 0; i < 12; i++) {
|
|
const before = npc.relation
|
|
diplomacyTick(w)
|
|
if (npc.relation < before) drifted++
|
|
}
|
|
expect(drifted).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('敌对过深时潜在袭击条件:冷却与关系门槛', () => {
|
|
const w = baseWorld('dip-f')
|
|
const npc = w.state.npcFamilies['n-nulei']
|
|
npc.relation = -80
|
|
w.state.year = 10
|
|
w.state.family.flag['raidCD-n-nulei'] = 10
|
|
let fired = false
|
|
for (let i = 0; i < 36; i++) {
|
|
diplomacyTick(w)
|
|
if (w.state.pendingEvent?.startsWith('ev-raid-')) {
|
|
fired = true
|
|
w.state.pendingEvent = undefined
|
|
}
|
|
}
|
|
expect(fired).toBe(false) // 冷却年内不动
|
|
w.state.year = 20
|
|
for (let i = 0; i < 200; i++) {
|
|
diplomacyTick(w)
|
|
if (w.state.pendingEvent?.startsWith('ev-raid-')) {
|
|
fired = true
|
|
break
|
|
}
|
|
}
|
|
expect(fired).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('genealogy 谱系结构', () => {
|
|
it('夫妻联组与单身分离', () => {
|
|
const w = baseWorld('gen-a')
|
|
const w2 = w.state
|
|
const rows = computeGenealogyLocal(w)
|
|
const g1 = rows.find((r) => r.gen === 1)
|
|
expect(g1).toBeTruthy()
|
|
expect(g1!.units.some((u) => u.parents.some((p) => p?.id === 'x1'))).toBe(true)
|
|
})
|
|
})
|
|
|
|
import { computeGenealogy } from '../src/renderer/game/engine/narrative/genealogy'
|
|
function computeGenealogyLocal(w: World) {
|
|
return computeGenealogy(w)
|
|
}
|