Add spell slot-level picker: warlocks can cast, any caster can upcast
Both cast UIs (SpellcastingSection + MyCharacterPanel) called castSpell without a level, so it always tried the spell's base level — a warlock (only a pact slot at the pact level) could never cast, and upcasting was impossible. New availableSlotLevels() lists the castable levels (regular slots ≥ spell level with a charge, plus the pact level); the UI shows a level selector and passes the chosen level. Adds a regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,20 @@ function consumeSlot(sc: Spellcasting, level: number): { spellcasting: Spellcast
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The slot levels at which a leveled spell can currently be cast: any regular slot
|
||||
* level ≥ the spell's level that still has a charge, plus the pact-magic level when
|
||||
* it qualifies. Returns [] for cantrips (no slot needed). Used by the cast UI to
|
||||
* offer a slot-level picker so warlocks can spend pact slots and any caster can upcast.
|
||||
*/
|
||||
export function availableSlotLevels(sc: Spellcasting, spellLevel: number): number[] {
|
||||
if (spellLevel <= 0) return [];
|
||||
const levels = new Set<number>();
|
||||
for (const s of sc.slots) if (s.level >= spellLevel && s.current > 0) levels.add(s.level);
|
||||
if (sc.pact && sc.pact.level >= spellLevel && sc.pact.current > 0) levels.add(sc.pact.level);
|
||||
return [...levels].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast a spell the character knows, at a chosen slot level (≥ the spell's own
|
||||
* level, to allow upcasting). Pure: decrements a slot, sets/replaces
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { characterDefaults, newSpellEntry, type Character } from '@/lib/schemas';
|
||||
import { castSpell, dropConcentration } from './cast';
|
||||
import { castSpell, availableSlotLevels, dropConcentration } from './cast';
|
||||
import { spendResource, regainResource } from './resources';
|
||||
import { rollDeathSave } from './deathSaves';
|
||||
|
||||
@@ -77,6 +77,15 @@ describe('castSpell', () => {
|
||||
expect(r.patch.spellcasting!.pact!.current).toBe(1);
|
||||
});
|
||||
|
||||
it('availableSlotLevels offers the pact level for a warlock and upcast levels for a caster', () => {
|
||||
// Warlock: a level-1 spell can be cast with the level-3 pact slot (the only option).
|
||||
expect(availableSlotLevels({ slots: [], pact: { level: 3, max: 2, current: 2 }, spells: [] }, 1)).toEqual([3]);
|
||||
// Wizard: a level-1 spell can go in the level-1 or level-3 slot (upcast), but not empties.
|
||||
expect(availableSlotLevels({ slots: [{ level: 1, max: 4, current: 4 }, { level: 2, max: 2, current: 0 }, { level: 3, max: 3, current: 2 }], spells: [] }, 1)).toEqual([1, 3]);
|
||||
// Cantrips need no slot.
|
||||
expect(availableSlotLevels({ slots: [{ level: 1, max: 4, current: 4 }], spells: [] }, 0)).toEqual([]);
|
||||
});
|
||||
|
||||
it('dropConcentration clears the slot', () => {
|
||||
const concentrating = mk({ concentration: { spellId: 's2', spellName: 'Haste' } });
|
||||
expect(dropConcentration(concentrating)).toEqual({ concentration: null });
|
||||
|
||||
@@ -15,7 +15,7 @@ export type { EnforceResult } from './result';
|
||||
export { normalizeSpell5e, normalizeSpellPf2e } from './normalize/spell';
|
||||
export { normalizeArmor5e } from './normalize/armor';
|
||||
export { normalizeMonsterDefenses, normalizeMonsterDefensesPf2e, type Pf2eDefenses } from './normalize/monsterDefenses';
|
||||
export { castSpell, dropConcentration, concentrationDC } from './cast';
|
||||
export { castSpell, availableSlotLevels, dropConcentration, concentrationDC } from './cast';
|
||||
export { spendResource, regainResource } from './resources';
|
||||
export { rollDeathSave, type DeathSaveOutcome } from './deathSaves';
|
||||
export { conditionEffects, CONDITION_EFFECTS_5E, CONDITION_EFFECTS_PF2E, type ConditionEffect, type PenaltyTarget } from './conditionEffects';
|
||||
|
||||
Reference in New Issue
Block a user