Files
DeltaGreen/module/applications/hard-experience-dialog.js
T

242 lines
7.6 KiB
JavaScript

import { BASE_TEMPLATE_PATH } from "../config/index.js";
import {
HARD_EXPERIENCE_BONUS_COUNT,
buildBonusSkillCatalog,
computeHardExperienceSkillValues,
} from "../profession/index.js";
import {
buildSkillDisplayRows,
formatBonusPickValidationMessages,
renderValidationMessagesHtml,
updateSkillGridBody,
} from "./character-creation-ui.js";
import { getDialogContentRoot, showDgDialog } from "./dg-dialog.js";
const { renderTemplate } = foundry.applications.handlebars;
/**
* @typedef {object} HardExperienceDialogState
* @property {string[]} [bonusCatalogIds]
* @property {string[]} [bonusTypedLabels]
* @property {number|null} [removedBondIndex]
*/
/**
* @param {import("../profession/commit-character-creation.js").CharacterCreationPayload} payload
* @param {HardExperienceDialogState} [state]
* @returns {Promise<{ outcome: 'submitted', path: 'hardExperience', bonusCatalogIds: string[], bonusTypedLabels: string[], removedBondIndex: number } | { outcome: 'back' } | null>}
*/
export default async function showHardExperienceDialog(payload, state = {}) {
const bonusCatalogIds = state.bonusCatalogIds
? [...state.bonusCatalogIds]
: Array(HARD_EXPERIENCE_BONUS_COUNT).fill("");
const bonusTypedLabels = state.bonusTypedLabels
? [...state.bonusTypedLabels]
: Array(HARD_EXPERIENCE_BONUS_COUNT).fill("");
let removedBondIndex =
state.removedBondIndex ?? (payload.bonds.length > 0 ? 0 : null);
/** @type {{ outcome: 'submitted', path: 'hardExperience', bonusCatalogIds: string[], bonusTypedLabels: string[], removedBondIndex: number } | { outcome: 'back' } | null} */
let result = null;
/** @type {import("foundry.applications.api.DialogV2").default | null} */
let dialog = null;
const bondRows = payload.bonds.map((bond, index) => ({
index,
name: bond.name,
relationship: bond.relationship,
checked: index === removedBondIndex,
}));
const bonusSlots = Array.from(
{ length: HARD_EXPERIENCE_BONUS_COUNT },
(_, index) => ({ index }),
);
const syncFormStateFromDom = () => {
const root = dialog ? getDialogContentRoot(dialog) : null;
if (!root) return;
for (let i = 0; i < HARD_EXPERIENCE_BONUS_COUNT; i++) {
const select = root.querySelector(`[name="bonusSkill-${i}"]`);
bonusCatalogIds[i] = select?.value ?? "";
const typedInput = root.querySelector(`[name="bonusTypedLabel-${i}"]`);
bonusTypedLabels[i] = typedInput?.value ?? "";
}
const bondSelected = root.querySelector(
'input[name="removedBondIndex"]:checked',
);
removedBondIndex = bondSelected ? Number(bondSelected.value) : null;
};
const compute = () =>
computeHardExperienceSkillValues(payload, {
bonusCatalogIds,
bonusTypedLabels,
});
const buildTemplateData = () => {
const computed = compute();
return {
skillRows: buildSkillDisplayRows(computed),
catalog: buildBonusSkillCatalog(),
bonusSlots,
bondRows,
validationMessages: formatBonusPickValidationMessages(
computed.validationErrors,
{
bonusSkillRequiredKey: "DG.HardExperience.Dialog.BonusSkillRequired",
bonusSkillWasteTooHighKey:
"DG.HardExperience.Dialog.BonusSkillWasteTooHigh",
},
),
isValid: computed.isValid && removedBondIndex !== null,
};
};
const content = await renderTemplate(
`${BASE_TEMPLATE_PATH}/dialog/hard-experience.html`,
buildTemplateData(),
);
const refreshUi = async () => {
syncFormStateFromDom();
const root = dialog ? getDialogContentRoot(dialog) : null;
if (!root) return;
const templateData = buildTemplateData();
updateSkillGridBody(
root.querySelector("[data-hard-experience-skills-body]"),
templateData.skillRows,
);
const messagesEl = root.querySelector("[data-hard-experience-messages]");
if (messagesEl) {
let messagesHtml = renderValidationMessagesHtml(
templateData.validationMessages,
);
if (removedBondIndex === null) {
messagesHtml += renderValidationMessagesHtml([
game.i18n.localize("DG.HardExperience.Dialog.BondRemovalRequired"),
]);
}
messagesEl.innerHTML = messagesHtml;
}
const submitBtn = dialog?.element?.querySelector(
'button[data-action="submit"]',
);
if (submitBtn) {
submitBtn.disabled = !(templateData.isValid && removedBondIndex !== null);
}
for (let i = 0; i < HARD_EXPERIENCE_BONUS_COUNT; i++) {
const select = root.querySelector(`[name="bonusSkill-${i}"]`);
if (select) select.value = bonusCatalogIds[i] ?? "";
const typedInput = root.querySelector(`[name="bonusTypedLabel-${i}"]`);
const isTyped = (bonusCatalogIds[i] ?? "").startsWith("typed:");
if (typedInput) {
typedInput.classList.toggle("hidden", !isTyped);
typedInput.value = bonusTypedLabels[i] ?? "";
}
}
if (removedBondIndex !== null) {
const bondInput = root.querySelector(
`input[name="removedBondIndex"][value="${removedBondIndex}"]`,
);
if (bondInput) bondInput.checked = true;
}
};
const bindListeners = () => {
const root = dialog ? getDialogContentRoot(dialog) : null;
if (!root) return;
root.querySelectorAll("select[data-bonus-index]").forEach((select) => {
select.addEventListener("change", () => {
const index = Number(select.dataset.bonusIndex);
bonusCatalogIds[index] = select.value;
const typedInput = root.querySelector(
`[name="bonusTypedLabel-${index}"]`,
);
const isTyped = select.value.startsWith("typed:");
if (typedInput) {
typedInput.classList.toggle("hidden", !isTyped);
if (!isTyped) {
typedInput.value = "";
bonusTypedLabels[index] = "";
}
}
refreshUi();
});
});
root.querySelectorAll("[data-bonus-typed-index]").forEach((input) => {
input.addEventListener("input", () => refreshUi());
});
root.querySelectorAll('input[name="removedBondIndex"]').forEach((input) => {
input.addEventListener("change", () => refreshUi());
});
};
await showDgDialog({
modifier: "hard-experience",
content,
window: {
title: game.i18n.localize("DG.HardExperience.Dialog.Title"),
},
position: { width: 1080, height: 780 },
form: { closeOnSubmit: false },
close: () => result,
onRender: async (instance) => {
dialog = instance;
bindListeners();
await refreshUi();
},
buttons: [
{
action: "back",
label: game.i18n.localize("DG.ProfessionSetup.GoBack"),
callback: async (_event, _button, instance) => {
syncFormStateFromDom();
result = {
outcome: "back",
bonusCatalogIds: [...bonusCatalogIds],
bonusTypedLabels: [...bonusTypedLabels],
removedBondIndex,
};
await instance.close();
return false;
},
},
{
action: "submit",
label: game.i18n.localize("DG.Profession.Dialog.Submit"),
default: true,
disabled: true,
callback: async () => {
syncFormStateFromDom();
const computed = compute();
if (!computed.isValid || removedBondIndex === null) return false;
result = {
outcome: "submitted",
path: "hardExperience",
bonusCatalogIds: [...bonusCatalogIds],
bonusTypedLabels: [...bonusTypedLabels],
removedBondIndex,
};
await dialog?.close();
return false;
},
},
],
});
return result;
}