Files
thzxx 04ef80faa5 v0.1.23: 生灭千秋 + 插件生态化第一课(1027 全绿)
【世界模型:世界会死人也会新生】
- 家族生灭: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 通过
2026-08-23 16:03:01 +08:00

338 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.23npcById 双源返回 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)
}
})
})