diff --git a/module/actor/actor-sheet.js b/module/actor/actor-sheet.js
index 80ea907..7a8a85a 100644
--- a/module/actor/actor-sheet.js
+++ b/module/actor/actor-sheet.js
@@ -1,38 +1,17 @@
import DG from "../config.js";
-import DGSheetMixin from "../base-sheet.js";
-import {
- DGPercentileRoll,
- DGLethalityRoll,
- DGDamageRoll,
- DGSanityDamageRoll,
-} from "../roll/roll.js";
-
-const { ActorSheetV2 } = foundry.applications.sheets;
+import DGActorSheet from "../sheets/base-actor-sheet.js";
/** @extends {DGSheetMixin(ActorSheetV2)} */
-export default class DeltaGreenActorSheet extends DGSheetMixin(ActorSheetV2) {
+export default class DGAgentSheet extends DGActorSheet {
/** @override */
static DEFAULT_OPTIONS = /** @type {const} */ ({
- css: ["actor"],
- position: { width: 750, height: 770 },
actions: {
- // Skill/Item actions.
- itemAction: DeltaGreenActorSheet._onItemAction,
- typedSkillAction: DeltaGreenActorSheet._onTypedSkillAction,
- specialTrainingAction: DeltaGreenActorSheet._onSpecialTrainingAction,
- roll: DeltaGreenActorSheet._onRoll,
- rollLuck: DeltaGreenActorSheet._onRollLuck,
// Toggles/resets.
- clearBondDamage: DeltaGreenActorSheet._clearBondDamage,
- toggleBondDamage: DeltaGreenActorSheet._toggleBondDamage,
- toggleEquipped: DeltaGreenActorSheet._toggleEquipped,
- toggleItemSortMode: DeltaGreenActorSheet._toggleItemSortMode,
- toggleShowUntrained: DeltaGreenActorSheet._toggleShowUntrained,
- toggleLethality: DeltaGreenActorSheet._toggleLethality,
- resetBreakingPoint: DeltaGreenActorSheet._resetBreakingPoint,
+ clearBondDamage: DGAgentSheet._clearBondDamage,
+ toggleBondDamage: DGAgentSheet._toggleBondDamage,
+ resetBreakingPoint: DGAgentSheet._resetBreakingPoint,
// Other actions.
- applySkillImprovements: DeltaGreenActorSheet._applySkillImprovements,
- browsePack: DeltaGreenActorSheet._browsePack,
+ applySkillImprovements: DGAgentSheet._applySkillImprovements,
},
});
@@ -104,81 +83,6 @@ export default class DeltaGreenActorSheet extends DGSheetMixin(ActorSheetV2) {
/* -------------------------------------------- */
- /** @override */
- get template() {
- const templatePath = "systems/deltagreen/templates/actor";
- let templateName = "limited-sheet.html";
-
- if (this.actor !== null) {
- const actorIsLimited = !game.user.isGM && this.actor.limited;
-
- switch (this.actor.type) {
- case "agent":
- templateName = actorIsLimited ? "limited-sheet.html" : "actor-sheet.html";
- break;
- case "unnatural":
- templateName = "unnatural-sheet.html"; // No limited sheet
- break;
- case "npc":
- templateName = actorIsLimited ? "npc-limited-sheet.html" : "npc-sheet.html";
- break;
- case "vehicle":
- templateName = "vehicle-sheet.html"; // No limited sheet
- break;
- default:
- templateName = "actor-sheet.html"; // No limited sheet
- break;
- }
- }
-
- return `${templatePath}/${templateName}`;
- }
-
- /** @override */
- _configureRenderOptions(options) {
- super._configureRenderOptions(options);
- switch (this.actor.type) {
- case "agent":
- break;
- case "unnatural":
- break;
- case "npc":
- options.parts = ["header", "tabs", ...this.constructor.TABS_NPC];
- break;
- case "vehicle":
- break;
- default:
- break;
- }
- }
-
- /** @override - Manipulate which tabs are rendered. */
- _prepareTabs(group) {
- const tabs = super._prepareTabs(group);
-
- /**
- * @function
- * @name filterTabs
- * @param {string[]} tabIds - List of tab IDs to keep. All other tabs will be removed.
- * @description Remove all tabs from the `tabs` object except those with IDs in `tabIds`.
- */
- const filterTabs = (tabIds) => {
- Object.values(tabs)
- .filter((tab) => !tabIds.includes(tab.id))
- .forEach(({ id }) => delete tabs[id]);
- };
-
- switch (this.actor.type) {
- case "npc":
- filterTabs(this.constructor.TABS_NPC);
- break;
- default:
- break;
- }
-
- return tabs;
- }
-
/** @override */
async _prepareContext(options) {
const context = await super._prepareContext(options);
@@ -190,139 +94,6 @@ export default class DeltaGreenActorSheet extends DGSheetMixin(ActorSheetV2) {
this.actor,
);
- // Make it easy for the sheet handlebars to understand how to sort the skills.
- context.sortSkillsSetting = game.settings.get("deltagreen", "sortSkills");
-
- if (this.actor.type !== "vehicle") {
- // fill an array that is sorted based on the appropriate localized entry
- const sortedSkills = [];
- for (const [key, skill] of Object.entries(this.actor.system.skills)) {
- skill.key = key;
-
- if (game.i18n.lang === "ja") {
- skill.sortLabel = game.i18n.localize(`DG.Skills.ruby.${key}`);
- } else {
- skill.sortLabel = game.i18n.localize(`DG.Skills.${key}`);
- }
-
- if (skill.sortLabel === "" || skill.sortLabel === `DG.Skills.${key}`) {
- skill.sortLabel = skill.key;
- }
-
- // if the actor is an NPC or Unnatural, and they have 'hide untrained skills' active,
- // it will break the sorting logic, so we have to skip over these
- if (
- !(
- (this.actor.type === "npc" || this.actor.type === "unnatural") &&
- this.actor.system.showUntrainedSkills &&
- skill.proficiency < 1
- )
- ) {
- sortedSkills.push(skill);
- }
- }
-
- sortedSkills.sort((a, b) => {
- return a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang);
- });
-
- // if sorting by columns, re-arrange the array to be columns first, then rows
- if (game.settings.get("deltagreen", "sortSkills")) {
- const columnSortedSkills = this.reorderForColumnSorting(
- sortedSkills,
- 3,
- );
-
- this.actor.system.sortedSkills = columnSortedSkills;
- } else {
- this.actor.system.sortedSkills = sortedSkills;
- }
- }
- // Prepare a simplified version of the special training for display on sheet.
- if (this.actor.type !== "vehicle") {
- const specialTraining = this.actor.system.specialTraining.map(
- (training) => {
- const simplifiedTraining = {
- name: training.name,
- id: training.id,
- key: training.attribute,
- };
- // Convert the machine-readable name to a human-readable one.
- switch (true) {
- // Stats
- case DG.statistics.includes(training.attribute):
- simplifiedTraining.attribute = `${training.attribute.toUpperCase()}x5`;
- simplifiedTraining.targetNumber =
- this.actor.system.statistics[training.attribute].x5;
- break;
- // Skills
- case DG.skills.includes(training.attribute):
- simplifiedTraining.attribute =
- this.actor.system.skills[training.attribute].label;
- simplifiedTraining.targetNumber =
- this.actor.system.skills[training.attribute].proficiency;
- break;
- // Typed Skills
- default:
- simplifiedTraining.attribute =
- this.actor.system.typedSkills[training.attribute].label;
- simplifiedTraining.targetNumber =
- this.actor.system.typedSkills[training.attribute].proficiency;
- break;
- }
- return simplifiedTraining;
- },
- );
- context.specialTraining = specialTraining;
- }
-
- // try to make a combined array of both typed skills and special trainings,
- // so that it can be sorted together neatly on the sheet
- if (this.actor.type !== "vehicle") {
- const sortedCustomSkills = [];
-
- for (const [key, skill] of Object.entries(
- this.actor.system.typedSkills,
- )) {
- skill.type = "typeSkill";
- skill.key = key;
- skill.sortLabel = `${skill.group}.${skill.label}`;
- skill.sortLabel = skill.sortLabel.toUpperCase();
- skill.actorType = this.actor.type;
-
- if (skill.sortLabel === "" || skill.sortLabel === `DG.Skills.${key}`) {
- skill.sortLabel = skill.key;
- }
-
- sortedCustomSkills.push(skill);
- }
-
- for (let i = 0; i < context.specialTraining.length; i++) {
- const training = context.specialTraining[i];
-
- training.type = "training";
- training.sortLabel = training.name.toUpperCase();
- training.actorType = this.actor.type;
-
- sortedCustomSkills.push(training);
- }
-
- sortedCustomSkills.sort(function (a, b) {
- return a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang);
- });
-
- if (game.settings.get("deltagreen", "sortSkills")) {
- const columnSortedSkills = this.reorderForColumnSorting(
- sortedCustomSkills,
- 2,
- );
-
- this.actor.system.sortedCustomSkills = columnSortedSkills;
- } else {
- this.actor.system.sortedCustomSkills = sortedCustomSkills;
- }
- }
-
switch (this.actor.type) {
case "agent": {
const enrichedDescription =
@@ -363,143 +134,6 @@ export default class DeltaGreenActorSheet extends DGSheetMixin(ActorSheetV2) {
return context;
}
- /** @override */
- async _onRender(context, options) {
- await super._onRender(context, options);
- const { element } = this;
-
- this._setRightClickListeners();
-
- if (this.actor.isOwner) {
- const handler = (ev) => this._onDragStart(ev);
- element.querySelectorAll("li.item").forEach((li) => {
- if (li.classList.contains("inventory-header")) return;
- li.setAttribute("draggable", true);
- li.addEventListener("dragstart", handler, false);
- });
- }
- }
-
- /** @override - Add buttons to the header controls. */
- _getHeaderControls() {
- const controls = super._getHeaderControls();
- controls.push({
- action: "rollLuck",
- label: "DG.RollLuck",
- icon: "fas fa-dice",
- });
- return controls;
- }
-
- /** @override */
- _onDragStart(event) {
- // Most of this is the standard Foundry implementation of _onDragStart
- const li = event.currentTarget;
- if (event.target.classList.contains("content-link")) return;
-
- // Create drag data
- let dragData;
-
- // Owned Items
- if (li.dataset.itemId) {
- const item = this.actor.items.get(li.dataset.itemId);
- dragData = item.toDragData();
- }
-
- // Active Effect
- if (li.dataset.effectId) {
- const effect = this.actor.effects.get(li.dataset.effectId);
- dragData = effect.toDragData();
- }
-
- if (!dragData) return;
-
- // this is custom, grab item data for creating item macros on the hotbar
- if (li.dataset.itemId) {
- const item = this.actor.items.get(li.dataset.itemId);
- dragData.itemData = item;
- }
-
- // Set data transfer
- event.dataTransfer.setData("text/plain", JSON.stringify(dragData));
- }
-
- /** @override */
- async _onDrop(event) {
- super._onDrop(event);
- // If alt key is held down, we will delete the original document.
- if (event.altKey) {
- // This is from Foundry. It will get the item data from the event.
- const TextEditor = foundry.applications.ux.TextEditor.implementation;
- const dragData = TextEditor.getDragEventData(event);
- // Make sure that we are dragging an item, otherwise this doesn't make sense.
- if (dragData.type === "Item") {
- const item = fromUuidSync(dragData.uuid);
- await item.delete();
- }
- }
- }
-
- /**
- * Listens for right click events on the actor sheet and executes a regular roll,
- * or luck roll, depending on the action.
- *
- * @returns {void}
- */
- _setRightClickListeners() {
- const { element } = this;
- element.addEventListener("contextmenu", (event) => {
- event.preventDefault();
- const target = event.target.closest(
- "[data-action='roll'],[data-action='rollLuck']",
- );
- if (!target) return;
-
- // Call _onRollLuck if luckRoll is the dispatched action.
- if (target.dataset.action === "rollLuck") {
- DeltaGreenActorSheet._onRollLuck.call(this, event, target);
- return;
- }
-
- // Otherwise, call _onRoll function.
- DeltaGreenActorSheet._onRoll.call(this, event, target);
- });
- }
-
- reorderForColumnSorting(arr, numCols) {
- const numRows = Math.ceil(arr.length / numCols); // Compute required rows
- const reordered = new Array(arr.length);
-
- // Determine how many elements each column gets
- const baseRowCount = Math.floor(arr.length / numCols); // Minimum rows per column
- const extraColumns = arr.length % numCols; // Some columns get an extra row
-
- const colHeights = new Array(numCols).fill(baseRowCount);
- for (let i = 0; i < extraColumns; i++) {
- colHeights[i] += 1; // Give extra elements to the first N columns
- }
-
- let index = 0; // move through alphabetical array, keeping track of what we've resorted already
-
- for (let col = 0; col < numCols; col++) {
- // need to check if this is a column that has more rows than the others or not
- const rowCount = colHeights[col];
-
- // loop down the column, filling out it's values from the alphabetical array
- for (let row = 0; row < rowCount; row++) {
- // calculate the new position for this value by column
- const newIndex = numCols * row + col;
-
- if (newIndex < arr.length) {
- reordered[newIndex] = arr[index];
- index += 1;
- }
- }
- }
-
- return reordered;
- }
-
// some handlers may wish to avoid leading players to think they should be seeking out magic
// so control whether an actor sheet shows the hypergeometry (rituals and tomes) section
shouldShowHyperGeometrySection(actor) {
@@ -662,33 +296,6 @@ export default class DeltaGreenActorSheet extends DGSheetMixin(ActorSheetV2) {
actor.tomes = tomes;
}
- // This only exists to give a chance to activate the modifier dialogue if desired
- // Cannot seem to trigger the event on a right-click, so unfortunately only applies to a shift-click currently.
- static async _onRollLuck(event) {
- // probably don't want rolls to trigger from a middle mouse click so just kill it here
- if (event.which === 2) return;
-
- const rollOptions = {
- rollType: "luck",
- key: "luck",
- actor: this.actor,
- };
-
- // Create a default 1d100 roll just in case.
- const roll = new DGPercentileRoll("1D100", {}, rollOptions);
- // Open dialog if user requests it.
- if (event.shiftKey || event.which === 3) {
- const dialogData = await roll.showDialog();
- if (!dialogData) return;
- roll.modifier += dialogData.targetModifier;
- roll.options.rollMode = dialogData.rollMode;
- }
- // Evaluate the roll.
- await roll.evaluate();
- // Send the roll to chat.
- roll.toChat();
- }
-
activeEffectTest(sheet) {
console.log(sheet.actor.uuid);
const owner = sheet.actor;
@@ -716,70 +323,6 @@ export default class DeltaGreenActorSheet extends DGSheetMixin(ActorSheetV2) {
/* -------------------------------------------- */
- static _onItemAction(event, target) {
- const li = target.closest(".item");
- const { itemId } = li.dataset;
- const { actionType, itemType } = target.dataset;
-
- switch (actionType) {
- case "create":
- this._onItemCreate(itemType);
- break;
- case "edit": {
- const item = this.actor.items.get(itemId);
- item.sheet.render(true);
- break;
- }
- case "delete": {
- this.actor.deleteEmbeddedDocuments("Item", [itemId]);
- break;
- }
- default:
- break;
- }
- }
-
- static _onTypedSkillAction(event, target) {
- const { actionType, typedskill } = target.dataset;
- switch (actionType) {
- case "create":
- this._showNewTypeSkillDialog();
- break;
- case "edit":
- this._showNewEditTypeSkillDialog(typedskill);
- break;
- case "delete":
- this.actor.update({ [`system.typedSkills.-=${typedskill}`]: null });
- break;
- default:
- break;
- }
- }
-
- static _onSpecialTrainingAction(event, target) {
- const { actionType, id } = target.dataset;
- switch (actionType) {
- case "delete":
- {
- const specialTrainingArray = foundry.utils.duplicate(
- this.actor.system.specialTraining,
- );
-
- // Get the index of the training to be deleted
- const index = specialTrainingArray.findIndex(
- (training) => training.id === id,
- );
-
- specialTrainingArray.splice(index, 1);
- this.actor.update({ "system.specialTraining": specialTrainingArray });
- }
- break;
- default:
- this._showSpecialTrainingDialog(actionType, id);
- break;
- }
- }
-
static _applySkillImprovements(event, target) {
const failedSkills = Object.entries(this.actor.system.skills).filter(
(skill) => skill[1].failure,
@@ -849,41 +392,6 @@ export default class DeltaGreenActorSheet extends DGSheetMixin(ActorSheetV2) {
}).render(true);
}
- static _browsePack(event, target) {
- const { packType } = target.dataset;
- switch (packType) {
- case "weapon": {
- new Dialog({
- title: "Select Compendium",
- buttons: {
- firearms: {
- icon: '',
- callback: () =>
- game.packs
- .find((k) => k.collection === "deltagreen.firearms")
- .render(true),
- },
- melee: {
- icon: '',
- callback: () =>
- game.packs
- .find(
- (k) => k.collection === "deltagreen.hand-to-hand-weapons",
- )
- .render(true),
- },
- },
- }).render(true);
- break;
- }
- default:
- game.packs
- .find((k) => k.collection === `deltagreen.${packType}`)
- .render(true);
- break;
- }
- }
-
static _toggleBondDamage(event, target) {
const li = target.closest(".item");
const item = this.actor.items.get(li.dataset.itemId);
@@ -900,22 +408,6 @@ export default class DeltaGreenActorSheet extends DGSheetMixin(ActorSheetV2) {
}
}
- static _toggleItemSortMode(event, target) {
- const itemType = target.dataset.gearType;
- const propString = `${itemType}SortAlphabetical`;
- const targetProp = `system.settings.sorting.${propString}`;
- const currentValue = foundry.utils.getProperty(this.actor, targetProp);
- this.actor.update({
- [targetProp]: !currentValue,
- });
- }
-
- static toggleShowUntrained() {
- const targetProp = "system.showUntrainedSkills";
- const currentVal = foundry.utils.getProperty(this.actor, targetProp);
- this.actor.update({ [targetProp]: !currentVal });
- }
-
/** Resets the actor's current breaking point based on their sanity and POW statistics. */
static _resetBreakingPoint() {
const systemData = this.actor.system;
@@ -928,517 +420,6 @@ export default class DeltaGreenActorSheet extends DGSheetMixin(ActorSheetV2) {
this.actor.update({ system: updatedData });
}
- /** Toggles lethality */
- static _toggleLethality(event, target) {
- const { itemId } = target.dataset;
- const isLethal = target.dataset.isLethal?.length === 0;
- const item = this.actor.items.get(itemId);
- item.update({ "system.isLethal": !isLethal });
- }
-
- _showNewEditTypeSkillDialog(targetSkill) {
- // TO DO: BUILD DIALOG TO CAPTURE UPDATED DATA
-
- const { typedSkills } = this.actor.system;
- const currentLabel = typedSkills[targetSkill].label;
- const currentGroup = typedSkills[targetSkill].group;
-
- let htmlContent = `
`;
- htmlContent += ` `;
- htmlContent += ` `;
-
- if (currentGroup === game.i18n.translations.DG?.TypeSkills?.Art ?? "Art") {
- htmlContent += ` `;
- } else {
- htmlContent += ` `;
- }
-
- if (
- currentGroup === game.i18n.translations.DG?.TypeSkills?.Craft ??
- "Craft"
- ) {
- htmlContent += ` `;
- } else {
- htmlContent += ` `;
- }
-
- if (
- currentGroup === game.i18n.translations.DG?.TypeSkills?.ForeignLanguage ??
- "Foreign Language"
- ) {
- htmlContent += ` `;
- } else {
- htmlContent += ` `;
- }
-
- if (
- currentGroup === game.i18n.translations.DG?.TypeSkills?.MilitaryScience ??
- "Military Science"
- ) {
- htmlContent += ` `;
- } else {
- htmlContent += ` `;
- }
-
- if (
- currentGroup === game.i18n.translations.DG?.TypeSkills?.Pilot ??
- "Pilot"
- ) {
- htmlContent += ` `;
- } else {
- htmlContent += ` `;
- }
-
- if (
- currentGroup === game.i18n.translations.DG?.TypeSkills?.Science ??
- "Science"
- ) {
- htmlContent += ` `;
- } else {
- htmlContent += ` `;
- }
-
- if (
- currentGroup === game.i18n.translations.DG?.TypeSkills?.Other ??
- "Other"
- ) {
- htmlContent += ` `;
- } else {
- htmlContent += ` `;
- }
-
- htmlContent += ` `;
- htmlContent += `
`;
-
- htmlContent += ``;
- htmlContent += ` `;
- htmlContent += ` `;
- htmlContent += `
`;
-
- new Dialog({
- content: htmlContent,
- title:
- game.i18n.translations.DG?.Skills?.EditTypedOrCustomSkill ??
- "Edit Typed or Custom Skill",
- default: "add",
- buttons: {
- add: {
- label: game.i18n.translations.DG?.Skills?.EditSkill ?? "Edit Skill",
- callback: (btn) => {
- const newTypeSkillLabel = btn
- .find("[name='new-type-skill-label']")
- .val();
- const newTypeSkillGroup = btn
- .find("[name='new-type-skill-group']")
- .val();
- this._updateTypedSkill(
- targetSkill,
- newTypeSkillLabel,
- newTypeSkillGroup,
- );
- },
- },
- },
- }).render(true);
- }
-
- _showNewTypeSkillDialog() {
- let htmlContent = "";
-
- htmlContent += ``;
- htmlContent += ` `;
- htmlContent += ` `;
- htmlContent += ` `;
- htmlContent += ` `;
- htmlContent += ` `;
- htmlContent += ` `;
- htmlContent += ` `;
- htmlContent += ` `;
- htmlContent += ` `;
- htmlContent += ` `;
- htmlContent += `
`;
-
- htmlContent += ``;
- htmlContent += ` `;
- htmlContent += ` `;
- htmlContent += `
`;
-
- new Dialog({
- content: htmlContent,
- title:
- game.i18n.translations.DG?.Skills?.AddTypedOrCustomSkill ??
- "Add Typed or Custom Skill",
- default: "add",
- buttons: {
- add: {
- label: game.i18n.translations.DG?.Skills?.AddSkill ?? "Add Skill",
- callback: (btn) => {
- const newTypeSkillLabel = btn
- .find("[name='new-type-skill-label']")
- .val();
- const newTypeSkillGroup = btn
- .find("[name='new-type-skill-group']")
- .val();
- this._addNewTypedSkill(newTypeSkillLabel, newTypeSkillGroup);
- },
- },
- },
- }).render(true);
- }
-
- _addNewTypedSkill(newSkillLabel, newSkillGroup) {
- const updatedData = foundry.utils.duplicate(this.actor.system);
- const { typedSkills } = updatedData;
-
- const d = new Date();
-
- const newSkillPropertyName =
- d.getFullYear().toString() +
- (d.getMonth() + 1).toString() +
- d.getDate().toString() +
- d.getHours().toString() +
- d.getMinutes().toString() +
- d.getSeconds().toString();
- // console.log(newSkillPropertyName);
- typedSkills[newSkillPropertyName] = {
- label: newSkillLabel,
- group: newSkillGroup,
- proficiency: 10,
- failure: false,
- };
-
- updatedData.typedSkills = typedSkills;
-
- this.actor.update({ system: updatedData });
- }
-
- _updateTypedSkill(targetSkill, newSkillLabel, newSkillGroup) {
- if (
- newSkillLabel !== null &&
- newSkillLabel !== "" &&
- newSkillGroup !== null &&
- newSkillGroup !== ""
- ) {
- const updatedData = foundry.utils.duplicate(this.actor.system);
-
- updatedData.typedSkills[targetSkill].label = newSkillLabel;
- updatedData.typedSkills[targetSkill].group = newSkillGroup;
-
- this.actor.update({ system: updatedData });
- }
- }
-
- async _showSpecialTrainingDialog(action, targetID) {
- const specialTraining = this.actor.system.specialTraining.find(
- (training) => training.id === targetID,
- );
-
- // Define the option groups for our drop-down menu.
- const optionGroups = {
- stats: game.i18n.localize(
- "DG.SpecialTraining.Dialog.DropDown.Statistics",
- ),
- skills: game.i18n.localize("DG.SpecialTraining.Dialog.DropDown.Skills"),
- typedSkills: game.i18n.localize(
- "DG.SpecialTraining.Dialog.DropDown.CustomSkills",
- ),
- };
-
- // Prepare simplified stat list
- const statList = Object.entries(this.actor.system.statistics).map(
- ([key, stat]) => ({
- value: key,
- group: optionGroups.stats,
- label: game.i18n.localize(`DG.Attributes.${key}`),
- targetNumber: stat.value * 5,
- }),
- );
-
- // Prepare simplified skill list
- const skillList = Object.entries(this.actor.system.skills).map(
- ([key, skill]) => ({
- value: key,
- group: optionGroups.skills,
- label: game.i18n.localize(`DG.Skills.${key}`),
- targetNumber: skill.proficiency,
- }),
- );
-
- // Prepare simplified typed/custom skill list
- const typedSkillList = Object.entries(this.actor.system.typedSkills).map(
- ([key, skill]) => ({
- value: key,
- group: optionGroups.typedSkills,
- label: `${game.i18n.localize(`DG.TypeSkills.${skill.group}`)} (${
- skill.label
- })`,
- targetNumber: skill.proficiency,
- }),
- );
-
- // Prepare the Select element
- const selectElement = foundry.applications.fields.createSelectInput({
- name: "special-training-skill",
- options: [...statList, ...skillList, ...typedSkillList],
- groups: Object.values(optionGroups),
- }).outerHTML;
-
- // Prepare the template to feed to Dialog.
- const { renderTemplate } = foundry.applications.handlebars;
- const content = await renderTemplate(
- "systems/deltagreen/templates/dialog/special-training.html",
- {
- name: specialTraining?.name || "",
- selectElement,
- currentAttribute: specialTraining?.attribute || "",
- statList,
- skillList,
- typedSkillList,
- },
- );
-
- const buttonLabel = game.i18n.localize(
- `DG.SpecialTraining.Dialog.${action.capitalize()}SpecialTraining`,
- );
-
- // Prepare and render dialog with above template.
- new Dialog({
- content,
- title: game.i18n.localize("DG.SpecialTraining.Dialog.Title"),
- default: "confirm",
- buttons: {
- confirm: {
- label: buttonLabel,
- callback: (btn) => {
- const specialTrainingLabel = btn
- .find("[name='special-training-label']")
- .val();
- const specialTrainingAttribute = btn
- .find("[name='special-training-skill']")
- .val();
- if (action === "create")
- this._createSpecialTraining(
- specialTrainingLabel,
- specialTrainingAttribute,
- );
- if (action === "edit")
- this._editSpecialTraining(
- specialTrainingLabel,
- specialTrainingAttribute,
- targetID,
- );
- },
- },
- },
- }).render(true);
- }
-
- _createSpecialTraining(label, attribute) {
- const specialTrainingArray = foundry.utils.duplicate(
- this.actor.system.specialTraining,
- );
- specialTrainingArray.push({
- name: label,
- attribute,
- id: foundry.utils.randomID(),
- });
- this.actor.update({ "system.specialTraining": specialTrainingArray });
- }
-
- _editSpecialTraining(label, attribute, id) {
- const specialTrainingArray = foundry.utils.duplicate(
- this.actor.system.specialTraining,
- );
- const specialTraining = specialTrainingArray.find(
- (training) => training.id === id,
- );
- specialTraining.name = label;
- specialTraining.attribute = attribute;
- this.actor.update({ "system.specialTraining": specialTrainingArray });
- }
-
- /**
- * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
- * @param {String} type The originating click event
- * @private
- */
- _onItemCreate(type) {
- // Initialize a default name.
- const name = game.i18n.format(
- game.i18n.translations.DOCUMENT?.New || "DG.FallbackText.newItem",
- {
- type: game.i18n.localize(`TYPES.Item.${type}`),
- },
- );
-
- // Prepare the item object.
- const itemData = {
- name,
- type,
- system: {},
- };
-
- if (type === "weapon") {
- // itemData.system.skill = "firearms"; //default skill to firearms, since that will be most common
- // itemData.system.expense = "Standard";
- } else if (type === "armor") {
- // itemData.system.armor = 3;
- // itemData.system.expense = "Standard";
- } else if (type === "bond") {
- // try to default bonds for an agent to their current CHA
- itemData.system.score = this.actor.system.statistics.cha.value; // Can vary, but at character creation starting bond score is usually agent's charisma
- // itemData.img = "icons/svg/mystery-man.svg"
- }
-
- // create the item
- return this.actor.createEmbeddedDocuments("Item", [itemData]);
- }
-
- /**
- * Handle clickable rolls.
- *
- * @param {Event} event The originating click event
- * @async
- * @private
- */
- static async _onRoll(event, target) {
- if (target.classList.contains("not-rollable") || event.which === 2) return;
-
- const { dataset } = target;
- const item = this.actor.items.get(dataset.iid);
- const rollOptions = {
- rollType: dataset.rolltype,
- key: dataset.key,
- actor: this.actor,
- specialTrainingName: dataset?.name || null, // Only applies to Special Training Rolls
- item,
- };
-
- // Create a default 1d100 roll just in case.
- let roll = new Roll("1d100", {});
- switch (dataset.rolltype) {
- case "stat":
- case "skill":
- case "sanity":
- case "special-training":
- case "weapon":
- roll = new DGPercentileRoll("1D100", {}, rollOptions);
- break;
- case "lethality":
- roll = new DGLethalityRoll("1D100", {}, rollOptions);
- break;
- case "damage": {
- let diceFormula = item.system.damage;
- const { skill } = item.system;
- if (
- this.actor.type === "agent" &&
- (skill === "unarmed_combat" || skill === "melee_weapons")
- ) {
- diceFormula +=
- this.actor.system.statistics.str.meleeDamageBonusFormula;
- }
- roll = new DGDamageRoll(diceFormula, {}, rollOptions);
- break;
- }
- case "sanity-damage": {
- const { successLoss, failedLoss } = this.actor.system.sanity;
- const combinedFormula = `{${successLoss}, ${failedLoss}}`;
- roll = new DGSanityDamageRoll(combinedFormula, {}, rollOptions);
- break;
- }
- default:
- break;
- }
- this.processRoll(event, roll, rollOptions);
- }
-
- /**
- * Show a dialog for the roll and then send to chat.
- * Broke this logic out from `_onRoll()` so that other files can call it,
- * namely the macro logic.
- *
- * TODO: Move this logic to the roll.js.
- *
- * @param {Event} event The originating click event
- * @param {Event} roll The roll to show a dialog for and then send to chat.
- * @async
- */
- async processRoll(event, roll) {
- // Open dialog if user requests it (no dialog for Sanity Damage rolls)
- if (
- (event.shiftKey || event.which === 3) &&
- !(roll instanceof DGSanityDamageRoll)
- ) {
- const dialogData = await roll.showDialog();
- if (!dialogData) return;
- if (dialogData.newFormula) {
- roll = new DGDamageRoll(dialogData.newFormula, {}, roll.options);
- }
- roll.modifier += dialogData.targetModifier;
- roll.options.rollMode = dialogData.rollMode;
- }
- // Evaluate the roll.
- await roll.evaluate();
- // Send the roll to chat.
- roll.toChat();
- }
-
_resetBreakingPoint(event) {
event.preventDefault();
@@ -1458,16 +439,6 @@ export default class DeltaGreenActorSheet extends DGSheetMixin(ActorSheetV2) {
this.actor.update({ system: updatedData });
}
- static _toggleEquipped(event, target) {
- event.preventDefault();
- const { id } = target.dataset;
- const item = this.actor.items.get(id);
-
- const targetProp = "system.equipped";
- const currentVal = foundry.utils.getProperty(item, targetProp);
- item.update({ [targetProp]: !currentVal });
- }
-
// For any skills a user has checked off as failed, roll the improvement and update the agent's skills to their new values
async _applySkillImprovements(
baseRollFormula,
diff --git a/module/sheets/base-actor-sheet.js b/module/sheets/base-actor-sheet.js
new file mode 100644
index 0000000..6074cbd
--- /dev/null
+++ b/module/sheets/base-actor-sheet.js
@@ -0,0 +1,961 @@
+import DG from "../config.js";
+import {
+ DGDamageRoll,
+ DGLethalityRoll,
+ DGPercentileRoll,
+ DGSanityDamageRoll,
+} from "../roll/roll.js";
+import DGSheetMixin from "./base-sheet.js";
+
+const { ActorSheetV2 } = foundry.applications.sheets;
+
+export default class DGActorSheet extends DGSheetMixin(ActorSheetV2) {
+ /** @override */
+ static DEFAULT_OPTIONS = /** @type {const} */ ({
+ position: { width: 750, height: 770 },
+ actions: {
+ // Skill/Item actions.
+ itemAction: this._onItemAction,
+ typedSkillAction: this._onTypedSkillAction,
+ specialTrainingAction: this._onSpecialTrainingAction,
+ roll: this._onRoll,
+ rollLuck: this._onRollLuck,
+ // Toggles/resets.
+ toggleEquipped: this._toggleEquipped,
+ toggleItemSortMode: this._toggleItemSortMode,
+ toggleShowUntrained: this._toggleShowUntrained,
+ toggleLethality: this._toggleLethality,
+ // Other actions.
+ browsePack: this._browsePack,
+ },
+ });
+
+ /** @override */
+ async _prepareContext(options) {
+ const context = await super._prepareContext(options);
+
+ // Make it easy for the sheet handlebars to understand how to sort the skills.
+ context.sortSkillsSetting = game.settings.get("deltagreen", "sortSkills");
+
+ // fill an array that is sorted based on the appropriate localized entry
+ const sortedSkills = [];
+ for (const [key, skill] of Object.entries(this.actor.system.skills)) {
+ skill.key = key;
+
+ if (game.i18n.lang === "ja") {
+ skill.sortLabel = game.i18n.localize(`DG.Skills.ruby.${key}`);
+ } else {
+ skill.sortLabel = game.i18n.localize(`DG.Skills.${key}`);
+ }
+
+ if (skill.sortLabel === "" || skill.sortLabel === `DG.Skills.${key}`) {
+ skill.sortLabel = skill.key;
+ }
+
+ // if the actor is an NPC or Unnatural, and they have 'hide untrained skills' active,
+ // it will break the sorting logic, so we have to skip over these
+ if (
+ !(
+ (this.actor.type === "npc" || this.actor.type === "unnatural") &&
+ this.actor.system.showUntrainedSkills &&
+ skill.proficiency < 1
+ )
+ ) {
+ sortedSkills.push(skill);
+ }
+ }
+
+ sortedSkills.sort((a, b) => {
+ return a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang);
+ });
+
+ // if sorting by columns, re-arrange the array to be columns first, then rows
+ if (game.settings.get("deltagreen", "sortSkills")) {
+ const columnSortedSkills = this.reorderForColumnSorting(sortedSkills, 3);
+
+ this.actor.system.sortedSkills = columnSortedSkills;
+ } else {
+ this.actor.system.sortedSkills = sortedSkills;
+ }
+
+ // Prepare a simplified version of the special training for display on sheet.
+ const specialTraining = this.actor.system.specialTraining.map(
+ (training) => {
+ const simplifiedTraining = {
+ name: training.name,
+ id: training.id,
+ key: training.attribute,
+ };
+ // Convert the machine-readable name to a human-readable one.
+ switch (true) {
+ // Stats
+ case DG.statistics.includes(training.attribute):
+ simplifiedTraining.attribute = `${training.attribute.toUpperCase()}x5`;
+ simplifiedTraining.targetNumber =
+ this.actor.system.statistics[training.attribute].x5;
+ break;
+ // Skills
+ case DG.skills.includes(training.attribute):
+ simplifiedTraining.attribute =
+ this.actor.system.skills[training.attribute].label;
+ simplifiedTraining.targetNumber =
+ this.actor.system.skills[training.attribute].proficiency;
+ break;
+ // Typed Skills
+ default:
+ simplifiedTraining.attribute =
+ this.actor.system.typedSkills[training.attribute].label;
+ simplifiedTraining.targetNumber =
+ this.actor.system.typedSkills[training.attribute].proficiency;
+ break;
+ }
+ return simplifiedTraining;
+ },
+ );
+ context.specialTraining = specialTraining;
+
+ // try to make a combined array of both typed skills and special trainings,
+ // so that it can be sorted together neatly on the sheet
+ const sortedCustomSkills = [];
+
+ for (const [key, skill] of Object.entries(this.actor.system.typedSkills)) {
+ skill.type = "typeSkill";
+ skill.key = key;
+ skill.sortLabel = `${skill.group}.${skill.label}`;
+ skill.sortLabel = skill.sortLabel.toUpperCase();
+ skill.actorType = this.actor.type;
+
+ if (skill.sortLabel === "" || skill.sortLabel === `DG.Skills.${key}`) {
+ skill.sortLabel = skill.key;
+ }
+
+ sortedCustomSkills.push(skill);
+ }
+
+ for (let i = 0; i < context.specialTraining.length; i++) {
+ const training = context.specialTraining[i];
+
+ training.type = "training";
+ training.sortLabel = training.name.toUpperCase();
+ training.actorType = this.actor.type;
+
+ sortedCustomSkills.push(training);
+ }
+
+ sortedCustomSkills.sort(function (a, b) {
+ return a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang);
+ });
+
+ if (game.settings.get("deltagreen", "sortSkills")) {
+ const columnSortedSkills = this.reorderForColumnSorting(
+ sortedCustomSkills,
+ 2,
+ );
+
+ this.actor.system.sortedCustomSkills = columnSortedSkills;
+ } else {
+ this.actor.system.sortedCustomSkills = sortedCustomSkills;
+ }
+
+ return context;
+ }
+
+ /** @override */
+ async _onRender(context, options) {
+ await super._onRender(context, options);
+ const { element } = this;
+
+ this._setRightClickListeners();
+
+ if (this.actor.isOwner) {
+ const handler = (ev) => this._onDragStart(ev);
+ element.querySelectorAll("li.item").forEach((li) => {
+ if (li.classList.contains("inventory-header")) return;
+ li.setAttribute("draggable", true);
+ li.addEventListener("dragstart", handler, false);
+ });
+ }
+ }
+
+ /** @override - Add buttons to the header controls. */
+ _getHeaderControls() {
+ const controls = super._getHeaderControls();
+ controls.push({
+ action: "rollLuck",
+ label: "DG.RollLuck",
+ icon: "fas fa-dice",
+ });
+ return controls;
+ }
+
+ /** @override */
+ _onDragStart(event) {
+ // Most of this is the standard Foundry implementation of _onDragStart
+ const li = event.currentTarget;
+ if (event.target.classList.contains("content-link")) return;
+
+ // Create drag data
+ let dragData;
+
+ // Owned Items
+ if (li.dataset.itemId) {
+ const item = this.actor.items.get(li.dataset.itemId);
+ dragData = item.toDragData();
+ }
+
+ // Active Effect
+ if (li.dataset.effectId) {
+ const effect = this.actor.effects.get(li.dataset.effectId);
+ dragData = effect.toDragData();
+ }
+
+ if (!dragData) return;
+
+ // this is custom, grab item data for creating item macros on the hotbar
+ if (li.dataset.itemId) {
+ const item = this.actor.items.get(li.dataset.itemId);
+ dragData.itemData = item;
+ }
+
+ // Set data transfer
+ event.dataTransfer.setData("text/plain", JSON.stringify(dragData));
+ }
+
+ /** @override */
+ async _onDrop(event) {
+ super._onDrop(event);
+ // If alt key is held down, we will delete the original document.
+ if (event.altKey) {
+ // This is from Foundry. It will get the item data from the event.
+ const TextEditor = foundry.applications.ux.TextEditor.implementation;
+ const dragData = TextEditor.getDragEventData(event);
+ // Make sure that we are dragging an item, otherwise this doesn't make sense.
+ if (dragData.type === "Item") {
+ const item = fromUuidSync(dragData.uuid);
+ await item.delete();
+ }
+ }
+ }
+
+ /**
+ * Listens for right click events on the actor sheet and executes a regular roll,
+ * or luck roll, depending on the action.
+ *
+ * @returns {void}
+ */
+ _setRightClickListeners() {
+ const { element } = this;
+ element.addEventListener("contextmenu", (event) => {
+ event.preventDefault();
+ const target = event.target.closest(
+ "[data-action='roll'],[data-action='rollLuck']",
+ );
+ if (!target) return;
+
+ // Call _onRollLuck if luckRoll is the dispatched action.
+ if (target.dataset.action === "rollLuck") {
+ DGActorSheet._onRollLuck.call(this, event, target);
+ return;
+ }
+
+ // Otherwise, call _onRoll function.
+ DGActorSheet._onRoll.call(this, event, target);
+ });
+ }
+
+ reorderForColumnSorting(arr, numCols) {
+ const numRows = Math.ceil(arr.length / numCols); // Compute required rows
+ const reordered = new Array(arr.length);
+
+ // Determine how many elements each column gets
+ const baseRowCount = Math.floor(arr.length / numCols); // Minimum rows per column
+ const extraColumns = arr.length % numCols; // Some columns get an extra row
+
+ const colHeights = new Array(numCols).fill(baseRowCount);
+ for (let i = 0; i < extraColumns; i++) {
+ colHeights[i] += 1; // Give extra elements to the first N columns
+ }
+
+ let index = 0; // move through alphabetical array, keeping track of what we've resorted already
+
+ for (let col = 0; col < numCols; col++) {
+ // need to check if this is a column that has more rows than the others or not
+ const rowCount = colHeights[col];
+
+ // loop down the column, filling out it's values from the alphabetical array
+ for (let row = 0; row < rowCount; row++) {
+ // calculate the new position for this value by column
+ const newIndex = numCols * row + col;
+
+ if (newIndex < arr.length) {
+ reordered[newIndex] = arr[index];
+ index += 1;
+ }
+ }
+ }
+
+ return reordered;
+ }
+
+ static _onItemAction(event, target) {
+ const li = target.closest(".item");
+ const { itemId } = li.dataset;
+ const { actionType, itemType } = target.dataset;
+
+ switch (actionType) {
+ case "create":
+ this._onItemCreate(itemType);
+ break;
+ case "edit": {
+ const item = this.actor.items.get(itemId);
+ item.sheet.render(true);
+ break;
+ }
+ case "delete": {
+ this.actor.deleteEmbeddedDocuments("Item", [itemId]);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
+ * @param {String} type The originating click event
+ * @private
+ */
+ _onItemCreate(type) {
+ // Initialize a default name.
+ const name = game.i18n.format(
+ game.i18n.translations.DOCUMENT?.New || "DG.FallbackText.newItem",
+ {
+ type: game.i18n.localize(`TYPES.Item.${type}`),
+ },
+ );
+
+ // Prepare the item object.
+ const itemData = {
+ name,
+ type,
+ system: {},
+ };
+
+ if (type === "weapon") {
+ // itemData.system.skill = "firearms"; //default skill to firearms, since that will be most common
+ // itemData.system.expense = "Standard";
+ } else if (type === "armor") {
+ // itemData.system.armor = 3;
+ // itemData.system.expense = "Standard";
+ } else if (type === "bond") {
+ // try to default bonds for an agent to their current CHA
+ itemData.system.score = this.actor.system.statistics.cha.value; // Can vary, but at character creation starting bond score is usually agent's charisma
+ // itemData.img = "icons/svg/mystery-man.svg"
+ }
+
+ // create the item
+ return this.actor.createEmbeddedDocuments("Item", [itemData]);
+ }
+
+ static _onTypedSkillAction(event, target) {
+ const { actionType, typedskill } = target.dataset;
+ switch (actionType) {
+ case "create":
+ this._showNewTypeSkillDialog();
+ break;
+ case "edit":
+ this._showNewEditTypeSkillDialog(typedskill);
+ break;
+ case "delete":
+ this.actor.update({ [`system.typedSkills.-=${typedskill}`]: null });
+ break;
+ default:
+ break;
+ }
+ }
+
+ _showNewEditTypeSkillDialog(targetSkill) {
+ // TO DO: BUILD DIALOG TO CAPTURE UPDATED DATA
+
+ const { typedSkills } = this.actor.system;
+ const currentLabel = typedSkills[targetSkill].label;
+ const currentGroup = typedSkills[targetSkill].group;
+
+ let htmlContent = ``;
+ htmlContent += ` `;
+ htmlContent += ` `;
+
+ if (currentGroup === game.i18n.translations.DG?.TypeSkills?.Art ?? "Art") {
+ htmlContent += ` `;
+ } else {
+ htmlContent += ` `;
+ }
+
+ if (
+ currentGroup === game.i18n.translations.DG?.TypeSkills?.Craft ??
+ "Craft"
+ ) {
+ htmlContent += ` `;
+ } else {
+ htmlContent += ` `;
+ }
+
+ if (
+ currentGroup === game.i18n.translations.DG?.TypeSkills?.ForeignLanguage ??
+ "Foreign Language"
+ ) {
+ htmlContent += ` `;
+ } else {
+ htmlContent += ` `;
+ }
+
+ if (
+ currentGroup === game.i18n.translations.DG?.TypeSkills?.MilitaryScience ??
+ "Military Science"
+ ) {
+ htmlContent += ` `;
+ } else {
+ htmlContent += ` `;
+ }
+
+ if (
+ currentGroup === game.i18n.translations.DG?.TypeSkills?.Pilot ??
+ "Pilot"
+ ) {
+ htmlContent += ` `;
+ } else {
+ htmlContent += ` `;
+ }
+
+ if (
+ currentGroup === game.i18n.translations.DG?.TypeSkills?.Science ??
+ "Science"
+ ) {
+ htmlContent += ` `;
+ } else {
+ htmlContent += ` `;
+ }
+
+ if (
+ currentGroup === game.i18n.translations.DG?.TypeSkills?.Other ??
+ "Other"
+ ) {
+ htmlContent += ` `;
+ } else {
+ htmlContent += ` `;
+ }
+
+ htmlContent += ` `;
+ htmlContent += `
`;
+
+ htmlContent += ``;
+ htmlContent += ` `;
+ htmlContent += ` `;
+ htmlContent += `
`;
+
+ new Dialog({
+ content: htmlContent,
+ title:
+ game.i18n.translations.DG?.Skills?.EditTypedOrCustomSkill ??
+ "Edit Typed or Custom Skill",
+ default: "add",
+ buttons: {
+ add: {
+ label: game.i18n.translations.DG?.Skills?.EditSkill ?? "Edit Skill",
+ callback: (btn) => {
+ const newTypeSkillLabel = btn
+ .find("[name='new-type-skill-label']")
+ .val();
+ const newTypeSkillGroup = btn
+ .find("[name='new-type-skill-group']")
+ .val();
+ this._updateTypedSkill(
+ targetSkill,
+ newTypeSkillLabel,
+ newTypeSkillGroup,
+ );
+ },
+ },
+ },
+ }).render(true);
+ }
+
+ _showNewTypeSkillDialog() {
+ let htmlContent = "";
+
+ htmlContent += ``;
+ htmlContent += ` `;
+ htmlContent += ` `;
+ htmlContent += ` `;
+ htmlContent += ` `;
+ htmlContent += ` `;
+ htmlContent += ` `;
+ htmlContent += ` `;
+ htmlContent += ` `;
+ htmlContent += ` `;
+ htmlContent += ` `;
+ htmlContent += `
`;
+
+ htmlContent += ``;
+ htmlContent += ` `;
+ htmlContent += ` `;
+ htmlContent += `
`;
+
+ new Dialog({
+ content: htmlContent,
+ title:
+ game.i18n.translations.DG?.Skills?.AddTypedOrCustomSkill ??
+ "Add Typed or Custom Skill",
+ default: "add",
+ buttons: {
+ add: {
+ label: game.i18n.translations.DG?.Skills?.AddSkill ?? "Add Skill",
+ callback: (btn) => {
+ const newTypeSkillLabel = btn
+ .find("[name='new-type-skill-label']")
+ .val();
+ const newTypeSkillGroup = btn
+ .find("[name='new-type-skill-group']")
+ .val();
+ this._addNewTypedSkill(newTypeSkillLabel, newTypeSkillGroup);
+ },
+ },
+ },
+ }).render(true);
+ }
+
+ _addNewTypedSkill(newSkillLabel, newSkillGroup) {
+ const updatedData = foundry.utils.duplicate(this.actor.system);
+ const { typedSkills } = updatedData;
+
+ const d = new Date();
+
+ const newSkillPropertyName =
+ d.getFullYear().toString() +
+ (d.getMonth() + 1).toString() +
+ d.getDate().toString() +
+ d.getHours().toString() +
+ d.getMinutes().toString() +
+ d.getSeconds().toString();
+ // console.log(newSkillPropertyName);
+ typedSkills[newSkillPropertyName] = {
+ label: newSkillLabel,
+ group: newSkillGroup,
+ proficiency: 10,
+ failure: false,
+ };
+
+ updatedData.typedSkills = typedSkills;
+
+ this.actor.update({ system: updatedData });
+ }
+
+ _updateTypedSkill(targetSkill, newSkillLabel, newSkillGroup) {
+ if (
+ newSkillLabel !== null &&
+ newSkillLabel !== "" &&
+ newSkillGroup !== null &&
+ newSkillGroup !== ""
+ ) {
+ const updatedData = foundry.utils.duplicate(this.actor.system);
+
+ updatedData.typedSkills[targetSkill].label = newSkillLabel;
+ updatedData.typedSkills[targetSkill].group = newSkillGroup;
+
+ this.actor.update({ system: updatedData });
+ }
+ }
+
+ static _onSpecialTrainingAction(event, target) {
+ const { actionType, id } = target.dataset;
+ switch (actionType) {
+ case "delete":
+ {
+ const specialTrainingArray = foundry.utils.duplicate(
+ this.actor.system.specialTraining,
+ );
+
+ // Get the index of the training to be deleted
+ const index = specialTrainingArray.findIndex(
+ (training) => training.id === id,
+ );
+
+ specialTrainingArray.splice(index, 1);
+ this.actor.update({ "system.specialTraining": specialTrainingArray });
+ }
+ break;
+ default:
+ this._showSpecialTrainingDialog(actionType, id);
+ break;
+ }
+ }
+
+ async _showSpecialTrainingDialog(action, targetID) {
+ const specialTraining = this.actor.system.specialTraining.find(
+ (training) => training.id === targetID,
+ );
+
+ // Define the option groups for our drop-down menu.
+ const optionGroups = {
+ stats: game.i18n.localize(
+ "DG.SpecialTraining.Dialog.DropDown.Statistics",
+ ),
+ skills: game.i18n.localize("DG.SpecialTraining.Dialog.DropDown.Skills"),
+ typedSkills: game.i18n.localize(
+ "DG.SpecialTraining.Dialog.DropDown.CustomSkills",
+ ),
+ };
+
+ // Prepare simplified stat list
+ const statList = Object.entries(this.actor.system.statistics).map(
+ ([key, stat]) => ({
+ value: key,
+ group: optionGroups.stats,
+ label: game.i18n.localize(`DG.Attributes.${key}`),
+ targetNumber: stat.value * 5,
+ }),
+ );
+
+ // Prepare simplified skill list
+ const skillList = Object.entries(this.actor.system.skills).map(
+ ([key, skill]) => ({
+ value: key,
+ group: optionGroups.skills,
+ label: game.i18n.localize(`DG.Skills.${key}`),
+ targetNumber: skill.proficiency,
+ }),
+ );
+
+ // Prepare simplified typed/custom skill list
+ const typedSkillList = Object.entries(this.actor.system.typedSkills).map(
+ ([key, skill]) => ({
+ value: key,
+ group: optionGroups.typedSkills,
+ label: `${game.i18n.localize(`DG.TypeSkills.${skill.group}`)} (${
+ skill.label
+ })`,
+ targetNumber: skill.proficiency,
+ }),
+ );
+
+ // Prepare the Select element
+ const selectElement = foundry.applications.fields.createSelectInput({
+ name: "special-training-skill",
+ options: [...statList, ...skillList, ...typedSkillList],
+ groups: Object.values(optionGroups),
+ }).outerHTML;
+
+ // Prepare the template to feed to Dialog.
+ const { renderTemplate } = foundry.applications.handlebars;
+ const content = await renderTemplate(
+ "systems/deltagreen/templates/dialog/special-training.html",
+ {
+ name: specialTraining?.name || "",
+ selectElement,
+ currentAttribute: specialTraining?.attribute || "",
+ statList,
+ skillList,
+ typedSkillList,
+ },
+ );
+
+ const buttonLabel = game.i18n.localize(
+ `DG.SpecialTraining.Dialog.${action.capitalize()}SpecialTraining`,
+ );
+
+ // Prepare and render dialog with above template.
+ new Dialog({
+ content,
+ title: game.i18n.localize("DG.SpecialTraining.Dialog.Title"),
+ default: "confirm",
+ buttons: {
+ confirm: {
+ label: buttonLabel,
+ callback: (btn) => {
+ const specialTrainingLabel = btn
+ .find("[name='special-training-label']")
+ .val();
+ const specialTrainingAttribute = btn
+ .find("[name='special-training-skill']")
+ .val();
+ if (action === "create")
+ this._createSpecialTraining(
+ specialTrainingLabel,
+ specialTrainingAttribute,
+ );
+ if (action === "edit")
+ this._editSpecialTraining(
+ specialTrainingLabel,
+ specialTrainingAttribute,
+ targetID,
+ );
+ },
+ },
+ },
+ }).render(true);
+ }
+
+ _createSpecialTraining(label, attribute) {
+ const specialTrainingArray = foundry.utils.duplicate(
+ this.actor.system.specialTraining,
+ );
+ specialTrainingArray.push({
+ name: label,
+ attribute,
+ id: foundry.utils.randomID(),
+ });
+ this.actor.update({ "system.specialTraining": specialTrainingArray });
+ }
+
+ _editSpecialTraining(label, attribute, id) {
+ const specialTrainingArray = foundry.utils.duplicate(
+ this.actor.system.specialTraining,
+ );
+ const specialTraining = specialTrainingArray.find(
+ (training) => training.id === id,
+ );
+ specialTraining.name = label;
+ specialTraining.attribute = attribute;
+ this.actor.update({ "system.specialTraining": specialTrainingArray });
+ }
+
+ /**
+ * Handle clickable rolls.
+ *
+ * @param {Event} event The originating click event
+ * @async
+ * @private
+ */
+ static async _onRoll(event, target) {
+ if (target.classList.contains("not-rollable") || event.which === 2) return;
+
+ const { dataset } = target;
+ const item = this.actor.items.get(dataset.iid);
+ const rollOptions = {
+ rollType: dataset.rolltype,
+ key: dataset.key,
+ actor: this.actor,
+ specialTrainingName: dataset?.name || null, // Only applies to Special Training Rolls
+ item,
+ };
+
+ // Create a default 1d100 roll just in case.
+ let roll = new Roll("1d100", {});
+ switch (dataset.rolltype) {
+ case "stat":
+ case "skill":
+ case "sanity":
+ case "special-training":
+ case "weapon":
+ roll = new DGPercentileRoll("1D100", {}, rollOptions);
+ break;
+ case "lethality":
+ roll = new DGLethalityRoll("1D100", {}, rollOptions);
+ break;
+ case "damage": {
+ let diceFormula = item.system.damage;
+ const { skill } = item.system;
+ if (
+ this.actor.type === "agent" &&
+ (skill === "unarmed_combat" || skill === "melee_weapons")
+ ) {
+ diceFormula +=
+ this.actor.system.statistics.str.meleeDamageBonusFormula;
+ }
+ roll = new DGDamageRoll(diceFormula, {}, rollOptions);
+ break;
+ }
+ case "sanity-damage": {
+ const { successLoss, failedLoss } = this.actor.system.sanity;
+ const combinedFormula = `{${successLoss}, ${failedLoss}}`;
+ roll = new DGSanityDamageRoll(combinedFormula, {}, rollOptions);
+ break;
+ }
+ default:
+ break;
+ }
+ this.processRoll(event, roll, rollOptions);
+ }
+
+ // This only exists to give a chance to activate the modifier dialogue if desired
+ // Cannot seem to trigger the event on a right-click, so unfortunately only applies to a shift-click currently.
+ static async _onRollLuck(event) {
+ // probably don't want rolls to trigger from a middle mouse click so just kill it here
+ if (event.which === 2) return;
+
+ const rollOptions = {
+ rollType: "luck",
+ key: "luck",
+ actor: this.actor,
+ };
+
+ // Create a default 1d100 roll just in case.
+ const roll = new DGPercentileRoll("1D100", {}, rollOptions);
+ // Open dialog if user requests it.
+ if (event.shiftKey || event.which === 3) {
+ const dialogData = await roll.showDialog();
+ if (!dialogData) return;
+ roll.modifier += dialogData.targetModifier;
+ roll.options.rollMode = dialogData.rollMode;
+ }
+ // Evaluate the roll.
+ await roll.evaluate();
+ // Send the roll to chat.
+ roll.toChat();
+ }
+
+ /**
+ * Show a dialog for the roll and then send to chat.
+ * Broke this logic out from `_onRoll()` so that other files can call it,
+ * namely the macro logic.
+ *
+ * TODO: Move this logic to the roll.js.
+ *
+ * @param {Event} event The originating click event
+ * @param {Event} roll The roll to show a dialog for and then send to chat.
+ * @async
+ */
+ async processRoll(event, roll) {
+ // Open dialog if user requests it (no dialog for Sanity Damage rolls)
+ if (
+ (event.shiftKey || event.which === 3) &&
+ !(roll instanceof DGSanityDamageRoll)
+ ) {
+ const dialogData = await roll.showDialog();
+ if (!dialogData) return;
+ if (dialogData.newFormula) {
+ roll = new DGDamageRoll(dialogData.newFormula, {}, roll.options);
+ }
+ roll.modifier += dialogData.targetModifier;
+ roll.options.rollMode = dialogData.rollMode;
+ }
+ // Evaluate the roll.
+ await roll.evaluate();
+ // Send the roll to chat.
+ roll.toChat();
+ }
+
+ static _toggleItemSortMode(event, target) {
+ const itemType = target.dataset.gearType;
+ const propString = `${itemType}SortAlphabetical`;
+ const targetProp = `system.settings.sorting.${propString}`;
+ const currentValue = foundry.utils.getProperty(this.actor, targetProp);
+ this.actor.update({
+ [targetProp]: !currentValue,
+ });
+ }
+
+ static _toggleShowUntrained() {
+ const targetProp = "system.showUntrainedSkills";
+ const currentVal = foundry.utils.getProperty(this.actor, targetProp);
+ this.actor.update({ [targetProp]: !currentVal });
+ }
+
+ static _toggleLethality(event, target) {
+ const { itemId } = target.dataset;
+ const isLethal = target.dataset.isLethal?.length === 0;
+ const item = this.actor.items.get(itemId);
+ item.update({ "system.isLethal": !isLethal });
+ }
+
+ static _toggleEquipped(event, target) {
+ event.preventDefault();
+ const { id } = target.dataset;
+ const item = this.actor.items.get(id);
+
+ const targetProp = "system.equipped";
+ const currentVal = foundry.utils.getProperty(item, targetProp);
+ item.update({ [targetProp]: !currentVal });
+ }
+
+ static _browsePack(event, target) {
+ const { packType } = target.dataset;
+ switch (packType) {
+ case "weapon": {
+ new Dialog({
+ title: "Select Compendium",
+ buttons: {
+ firearms: {
+ icon: '',
+ callback: () =>
+ game.packs
+ .find((k) => k.collection === "deltagreen.firearms")
+ .render(true),
+ },
+ melee: {
+ icon: '',
+ callback: () =>
+ game.packs
+ .find(
+ (k) => k.collection === "deltagreen.hand-to-hand-weapons",
+ )
+ .render(true),
+ },
+ },
+ }).render(true);
+ break;
+ }
+ default:
+ game.packs
+ .find((k) => k.collection === `deltagreen.${packType}`)
+ .render(true);
+ break;
+ }
+ }
+}
diff --git a/module/base-sheet.js b/module/sheets/base-sheet.js
similarity index 98%
rename from module/base-sheet.js
rename to module/sheets/base-sheet.js
index 62221fe..de42d33 100644
--- a/module/base-sheet.js
+++ b/module/sheets/base-sheet.js
@@ -1,4 +1,4 @@
-import DG from "./config.js";
+import DG from "../config.js";
const HbsAppMixin = foundry.applications.api.HandlebarsApplicationMixin;