vagabond-rpg-foundryvtt/module/tests/crit-threshold.test.mjs
Cal Corum 062189e315 Add crit threshold modifier tests and fix effect application
- Add comprehensive Quench tests for crit threshold system:
  - Skill crit thresholds modified by Active Effects
  - Attack crit thresholds (melee, ranged, brawl, finesse)
  - Effect stacking, disabled effects, minimum threshold enforcement
  - Helper function validation

- Fix crit threshold clamping in CharacterData.prepareDerivedData():
  - Skills: clamp in _calculateSkillDifficulties() after effects apply
  - Attacks: new _clampAttackCritThresholds() method
  - Ensures thresholds stay within 1-20 range after Active Effects

- Note: Foundry v13 auto-prepares data after createEmbeddedDocuments,
  manual prepareData() calls cause double effect application

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

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

183 lines
7.2 KiB
JavaScript

/**
* Crit Threshold Modifier System Tests
*
* Tests that Active Effects can modify crit thresholds for:
* - Individual skills (system.skills.<id>.critThreshold)
* - Attack types (system.attacks.<type>.critThreshold)
*
* Use cases:
* - Fighter's Valor: reduces melee crit threshold by 1/2/3
* - Gunslinger's Deadeye: reduces ranged crit threshold dynamically
*
* @module tests/crit-threshold
*/
import { createCritReductionEffect, EFFECT_MODES } from "../helpers/effects.mjs";
/**
* Register crit threshold tests with Quench
* @param {Quench} quenchRunner - The Quench test runner
*/
export function registerCritThresholdTests(quenchRunner) {
quenchRunner.registerBatch(
"vagabond.critThreshold.skills",
(context) => {
const { describe, it, expect, beforeEach, afterEach } = context;
describe("Skill Crit Threshold Modifiers", () => {
let actor;
beforeEach(async () => {
actor = await Actor.create({
name: "Test Fighter",
type: "character",
});
});
afterEach(async () => {
await actor?.delete();
});
it("should have default crit threshold of 20 for all skills", () => {
expect(actor.system.skills.arcana.critThreshold).to.equal(20);
expect(actor.system.skills.brawl.critThreshold).to.equal(20);
expect(actor.system.skills.finesse.critThreshold).to.equal(20);
});
it("should reduce skill crit threshold with Active Effect", async () => {
// Create an effect that reduces brawl crit by 2 (like Fighter's Valor at level 4)
const effectData = createCritReductionEffect("brawl", 2, "Valor (Brawl)");
await actor.createEmbeddedDocuments("ActiveEffect", [effectData]);
// Foundry automatically prepares data after embedded document creation
// Crit threshold should now be 18
expect(actor.system.skills.brawl.critThreshold).to.equal(18);
// Other skills should be unaffected
expect(actor.system.skills.arcana.critThreshold).to.equal(20);
});
it("should stack multiple crit reductions on same skill", async () => {
// Create two effects reducing brawl crit
const effect1 = createCritReductionEffect("brawl", 1, "Effect 1");
const effect2 = createCritReductionEffect("brawl", 2, "Effect 2");
await actor.createEmbeddedDocuments("ActiveEffect", [effect1, effect2]);
// Total reduction: 1 + 2 = 3, so threshold is 17
expect(actor.system.skills.brawl.critThreshold).to.equal(17);
});
it("should allow different skills to have different thresholds", async () => {
// Reduce brawl by 2, finesse by 1
const brawlEffect = createCritReductionEffect("brawl", 2, "Brawl Crit");
const finesseEffect = createCritReductionEffect("finesse", 1, "Finesse Crit");
await actor.createEmbeddedDocuments("ActiveEffect", [brawlEffect, finesseEffect]);
expect(actor.system.skills.brawl.critThreshold).to.equal(18);
expect(actor.system.skills.finesse.critThreshold).to.equal(19);
expect(actor.system.skills.arcana.critThreshold).to.equal(20);
});
it("should not reduce crit threshold below 1", async () => {
// Create an absurd reduction
const effectData = createCritReductionEffect("brawl", 25, "Overpowered");
await actor.createEmbeddedDocuments("ActiveEffect", [effectData]);
// Crit threshold should be clamped to minimum of 1
expect(actor.system.skills.brawl.critThreshold).to.be.at.least(1);
});
it("should not affect threshold when effect is disabled", async () => {
const effectData = createCritReductionEffect("brawl", 2, "Disabled Effect");
effectData.disabled = true;
await actor.createEmbeddedDocuments("ActiveEffect", [effectData]);
// Should remain at default since effect is disabled
expect(actor.system.skills.brawl.critThreshold).to.equal(20);
});
});
},
{ displayName: "Vagabond: Skill Crit Thresholds" }
);
quenchRunner.registerBatch(
"vagabond.critThreshold.attacks",
(context) => {
const { describe, it, expect, beforeEach, afterEach } = context;
describe("Attack Crit Threshold Modifiers", () => {
let actor;
beforeEach(async () => {
actor = await Actor.create({
name: "Test Fighter",
type: "character",
});
});
afterEach(async () => {
await actor?.delete();
});
it("should have default crit threshold of 20 for all attack types", () => {
expect(actor.system.attacks.melee.critThreshold).to.equal(20);
expect(actor.system.attacks.brawl.critThreshold).to.equal(20);
expect(actor.system.attacks.ranged.critThreshold).to.equal(20);
expect(actor.system.attacks.finesse.critThreshold).to.equal(20);
});
it("should reduce attack crit threshold with Active Effect", async () => {
// Create effect reducing melee crit by 1 (Fighter's Valor level 1)
const effectData = createCritReductionEffect("attack.melee", 1, "Valor (Melee)");
await actor.createEmbeddedDocuments("ActiveEffect", [effectData]);
expect(actor.system.attacks.melee.critThreshold).to.equal(19);
expect(actor.system.attacks.ranged.critThreshold).to.equal(20);
});
it("should reduce ranged crit threshold (Gunslinger style)", async () => {
// Simulate Gunslinger's Deadeye reducing ranged crit
const effectData = createCritReductionEffect("attack.ranged", 3, "Deadeye");
await actor.createEmbeddedDocuments("ActiveEffect", [effectData]);
expect(actor.system.attacks.ranged.critThreshold).to.equal(17);
});
});
},
{ displayName: "Vagabond: Attack Crit Thresholds" }
);
quenchRunner.registerBatch(
"vagabond.critThreshold.effectHelpers",
(context) => {
const { describe, it, expect } = context;
describe("Crit Effect Helper Functions", () => {
it("should create valid crit reduction effect data", () => {
const effectData = createCritReductionEffect("brawl", 2, "Test Effect");
expect(effectData.name).to.equal("Test Effect");
expect(effectData.changes).to.have.length(1);
expect(effectData.changes[0].key).to.equal("system.skills.brawl.critThreshold");
expect(effectData.changes[0].value).to.equal("-2");
expect(effectData.changes[0].mode).to.equal(EFFECT_MODES.ADD);
});
it("should create attack crit reduction with correct key", () => {
const effectData = createCritReductionEffect("attack.melee", 1, "Valor");
expect(effectData.changes[0].key).to.equal("system.attacks.melee.critThreshold");
expect(effectData.changes[0].value).to.equal("-1");
});
it("should always use negative value for reduction", () => {
// Even if positive is passed, should be negative
const effectData = createCritReductionEffect("brawl", 3, "Test");
expect(effectData.changes[0].value).to.equal("-3");
});
});
},
{ displayName: "Vagabond: Crit Effect Helpers" }
);
}