272 lines
7.6 KiB
JavaScript
272 lines
7.6 KiB
JavaScript
import { BASE_TEMPLATE_PATH } from "../config/index.js";
|
|
import { createDGChatMessage } from "../chat/dg-chat-card.js";
|
|
import {
|
|
STAT_KEYS,
|
|
applyAgentStatistics,
|
|
buildRollStatsChatContent,
|
|
buildStatisticRows,
|
|
rollStatisticScores,
|
|
} from "../profession/stat-setup.js";
|
|
import { getDialogContentRoot, showDgDialog } from "./dg-dialog.js";
|
|
|
|
const { renderTemplate } = foundry.applications.handlebars;
|
|
|
|
/** @typedef {'submitted' | 'back'} RollStatsOutcome */
|
|
|
|
/**
|
|
* @param {object} state
|
|
* @param {DialogV2} dialog
|
|
* @param {number|null} rollIndex
|
|
* @returns {string|null}
|
|
*/
|
|
function findStatKeyForRollIndex(state, rollIndex) {
|
|
for (const key of STAT_KEYS) {
|
|
if (state.assignments[key] === rollIndex) return key;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param {object} state
|
|
* @param {DialogV2} dialog
|
|
*/
|
|
function syncDomFromAssignments(state, dialog) {
|
|
const root = getDialogContentRoot(dialog);
|
|
if (!root) return;
|
|
|
|
const pool = root.querySelector("[data-roll-stats-pool]");
|
|
if (!pool) return;
|
|
|
|
root.querySelectorAll(".roll-stat-token").forEach((token) => {
|
|
pool.appendChild(token);
|
|
});
|
|
|
|
for (const key of STAT_KEYS) {
|
|
const rollIndex = state.assignments[key];
|
|
if (rollIndex !== null) {
|
|
const slot = root.querySelector(
|
|
`[data-stat-slot][data-stat-key="${key}"]`,
|
|
);
|
|
const token = pool.querySelector(`[data-roll-index="${rollIndex}"]`);
|
|
if (slot && token) {
|
|
slot.appendChild(token);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {object} state
|
|
* @param {DialogV2} dialog
|
|
*/
|
|
function refreshRollStatsUi(state, dialog) {
|
|
const submitBtn = dialog.element?.querySelector(
|
|
'button[data-action="submit"]',
|
|
);
|
|
if (submitBtn) {
|
|
submitBtn.disabled = !STAT_KEYS.every(
|
|
(key) => state.assignments[key] !== null,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {object} state
|
|
* @param {DialogV2} dialog
|
|
*/
|
|
function bindRollStatsListeners(state, dialog) {
|
|
const root = getDialogContentRoot(dialog);
|
|
if (!root) return;
|
|
if (root.dataset.rollStatsListenersBound === "true") return;
|
|
root.dataset.rollStatsListenersBound = "true";
|
|
|
|
root.querySelectorAll(".roll-stat-token").forEach((token) => {
|
|
token.addEventListener("dragstart", (event) => {
|
|
const rollIndex = Number(token.dataset.rollIndex);
|
|
state.dragRollIndex = rollIndex;
|
|
token.classList.add("is-dragging");
|
|
event.dataTransfer?.setData("text/plain", String(rollIndex));
|
|
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
|
|
});
|
|
token.addEventListener("dragend", () => {
|
|
token.classList.remove("is-dragging");
|
|
state.dragRollIndex = null;
|
|
root
|
|
.querySelectorAll(".drag-over")
|
|
.forEach((el) => el.classList.remove("drag-over"));
|
|
});
|
|
});
|
|
|
|
const readDragRollIndex = (event) => {
|
|
const fromData = event.dataTransfer?.getData("text/plain");
|
|
if (fromData !== undefined && fromData !== "") {
|
|
const parsed = Number(fromData);
|
|
if (Number.isInteger(parsed)) return parsed;
|
|
}
|
|
if (state.dragRollIndex !== null) return state.dragRollIndex;
|
|
return null;
|
|
};
|
|
|
|
root.querySelectorAll("[data-stat-slot]").forEach((slot) => {
|
|
slot.addEventListener("dragover", (event) => {
|
|
event.preventDefault();
|
|
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
|
slot.classList.add("drag-over");
|
|
});
|
|
slot.addEventListener("dragleave", () =>
|
|
slot.classList.remove("drag-over"),
|
|
);
|
|
slot.addEventListener("drop", (event) => {
|
|
event.preventDefault();
|
|
slot.classList.remove("drag-over");
|
|
|
|
const rollIndex = readDragRollIndex(event);
|
|
if (rollIndex === null) return;
|
|
|
|
const { statKey } = slot.dataset;
|
|
if (!statKey) return;
|
|
|
|
const sourceStatKey = findStatKeyForRollIndex(state, rollIndex);
|
|
const displacedIndex = state.assignments[statKey];
|
|
|
|
if (sourceStatKey === statKey) return;
|
|
|
|
if (sourceStatKey) {
|
|
state.assignments[sourceStatKey] = null;
|
|
}
|
|
|
|
if (displacedIndex !== null && displacedIndex !== rollIndex) {
|
|
if (sourceStatKey) {
|
|
state.assignments[sourceStatKey] = displacedIndex;
|
|
}
|
|
}
|
|
|
|
state.assignments[statKey] = rollIndex;
|
|
syncDomFromAssignments(state, dialog);
|
|
refreshRollStatsUi(state, dialog);
|
|
});
|
|
});
|
|
|
|
const pool = root.querySelector("[data-roll-stats-pool]");
|
|
if (!pool) return;
|
|
|
|
pool.addEventListener("dragover", (event) => {
|
|
event.preventDefault();
|
|
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
|
pool.classList.add("drag-over");
|
|
});
|
|
pool.addEventListener("dragleave", (event) => {
|
|
if (
|
|
event.currentTarget === event.target ||
|
|
!event.currentTarget.contains(event.relatedTarget)
|
|
) {
|
|
pool.classList.remove("drag-over");
|
|
}
|
|
});
|
|
pool.addEventListener("drop", (event) => {
|
|
event.preventDefault();
|
|
pool.classList.remove("drag-over");
|
|
|
|
const rollIndex = readDragRollIndex(event);
|
|
if (rollIndex === null) return;
|
|
|
|
const sourceStatKey = findStatKeyForRollIndex(state, rollIndex);
|
|
if (sourceStatKey) {
|
|
state.assignments[sourceStatKey] = null;
|
|
syncDomFromAssignments(state, dialog);
|
|
refreshRollStatsUi(state, dialog);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {Actor} actor
|
|
* @param {object} [options]
|
|
* @param {TokenDocument|null} [options.token]
|
|
* @returns {Promise<{ outcome: 'submitted' } | { outcome: 'back' } | null>}
|
|
*/
|
|
export default async function showRollStatsDialog(
|
|
actor,
|
|
{ token = null } = {},
|
|
) {
|
|
const rolled = await rollStatisticScores({ actor, token });
|
|
const rolls = rolled.map((entry, index) => ({
|
|
index,
|
|
total: entry.total,
|
|
}));
|
|
|
|
await createDGChatMessage({
|
|
actor,
|
|
token,
|
|
title: game.i18n.localize("DG.ProfessionSetup.RollStats.ChatLabel"),
|
|
content: buildRollStatsChatContent(rolls),
|
|
});
|
|
|
|
const state = {
|
|
rolls,
|
|
assignments: Object.fromEntries(STAT_KEYS.map((key) => [key, null])),
|
|
dragRollIndex: null,
|
|
};
|
|
/** @type {{ outcome: 'submitted' } | { outcome: 'back' } | null} */
|
|
let result = null;
|
|
|
|
const content = await renderTemplate(
|
|
`${BASE_TEMPLATE_PATH}/dialog/roll-stats.html`,
|
|
{
|
|
rolls: state.rolls,
|
|
stats: buildStatisticRows(),
|
|
},
|
|
);
|
|
|
|
return showDgDialog({
|
|
modifier: "roll-stats",
|
|
content,
|
|
window: {
|
|
title: game.i18n.localize("DG.ProfessionSetup.RollStats.Title"),
|
|
},
|
|
position: { width: 520 },
|
|
form: { closeOnSubmit: false },
|
|
onRender: (dialog) => {
|
|
bindRollStatsListeners(state, dialog);
|
|
refreshRollStatsUi(state, dialog);
|
|
},
|
|
close: () => result,
|
|
buttons: [
|
|
{
|
|
action: "back",
|
|
label: game.i18n.localize("DG.ProfessionSetup.GoBack"),
|
|
callback: async (_event, _button, dialog) => {
|
|
result = { outcome: "back" };
|
|
await dialog.close();
|
|
},
|
|
},
|
|
{
|
|
action: "submit",
|
|
label: game.i18n.localize("DG.ProfessionSetup.RollStats.Submit"),
|
|
default: true,
|
|
disabled: true,
|
|
callback: async (_event, _button, dialog) => {
|
|
const isComplete = STAT_KEYS.every(
|
|
(key) => state.assignments[key] !== null,
|
|
);
|
|
if (!isComplete) return false;
|
|
|
|
/** @type {Record<string, number>} */
|
|
const valuesByKey = {};
|
|
for (const key of STAT_KEYS) {
|
|
const rollIndex = state.assignments[key];
|
|
const roll = state.rolls.find((entry) => entry.index === rollIndex);
|
|
if (!roll) return false;
|
|
valuesByKey[key] = roll.total;
|
|
}
|
|
|
|
await applyAgentStatistics(actor, valuesByKey);
|
|
result = { outcome: "submitted" };
|
|
await dialog.close();
|
|
return false;
|
|
},
|
|
},
|
|
],
|
|
});
|
|
}
|