feat(gameplay): enforce core SRD d20 rules
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, shouldCloseRound, type RandomSource } from './index'
|
||||
import { abilityModifier, applyMechanicalEvents, bindMechanicalEventsToRolls, resolveCheck, resolveChecks, shouldCloseRound, type RandomSource } from './index'
|
||||
import type { Character } from '@dng/shared'
|
||||
|
||||
const fixed: RandomSource = { integer: () => 12 }
|
||||
@@ -18,7 +18,7 @@ describe('game engine', () => {
|
||||
it('keeps dice server-owned and auditable', () => {
|
||||
const roll = resolveCheck({ actorId: 'hero', kind: 'ability', ability: 'dex', difficulty: 15, mode: 'normal', reason: 'Leap' }, hero, fixed)
|
||||
expect(roll.rolls).toEqual([12])
|
||||
expect(roll.total).toBe(17)
|
||||
expect(roll.total).toBe(15)
|
||||
expect(roll.success).toBe(true)
|
||||
expect(roll.id).toMatch(/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i)
|
||||
})
|
||||
@@ -30,10 +30,60 @@ describe('game engine', () => {
|
||||
hero,
|
||||
{ integer: () => values.shift()! },
|
||||
)
|
||||
expect(roll.formula).toBe('2d20kl1+5')
|
||||
expect(roll.formula).toBe('2d20kl1+3')
|
||||
expect(roll.kept).toEqual([4])
|
||||
})
|
||||
|
||||
it('applies proficiency only to proficient skills and saving throws', () => {
|
||||
const perception = resolveCheck({
|
||||
actorId: 'hero', kind: 'skill', skill: 'perception', proficient: true,
|
||||
difficulty: 12, mode: 'normal', reason: 'Spot an ambush',
|
||||
}, hero, fixed)
|
||||
const constitutionSave = resolveCheck({
|
||||
actorId: 'hero', kind: 'savingThrow', ability: 'con', proficient: false,
|
||||
difficulty: 12, mode: 'normal', reason: 'Endure poison',
|
||||
}, hero, fixed)
|
||||
|
||||
expect(perception.modifier).toBe(1)
|
||||
expect(constitutionSave.modifier).toBe(1)
|
||||
})
|
||||
|
||||
it('uses server-owned Armor Class and natural attack outcomes', () => {
|
||||
const target: Character = { ...hero, id: 'target', defense: 30 }
|
||||
const naturalTwenty = resolveCheck({
|
||||
actorId: 'hero', targetId: 'target', kind: 'attack', ability: 'str',
|
||||
difficulty: 1, mode: 'normal', reason: 'Sword strike',
|
||||
}, hero, { integer: () => 20 }, target)
|
||||
const naturalOne = resolveCheck({
|
||||
actorId: 'hero', targetId: 'target', kind: 'attack', ability: 'str',
|
||||
difficulty: 1, mode: 'normal', reason: 'Sword strike',
|
||||
}, hero, { integer: () => 1 }, { ...target, defense: 1 })
|
||||
|
||||
expect(naturalTwenty.difficulty).toBe(30)
|
||||
expect(naturalTwenty.success).toBe(true)
|
||||
expect(naturalOne.difficulty).toBe(1)
|
||||
expect(naturalOne.success).toBe(false)
|
||||
})
|
||||
|
||||
it('skips damage on a miss and doubles only critical damage dice', () => {
|
||||
const target: Character = { ...hero, id: 'target', defense: 14 }
|
||||
const checks = [
|
||||
{ actorId: 'hero', targetId: 'target', kind: 'attack' as const, ability: 'str' as const, mode: 'normal' as const, reason: 'Sword strike' },
|
||||
{ actorId: 'hero', targetId: 'target', kind: 'damage' as const, dice: '1d6+2', mode: 'normal' as const, reason: 'Sword damage' },
|
||||
]
|
||||
|
||||
const missed = resolveChecks(checks, [hero, target], { integer: () => 1 })
|
||||
expect(missed).toHaveLength(1)
|
||||
expect(missed[0]?.checkKind).toBe('attack')
|
||||
|
||||
const values = [20, 4, 5]
|
||||
const critical = resolveChecks(checks, [hero, target], { integer: () => values.shift()! })
|
||||
expect(critical).toHaveLength(2)
|
||||
expect(critical[1]?.formula).toBe('2d6+2')
|
||||
expect(critical[1]?.rolls).toEqual([4, 5])
|
||||
expect(critical[1]?.total).toBe(11)
|
||||
})
|
||||
|
||||
it('replaces model-proposed damage with the authoritative roll total', () => {
|
||||
const damage = resolveCheck(
|
||||
{ actorId: 'hero', targetId: 'target', kind: 'damage', dice: '1d6+2', mode: 'normal', reason: 'Strike' },
|
||||
|
||||
Reference in New Issue
Block a user