Ajout des fonctions de gestion des soins

This commit is contained in:
2026-04-21 10:22:39 +02:00
parent df0a93d715
commit 74f1b581f7
52 changed files with 2697 additions and 650 deletions

View File

@@ -33,6 +33,7 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
traitEdit: TravellerCharacterSheet.#onTraitEdit,
traitDelete: TravellerCharacterSheet.#onTraitDelete,
openEditor: TravellerCharacterSheet.#onOpenEditor,
heal: TravellerCharacterSheet.#onHeal,
},
}
@@ -54,7 +55,11 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
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.enrichedFinanceNotes = await enrich(actor.system.finance?.notes);
context.settings = {
weightUnit: "kg",
usePronouns: game.settings.get("mgt2", "usePronouns"),
@@ -152,9 +157,10 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
if (item.hasOwnProperty("equipped")) {
i._canEquip = true;
i._toggleClass = item.equipped ? "active" : "";
i.toggleClass = item.equipped ? "active" : "";
} else {
i._canEquip = false;
i.toggleClass = "";
}
switch (i.type) {
@@ -288,7 +294,6 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
this._bindClassEvent(html, ".item-create", "click", TravellerCharacterSheet.#onCreateItem);
this._bindClassEvent(html, ".item-edit", "click", TravellerCharacterSheet.#onEditItem);
this._bindClassEvent(html, ".item-delete", "click", TravellerCharacterSheet.#onDeleteItem);
this._bindClassEvent(html, ".item-equip", "click", TravellerCharacterSheet.#onEquipItem);
this._bindClassEvent(html, ".item-storage-in", "click", TravellerCharacterSheet.#onItemStorageIn);
this._bindClassEvent(html, ".item-storage-out", "click", TravellerCharacterSheet.#onItemStorageOut);
this._bindClassEvent(html, ".software-eject", "click", TravellerCharacterSheet.#onSoftwareEject);
@@ -452,17 +457,34 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
if (item) item.sheet.render(true);
}
static async #confirmDelete(name) {
return foundry.applications.api.DialogV2.confirm({
window: { title: game.i18n.localize("MGT2.Dialog.ConfirmDeleteTitle") },
content: `<p>${game.i18n.format("MGT2.Dialog.ConfirmDeleteContent", { name })}</p>`,
yes: { label: game.i18n.localize("MGT2.Dialog.Yes"), icon: "fas fa-trash" },
no: { label: game.i18n.localize("MGT2.Dialog.No"), icon: "fas fa-times" },
rejectClose: false,
modal: true
});
}
static async #onDeleteItem(event, target) {
event.preventDefault();
const li = target.closest("[data-item-id]");
if (!li?.dataset.itemId) return;
const item = this.actor.items.get(li.dataset.itemId);
if (!item) return;
const confirmed = await TravellerCharacterSheet.#confirmDelete(item.name);
if (!confirmed) return;
this.actor.deleteEmbeddedDocuments("Item", [li.dataset.itemId]);
}
static async #onEquipItem(event, target) {
event.preventDefault();
const li = target.closest("[data-item-id]");
const item = this.actor.getEmbeddedDocument("Item", li?.dataset.itemId);
const itemId = li?.dataset.itemId;
if (!itemId) return;
const item = this.actor.items.get(itemId);
if (!item) return;
await item.update({ "system.equipped": !item.system.equipped });
}
@@ -530,6 +552,9 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
const container = containers.find(x => x._id === this.actor.system.containerView);
if (!container) return;
const confirmed = await TravellerCharacterSheet.#confirmDelete(container.name);
if (!confirmed) return;
const containerItems = this.actor.items.filter(
x => x.system.hasOwnProperty("container") && x.system.container.id === container._id
);
@@ -564,6 +589,7 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
encumbrance: this.actor.system.states.encumbrance,
difficulty: null,
damageFormula: null,
isMelee: false,
};
const cardButtons = [];
@@ -631,6 +657,9 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
if (itemObj?.system.hasOwnProperty("damage")) {
rollOptions.damageFormula = itemObj.system.damage;
if (itemObj.type === "weapon") {
rollOptions.isMelee = itemObj.system.range?.isMelee === true;
}
if (itemObj.type === "disease") {
if (itemObj.system.subType === "disease")
rollOptions.rollTypeName = game.i18n.localize("MGT2.DiseaseSubType.disease");
@@ -695,13 +724,13 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
rollModifiers.push(game.i18n.localize("MGT2.Actor.Fatigue") + " -2");
}
if (userRollData.customDM) {
const s = userRollData.customDM.trim();
if (/^[0-9]/.test(s)) rollFormulaParts.push("+");
rollFormulaParts.push(s);
const customDMVal = parseInt(userRollData.customDM ?? "0", 10);
if (!isNaN(customDMVal) && customDMVal !== 0) {
rollFormulaParts.push(customDMVal > 0 ? `+${customDMVal}` : `${customDMVal}`);
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.CustomDM") + " " + (customDMVal > 0 ? `+${customDMVal}` : `${customDMVal}`));
}
if (MGT2Helper.hasValue(userRollData, "difficulty")) rollOptions.difficulty = userRollData.difficulty;
if (MGT2Helper.hasValue(userRollData, "difficulty") && userRollData.difficulty !== "") rollOptions.difficulty = userRollData.difficulty;
const rollFormula = rollFormulaParts.join("");
if (!Roll.validate(rollFormula)) {
@@ -715,37 +744,74 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
await this.token.combatant.update({ initiative: roll.total });
}
// ── Compute effect and effective damage formula ──────────────────────
let rollSuccess = false;
let rollFailure = false;
let rollEffect = undefined;
let rollEffectStr = undefined;
let difficultyValue = null;
if (MGT2Helper.hasValue(rollOptions, "difficulty")) {
difficultyValue = MGT2Helper.getDifficultyValue(rollOptions.difficulty);
rollEffect = roll.total - difficultyValue;
rollEffectStr = (rollEffect >= 0 ? "+" : "") + rollEffect;
rollSuccess = rollEffect >= 0;
rollFailure = !rollSuccess;
}
// Build effective damage formula: base + effect + STR DM (melee)
let effectiveDamageFormula = rollOptions.damageFormula || null;
if (effectiveDamageFormula) {
if (rollEffect !== undefined && rollEffect !== 0) {
effectiveDamageFormula += (rollEffect >= 0 ? "+" : "") + rollEffect;
}
if (rollOptions.isMelee) {
const strDm = this.actor.system.characteristics.strength?.dm ?? 0;
if (strDm !== 0) effectiveDamageFormula += (strDm >= 0 ? "+" : "") + strDm;
}
}
// ── Build roll breakdown tooltip ─────────────────────────────────────
const diceRawTotal = roll.dice.reduce((s, d) => s + d.total, 0);
const breakdownParts = [game.i18n.localize("MGT2.Chat.Roll.Dice") + " " + diceRawTotal];
for (const mod of rollModifiers) breakdownParts.push(mod);
if (rollEffectStr !== undefined)
breakdownParts.push(game.i18n.localize("MGT2.Chat.Roll.Effect") + " " + rollEffectStr);
const rollBreakdown = breakdownParts.join(" | ");
const chatData = {
user: game.user.id,
speaker: this.actor ? ChatMessage.getSpeaker({ actor: this.actor }) : null,
formula: roll._formula,
tooltip: await roll.getTooltip(),
total: Math.round(roll.total * 100) / 100,
rollBreakdown,
showButtons: true,
showLifeButtons: false,
showRollRequest: false,
rollTypeName: rollOptions.rollTypeName,
rollObjectName: rollOptions.rollObjectName,
rollModifiers: rollModifiers,
showRollDamage: rollOptions.damageFormula !== null && rollOptions.damageFormula !== "",
// Show damage button only if there's a formula AND (no difficulty check OR roll succeeded)
showRollDamage: !!effectiveDamageFormula && (!difficultyValue || rollSuccess),
cardButtons: cardButtons,
};
if (MGT2Helper.hasValue(rollOptions, "difficulty")) {
chatData.rollDifficulty = rollOptions.difficulty;
chatData.rollDifficultyLabel = MGT2Helper.getDifficultyDisplay(rollOptions.difficulty);
if (roll.total >= MGT2Helper.getDifficultyValue(rollOptions.difficulty))
chatData.rollSuccess = true;
else
chatData.rollFailure = true;
chatData.rollEffect = rollEffect;
chatData.rollEffectStr = rollEffectStr;
chatData.rollSuccess = rollSuccess || undefined;
chatData.rollFailure = rollFailure || undefined;
}
const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/roll.html", chatData);
chatData.content = html;
let flags = null;
if (rollOptions.damageFormula) {
flags = { mgt2: { damage: { formula: rollOptions.damageFormula, rollObjectName: rollOptions.rollObjectName, rollTypeName: rollOptions.rollTypeName } } };
if (effectiveDamageFormula) {
flags = { mgt2: { damage: { formula: effectiveDamageFormula, rollObjectName: rollOptions.rollObjectName, rollTypeName: rollOptions.rollTypeName } } };
}
if (cardButtons.length > 0) {
if (!flags) flags = { mgt2: {} };
@@ -816,6 +882,8 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
static async #onTraitDelete(event, target) {
event.preventDefault();
const confirmed = await TravellerCharacterSheet.#confirmDelete(game.i18n.localize("MGT2.Actor.ThisTrait"));
if (!confirmed) return;
const element = target.closest("[data-traits-part]");
const index = Number(element.dataset.traitsPart);
const traits = foundry.utils.deepClone(this.actor.system.personal.traits);
@@ -832,4 +900,323 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
this.actor.system.personal.speciesText.descriptionLong
);
}
static async #onHeal(event, target) {
event.preventDefault();
const healType = target.dataset.healType;
if (canvas.tokens.controlled.length === 0) {
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
return;
}
if (healType === "firstaid") {
// Find Medicine skill to pre-select
// Use normalized string matching to handle accents
const medSkill = this.actor.items.find(i => {
if (i.type !== "talent" || i.system.subType !== "skill") return false;
const normalized = i.name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return normalized.includes("medecin") || normalized.includes("medicine");
});
// Only EDU characteristic available for First Aid
const characteristics = [
{ _id: "education", name: game.i18n.localize("MGT2.Characteristics.education.name") }
];
const rollOptions = {
rollTypeName: game.i18n.localize("MGT2.Healing.FirstAid"),
rollObjectName: this.actor.name,
characteristics: characteristics, // Only EDU
characteristic: "education", // Pre-selected
skill: medSkill?.id ?? "", // Medicine skill ID for pre-selection (must match _id in array)
skillName: medSkill?.name ?? game.i18n.localize("MGT2.Healing.NoMedicineSkill"), // Display name
skillLevel: medSkill?.system.level ?? -3, // -3 if not found
skills: medSkill ? [{ _id: medSkill.id, name: medSkill.name, level: medSkill.system.level }] : [],
difficulty: "Average", // First Aid difficulty is 8 (Average)
showHeal: true,
healType: MGT2.HealingType.FIRST_AID,
};
const userRollData = await RollPromptHelper.roll(rollOptions);
if (userRollData) {
// Build formula with all DMs — same pattern as standard skill roll
const rollFormulaParts = [];
const rollModifiers = [];
if (userRollData.diceModifier) {
rollFormulaParts.push("3d6");
rollFormulaParts.push(userRollData.diceModifier);
} else {
rollFormulaParts.push("2d6");
}
if (userRollData.characteristic) {
const c = this.actor.system.characteristics[userRollData.characteristic];
rollFormulaParts.push(MGT2Helper.getFormulaDM(c.dm));
rollModifiers.push(game.i18n.localize(`MGT2.Characteristics.${userRollData.characteristic}.name`) + MGT2Helper.getDisplayDM(c.dm));
}
if (userRollData.skill && userRollData.skill !== "") {
if (userRollData.skill === "NP") {
rollFormulaParts.push("-3");
rollModifiers.push(game.i18n.localize("MGT2.Items.NotProficient"));
} else {
const skillObj = this.actor.getEmbeddedDocument("Item", userRollData.skill);
rollFormulaParts.push(MGT2Helper.getFormulaDM(skillObj.system.level));
rollModifiers.push(skillObj.getRollDisplay());
}
}
if (userRollData.customDM && userRollData.customDM !== "") {
let s = userRollData.customDM.trim();
if (/^[0-9]/.test(s)) rollFormulaParts.push("+");
rollFormulaParts.push(s);
rollModifiers.push("DM " + s);
}
const rollFormula = rollFormulaParts.join("");
const roll = await new Roll(rollFormula, this.actor.getRollData()).roll();
// Difficulty for First Aid is Average (8)
const difficulty = 8;
const effect = roll.total - difficulty;
const isSuccess = effect >= 0;
const healing = isSuccess ? Math.max(1, effect) : 0;
const cardButtons = isSuccess
? [{ label: game.i18n.localize("MGT2.Healing.ApplyHealing"), action: "healing" }]
: [];
const chatData = {
user: game.user.id,
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
formula: roll._formula,
tooltip: await roll.getTooltip(),
total: Math.round(roll.total * 100) / 100,
rollTypeName: game.i18n.localize("MGT2.Healing.FirstAid"),
rollObjectName: this.actor.name,
rollModifiers: rollModifiers,
rollDifficulty: difficulty,
rollDifficultyLabel: MGT2Helper.getDifficultyDisplay("Average"),
rollEffectStr: isSuccess ? effect.toString() : undefined,
healingAmount: isSuccess ? healing : undefined,
rollSuccess: isSuccess || undefined,
rollFailure: !isSuccess || undefined,
showButtons: isSuccess,
hasDamage: false,
showRollDamage: false,
cardButtons: cardButtons,
};
const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/roll.html", chatData);
chatData.content = html;
chatData.flags = { mgt2: { healing: { amount: healing } } };
return roll.toMessage(chatData);
}
} else if (healType === "surgery") {
// Find Medicine skill to pre-select (same as first aid)
const medSkill = this.actor.items.find(i => {
if (i.type !== "talent" || i.system.subType !== "skill") return false;
const normalized = i.name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return normalized.includes("medecin") || normalized.includes("medicine");
});
const characteristics = [
{ _id: "education", name: game.i18n.localize("MGT2.Characteristics.education.name") }
];
const rollOptions = {
rollTypeName: game.i18n.localize("MGT2.Healing.Surgery"),
rollObjectName: this.actor.name,
characteristics: characteristics,
characteristic: "education",
skill: medSkill?.id ?? "",
skillName: medSkill?.name ?? game.i18n.localize("MGT2.Healing.NoMedicineSkill"),
skillLevel: medSkill?.system.level ?? -3,
skills: medSkill ? [{ _id: medSkill.id, name: medSkill.name, level: medSkill.system.level }] : [],
difficulty: "Average",
showHeal: true,
healType: MGT2.HealingType.SURGERY,
};
const userRollData = await RollPromptHelper.roll(rollOptions);
if (userRollData) {
// Build formula with all DMs — same pattern as standard skill roll
const rollFormulaParts = [];
const rollModifiers = [];
if (userRollData.diceModifier) {
rollFormulaParts.push("3d6");
rollFormulaParts.push(userRollData.diceModifier);
} else {
rollFormulaParts.push("2d6");
}
if (userRollData.characteristic) {
const c = this.actor.system.characteristics[userRollData.characteristic];
rollFormulaParts.push(MGT2Helper.getFormulaDM(c.dm));
rollModifiers.push(game.i18n.localize(`MGT2.Characteristics.${userRollData.characteristic}.name`) + MGT2Helper.getDisplayDM(c.dm));
}
if (userRollData.skill && userRollData.skill !== "") {
if (userRollData.skill === "NP") {
rollFormulaParts.push("-3");
rollModifiers.push(game.i18n.localize("MGT2.Items.NotProficient"));
} else {
const skillObj = this.actor.getEmbeddedDocument("Item", userRollData.skill);
rollFormulaParts.push(MGT2Helper.getFormulaDM(skillObj.system.level));
rollModifiers.push(skillObj.getRollDisplay());
}
}
if (userRollData.customDM && userRollData.customDM !== "") {
let s = userRollData.customDM.trim();
if (/^[0-9]/.test(s)) rollFormulaParts.push("+");
rollFormulaParts.push(s);
rollModifiers.push("DM " + s);
}
const rollFormula = rollFormulaParts.join("");
const roll = await new Roll(rollFormula, this.actor.getRollData()).roll();
// Difficulty for Surgery is Average (8)
const difficulty = 8;
const effect = roll.total - difficulty;
const isSuccess = effect >= 0;
// Success: heal Math.max(1, effect); Failure: patient takes 3 + |effect| damage
const healing = isSuccess ? Math.max(1, effect) : 0;
const surgeryDamage = isSuccess ? 0 : 3 + Math.abs(effect);
const cardButtons = [];
if (isSuccess) {
cardButtons.push({ label: game.i18n.localize("MGT2.Healing.ApplyHealing"), action: "healing" });
} else {
cardButtons.push({ label: game.i18n.localize("MGT2.Healing.ApplySurgeryDamage"), action: "surgeryDamage" });
}
const chatData = {
user: game.user.id,
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
formula: roll._formula,
tooltip: await roll.getTooltip(),
total: Math.round(roll.total * 100) / 100,
rollTypeName: game.i18n.localize("MGT2.Healing.Surgery"),
rollObjectName: this.actor.name,
rollModifiers: rollModifiers,
rollDifficulty: difficulty,
rollDifficultyLabel: MGT2Helper.getDifficultyDisplay("Average"),
rollEffectStr: effect.toString(),
healingAmount: isSuccess ? healing : undefined,
surgeryDamageAmount: isSuccess ? undefined : surgeryDamage,
rollSuccess: isSuccess || undefined,
rollFailure: !isSuccess || undefined,
showButtons: true,
hasDamage: false,
showRollDamage: false,
cardButtons: cardButtons,
};
const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/roll.html", chatData);
chatData.content = html;
chatData.flags = { mgt2: { surgery: { healing, surgeryDamage } } };
return roll.toMessage(chatData);
}
} else if (healType === "medical") {
const result = await CharacterPrompts.openHealingDays();
if (result) {
const endMD = this.actor.system.characteristics.endurance.dm;
const medSkill = this.actor.items.find(i =>
i.type === "talent" &&
i.system.subType === "skill" &&
(i.name.toLowerCase().includes("medecin") || i.name.toLowerCase().includes("medicine"))
);
const skillValue = medSkill ? medSkill.system.level : 0;
const days = result.days;
const healingPerDay = Math.max(1, 3 + endMD + skillValue);
const totalHealing = healingPerDay * days;
const rollModifiers = [
`3 (base)`,
`${endMD >= 0 ? "+" : ""}${endMD} END`,
`+${skillValue} ${medSkill?.name ?? "Médecine"}`,
`× ${days} ${game.i18n.localize("MGT2.RollPrompt.Days").toLowerCase()}`
];
const templateData = {
rollObjectName: this.actor.name,
rollTypeName: game.i18n.localize("MGT2.Healing.MedicalCare"),
rollModifiers,
formula: `${healingPerDay} ${game.i18n.localize("MGT2.Items.PerDay")}`,
tooltip: "",
total: totalHealing,
rollSuccess: true,
showButtons: true,
cardButtons: [
{ action: "healing", label: game.i18n.localize("MGT2.Healing.ApplyHealing") }
]
};
const content = await renderTemplate(
"systems/mgt2/templates/chat/roll.html",
templateData
);
await ChatMessage.create({
user: game.user.id,
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
content,
flags: { mgt2: { healing: { amount: totalHealing } } }
});
}
} else if (healType === "natural") {
const result = await CharacterPrompts.openHealingDays();
if (result) {
const endMD = this.actor.system.characteristics.endurance.dm;
let totalAmount = 0;
const rolls = [];
for (let i = 0; i < result.days; i++) {
const roll = await new Roll("1d6").evaluate();
const dayHealing = Math.max(1, roll.total + endMD);
rolls.push({ roll, dayHealing });
totalAmount += dayHealing;
}
// Build roll details
const rollDisplay = rolls.map((r, idx) =>
`<div><strong>${game.i18n.localize("MGT2.RollPrompt.Days")} ${idx + 1}:</strong> 1d6 = ${r.roll.total} + ${endMD > 0 ? "+" : ""}${endMD} = <strong>${r.dayHealing}</strong></div>`
).join("");
const chatData = {
user: game.user.id,
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
content: `<div class="mgt2-chat-roll">
<div class="mgt2-roll-header">
<span class="mgt2-roll-char-name">${this.actor.name}</span>
<div class="mgt2-roll-meta">
<span class="mgt2-roll-type">${game.i18n.localize("MGT2.Healing.NaturalHealing")}</span>
</div>
</div>
<div class="mgt2-roll-modifier">${result.days} ${game.i18n.localize("MGT2.RollPrompt.Days")}</div>
<div style="padding: 8px 0; font-size: 0.9em;">
${rollDisplay}
</div>
<div class="mgt2-effect is-success">
${game.i18n.localize("MGT2.Chat.Roll.Effect")} <span class="mgt2-effect-value">${totalAmount}</span>
</div>
</div>`,
flags: { mgt2: { healing: { amount: totalAmount } } }
};
await ChatMessage.create(chatData);
// Apply healing immediately
await this.actor.applyHealing(totalAmount);
ui.notifications.info(
game.i18n.format("MGT2.Notifications.HealingApplied",
{ name: this.actor.name, amount: totalAmount })
);
}
}
}
}

View File

@@ -1,4 +1,8 @@
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) {
@@ -46,13 +50,16 @@ export default class TravellerCreatureSheet extends MGT2ActorSheet {
}
/** @override */
tabGroups = { primary: "skills" }
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;
@@ -88,101 +95,36 @@ export default class TravellerCreatureSheet extends MGT2ActorSheet {
return "Grand (+6)";
}
// ───────────────────────────────────────────────────────── Roll Handlers
// ───────────────────────────────────────────────────────── Roll Helpers
/** Roll an attack (damage) with optional difficulty dialog */
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;
static async #postCreatureRoll({ actor, roll, rollLabel, dm, difficulty, difficultyLabel, rollMode, extraTooltip }) {
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 rollFormula = normalizeDice(attack.damage);
const roll = await new Roll(rollFormula).evaluate();
const total = roll.total;
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor }),
flavor: `<strong>${actor.name}</strong> — ${attack.name}`,
rollMode: game.settings.get("core", "rollMode"),
});
}
/** Roll a skill check (2d6 + level vs difficulty) */
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 htmlContent = await renderTemplate(
"systems/mgt2/templates/actors/creature-roll-prompt.html",
{
skillName: skill.name,
skillLevel: skill.level,
config: CONFIG.MGT2
}
);
const result = await foundry.applications.api.DialogV2.wait({
window: { title: game.i18n.localize("MGT2.Creature.RollSkill") + " — " + skill.name },
content: htmlContent,
rejectClose: false,
buttons: [
{
action: "boon",
label: game.i18n.localize("MGT2.RollPrompt.Boon"),
callback: (event, button, dialog) => {
const fd = new foundry.applications.ux.FormDataExtended(dialog.element.querySelector("form")).object;
fd.diceModifier = "dl";
return fd;
}
},
{
action: "roll",
label: game.i18n.localize("MGT2.RollPrompt.Roll"),
icon: '<i class="fa-solid fa-dice"></i>',
default: true,
callback: (event, button, dialog) =>
new foundry.applications.ux.FormDataExtended(dialog.element.querySelector("form")).object
},
{
action: "bane",
label: game.i18n.localize("MGT2.RollPrompt.Bane"),
callback: (event, button, dialog) => {
const fd = new foundry.applications.ux.FormDataExtended(dialog.element.querySelector("form")).object;
fd.diceModifier = "dh";
return fd;
}
}
]
});
if (!result) return;
const dm = parseInt(result.dm ?? 0) + (skill.level ?? 0);
const modifier = result.diceModifier ?? "";
const difficultyTarget = parseInt(result.difficulty ?? 8);
const difficultyLabel = result.difficultyLabel ?? "";
const diceFormula = modifier ? `3d6${modifier}` : "2d6";
const fullFormula = dm !== 0 ? `${diceFormula} + ${dm}` : diceFormula;
const roll = await new Roll(fullFormula).evaluate();
const success = roll.total >= difficultyTarget;
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 chatData = {
creatureName: actor.name,
creatureImg: actor.img,
rollLabel: skill.name.toUpperCase(),
formula: fullFormula,
rollLabel,
formula: roll.formula,
total: roll.total,
tooltip: await roll.getTooltip(),
difficulty: difficultyTarget,
difficultyLabel,
success,
failure: !success,
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}`] : [],
};
@@ -195,8 +137,125 @@ export default class TravellerCreatureSheet extends MGT2ActorSheet {
content: chatContent,
speaker: ChatMessage.getSpeaker({ actor }),
rolls: [roll],
rollMode: game.settings.get("core", "rollMode"),
rollMode: rollMode ?? game.settings.get("core", "rollMode"),
});
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}`);
const { success } = await TravellerCreatureSheet.#postCreatureRoll({
actor, roll, rollLabel,
dm,
difficulty: result.difficulty,
rollMode: result.rollMode,
extraTooltip: tooltipParts.join(" | "),
});
// Roll damage only on success
if (success && attack.damage) {
const dmgFormula = normalizeDice(attack.damage);
const dmgRoll = await new Roll(dmgFormula).evaluate();
await dmgRoll.toMessage({
speaker: ChatMessage.getSpeaker({ actor }),
flavor: `<strong>${actor.name}</strong> — ${game.i18n.localize("MGT2.Chat.Weapon.Damage")}: ${attack.name} (${attack.damage})`,
rollMode: result.rollMode ?? game.settings.get("core", "rollMode"),
});
}
}
// ───────────────────────────────────────────────────────── CRUD Handlers
@@ -223,7 +282,7 @@ export default class TravellerCreatureSheet extends MGT2ActorSheet {
_getDefaultRow(prop) {
switch (prop) {
case "skills": return { name: "", level: 0, note: "" };
case "attacks": return { name: "", damage: "1D", description: "" };
case "attacks": return { name: "", damage: "1D", skill: -1, description: "" };
case "traits": return { name: "", value: "", description: "" };
default: return {};
}

View File

@@ -73,6 +73,8 @@ export default class TravellerItemSheet extends HandlebarsApplicationMixin(found
skills.sort(MGT2Helper.compareByName);
skills = [{ _id: "NP", name: game.i18n.localize("MGT2.Items.NotProficient") }].concat(skills);
const enrich = (html) => foundry.applications.ux.TextEditor.implementation.enrichHTML(html ?? "", { async: true });
return {
item: item,
document: item,
@@ -91,6 +93,10 @@ export default class TravellerItemSheet extends HandlebarsApplicationMixin(found
weight: weight,
unitlabels: { weight: MGT2Helper.getWeightLabel() },
skills: skills,
enrichedDescription: await enrich(item.system.description),
enrichedDescriptionLong: await enrich(item.system.descriptionLong),
enrichedNotes: await enrich(item.system.notes),
enrichedLockedDescription: await enrich(item.system.lockedDescription),
};
}
@@ -126,6 +132,27 @@ export default class TravellerItemSheet extends HandlebarsApplicationMixin(found
bind(".options-delete", TravellerItemSheet.#onOptionDelete);
bind(".modifiers-create", TravellerItemSheet.#onModifierCreate);
bind(".modifiers-delete", TravellerItemSheet.#onModifierDelete);
// Activate ProseMirror editors for HTMLField fields
for (const btn of html.querySelectorAll(".editor-edit")) {
btn.addEventListener("click", async (event) => {
event.preventDefault();
const editorWrapper = btn.closest(".editor");
if (!editorWrapper) return;
const editorContent = editorWrapper.querySelector(".editor-content");
if (!editorContent || editorContent.classList.contains("ProseMirror")) return;
const target = editorContent.dataset.edit;
const value = foundry.utils.getProperty(this.document, target) ?? "";
btn.remove();
editorWrapper.classList.add("prosemirror");
await ProseMirrorEditor.create(editorContent, value, {
document: this.document,
fieldName: target,
plugins: {},
collaborate: false,
});
});
}
}
_activateTabGroups() {

View File

@@ -21,4 +21,15 @@ export default class TravellerVehiculeSheet extends MGT2ActorSheet {
/** @override */
tabGroups = { primary: "stats" }
/** @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.enrichedDescription = await enrich(actor.system.description);
context.enrichedNotes = await enrich(actor.system.notes);
return context;
}
}