400 lines
12 KiB
JavaScript
400 lines
12 KiB
JavaScript
import DG, { BASE_TEMPLATE_PATH } from "../config.js";
|
|
import { packSkillGroupsIntoColumns } from "../utils/skill-layout.js";
|
|
import {
|
|
createSkillImprovementChatMessage,
|
|
evaluateSkillImprovementRolls,
|
|
getSkillImprovementFormulaAsPercent,
|
|
} from "../roll/skill-improvement-roll.js";
|
|
import DGActorSheet from "./base-actor-sheet.js";
|
|
import ActorEditStatForm from "../applications/edit-stats.js";
|
|
|
|
const { renderTemplate } = foundry.applications.handlebars;
|
|
|
|
/** @extends {DGActorSheet} */
|
|
export default class DGAgentSheet extends DGActorSheet {
|
|
/** @override */
|
|
static DEFAULT_OPTIONS = /** @type {const} */ ({
|
|
classes: ["agent-sheet"],
|
|
position: { width: 958, height: 700 },
|
|
actions: {
|
|
// Resets.
|
|
clearBondDamage: DGAgentSheet._clearBondDamage,
|
|
resetBreakingPoint: DGAgentSheet._resetBreakingPoint,
|
|
// Other actions.
|
|
applySkillImprovements: DGAgentSheet._processSkillImprovements,
|
|
openStatsEdit: DGAgentSheet._openStatsEdit,
|
|
toggleCustomSkillsEdit: function toggleCustomSkillsEdit(event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
this._customSkillsEditMode = !this._customSkillsEditMode;
|
|
this._syncCustomSkillsEditModeUi();
|
|
return this.render();
|
|
},
|
|
},
|
|
});
|
|
|
|
/** @type {boolean} */
|
|
_customSkillsEditMode = false;
|
|
|
|
/** @override */
|
|
static TABS = /** @type {const} */ ({
|
|
primary: {
|
|
initial: "skills",
|
|
labelPrefix: "DG.Navigation.Agent",
|
|
tabs: [
|
|
{ id: "skills" },
|
|
{ id: "combat" },
|
|
{ id: "motivations" },
|
|
{ id: "gear" },
|
|
{ id: "personal" },
|
|
{ id: "about", icon: "fas fa-question-circle", label: "" },
|
|
],
|
|
},
|
|
});
|
|
|
|
/** @override */
|
|
static PARTS = /** @type {const} */ ({
|
|
leftBar: {
|
|
template: `${this.TEMPLATE_PATH}/parts/left-bar.html`,
|
|
templates: [`${this.TEMPLATE_PATH}/partials/sanity-partial.html`],
|
|
},
|
|
tabs: this.BASE_PARTS.tabs,
|
|
rightBar: {
|
|
template: `${this.TEMPLATE_PATH}/parts/right-bar.html`,
|
|
templates: [
|
|
`${this.TEMPLATE_PATH}/parts/skills-tab-agent.html`,
|
|
`${this.TEMPLATE_PATH}/parts/combat-tab-agent.html`,
|
|
`${this.TEMPLATE_PATH}/parts/motivations-tab-agent.html`,
|
|
`${this.TEMPLATE_PATH}/parts/gear-tab-agent.html`,
|
|
`${this.TEMPLATE_PATH}/parts/personal-tab-agent.html`,
|
|
`${this.TEMPLATE_PATH}/parts/about-tab.html`,
|
|
`${this.TEMPLATE_PATH}/partials/custom-skills-partial-agent.html`,
|
|
`${this.TEMPLATE_PATH}/partials/bonds-section-partial.html`,
|
|
`${this.TEMPLATE_PATH}/partials/weapons-section-partial.html`,
|
|
`${this.TEMPLATE_PATH}/partials/armor-section-partial.html`,
|
|
`${this.TEMPLATE_PATH}/partials/injuries-section-partial.html`,
|
|
`${this.TEMPLATE_PATH}/partials/other-gear-section-partial.html`,
|
|
`${this.TEMPLATE_PATH}/partials/notes-partial.html`,
|
|
],
|
|
},
|
|
});
|
|
|
|
/** @override */
|
|
_configureRenderOptions(options) {
|
|
super._configureRenderOptions(options);
|
|
|
|
if (!this.actor.limited) return;
|
|
|
|
options.parts = ["leftBar", "tabs", "rightBar"];
|
|
}
|
|
|
|
/** @override */
|
|
_prepareTabs(group) {
|
|
const tabs = super._prepareTabs(group);
|
|
|
|
if (!this.actor.limited || this.actor.type !== "agent") return tabs;
|
|
|
|
return {
|
|
personal: { ...tabs.personal, active: true, cssClass: "active" },
|
|
};
|
|
}
|
|
|
|
/** @override */
|
|
async _prepareContext(options) {
|
|
const context = await super._prepareContext(options);
|
|
|
|
if (this.actor.type !== "agent") return context;
|
|
|
|
context.typedSkillColumns = this._prepareTypedSkillColumns();
|
|
context.customSkillsEditMode = this._customSkillsEditMode;
|
|
|
|
return context;
|
|
}
|
|
|
|
/** @override */
|
|
_sortCustomSkills() {
|
|
// Agent sheet uses typedSkillColumns instead of sortedCustomSkills.
|
|
}
|
|
|
|
/** @override */
|
|
async _onRender(context, options) {
|
|
await super._onRender(context, options);
|
|
this._syncCustomSkillsEditModeUi();
|
|
}
|
|
|
|
/** @returns {void} */
|
|
_syncCustomSkillsEditModeUi() {
|
|
this.element?.classList.toggle(
|
|
"custom-skills-edit-mode",
|
|
Boolean(this._customSkillsEditMode),
|
|
);
|
|
}
|
|
|
|
static _clearBondDamage() {
|
|
for (const bond of this.actor.itemTypes.bond) {
|
|
if (!bond.system.hasBeenDamagedSinceLastHomeScene) continue;
|
|
bond.update({ "system.hasBeenDamagedSinceLastHomeScene": false });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resets the actor's current breaking point by recalculating it as the difference
|
|
* between the actor's sanity value and POW value, ensuring it is non-negative,
|
|
* and updates the actor's system with this new breaking point.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
static _resetBreakingPoint() {
|
|
const currentBreakingPoint = Math.max(
|
|
this.actor.system.sanity.value - this.actor.system.statistics.pow.value,
|
|
0,
|
|
);
|
|
|
|
const dataToUpdate = {
|
|
"system.sanity.currentBreakingPoint": currentBreakingPoint,
|
|
};
|
|
if (!this.actor.system.sanity.adaptations.violence.isAdapted) {
|
|
dataToUpdate["system.sanity.adaptations.violence.incident1"] = false;
|
|
dataToUpdate["system.sanity.adaptations.violence.incident2"] = false;
|
|
dataToUpdate["system.sanity.adaptations.violence.incident3"] = false;
|
|
}
|
|
if (!this.actor.system.sanity.adaptations.helplessness.isAdapted) {
|
|
dataToUpdate["system.sanity.adaptations.helplessness.incident1"] = false;
|
|
dataToUpdate["system.sanity.adaptations.helplessness.incident2"] = false;
|
|
dataToUpdate["system.sanity.adaptations.helplessness.incident3"] = false;
|
|
}
|
|
|
|
this.actor.update(dataToUpdate);
|
|
}
|
|
|
|
/**
|
|
* Builds grouped typed-skill columns for the skills tab.
|
|
*
|
|
* @returns {object[][]}
|
|
*/
|
|
_prepareTypedSkillColumns() {
|
|
const groupsMap = new Map();
|
|
|
|
for (const [key, skill] of Object.entries(this.actor.system.typedSkills)) {
|
|
const groupKey = skill.group;
|
|
if (!groupsMap.has(groupKey)) {
|
|
groupsMap.set(groupKey, {
|
|
group: groupKey,
|
|
label: game.i18n.localize(`DG.TypeSkills.${groupKey}`),
|
|
skills: [],
|
|
rowCount: 0,
|
|
});
|
|
}
|
|
|
|
groupsMap.get(groupKey).skills.push({
|
|
...skill,
|
|
key,
|
|
actorType: this.actor.type,
|
|
});
|
|
}
|
|
|
|
const groups = [...groupsMap.values()].map((group) => {
|
|
group.skills.sort((a, b) =>
|
|
a.label.localeCompare(b.label, game.i18n.lang),
|
|
);
|
|
group.rowCount = 1 + group.skills.length;
|
|
return group;
|
|
});
|
|
|
|
groups.sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang));
|
|
|
|
const trainings = this._prepareSpecialTrainingBlocks();
|
|
if (trainings.length > 0) {
|
|
groups.push({
|
|
group: "specialTraining",
|
|
isSpecialTraining: true,
|
|
label: game.i18n.localize("DG.Sheet.BlockHeaders.SpecialTraining"),
|
|
skills: trainings,
|
|
rowCount: 1 + trainings.length,
|
|
});
|
|
}
|
|
|
|
return packSkillGroupsIntoColumns(
|
|
groups,
|
|
3,
|
|
game.settings.get("deltagreen", "sortSkills"),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sorted special-training blocks for the skills tab (single-column list).
|
|
*
|
|
* @returns {object[]}
|
|
*/
|
|
_prepareSpecialTrainingBlocks() {
|
|
const trainings = this._prepareSpecialTrainings().map((training) => ({
|
|
...training,
|
|
sortLabel: training.name.toUpperCase(),
|
|
actorType: this.actor.type,
|
|
}));
|
|
|
|
trainings.sort((a, b) =>
|
|
a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang),
|
|
);
|
|
|
|
return trainings;
|
|
}
|
|
|
|
static _openStatsEdit() {
|
|
const form = new ActorEditStatForm({
|
|
actor: this.actor,
|
|
});
|
|
form.render(true);
|
|
}
|
|
|
|
/**
|
|
* Runs through the whole process of improving skills,
|
|
* i.e., prompting the user, rolling, and creating the chat card.
|
|
*
|
|
* @returns {Promise<void>}
|
|
*/
|
|
static async _processSkillImprovements() {
|
|
const { skills, typedSkills } = this.actor.system;
|
|
|
|
const failedSkills = Object.values(skills).filter(
|
|
(skill) => skill.failure && !skill.cannotBeImprovedByFailure,
|
|
);
|
|
const failedTypedSkills = Object.values(typedSkills).filter(
|
|
(skill) => skill.failure && !skill.cannotBeImprovedByFailure,
|
|
);
|
|
|
|
if (failedSkills.length + failedTypedSkills.length === 0) {
|
|
ui.notifications.warn("DG.Skills.ApplySkillImprovements.Warning", {
|
|
localize: true,
|
|
});
|
|
return null;
|
|
}
|
|
|
|
const baseRollFormula = game.settings.get(DG.ID, "skillImprovementFormula");
|
|
|
|
const prompt = await DGAgentSheet._createSkillImprovementDialog(
|
|
baseRollFormula,
|
|
failedSkills,
|
|
failedTypedSkills,
|
|
);
|
|
|
|
if (!prompt) return null;
|
|
|
|
const resultObj = await evaluateSkillImprovementRolls(
|
|
this.actor,
|
|
baseRollFormula,
|
|
failedSkills,
|
|
failedTypedSkills,
|
|
);
|
|
|
|
await createSkillImprovementChatMessage({
|
|
actor: this.actor,
|
|
token: this.token,
|
|
baseFormula: baseRollFormula,
|
|
failedSkills,
|
|
failedTypedSkills,
|
|
resultObj,
|
|
});
|
|
|
|
return this._applySkillImprovements(
|
|
failedSkills,
|
|
failedTypedSkills,
|
|
resultObj,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* A map of skill keys -> number to improve them by
|
|
* @typedef {Object<string, number>} ResultObj
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} FailedSkill
|
|
*/
|
|
|
|
/**
|
|
* @typedef {FailedSkill} FailedTypedSkill
|
|
*/
|
|
|
|
/**
|
|
* The formula used to calculate skill improvements
|
|
* Note. There is not a leading number of dice here, just 1 or dX-Y.
|
|
* @typedef {"1"|"d3"|"d4"|"d4-1"} SkillImprovementFormula
|
|
*/
|
|
|
|
/**
|
|
* Creates and displays a dialog to approve applying skill improvements to failed skills.
|
|
*
|
|
* @param {SkillImprovementFormula} baseFormula - The formula used to calculate skill improvements.
|
|
* @param {FailedSkill[]} failedSkills - An array of failed skills.
|
|
* @param {FailedTypedSkill[]} failedTypedSkills - An array of failed typed skills.
|
|
*
|
|
* @returns {Promise<Boolean>} - A promise that resolves to `true` if accepted, `false` otherwise.
|
|
*/
|
|
static async _createSkillImprovementDialog(
|
|
baseFormula,
|
|
failedSkills,
|
|
failedTypedSkills,
|
|
) {
|
|
const localizedFailedSkills = failedSkills.map((skill) =>
|
|
game.i18n.localize(`DG.Skills.${skill.key}`),
|
|
);
|
|
|
|
const localizedFailedTypedSkills = failedTypedSkills.map((skill) => {
|
|
const groupKey = `DG.TypeSkills.${skill.group.replace(/\s+/g, "")}`;
|
|
const groupLabel = game.i18n.localize(groupKey);
|
|
return `${groupLabel} (${skill.label})`;
|
|
});
|
|
|
|
const failedSkillNames = [
|
|
...localizedFailedSkills,
|
|
...localizedFailedTypedSkills,
|
|
].join(", ");
|
|
|
|
const content = await renderTemplate(
|
|
`${BASE_TEMPLATE_PATH}/dialog/apply-skill-improvements.html`,
|
|
{
|
|
failedSkillNames,
|
|
baseFormula: getSkillImprovementFormulaAsPercent(baseFormula),
|
|
},
|
|
);
|
|
|
|
return foundry.applications.api.DialogV2.wait({
|
|
content,
|
|
window: {
|
|
title: game.i18n.localize("DG.Skills.ApplySkillImprovements.Title"),
|
|
},
|
|
buttons: [
|
|
{
|
|
default: true,
|
|
action: "apply",
|
|
label: game.i18n.localize("DG.Skills.Apply"),
|
|
icon: "<i class='fas fa-check'></i>",
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
_applySkillImprovements(failedSkills, failedTypedSkills, resultObj) {
|
|
const updateSkills = (skillsArray, updatedTarget) => {
|
|
skillsArray.forEach((skill) => {
|
|
const increment = resultObj[skill.key] ?? 1;
|
|
updatedTarget[skill.key].proficiency += increment;
|
|
updatedTarget[skill.key].failure = false;
|
|
});
|
|
};
|
|
|
|
const actorData = this.actor.system;
|
|
// Get data and update it.
|
|
const updatedSkills = foundry.utils.duplicate(actorData.skills);
|
|
const updatedTypedSkills = foundry.utils.duplicate(actorData.typedSkills);
|
|
updateSkills(failedSkills, updatedSkills);
|
|
updateSkills(failedTypedSkills, updatedTypedSkills);
|
|
|
|
// Send updates to database.
|
|
return this.actor.update({
|
|
system: { skills: updatedSkills, typedSkills: updatedTypedSkills },
|
|
});
|
|
}
|
|
}
|