vagabond-rpg-foundryvtt/module/data/item/weapon.mjs
Cal Corum 517b7045c7 Add Phase 2 core system logic: document classes, dice rolling, and fixes
Implements Phase 2 foundational components:
- VagabondActor document class with item management, resource tracking,
  damage/healing, rest mechanics, and combat helpers
- VagabondItem document class with chat card generation and item usage
- Comprehensive dice rolling module (d20 checks, skill/attack/save rolls,
  damage with crit doubling, countdown dice, morale checks)
- Quench tests for all dice rolling functions

Fixes Foundry VTT v13 compatibility issues:
- Add documentTypes to system.json declaring valid Actor/Item types
- Fix StringField validation errors by using nullable/null pattern
  instead of blank string choices for optional fields
- Update actor tests to use embedded documents for slot calculations

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

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

258 lines
7.2 KiB
JavaScript

/**
* Weapon Item Data Model
*
* Defines the data schema for weapons in Vagabond RPG.
*
* Key properties:
* - Damage dice (e.g., "1d6", "2d6")
* - Grip type (1H, 2H, Versatile)
* - Attack skill (Melee, Brawl, Ranged, Finesse)
* - Weapon properties (Finesse, Thrown, Cleave, Reach, etc.)
* - Slot cost for inventory management
*
* @extends VagabondItemBase
*/
import VagabondItemBase from "./base-item.mjs";
export default class WeaponData extends VagabondItemBase {
/**
* Define the schema for weapon items.
*
* @returns {Object} The schema definition
*/
static defineSchema() {
const fields = foundry.data.fields;
const baseSchema = super.defineSchema();
return {
...baseSchema,
// Damage dice formula
damage: new fields.StringField({
required: true,
initial: "1d6",
}),
// Damage type (physical: blunt/piercing/slashing, elemental: fire/cold/shock/acid/poison)
damageType: new fields.StringField({
required: true,
initial: "blunt",
choices: ["blunt", "piercing", "slashing", "fire", "cold", "shock", "acid", "poison"],
}),
// Bonus damage (from magic, etc.)
bonusDamage: new fields.NumberField({
integer: true,
initial: 0,
}),
// Grip type (1h, 2h, versatile, fist)
grip: new fields.StringField({
required: true,
initial: "1h",
choices: ["1h", "2h", "versatile", "fist"],
}),
// Versatile damage (when wielded 2H)
versatileDamage: new fields.StringField({
required: false,
blank: true,
}),
// Attack skill used
attackType: new fields.StringField({
required: true,
initial: "melee",
choices: ["melee", "brawl", "ranged", "finesse"],
}),
// Range (for ranged/thrown weapons)
range: new fields.SchemaField({
value: new fields.NumberField({ integer: true, initial: 0 }),
units: new fields.StringField({ initial: "ft" }),
}),
// Weapon properties
properties: new fields.SchemaField({
finesse: new fields.BooleanField({ initial: false }),
thrown: new fields.BooleanField({ initial: false }),
cleave: new fields.BooleanField({ initial: false }),
reach: new fields.BooleanField({ initial: false }),
loading: new fields.BooleanField({ initial: false }),
brawl: new fields.BooleanField({ initial: false }),
crude: new fields.BooleanField({ initial: false }),
versatile: new fields.BooleanField({ initial: false }),
}),
// Weapon material (affects damage vs certain creatures)
material: new fields.StringField({
initial: "mundane",
choices: ["mundane", "silvered", "adamantine", "magical"],
}),
// Inventory slot cost
slots: new fields.NumberField({
integer: true,
initial: 1,
min: 0,
}),
// Monetary value (in copper)
value: new fields.NumberField({
integer: true,
initial: 0,
min: 0,
}),
// Is this weapon equipped?
equipped: new fields.BooleanField({ initial: false }),
// Which hand is this weapon equipped in? (for dual-wielding, null = not equipped)
equippedHand: new fields.StringField({
required: false,
nullable: true,
blank: false,
initial: null,
choices: ["main", "off", "both"],
}),
// Quantity (for ammunition, thrown weapons)
quantity: new fields.NumberField({
integer: true,
initial: 1,
min: 0,
}),
// Attack bonus (from magic, etc.)
attackBonus: new fields.NumberField({
integer: true,
initial: 0,
}),
// Critical threshold override (if different from skill default)
critThreshold: new fields.NumberField({
integer: true,
nullable: true,
initial: null,
min: 1,
max: 20,
}),
// Relic System - Powerful magic items with unique abilities
relic: new fields.SchemaField({
// Is this item a relic?
isRelic: new fields.BooleanField({ initial: false }),
// Relic tier (determines power level)
tier: new fields.NumberField({
integer: true,
initial: 1,
min: 1,
max: 5,
}),
// Unique ability name
abilityName: new fields.StringField({ required: false, blank: true }),
// Unique ability description
abilityDescription: new fields.HTMLField({ required: false, blank: true }),
// Activation cost (mana, luck, etc.)
activationCost: new fields.StringField({ required: false, blank: true }),
// Uses per day (0 = unlimited or passive)
usesPerDay: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
// Current uses remaining
usesRemaining: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
// Attunement required?
requiresAttunement: new fields.BooleanField({ initial: false }),
// Currently attuned?
attuned: new fields.BooleanField({ initial: false }),
// Lore/history of the relic
lore: new fields.HTMLField({ required: false, blank: true }),
}),
};
}
/**
* Get the effective attack stat for this weapon.
* Accounts for Finesse property allowing DEX for melee.
*
* @returns {string} The stat key to use for attack rolls
*/
getAttackStat() {
const attackStats = {
melee: "might",
brawl: "might",
ranged: "awareness",
finesse: "dexterity",
};
// Finesse weapons can use DEX for melee
if (this.properties.finesse && this.attackType === "melee") {
return "dexterity";
}
return attackStats[this.attackType] || "might";
}
/**
* Get the active weapon properties as an array.
*
* @returns {Array<string>} Array of active property keys
*/
getActiveProperties() {
return Object.entries(this.properties)
.filter(([, active]) => active)
.map(([prop]) => prop);
}
/**
* Get the effective damage formula.
*
* @param {boolean} twoHanded - Whether using two-handed grip for versatile
* @returns {string} Damage formula
*/
getDamageFormula(twoHanded = false) {
// Use versatile damage if 2H and available
if (twoHanded && this.properties.versatile && this.versatileDamage) {
return this.bonusDamage > 0
? `${this.versatileDamage}+${this.bonusDamage}`
: this.versatileDamage;
}
return this.bonusDamage > 0 ? `${this.damage}+${this.bonusDamage}` : this.damage;
}
/**
* Calculate the slot cost when equipped.
*
* @returns {number} Slot cost
*/
getEquippedSlots() {
return this.equipped ? this.slots : 0;
}
/**
* Get chat card data for displaying weapon information.
*
* @returns {Object} Chat card data
*/
getChatData() {
const data = super.getChatData();
data.damage = this.damage;
data.damageType = this.damageType;
data.grip = this.grip;
data.attackType = this.attackType;
data.properties = this.getActiveProperties();
data.range = this.range.value > 0 ? `${this.range.value} ${this.range.units}` : null;
return data;
}
}