vagabond-rpg-foundryvtt/module/tests/spell.test.mjs
Cal Corum 6247004b58 Implement spell casting system with rulebook-accurate mana costs
- Add SpellCastDialog with delivery/duration/damage configuration
- Fix mana cost calculation to match rulebook formula:
  - Effect-only or 1d6 damage-only = 0 mana
  - Both damage AND effect = 1 mana base
  - +1 per extra damage die beyond first
  - +delivery cost (Touch/Remote/Imbue=0, Cube=1, Area=2)
  - Duration has no initial cost (Focus requires maintenance)
- Add "Include Effect" toggle for damage vs effect choice
- Create spell cast chat card template
- Add 20+ i18n strings for spell casting UI
- Create comprehensive Quench tests for mana calculation
- Add Cast Spell macro for testing
- Update CLAUDE.md with NoteDiscovery access instructions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 20:22:20 -06:00

256 lines
7.3 KiB
JavaScript

/**
* Spell Casting System Tests
*
* Tests the spell casting mechanics including:
* - Mana cost calculation (rulebook formula)
* - Delivery type filtering
* - Duration type filtering
* - Damage detection
*
* @module tests/spell
*/
/**
* Register spell casting tests with Quench
* @param {Quench} quenchRunner - The Quench test runner
*/
export function registerSpellTests(quenchRunner) {
quenchRunner.registerBatch(
"vagabond.spells.manaCost",
(context) => {
const { describe, it, expect, beforeEach, afterEach } = context;
describe("Spell Mana Cost Calculation", () => {
let actor;
let spell;
beforeEach(async () => {
// Create a test actor
actor = await Actor.create({
name: "Test Caster",
type: "character",
});
// Create a test spell with effect
spell = await Item.create({
name: "Test Fireball",
type: "spell",
system: {
effect: "Target takes fire damage and catches fire",
damageType: "fire",
damageBase: "d6",
maxDice: 5,
deliveryTypes: {
touch: true,
remote: true,
sphere: true,
},
durationTypes: {
instant: true,
focus: true,
},
},
});
});
afterEach(async () => {
await actor?.delete();
await spell?.delete();
});
it("should cost 0 mana for effect-only cast (no damage)", () => {
const cost = spell.system.calculateManaCost({
damageDice: 0,
delivery: "touch",
duration: "instant",
includeEffect: true,
});
expect(cost).to.equal(0);
});
it("should cost 0 mana for 1d6 damage-only cast (no effect)", () => {
const cost = spell.system.calculateManaCost({
damageDice: 1,
delivery: "touch",
duration: "instant",
includeEffect: false,
});
expect(cost).to.equal(0);
});
it("should cost 1 mana for 1d6 damage WITH effect", () => {
const cost = spell.system.calculateManaCost({
damageDice: 1,
delivery: "touch",
duration: "instant",
includeEffect: true,
});
expect(cost).to.equal(1);
});
it("should add +1 mana per extra damage die beyond first", () => {
// 3d6 damage with effect: 1 base + 2 extra dice = 3
const cost = spell.system.calculateManaCost({
damageDice: 3,
delivery: "touch",
duration: "instant",
includeEffect: true,
});
expect(cost).to.equal(3);
});
it("should add delivery cost for area spells", () => {
// Sphere costs 2
const cost = spell.system.calculateManaCost({
damageDice: 1,
delivery: "sphere",
duration: "instant",
includeEffect: true,
});
// 1 base (damage+effect) + 2 sphere = 3
expect(cost).to.equal(3);
});
it("should not add duration cost (rulebook: no initial cost)", () => {
const instantCost = spell.system.calculateManaCost({
damageDice: 1,
delivery: "touch",
duration: "instant",
includeEffect: true,
});
const focusCost = spell.system.calculateManaCost({
damageDice: 1,
delivery: "touch",
duration: "focus",
includeEffect: true,
});
expect(focusCost).to.equal(instantCost);
});
it("should calculate complex spell cost correctly", () => {
// Example from rulebook: 3d6 sphere = 1 base + 2 extra dice + 2 sphere = 5
const cost = spell.system.calculateManaCost({
damageDice: 3,
delivery: "sphere",
duration: "instant",
includeEffect: true,
});
expect(cost).to.equal(5);
});
it("should handle damage-only with area delivery", () => {
// 2d6 damage only with cone: 0 base + 1 extra die + 2 cone = 3
const cost = spell.system.calculateManaCost({
damageDice: 2,
delivery: "cone",
duration: "instant",
includeEffect: false,
});
expect(cost).to.equal(3);
});
});
},
{ displayName: "Vagabond: Spell Mana Cost" }
);
quenchRunner.registerBatch(
"vagabond.spells.deliveryDuration",
(context) => {
const { describe, it, expect, beforeEach, afterEach } = context;
describe("Spell Delivery and Duration Types", () => {
let spell;
beforeEach(async () => {
spell = await Item.create({
name: "Test Spell",
type: "spell",
system: {
effect: "Test effect",
deliveryTypes: {
touch: true,
remote: true,
sphere: false,
cone: true,
},
durationTypes: {
instant: true,
focus: true,
continual: false,
},
},
});
});
afterEach(async () => {
await spell?.delete();
});
it("should return only valid delivery types", () => {
const valid = spell.system.getValidDeliveryTypes();
expect(valid).to.include("touch");
expect(valid).to.include("remote");
expect(valid).to.include("cone");
expect(valid).to.not.include("sphere");
});
it("should return only valid duration types", () => {
const valid = spell.system.getValidDurationTypes();
expect(valid).to.include("instant");
expect(valid).to.include("focus");
expect(valid).to.not.include("continual");
});
});
},
{ displayName: "Vagabond: Spell Delivery/Duration" }
);
quenchRunner.registerBatch(
"vagabond.spells.damage",
(context) => {
const { describe, it, expect, beforeEach, afterEach } = context;
describe("Spell Damage Detection", () => {
let damagingSpell;
let utilitySpell;
beforeEach(async () => {
damagingSpell = await Item.create({
name: "Fireball",
type: "spell",
system: {
damageType: "fire",
damageBase: "d6",
maxDice: 5,
},
});
utilitySpell = await Item.create({
name: "Light",
type: "spell",
system: {
damageType: "",
damageBase: "",
maxDice: 0,
},
});
});
afterEach(async () => {
await damagingSpell?.delete();
await utilitySpell?.delete();
});
it("should detect damaging spells", () => {
expect(damagingSpell.system.isDamaging()).to.be.true;
});
it("should detect utility (non-damaging) spells", () => {
expect(utilitySpell.system.isDamaging()).to.be.false;
});
});
},
{ displayName: "Vagabond: Spell Damage Detection" }
);
}