【世界模型:世界会死人也会新生】 - 家族生灭:declineYears(power<58+景气<38 连续8年)→ 覆灭(最强邻分食+史官广播)+ 新贵补位(6%/年,乱世×2) —— NPC 永远四家铁打的现状终结;玩家补刀(raid×0.82)可加速衰亡 - npcById 双源可空(静态+动态注册表;不再 throw——15 处消费者防御化) - purgeNpc 全局清理(raidCD/事件队列/恩怨网/distress/动态 def) - 秘境产出回池(loot 20% 经 tradeSettle 回流)——资源循环最后单向封口 - WorldSnapshot 只读摘要门面(UI/史书消费,零行为变更) - 潮汐长波(10 年正弦 ±0.08 叠加,灵潮三十年河东) 【插件生态化(0.1.8 后第一次大进化)】 - 持久化:GameState.plugins 落盘 + PLUGIN_REGISTRY 读档重装(engineFromSnapshot 真实路径验证) - plugin-public.ts 公共导出面(第三方单入口+纪律) - 双闸:能力卡随插件启停联动;池自动回滚 - 契约补完:Proxy 追踪(池/卡/pack 自动回滚)+ onUninstall 真调 + 能力卡入 World 实例(防跨档泄漏) - SettingsPanel 启停按钮/依赖详情;demo 与 tests 去重(发现并修复:npcById 双源静态源未绑定——全局 NPC 停滞回归) 【测试】1027 全绿(44 套件):dynasty-plugin 7 例(覆灭/新贵/回池/快照/持久化/双闸/自动回滚); 金钟罩 0.1.23 基线固化;build 通过
338 lines
12 KiB
TypeScript
338 lines
12 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
import {
|
||
MAJORS,
|
||
MAJOR_ORDER,
|
||
MAJOR_NAMES,
|
||
describeRealm,
|
||
realmTier,
|
||
compareRealm,
|
||
nextRealm,
|
||
maxRealmExp,
|
||
basePower,
|
||
breakthroughBaseChance,
|
||
realmDeathChance,
|
||
TECHNIQUE_GRADE_NAMES
|
||
} from '../src/renderer/game/data/realms'
|
||
import { ELEMENT_LIST, ROOT_GRADES, ROOT_GRADE_NAMES, describeRoots } from '../src/renderer/game/data/elements'
|
||
import { ITEMS, ARTIFACT_POWER } from '../src/renderer/game/data/items'
|
||
import { TECHNIQUES } from '../src/renderer/game/data/techniques'
|
||
import { BUILDINGS } from '../src/renderer/game/data/buildings'
|
||
import { MISSIONS, ENEMIES, missionById } from '../src/renderer/game/data/secrets'
|
||
import { NPCS, npcById } from '../src/renderer/game/data/npcs'
|
||
import { POSTS, POST_ORDER } from '../src/renderer/game/data/posts'
|
||
import { EVENTS } from '../src/renderer/game/data/events'
|
||
import { MAJOR_RATE, masteryRateOfMajor } from '../src/renderer/game/data/pacing'
|
||
import { SURNAME_POOL, MALE_GIVEN, FEMALE_GIVEN } from '../src/renderer/game/engine/kernel/names'
|
||
import { TRAITS } from '../src/renderer/game/data/traits'
|
||
|
||
describe('realms 境界表', () => {
|
||
it('每大境界结构完整(名称/层数/寿元/修为递增进)', () => {
|
||
for (const major of MAJOR_ORDER) {
|
||
const def = MAJORS[major]
|
||
expect(def.name).toBeTruthy()
|
||
expect(def.minorLayers).toBeGreaterThanOrEqual(0)
|
||
expect(def.lifespan).toBeGreaterThan(0)
|
||
expect(def.expBase).toBeGreaterThanOrEqual(0)
|
||
expect(def.expGrowth).toBeGreaterThanOrEqual(1)
|
||
}
|
||
expect(MAJOR_NAMES.mortal).toBe('凡人')
|
||
expect(MAJOR_NAMES.spirit).toBe('化神')
|
||
})
|
||
|
||
it('大境界寿元单调递增', () => {
|
||
for (let i = 1; i < MAJOR_ORDER.length; i++) {
|
||
expect(MAJORS[MAJOR_ORDER[i]].lifespan).toBeGreaterThan(MAJORS[MAJOR_ORDER[i - 1]].lifespan)
|
||
}
|
||
})
|
||
|
||
it('describeRealm 映射正确', () => {
|
||
expect(describeRealm({ major: 'mortal', minor: 0 })).toBe('凡人')
|
||
expect(describeRealm({ major: 'qi', minor: 0 })).toBe('炼气1层')
|
||
expect(describeRealm({ major: 'qi', minor: 8 })).toBe('炼气9层')
|
||
expect(describeRealm({ major: 'spirit', minor: 2 })).toBe('化神3层')
|
||
})
|
||
|
||
it('realmTier/compareRealm 正确排序', () => {
|
||
expect(realmTier({ major: 'mortal', minor: 0 })).toBe(0)
|
||
expect(realmTier({ major: 'qi', minor: 0 })).toBe(11)
|
||
expect(realmTier({ major: 'core', minor: 0 })).toBe(31)
|
||
expect(compareRealm({ major: 'qi', minor: 3 }, { major: 'foundation', minor: 0 })).toBeLessThan(0)
|
||
expect(compareRealm({ major: 'qi', minor: 9 }, { major: 'qi', minor: 5 })).toBeGreaterThan(0)
|
||
})
|
||
|
||
it('nextRealm 链完整走完直至化神壁', () => {
|
||
let r = { major: 'mortal', minor: 0 } as { major: (typeof MAJOR_ORDER)[number]; minor: number }
|
||
r = nextRealm(r)!
|
||
expect(r).toEqual({ major: 'qi', minor: 0 })
|
||
r = nextRealm(r)!
|
||
expect(r).toEqual({ major: 'qi', minor: 1 })
|
||
r = { major: 'qi', minor: 8 }
|
||
r = nextRealm(r)!
|
||
expect(r).toEqual({ major: 'foundation', minor: 0 })
|
||
r = { major: 'foundation', minor: 2 }
|
||
r = nextRealm(r)!
|
||
expect(r).toEqual({ major: 'core', minor: 0 })
|
||
r = { major: 'spirit', minor: 2 }
|
||
expect(nextRealm(r)).toBeNull()
|
||
})
|
||
|
||
it('maxRealmExp 随层数严格增长', () => {
|
||
for (const major of ['qi', 'foundation', 'core', 'nascent'] as const) {
|
||
for (let i = 0; i < MAJORS[major].minorLayers - 1; i++) {
|
||
expect(maxRealmExp(major, i + 1)).toBeGreaterThan(maxRealmExp(major, i))
|
||
}
|
||
}
|
||
})
|
||
|
||
it('basePower 战力随境界单调递增', () => {
|
||
const seq: { major: (typeof MAJOR_ORDER)[number]; minor: number }[] = [
|
||
{ major: 'mortal', minor: 0 },
|
||
{ major: 'qi', minor: 0 },
|
||
{ major: 'qi', minor: 8 },
|
||
{ major: 'foundation', minor: 0 },
|
||
{ major: 'core', minor: 0 },
|
||
{ major: 'nascent', minor: 0 },
|
||
{ major: 'spirit', minor: 0 }
|
||
]
|
||
for (let i = 1; i < seq.length; i++) {
|
||
expect(basePower(seq[i])).toBeGreaterThan(basePower(seq[i - 1]))
|
||
}
|
||
})
|
||
|
||
it('突破基础概率与死亡风险在合法区间且递减/递增', () => {
|
||
const majors = ['mortal', 'qi', 'foundation', 'core', 'nascent', 'spirit']
|
||
for (const major of majors) {
|
||
const b = breakthroughBaseChance({ major: major as never, minor: 0 })
|
||
expect(b).toBeGreaterThan(0)
|
||
expect(b).toBeLessThanOrEqual(0.9)
|
||
}
|
||
for (let i = 1; i < majors.length; i++) {
|
||
expect(breakthroughBaseChance({ major: majors[i] as never, minor: 0 })).toBeLessThan(
|
||
breakthroughBaseChance({ major: majors[i - 1] as never, minor: 0 })
|
||
)
|
||
}
|
||
for (const major of majors) {
|
||
expect(realmDeathChance({ major: major as never, minor: 0 }, 5)).toBeGreaterThanOrEqual(0)
|
||
expect(realmDeathChance({ major: major as never, minor: 0 }, 5)).toBeLessThan(0.06)
|
||
}
|
||
})
|
||
|
||
it('品阶名数量一致(含凡阶哨兵)', () => {
|
||
expect(TECHNIQUE_GRADE_NAMES.length).toBe(6)
|
||
})
|
||
})
|
||
|
||
describe('elements 灵根', () => {
|
||
it('五行与品阶定义齐全', () => {
|
||
expect(ELEMENT_LIST.length).toBe(5)
|
||
for (const g of [0, 1, 2, 3, 4, 5]) {
|
||
expect(ROOT_GRADES[g]).toBeTruthy()
|
||
expect(ROOT_GRADES[g].drawWeight).toBeGreaterThan(0)
|
||
expect(ROOT_GRADES[g].expBonus).toBeGreaterThan(0)
|
||
}
|
||
expect(ROOT_GRADE_NAMES.length).toBe(6)
|
||
})
|
||
|
||
it('品阶修为加成单调', () => {
|
||
for (let g = 1; g <= 5; g++) {
|
||
expect(ROOT_GRADES[g].expBonus).toBeGreaterThan(ROOT_GRADES[g - 1].expBonus)
|
||
}
|
||
})
|
||
|
||
it('describeRoots 包含主属性与品阶', () => {
|
||
const label = describeRoots({ grade: 3, primary: '火', secondary: ['金', '水'] })
|
||
expect(label).toContain('火')
|
||
expect(label).toContain('真品灵根')
|
||
})
|
||
})
|
||
|
||
describe('items 物品表', () => {
|
||
it('物品 id 唯一且价格为正', () => {
|
||
const ids = new Set<string>()
|
||
for (const [id, item] of Object.entries(ITEMS)) {
|
||
expect(ids.has(id)).toBe(false)
|
||
ids.add(id)
|
||
expect(item.basePrice).toBeGreaterThan(0)
|
||
expect(item.name).toBeTruthy()
|
||
}
|
||
})
|
||
|
||
it('法宝战力表与法器条目一一对应', () => {
|
||
const artifacts = Object.entries(ITEMS).filter(([, v]) => v.kind === 'artifact')
|
||
for (const [id] of artifacts) {
|
||
expect(ARTIFACT_POWER[id]).toBeGreaterThan(0)
|
||
}
|
||
expect(Object.keys(ARTIFACT_POWER).length).toBe(artifacts.length)
|
||
})
|
||
})
|
||
|
||
describe('techniques 功法表', () => {
|
||
it('字段在法律区间且元素合法', () => {
|
||
for (const t of TECHNIQUES) {
|
||
expect(t.grade).toBeGreaterThanOrEqual(0)
|
||
expect(t.grade).toBeLessThanOrEqual(4)
|
||
expect(t.expBonus).toBeGreaterThan(0)
|
||
expect(t.powerBonus).toBeGreaterThan(0)
|
||
expect(ELEMENT_LIST).toContain(t.element)
|
||
expect(['剑修', '体修', '丹修', '阵修', '符修', '御灵']).toContain(t.path)
|
||
}
|
||
})
|
||
|
||
it('品阶越高综合面板越强(对比均值)', () => {
|
||
const avg = (g: number) => {
|
||
const pool = TECHNIQUES.filter((t) => t.grade === g)
|
||
const exp = pool.reduce((a, t) => a + t.expBonus, 0) / pool.length
|
||
const pow = pool.reduce((a, t) => a + t.powerBonus, 0) / pool.length
|
||
return exp + pow
|
||
}
|
||
for (let g = 2; g <= 4; g++) {
|
||
expect(avg(g)).toBeGreaterThan(avg(g - 1))
|
||
}
|
||
})
|
||
})
|
||
|
||
describe('buildings 建筑表', () => {
|
||
it('所有建筑定义完整、升级成本递增', () => {
|
||
for (const [id, def] of Object.entries(BUILDINGS)) {
|
||
expect(def.maxLevel).toBeGreaterThan(0)
|
||
expect(def.name).toBeTruthy()
|
||
for (let l = 1; l < def.maxLevel; l++) {
|
||
const now = def.upgradeCost(l)
|
||
const next = def.upgradeCost(l + 1)
|
||
expect(next.stones).toBeGreaterThan(now.stones)
|
||
expect(next.lingkuang).toBeGreaterThan(now.lingkuang)
|
||
if (def.produceTable) {
|
||
const p0 = def.produceTable(l)
|
||
const p1 = def.produceTable(l + 1)
|
||
const key = Object.keys(p0)[0]
|
||
expect((p1[key] ?? 0)).toBeGreaterThanOrEqual((p0[key] ?? 0))
|
||
}
|
||
}
|
||
}
|
||
})
|
||
|
||
it('id 无重复', () => {
|
||
expect(new Set(Object.keys(BUILDINGS)).size).toBe(Object.keys(BUILDINGS).length)
|
||
})
|
||
})
|
||
|
||
describe('secrets 秘境', () => {
|
||
it('每个秘境阶段引用存在的敌人、奖励合法', () => {
|
||
const enemyIds = new Set(ENEMIES.map((e) => e.id))
|
||
for (const m of MISSIONS) {
|
||
expect(m.minMembers).toBeGreaterThan(0)
|
||
expect(m.minMembers).toBeLessThanOrEqual(m.maxMembers)
|
||
expect(m.risk).toBeGreaterThan(0)
|
||
expect(m.risk).toBeLessThanOrEqual(1)
|
||
expect(m.stages.length).toBeGreaterThan(0)
|
||
for (const st of m.stages) {
|
||
expect(['event', 'combat', 'resource', 'boss']).toContain(st.kind)
|
||
expect(st.months).toBeGreaterThan(0)
|
||
if (st.enemyId) expect(enemyIds.has(st.enemyId)).toBe(true)
|
||
if (st.loot) {
|
||
for (const r of Object.values(st.loot.resources)) {
|
||
expect(r[0]).toBeGreaterThanOrEqual(0)
|
||
expect(r[1]).toBeGreaterThanOrEqual(r[0])
|
||
}
|
||
}
|
||
}
|
||
for (const r of Object.values(m.completionLoot.resources)) {
|
||
expect(r[1]).toBeGreaterThan(r[0])
|
||
}
|
||
}
|
||
expect(missionById('m-anmoku').name).toBe('暗墨林')
|
||
expect(() => missionById('nope')).toThrow()
|
||
})
|
||
})
|
||
|
||
describe('npcs 势力表', () => {
|
||
it('四家齐全、growth 为正、leader 境界合法', () => {
|
||
expect(NPCS.length).toBe(4)
|
||
for (const n of NPCS) {
|
||
expect(n.powerGrowth[0]).toBeGreaterThan(0)
|
||
expect(n.powerGrowth[1]).toBeGreaterThanOrEqual(n.powerGrowth[0])
|
||
expect(MAJOR_ORDER).toContain(n.leaderRealm)
|
||
}
|
||
// 0.1.23:npcById 双源返回 undefined(不抛)——缺失防御化
|
||
expect(npcById('n-nulei')?.name).toBe('怒雷祝氏')
|
||
expect(npcById('nope')).toBeUndefined()
|
||
})
|
||
})
|
||
|
||
describe('posts 职事', () => {
|
||
it('职事完备且加成归一', () => {
|
||
expect(POST_ORDER.length).toBe(5)
|
||
expect(POSTS['head'].max).toBe(1)
|
||
for (const id of POST_ORDER) {
|
||
expect(POSTS[id].max).toBeGreaterThan(0)
|
||
expect(POSTS[id].effect.value).toBeGreaterThan(0)
|
||
}
|
||
})
|
||
})
|
||
|
||
describe('events 事件池', () => {
|
||
it('事件 id 唯一、结构合法、once 权重边界', () => {
|
||
const ids = new Set<string>()
|
||
for (const e of EVENTS) {
|
||
expect(ids.has(e.id)).toBe(false)
|
||
ids.add(e.id)
|
||
expect(e.weight).toBeGreaterThan(0)
|
||
expect(['daily', 'major', 'fate']).toContain(e.category)
|
||
expect(e.options.length).toBeGreaterThan(0)
|
||
expect(e.text.length).toBeGreaterThan(5)
|
||
for (const o of e.options) {
|
||
expect(o.label.length).toBeGreaterThan(0)
|
||
}
|
||
}
|
||
})
|
||
|
||
it('事件条件引用的建筑/资源存在', () => {
|
||
for (const e of EVENTS) {
|
||
const cond = e.cond
|
||
if (cond?.minBuilding) {
|
||
expect(BUILDINGS[cond.minBuilding.id]).toBeTruthy()
|
||
}
|
||
if (cond?.minResource) {
|
||
const id = cond.minResource.id
|
||
expect(id === 'stones' || ITEMS[id]).toBeTruthy()
|
||
}
|
||
if (cond?.relation) {
|
||
expect(NPCS.some((n) => n.id === cond.relation!.npcId)).toBe(true)
|
||
}
|
||
}
|
||
})
|
||
})
|
||
|
||
describe('pacing 节奏', () => {
|
||
it('全部大境界有修炼速率', () => {
|
||
for (const major of MAJOR_ORDER) {
|
||
const rate = masteryRateOfMajor(major)
|
||
expect(rate).toBeGreaterThan(0)
|
||
// 0.1.10 提速后 qi=2.2 属设计,仅验证仍低于高境界速率
|
||
}
|
||
// 高境界低于低境界
|
||
expect(MAJOR_RATE['spirit']).toBeLessThan(MAJOR_RATE['foundation'])
|
||
expect(MAJOR_RATE['nascent']).toBeLessThan(MAJOR_RATE['core'])
|
||
})
|
||
})
|
||
|
||
describe('names 人名池', () => {
|
||
it('姓氏/名池非空且无重复', () => {
|
||
expect(SURNAME_POOL.length).toBeGreaterThan(20)
|
||
expect(MALE_GIVEN.length).toBeGreaterThan(20)
|
||
expect(FEMALE_GIVEN.length).toBeGreaterThan(20)
|
||
expect(new Set(MALE_GIVEN).size).toBe(MALE_GIVEN.length)
|
||
expect(new Set(FEMALE_GIVEN).size).toBe(FEMALE_GIVEN.length)
|
||
})
|
||
})
|
||
|
||
describe('traits 秉性', () => {
|
||
it('全部特质定义完整、danger 在界', () => {
|
||
for (const t of Object.values(TRAITS)) {
|
||
expect(t.name).toBeTruthy()
|
||
expect(t.danger).toBeGreaterThanOrEqual(0)
|
||
expect(t.danger).toBeLessThanOrEqual(1)
|
||
}
|
||
})
|
||
})
|