Ajout des fonctions de gestion des soins
This commit is contained in:
@@ -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 })
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user