Gestion des traits d'arme et des munitions

This commit is contained in:
2026-04-21 18:50:49 +02:00
parent 74f1b581f7
commit 6ef454d533
22 changed files with 1448 additions and 195 deletions

View File

@@ -3,6 +3,7 @@ import { MGT2 } from "../../config.js";
import { MGT2Helper } from "../../helper.js";
import { RollPromptHelper } from "../../roll-prompt.js";
import { CharacterPrompts } from "../../actors/character-prompts.js";
import WeaponData from "../../models/items/weapon.mjs";
export default class TravellerCharacterSheet extends MGT2ActorSheet {
@@ -200,8 +201,7 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
i._range = i.system.range.isMelee
? game.i18n.localize("MGT2.Melee")
: MGT2Helper.getRangeDisplay(i.system.range);
if (i.system.traits?.length > 0)
i._subInfo = i.system.traits.map(x => x.name).join(", ");
i._subInfo = WeaponData.getTraitsSummary(i.system.traits);
weapons.push(i);
break;
@@ -589,7 +589,14 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
encumbrance: this.actor.system.states.encumbrance,
difficulty: null,
damageFormula: null,
damageAP: 0,
blastRadius: 0,
stun: false,
radiation: false,
isMelee: false,
isRanged: false,
bulky: false,
veryBulky: false,
};
const cardButtons = [];
@@ -659,6 +666,18 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
rollOptions.damageFormula = itemObj.system.damage;
if (itemObj.type === "weapon") {
rollOptions.isMelee = itemObj.system.range?.isMelee === true;
rollOptions.isRanged = itemObj.type === "weapon" && !itemObj.system.range?.isMelee;
rollOptions.damageAP = itemObj.system.traits?.ap ?? 0;
rollOptions.blastRadius = itemObj.system.traits?.blast ?? 0;
rollOptions.stun = itemObj.system.traits?.stun === true;
rollOptions.radiation = itemObj.system.traits?.radiation === true;
rollOptions.scope = itemObj.system.traits?.scope === true;
rollOptions.zeroG = itemObj.system.traits?.zeroG === true;
rollOptions.autoLevel = itemObj.system.traits?.auto ?? 0;
rollOptions.itemId = itemObj._id;
rollOptions.magazine = itemObj.system.magazine ?? -1; // -1 = not tracked
rollOptions.bulky = itemObj.system.traits?.bulky === true;
rollOptions.veryBulky = itemObj.system.traits?.veryBulky === true;
}
if (itemObj.type === "disease") {
if (itemObj.system.subType === "disease")
@@ -680,6 +699,10 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
const rollModifiers = [];
const rollFormulaParts = [];
// Auto trait — fire mode
const autoLevel = rollOptions.autoLevel ?? 0;
const autoMode = autoLevel > 0 ? (userRollData.autoMode ?? "single") : "single";
if (userRollData.diceModifier) {
rollFormulaParts.push("3d6", userRollData.diceModifier);
} else {
@@ -730,14 +753,127 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.CustomDM") + " " + (customDMVal > 0 ? `+${customDMVal}` : `${customDMVal}`));
}
if (rollOptions.isRanged) {
const rangedRange = parseInt(userRollData.rangedRange ?? "0", 10);
if (rangedRange === 1) {
rollFormulaParts.push("+1");
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.RangeShort") + " +1");
} else if (rangedRange === -2) {
rollFormulaParts.push("-2");
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.RangeLong") + " 2");
} else if (rangedRange === -4) {
rollFormulaParts.push("-4");
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.RangeExtreme") + " 4");
}
const rangedAim = parseInt(userRollData.rangedAim ?? "0", 10);
// Auto: burst/full-auto cancels all aiming advantages (rules p.75)
if (rangedAim > 0 && autoMode === "single") {
rollFormulaParts.push(`+${rangedAim}`);
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.Aim") + ` +${rangedAim}`);
if (userRollData.rangedLaserSight === true || userRollData.rangedLaserSight === "true") {
rollFormulaParts.push("+1");
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.LaserSight") + " +1");
}
}
const rangedFastTarget = parseInt(userRollData.rangedFastTarget ?? "0", 10);
if (rangedFastTarget < 0) {
rollFormulaParts.push(`${rangedFastTarget}`);
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.FastTarget") + ` ${rangedFastTarget}`);
}
if (userRollData.rangedCover === true || userRollData.rangedCover === "true") {
rollFormulaParts.push("-2");
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.Cover") + " 2");
}
if (userRollData.rangedProne === true || userRollData.rangedProne === "true") {
rollFormulaParts.push("-1");
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.Prone") + " 1");
}
if (userRollData.rangedDodge === true || userRollData.rangedDodge === "true") {
const dodgeDM = parseInt(userRollData.rangedDodgeDM ?? "0", 10);
if (dodgeDM < 0) {
rollFormulaParts.push(`${dodgeDM}`);
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.Dodge") + ` ${dodgeDM}`);
}
}
}
if (rollOptions.isMelee) {
if (userRollData.meleeDodge === true || userRollData.meleeDodge === "true") {
const dodgeDM = parseInt(userRollData.meleeDodgeDM ?? "0", 10);
if (dodgeDM < 0) {
rollFormulaParts.push(`${dodgeDM}`);
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.Dodge") + ` ${dodgeDM}`);
}
}
if (userRollData.meleeParry === true || userRollData.meleeParry === "true") {
const parryDM = parseInt(userRollData.meleeParryDM ?? "0", 10);
if (parryDM < 0) {
rollFormulaParts.push(`${parryDM}`);
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.Parry") + ` ${parryDM}`);
}
}
}
if (MGT2Helper.hasValue(userRollData, "difficulty") && userRollData.difficulty !== "") rollOptions.difficulty = userRollData.difficulty;
// ── Bulky / Very Bulky trait: STR penalty ────────────────────────────
const strDm = this.actor.system.characteristics.strength?.dm ?? 0;
if (rollOptions.veryBulky) {
// Very Bulky: requires STR DM ≥ +2
if (strDm < 2) {
const penalty = strDm - 2;
rollFormulaParts.push(`${penalty}`);
rollModifiers.push(game.i18n.localize("MGT2.WeaponTraits.VeryBulky") + ` ${penalty}`);
ui.notifications.warn(game.i18n.format("MGT2.Notifications.VeryBulkyPenalty", { penalty }));
}
} else if (rollOptions.bulky) {
// Bulky: requires STR DM ≥ +1
if (strDm < 1) {
const penalty = strDm - 1;
rollFormulaParts.push(`${penalty}`);
rollModifiers.push(game.i18n.localize("MGT2.WeaponTraits.Bulky") + ` ${penalty}`);
ui.notifications.warn(game.i18n.format("MGT2.Notifications.BulkyPenalty", { penalty }));
}
}
const rollFormula = rollFormulaParts.join("");
if (!Roll.validate(rollFormula)) {
ui.notifications.error(game.i18n.localize("MGT2.Errors.InvalidRollFormula"));
return;
}
// ── Ammo decrement (ranged weapons with magazine tracking) ───────────
if (rollOptions.isRanged && rollOptions.itemId && rollOptions.magazine >= 0) {
let ammoUsed = 1;
if (autoMode === "burst" && autoLevel > 0) ammoUsed = autoLevel;
else if (autoMode === "fullAuto" && autoLevel > 0) ammoUsed = autoLevel * 3;
const currentMag = rollOptions.magazine;
if (currentMag <= 0) {
ui.notifications.warn(
game.i18n.format("MGT2.Notifications.NoAmmo", { weapon: rollOptions.rollObjectName })
);
} else {
const newMag = Math.max(0, currentMag - ammoUsed);
const weaponItem = this.actor.getEmbeddedDocument("Item", rollOptions.itemId);
if (weaponItem) await weaponItem.update({ "system.magazine": newMag });
if (newMag === 0) {
ui.notifications.warn(
game.i18n.format("MGT2.Notifications.AmmoEmpty", { weapon: rollOptions.rollObjectName })
);
} else {
ui.notifications.info(
game.i18n.format("MGT2.Notifications.AmmoUsed", { used: ammoUsed, remaining: newMag, weapon: rollOptions.rollObjectName })
);
}
}
}
let roll = await new Roll(rollFormula, this.actor.getRollData()).roll({ rollMode: userRollData.rollMode });
if (isInitiative && this.token?.combatant) {
@@ -759,7 +895,7 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
rollFailure = !rollSuccess;
}
// Build effective damage formula: base + effect + STR DM (melee)
// Build effective damage formula: base + effect + STR DM (melee) + Auto burst
let effectiveDamageFormula = rollOptions.damageFormula || null;
if (effectiveDamageFormula) {
if (rollEffect !== undefined && rollEffect !== 0) {
@@ -769,6 +905,18 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
const strDm = this.actor.system.characteristics.strength?.dm ?? 0;
if (strDm !== 0) effectiveDamageFormula += (strDm >= 0 ? "+" : "") + strDm;
}
// Burst: add Auto level to damage
if (autoMode === "burst" && autoLevel > 0) {
effectiveDamageFormula += `+${autoLevel}`;
}
}
// Auto fire mode chat info
let autoInfo = null;
if (autoMode === "burst" && autoLevel > 0) {
autoInfo = game.i18n.format("MGT2.RollPrompt.AutoBurstInfo", { level: autoLevel, ammo: autoLevel });
} else if (autoMode === "fullAuto" && autoLevel > 0) {
autoInfo = game.i18n.format("MGT2.RollPrompt.AutoFullInfo", { level: autoLevel, ammo: autoLevel * 3 });
}
// ── Build roll breakdown tooltip ─────────────────────────────────────
@@ -795,6 +943,7 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
// Show damage button only if there's a formula AND (no difficulty check OR roll succeeded)
showRollDamage: !!effectiveDamageFormula && (!difficultyValue || rollSuccess),
cardButtons: cardButtons,
autoInfo,
};
if (MGT2Helper.hasValue(rollOptions, "difficulty")) {
@@ -811,7 +960,7 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
let flags = null;
if (effectiveDamageFormula) {
flags = { mgt2: { damage: { formula: effectiveDamageFormula, rollObjectName: rollOptions.rollObjectName, rollTypeName: rollOptions.rollTypeName } } };
flags = { mgt2: { damage: { formula: effectiveDamageFormula, ap: rollOptions.damageAP ?? 0, blast: rollOptions.blastRadius ?? 0, stun: rollOptions.stun ?? false, radiation: rollOptions.radiation ?? false, rollObjectName: rollOptions.rollObjectName, rollTypeName: rollOptions.rollTypeName } } };
}
if (cardButtons.length > 0) {
if (!flags) flags = { mgt2: {} };
@@ -895,17 +1044,18 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
static async #onOpenEditor(event) {
event.preventDefault();
await CharacterPrompts.openEditorFullView(
this.actor.system.personal.species,
this.actor.system.personal.speciesText.descriptionLong
);
const title = this.actor.system.personal.species
|| game.i18n.localize("MGT2.Actor.Species")
|| "Species";
const html = this.actor.system.personal.speciesText?.descriptionLong ?? "";
await CharacterPrompts.openEditorFullView(title, html);
}
static async #onHeal(event, target) {
event.preventDefault();
const healType = target.dataset.healType;
if (canvas.tokens.controlled.length === 0) {
if (game.user.targets.size === 0) {
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
return;
}

View File

@@ -97,7 +97,7 @@ export default class TravellerCreatureSheet extends MGT2ActorSheet {
// ───────────────────────────────────────────────────────── Roll Helpers
static async #postCreatureRoll({ actor, roll, rollLabel, dm, difficulty, difficultyLabel, rollMode, extraTooltip }) {
static async #postCreatureRoll({ actor, roll, rollLabel, dm, difficulty, difficultyLabel, rollMode, extraTooltip, damageFormula }) {
const diffTarget = MGT2Helper.getDifficultyValue(difficulty ?? "Average");
const hasDifficulty = !!difficulty;
const success = hasDifficulty ? roll.total >= diffTarget : true;
@@ -111,6 +111,8 @@ export default class TravellerCreatureSheet extends MGT2ActorSheet {
if (extraTooltip) breakdownParts.push(extraTooltip);
const rollBreakdown = breakdownParts.join(" | ");
const showRollDamage = success && !!damageFormula;
const chatData = {
creatureName: actor.name,
creatureImg: actor.img,
@@ -126,6 +128,7 @@ export default class TravellerCreatureSheet extends MGT2ActorSheet {
effect: hasDifficulty ? effect : null,
effectStr: hasDifficulty ? effectStr : null,
modifiers: dm !== 0 ? [`DM ${dm >= 0 ? "+" : ""}${dm}`] : [],
showRollDamage,
};
const chatContent = await renderTemplate(
@@ -133,11 +136,23 @@ export default class TravellerCreatureSheet extends MGT2ActorSheet {
chatData
);
const flags = showRollDamage ? {
mgt2: {
damage: {
formula: normalizeDice(damageFormula),
effect,
rollObjectName: actor.name,
rollTypeName: rollLabel,
}
}
} : {};
await ChatMessage.create({
content: chatContent,
speaker: ChatMessage.getSpeaker({ actor }),
rolls: [roll],
rollMode: rollMode ?? game.settings.get("core", "rollMode"),
flags,
});
return { success, effect, total: roll.total };
@@ -238,24 +253,14 @@ export default class TravellerCreatureSheet extends MGT2ActorSheet {
if (chosenSkill) tooltipParts.push(`${chosenSkill.name} ${skillLevel >= 0 ? "+" : ""}${skillLevel}`);
if (customDM !== 0) tooltipParts.push(`MD perso ${customDM >= 0 ? "+" : ""}${customDM}`);
const { success } = await TravellerCreatureSheet.#postCreatureRoll({
await TravellerCreatureSheet.#postCreatureRoll({
actor, roll, rollLabel,
dm,
difficulty: result.difficulty,
rollMode: result.rollMode,
extraTooltip: tooltipParts.join(" | "),
damageFormula: attack.damage || null,
});
// 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