import ActorSheet5e from "./base.js"; /** * An Actor sheet for Vehicle type actors. * Extends the base ActorSheet5e class. * @type {ActorSheet5e} */ export default class ActorSheet5eVehicle extends ActorSheet5e { /** * Define default rendering options for the Vehicle sheet. * @returns {Object} */ static get defaultOptions() { return mergeObject(super.defaultOptions, { classes: ["sw5e", "sheet", "actor", "vehicle"], width: 605, height: 680 }); } /* -------------------------------------------- */ /** @override */ static unsupportedItemTypes = new Set(["class"]); /* -------------------------------------------- */ /** * Creates a new cargo entry for a vehicle Actor. */ static get newCargo() { return { name: '', quantity: 1 }; } /* -------------------------------------------- */ /** * Compute the total weight of the vehicle's cargo. * @param {Number} totalWeight The cumulative item weight from inventory items * @param {Object} actorData The data object for the Actor being rendered * @returns {{max: number, value: number, pct: number}} * @private */ _computeEncumbrance(totalWeight, actorData) { // Compute currency weight const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0); totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; // Vehicle weights are an order of magnitude greater. totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier; // Compute overall encumbrance const max = actorData.data.attributes.capacity.cargo; const pct = Math.clamped((totalWeight * 100) / max, 0, 100); return {value: totalWeight.toNearest(0.1), max, pct}; } /* -------------------------------------------- */ /** @override */ _getMovementSpeed(actorData, largestPrimary=true) { return super._getMovementSpeed(actorData, largestPrimary); } /* -------------------------------------------- */ /** * Prepare items that are mounted to a vehicle and require one or more crew * to operate. * @private */ _prepareCrewedItem(item) { // Determine crewed status const isCrewed = item.data.crewed; item.toggleClass = isCrewed ? 'active' : ''; item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`); // Handle crew actions if (item.type === 'feat' && item.data.activation.type === 'crew') { item.crew = item.data.activation.cost; item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`); if (item.data.cover === .5) item.cover = '½'; else if (item.data.cover === .75) item.cover = '¾'; else if (item.data.cover === null) item.cover = '—'; if (item.crew < 1 || item.crew === null) item.crew = '—'; } // Prepare vehicle weapons if (item.type === 'equipment' || item.type === 'weapon') { item.threshold = item.data.hp.dt ? item.data.hp.dt : '—'; } } /* -------------------------------------------- */ /** * Organize Owned Items for rendering the Vehicle sheet. * @private */ _prepareItems(data) { const cargoColumns = [{ label: game.i18n.localize('SW5E.Quantity'), css: 'item-qty', property: 'quantity', editable: 'Number' }]; const equipmentColumns = [{ label: game.i18n.localize('SW5E.Quantity'), css: 'item-qty', property: 'data.quantity' }, { label: game.i18n.localize('SW5E.AC'), css: 'item-ac', property: 'data.armor.value' }, { label: game.i18n.localize('SW5E.HP'), css: 'item-hp', property: 'data.hp.value', editable: 'Number' }, { label: game.i18n.localize('SW5E.Threshold'), css: 'item-threshold', property: 'threshold' }]; const features = { actions: { label: game.i18n.localize('SW5E.ActionPl'), items: [], crewable: true, dataset: {type: 'feat', 'activation.type': 'crew'}, columns: [{ label: game.i18n.localize('SW5E.VehicleCrew'), css: 'item-crew', property: 'crew' }, { label: game.i18n.localize('SW5E.Cover'), css: 'item-cover', property: 'cover' }] }, equipment: { label: game.i18n.localize('SW5E.ItemTypeEquipment'), items: [], crewable: true, dataset: {type: 'equipment', 'armor.type': 'vehicle'}, columns: equipmentColumns }, passive: { label: game.i18n.localize('SW5E.Features'), items: [], dataset: {type: 'feat'} }, reactions: { label: game.i18n.localize('SW5E.ReactionPl'), items: [], dataset: {type: 'feat', 'activation.type': 'reaction'} }, weapons: { label: game.i18n.localize('SW5E.ItemTypeWeaponPl'), items: [], crewable: true, dataset: {type: 'weapon', 'weapon-type': 'siege'}, columns: equipmentColumns } }; const cargo = { crew: { label: game.i18n.localize('SW5E.VehicleCrew'), items: data.data.cargo.crew, css: 'cargo-row crew', editableName: true, dataset: {type: 'crew'}, columns: cargoColumns }, passengers: { label: game.i18n.localize('SW5E.VehiclePassengers'), items: data.data.cargo.passengers, css: 'cargo-row passengers', editableName: true, dataset: {type: 'passengers'}, columns: cargoColumns }, cargo: { label: game.i18n.localize('SW5E.VehicleCargo'), items: [], dataset: {type: 'loot'}, columns: [{ label: game.i18n.localize('SW5E.Quantity'), css: 'item-qty', property: 'data.quantity', editable: 'Number' }, { label: game.i18n.localize('SW5E.Price'), css: 'item-price', property: 'data.price', editable: 'Number' }, { label: game.i18n.localize('SW5E.Weight'), css: 'item-weight', property: 'data.weight', editable: 'Number' }] } }; // Classify items owned by the vehicle and compute total cargo weight let totalWeight = 0; for (const item of data.items) { this._prepareCrewedItem(item); // Handle cargo explicitly const isCargo = item.flags.sw5e?.vehicleCargo === true; if ( isCargo ) { totalWeight += (item.data.weight || 0) * item.data.quantity; cargo.cargo.items.push(item); continue } // Handle non-cargo item types switch ( item.type ) { case "weapon": features.weapons.items.push(item); break; case "equipment": features.equipment.items.push(item); break; case "feat": if ( !item.data.activation.type || (item.data.activation.type === "none") ) features.passive.items.push(item); else if (item.data.activation.type === 'reaction') features.reactions.items.push(item); else features.actions.items.push(item); break; default: totalWeight += (item.data.weight || 0) * item.data.quantity; cargo.cargo.items.push(item); } } // Update the rendering context data data.features = Object.values(features); data.cargo = Object.values(cargo); data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data); } /* -------------------------------------------- */ /* Event Listeners and Handlers */ /* -------------------------------------------- */ /** @override */ activateListeners(html) { super.activateListeners(html); if (!this.options.editable) return; html.find('.item-toggle').click(this._onToggleItem.bind(this)); html.find('.item-hp input') .click(evt => evt.target.select()) .change(this._onHPChange.bind(this)); html.find('.item:not(.cargo-row) input[data-property]') .click(evt => evt.target.select()) .change(this._onEditInSheet.bind(this)); html.find('.cargo-row input') .click(evt => evt.target.select()) .change(this._onCargoRowChange.bind(this)); if (this.actor.data.data.attributes.actions.stations) { html.find('.counter.actions, .counter.action-thresholds').hide(); } } /* -------------------------------------------- */ /** * Handle saving a cargo row (i.e. crew or passenger) in-sheet. * @param event {Event} * @returns {Promise|null} * @private */ _onCargoRowChange(event) { event.preventDefault(); const target = event.currentTarget; const row = target.closest('.item'); const idx = Number(row.dataset.itemId); const property = row.classList.contains('crew') ? 'crew' : 'passengers'; // Get the cargo entry const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]); const entry = cargo[idx]; if (!entry) return null; // Update the cargo value const key = target.dataset.property || 'name'; const type = target.dataset.dtype; let value = target.value; if (type === 'Number') value = Number(value); entry[key] = value; // Perform the Actor update return this.actor.update({[`data.cargo.${property}`]: cargo}); } /* -------------------------------------------- */ /** * Handle editing certain values like quantity, price, and weight in-sheet. * @param event {Event} * @returns {Promise} * @private */ _onEditInSheet(event) { event.preventDefault(); const itemID = event.currentTarget.closest('.item').dataset.itemId; const item = this.actor.items.get(itemID); const property = event.currentTarget.dataset.property; const type = event.currentTarget.dataset.dtype; let value = event.currentTarget.value; switch (type) { case 'Number': value = parseInt(value); break; case 'Boolean': value = value === 'true'; break; } return item.update({[`${property}`]: value}); } /* -------------------------------------------- */ /** * Handle creating a new crew or passenger row. * @param event {Event} * @returns {Promise} * @private */ _onItemCreate(event) { event.preventDefault(); const target = event.currentTarget; const type = target.dataset.type; if (type === 'crew' || type === 'passengers') { const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]); cargo.push(this.constructor.newCargo); return this.actor.update({[`data.cargo.${type}`]: cargo}); } return super._onItemCreate(event); } /* -------------------------------------------- */ /** * Handle deleting a crew or passenger row. * @param event {Event} * @returns {Promise} * @private */ _onItemDelete(event) { event.preventDefault(); const row = event.currentTarget.closest('.item'); if (row.classList.contains('cargo-row')) { const idx = Number(row.dataset.itemId); const type = row.classList.contains('crew') ? 'crew' : 'passengers'; const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx); return this.actor.update({[`data.cargo.${type}`]: cargo}); } return super._onItemDelete(event); } /** @override */ async _onDropItemCreate(itemData) { const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"]; const isCargo = cargoTypes.includes(itemData.type) && (this._tabs[0].active === "cargo"); foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo); return super._onDropItemCreate(itemData); } /* -------------------------------------------- */ /** * Special handling for editing HP to clamp it within appropriate range. * @param event {Event} * @returns {Promise} * @private */ _onHPChange(event) { event.preventDefault(); const itemID = event.currentTarget.closest('.item').dataset.itemId; const item = this.actor.items.get(itemID); const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max); event.currentTarget.value = hp; return item.update({'data.hp.value': hp}); } /* -------------------------------------------- */ /** * Handle toggling an item's crewed status. * @param event {Event} * @returns {Promise} * @private */ _onToggleItem(event) { event.preventDefault(); const itemID = event.currentTarget.closest('.item').dataset.itemId; const item = this.actor.items.get(itemID); const crewed = !!item.data.data.crewed; return item.update({'data.crewed': !crewed}); } };