CharacterData additions: - Senses (darksight, blindsight, tremorsense, elvenEyes, witchsight, sixthSense) - Languages array (default: ["Common"]) - Rest tracking (lastRest, breathersTaken, restBonuses) - Travel tracking (milesThisDay, pace, canForage, shiftsElapsed) - Crafting projects (activeProjects with materials, shifts, bonuses) - Combat tracking (isFlanked, flankingAllies, ignoresFlankingPenalty, currentZone, isDualWielding, mainHandWeapon, offHandWeapon) - Casting tracking (equippedTrinket, canCastThroughWeapon/Instrument) - Downtime activities with type and shifts - Quest tracking (active, completed, lastQuestCompleted) - Death state (isDead, deathCause, canBeRevived, luminaryRevivifyUsed) - Summoned creatures (active array with type, HD, HP, command method) - Size mechanical effects (unitsOccupied, allowsMovementThrough) - Being type choices (humanlike, fae, cryptid, etc.) - preferredZone for class-based positioning NPCData additions: - Morale status tracking (checkedThisCombat, lastTrigger, lastResult, broken) - Senses (darksight, blindsight, tremorsense) ArmorData additions: - Medium armor type, hindersDodge, preventsRage flags WeaponData additions: - Damage type choices (blunt/piercing/slashing + elemental) - equippedHand for dual-wielding (main/off/both) EquipmentData additions: - isTrinket and canCastThrough for spell component tracking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
225 lines
6.8 KiB
JavaScript
225 lines
6.8 KiB
JavaScript
/**
|
|
* NPC/Monster Data Model
|
|
*
|
|
* Defines the data schema for non-player characters and monsters in Vagabond RPG.
|
|
* Uses a simplified stat block format common in OSR-style games.
|
|
*
|
|
* Key Attributes:
|
|
* - HD (Hit Dice): Determines combat prowess
|
|
* - HP: Hit points (separate from HD for flexibility)
|
|
* - TL (Threat Level): Encounter balancing value (0.1 to 10+)
|
|
* - Zone: AI behavior hint (Frontline, Midline, Backline)
|
|
* - Morale: 2d6 check threshold for fleeing
|
|
* - Actions: Attack actions with names and damage
|
|
* - Abilities: Special abilities and traits
|
|
*
|
|
* @extends VagabondActorBase
|
|
*/
|
|
import VagabondActorBase from "./base-actor.mjs";
|
|
|
|
export default class NPCData extends VagabondActorBase {
|
|
/**
|
|
* Define the schema for NPC actors.
|
|
*
|
|
* @returns {Object} The schema definition
|
|
*/
|
|
static defineSchema() {
|
|
const fields = foundry.data.fields;
|
|
const baseSchema = super.defineSchema();
|
|
|
|
return {
|
|
...baseSchema,
|
|
|
|
// Hit Dice - represents overall power level
|
|
hd: new fields.NumberField({
|
|
required: true,
|
|
nullable: false,
|
|
integer: true,
|
|
initial: 1,
|
|
min: 0,
|
|
}),
|
|
|
|
// Hit Points
|
|
hp: new fields.SchemaField({
|
|
value: new fields.NumberField({ integer: true, initial: 4, min: 0 }),
|
|
max: new fields.NumberField({ integer: true, initial: 4, min: 1 }),
|
|
}),
|
|
|
|
// Threat Level - used for encounter balancing
|
|
// 0.1 = minion, 1.0 = standard, 2.0+ = elite/boss
|
|
tl: new fields.NumberField({
|
|
required: true,
|
|
nullable: false,
|
|
initial: 1,
|
|
min: 0,
|
|
}),
|
|
|
|
// Armor value
|
|
armor: new fields.NumberField({
|
|
required: true,
|
|
nullable: false,
|
|
integer: true,
|
|
initial: 0,
|
|
min: 0,
|
|
}),
|
|
|
|
// Morale score - flee on 2d6 > Morale when triggered
|
|
morale: new fields.NumberField({
|
|
required: true,
|
|
nullable: false,
|
|
integer: true,
|
|
initial: 7,
|
|
min: 2,
|
|
max: 12,
|
|
}),
|
|
|
|
// Morale check tracking
|
|
moraleStatus: new fields.SchemaField({
|
|
// Has a morale check been triggered this combat?
|
|
checkedThisCombat: new fields.BooleanField({ initial: false }),
|
|
// What triggered the last check?
|
|
lastTrigger: new fields.StringField({
|
|
initial: "",
|
|
choices: ["", "first-death", "half-hp", "half-incapacitated", "leader-death"],
|
|
}),
|
|
// Result of the last morale check
|
|
lastResult: new fields.StringField({
|
|
initial: "",
|
|
choices: ["", "passed", "failed-retreat", "failed-surrender"],
|
|
}),
|
|
// Is this NPC currently fleeing/surrendered?
|
|
broken: new fields.BooleanField({ initial: false }),
|
|
}),
|
|
|
|
// Number appearing (for random encounters)
|
|
appearing: new fields.StringField({ initial: "1d6" }),
|
|
|
|
// Preferred combat zone (AI hint)
|
|
zone: new fields.StringField({
|
|
required: true,
|
|
initial: "frontline",
|
|
choices: ["frontline", "midline", "backline"],
|
|
}),
|
|
|
|
// Size category
|
|
size: new fields.StringField({ initial: "medium" }),
|
|
|
|
// Being type (for targeting by certain effects)
|
|
beingType: new fields.StringField({ initial: "beast" }),
|
|
|
|
// Senses (vision types)
|
|
senses: new fields.SchemaField({
|
|
darksight: new fields.BooleanField({ initial: false }),
|
|
blindsight: new fields.NumberField({ integer: true, initial: 0, min: 0 }), // Range in feet
|
|
tremorsense: new fields.NumberField({ integer: true, initial: 0, min: 0 }), // Range in feet
|
|
}),
|
|
|
|
// Movement speed
|
|
speed: new fields.SchemaField({
|
|
value: new fields.NumberField({ integer: true, initial: 30 }),
|
|
fly: new fields.NumberField({ integer: true, initial: 0 }),
|
|
swim: new fields.NumberField({ integer: true, initial: 0 }),
|
|
climb: new fields.NumberField({ integer: true, initial: 0 }),
|
|
}),
|
|
|
|
// Damage immunities
|
|
immunities: new fields.ArrayField(new fields.StringField(), { initial: [] }),
|
|
|
|
// Damage vulnerabilities (weaknesses)
|
|
weaknesses: new fields.ArrayField(new fields.StringField(), { initial: [] }),
|
|
|
|
// Damage resistances
|
|
resistances: new fields.ArrayField(new fields.StringField(), { initial: [] }),
|
|
|
|
// Attack actions
|
|
actions: new fields.ArrayField(
|
|
new fields.SchemaField({
|
|
name: new fields.StringField({ required: true }),
|
|
description: new fields.HTMLField({ required: false, blank: true }),
|
|
attackType: new fields.StringField({ initial: "melee" }),
|
|
damage: new fields.StringField({ initial: "1d6" }),
|
|
damageType: new fields.StringField({ initial: "blunt" }),
|
|
range: new fields.StringField({ required: false }),
|
|
properties: new fields.ArrayField(new fields.StringField(), { initial: [] }),
|
|
}),
|
|
{ initial: [] }
|
|
),
|
|
|
|
// Special abilities
|
|
abilities: new fields.ArrayField(
|
|
new fields.SchemaField({
|
|
name: new fields.StringField({ required: true }),
|
|
description: new fields.HTMLField({ required: true }),
|
|
passive: new fields.BooleanField({ initial: true }),
|
|
}),
|
|
{ initial: [] }
|
|
),
|
|
|
|
// Loot/treasure
|
|
loot: new fields.HTMLField({ required: false, blank: true }),
|
|
|
|
// Notes for GM
|
|
gmNotes: new fields.HTMLField({ required: false, blank: true }),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Prepare derived data for the NPC.
|
|
*/
|
|
prepareDerivedData() {
|
|
super.prepareDerivedData();
|
|
|
|
// Ensure HP max is at least HD if not set higher
|
|
if (this.hp.max < this.hd) {
|
|
this.hp.max = this.hd * 4; // Default HD to HP conversion
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get zone behavior description for the GM.
|
|
*
|
|
* @returns {string} Description of typical behavior for this zone
|
|
*/
|
|
getZoneBehavior() {
|
|
const behaviors = {
|
|
frontline:
|
|
"Engages in melee combat. Moves toward nearest enemy. Prioritizes blocking access to allies.",
|
|
midline:
|
|
"Maintains medium range. Uses ranged attacks or support abilities. Retreats if engaged in melee.",
|
|
backline:
|
|
"Stays at maximum range. Uses ranged attacks, magic, or support. Flees if enemies approach.",
|
|
};
|
|
return behaviors[this.zone] || behaviors.frontline;
|
|
}
|
|
|
|
/**
|
|
* Check if morale check is needed.
|
|
* Typically triggered when:
|
|
* - First ally dies
|
|
* - NPC reduced to half HP
|
|
* - Leader dies
|
|
*
|
|
* @returns {boolean} True if morale should be checked
|
|
*/
|
|
shouldCheckMorale() {
|
|
// Check if at or below half HP
|
|
return this.hp.value <= Math.floor(this.hp.max / 2);
|
|
}
|
|
|
|
/**
|
|
* Get the roll data for this NPC.
|
|
*
|
|
* @returns {Object} Roll data object
|
|
*/
|
|
getRollData() {
|
|
const data = super.getRollData();
|
|
|
|
// Add core stats for formulas
|
|
data.hd = this.hd;
|
|
data.armor = this.armor;
|
|
data.morale = this.morale;
|
|
|
|
return data;
|
|
}
|
|
}
|