【世界种子生成器(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 通过
204 lines
6.8 KiB
TypeScript
204 lines
6.8 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { World } from '../src/renderer/game/engine/runtime/World'
|
|
import { combatPowerOf, resolveEncounter, resolveRaid, rollWarbooty } from '../src/renderer/game/engine/runtime/Systems/combat'
|
|
import { ENEMIES } from '../src/renderer/game/data/secrets'
|
|
import { sendMission, recallAll } from '../src/renderer/game/engine/runtime/Systems/missions'
|
|
import { missionById } from '../src/renderer/game/data/secrets'
|
|
import { anyNpc } from './world.helpers'
|
|
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('combat 战斗结算', () => {
|
|
it('战力复数化:高装备>低装备,闭关者加成累计', () => {
|
|
const w = baseWorld('cb-a')
|
|
const c = w.state.members['x4']
|
|
const p0 = combatPowerOf(w, c)
|
|
w.state.family.inventory['weapon-qi'] = 3
|
|
w.equip(c.id, 'weapon-qi')
|
|
const p1 = combatPowerOf(w, c)
|
|
expect(p1).toBeGreaterThan(p0)
|
|
c.techniqueRank = 2
|
|
expect(combatPowerOf(w, c)).toBeGreaterThan(p1)
|
|
})
|
|
|
|
it('重伤员战力折减(health 因子)', () => {
|
|
const w = baseWorld('cb-b')
|
|
const c = w.state.members['x3']
|
|
const p0 = combatPowerOf(w, c)
|
|
c.health = 20
|
|
const p1 = combatPowerOf(w, c)
|
|
expect(p1).toBeLessThan(p0)
|
|
expect(p1).toBeGreaterThan(p0 * 0.3)
|
|
})
|
|
|
|
it('家族战力随供奉职事提升', () => {
|
|
const w = baseWorld('cb-c')
|
|
const before = w.familyPower()
|
|
w.assignPost('x3', 'guardian')
|
|
w.assignPost('x4', 'guardian')
|
|
expect(w.familyPower()).toBeGreaterThan(before)
|
|
})
|
|
|
|
it('遭遇战必胜路径产出战利并写入战报', () => {
|
|
const w = baseWorld('cb-d')
|
|
const c = w.state.members['x4']
|
|
c.realm = { major: 'foundation', minor: 0 }
|
|
c.health = 100
|
|
const res = resolveEncounter(w, {
|
|
title: '模拟遭遇',
|
|
enemy: ENEMIES.find((e) => e.id === 'e-huiyuan')!,
|
|
risk: 0.2,
|
|
team: [c],
|
|
kind: 'scout',
|
|
year: w.state.year,
|
|
month: w.state.month
|
|
})
|
|
expect(res.win).toBe(true)
|
|
expect(res.loot).toBeTruthy()
|
|
expect(w.state.battles.length).toBe(1)
|
|
expect(w.state.battles[0].lines.length).toBeGreaterThan(2)
|
|
})
|
|
|
|
it('遭遇战必败路径全体重伤/损失且不崩塌', () => {
|
|
const w = baseWorld('cb-e')
|
|
const c = w.state.members['x3']
|
|
c.realm = { major: 'mortal', minor: 0 }
|
|
c.health = 50
|
|
const res = resolveEncounter(w, {
|
|
title: '必败模拟',
|
|
enemy: ENEMIES.find((e) => e.id === 'e-kuangzun')!,
|
|
risk: 1,
|
|
team: [c],
|
|
kind: 'scout',
|
|
year: w.state.year,
|
|
month: w.state.month
|
|
})
|
|
expect(res.win).toBe(false)
|
|
expect(c.health).toBeLessThanOrEqual(50)
|
|
expect(w.state.battles[w.state.battles.length - 1].winner).toBe('enemy')
|
|
})
|
|
|
|
it('raid 后关系变化方向正确', () => {
|
|
const w = baseWorld('cb-f')
|
|
const npc = w.state.npcFamilies[anyNpc(w, ['n-nulei'])]
|
|
npc.relation = -60
|
|
const npcId = anyNpc(w, ['n-nulei'])
|
|
const pre = npc.power
|
|
const team = [w.state.members['x4']]
|
|
team[0].realm = { major: 'core', minor: 0 }
|
|
const res = resolveRaid(w, npcId, team)
|
|
if (res.win) {
|
|
expect(npc.relation).toBeGreaterThan(-60)
|
|
} else {
|
|
expect(npc.relation).toBeLessThan(-60)
|
|
expect(w.state.family.stones).toBeLessThanOrEqual(1000)
|
|
}
|
|
void pre
|
|
})
|
|
|
|
it('rollWarbooty 概率性产物写入库存或功法阁', () => {
|
|
const w = baseWorld('cb-g')
|
|
const loot = rollWarbooty(w, {
|
|
resources: { lingcao: [5, 5] },
|
|
artifactChance: 1,
|
|
techniqueChance: 1
|
|
})
|
|
expect(loot.lingcao).toBe(5)
|
|
const arti = Object.keys(loot).find((k) => k.startsWith('weapon'))
|
|
expect(arti).toBeTruthy()
|
|
expect(w.state.family.techniques.length).toBeGreaterThan(2)
|
|
})
|
|
|
|
it('战斗人数攻击力线性叠加(阵型抖动内保持单调)', () => {
|
|
const w = baseWorld('cb-h')
|
|
const a = w.state.members['x3']
|
|
const b = w.state.members['x4']
|
|
const solo = combatPowerOf(w, a)
|
|
const duo = combatPowerOf(w, a) + combatPowerOf(w, b)
|
|
expect(duo).toBeGreaterThan(solo)
|
|
})
|
|
})
|
|
|
|
describe('missions 探索任务', () => {
|
|
it('派遣属性校验:人数不足/重复派遣/并发上限', () => {
|
|
const w = baseWorld('ms-a')
|
|
expect(sendMission(w, 'm-guzhan', ['x1'])).toBe(false) // 不足 min 2
|
|
expect(sendMission(w, 'm-guzhan', ['x3', 'x4'])).toBe(true)
|
|
expect(sendMission(w, 'm-guzhan', ['x4', 'x5'])).toBe(false) // x4 已在途
|
|
expect(sendMission(w, 'm-anmoku', ['x4'])).toBe(false) // 在职者二次征召被拒
|
|
})
|
|
|
|
it('队伍出发后成员状态为出探,无法再被派', () => {
|
|
const w = baseWorld('ms-b')
|
|
sendMission(w, 'm-anmoku', ['x3'])
|
|
expect(w.state.members['x3'].state).toBe('expedition')
|
|
expect(sendMission(w, 'm-anmoku', ['x3', 'x5'])).toBe(false)
|
|
})
|
|
|
|
it('任务按期推进并最终完成,战利入库且状态归位', () => {
|
|
const w = baseWorld('ms-c')
|
|
const c = w.state.members['x5']
|
|
c.realm = { major: 'qi', minor: 5 } // 保证能打
|
|
const squad = [c, w.state.members['x3']]
|
|
sendMission(w, 'm-anmoku', squad.map((x) => x.id))
|
|
const m = w.state.missions[0]
|
|
let guard = 0
|
|
while (!m.done && guard < 40) {
|
|
if (w.state.gameOver) break
|
|
w.advanceMonth()
|
|
guard++
|
|
}
|
|
expect(m.done).toBe(true)
|
|
expect(['success', 'retreat', 'disband']).toContain(m.result)
|
|
// 若成功则状态归位闲居
|
|
if (m.result === 'success') {
|
|
expect(w.state.members['x3'].state).not.toBe('expedition')
|
|
expect(w.state.members['x5'].state).not.toBe('expedition')
|
|
}
|
|
})
|
|
|
|
it('召回立刻释放队员', () => {
|
|
const w = baseWorld('ms-d')
|
|
sendMission(w, 'm-anmoku', ['x3'])
|
|
const m = w.state.missions[0]
|
|
recallAll(w, m.id)
|
|
expect(m.done).toBe(true)
|
|
expect(w.state.members['x3'].state).toBe('idle')
|
|
})
|
|
|
|
it('全员告急:队员尽亡任务仍完成不挂死(保护)', () => {
|
|
const w = baseWorld('ms-e')
|
|
sendMission(w, 'm-anmoku', ['x3'])
|
|
const m = w.state.missions[0]
|
|
w.state.members['x3'].alive = false
|
|
w.state.members['x3'].deathYear = w.state.year
|
|
w.advanceMonth()
|
|
expect(m.done).toBe(true)
|
|
})
|
|
|
|
it('秘境阶段索引不会越界', () => {
|
|
const w = baseWorld('ms-f')
|
|
const def = missionById('m-anmoku')
|
|
const c = w.state.members['x5']
|
|
c.realm = { major: 'foundation', minor: 0 }
|
|
sendMission(w, 'm-anmoku', [c.id])
|
|
const m = w.state.missions[0]
|
|
let guard = 0
|
|
while (!m.done && guard < 60) {
|
|
w.advanceMonth()
|
|
guard++
|
|
if (m.stage >= def.stages.length + 1 && !m.done) {
|
|
// 不允许无限加厚
|
|
break
|
|
}
|
|
}
|
|
expect(m.stage).toBeLessThanOrEqual(def.stages.length)
|
|
})
|
|
})
|