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 {}; } } }