v0.1.0: 仙途家族志 · Chronicle of the Immortal Clan

家族模拟器首版:修仙/经营/战斗/外交/叙事全套系统
- Electron + React + TS + MetonaSqlark(aria+OPFS)
- 水墨中国风 UI
- 引擎种子随机、确定性可回放
- 19 项单元测试、端到端冒烟验证
This commit is contained in:
2026-08-23 00:04:16 +08:00
commit adb22f0558
69 changed files with 14305 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
import { describe, expect, it } from 'vitest'
import { World } from '../src/renderer/game/engine/world'
import { combatPowerOf } from '../src/renderer/game/engine/systems/combat'
import { marketPrice, buyItem, sellItem, buyTechnique } from '../src/renderer/game/engine/market'
import { resolveBreakthrough } from '../src/renderer/game/engine/systems/cultivation'
import { matchesCondPub } from './events.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)
})
})