feat(gameplay): add versioned SRD rules migration
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { abilityModifier, applyMechanicalEvents, bindMechanicalEventsToRolls, resolveCheck, resolveChecks, shouldCloseRound, type RandomSource } from './index'
|
||||
import { abilityModifier, applyMechanicalEvents, bindMechanicalEventsToRolls, proficiencyBonus, resolveCheck, resolveChecks, shouldCloseRound, type RandomSource } from './index'
|
||||
import type { Character } from '@dng/shared'
|
||||
|
||||
const fixed: RandomSource = { integer: () => 12 }
|
||||
@@ -91,13 +91,13 @@ describe('game engine', () => {
|
||||
{ integer: () => 4 },
|
||||
)
|
||||
const events = bindMechanicalEventsToRolls([
|
||||
{ type: 'damage', actorId: 'hero', targetId: 'target', value: 999, item: null, description: 'Strike' },
|
||||
{ type: 'damage', actorId: 'hero', targetId: 'target', value: 999, item: null, damageType: 'slashing', description: 'Strike' },
|
||||
], [damage])
|
||||
expect(events[0]?.value).toBe(6)
|
||||
})
|
||||
|
||||
it('clamps mechanical state', () => {
|
||||
const result = applyMechanicalEvents([hero], [{ type: 'damage', actorId: null, targetId: 'hero', value: 99, item: null, description: 'Catastrophic hit' }])
|
||||
const result = applyMechanicalEvents([hero], [{ type: 'damage', actorId: null, targetId: 'hero', value: 99, item: null, damageType: 'force', description: 'Catastrophic hit' }])
|
||||
expect(result.characters[0]?.hp).toBe(0)
|
||||
expect(result.audit).toHaveLength(1)
|
||||
})
|
||||
@@ -107,4 +107,73 @@ describe('game engine', () => {
|
||||
expect(shouldCloseRound(['a', 'b'], ['a', 'b'])).toBe(true)
|
||||
expect(shouldCloseRound(['a', 'b'], [], true)).toBe(true)
|
||||
})
|
||||
|
||||
it('derives proficiency and expertise from the server-owned rules profile', () => {
|
||||
const trained: Character = {
|
||||
...hero,
|
||||
rules: {
|
||||
version: 2, level: 9, skillProficiencies: ['perception'], skillExpertise: ['perception'],
|
||||
savingThrowProficiencies: ['con'], temporaryHp: 0, deathSaves: { successes: 0, failures: 0 },
|
||||
exhaustion: 1, damageResistances: [], damageVulnerabilities: [], damageImmunities: [],
|
||||
resources: {}, legacyProficiencyFallback: false,
|
||||
},
|
||||
}
|
||||
const roll = resolveCheck({
|
||||
actorId: 'hero', kind: 'skill', skill: 'perception', proficient: false,
|
||||
difficulty: 10, mode: 'normal', reason: 'Notice the rune',
|
||||
}, trained, fixed)
|
||||
|
||||
expect(proficiencyBonus(9)).toBe(4)
|
||||
// WIS -1 + expertise 8 - exhaustion 2.
|
||||
expect(roll.modifier).toBe(5)
|
||||
})
|
||||
|
||||
it('applies resistance, temporary HP, zero-HP state and healing recovery', () => {
|
||||
const guarded: Character = {
|
||||
...hero,
|
||||
hp: 4,
|
||||
rules: {
|
||||
version: 2, level: 1, skillProficiencies: [], skillExpertise: [], savingThrowProficiencies: [],
|
||||
temporaryHp: 2, deathSaves: { successes: 0, failures: 0 }, exhaustion: 0,
|
||||
damageResistances: ['fire'], damageVulnerabilities: [], damageImmunities: [],
|
||||
resources: {}, legacyProficiencyFallback: false,
|
||||
},
|
||||
}
|
||||
const damaged = applyMechanicalEvents([guarded], [{
|
||||
type: 'damage', actorId: null, targetId: 'hero', value: 12, item: null,
|
||||
damageType: 'fire', description: 'Flames engulf the hero',
|
||||
}]).characters[0]!
|
||||
expect(damaged.rules?.temporaryHp).toBe(0)
|
||||
expect(damaged.hp).toBe(0)
|
||||
expect(damaged.statuses).toContain('unconscious')
|
||||
|
||||
const healed = applyMechanicalEvents([damaged], [{
|
||||
type: 'healing', actorId: null, targetId: 'hero', value: 4, item: null,
|
||||
damageType: null, description: 'An ally restores the hero',
|
||||
}]).characters[0]!
|
||||
expect(healed.hp).toBe(4)
|
||||
expect(healed.statuses).not.toContain('unconscious')
|
||||
})
|
||||
|
||||
it('tracks natural death-save outcomes without model-authored state changes', () => {
|
||||
const dying: Character = {
|
||||
...hero,
|
||||
hp: 0,
|
||||
statuses: ['unconscious'],
|
||||
rules: {
|
||||
version: 2, level: 1, skillProficiencies: [], skillExpertise: [], savingThrowProficiencies: [],
|
||||
temporaryHp: 0, deathSaves: { successes: 0, failures: 0 }, exhaustion: 0,
|
||||
damageResistances: [], damageVulnerabilities: [], damageImmunities: [],
|
||||
resources: {}, legacyProficiencyFallback: false,
|
||||
},
|
||||
}
|
||||
const deathRoll = resolveCheck({
|
||||
actorId: 'hero', kind: 'deathSavingThrow', mode: 'normal', reason: 'Cling to life',
|
||||
}, dying, { integer: () => 20 })
|
||||
const recovered = applyMechanicalEvents([dying], [], [deathRoll]).characters[0]!
|
||||
|
||||
expect(recovered.hp).toBe(1)
|
||||
expect(recovered.statuses).not.toContain('unconscious')
|
||||
expect(recovered.rules?.deathSaves).toEqual({ successes: 0, failures: 0 })
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user