Gestion des traits d'arme et des munitions
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user