490 lines
17 KiB
JavaScript
490 lines
17 KiB
JavaScript
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: '<p>Please confirm you want to change your role. This will also reset your specialty.</p>',
|
|
buttons: {
|
|
yes: {
|
|
icon: '<i class="fas fa-check"></i>',
|
|
label: 'Yes',
|
|
callback: async () => {
|
|
await this.setSpecialty(null, true);
|
|
await this.setRole(item);
|
|
super._onDropItemCreate(item);
|
|
},
|
|
},
|
|
no: {
|
|
icon: '<i class="fas fa-times"></i>',
|
|
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;
|
|
}
|
|
}
|