- 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>
256 lines
7.3 KiB
JavaScript
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" }
|
|
);
|
|
}
|