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

@@ -52,9 +52,32 @@ export class TravellerActor extends Actor {
}
}
applyDamage(amount) {
applyDamage(amount, { ignoreArmor = false } = {}) {
if (this.type === "character") {
ActorCharacter.applyDamage(this, amount);
return ActorCharacter.applyDamage(this, amount, { ignoreArmor });
} else if (this.type === "creature") {
if (isNaN(amount) || amount === 0) return;
if (amount < 0) amount = Math.abs(amount);
const armorValue = ignoreArmor ? 0 : (this.system.armor ?? 0);
const effective = Math.max(0, amount - armorValue);
if (effective === 0) return;
const newValue = Math.max(0, (this.system.life.value ?? 0) - effective);
return this.update({ "system.life.value": newValue });
}
}
applyHealing(amount) {
if (this.type === "character") {
return ActorCharacter.applyHealing(this, amount);
} else if (this.type === "creature") {
if (isNaN(amount) || amount === 0) return;
if (amount < 0) amount = Math.abs(amount);
const maxValue = this.system.life.max ?? 0;
const current = this.system.life.value ?? 0;
const newValue = Math.min(current + amount, maxValue);
if (newValue !== current) {
return this.update({ "system.life.value": newValue });
}
}
}

View File

@@ -70,4 +70,35 @@ export class CharacterPrompts {
]
});
}
static async openHealingDays() {
return await DialogV2.wait({
window: {
title: game.i18n.localize("MGT2.Healing.Title")
},
classes: ["mgt2-roll-dialog"],
content: `
<form>
<div style="padding: 12px;">
<div class="form-group">
<label>${game.i18n.localize("MGT2.RollPrompt.Days") || "Jours"}</label>
<input type="number" name="days" value="1" min="1" max="999" />
</div>
</div>
</form>
`,
rejectClose: false,
buttons: [
{
action: "submit",
label: game.i18n.localize("MGT2.Save"),
icon: '<i class="fa-solid fa-floppy-disk"></i>',
default: true,
callback: (event, button, dialog) => {
return new FormDataExtended(dialog.element.querySelector('form')).object;
}
}
]
});
}
}

View File

@@ -331,7 +331,7 @@ export class ActorCharacter {
// $this.update({ system: { characteristics: data } });
// }
static applyDamage($this, amount) {
static applyDamage($this, amount, { ignoreArmor = false } = {}) {
if (isNaN(amount) || amount === 0) return;
const rank1 = $this.system.config.damages.rank1;
const rank2 = $this.system.config.damages.rank2;
@@ -344,6 +344,12 @@ export class ActorCharacter {
if (amount < 0) amount = Math.abs(amount);
if (!ignoreArmor) {
const armorValue = $this.system.inventory?.armor ?? 0;
amount = Math.max(0, amount - armorValue);
if (amount === 0) return;
}
for (const [key, rank] of Object.entries(data)) {
if (rank.value > 0) {
if (rank.value >= amount) {
@@ -361,6 +367,48 @@ export class ActorCharacter {
$this.update({ system: { characteristics: data } });
}
static applyHealing($this, amount, type) {
if (isNaN(amount) || amount === 0) return;
const rank1 = $this.system.config.damages.rank1;
const rank2 = $this.system.config.damages.rank2;
const rank3 = $this.system.config.damages.rank3;
// Data to restore (reverse cascade: END → DEX → STR)
const data = {};
const rankOrder = [rank3, rank2, rank1]; // Reverse order for healing
const maxValues = {
[rank1]: $this.system.characteristics[rank1].max,
[rank2]: $this.system.characteristics[rank2].max,
[rank3]: $this.system.characteristics[rank3].max
};
if (amount < 0) amount = Math.abs(amount);
// Distribute healing from lowest rank first (END → DEX → STR typically)
for (const rank of rankOrder) {
const current = $this.system.characteristics[rank].value;
const max = maxValues[rank];
if (current < max && amount > 0) {
const canRestore = max - current;
const restore = Math.min(amount, canRestore);
if (!data[rank]) {
data[rank] = { value: current };
}
data[rank].value += restore;
data[rank].dm = this.getModifier(data[rank].value);
amount -= restore;
}
}
// Only update if something was restored
if (Object.keys(data).length > 0) {
return $this.update({ system: { characteristics: data } });
}
}
static getContainers($this) {
const containers = [];
for (let item of $this.items) {

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

View File

@@ -20,11 +20,17 @@ export class ChatHelper {
element.querySelectorAll('button[data-action="healing"]').forEach(el => {
el.addEventListener('click', async event => {
ui.notifications.warn("healing");
await this._applyChatCardHealing(message, event);
});
});
element.querySelectorAll('button[data-index]').forEach(el => {
element.querySelectorAll('button[data-action="surgeryDamage"]').forEach(el => {
el.addEventListener('click', async event => {
await this._applyChatCardSurgeryDamage(message, event);
});
});
element.querySelectorAll('button[data-index]:not([data-action])').forEach(el => {
el.addEventListener('click', async event => {
await this._processRollButtonEvent(message, event);
});
@@ -90,10 +96,41 @@ export class ChatHelper {
}
static _applyChatCardDamage(message, event) {
if (canvas.tokens.controlled.length === 0) {
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
return;
}
const roll = message.rolls[0];
return Promise.all(canvas.tokens.controlled.map(t => {
const a = t.actor;
return a.applyDamage(roll.total);
}));
}
static _applyChatCardHealing(message, event) {
if (canvas.tokens.controlled.length === 0) {
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
return;
}
// For First Aid/Surgery healing, use amount from flags; otherwise use roll total
const amount = message.flags?.mgt2?.healing?.amount
?? message.flags?.mgt2?.surgery?.healing
?? Math.max(1, message.rolls[0].total);
return Promise.all(canvas.tokens.controlled.map(t => {
const a = t.actor;
return a.applyHealing(amount);
}));
}
static _applyChatCardSurgeryDamage(message, event) {
if (canvas.tokens.controlled.length === 0) {
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
return;
}
const amount = message.flags?.mgt2?.surgery?.surgeryDamage ?? 3;
return Promise.all(canvas.tokens.controlled.map(t => {
const a = t.actor;
return a.applyDamage(amount, { ignoreArmor: true });
}));
}
}

View File

@@ -143,7 +143,7 @@ MGT2.SpeedBands = Object.freeze({
VerySlow: "MGT2.SpeedBands.VerySlow",
Slow: "MGT2.SpeedBands.Slow",
Medium: "MGT2.SpeedBands.Medium",
High: "MGT2.SpeedBands.High.",
High: "MGT2.SpeedBands.High",
Fast: "MGT2.SpeedBands.Fast",
VeryFast: "MGT2.SpeedBands.VeryFast",
Subsonic: "MGT2.SpeedBands.Subsonic",
@@ -177,4 +177,11 @@ MGT2.CreatureBehaviorSubType = Object.freeze({
necrophage: "MGT2.CreatureBehaviorSubType.necrophage",
reducteur: "MGT2.CreatureBehaviorSubType.reducteur",
opportuniste: "MGT2.CreatureBehaviorSubType.opportuniste"
});
MGT2.HealingType = Object.freeze({
FIRST_AID: "MGT2.Healing.FirstAid",
SURGERY: "MGT2.Healing.Surgery",
MEDICAL_CARE: "MGT2.Healing.MedicalCare",
NATURAL_HEALING: "MGT2.Healing.NaturalHealing"
});

View File

@@ -47,7 +47,9 @@ export default class CharacterData extends foundry.abstract.TypeDataModel {
}),
health: new fields.SchemaField({
radiations: new fields.NumberField({ required: false, initial: 0, min: 0, integer: true })
radiations: new fields.NumberField({ required: false, initial: 0, min: 0, integer: true }),
lastFirstAidDate: new fields.StringField({ required: false, blank: true, trim: true }),
healingRecoveryMode: new fields.StringField({ required: false, blank: true, trim: true, initial: "" })
}),
study: new fields.SchemaField({
skill: new fields.StringField({ required: false, blank: true, trim: true, initial: "" }),
@@ -61,7 +63,7 @@ export default class CharacterData extends foundry.abstract.TypeDataModel {
debt: new fields.NumberField({ required: true, initial: 0, min: 0, integer: true }),
livingCost: new fields.NumberField({ required: true, initial: 0, min: 0, integer: true }),
monthlyShipPayments: new fields.NumberField({ required: true, initial: 0, min: 0, integer: true }),
notes: new fields.StringField({ required: false, blank: true, trim: true, initial: "" })
notes: new fields.HTMLField({ required: false, blank: true, trim: true })
}),
containerView: new fields.StringField({ required: false, blank: true, trim: true, initial: "" }),
containerDropIn: new fields.StringField({ required: false, blank: true, trim: true, initial: "" }),

View File

@@ -29,6 +29,7 @@ export default class CreatureData extends foundry.abstract.TypeDataModel {
new fields.SchemaField({
name: new fields.StringField({ required: true, blank: true, trim: true, initial: "" }),
damage: new fields.StringField({ required: true, blank: true, trim: true, initial: "1D" }),
skill: new fields.NumberField({ required: false, initial: -1, integer: true }),
description: new fields.StringField({ required: false, blank: true, trim: true, initial: "" })
})
),

View File

@@ -13,7 +13,7 @@ export function createCharacteristicField(show = true, showMax = false) {
export class ItemBaseData extends foundry.abstract.TypeDataModel {
static defineSchema() {
return {
description: new fields.StringField({ required: false, blank: true, trim: true, nullable: true }),
description: new fields.HTMLField({ required: false, blank: true, trim: true }),
subType: new fields.StringField({ required: false, blank: false, nullable: true })
};
}

View File

@@ -10,7 +10,7 @@ export default class ItemContainerData extends ItemBaseData {
schema.weight = new fields.NumberField({ required: false, initial: 0, integer: false });
schema.weightless = new fields.BooleanField({ required: false, initial: false });
schema.locked = new fields.BooleanField({ required: false, initial: false }); // GM only
schema.lockedDescription = new fields.StringField({ required: false, blank: true, trim: true, nullable: true });
schema.lockedDescription = new fields.HTMLField({ required: false, blank: true, trim: true });
return schema;
}
}

View File

@@ -3,7 +3,7 @@ const fields = foundry.data.fields;
export default class SpeciesData extends foundry.abstract.TypeDataModel {
static defineSchema() {
return {
description: new fields.StringField({ required: false, blank: true, trim: true, nullable: true }),
description: new fields.HTMLField({ required: false, blank: true, trim: true }),
descriptionLong: new fields.HTMLField({ required: false, blank: true, trim: true }),
traits: new fields.ArrayField(
new fields.SchemaField({

View File

@@ -27,7 +27,9 @@ export default class VehiculeData extends foundry.abstract.TypeDataModel {
}),
skills: new fields.SchemaField({
autopilot: new fields.NumberField({ required: true, initial: 0, integer: true })
})
}),
description: new fields.HTMLField({ required: false, blank: true, trim: true }),
notes: new fields.HTMLField({ required: false, blank: true, trim: true })
};
}
}

View File

@@ -5,19 +5,39 @@ const { FormDataExtended } = foundry.applications.ux;
export class RollPromptHelper {
static async roll(options) {
// Backward compat: allow (actor, options) or just (options)
if (options.rollTypeName || options.characteristics || options.skill !== undefined) {
// Normal call with options
} else {
// Called with (actor, options)
options = arguments[1] || options;
}
const htmlContent = await renderTemplate('systems/mgt2/templates/roll-prompt.html', {
config: CONFIG.MGT2,
characteristics: options.characteristics,
characteristic: options.characteristic,
skills: options.skills,
skill: options.skill,
fatigue: options.fatigue,
encumbrance: options.encumbrance,
difficulty: options.difficulty
// Character-mode fields
characteristics: options.characteristics ?? [],
characteristic: options.characteristic ?? "",
skills: options.skills ?? [],
skill: options.skill ?? "",
fatigue: options.fatigue ?? false,
encumbrance: options.encumbrance ?? false,
difficulty: options.difficulty ?? "Average",
timeframe: options.timeframe ?? "Normal",
customDM: options.customDM ?? "0",
rollMode: options.rollMode ?? "publicroll",
// Creature-mode flags
isCreature: options.isCreature ?? false,
creatureSkills: options.creatureSkills ?? [],
selectedSkillIndex: options.selectedSkillIndex ?? -1,
showSkillSelector: options.showSkillSelector ?? false,
skillName: options.skillName ?? "",
skillLevel: options.skillLevel ?? 0,
// Healing fields
showHeal: options.showHeal ?? false,
healType: options.healType ?? null
});
const theme = game.settings.get("mgt2", "theme");
return await DialogV2.wait({
window: { title: options.title ?? options.rollTypeName ?? game.i18n.localize("MGT2.RollPrompt.Roll") },
classes: ["mgt2-roll-dialog"],
@@ -54,4 +74,4 @@ export class RollPromptHelper {
]
});
}
}
}

View File

@@ -19,6 +19,7 @@ export const preloadHandlebarsTemplates = async function() {
"systems/mgt2/templates/items/weapon-sheet.html",
"systems/mgt2/templates/items/parts/sheet-configuration.html",
"systems/mgt2/templates/items/parts/sheet-physical-item.html",
"systems/mgt2/templates/items/parts/sheet-physical-item-tab.html",
"systems/mgt2/templates/roll-prompt.html",
"systems/mgt2/templates/chat/roll.html",
//"systems/mgt2/templates/chat/roll-characteristic.html",
@@ -26,10 +27,10 @@ export const preloadHandlebarsTemplates = async function() {
"systems/mgt2/templates/actors/actor-config-characteristic-sheet.html",
"systems/mgt2/templates/actors/trait-sheet.html",
"systems/mgt2/templates/actors/creature-sheet.html",
"systems/mgt2/templates/actors/creature-roll-prompt.html",
"systems/mgt2/templates/chat/creature-roll.html",
"systems/mgt2/templates/editor-fullview.html"
];
return loadTemplates(templatePaths);
const loader = foundry.applications?.handlebars?.loadTemplates ?? loadTemplates;
return loader(templatePaths);
};

View File

@@ -213,6 +213,7 @@ ul
flex-direction: row
align-content: flex-start
flex-wrap: nowrap
min-height: 330px
.tab
width: 100%
@@ -277,3 +278,15 @@ ul
flex-shrink: 0
display: flex
flex-direction: row
// HTMLField editor min-height in notes/biography/finance tabs
.mgt2.character
.tab[data-tab="notes"],
.tab[data-tab="biography"],
.tab[data-tab="finance"]
.editor,
.editor-content,
prose-mirror
min-height: 300px !important
height: auto !important

View File

@@ -294,6 +294,41 @@ li.chat-message
color: #EE4050
border-top: 1px solid rgba(238,64,80,0.2)
// Effect line
.mgt2-effect
text-align: center
font-size: 0.72rem
font-weight: 600
letter-spacing: 1.5px
text-transform: uppercase
padding: 2px 10px 4px 10px
border-bottom: 2px solid transparent
.mgt2-effect-value
font-size: 1rem
font-weight: 900
margin-left: 4px
.mgt2-healing-amount
font-size: 0.75rem
font-weight: 600
margin-left: 6px
opacity: 0.85
&.is-success
color: #1a8840
border-bottom-color: rgba(26,136,64,0.25)
.mgt2-effect-value
color: #1a8840
&.is-failure
color: #EE4050
border-bottom-color: rgba(238,64,80,0.25)
.mgt2-effect-value
color: #EE4050
// Action buttons
.mgt2-buttons
display: flex

View File

@@ -105,15 +105,19 @@
flex-direction: row
align-items: center
gap: 0.5rem
flex-wrap: wrap
flex-wrap: nowrap
label
flex: 0 0 auto
font-size: 0.75rem
text-transform: uppercase
color: var(--mgt2-color-primary)
font-weight: 700
white-space: nowrap
.behavior-select
flex: 1 1 auto
min-width: 0
background: var(--mgt2-bgcolor-form)
color: var(--mgt2-color-form)
border: 1px solid var(--mgt2-color-primary)
@@ -122,24 +126,29 @@
padding: 1px 4px
.behavior-sep
flex: 0 0 auto
color: var(--mgt2-color-form)
opacity: 0.5
.creature-size-badge
margin-left: auto
flex: 0 0 auto
white-space: nowrap
font-size: 0.75rem
font-style: italic
color: var(--mgt2-color-secondary)
color: var(--mgt2-bgcolor-form)
background: var(--mgt2-bgcolor-primary)
border: 1px solid var(--mgt2-color-secondary)
border: 1px solid var(--mgt2-color-primary)
border-radius: 3px
padding: 1px 6px
// Body / Tabs
// min-height ensures all 4 sidebar tabs are always visible
// (4 tabs × 54px each + 8px padding = 224px)
.creature-body
flex: 1
overflow-y: auto
padding: 0.5rem 0.75rem
min-height: 228px
.tab
display: none
@@ -199,3 +208,18 @@
.mgt2-roll-header-text
flex: 1
// ── Section headers: use shared .header class (same as character sheet)
// HTMLField editor min-height in info tab
.mgt2.creature
// Section headers (skills/attacks/traits) margin between successive sections
.table-container + .header
margin-top: 8px
.tab[data-tab="info"]
.editor,
.editor-content,
prose-mirror
min-height: 300px !important
height: auto !important

View File

@@ -31,15 +31,15 @@
.dialog-content, .standard-form
background: #ffffff !important
padding: 14px 18px 10px !important
padding: 8px 14px 6px !important
// Form group rows
.form-group
display: flex !important
align-items: center !important
gap: 10px !important
margin-bottom: 8px !important
padding: 4px 0 !important
gap: 8px !important
margin-bottom: 3px !important
padding: 2px 0 !important
border-bottom: 1px solid #e8e0e0 !important
&:last-child
@@ -61,7 +61,7 @@
border: 1px solid #ccbbbb !important
color: #0A0405 !important
border-radius: 3px !important
padding: 5px 10px !important
padding: 3px 8px !important
font-family: 'Barlow Condensed', sans-serif !important
font-size: 0.9rem !important
transition: border-color 150ms ease !important
@@ -80,8 +80,8 @@
background: #fdf8f8 !important
border: 1px solid #e0c8c8 !important
border-radius: 5px !important
padding: 10px 14px !important
margin-bottom: 8px !important
padding: 5px 10px !important
margin-bottom: 4px !important
legend
color: #EE4050 !important
@@ -112,7 +112,7 @@
.dialog-buttons, .form-footer, footer
background: #f5eeee !important
border-top: 2px solid #EE4050 !important
padding: 10px 14px !important
padding: 7px 14px !important
display: flex !important
gap: 8px !important
justify-content: center !important
@@ -124,7 +124,7 @@
border: 1px solid #ccbbbb !important
color: #3a2020 !important
border-radius: 4px !important
padding: 7px 14px !important
padding: 5px 12px !important
font-family: 'Barlow Condensed', sans-serif !important
font-size: 0.82rem !important
font-weight: 700 !important

View File

@@ -343,3 +343,76 @@
.itemsheet-panel
display: contents !important
// ── Details tab: 2-column grid layout
.itemsheet
.item-details-grid
display: grid !important
grid-template-columns: 1fr 1fr !important
gap: 4px 16px !important
align-items: start !important
// Traits table spans full width
.table-container
grid-column: 1 / -1 !important
margin-top: 10px !important
// ── Field row: label + input on the same line
.itemsheet
.field-row
display: flex !important
align-items: center !important
gap: 8px !important
min-height: 28px !important
label
flex: 0 0 100px !important
min-width: 0 !important
margin-bottom: 0 !important
white-space: nowrap !important
overflow: hidden !important
text-overflow: ellipsis !important
input[type="text"],
input[type="number"],
select
flex: 1 !important
width: auto !important
height: 24px !important
padding: 2px 6px !important
input.short
flex: 0 0 56px !important
width: 56px !important
.range-inputs
display: flex !important
gap: 4px !important
flex: 1 !important
input
flex: 0 0 52px !important
width: 52px !important
select
flex: 1 !important
// Full-width row (e.g. storage)
&.full
grid-column: 1 / -1 !important
// Checkbox row variant
.field-row--check
label
flex: unset !important
display: flex !important
align-items: center !important
gap: 6px !important
cursor: pointer !important
// ── Description tab: editor min-height
.itemsheet
.tab[data-tab="tab1"]
.editor,
.editor-container
min-height: 200px !important

View File

@@ -3,7 +3,7 @@
// nav (left: 100% of character-body) can extend to the right of the window border.
// Layered !important beats Foundry's unlayered overflow:hidden per CSS cascade spec.
.mgt2.character, .mgt2.creature
.mgt2.character, .mgt2.creature, .mgt2.vehicule
overflow: visible !important
> .window-content
overflow: visible !important
@@ -18,13 +18,16 @@
position: relative !important
overflow: visible !important
.mgt2.vehicule .vehicule-content
position: relative !important
overflow: visible !important
// Vertical sidebar tab navigation (outside window, right side)
.mgt2
nav.sheet-sidebar.tabs
position: absolute !important
left: 100% !important
top: 0 !important
bottom: 0 !important
width: 62px !important
flex: none !important
display: flex !important

View File

@@ -0,0 +1,203 @@
//
// Vehicule Sheet Styles
//
.vehicule-sheet
// Header
.vehicule-header
display: flex
flex-direction: row
align-items: flex-start
gap: 0.75rem
padding: 0.5rem 0.75rem
background: var(--mgt2-bgcolor-form)
border-bottom: 2px solid var(--mgt2-color-primary)
flex-shrink: 0
.vehicule-header-img
flex: 0 0 90px
img.profile
width: 90px
height: 90px
object-fit: cover
border: 2px solid var(--mgt2-color-primary)
border-radius: 4px
cursor: pointer
.vehicule-header-body
flex: 1
display: flex
flex-direction: column
gap: 0.4rem
min-width: 0
.vehicule-name
font-family: "Barlow Condensed", sans-serif
font-size: 1.6rem
font-weight: 700
font-style: italic
color: var(--mgt2-color-form)
background: transparent
border: none
border-bottom: 1px solid var(--mgt2-color-primary)
width: 100%
padding: 0
&:focus
outline: none
border-bottom-color: var(--mgt2-color-secondary)
.vehicule-header-stats
display: flex
flex-direction: row
align-items: flex-start
gap: 0.75rem
flex-wrap: wrap
// Stat boxes (hull, armor)
.vehicule-stat-box
display: flex
flex-direction: column
align-items: center
background: var(--mgt2-bgcolor-primary)
border: 1px solid var(--mgt2-color-primary)
border-radius: 4px
padding: 3px 8px
min-width: 4rem
label
font-family: "Barlow Condensed", sans-serif
font-size: 0.65rem
font-weight: 700
text-transform: uppercase
color: var(--mgt2-color-primary)
line-height: 1.2
white-space: nowrap
.vehicule-stat-value
display: flex
align-items: center
gap: 2px
span
color: var(--mgt2-color-primary)
font-weight: 700
input[type="number"]
width: 2.8rem
text-align: center
background: transparent
border: none
color: var(--mgt2-color-form)
font-family: "Rubik", monospace
font-size: 1rem
font-weight: 600
padding: 0
&:focus
outline: none
border-bottom: 1px solid var(--mgt2-color-secondary)
.vehicule-hull
min-width: 6rem
.vehicule-stat-value input[type="number"]
width: 2.5rem
// Armor group
.vehicule-armor-group
display: flex
flex-direction: column
gap: 2px
.vehicule-armor-label
font-family: "Barlow Condensed", sans-serif
font-size: 0.65rem
font-weight: 700
text-transform: uppercase
color: var(--mgt2-color-primary)
text-align: center
.vehicule-armor-row
display: flex
flex-direction: row
gap: 4px
.vehicule-armor-box
min-width: 3.5rem
// Body wrapper (contains tabs + sidebar nav)
// min-height ensures both tabs in the sidebar are always visible
// (2 tabs × 54px each + 8px padding = 116px)
.vehicule-content
flex: 1
display: flex
flex-direction: column
overflow: hidden
min-height: 320px
// Tab panels
.vehicule-tab
flex: 1
overflow-y: auto
padding: 0.75rem
display: none
&.active
display: block
// Stats grid
.vehicule-stats-grid
display: grid
grid-template-columns: 1fr 1fr
gap: 4px 12px
.vehicule-field
display: flex
flex-direction: row
align-items: center
gap: 8px
padding: 3px 0
border-bottom: 1px solid rgba(0,0,0,0.08)
&:last-child
border-bottom: none
label
font-family: "Barlow Condensed", sans-serif
font-size: 0.72rem
font-weight: 700
text-transform: uppercase
color: var(--mgt2-color-primary)
flex: 0 0 120px
white-space: nowrap
input[type="number"],
select
flex: 1
background: transparent
border: 1px solid transparent
border-radius: 3px
color: var(--mgt2-color-form)
font-family: "Barlow Condensed", sans-serif
font-size: 0.9rem
padding: 2px 4px
&:focus
outline: none
border-color: var(--mgt2-color-primary)
background: rgba(255,255,255,0.1)
input[type="number"]
text-align: center
width: 4rem
select
cursor: pointer
// HTMLField editor min-height in description tab
.mgt2.vehicule
.vehicule-tab[data-tab="description"]
.editor,
.editor-content,
prose-mirror
min-height: 300px !important
height: auto !important

View File

@@ -13,4 +13,5 @@
@import 'components/_tabs'
@import 'components/_tab-sidebar'
@import 'components/_tables'
@import 'components/_creature'
@import 'components/_creature'
@import 'components/_vehicule'