【世界种子生成器(NPC 数量不再定死)】 - sim/worldgen.ts:generateWorld(seed) 确定性塑造天下——NPC 4~8 家随机 (老牌世家模板 + 词库组合新贵,名字去重)、初始关系网(1-2世仇+1盟友)、 开局 era 三态、区域风味、市场偏移 ±15% - 独立派生 rng(seed::worldgen)——World.rng 主序列零消耗:同 seed 世界可重放、 玩法随机序列不受生成器移动(金钟罩玩法序保护) - state.worldGen 落档(normalize 旧档按 seed 重放=同世界,存档兼容) - creation 初始化 npcFamilies/initSim 关系网/era/池偏置全走 worldgen——开局即恩怨 - 局内补位目标 = 开局家数(4~8),生灭闭环持续 【插件深化(0.1.24 第二组钩子)】 - PluginContext.beforePhase/afterPhase(时轮锚点:phase 前后挂勾,卸载全摘) - PluginContext.addNpcTemplate(世界模板注入——词库插件化) - plugin-public 文档同步;示例 Plugin 契约升级 【测试适配(世界名单随机化)】 - 静态 npc id 假设全面改动态取家(anyNpc helper 共享); - 难度梯度用例改「同 seed 不同难度」比较;worldgen 确定性/范围/去重/关系对称 7 例 【测试】1034 全绿(45 套件);金钟罩 0.1.24 基线固化(worldgen 随机世界)+ worldAnnals 等; build 通过
268 lines
11 KiB
TypeScript
268 lines
11 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { World } from '../src/renderer/game/engine/runtime/World'
|
|
import { combatPowerOf } from '../src/renderer/game/engine/runtime/Systems/combat'
|
|
import { marketPrice, buyItem, sellItem, buyTechnique } from '../src/renderer/game/engine/sim/Market'
|
|
import { resolveBreakthrough, monthlyRate } from '../src/renderer/game/engine/runtime/Systems/cultivation'
|
|
import { applyEventChoice } from '../src/renderer/game/engine/runtime/Systems/events'
|
|
import { computeGenealogy } from '../src/renderer/game/engine/narrative/genealogy'
|
|
import { matchesCondPub } from './events.helpers'
|
|
import { resetSaveBus, attachLogSink } from './world.helpers'
|
|
|
|
describe('Combat power', () => {
|
|
it('scales with realm', () => {
|
|
const w = World.create({ seed: 'p', surname: '赵', familyName: '赵家', motto: 'm', difficulty: 'normal' })
|
|
const head = w.head()
|
|
const power = combatPowerOf(w, head)
|
|
expect(power).toBeGreaterThan(3)
|
|
power > 0
|
|
})
|
|
|
|
it('higher realm beats lower realm', () => {
|
|
const w = World.create({ seed: 'p2', surname: '赵', familyName: '赵家', motto: 'm', difficulty: 'normal' })
|
|
const low = w.state.members['x5']
|
|
const high = w.state.members['x4']
|
|
expect(combatPowerOf(w, high)).toBeGreaterThan(combatPowerOf(w, low))
|
|
})
|
|
})
|
|
|
|
describe('Market', () => {
|
|
it('buy/sell roundtrip keeps stones consistent', () => {
|
|
const w = World.create({ seed: 'mk', surname: '王', familyName: '王家', motto: 'm', difficulty: 'normal' })
|
|
const fam = w.state.family
|
|
const price = marketPrice(w, 'lingcao')
|
|
const stones0 = fam.stones
|
|
const buyOk = buyItem(w, 'lingcao', 11)
|
|
expect(buyOk).toBe(stones0 >= price * 11)
|
|
if (buyOk) {
|
|
expect(fam.inventory['lingcao']).toBe(60 + 11)
|
|
expect(fam.stones).toBe(stones0 - price * 11)
|
|
sellItem(w, 'lingcao', 11)
|
|
expect(fam.stones).toBe(stones0)
|
|
}
|
|
})
|
|
|
|
it('cannot buy beyond treasury', () => {
|
|
const w = World.create({ seed: 'mk2', surname: '王', familyName: '王家', motto: 'm', difficulty: 'hard' })
|
|
w.state.family.stones = 10
|
|
expect(buyItem(w, 'lingcao', 999)).toBe(false)
|
|
})
|
|
|
|
it('buy technique adds to library once', () => {
|
|
const w = World.create({ seed: 'mk3', surname: '王', familyName: '王家', motto: 'm', difficulty: 'normal' })
|
|
w.state.family.stones = 5000
|
|
expect(buyTechnique(w, 't-zhenyu', 700)).toBe(true)
|
|
expect(w.state.family.techniques.includes('t-zhenyu')).toBe(true)
|
|
expect(buyTechnique(w, 't-zhenyu', 700)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('Breakthrough', () => {
|
|
it('advances realm on success', () => {
|
|
const w = World.create({ seed: 'bt', surname: '李', familyName: '李家', motto: 'm', difficulty: 'easy' })
|
|
const c = w.state.members['x5']
|
|
c.realm = { major: 'mortal', minor: 0 }
|
|
c.realmProgress = 100
|
|
c.perception = 9
|
|
c.mind = 9
|
|
resolveBreakthrough(w, c, 0.3)
|
|
expect(c.realm.major).toBe('qi')
|
|
})
|
|
|
|
it('failed breakthrough costs progress but not death', () => {
|
|
const w = World.create({ seed: 'bt2', surname: '李', familyName: '李家', motto: 'm', difficulty: 'normal' })
|
|
const c = w.state.members['x5']
|
|
c.realm = { major: 'mortal', minor: 0 }
|
|
c.realmProgress = 100
|
|
resolveBreakthrough(w, c, -0.5)
|
|
// chance was min-clamped; both outcomes acceptable, but check invariants
|
|
if (c.realm.major === 'mortal') {
|
|
expect(c.realmProgress).toBeLessThan(100)
|
|
} else {
|
|
expect(c.realmProgress).toBe(0)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('Event conditions', () => {
|
|
it('leading event ids exist', () => {
|
|
const w = World.create({ seed: 'evc', surname: '刘', familyName: '刘家', motto: 'm', difficulty: 'normal' })
|
|
expect(matchesCondPub(w, { minBuilding: { id: 'lingtian', level: 1 } })).toBe(true)
|
|
expect(matchesCondPub(w, { minBuilding: { id: 'danfang', level: 1 } })).toBe(false)
|
|
expect(matchesCondPub(w, { minAdult: 3 })).toBe(true)
|
|
expect(matchesCondPub(w, { maxAdult: 2 })).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('Marriage & equipment & rites', () => {
|
|
it('rejects siblings, accepts strangers', () => {
|
|
const w = World.create({ seed: 'marry', surname: '王', familyName: '王家', motto: 'm', difficulty: 'normal' })
|
|
const bro = w.state.members['x3']
|
|
const sis = w.state.members['x5']
|
|
expect(w.canMarry(bro.id)).toBe(true)
|
|
expect(w.canMarry(sis.id)).toBe(true)
|
|
expect(w.marriageCandidatesOf(bro.id).map((c) => c.id)).not.toContain(sis.id)
|
|
})
|
|
|
|
it('widow can remarry via candidates', () => {
|
|
const w = World.create({ seed: 'remarry', surname: '赵', familyName: '赵家', motto: 'm', difficulty: 'normal' })
|
|
const wife = w.state.members['x2']
|
|
wife.spouseId = 'x1'
|
|
w.state.members['x1'].alive = false
|
|
expect(w.canMarry(wife.id)).toBe(true)
|
|
expect(w.marriageCandidatesOf(wife.id).length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('equipping artifact raises combat power', () => {
|
|
const w = World.create({ seed: 'equip', surname: '钱', familyName: '钱家', motto: 'm', difficulty: 'normal' })
|
|
const c = w.state.members['x4']
|
|
const power0 = combatPowerOf(w, c)
|
|
w.state.family.inventory['weapon-qi'] = 1
|
|
w.equip(c.id, 'weapon-qi')
|
|
expect(combatPowerOf(w, c)).toBeGreaterThan(power0)
|
|
})
|
|
|
|
it('ancestral rite costs, respects cooldown, grants boon', () => {
|
|
const w = World.create({ seed: 'rite', surname: '孙', familyName: '孙家', motto: 'm', difficulty: 'easy' })
|
|
const fam = w.state.family
|
|
fam.stones = 1000
|
|
const rep0 = fam.reputation
|
|
const head = w.state.members['x1']
|
|
head.realmProgress = 50
|
|
expect(w.ancestralRite()).toBe(true)
|
|
expect(fam.stones).toBe(850)
|
|
expect(fam.reputation).toBe(rep0 + 6)
|
|
expect(head.realmProgress).toBe(53)
|
|
expect(w.ancestralRite()).toBe(false)
|
|
})
|
|
|
|
it('taunt has 1-year cooldown', () => {
|
|
const w = World.create({ seed: 'taunt', surname: '周', familyName: '周家', motto: 'm', difficulty: 'normal' })
|
|
const npcId = Object.keys(w.state.npcFamilies)[0]!
|
|
const before = w.state.npcFamilies[npcId].relation
|
|
expect(w.tauntNpc(npcId)).toBe(true)
|
|
expect(w.state.npcFamilies[npcId].relation).toBe(before - 20)
|
|
expect(w.tauntNpc(npcId)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('Yearly report', () => {
|
|
it('generates a report per full year', () => {
|
|
const w = World.create({ seed: 'rep', surname: '吴', familyName: '吴家', motto: 'm', difficulty: 'normal' })
|
|
for (let i = 0; i < 14; i++) w.advanceMonth()
|
|
expect(w.state.yearlyReports.length).toBe(1)
|
|
const r = w.state.yearlyReports[0]
|
|
expect(r.year).toBe(1)
|
|
expect(r.nets).toBeGreaterThanOrEqual(0)
|
|
expect(r.power).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
describe('Posts (职事)', () => {
|
|
it('assigns up to max and rejects over-cap', () => {
|
|
const w = World.create({ seed: 'posts', surname: '徐', familyName: '徐家', motto: 'm', difficulty: 'normal' })
|
|
expect(w.assignPost('x3', 'elder')).toBe(true)
|
|
expect(w.assignPost('x5', 'elder')).toBe(true)
|
|
expect(w.assignPost('x4', 'elder')).toBe(false)
|
|
expect(w.postCount('elder')).toBe(2)
|
|
})
|
|
|
|
it('elder boosts cultivation rate', () => {
|
|
const w = World.create({ seed: 'posts2', surname: '许', familyName: '许家', motto: 'm', difficulty: 'normal' })
|
|
const c = w.state.members['x5']
|
|
w.state.members['x3'].state = 'meditation'
|
|
c.realm = { major: 'qi', minor: 1 }
|
|
const r0 = monthlyRate(w, c)
|
|
w.assignPost('x3', 'elder')
|
|
w.assignPost('x4', 'elder')
|
|
const r1 = monthlyRate(w, c)
|
|
expect(r1).toBeGreaterThan(r0)
|
|
})
|
|
|
|
it('steward boosts market income', () => {
|
|
const w = World.create({ seed: 'posts3', surname: '邓', familyName: '邓家', motto: 'm', difficulty: 'normal' })
|
|
w.state.family.buildings['fangshi'] = 2
|
|
w.assignPost('x3', 'steward')
|
|
const stones0 = w.state.family.stones
|
|
w.advanceMonth()
|
|
expect(w.state.family.stones).toBeGreaterThan(stones0 + 110)
|
|
})
|
|
})
|
|
|
|
describe('Technique wudao (悟道)', () => {
|
|
it('accumulates progress and ranks up talents', () => {
|
|
const w = World.create({ seed: 'wudao', surname: '康', familyName: '康家', motto: 'm', difficulty: 'easy' })
|
|
const c = w.state.members['x4']
|
|
c.realm = { major: 'qi', minor: 1 }
|
|
c.realmProgress = 0
|
|
c.techniqueProgress = 95
|
|
c.techniqueRank = 0
|
|
c.techniqueId = 't-houtu'
|
|
c.perception = 9
|
|
for (let i = 0; i < 30; i++) w.advanceMonth()
|
|
expect(c.techniqueRank ?? 0).toBeGreaterThanOrEqual(1)
|
|
})
|
|
|
|
it('carries wudao to a disciple on major jump', () => {
|
|
const w = World.create({ seed: 'wd2', surname: '封', familyName: '封家', motto: 'm', difficulty: 'easy' })
|
|
const c = w.state.members['x1']
|
|
c.realm = { major: 'qi', minor: 9 }
|
|
c.realmProgress = 100
|
|
c.mind = 9
|
|
c.perception = 9
|
|
const child = w.state.members['x3']
|
|
child.techniqueId = 't-houtu'
|
|
child.techniqueProgress = 10
|
|
child.realm = { major: 'qi', minor: 2 }
|
|
resolveBreakthrough(w, c, 0.5)
|
|
expect(c.realm.major).toBe('foundation')
|
|
expect(child.techniqueProgress).toBeGreaterThanOrEqual(60)
|
|
})
|
|
})
|
|
|
|
describe('Monument & milestones', () => {
|
|
it('writes monument on head death, inherits reign', () => {
|
|
const w = World.create({ seed: 'mon', surname: '侯', familyName: '侯家', motto: 'm', difficulty: 'normal' })
|
|
const head = w.state.members['x1']
|
|
head.alive = false
|
|
head.deathYear = w.state.year
|
|
w.advanceMonth()
|
|
const monument = w.state.chronicle.find((e) => e.text.startsWith('功德碑'))
|
|
expect(monument).toBeTruthy()
|
|
expect(w.state.family.headId).toBe('x3')
|
|
expect(w.state.family.flag['reignStart']).toBe(w.state.year)
|
|
})
|
|
|
|
it('triggers centennial once at year 100', () => {
|
|
const w = World.create({ seed: 'cen', surname: '欧阳', familyName: '欧阳家', motto: 'm', difficulty: 'normal' })
|
|
resetSaveBus()
|
|
attachLogSink(w)
|
|
w.state.year = 99
|
|
w.state.month = 12
|
|
for (let i = 0; i < 3; i++) w.advanceMonth()
|
|
expect(w.state.pendingEvent).toBe('ev-centennial')
|
|
})
|
|
|
|
it('feisheng effect grants trail or ascends', () => {
|
|
const w = World.create({ seed: 'fs', surname: '皇甫', familyName: '皇甫家', motto: 'm', difficulty: 'normal' })
|
|
const c = w.state.members['x4']
|
|
c.realm = { major: 'spirit', minor: 1 }
|
|
applyEventChoice(w, 'ev-feisheng', 0)
|
|
expect(c.traits).toContain('fengxian')
|
|
})
|
|
})
|
|
|
|
describe('Genealogy', () => {
|
|
it('builds rows per generation with couple units', () => {
|
|
const w = World.create({ seed: 'gene', surname: '惠', familyName: '惠家', motto: 'm', difficulty: 'normal' })
|
|
const rows = computeGenealogy(w)
|
|
expect(rows.length).toBeGreaterThanOrEqual(2)
|
|
const second = rows.find((r) => r.gen === 2)
|
|
expect(second).toBeTruthy()
|
|
const units = second!.units
|
|
// x1/x2 couple should appear in gen1
|
|
const g1 = rows.find((r) => r.gen === 1)!
|
|
expect(g1.units.some((u) => u.parents[0]?.id === 'x1' || u.parents[1]?.id === 'x1')).toBe(true)
|
|
expect(units).toBeTruthy()
|
|
})
|
|
})
|