Files
thzxx ec25bf92fc v0.1.24: 寰宇初构——世界种子生成器 + 插件时轮锚点(1034 全绿)
【世界种子生成器(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 通过
2026-08-23 16:15:22 +08:00

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)
})
})