import { Action } from '../documents/action.mjs'; import { deepFind } from '../helpers/util.mjs'; import { CANDELAFVTT } from '../helpers/config.mjs'; /** * Extend the basic ActorSheet with some very simple modifications * @extends {ActorSheet} */ export class CandelafvttActorSheet extends ActorSheet { /** @override */ static get defaultOptions() { return mergeObject(super.defaultOptions, { classes: ['candelafvtt', 'sheet', 'actor'], template: 'systems/candelafvtt/templates/actor/actor-sheet.hbs', width: 600, height: 600, tabs: [ { navSelector: '.sheet-tabs', contentSelector: '.sheet-body', initial: 'actions', }, { navSelector: '.bio-tabs', contentSelector: '.bio-body', initial: 'bio-background', }, ], }); } /** @override */ get template() { return `systems/candelafvtt/templates/actor/actor-${this.actor.type.toLowerCase()}-sheet.hbs`; } /* -------------------------------------------- */ /** @override */ getData() { // Retrieve the data structure from the base sheet. You can inspect or log // the context variable to see the structure, but some key properties for // sheets are the actor object, the data object, whether or not it's // editable, the items array, and the effects array. const context = super.getData(); // Use a safe clone of the actor data for further operations. const actorData = this.actor.toObject(false); // Add the actor's data to context.data for easier access, as well as flags. context.system = actorData.system; context.flags = actorData.flags; // Prepare character data and items. if (actorData.type == CONFIG.CANDELAFVTT.types.character) { this._prepareItems(context); this._prepareCharacterData(context); } // Prepare circle data and items. if (actorData.type == CONFIG.CANDELAFVTT.types.circle) { this._prepareItems(context); this._prepareCircleData(context); } // Add roll data for TinyMCE editors. context.rollData = context.actor.getRollData(); // TODO remove console.log(context); return context; } /** * Prepare character sheets. * * @param {Object} context The actor context. */ async _prepareCharacterData(context) { // get current circle data if (context.system.circle.uuid) { const actors = foundry.utils.parseUuid(context.system.circle.uuid); let circle = actors.collection.get(actors.documentId); if (!circle) { // circle is probably gone, clear it let updateData = {}; updateData['system.circle.uuid'] = ''; updateData['system.circle.name'] = ''; updateData['system.circle.color'] = ''; context.system.circle.name = ''; context.system.circle.color = ''; await this.actor.update(updateData); } else { context.system.circle.name = circle.name; context.system.circle.color = circle.system.color; document.documentElement.style.setProperty('--color-shadow-primary', circle.system.color); } } } /** * Prepare circle sheets. * * @param {Object} context The actor context. */ _prepareCircleData(context) { // add image paths context.system.illuminationCandleImg = 'systems/candelafvtt/img/illumination_candle.png'; context.system.flameImg = 'systems/candelafvtt/img/flame.svg'; } /** * Organize and classify Items. * * @param {Object} context The actor context. */ _prepareItems(context) { // Initialize containers. const gear = []; const abilities = []; const illuminationKeys = []; // Iterate through items, allocating to containers for (let i of context.items) { i.img = i.img || DEFAULT_TOKEN; if (i.type === CONFIG.CANDELAFVTT.types.gear) { gear.push(i); } // Append to features. else if (i.type === CONFIG.CANDELAFVTT.types.ability) { abilities.push(i); } else if (i.type === CONFIG.CANDELAFVTT.types.illuminationKey) { illuminationKeys.push(i); } } // Assign and return context.gear = gear; context.abilities = abilities; context.illuminationKeys = illuminationKeys; } /* -------------------------------------------- */ /** @override */ activateListeners(html) { super.activateListeners(html); // Render the item sheet for viewing/editing prior to the editable check. html.find('.item-edit').click(ev => { const li = $(ev.currentTarget).parents('.item'); const item = this.actor.items.get(li.data('itemId')); item.sheet.render(true); }); // ------------------------------------------------------------- // Everything below here is only needed if the sheet is editable if (!this.isEditable) return; // Add Inventory Item html.find('.item-create').click(this._onItemCreate.bind(this)); // Delete Inventory Item html.find('.item-delete').click(ev => { const li = $(ev.currentTarget).parents('.item'); const item = this.actor.items.get(li.data('itemId')); item.delete(); li.slideUp(200, () => this.render(false)); }); // Delete Inventory Item html.find('.item-toggle-equip').click(ev => { const li = $(ev.currentTarget).parents('.item'); const item = this.actor.items.get(li.data('itemId')); let updateData = {}; updateData['system.equipped'] = !item.system.equipped; item.update(updateData); }); if (this.actor.type == CANDELAFVTT.types.circle) { // illumination. html.find('.illumination-point').click(this.onIlluminationClick.bind(this)); html.find('.illumination-reset').click(this.onIlluminationReset.bind(this)); // remove circle member. html.find('.member-remove').click(this.onMemberRemove.bind(this)); } // Rollable abilities. html.find('.rollable').click(this._onRoll.bind(this)); html.find('.character-biography').click(() => { setTimeout(() => { this.render(true); }, 50); }); // Drag events for macros. if (this.actor.isOwner) { let handler = ev => this._onDragStart(ev); html.find('li.item').each((i, li) => { if (li.classList.contains('inventory-header')) return; li.setAttribute('draggable', true); li.addEventListener('dragstart', handler, false); }); } } /** * Handle a dropped actor (which should be a circle being dropped on an actor) * @param {Event} event The event * @param {any} actor The actor ID object being dropped * @private */ /** @override */ async _onDropActor(event, actor) { let actors = foundry.utils.parseUuid(actor.uuid); let circle = actors.collection.get(actors.documentId); if (this.actor.type == CONFIG.CANDELAFVTT.types.character && circle.type == CONFIG.CANDELAFVTT.types.circle) { let updateData = {}; updateData['system.circle.uuid'] = circle.uuid; updateData['system.circle.name'] = circle.name; updateData['system.circle.color'] = circle.system.color; let circleUpdateData = {}; circleUpdateData['system.members'] = circle.system.members; circleUpdateData['system.members'].push({ uuid: this.actor.uuid }); await this.actor.update(updateData); await circle.update(circleUpdateData); } super._onDropActor(event); } /** * Handle a dropped item * @param {Event} event The originating click event * @private */ /** @override */ async _onDropItemCreate(item) { // if item is a role, ask for confirmation and clean out the current role and spec if (item.type == CONFIG.CANDELAFVTT.types.role) { let d = new Dialog({ title: 'Change Role', content: '

Please confirm you want to change your role. This will also reset your specialty.

', buttons: { yes: { icon: '', label: 'Yes', callback: async () => { await this.setSpecialty(null, true); await this.setRole(item); super._onDropItemCreate(item); }, }, no: { icon: '', label: 'No', callback: () => {}, }, }, default: 'no', }); d.render(true); // if item is a spec, set the spec } else if (item.type == CONFIG.CANDELAFVTT.types.specialty) { this.setSpecialty(item); // else just let the default handler handle it } else { super._onDropItemCreate(item); } } /** * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset * @param {Event} event The originating click event * @private */ async _onItemCreate(event) { event.preventDefault(); const header = event.currentTarget; // Get the type of item to create. const type = header.dataset.type; // Grab any data associated with this control. const data = duplicate(header.dataset); // Initialize a default name. const name = `New ${type.capitalize()}`; // Prepare the item object. const itemData = { name: name, type: type, system: data, }; // Remove the type from the dataset since it's in the itemData.type prop. delete itemData.system['type']; // Finally, create the item! return await Item.create(itemData, { parent: this.actor }); } /** * Handle clickable rolls. * @param {Event} event The originating click event * @private */ _onRoll(event) { event.preventDefault(); const element = event.currentTarget; const dataset = element.dataset; // Handle item rolls. if (dataset.rollType) { // normal item roll if (dataset.rollType == 'item') { const itemId = element.closest('.item').dataset.itemId; const item = this.actor.items.get(itemId); if (item) return item.roll(); // action roll, use custom roll } else if (dataset.rollType == 'action') { const actionId = element.closest('.action').dataset.actionId; const actionPath = element.closest('.action').dataset.attribute; const action = deepFind(this.actor, actionPath); if (action) return Action.rollAction(action, actionId); } } // Handle rolls that supply the formula directly. if (dataset.roll) { let label = dataset.label ? `[ability] ${dataset.label}` : ''; let roll = new Roll(dataset.roll, this.actor.getRollData()); roll.toMessage({ speaker: ChatMessage.getSpeaker({ actor: this.actor }), flavor: label, rollMode: game.settings.get('core', 'rollMode'), type: CONST.CHAT_MESSAGE_TYPES.ROLL, rolls: [roll], }); return roll; } } /* -------------------------------------------- */ /** * Handle a click on illumination points. * @param {Event} event The originating click event * @private */ onIlluminationClick(event) { const button = event.currentTarget; const value = Number(button.dataset.value); const currentValue = this.actor.system.illumination.value; let updateData = {}; // if clicking a selected point, reset it if (currentValue == value) { updateData['system.illumination.value'] = value - 1; } else { updateData['system.illumination.value'] = value; } this.actor.update(updateData); } /** * Handle a click on the illumination reset button. * @param {Event} event The originating click event * @private */ onIlluminationReset() { let updateData = {}; updateData['system.illumination.value'] = 0; this.actor.update(updateData); } /** * Handle a click on the illumination reset button. * @param {Event} event The originating click event * @private */ onMemberRemove(event) { const uuid = $(event.currentTarget).closest('.item').data('memberUuid'); let circleUpdateData = {}; circleUpdateData['system.members'] = this.actor.system.members.filter(function (member) { return member.uuid !== uuid; }); this.actor.update(circleUpdateData); const actors = foundry.utils.parseUuid(uuid); const actor = actors.collection.get(actors.documentId); console.log(actor); if (actor) { let actorUpdateData = {}; actorUpdateData['system.circle.uuid'] = ''; actorUpdateData['system.circle.color'] = ''; actorUpdateData['system.circle.name'] = ''; actor.update(actorUpdateData); } } /* -------------------------------------------- */ /** * Reset the role * @param {Object} role The new role * @private */ async setRole(role) { // get id of the current role of the actor let embeddedUpdateData = []; for (let i of this.actor.items) { if (i.type == CONFIG.CANDELAFVTT.types.role) { embeddedUpdateData.push(i.id); } } // delete current role of the actor await this.actor.deleteEmbeddedDocuments('Item', embeddedUpdateData); // prepare new role data let updateData = {}; updateData['system.role'] = role; // set new role await this.actor.update(updateData); } /** * (Re)set the specialty * @param {Object} spec The new role * @param {boolean} reset Force a reset * @private */ async setSpecialty(spec, reset = false) { // check if specialty matches role or reset is forced if (reset || this.checkSpecialtyAgainstRole(spec.system.roleIdentifier)) { // find current specialty let embeddedUpdateData = []; for (let i of this.actor.items) { if (i.type == CONFIG.CANDELAFVTT.types.specialty) { embeddedUpdateData.push(i.id); } } // delete current specialty await this.actor.deleteEmbeddedDocuments('Item', embeddedUpdateData); // set new specialty if (!reset) { let updateData = {}; updateData['system.specialty'] = spec; for (let [k, v] of Object.entries(spec.system.drives)) { updateData['system.actionCategories.' + k + '.drives.max'] = v; updateData['system.actionCategories.' + k + '.drives.value'] = v; updateData['system.actionCategories.' + k + '.resistance.max'] = Math.floor(v / 3); updateData['system.actionCategories.' + k + '.resistance.value'] = Math.floor(v / 3); } for (let [ck, cv] of Object.entries(spec.system.actionRatings)) { for (let [k, v] of Object.entries(cv)) { updateData['system.actionCategories.' + ck + '.actions.' + k + '.value'] = v; } } await this.actor.update(updateData); } // if the spec is invalid, prompt an error and return } else { ui.notifications.error(game.i18n.localize('CANDELAFVTT.errors-invalid-spec')); return; } } /** * Check if spec role id matches selected role * @param {string} specRoleID The spec id * @private */ checkSpecialtyAgainstRole(specRoleID) { if (this.actor.system.role && this.actor.system.role.system.identifier == specRoleID) { return true; } return false; } }