vagabond-rpg-foundryvtt/module/data/actor/npc.mjs
Cal Corum 694b11f423 Implement movement capability system with boolean toggles
Movement types (Climb, Cling, Fly, Phase, Swim) now use boolean
toggles instead of separate speed values, matching RAW where all
special movement uses base speed.

Changes:
- Update NPC and Character data models with movement schema
- Add movement section to NPC stats template (grid layout)
- Add movement section to character biography template
- Add localization strings with tooltip hints for each type
- Style movement grid to match senses section pattern
- Add rollable # Appearing label for NPC sheets
- Fix NPC sheet scrollbar visibility

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 00:16:44 -06:00

242 lines
7.7 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({
required: false,
nullable: true,
blank: false,
initial: null,
choices: ["first-death", "half-hp", "half-incapacitated", "leader-death", "manual"],
}),
// Result of the last morale check
lastResult: new fields.StringField({
required: false,
nullable: true,
blank: false,
initial: null,
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 - binary toggles, may be granted by abilities/traits
senses: new fields.SchemaField({
allsight: new fields.BooleanField({ initial: false }),
blindsight: new fields.BooleanField({ initial: false }),
darkvision: new fields.BooleanField({ initial: false }),
echolocation: new fields.BooleanField({ initial: false }),
seismicsense: new fields.BooleanField({ initial: false }),
telepathy: new fields.BooleanField({ initial: false }),
}),
// Movement speed - base value plus boolean movement capabilities
// All special movement types use the base speed value per RAW
speed: new fields.SchemaField({
value: new fields.NumberField({ integer: true, initial: 30 }),
}),
// Movement capabilities - boolean toggles for special movement types
// All use base speed value when enabled
movement: new fields.SchemaField({
climb: new fields.BooleanField({ initial: false }), // Full Speed while climbing
cling: new fields.BooleanField({ initial: false }), // As Climb, but can Move on ceilings
fly: new fields.BooleanField({ initial: false }), // Move through the air at full Speed
phase: new fields.BooleanField({ initial: false }), // Move in occupied space (5 dmg if ends turn there)
swim: new fields.BooleanField({ initial: false }), // Full Speed while swimming
}),
// 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;
}
}