/** * NPC/Monster Sheet for Vagabond RPG * * Compact stat block format sheet for NPCs and monsters: * - Header with HD, HP, TL, Zone * - Combat stats (Armor, Morale, Speed) * - Immunities/Weaknesses/Resistances * - Actions list with attack buttons * - Abilities list * - GM notes * * @extends VagabondActorSheet */ import VagabondActorSheet from "./base-actor-sheet.mjs"; export default class VagabondNPCSheet extends VagabondActorSheet { /* -------------------------------------------- */ /* Static Properties */ /* -------------------------------------------- */ /** @override */ static DEFAULT_OPTIONS = foundry.utils.mergeObject( super.DEFAULT_OPTIONS, { classes: ["vagabond", "sheet", "actor", "npc"], position: { width: 520, height: 600, }, actions: { ...VagabondActorSheet.DEFAULT_OPTIONS.actions, rollMorale: VagabondNPCSheet.#onRollMorale, rollAction: VagabondNPCSheet.#onRollAction, rollAppearing: VagabondNPCSheet.#onRollAppearing, addAction: VagabondNPCSheet.#onAddAction, deleteAction: VagabondNPCSheet.#onDeleteAction, addAbility: VagabondNPCSheet.#onAddAbility, deleteAbility: VagabondNPCSheet.#onDeleteAbility, }, }, { inplace: false } ); /** @override */ static PARTS = { header: { template: "systems/vagabond/templates/actor/npc-header.hbs", }, body: { template: "systems/vagabond/templates/actor/npc-body.hbs", }, }; /* -------------------------------------------- */ /* Getters */ /* -------------------------------------------- */ /** @override */ get tabs() { // NPC sheets don't use tabs - all content on one page return []; } /* -------------------------------------------- */ /* Data Preparation */ /* -------------------------------------------- */ /** @override */ async _prepareTypeContext(context, _options) { const system = this.actor.system; // Core combat stats context.hd = system.hd; context.hp = { value: system.hp.value, max: system.hp.max, percent: Math.round((system.hp.value / system.hp.max) * 100) || 0, isHalf: system.hp.value <= Math.floor(system.hp.max / 2), isDead: system.hp.value <= 0, }; context.tl = system.tl; context.armor = system.armor; context.morale = system.morale; context.moraleStatus = system.moraleStatus; // Zone with behavior hint context.zone = system.zone; context.zoneBehavior = system.getZoneBehavior?.() || ""; context.zoneOptions = { frontline: "VAGABOND.ZoneFrontline", midline: "VAGABOND.ZoneMidline", backline: "VAGABOND.ZoneBackline", }; // Size and being type context.size = system.size; context.beingType = system.beingType; context.sizeOptions = CONFIG.VAGABOND?.sizes || {}; context.beingTypeOptions = CONFIG.VAGABOND?.beingTypes || {}; // Speed (base value only) context.speed = system.speed.value; // Movement capabilities (boolean toggles) context.movement = system.movement; context.hasMovement = system.movement.climb || system.movement.cling || system.movement.fly || system.movement.phase || system.movement.swim; // Senses context.senses = system.senses; context.hasSenses = system.senses.allsight || system.senses.blindsight || system.senses.darkvision || system.senses.echolocation || system.senses.seismicsense || system.senses.telepathy; // Damage modifiers context.immunities = system.immunities || []; context.weaknesses = system.weaknesses || []; context.resistances = system.resistances || []; context.hasDamageModifiers = context.immunities.length > 0 || context.weaknesses.length > 0 || context.resistances.length > 0; // Actions with index for editing context.actions = (system.actions || []).map((action, index) => ({ ...action, index, })); context.hasActions = context.actions.length > 0; // Abilities with index for editing context.abilities = (system.abilities || []).map((ability, index) => ({ ...ability, index, })); context.hasAbilities = context.abilities.length > 0; // Appearing (encounter numbers) context.appearing = system.appearing; // Loot and GM notes context.loot = system.loot; context.gmNotes = system.gmNotes; // Damage type options for actions context.damageTypeOptions = CONFIG.VAGABOND?.damageTypes || {}; context.attackTypeOptions = { melee: "VAGABOND.AttackMelee", ranged: "VAGABOND.AttackRanged", }; } /** @override */ _configureRenderOptions(options) { super._configureRenderOptions(options); // NPC sheets render header and body (no tabs) options.parts = ["header", "body"]; } /* -------------------------------------------- */ /* Action Handlers */ /* -------------------------------------------- */ /** * Handle morale roll. * @param {PointerEvent} event * @param {HTMLElement} target */ static async #onRollMorale(event, _target) { event.preventDefault(); await this.actor.rollMorale({ trigger: "manual" }); } /** * Handle action roll (attack). * @param {PointerEvent} event * @param {HTMLElement} target */ static async #onRollAction(event, target) { event.preventDefault(); const actionIndex = parseInt(target.dataset.actionIndex, 10); const action = this.actor.system.actions[actionIndex]; if (!action) return; // Roll the damage for this action const roll = await new Roll(action.damage).evaluate(); // Create chat message const content = `
${action.description}
` : ""}Damage: [[/r ${action.damage}]] ${action.damageType}
${action.range ? `Range: ${action.range}
` : ""}