296 lines
12 KiB
JavaScript
296 lines
12 KiB
JavaScript
import MGT2ActorSheet from "./base-actor-sheet.mjs";
|
||
import { RollPromptHelper } from "../../roll-prompt.js";
|
||
import { MGT2Helper } from "../../helper.js";
|
||
|
||
const { renderTemplate } = foundry.applications.handlebars;
|
||
|
||
/** Convert Traveller dice notation (e.g. "2D", "4D+2", "3D6") to FoundryVTT formula */
|
||
function normalizeDice(formula) {
|
||
if (!formula) return "1d6";
|
||
return formula
|
||
.replace(/(\d*)D(\d*)([+-]\d+)?/gi, (_, count, sides, mod) => {
|
||
const n = count || "1";
|
||
const d = sides || "6";
|
||
return mod ? `${n}d${d}${mod}` : `${n}d${d}`;
|
||
});
|
||
}
|
||
|
||
export default class TravellerCreatureSheet extends MGT2ActorSheet {
|
||
|
||
/** @override */
|
||
static DEFAULT_OPTIONS = {
|
||
...super.DEFAULT_OPTIONS,
|
||
classes: [...super.DEFAULT_OPTIONS.classes, "creature", "nopad"],
|
||
position: {
|
||
width: 720,
|
||
height: 600,
|
||
},
|
||
window: {
|
||
...super.DEFAULT_OPTIONS.window,
|
||
title: "TYPES.Actor.creature",
|
||
},
|
||
actions: {
|
||
...super.DEFAULT_OPTIONS.actions,
|
||
rollAttack: TravellerCreatureSheet.#onRollAttack,
|
||
rollSkill: TravellerCreatureSheet.#onRollSkill,
|
||
addSkill: TravellerCreatureSheet.#onAddRow,
|
||
deleteSkill: TravellerCreatureSheet.#onDeleteRow,
|
||
addAttack: TravellerCreatureSheet.#onAddRow,
|
||
deleteAttack: TravellerCreatureSheet.#onDeleteRow,
|
||
addTrait: TravellerCreatureSheet.#onAddRow,
|
||
deleteTrait: TravellerCreatureSheet.#onDeleteRow,
|
||
},
|
||
}
|
||
|
||
/** @override */
|
||
static PARTS = {
|
||
sheet: {
|
||
template: "systems/mgt2/templates/actors/creature-sheet.html",
|
||
},
|
||
}
|
||
|
||
/** @override */
|
||
tabGroups = { primary: "combat" }
|
||
|
||
/** @override */
|
||
async _prepareContext() {
|
||
const context = await super._prepareContext();
|
||
const actor = this.document;
|
||
const enrich = (html) => foundry.applications.ux.TextEditor.implementation.enrichHTML(html ?? "", { async: true });
|
||
|
||
context.enrichedBiography = await enrich(actor.system.biography);
|
||
context.enrichedNotes = await enrich(actor.system.notes);
|
||
context.sizeLabel = this._getSizeLabel(actor.system.life.max);
|
||
context.sizeTraitLabel = this._getSizeTrait(actor.system.life.max);
|
||
context.config = CONFIG.MGT2;
|
||
|
||
return context;
|
||
}
|
||
|
||
_getSizeLabel(pdv) {
|
||
if (pdv <= 2) return "Souris / Rat";
|
||
if (pdv <= 5) return "Chat";
|
||
if (pdv <= 7) return "Blaireau / Chien";
|
||
if (pdv <= 13) return "Chimpanzé / Chèvre";
|
||
if (pdv <= 28) return "Humain";
|
||
if (pdv <= 35) return "Vache / Cheval";
|
||
if (pdv <= 49) return "Requin";
|
||
if (pdv <= 70) return "Rhinocéros";
|
||
if (pdv <= 90) return "Éléphant";
|
||
if (pdv <= 125) return "Carnosaure";
|
||
return "Sauropode / Baleine";
|
||
}
|
||
|
||
_getSizeTrait(pdv) {
|
||
if (pdv <= 2) return "Petit (−4)";
|
||
if (pdv <= 5) return "Petit (−3)";
|
||
if (pdv <= 7) return "Petit (−2)";
|
||
if (pdv <= 13) return "Petit (−1)";
|
||
if (pdv <= 28) return "—";
|
||
if (pdv <= 35) return "Grand (+1)";
|
||
if (pdv <= 49) return "Grand (+2)";
|
||
if (pdv <= 70) return "Grand (+3)";
|
||
if (pdv <= 90) return "Grand (+4)";
|
||
if (pdv <= 125) return "Grand (+5)";
|
||
return "Grand (+6)";
|
||
}
|
||
|
||
// ───────────────────────────────────────────────────────── Roll Helpers
|
||
|
||
static async #postCreatureRoll({ actor, roll, rollLabel, dm, difficulty, difficultyLabel, rollMode, extraTooltip, damageFormula }) {
|
||
const diffTarget = MGT2Helper.getDifficultyValue(difficulty ?? "Average");
|
||
const hasDifficulty = !!difficulty;
|
||
const success = hasDifficulty ? roll.total >= diffTarget : true;
|
||
const effect = roll.total - diffTarget;
|
||
const effectStr = (effect >= 0 ? "+" : "") + effect;
|
||
|
||
const diceRawTotal = roll.dice.reduce((s, d) => s + d.total, 0);
|
||
const breakdownParts = [game.i18n.localize("MGT2.Chat.Roll.Dice") + " " + diceRawTotal];
|
||
if (dm !== 0) breakdownParts.push(`DM ${dm >= 0 ? "+" : ""}${dm}`);
|
||
if (hasDifficulty) breakdownParts.push(game.i18n.localize("MGT2.Chat.Roll.Effect") + " " + effectStr);
|
||
if (extraTooltip) breakdownParts.push(extraTooltip);
|
||
const rollBreakdown = breakdownParts.join(" | ");
|
||
|
||
const showRollDamage = success && !!damageFormula;
|
||
|
||
const chatData = {
|
||
creatureName: actor.name,
|
||
creatureImg: actor.img,
|
||
rollLabel,
|
||
formula: roll.formula,
|
||
total: roll.total,
|
||
tooltip: await roll.getTooltip(),
|
||
rollBreakdown,
|
||
difficulty: hasDifficulty ? diffTarget : null,
|
||
difficultyLabel: difficultyLabel ?? MGT2Helper.getDifficultyDisplay(difficulty),
|
||
success: hasDifficulty ? success : null,
|
||
failure: hasDifficulty ? !success : null,
|
||
effect: hasDifficulty ? effect : null,
|
||
effectStr: hasDifficulty ? effectStr : null,
|
||
modifiers: dm !== 0 ? [`DM ${dm >= 0 ? "+" : ""}${dm}`] : [],
|
||
showRollDamage,
|
||
};
|
||
|
||
const chatContent = await renderTemplate(
|
||
"systems/mgt2/templates/chat/creature-roll.html",
|
||
chatData
|
||
);
|
||
|
||
const flags = showRollDamage ? {
|
||
mgt2: {
|
||
damage: {
|
||
formula: normalizeDice(damageFormula),
|
||
effect,
|
||
rollObjectName: actor.name,
|
||
rollTypeName: rollLabel,
|
||
}
|
||
}
|
||
} : {};
|
||
|
||
await ChatMessage.create({
|
||
content: chatContent,
|
||
speaker: ChatMessage.getSpeaker({ actor }),
|
||
rolls: [roll],
|
||
rollMode: rollMode ?? game.settings.get("core", "rollMode"),
|
||
flags,
|
||
});
|
||
|
||
return { success, effect, total: roll.total };
|
||
}
|
||
|
||
// ───────────────────────────────────────────────────────── Roll Handlers
|
||
|
||
/** Roll a skill check (2d6 + level vs difficulty) — uses unified dialog */
|
||
static async #onRollSkill(event, target) {
|
||
const index = parseInt(target.dataset.index ?? 0);
|
||
const actor = this.document;
|
||
const skill = actor.system.skills[index];
|
||
if (!skill) return;
|
||
|
||
const result = await RollPromptHelper.roll({
|
||
isCreature: true,
|
||
showSkillSelector: false,
|
||
skillName: skill.name,
|
||
skillLevel: skill.level,
|
||
difficulty: "Average",
|
||
title: game.i18n.localize("MGT2.Creature.RollSkill") + " — " + skill.name,
|
||
});
|
||
if (!result) return;
|
||
|
||
const customDM = parseInt(result.customDM ?? "0", 10) || 0;
|
||
const skillLevel = parseInt(skill.level ?? 0, 10) || 0;
|
||
const dm = skillLevel + customDM;
|
||
const diceModifier = result.diceModifier ?? "";
|
||
|
||
// Build formula exactly like character-sheet: parts joined without spaces
|
||
const parts = [];
|
||
if (diceModifier) {
|
||
parts.push("3d6", diceModifier);
|
||
} else {
|
||
parts.push("2d6");
|
||
}
|
||
if (dm !== 0) parts.push(MGT2Helper.getFormulaDM(dm));
|
||
const fullFormula = parts.join("");
|
||
|
||
const roll = await new Roll(fullFormula).evaluate();
|
||
const rollLabel = `${skill.name.toUpperCase()} (${skillLevel >= 0 ? "+" : ""}${skillLevel})`;
|
||
|
||
const tooltipParts = [`Dés: ${roll.dice.reduce((s, d) => s + d.total, 0)}`];
|
||
if (skillLevel !== 0) tooltipParts.push(`${skill.name} ${skillLevel >= 0 ? "+" : ""}${skillLevel}`);
|
||
if (customDM !== 0) tooltipParts.push(`MD perso ${customDM >= 0 ? "+" : ""}${customDM}`);
|
||
|
||
await TravellerCreatureSheet.#postCreatureRoll({
|
||
actor, roll, rollLabel,
|
||
dm,
|
||
difficulty: result.difficulty,
|
||
rollMode: result.rollMode,
|
||
extraTooltip: tooltipParts.join(" | "),
|
||
});
|
||
}
|
||
|
||
/** Roll an attack: dialog with skill selector, then roll 2d6+skill+DM vs difficulty; on success roll damage */
|
||
static async #onRollAttack(event, target) {
|
||
const index = parseInt(target.dataset.index ?? 0);
|
||
const actor = this.document;
|
||
const attack = actor.system.attacks[index];
|
||
if (!attack) return;
|
||
|
||
const skills = actor.system.skills ?? [];
|
||
|
||
const result = await RollPromptHelper.roll({
|
||
isCreature: true,
|
||
showSkillSelector: true,
|
||
creatureSkills: skills,
|
||
selectedSkillIndex: attack.skill ?? -1,
|
||
difficulty: "Average",
|
||
title: game.i18n.localize("MGT2.Creature.RollAttack") + " — " + attack.name,
|
||
});
|
||
if (!result) return;
|
||
|
||
const skillIndex = parseInt(result.creatureSkillIndex ?? "-1", 10);
|
||
const chosenSkill = (skillIndex >= 0 && skillIndex < skills.length) ? skills[skillIndex] : null;
|
||
const skillLevel = parseInt(chosenSkill?.level ?? 0, 10) || 0;
|
||
const customDM = parseInt(result.customDM ?? "0", 10) || 0;
|
||
const dm = skillLevel + customDM;
|
||
const diceModifier = result.diceModifier ?? "";
|
||
|
||
// Build formula exactly like character-sheet: parts joined without spaces
|
||
const parts = [];
|
||
if (diceModifier) {
|
||
parts.push("3d6", diceModifier);
|
||
} else {
|
||
parts.push("2d6");
|
||
}
|
||
if (dm !== 0) parts.push(MGT2Helper.getFormulaDM(dm));
|
||
const fullFormula = parts.join("");
|
||
|
||
const roll = await new Roll(fullFormula).evaluate();
|
||
const rollLabel = chosenSkill
|
||
? `${attack.name} — ${chosenSkill.name} (${skillLevel >= 0 ? "+" : ""}${skillLevel})`
|
||
: attack.name;
|
||
|
||
const tooltipParts = [`Dés: ${roll.dice.reduce((s, d) => s + d.total, 0)}`];
|
||
if (chosenSkill) tooltipParts.push(`${chosenSkill.name} ${skillLevel >= 0 ? "+" : ""}${skillLevel}`);
|
||
if (customDM !== 0) tooltipParts.push(`MD perso ${customDM >= 0 ? "+" : ""}${customDM}`);
|
||
|
||
await TravellerCreatureSheet.#postCreatureRoll({
|
||
actor, roll, rollLabel,
|
||
dm,
|
||
difficulty: result.difficulty,
|
||
rollMode: result.rollMode,
|
||
extraTooltip: tooltipParts.join(" | "),
|
||
damageFormula: attack.damage || null,
|
||
});
|
||
}
|
||
|
||
// ───────────────────────────────────────────────────────── CRUD Handlers
|
||
|
||
static async #onAddRow(event, target) {
|
||
const prop = target.dataset.prop;
|
||
if (!prop) return;
|
||
const actor = this.document;
|
||
const arr = foundry.utils.deepClone(actor.system[prop] ?? []);
|
||
arr.push(this._getDefaultRow(prop));
|
||
await actor.update({ [`system.${prop}`]: arr });
|
||
}
|
||
|
||
static async #onDeleteRow(event, target) {
|
||
const prop = target.dataset.prop;
|
||
const index = parseInt(target.dataset.index);
|
||
if (!prop || isNaN(index)) return;
|
||
const actor = this.document;
|
||
const arr = foundry.utils.deepClone(actor.system[prop] ?? []);
|
||
arr.splice(index, 1);
|
||
await actor.update({ [`system.${prop}`]: arr });
|
||
}
|
||
|
||
_getDefaultRow(prop) {
|
||
switch (prop) {
|
||
case "skills": return { name: "", level: 0, note: "" };
|
||
case "attacks": return { name: "", damage: "1D", skill: -1, description: "" };
|
||
case "traits": return { name: "", value: "", description: "" };
|
||
default: return {};
|
||
}
|
||
}
|
||
}
|