/** * L5R GM Monitor Windows * @extends {FormApplication} */ export class GmMonitor extends FormApplication { /** * Settings */ object = { view: "characters", // characters|armies actors: [], }; /** * Assign the default options * @override */ static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { id: "l5r5e-gm-monitor", classes: ["l5r5e", "gm-monitor"], template: CONFIG.l5r5e.paths.templates + "gm/gm-monitor.html", title: game.i18n.localize("l5r5e.gm.monitor.title"), width: 800, height: 300, resizable: true, closeOnSubmit: false, submitOnClose: false, submitOnChange: false, dragDrop: [{ dragSelector: null, dropSelector: null }], }); } /** * Add the Switch View button on top of sheet * @override */ _getHeaderButtons() { let buttons = super._getHeaderButtons(); // Switch view Characters/Armies buttons.unshift({ label: game.i18n.localize("l5r5e.gm.monitor.switch_view"), class: "switch-view", icon: "fas fa-repeat", onclick: () => game.l5r5e.HelpersL5r5e.debounce( "SwitchView-" + this.object.id, () => { this.object.view = this.object.view === "armies" ? "characters" : "armies"; this.render(false); }, 500, true )(), }); // Add selected tokens buttons.unshift({ label: game.i18n.localize("l5r5e.gm.monitor.add_selected_tokens"), class: "add-selected-token", icon: "fas fa-users", onclick: () => game.l5r5e.HelpersL5r5e.debounce( "AddSelectedToken-" + this.object.id, () => this.#addSelectedTokens(), 500, true )(), }); return buttons; } /** * Constructor * @param {ApplicationOptions} options */ constructor(options = {}) { super(options); this._initialize(); } /** * Refresh data (used from socket) */ async refresh() { if (!game.user.isGM) { return; } this._initialize(); this.render(false); } /** * Initialize the values * @private */ _initialize() { let actors; const uuidList = game.settings.get(CONFIG.l5r5e.namespace, "gm-monitor-actors"); if (uuidList.length > 0) { // Get actors from stored uuids actors = uuidList .map(uuid => { const doc = fromUuidSync(uuid); if (doc instanceof TokenDocument) { return doc.actor; } return doc; }) .filter(a => !!a); // skip null } else { // If empty add pc with owner actors = game.actors.filter((actor) => actor.type === "character" && actor.hasPlayerOwnerActive); this._saveActorsIds(); } // Sort by name asc actors.sort((a, b) => { return a.name.localeCompare(b.name); }); this.object.actors = actors; } /** * Add selected token on monitor if not already present */ #addSelectedTokens() { if (canvas.tokens.controlled.length > 0) { const actors2Add = canvas.tokens.controlled .map(t => t.actor) .filter(t => !!t && !this.object.actors.find((a) => a.uuid === t.uuid)); if (actors2Add.length < 1) { return; } this.object.actors = [ ...this.object.actors, ...actors2Add ]; this._saveActorsIds().then(() => this.render(false)); } } /** * Prevent non GM to render this windows * @override */ render(force = false, options = {}) { if (!game.user.isGM) { return false; } return super.render(force, options); } /** * Construct and return the data object used to render the HTML template for this form application. * @param options * @return {Object} * @override */ async getData(options = null) { return { ...(await super.getData(options)), data: { ...this.object, actors: this.object.actors.filter((a) => (this.object.view === "armies" ? a.isArmy : !a.isArmy)), }, }; } /** * Listen to html elements * @param {jQuery} html HTML content of the sheet. * @override */ activateListeners(html) { super.activateListeners(html); if (!game.user.isGM) { return; } // Commons game.l5r5e.HelpersL5r5e.commonListeners(html); // Delete html.find(`.actor-remove-control`).on("click", this._removeActor.bind(this)); // Add/Subtract html.find(`.actor-modify-control`).on("mousedown", this._modifyActor.bind(this)); // Tooltips game.l5r5e.HelpersL5r5e.popupManager(html.find(".actor-infos-control"), async (event) => { const type = $(event.currentTarget).data("type"); if (!type) { return; } if (type === "text") { return $(event.currentTarget).data("text"); } const uuid = $(event.currentTarget).data("actor-uuid"); if (!uuid) { return; } const actor = this.object.actors.find((a) => a.uuid === uuid); if (!actor) { return; } switch (type) { case "armors": return this._getTooltipArmors(actor); case "weapons": return this._getTooltipWeapons(actor); case "global": return actor.isArmy ? this._getTooltipArmiesGlobal(actor) : this._getTooltipGlobal(actor); } }); } /** * Handle dropped data on the Actor sheet * @param {DragEvent} event */ async _onDrop(event) { // *** Everything below here is only needed if the sheet is editable *** if (!this.isEditable) { return; } const json = event.dataTransfer.getData("text/plain"); if (!json) { return; } const data = JSON.parse(json); if (!data || data.type !== "Actor" || !data.uuid || !!this.object.actors.find((a) => a.uuid === data.uuid)) { return; } const actor = fromUuidSync(data.uuid); if (!actor) { return; } // Switch view to current character type this.object.view = actor.isArmy ? "armies" : "characters"; this.object.actors.push(actor); return this._saveActorsIds(); } /** * Save the actors ids in settings * @return {Promise<*>} * @private */ async _saveActorsIds() { return game.settings.set( CONFIG.l5r5e.namespace, "gm-monitor-actors", this.object.actors.map((a) => a.uuid) ); } /** * Remove the link to a property for the current item * @param {Event} event * @return {Promise} * @private */ async _removeActor(event) { event.preventDefault(); event.stopPropagation(); const uuid = $(event.currentTarget).data("actor-uuid"); if (!uuid) { return; } this.object.actors = this.object.actors.filter((a) => a.uuid !== uuid); return this._saveActorsIds(); } /** * Add or subtract fatigue/strife/void/casualties/panic * @param event * @return {Promise} * @private */ async _modifyActor(event) { event.preventDefault(); event.stopPropagation(); const type = $(event.currentTarget).data("type"); if (!type) { console.warn("L5R5E | GMM | type not set", type); return; } const uuid = $(event.currentTarget).data("actor-uuid"); if (!uuid) { console.warn("L5R5E | GMM | actor uuid not set", type); return; } const actor = fromUuidSync(uuid); if (!actor) { console.warn("L5R5E | GMM | Actor not found", type); return; } // Mouse bt : middle = 0, left +1, right -1 const add = event.which === 2 ? -999 : event.which === 1 ? 1 : -1; // Stance let stanceIdx = CONFIG.l5r5e.stances.findIndex((s) => s === actor.system.stance) + (event.which === 1 ? 1 : -1); if (stanceIdx < 0) { stanceIdx = CONFIG.l5r5e.stances.length - 1; } else if (stanceIdx > CONFIG.l5r5e.stances.length - 1) { stanceIdx = 0; } const updateData = {}; switch (type) { // *** Characters *** case "fatigue": updateData["system.fatigue.value"] = Math.max(0, actor.system.fatigue.value + add); break; case "strife": updateData["system.strife.value"] = Math.max(0, actor.system.strife.value + add); break; case "void_points": updateData["system.void_points.value"] = Math.min( actor.system.void_points.max, Math.max(0, actor.system.void_points.value + add) ); break; case "stance": updateData["system.stance"] = CONFIG.l5r5e.stances[stanceIdx]; break; case "prepared": updateData["system.prepared"] = !actor.system.prepared; break; // *** Armies *** case "casualties": updateData["system.battle_readiness.casualties_strength.value"] = Math.max( 0, actor.system.battle_readiness.casualties_strength.value + add ); break; case "panic": updateData["system.battle_readiness.panic_discipline.value"] = Math.max( 0, actor.system.battle_readiness.panic_discipline.value + add ); break; default: console.warn("L5R5E | GMM | Unsupported type", type); break; } if (!foundry.utils.isEmpty(updateData)) { await actor.update(updateData); this.render(false); } } /** * Get tooltips information for this character * @param {ActorL5r5e} actor * @return {string} * @private */ async _getTooltipGlobal(actor) { const actorData = (await actor.sheet?.getData()?.data) || actor; // Peculiarities const pec = actor.items.filter((e) => e.type === "peculiarity"); const adv = pec .filter((e) => ["distinction", "passion"].includes(e.system.peculiarity_type)) .map((e) => e.name) .join(", "); const dis = pec .filter((e) => ["adversity", "anxiety"].includes(e.system.peculiarity_type)) .map((e) => e.name) .join(", "); // *** Template *** return renderTemplate(`${CONFIG.l5r5e.paths.templates}gm/monitor-tooltips/global.html`, { actorData: actorData, advantages: adv, disadvantages: dis, suffix: actorData.system.template === "pow" ? "_pow" : "", actor_type: actor.type, }); } /** * Get tooltips information for this army * @param {ActorL5r5e} actor * @return {string} * @private */ async _getTooltipArmiesGlobal(actor) { const actorData = (await actor.sheet?.getData()?.data) || actor; // *** Template *** return renderTemplate(`${CONFIG.l5r5e.paths.templates}gm/monitor-tooltips/global-armies.html`, { actorData: actorData, }); } /** * Get weapons information for this actor * @param {ActorL5r5e} actor * @return {string} * @private */ async _getTooltipWeapons(actor) { const display = (e) => { return ( e.name + ` ( ${e.system.range}` + ` / ${e.system.damage}` + ` / ${e.system.deadliness})` ); }; // Readied Weapons const readied = actor.items .filter((e) => e.type === "weapon" && e.system.equipped && !!e.system.readied) .map((e) => display(e)); // Equipped Weapons const sheathed = actor.items .filter((e) => e.type === "weapon" && e.system.equipped && !e.system.readied) .map((e) => display(e)); // *** Template *** return renderTemplate(`${CONFIG.l5r5e.paths.templates}gm/monitor-tooltips/weapons.html`, { readied, sheathed, }); } /** * Get armors information for this actor * @param {ActorL5r5e} actor * @return {string} * @private */ async _getTooltipArmors(actor) { // Equipped Armors const armors = actor.items .filter((e) => e.type === "armor" && e.system.equipped) .map( (e) => e.name + ` (${e.system.armor.physical}` + ` / ${e.system.armor.supernatural})` ); // *** Template *** return renderTemplate(`${CONFIG.l5r5e.paths.templates}gm/monitor-tooltips/armors.html`, { armors, }); } }