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

@@ -353,7 +353,33 @@
"Bane": "Bane",
"CreatureSkill": "Skill",
"NoSkill": "No skill",
"Days": "Days"
"Days": "Days",
"RangedModifiers": "Ranged Modifiers",
"Range": "Range",
"RangeShort": "Short Range",
"RangeNormal": "Normal Range",
"RangeLong": "Long Range",
"RangeExtreme": "Extreme Range",
"Aim": "Aim",
"LaserSight": "Laser Sight",
"FastTarget": "Fast-Moving Target",
"Cover": "Cover / Diving",
"Prone": "Prone Target",
"MeleeModifiers": "Melee Modifiers",
"Parry": "Parry (defender's Melee score)",
"Dodge": "Target Dodging",
"DodgeDM": "Dodge DM",
"FireMode": "Fire Mode",
"AutoSingle": "Single",
"AutoBurst": "Burst",
"AutoFull": "Full Auto",
"AutoNoAim": "Burst / Full-Auto: all aiming advantages are cancelled.",
"AutoBurstInfo": "Burst (Auto {level}) — +{level} damage — ammo: {ammo}",
"AutoFullInfo": "Full Auto (Auto {level}) — {level} attacks — ammo: {ammo}",
"ScopeActive": "Scope active",
"ScopeHint": "Scope trait: ignores the automatic Extreme Range rule beyond 100m, provided the traveller aims before firing. Select the actual range below.",
"ZeroGActive": "Zero-G",
"ZeroGHint": "Zero-G trait: this weapon has little or no recoil. It can be used in low or zero gravity without requiring an Athletics (DEX) test. No additional DM required in those conditions."
},
"Timeframes": {
"Normal": "Normal",
@@ -369,7 +395,22 @@
"Effect": "Effect",
"Dice": "Dice",
"Result": "Result",
"DiceModifier": "Dice Modifier"
"DiceModifier": "Dice Modifier",
"APIgnore": "AP",
"APIgnoreHint": "This weapon ignores {ap} points of armor (AP trait)",
"BlastArea": "Blast",
"BlastHint": "Blast weapon: damage is applied to all targets within the blast radius (in meters). Dodge reactions cannot be used, but targets may dive for cover. Cover between target and explosion center applies.",
"BlastRules": "No dodge — dive for cover possible",
"StunWeapon": "Stun Weapon — END damage only",
"StunHint": "Stun weapon: damage is only deducted from END (after armor). If END reaches 0, the target is incapacitated for a number of rounds equal to the difference between damage dealt and the target's END. Fully healed after 1 hour of rest."
},
"Radiation": {
"Badge": "Radiation Weapon — RADs",
"Hint": "This weapon emits radiation. On a successful attack, the target receives 2D×20 additional rads. Click the button to roll radiation damage.",
"Title": "Radiation Damage",
"Rads": "RADs",
"RollButton": "Roll RADs",
"Rules": "See p.78 for radiation effects (nausea, illness, death). ×3 multiplier for starship-scale weapons."
}
},
"Items": {
@@ -458,6 +499,29 @@
"Weightless": "Weightless",
"Quantity": "Quantity"
},
"WeaponTraits": {
"SectionTitle": "Traits",
"AP": "AP (Armor Piercing)",
"APHint": "Ignores X points of armor protection",
"Auto": "Auto",
"AutoHint": "Automatic fire — Burst and Full-Auto modes available",
"Blast": "Blast",
"BlastHint": "Explosion radius in meters",
"Bulky": "Bulky",
"BulkyHint": "Requires STR 9+ or suffer negative DM",
"VeryBulky": "Very Bulky",
"VeryBulkyHint": "Requires STR 12+ or suffer negative DM",
"Stun": "Stun",
"StunHint": "Non-lethal damage — deducted from END only",
"Smart": "Smart",
"SmartHint": "Guided projectiles — DM = TL difference (min +1, max +6)",
"Radiation": "Radiation",
"RadiationHint": "Inflicts 2D×20 rads on target",
"Scope": "Scope",
"ScopeHint": "Ignores extreme range rule (>100m) if aiming",
"ZeroG": "Zero-G",
"ZeroGHint": "No recoil — no Athletics check needed in microgravity"
},
"Durations": {
"Seconds": "Seconds",
"Minutes": "Minutes",
@@ -565,10 +629,18 @@
},
"Notifications": {
"HealingApplied": "{name} has been healed for {amount} points.",
"DamageApplied": "{name} has taken {amount} damage."
"DamageApplied": "{name} has taken {amount} damage.",
"DamageAppliedAP": "{name} has taken {amount} damage (armor reduced by {ap} from AP trait).",
"BulkyPenalty": "Bulky weapon: insufficient STR (DM too low). Penalty {penalty} applied to roll.",
"VeryBulkyPenalty": "Very Bulky weapon: insufficient STR (DM too low). Penalty {penalty} applied to roll.",
"StunDamageApplied": "{name} took {amount} stun damage (END only).",
"StunIncapacitated": "{name} is incapacitated for {rounds} round(s)! (END reduced to 0)",
"AmmoUsed": "{weapon}: {used} round(s) expended. Magazine remaining: {remaining}.",
"AmmoEmpty": "{weapon}: magazine empty after this shot!",
"NoAmmo": "{weapon}: magazine empty! Reload before firing."
},
"Errors": {
"NoTokenSelected": "No token selected. Select a token on the scene before applying.",
"NoTokenSelected": "No active target. Target a token on the scene before applying.",
"InvalidRollFormula": "Invalid roll formula."
}
},

View File

@@ -353,7 +353,33 @@
"Bane": "Désavantage",
"CreatureSkill": "Compétence",
"NoSkill": "Aucune compétence",
"Days": "Jours"
"Days": "Jours",
"RangedModifiers": "Modificateurs de tir",
"Range": "Portée",
"RangeShort": "Courte portée",
"RangeNormal": "Portée normale",
"RangeLong": "Longue portée",
"RangeExtreme": "Portée extrême",
"Aim": "Visée",
"LaserSight": "Pointeur laser",
"FastTarget": "Cible bougeant vite",
"Cover": "À couvert / Plongé",
"Prone": "À plat ventre",
"MeleeModifiers": "Modificateurs de mêlée",
"Parry": "Parade (score Mêlée du défenseur)",
"Dodge": "Esquive de la cible",
"DodgeDM": "MD Esquive",
"FireMode": "Mode de tir",
"AutoSingle": "Simple",
"AutoBurst": "Rafale",
"AutoFull": "Auto complet",
"AutoNoAim": "Rafale / Auto : les avantages de la visée sont annulés.",
"AutoBurstInfo": "Rafale (Auto {level}) — +{level} dégâts — munitions: {ammo}",
"AutoFullInfo": "Auto complet (Auto {level}) — {level} attaques — munitions: {ammo}",
"ScopeActive": "Viseur actif",
"ScopeHint": "Trait Viseur : ignore la règle Portée Extrême automatique au-delà de 100m, à condition de viser avant de tirer. Choisissez la portée réelle dans le sélecteur ci-dessous.",
"ZeroGActive": "Zéro-G",
"ZeroGHint": "Trait Zéro-G : cette arme a peu ou pas de recul. Elle peut être utilisée en gravité faible ou nulle sans nécessiter de test d'Athlétisme (DEX). Aucun MD supplémentaire requis dans ces conditions."
},
"Timeframes": {
"Normal": "Normal",
@@ -369,7 +395,22 @@
"Effect": "Effet",
"Dice": "Dés",
"Result": "Résultat",
"DiceModifier": "Modificateur de dés"
"DiceModifier": "Modificateur de dés",
"APIgnore": "AP",
"APIgnoreHint": "Cette arme ignore {ap} points d'armure (trait AP)",
"BlastArea": "Explosion",
"BlastHint": "Arme à explosion : les dégâts sont infligés à toutes les cibles dans le rayon indiqué (en mètres). Pas d'esquive possible, mais possibilité de plonger à couvert. Le couvert entre la cible et le centre de l'explosion s'applique.",
"BlastRules": "Pas d'esquive — plonger à couvert possible",
"StunWeapon": "Arme Incapacitante — dégâts END uniquement",
"StunHint": "Arme incapacitante : les dégâts sont déduits uniquement de l'END (après armure). Si l'END atteint 0, la cible est neutralisée pendant un nombre de rounds égal à la différence entre les dégâts et l'END initiale. Guérison complète après 1h de repos."
},
"Radiation": {
"Badge": "Arme à Rayonnement — RADs",
"Hint": "Cette arme émet des radiations. En cas d'attaque réussie, la cible reçoit 2D×20 rads supplémentaires. Cliquez le bouton pour lancer les dégâts de RAD.",
"Title": "Dégâts de Rayonnement",
"Rads": "RADs",
"RollButton": "Lancer RADs",
"Rules": "Consultez p.78 pour l'effet des radiations (nausées, maladies, mort). Multiplicé ×3 pour une arme à l'échelle spatiale."
}
},
"Items": {
@@ -458,6 +499,29 @@
"Weightless": "Aucun Poids",
"Quantity": "Quantité"
},
"WeaponTraits": {
"SectionTitle": "Traits",
"AP": "AP (Perforant)",
"APHint": "Ignore X points de protection d'armure",
"Auto": "Auto",
"AutoHint": "Tir automatique — Rafale et Auto complet disponibles",
"Blast": "Explosion",
"BlastHint": "Rayon de l'explosion en mètres",
"Bulky": "Encombrant",
"BulkyHint": "Requiert FOR 9+ sinon MD négatif",
"VeryBulky": "Très Encombrant",
"VeryBulkyHint": "Requiert FOR 12+ sinon MD négatif",
"Stun": "Incapacitante",
"StunHint": "Dégâts non létaux — déduits uniquement de l'END",
"Smart": "Intelligente",
"SmartHint": "Projectiles guidés — MD = différence de NT (min +1, max +6)",
"Radiation": "Rayonnement",
"RadiationHint": "Inflige 2D×20 rads à la cible",
"Scope": "Viseur",
"ScopeHint": "Ignore la règle portée extrême (>100m) si le tireur vise",
"ZeroG": "Zéro-G",
"ZeroGHint": "Pas de recul — aucun jet d'Athlétisme requis en microgravité"
},
"Durations": {
"Seconds": "Secondes",
"Minutes": "Minutes",
@@ -565,10 +629,18 @@
},
"Notifications": {
"HealingApplied": "{name} a été soigné(e) de {amount} points.",
"DamageApplied": "{name} a subi {amount} dégâts."
"DamageApplied": "{name} a subi {amount} dégâts.",
"DamageAppliedAP": "{name} a subi {amount} dégâts (armure réduite de {ap} par AP).",
"BulkyPenalty": "Arme Encombrante : FOR insuffisante (MD FOR trop faible). Pénalité {penalty} appliquée au jet.",
"VeryBulkyPenalty": "Arme Très Encombrante : FOR insuffisante (MD FOR trop faible). Pénalité {penalty} appliquée au jet.",
"StunDamageApplied": "{name} a subi {amount} dégâts incapacitants (END seulement).",
"StunIncapacitated": "{name} est neutralisé(e) pour {rounds} round(s) ! (END réduite à 0)",
"AmmoUsed": "{weapon} : {used} munition(s) consommée(s). Chargeur restant : {remaining}.",
"AmmoEmpty": "{weapon} : chargeur vide après ce tir !",
"NoAmmo": "{weapon} : chargeur vide ! Rechargez avant de tirer."
},
"Errors": {
"NoTokenSelected": "Aucun token sélectionné. Sélectionnez un token sur la scène avant d'appliquer.",
"NoTokenSelected": "Aucune cible active. Ciblez un token sur la scène avant d'appliquer.",
"InvalidRollFormula": "Formule de jet invalide."
}
},

View File

@@ -403,12 +403,18 @@ class WeaponData extends PhysicalItemData {
schema.damage = new fields$4.StringField({ required: false, blank: true, trim: true });
schema.magazine = new fields$4.NumberField({ required: false, initial: 0, min: 0, integer: true });
schema.magazineCost = new fields$4.NumberField({ required: false, initial: 0, min: 0, integer: true });
schema.traits = new fields$4.ArrayField(
new fields$4.SchemaField({
name: new fields$4.StringField({ required: true, blank: true, trim: true }),
description: new fields$4.StringField({ required: false, blank: true, trim: true })
})
);
schema.traits = new fields$4.SchemaField({
ap: new fields$4.NumberField({ required: false, initial: 0, min: 0, integer: true }),
auto: new fields$4.NumberField({ required: false, initial: 0, min: 0, integer: true }),
blast: new fields$4.NumberField({ required: false, initial: 0, min: 0, integer: true }),
bulky: new fields$4.BooleanField({ required: false, initial: false }),
veryBulky: new fields$4.BooleanField({ required: false, initial: false }),
stun: new fields$4.BooleanField({ required: false, initial: false }),
smart: new fields$4.BooleanField({ required: false, initial: false }),
radiation: new fields$4.BooleanField({ required: false, initial: false }),
scope: new fields$4.BooleanField({ required: false, initial: false }),
zeroG: new fields$4.BooleanField({ required: false, initial: false })
});
schema.options = new fields$4.ArrayField(
new fields$4.SchemaField({
name: new fields$4.StringField({ required: true, blank: true, trim: true }),
@@ -417,6 +423,23 @@ class WeaponData extends PhysicalItemData {
);
return schema;
}
/** Returns a compact display string of active traits (e.g. "AP 2, Auto 3, Blast 5, Bulky") */
static getTraitsSummary(traits) {
if (!traits) return "";
const parts = [];
if (traits.ap > 0) parts.push(`AP ${traits.ap}`);
if (traits.auto > 0) parts.push(`Auto ${traits.auto}`);
if (traits.blast > 0) parts.push(`Blast ${traits.blast}`);
if (traits.bulky) parts.push(game.i18n.localize("MGT2.WeaponTraits.Bulky"));
if (traits.veryBulky) parts.push(game.i18n.localize("MGT2.WeaponTraits.VeryBulky"));
if (traits.stun) parts.push(game.i18n.localize("MGT2.WeaponTraits.Stun"));
if (traits.smart) parts.push(game.i18n.localize("MGT2.WeaponTraits.Smart"));
if (traits.radiation) parts.push(game.i18n.localize("MGT2.WeaponTraits.Radiation"));
if (traits.scope) parts.push(game.i18n.localize("MGT2.WeaponTraits.Scope"));
if (traits.zeroG) parts.push(game.i18n.localize("MGT2.WeaponTraits.ZeroG"));
return parts.join(", ");
}
}
const fields$3 = foundry.data.fields;
@@ -943,7 +966,10 @@ class ActorCharacter {
}
updateData["system.inventory.weight"] = onHandWeight;
updateData["system.states.encumbrance"] = onHandWeight > $this.system.inventory.encumbrance.normal;
// Use the threshold from updateData if it was just recalculated (e.g. STR/END talent change),
// otherwise fall back to the persisted value.
const encumbranceThreshold = updateData["system.inventory.encumbrance.normal"] ?? $this.system.inventory.encumbrance.normal;
updateData["system.states.encumbrance"] = onHandWeight > encumbranceThreshold;
await $this.update(updateData);
@@ -964,6 +990,8 @@ class ActorCharacter {
let heavy = normal * 2;
foundry.utils.setProperty(changed, "system.inventory.encumbrance.normal", normal);
foundry.utils.setProperty(changed, "system.inventory.encumbrance.heavy", heavy);
// Also update the encumbrance state flag against the new threshold
foundry.utils.setProperty(changed, "system.states.encumbrance", $this.system.inventory.weight > normal);
}
//console.log(foundry.utils.getProperty(changed, "system.characteristics.strength.value"));
@@ -1017,25 +1045,39 @@ class ActorCharacter {
// $this.update({ system: { characteristics: data } });
// }
static applyDamage($this, amount, { ignoreArmor = false } = {}) {
if (isNaN(amount) || amount === 0) return;
static async applyDamage($this, amount, { ignoreArmor = false, ap = 0, stun = false } = {}) {
if (isNaN(amount) || amount === 0) return { incapRounds: 0 };
const rank1 = $this.system.config.damages.rank1;
const rank2 = $this.system.config.damages.rank2;
const rank3 = $this.system.config.damages.rank3;
if (amount < 0) amount = Math.abs(amount);
if (!ignoreArmor) {
const rawArmor = $this.system.inventory?.armor ?? 0;
const armorValue = Math.max(0, rawArmor - ap);
amount = Math.max(0, amount - armorValue);
if (amount === 0) return { incapRounds: 0 };
}
// ── Stun / Incapacitating: only deduct from endurance (rank3) ─────────
if (stun) {
const endKey = rank3; // "endurance" by default
const prevEnd = $this.system.characteristics[endKey].value;
const newEnd = Math.max(0, prevEnd - amount);
const incapRounds = newEnd === 0 ? Math.max(0, amount - prevEnd) : 0;
await $this.update({
system: { characteristics: { [endKey]: { value: newEnd, dm: this.getModifier(newEnd) } } }
});
return { incapRounds };
}
// ── Normal damage cascade: rank1 → rank2 → rank3 ────────────────────
const data = {};
data[rank1] = { value: $this.system.characteristics[rank1].value };
data[rank2] = { value: $this.system.characteristics[rank2].value };
data[rank3] = { value: $this.system.characteristics[rank3].value };
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) {
@@ -1050,7 +1092,8 @@ class ActorCharacter {
}
}
$this.update({ system: { characteristics: data } });
await $this.update({ system: { characteristics: data } });
return { incapRounds: 0 };
}
static applyHealing($this, amount, type) {
@@ -1237,17 +1280,18 @@ class TravellerActor extends Actor {
}
}
applyDamage(amount, { ignoreArmor = false } = {}) {
applyDamage(amount, { ignoreArmor = false, ap = 0, stun = false } = {}) {
if (this.type === "character") {
return ActorCharacter.applyDamage(this, amount, { ignoreArmor });
return ActorCharacter.applyDamage(this, amount, { ignoreArmor, ap, stun });
} else if (this.type === "creature") {
if (isNaN(amount) || amount === 0) return;
if (isNaN(amount) || amount === 0) return Promise.resolve({ incapRounds: 0 });
if (amount < 0) amount = Math.abs(amount);
const armorValue = ignoreArmor ? 0 : (this.system.armor ?? 0);
const rawArmor = ignoreArmor ? 0 : (this.system.armor ?? 0);
const armorValue = Math.max(0, rawArmor - ap);
const effective = Math.max(0, amount - armorValue);
if (effective === 0) return;
if (effective === 0) return Promise.resolve({ incapRounds: 0 });
const newValue = Math.max(0, (this.system.life.value ?? 0) - effective);
return this.update({ "system.life.value": newValue });
return this.update({ "system.life.value": newValue }).then(() => ({ incapRounds: 0 }));
}
}
@@ -1722,7 +1766,14 @@ class RollPromptHelper {
skillLevel: options.skillLevel ?? 0,
// Healing fields
showHeal: options.showHeal ?? false,
healType: options.healType ?? null
healType: options.healType ?? null,
// Ranged/Melee weapon flags
isRanged: options.isRanged ?? false,
isMelee: options.isMelee ?? false,
isAttack: (options.isRanged ?? false) || (options.isMelee ?? false),
autoLevel: options.autoLevel ?? 0,
hasScope: options.scope ?? false,
hasZeroG: options.zeroG ?? false,
});
return await DialogV2$1.wait({
@@ -1816,13 +1867,14 @@ class CharacterPrompts {
}
static async openEditorFullView(title, html) {
const safeTitle = title || game.i18n.localize("MGT2.Actor.Species") || "Species";
const htmlContent = await renderTemplate$2("systems/mgt2/templates/editor-fullview.html", {
config: CONFIG.MGT2,
html
html: html ?? ""
});
game.settings.get("mgt2", "theme");
await DialogV2.wait({
window: { title },
window: { title: safeTitle },
content: htmlContent,
rejectClose: false,
buttons: [
@@ -2064,8 +2116,7 @@ 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;
@@ -2453,7 +2504,14 @@ 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 = [];
@@ -2523,6 +2581,18 @@ 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")
@@ -2544,6 +2614,10 @@ 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 {
@@ -2594,14 +2668,127 @@ 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) {
@@ -2623,7 +2810,7 @@ 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) {
@@ -2633,6 +2820,18 @@ 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 ─────────────────────────────────────
@@ -2659,6 +2858,7 @@ 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")) {
@@ -2675,7 +2875,7 @@ 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: {} };
@@ -2759,17 +2959,18 @@ 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;
}
@@ -3214,7 +3415,7 @@ 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;
@@ -3228,6 +3429,8 @@ 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,
@@ -3243,6 +3446,7 @@ class TravellerCreatureSheet extends MGT2ActorSheet {
effect: hasDifficulty ? effect : null,
effectStr: hasDifficulty ? effectStr : null,
modifiers: dm !== 0 ? [`DM ${dm >= 0 ? "+" : ""}${dm}`] : [],
showRollDamage,
};
const chatContent = await renderTemplate$1(
@@ -3250,11 +3454,23 @@ 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 };
@@ -3355,24 +3571,14 @@ 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
@@ -3716,6 +3922,7 @@ const preloadHandlebarsTemplates = async function() {
"systems/mgt2/templates/roll-prompt.html",
"systems/mgt2/templates/chat/roll.html",
//"systems/mgt2/templates/chat/roll-characteristic.html",
"systems/mgt2/templates/chat/radiation.html",
"systems/mgt2/templates/actors/actor-config-sheet.html",
"systems/mgt2/templates/actors/actor-config-characteristic-sheet.html",
"systems/mgt2/templates/actors/trait-sheet.html",
@@ -3734,12 +3941,45 @@ class ChatHelper {
if (!message || !element) {
return;
}
// Restore disabled state for already-applied buttons
const appliedActions = message.flags?.mgt2?.appliedActions ?? [];
if (appliedActions.length > 0) {
appliedActions.forEach(action => {
element.querySelectorAll(`button[data-action="${action}"]`).forEach(btn => {
btn.disabled = true;
});
});
}
// Apply buttons are GM-only: hide them for players
const GM_ACTIONS = ["damage", "healing", "surgeryDamage"];
if (!game.user.isGM) {
GM_ACTIONS.forEach(action => {
element.querySelectorAll(`button[data-action="${action}"]`).forEach(btn => {
btn.style.display = "none";
});
});
// Hide the buttons container if no visible buttons remain
element.querySelectorAll(".mgt2-buttons").forEach(container => {
const hasVisible = [...container.querySelectorAll("button")]
.some(b => b.style.display !== "none");
if (!hasVisible) container.style.display = "none";
});
}
element.querySelectorAll('button[data-action="rollDamage"]').forEach(el => {
el.addEventListener('click', async event => {
await this._processRollDamageButtonEvent(message, event);
});
});
element.querySelectorAll('button[data-action="rollRadiation"]').forEach(el => {
el.addEventListener('click', async event => {
await this._rollRadiationDamage(message, event);
});
});
element.querySelectorAll('button[data-action="damage"]').forEach(el => {
el.addEventListener('click', async event => {
await this._applyChatCardDamage(message, event);
@@ -3791,75 +4031,144 @@ class ChatHelper {
static async _processRollDamageButtonEvent(message, event) {
event.preventDefault();
event.stopPropagation();
let rollFormula = message.flags.mgt2.damage.formula;
let roll = await new Roll(rollFormula, {}).roll();
let speaker;
let selectTokens = canvas.tokens.controlled;
if (selectTokens.length > 0) {
speaker = selectTokens[0].actor;
} else {
speaker = game.user.character;
const damageFlags = message.flags?.mgt2?.damage;
if (!damageFlags?.formula) {
ui.notifications.warn(game.i18n.localize("MGT2.Errors.InvalidRollFormula"));
return;
}
let rollTypeName = message.flags.mgt2.damage.rollTypeName ? message.flags.mgt2.damage.rollTypeName + " " + game.i18n.localize("MGT2.Actor.Damage") : null;
const effect = damageFlags.effect ?? 0;
const effectPart = effect > 0 ? `+${effect}` : effect < 0 ? `${effect}` : "";
const rollFormula = damageFlags.formula + effectPart;
let roll;
try {
roll = await new Roll(rollFormula, {}).roll();
} catch (e) {
ui.notifications.warn(game.i18n.localize("MGT2.Errors.InvalidRollFormula"));
return;
}
const rollTypeName = damageFlags.rollTypeName
? damageFlags.rollTypeName + " " + game.i18n.localize("MGT2.Actor.Damage")
: null;
const ap = damageFlags.ap ?? 0;
const blast = damageFlags.blast ?? 0;
const stun = damageFlags.stun ?? false;
const radiation = damageFlags.radiation ?? false;
const chatData = {
user: game.user.id,
speaker: ChatMessage.getSpeaker({ actor: speaker }),
speaker: message.speaker,
formula: roll._formula,
tooltip: await roll.getTooltip(),
total: Math.round(roll.total * 100) / 100,
showButtons: true,
hasDamage: true,
rollTypeName: rollTypeName,
rollObjectName: message.flags.mgt2.damage.rollObjectName
rollTypeName,
rollObjectName: damageFlags.rollObjectName,
apValue: ap > 0 ? ap : null,
blastRadius: blast > 0 ? blast : null,
stunWeapon: stun || null,
radiationWeapon: radiation || null,
};
const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/roll.html", chatData);
chatData.content = html;
// Persist ap, blast, stun, radiation in damage message flags so handlers can read them
if (ap > 0 || blast > 0 || stun || radiation) chatData.flags = { mgt2: { damage: { ap, blast, stun, radiation, rollObjectName: damageFlags.rollObjectName } } };
return roll.toMessage(chatData);
}
static _applyChatCardDamage(message, event) {
if (canvas.tokens.controlled.length === 0) {
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
return;
static async #markButtonApplied(message, btn, action) {
const existing = message.flags?.mgt2?.appliedActions ?? [];
if (!existing.includes(action)) {
await message.setFlag("mgt2", "appliedActions", [...existing, action]);
}
const roll = message.rolls[0];
return Promise.all(canvas.tokens.controlled.map(t => {
const a = t.actor;
return a.applyDamage(roll.total);
}));
if (btn) btn.disabled = true;
}
static _applyChatCardHealing(message, event) {
if (canvas.tokens.controlled.length === 0) {
static async _applyChatCardDamage(message, event) {
if (game.user.targets.size === 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 btn = event.currentTarget;
const roll = message.rolls[0];
const amount = Math.round(roll.total * 100) / 100;
const ap = message.flags?.mgt2?.damage?.ap ?? 0;
const stun = message.flags?.mgt2?.damage?.stun ?? false;
await Promise.all([...game.user.targets].map(async t => {
const result = await t.actor.applyDamage(amount, { ap, stun });
if (stun) {
const incapRounds = result?.incapRounds ?? 0;
if (incapRounds > 0) {
ui.notifications.warn(game.i18n.format("MGT2.Notifications.StunIncapacitated", { name: t.actor.name, rounds: incapRounds }));
} else {
ui.notifications.info(game.i18n.format("MGT2.Notifications.StunDamageApplied", { name: t.actor.name, amount }));
}
} else {
const notifKey = ap > 0 ? "MGT2.Notifications.DamageAppliedAP" : "MGT2.Notifications.DamageApplied";
ui.notifications.info(game.i18n.format(notifKey, { name: t.actor.name, amount, ap }));
}
}));
await ChatHelper.#markButtonApplied(message, btn, "damage");
}
static async _applyChatCardHealing(message, event) {
if (game.user.targets.size === 0) {
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
return;
}
const btn = event.currentTarget;
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);
await Promise.all([...game.user.targets].map(async t => {
await t.actor.applyHealing(amount);
ui.notifications.info(game.i18n.format("MGT2.Notifications.HealingApplied", { name: t.actor.name, amount }));
}));
await ChatHelper.#markButtonApplied(message, btn, "healing");
}
static _applyChatCardSurgeryDamage(message, event) {
if (canvas.tokens.controlled.length === 0) {
static async _applyChatCardSurgeryDamage(message, event) {
if (game.user.targets.size === 0) {
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
return;
}
const btn = event.currentTarget;
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 });
await Promise.all([...game.user.targets].map(async t => {
await t.actor.applyDamage(amount, { ignoreArmor: true });
ui.notifications.info(game.i18n.format("MGT2.Notifications.DamageApplied", { name: t.actor.name, amount }));
}));
await ChatHelper.#markButtonApplied(message, btn, "surgeryDamage");
}
static async _rollRadiationDamage(message, event) {
event.preventDefault();
event.stopPropagation();
const damageFlags = message.flags?.mgt2?.damage;
const rollObjectName = damageFlags?.rollObjectName ?? "";
// 2D × 20 rads (MGT2 rule: 2d6 × 20)
const roll = await new Roll("2d6 * 20", {}).roll();
const chatData = {
user: game.user.id,
speaker: message.speaker,
formula: roll._formula,
tooltip: await roll.getTooltip(),
total: Math.round(roll.total),
rollObjectName,
};
const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/radiation.html", chatData);
chatData.content = html;
return roll.toMessage(chatData);
}
}

File diff suppressed because one or more lines are too long

View File

@@ -52,17 +52,18 @@ export class TravellerActor extends Actor {
}
}
applyDamage(amount, { ignoreArmor = false } = {}) {
applyDamage(amount, { ignoreArmor = false, ap = 0, stun = false } = {}) {
if (this.type === "character") {
return ActorCharacter.applyDamage(this, amount, { ignoreArmor });
return ActorCharacter.applyDamage(this, amount, { ignoreArmor, ap, stun });
} else if (this.type === "creature") {
if (isNaN(amount) || amount === 0) return;
if (isNaN(amount) || amount === 0) return Promise.resolve({ incapRounds: 0 });
if (amount < 0) amount = Math.abs(amount);
const armorValue = ignoreArmor ? 0 : (this.system.armor ?? 0);
const rawArmor = ignoreArmor ? 0 : (this.system.armor ?? 0);
const armorValue = Math.max(0, rawArmor - ap);
const effective = Math.max(0, amount - armorValue);
if (effective === 0) return;
if (effective === 0) return Promise.resolve({ incapRounds: 0 });
const newValue = Math.max(0, (this.system.life.value ?? 0) - effective);
return this.update({ "system.life.value": newValue });
return this.update({ "system.life.value": newValue }).then(() => ({ incapRounds: 0 }));
}
}

View File

@@ -51,13 +51,14 @@ export class CharacterPrompts {
}
static async openEditorFullView(title, html) {
const safeTitle = title || game.i18n.localize("MGT2.Actor.Species") || "Species";
const htmlContent = await renderTemplate("systems/mgt2/templates/editor-fullview.html", {
config: CONFIG.MGT2,
html
html: html ?? ""
});
const theme = game.settings.get("mgt2", "theme");
await DialogV2.wait({
window: { title },
window: { title: safeTitle },
content: htmlContent,
rejectClose: false,
buttons: [

View File

@@ -254,7 +254,10 @@ export class ActorCharacter {
}
updateData["system.inventory.weight"] = onHandWeight;
updateData["system.states.encumbrance"] = onHandWeight > $this.system.inventory.encumbrance.normal;
// Use the threshold from updateData if it was just recalculated (e.g. STR/END talent change),
// otherwise fall back to the persisted value.
const encumbranceThreshold = updateData["system.inventory.encumbrance.normal"] ?? $this.system.inventory.encumbrance.normal;
updateData["system.states.encumbrance"] = onHandWeight > encumbranceThreshold;
await $this.update(updateData);
@@ -275,6 +278,8 @@ export class ActorCharacter {
let heavy = normal * 2;
foundry.utils.setProperty(changed, "system.inventory.encumbrance.normal", normal);
foundry.utils.setProperty(changed, "system.inventory.encumbrance.heavy", heavy);
// Also update the encumbrance state flag against the new threshold
foundry.utils.setProperty(changed, "system.states.encumbrance", $this.system.inventory.weight > normal);
}
//console.log(foundry.utils.getProperty(changed, "system.characteristics.strength.value"));
@@ -331,25 +336,39 @@ export class ActorCharacter {
// $this.update({ system: { characteristics: data } });
// }
static applyDamage($this, amount, { ignoreArmor = false } = {}) {
if (isNaN(amount) || amount === 0) return;
static async applyDamage($this, amount, { ignoreArmor = false, ap = 0, stun = false } = {}) {
if (isNaN(amount) || amount === 0) return { incapRounds: 0 };
const rank1 = $this.system.config.damages.rank1;
const rank2 = $this.system.config.damages.rank2;
const rank3 = $this.system.config.damages.rank3;
if (amount < 0) amount = Math.abs(amount);
if (!ignoreArmor) {
const rawArmor = $this.system.inventory?.armor ?? 0;
const armorValue = Math.max(0, rawArmor - ap);
amount = Math.max(0, amount - armorValue);
if (amount === 0) return { incapRounds: 0 };
}
// ── Stun / Incapacitating: only deduct from endurance (rank3) ─────────
if (stun) {
const endKey = rank3; // "endurance" by default
const prevEnd = $this.system.characteristics[endKey].value;
const newEnd = Math.max(0, prevEnd - amount);
const incapRounds = newEnd === 0 ? Math.max(0, amount - prevEnd) : 0;
await $this.update({
system: { characteristics: { [endKey]: { value: newEnd, dm: this.getModifier(newEnd) } } }
});
return { incapRounds };
}
// ── Normal damage cascade: rank1 → rank2 → rank3 ────────────────────
const data = {};
data[rank1] = { value: $this.system.characteristics[rank1].value };
data[rank2] = { value: $this.system.characteristics[rank2].value };
data[rank3] = { value: $this.system.characteristics[rank3].value };
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) {
@@ -364,7 +383,8 @@ export class ActorCharacter {
}
}
$this.update({ system: { characteristics: data } });
await $this.update({ system: { characteristics: data } });
return { incapRounds: 0 };
}
static applyHealing($this, amount, type) {

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

View File

@@ -6,12 +6,45 @@ export class ChatHelper {
if (!message || !element) {
return;
}
// Restore disabled state for already-applied buttons
const appliedActions = message.flags?.mgt2?.appliedActions ?? [];
if (appliedActions.length > 0) {
appliedActions.forEach(action => {
element.querySelectorAll(`button[data-action="${action}"]`).forEach(btn => {
btn.disabled = true;
});
});
}
// Apply buttons are GM-only: hide them for players
const GM_ACTIONS = ["damage", "healing", "surgeryDamage"];
if (!game.user.isGM) {
GM_ACTIONS.forEach(action => {
element.querySelectorAll(`button[data-action="${action}"]`).forEach(btn => {
btn.style.display = "none";
});
});
// Hide the buttons container if no visible buttons remain
element.querySelectorAll(".mgt2-buttons").forEach(container => {
const hasVisible = [...container.querySelectorAll("button")]
.some(b => b.style.display !== "none");
if (!hasVisible) container.style.display = "none";
});
}
element.querySelectorAll('button[data-action="rollDamage"]').forEach(el => {
el.addEventListener('click', async event => {
await this._processRollDamageButtonEvent(message, event);
});
});
element.querySelectorAll('button[data-action="rollRadiation"]').forEach(el => {
el.addEventListener('click', async event => {
await this._rollRadiationDamage(message, event);
});
});
element.querySelectorAll('button[data-action="damage"]').forEach(el => {
el.addEventListener('click', async event => {
await this._applyChatCardDamage(message, event);
@@ -63,74 +96,143 @@ export class ChatHelper {
static async _processRollDamageButtonEvent(message, event) {
event.preventDefault();
event.stopPropagation();
let rollFormula = message.flags.mgt2.damage.formula;
let roll = await new Roll(rollFormula, {}).roll();
let speaker;
let selectTokens = canvas.tokens.controlled;
if (selectTokens.length > 0) {
speaker = selectTokens[0].actor;
} else {
speaker = game.user.character;
const damageFlags = message.flags?.mgt2?.damage;
if (!damageFlags?.formula) {
ui.notifications.warn(game.i18n.localize("MGT2.Errors.InvalidRollFormula"));
return;
}
let rollTypeName = message.flags.mgt2.damage.rollTypeName ? message.flags.mgt2.damage.rollTypeName + " " + game.i18n.localize("MGT2.Actor.Damage") : null;
const effect = damageFlags.effect ?? 0;
const effectPart = effect > 0 ? `+${effect}` : effect < 0 ? `${effect}` : "";
const rollFormula = damageFlags.formula + effectPart;
let roll;
try {
roll = await new Roll(rollFormula, {}).roll();
} catch (e) {
ui.notifications.warn(game.i18n.localize("MGT2.Errors.InvalidRollFormula"));
return;
}
const rollTypeName = damageFlags.rollTypeName
? damageFlags.rollTypeName + " " + game.i18n.localize("MGT2.Actor.Damage")
: null;
const ap = damageFlags.ap ?? 0;
const blast = damageFlags.blast ?? 0;
const stun = damageFlags.stun ?? false;
const radiation = damageFlags.radiation ?? false;
const chatData = {
user: game.user.id,
speaker: ChatMessage.getSpeaker({ actor: speaker }),
speaker: message.speaker,
formula: roll._formula,
tooltip: await roll.getTooltip(),
total: Math.round(roll.total * 100) / 100,
showButtons: true,
hasDamage: true,
rollTypeName: rollTypeName,
rollObjectName: message.flags.mgt2.damage.rollObjectName
rollTypeName,
rollObjectName: damageFlags.rollObjectName,
apValue: ap > 0 ? ap : null,
blastRadius: blast > 0 ? blast : null,
stunWeapon: stun || null,
radiationWeapon: radiation || null,
};
const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/roll.html", chatData);
chatData.content = html;
// Persist ap, blast, stun, radiation in damage message flags so handlers can read them
if (ap > 0 || blast > 0 || stun || radiation) chatData.flags = { mgt2: { damage: { ap, blast, stun, radiation, rollObjectName: damageFlags.rollObjectName } } };
return roll.toMessage(chatData);
}
static _applyChatCardDamage(message, event) {
if (canvas.tokens.controlled.length === 0) {
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
return;
static async #markButtonApplied(message, btn, action) {
const existing = message.flags?.mgt2?.appliedActions ?? [];
if (!existing.includes(action)) {
await message.setFlag("mgt2", "appliedActions", [...existing, action]);
}
const roll = message.rolls[0];
return Promise.all(canvas.tokens.controlled.map(t => {
const a = t.actor;
return a.applyDamage(roll.total);
}));
if (btn) btn.disabled = true;
}
static _applyChatCardHealing(message, event) {
if (canvas.tokens.controlled.length === 0) {
static async _applyChatCardDamage(message, event) {
if (game.user.targets.size === 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 btn = event.currentTarget;
const roll = message.rolls[0];
const amount = Math.round(roll.total * 100) / 100;
const ap = message.flags?.mgt2?.damage?.ap ?? 0;
const stun = message.flags?.mgt2?.damage?.stun ?? false;
await Promise.all([...game.user.targets].map(async t => {
const result = await t.actor.applyDamage(amount, { ap, stun });
if (stun) {
const incapRounds = result?.incapRounds ?? 0;
if (incapRounds > 0) {
ui.notifications.warn(game.i18n.format("MGT2.Notifications.StunIncapacitated", { name: t.actor.name, rounds: incapRounds }));
} else {
ui.notifications.info(game.i18n.format("MGT2.Notifications.StunDamageApplied", { name: t.actor.name, amount }));
}
} else {
const notifKey = ap > 0 ? "MGT2.Notifications.DamageAppliedAP" : "MGT2.Notifications.DamageApplied";
ui.notifications.info(game.i18n.format(notifKey, { name: t.actor.name, amount, ap }));
}
}));
await ChatHelper.#markButtonApplied(message, btn, "damage");
}
static async _applyChatCardHealing(message, event) {
if (game.user.targets.size === 0) {
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
return;
}
const btn = event.currentTarget;
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);
await Promise.all([...game.user.targets].map(async t => {
await t.actor.applyHealing(amount);
ui.notifications.info(game.i18n.format("MGT2.Notifications.HealingApplied", { name: t.actor.name, amount }));
}));
await ChatHelper.#markButtonApplied(message, btn, "healing");
}
static _applyChatCardSurgeryDamage(message, event) {
if (canvas.tokens.controlled.length === 0) {
static async _applyChatCardSurgeryDamage(message, event) {
if (game.user.targets.size === 0) {
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
return;
}
const btn = event.currentTarget;
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 });
await Promise.all([...game.user.targets].map(async t => {
await t.actor.applyDamage(amount, { ignoreArmor: true });
ui.notifications.info(game.i18n.format("MGT2.Notifications.DamageApplied", { name: t.actor.name, amount }));
}));
await ChatHelper.#markButtonApplied(message, btn, "surgeryDamage");
}
static async _rollRadiationDamage(message, event) {
event.preventDefault();
event.stopPropagation();
const damageFlags = message.flags?.mgt2?.damage;
const rollObjectName = damageFlags?.rollObjectName ?? "";
// 2D × 20 rads (MGT2 rule: 2d6 × 20)
const roll = await new Roll("2d6 * 20", {}).roll();
const chatData = {
user: game.user.id,
speaker: message.speaker,
formula: roll._formula,
tooltip: await roll.getTooltip(),
total: Math.round(roll.total),
rollObjectName,
};
const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/radiation.html", chatData);
chatData.content = html;
return roll.toMessage(chatData);
}
}

View File

@@ -13,12 +13,18 @@ export default class WeaponData extends PhysicalItemData {
schema.damage = new fields.StringField({ required: false, blank: true, trim: true });
schema.magazine = new fields.NumberField({ required: false, initial: 0, min: 0, integer: true });
schema.magazineCost = new fields.NumberField({ required: false, initial: 0, min: 0, integer: true });
schema.traits = new fields.ArrayField(
new fields.SchemaField({
name: new fields.StringField({ required: true, blank: true, trim: true }),
description: new fields.StringField({ required: false, blank: true, trim: true })
})
);
schema.traits = new fields.SchemaField({
ap: new fields.NumberField({ required: false, initial: 0, min: 0, integer: true }),
auto: new fields.NumberField({ required: false, initial: 0, min: 0, integer: true }),
blast: new fields.NumberField({ required: false, initial: 0, min: 0, integer: true }),
bulky: new fields.BooleanField({ required: false, initial: false }),
veryBulky: new fields.BooleanField({ required: false, initial: false }),
stun: new fields.BooleanField({ required: false, initial: false }),
smart: new fields.BooleanField({ required: false, initial: false }),
radiation: new fields.BooleanField({ required: false, initial: false }),
scope: new fields.BooleanField({ required: false, initial: false }),
zeroG: new fields.BooleanField({ required: false, initial: false })
});
schema.options = new fields.ArrayField(
new fields.SchemaField({
name: new fields.StringField({ required: true, blank: true, trim: true }),
@@ -27,4 +33,21 @@ export default class WeaponData extends PhysicalItemData {
);
return schema;
}
/** Returns a compact display string of active traits (e.g. "AP 2, Auto 3, Blast 5, Bulky") */
static getTraitsSummary(traits) {
if (!traits) return "";
const parts = [];
if (traits.ap > 0) parts.push(`AP ${traits.ap}`);
if (traits.auto > 0) parts.push(`Auto ${traits.auto}`);
if (traits.blast > 0) parts.push(`Blast ${traits.blast}`);
if (traits.bulky) parts.push(game.i18n.localize("MGT2.WeaponTraits.Bulky"));
if (traits.veryBulky) parts.push(game.i18n.localize("MGT2.WeaponTraits.VeryBulky"));
if (traits.stun) parts.push(game.i18n.localize("MGT2.WeaponTraits.Stun"));
if (traits.smart) parts.push(game.i18n.localize("MGT2.WeaponTraits.Smart"));
if (traits.radiation) parts.push(game.i18n.localize("MGT2.WeaponTraits.Radiation"));
if (traits.scope) parts.push(game.i18n.localize("MGT2.WeaponTraits.Scope"));
if (traits.zeroG) parts.push(game.i18n.localize("MGT2.WeaponTraits.ZeroG"));
return parts.join(", ");
}
}

View File

@@ -35,7 +35,14 @@ export class RollPromptHelper {
skillLevel: options.skillLevel ?? 0,
// Healing fields
showHeal: options.showHeal ?? false,
healType: options.healType ?? null
healType: options.healType ?? null,
// Ranged/Melee weapon flags
isRanged: options.isRanged ?? false,
isMelee: options.isMelee ?? false,
isAttack: (options.isRanged ?? false) || (options.isMelee ?? false),
autoLevel: options.autoLevel ?? 0,
hasScope: options.scope ?? false,
hasZeroG: options.zeroG ?? false,
});
return await DialogV2.wait({

View File

@@ -23,6 +23,7 @@ export const preloadHandlebarsTemplates = async function() {
"systems/mgt2/templates/roll-prompt.html",
"systems/mgt2/templates/chat/roll.html",
//"systems/mgt2/templates/chat/roll-characteristic.html",
"systems/mgt2/templates/chat/radiation.html",
"systems/mgt2/templates/actors/actor-config-sheet.html",
"systems/mgt2/templates/actors/actor-config-characteristic-sheet.html",
"systems/mgt2/templates/actors/trait-sheet.html",

View File

@@ -329,6 +329,114 @@ li.chat-message
.mgt2-effect-value
color: #EE4050
// AP info badge
.mgt2-ap-info
display: flex
align-items: center
justify-content: center
gap: 5px
font-size: 0.75rem
font-weight: 600
color: #6b4e1a
background: rgba(180,130,40,0.12)
border: 1px solid rgba(180,130,40,0.3)
border-radius: 4px
padding: 3px 8px
margin: 4px 8px 0 8px
i
color: #b48228
// Auto fire mode info badge
.mgt2-auto-info
display: flex
align-items: center
justify-content: center
gap: 5px
font-size: 0.75rem
font-weight: 600
color: #1a3f6b
background: rgba(40,90,180,0.10)
border: 1px solid rgba(40,90,180,0.25)
border-radius: 4px
padding: 3px 8px
margin: 4px 8px 0 8px
i
color: #2855b4
// Blast / explosion area info badge
.mgt2-blast-info
display: flex
align-items: center
justify-content: center
gap: 5px
font-size: 0.75rem
font-weight: 600
color: #7a2a00
background: rgba(210,80,20,0.10)
border: 1px solid rgba(210,80,20,0.30)
border-radius: 4px
padding: 3px 8px
margin: 4px 8px 0 8px
i
color: #d24a10
// Stun / incapacitating weapon info badge
.mgt2-stun-info
display: flex
align-items: center
justify-content: center
gap: 5px
font-size: 0.75rem
font-weight: 600
color: #4a1a6b
background: rgba(120,40,180,0.10)
border: 1px solid rgba(120,40,180,0.28)
border-radius: 4px
padding: 3px 8px
margin: 4px 8px 0 8px
i
color: #8a28c8
// Radiation weapon info badge
.mgt2-radiation-info
display: flex
align-items: center
justify-content: center
gap: 5px
font-size: 0.75rem
font-weight: 600
color: #1a4a1a
background: rgba(40,160,40,0.10)
border: 1px solid rgba(40,160,40,0.30)
border-radius: 4px
padding: 3px 8px
margin: 4px 8px 0 8px
i
color: #28a028
// Radiation roll result card
.mgt2-radiation-card
.mgt2-radiation-label
color: #28a028
font-weight: 700
i
margin-right: 4px
.mgt2-radiation-rules
font-size: 0.72rem
color: #555
background: rgba(40,160,40,0.07)
border: 1px solid rgba(40,160,40,0.20)
border-radius: 4px
padding: 4px 8px
margin: 4px 8px 0 8px
display: flex
gap: 5px
align-items: flex-start
i
color: #e0a020
margin-top: 2px
flex-shrink: 0
// Action buttons
.mgt2-buttons
display: flex
@@ -363,4 +471,12 @@ li.chat-message
background: #EE4050
border-color: #EE4050
color: #fff
box-shadow: 0 0 8px rgba(238,64,80,0.25)
box-shadow: 0 0 8px rgba(238,64,80,0.25)
&:disabled
background: #e8e0e0
border-color: #c8b8b8
color: #a08080
cursor: not-allowed
box-shadow: none
opacity: 0.6

View File

@@ -108,6 +108,39 @@
width: 14px !important
height: 14px !important
// Read-only state badges
.roll-prompt-states
display: flex !important
gap: 6px !important
flex-wrap: wrap !important
padding: 2px 0 !important
.roll-prompt-state-badge
display: inline-flex !important
align-items: center !important
gap: 4px !important
padding: 2px 8px !important
border-radius: 3px !important
font-family: 'Barlow Condensed', sans-serif !important
font-size: 0.75rem !important
font-weight: 600 !important
text-transform: uppercase !important
letter-spacing: 0.8px !important
background: #ede8e8 !important
border: 1px solid #ccc !important
color: #999 !important
i
font-size: 0.7rem !important
&.is-active
background: #fdf0f0 !important
border-color: #EE4050 !important
color: #EE4050 !important
strong
font-weight: 800 !important
// Footer buttons
.dialog-buttons, .form-footer, footer
background: #f5eeee !important
@@ -148,3 +181,95 @@
&:hover
background: #ff5060 !important
box-shadow: 0 4px 18px rgba(238,64,80,0.45) !important
// Ranged modifiers fieldset
.mgt2-ranged-modifiers
margin-top: 4px !important
.mgt2-ranged-checkboxes, .mgt2-ranged-dodge
display: flex !important
flex-wrap: wrap !important
gap: 6px !important
align-items: center !important
border-bottom: none !important
.mgt2-checkbox-tag
display: inline-flex !important
align-items: center !important
gap: 4px !important
padding: 2px 7px !important
border: 1px solid #ddc8c8 !important
border-radius: 3px !important
background: #fdf8f8 !important
font-family: 'Barlow Condensed', sans-serif !important
font-size: 0.73rem !important
font-weight: 600 !important
text-transform: uppercase !important
letter-spacing: 0.8px !important
color: #3a2020 !important
cursor: pointer !important
transition: border-color 0.15s, background 0.15s !important
em
font-style: normal !important
color: #999 !important
font-weight: 400 !important
input[type="checkbox"]
accent-color: #EE4050 !important
width: 13px !important
height: 13px !important
margin: 0 !important
&:has(input:checked)
border-color: #EE4050 !important
background: #fdf0f0 !important
color: #EE4050 !important
em
color: #EE4050 !important
// Auto fire mode selector
.mgt2-auto-mode
border-bottom: 1px solid #ddc8c8 !important
padding-bottom: 4px !important
margin-bottom: 2px !important
.mgt2-auto-hint
font-size: 0.72rem !important
color: #9a6520 !important
margin: 0 0 4px 0 !important
font-style: italic !important
text-align: center !important
.mgt2-scope-badge
display: inline-flex
align-items: center
gap: 5px
font-size: 0.72rem
font-weight: 600
color: #1a4060
background: rgba(30,100,180,0.10)
border: 1px solid rgba(30,100,180,0.28)
border-radius: 4px
padding: 2px 8px
margin-bottom: 4px
cursor: help
i
color: #1a6090
.mgt2-zerog-badge
display: inline-flex
align-items: center
gap: 5px
font-size: 0.72rem
font-weight: 600
color: #2a3a50
background: rgba(60,80,120,0.10)
border: 1px solid rgba(60,80,120,0.28)
border-radius: 4px
padding: 2px 8px
margin-bottom: 4px
cursor: help
i
color: #506090

View File

@@ -416,3 +416,44 @@
.editor,
.editor-container
min-height: 200px !important
// Weapon traits structured grid
.mgt2-weapon-traits
border: 1px solid var(--color-border-light-tertiary)
border-radius: 4px
padding: 6px 8px
margin-top: 4px
legend
font-size: 0.85em
font-weight: bold
padding: 0 4px
.mgt2-weapon-traits-grid
display: flex
flex-wrap: wrap
gap: 6px 12px
align-items: center
.mgt2-trait-num
display: flex
align-items: center
gap: 4px
label
font-size: 0.85em
white-space: nowrap
input[type="number"]
width: 44px
text-align: center
flex: 0 0 44px
.mgt2-trait-bool
display: flex
align-items: center
gap: 4px
label
font-size: 0.85em
white-space: nowrap
cursor: pointer
input[type="checkbox"]
cursor: pointer

2
styles/mgt2.min.css vendored

File diff suppressed because one or more lines are too long

View File

@@ -40,4 +40,10 @@
{{ localize 'MGT2.Chat.Roll.Effect' }} <span class="mgt2-effect-value">{{effectStr}}</span>
</div>
{{/if}}
{{#if showRollDamage}}
<div class="mgt2-buttons">
<button data-action="rollDamage">{{ localize 'MGT2.Chat.Roll.Damages' }}</button>
</div>
{{/if}}
</div>

View File

@@ -0,0 +1,24 @@
<div class="mgt2-chat-roll mgt2-radiation-card">
<div class="mgt2-roll-header">
<span class="mgt2-roll-char-name">{{rollObjectName}}</span>
<div class="mgt2-roll-meta">
<span class="mgt2-roll-type mgt2-radiation-label">
<i class="fa-solid fa-radiation"></i>
{{ localize 'MGT2.Chat.Radiation.Title' }}
</span>
</div>
</div>
<div class="dice-roll">
<div class="dice-result">
<div class="dice-formula">{{formula}}</div>
{{{tooltip}}}
<h4 class="dice-total">{{total}} {{ localize 'MGT2.Chat.Radiation.Rads' }}</h4>
</div>
</div>
<div class="mgt2-radiation-rules">
<i class="fa-solid fa-triangle-exclamation"></i>
{{ localize 'MGT2.Chat.Radiation.Rules' }}
</div>
</div>

View File

@@ -34,6 +34,40 @@
</div>
</div>
{{#if apValue}}
<div class="mgt2-ap-info" data-tooltip="{{ localize 'MGT2.Chat.Roll.APIgnoreHint' }}">
<i class="fa-solid fa-shield-halved"></i>
{{ localize 'MGT2.Chat.Roll.APIgnore' }} {{apValue}}
</div>
{{/if}}
{{#if blastRadius}}
<div class="mgt2-blast-info" data-tooltip="{{ localize 'MGT2.Chat.Roll.BlastHint' }}">
<i class="fa-solid fa-burst"></i>
{{ localize 'MGT2.Chat.Roll.BlastArea' }} {{blastRadius}}m — {{ localize 'MGT2.Chat.Roll.BlastRules' }}
</div>
{{/if}}
{{#if stunWeapon}}
<div class="mgt2-stun-info" data-tooltip="{{ localize 'MGT2.Chat.Roll.StunHint' }}">
<i class="fa-solid fa-bolt"></i>
{{ localize 'MGT2.Chat.Roll.StunWeapon' }}
</div>
{{/if}}
{{#if radiationWeapon}}
<div class="mgt2-radiation-info" data-tooltip="{{ localize 'MGT2.Chat.Radiation.Hint' }}">
<i class="fa-solid fa-radiation"></i>
{{ localize 'MGT2.Chat.Radiation.Badge' }}
</div>
{{/if}}
{{#if autoInfo}}
<div class="mgt2-auto-info">
<i class="fa-solid fa-gun"></i> {{autoInfo}}
</div>
{{/if}}
{{#if rollSuccess}}
<div class="mgt2-outcome is-success"><i class="fa-solid fa-check"></i> {{ localize 'MGT2.Chat.Roll.Success' }}</div>
{{else if rollFailure}}
@@ -56,6 +90,11 @@
{{#if showRollDamage}}
<button data-action="rollDamage">{{ localize 'MGT2.Chat.Roll.Damages' }}</button>
{{/if}}
{{#if radiationWeapon}}
<button data-action="rollRadiation" title="{{ localize 'MGT2.Chat.Radiation.RollButton' }}">
<i class="fa-solid fa-radiation"></i> {{ localize 'MGT2.Chat.Radiation.RollButton' }}
</button>
{{/if}}
{{#each cardButtons as |cardButton|}}
<button data-action="{{cardButton.action}}" title="{{cardButton.label}}">{{cardButton.label}}</button>
{{/each}}

View File

@@ -78,24 +78,44 @@
</div>
{{/if}}
</div>
<div class="table-container">
<div class="table-row heading">
<div class="row-item row-item-left">{{ localize 'MGT2.Items.Trait' }}</div>
<div class="row-item row-item-left">{{ localize 'MGT2.Items.Description' }}</div>
<div class="row-item row-item-right"><a class="options-create" data-property="traits"><i class="fas fa-plus"></i></a></div>
</div>
{{#each system.traits as |trait i| }}
<div class="table-row dropitem options-part" data-options-part="{{i}}" data-property="traits" role="rowgroup">
<div class="row-item row-item-left"><input type="text" name="system.traits.{{i}}.name" value="{{trait.name}}" /></div>
<div class="row-item row-item-left">
<textarea name="system.traits.{{i}}.description" rows="2">{{trait.description}}</textarea>
<fieldset class="mgt2-weapon-traits">
<legend>{{ localize 'MGT2.WeaponTraits.SectionTitle' }}</legend>
<div class="mgt2-weapon-traits-grid">
<div class="mgt2-trait-num" data-tooltip="{{ localize 'MGT2.WeaponTraits.APHint' }}">
<label>{{ localize 'MGT2.WeaponTraits.AP' }}</label>
<input type="number" name="system.traits.ap" value="{{system.traits.ap}}" data-dtype="Number" min="0" class="short" />
</div>
<div class="row-item row-item-right item-controls">
<a class="item-control options-delete" title="Delete Trait"><i class="fas fa-trash"></i></a>
<div class="mgt2-trait-num" data-tooltip="{{ localize 'MGT2.WeaponTraits.AutoHint' }}">
<label>{{ localize 'MGT2.WeaponTraits.Auto' }}</label>
<input type="number" name="system.traits.auto" value="{{system.traits.auto}}" data-dtype="Number" min="0" class="short" />
</div>
<div class="mgt2-trait-num" data-tooltip="{{ localize 'MGT2.WeaponTraits.BlastHint' }}">
<label>{{ localize 'MGT2.WeaponTraits.Blast' }}</label>
<input type="number" name="system.traits.blast" value="{{system.traits.blast}}" data-dtype="Number" min="0" class="short" />
</div>
<label class="mgt2-checkbox mgt2-trait-bool" data-tooltip="{{ localize 'MGT2.WeaponTraits.BulkyHint' }}">
<input type="checkbox" name="system.traits.bulky" data-dtype="Boolean" {{checked system.traits.bulky}} />{{ localize 'MGT2.WeaponTraits.Bulky' }}
</label>
<label class="mgt2-checkbox mgt2-trait-bool" data-tooltip="{{ localize 'MGT2.WeaponTraits.VeryBulkyHint' }}">
<input type="checkbox" name="system.traits.veryBulky" data-dtype="Boolean" {{checked system.traits.veryBulky}} />{{ localize 'MGT2.WeaponTraits.VeryBulky' }}
</label>
<label class="mgt2-checkbox mgt2-trait-bool" data-tooltip="{{ localize 'MGT2.WeaponTraits.StunHint' }}">
<input type="checkbox" name="system.traits.stun" data-dtype="Boolean" {{checked system.traits.stun}} />{{ localize 'MGT2.WeaponTraits.Stun' }}
</label>
<label class="mgt2-checkbox mgt2-trait-bool" data-tooltip="{{ localize 'MGT2.WeaponTraits.SmartHint' }}">
<input type="checkbox" name="system.traits.smart" data-dtype="Boolean" {{checked system.traits.smart}} />{{ localize 'MGT2.WeaponTraits.Smart' }}
</label>
<label class="mgt2-checkbox mgt2-trait-bool" data-tooltip="{{ localize 'MGT2.WeaponTraits.RadiationHint' }}">
<input type="checkbox" name="system.traits.radiation" data-dtype="Boolean" {{checked system.traits.radiation}} />{{ localize 'MGT2.WeaponTraits.Radiation' }}
</label>
<label class="mgt2-checkbox mgt2-trait-bool" data-tooltip="{{ localize 'MGT2.WeaponTraits.ScopeHint' }}">
<input type="checkbox" name="system.traits.scope" data-dtype="Boolean" {{checked system.traits.scope}} />{{ localize 'MGT2.WeaponTraits.Scope' }}
</label>
<label class="mgt2-checkbox mgt2-trait-bool" data-tooltip="{{ localize 'MGT2.WeaponTraits.ZeroGHint' }}">
<input type="checkbox" name="system.traits.zeroG" data-dtype="Boolean" {{checked system.traits.zeroG}} />{{ localize 'MGT2.WeaponTraits.ZeroG' }}
</label>
</div>
{{/each}}
</div>
</fieldset>
</div>
<div class="tab" data-group="primary" data-tab="tab3">
{{> systems/mgt2/templates/items/parts/sheet-configuration.html }}

View File

@@ -40,19 +40,138 @@
{{selectOptions skills selected=skill valueAttr="_id" labelAttr="name"}}
</select>
</div>
{{#unless isAttack}}
<div class="form-group">
<label>{{ localize 'MGT2.RollPrompt.Timeframes' }}</label>
<select name="timeframes">
{{selectOptions config.Timeframes selected = timeframe localize = true}}
</select>
</div>
{{/unless}}
<fieldset>
<legend>{{ localize 'MGT2.RollPrompt.States' }}</legend>
<div class="form-group">
<label class="mgt2-checkbox"><input type="checkbox" name="encumbrance" data-dtype="Boolean" {{checked encumbrance}} />{{ localize 'MGT2.RollPrompt.EncumbranceDM' }}</label>
<label class="mgt2-checkbox"><input type="checkbox" name="fatigue" data-dtype="Boolean" {{checked fatigue}} />{{ localize 'MGT2.RollPrompt.FatigueDM' }}</label>
{{!-- Hidden checkboxes preserve form values for the roll calculation --}}
<input type="checkbox" name="encumbrance" data-dtype="Boolean" {{checked encumbrance}} style="display:none" />
<input type="checkbox" name="fatigue" data-dtype="Boolean" {{checked fatigue}} style="display:none" />
{{!-- Read-only state badges --}}
<div class="roll-prompt-states">
<span class="roll-prompt-state-badge {{#if encumbrance}}is-active{{/if}}">
<i class="fa-solid fa-weight-hanging"></i>
{{ localize 'MGT2.RollPrompt.EncumbranceDM' }}
{{#if encumbrance}}<strong>2</strong>{{/if}}
</span>
<span class="roll-prompt-state-badge {{#if fatigue}}is-active{{/if}}">
<i class="fa-solid fa-person-dots-from-line"></i>
{{ localize 'MGT2.RollPrompt.FatigueDM' }}
{{#if fatigue}}<strong>2</strong>{{/if}}
</span>
</div>
</fieldset>
{{#if isRanged}}
<fieldset class="mgt2-ranged-modifiers">
<legend>{{ localize 'MGT2.RollPrompt.RangedModifiers' }}</legend>
{{#if hasScope}}
<div class="mgt2-scope-badge" data-tooltip="{{ localize 'MGT2.RollPrompt.ScopeHint' }}">
<i class="fa-solid fa-crosshairs"></i>
{{ localize 'MGT2.RollPrompt.ScopeActive' }}
</div>
{{/if}}
{{#if hasZeroG}}
<div class="mgt2-zerog-badge" data-tooltip="{{ localize 'MGT2.RollPrompt.ZeroGHint' }}">
<i class="fa-solid fa-satellite"></i>
{{ localize 'MGT2.RollPrompt.ZeroGActive' }}
</div>
{{/if}}
{{#if autoLevel}}
<div class="form-group mgt2-auto-mode">
<label>{{ localize 'MGT2.RollPrompt.FireMode' }}</label>
<select name="autoMode">
<option value="single">{{ localize 'MGT2.RollPrompt.AutoSingle' }}</option>
<option value="burst">{{ localize 'MGT2.RollPrompt.AutoBurst' }} (+{{autoLevel}})</option>
<option value="fullAuto">{{ localize 'MGT2.RollPrompt.AutoFull' }} (×{{autoLevel}})</option>
</select>
</div>
<p class="mgt2-auto-hint">⚠ {{ localize 'MGT2.RollPrompt.AutoNoAim' }}</p>
{{/if}}
<div class="form-group">
<label>{{ localize 'MGT2.RollPrompt.Range' }}</label>
<select name="rangedRange">
<option value="1">{{ localize 'MGT2.RollPrompt.RangeShort' }} (+1)</option>
<option value="0" selected>{{ localize 'MGT2.RollPrompt.RangeNormal' }}</option>
<option value="-2">{{ localize 'MGT2.RollPrompt.RangeLong' }} (2)</option>
<option value="-4">{{ localize 'MGT2.RollPrompt.RangeExtreme' }} (4)</option>
</select>
</div>
<div class="form-group">
<label>{{ localize 'MGT2.RollPrompt.Aim' }}</label>
<select name="rangedAim">
<option value="0" selected>0</option>
<option value="1">+1</option>
<option value="2">+2</option>
<option value="3">+3</option>
<option value="4">+4</option>
<option value="5">+5</option>
<option value="6">+6</option>
</select>
</div>
<div class="form-group">
<label>{{ localize 'MGT2.RollPrompt.FastTarget' }}</label>
<select name="rangedFastTarget">
<option value="0" selected>0</option>
<option value="-1">1</option>
<option value="-2">2</option>
<option value="-3">3</option>
<option value="-4">4</option>
</select>
</div>
<div class="form-group mgt2-ranged-checkboxes">
<label class="mgt2-checkbox-tag"><input type="checkbox" name="rangedLaserSight" data-dtype="Boolean" />{{ localize 'MGT2.RollPrompt.LaserSight' }} <em>(+1 si Viser)</em></label>
<label class="mgt2-checkbox-tag"><input type="checkbox" name="rangedCover" data-dtype="Boolean" />{{ localize 'MGT2.RollPrompt.Cover' }} <em>(2)</em></label>
<label class="mgt2-checkbox-tag"><input type="checkbox" name="rangedProne" data-dtype="Boolean" />{{ localize 'MGT2.RollPrompt.Prone' }} <em>(1)</em></label>
</div>
<div class="form-group mgt2-ranged-dodge">
<label class="mgt2-checkbox-tag"><input type="checkbox" name="rangedDodge" data-dtype="Boolean" />{{ localize 'MGT2.RollPrompt.Dodge' }}</label>
<select name="rangedDodgeDM">
<option value="0" selected>MD 0</option>
<option value="-1">MD 1</option>
<option value="-2">MD 2</option>
<option value="-3">MD 3</option>
<option value="-4">MD 4</option>
<option value="-5">MD 5</option>
<option value="-6">MD 6</option>
</select>
</div>
</fieldset>
{{/if}}
{{#if isMelee}}
<fieldset class="mgt2-ranged-modifiers">
<legend>{{ localize 'MGT2.RollPrompt.MeleeModifiers' }}</legend>
<div class="form-group mgt2-ranged-dodge">
<label class="mgt2-checkbox-tag"><input type="checkbox" name="meleeDodge" data-dtype="Boolean" />{{ localize 'MGT2.RollPrompt.Dodge' }}</label>
<select name="meleeDodgeDM">
<option value="0" selected>MD 0</option>
<option value="-1">MD 1</option>
<option value="-2">MD 2</option>
<option value="-3">MD 3</option>
<option value="-4">MD 4</option>
<option value="-5">MD 5</option>
<option value="-6">MD 6</option>
</select>
</div>
<div class="form-group mgt2-ranged-dodge">
<label class="mgt2-checkbox-tag"><input type="checkbox" name="meleeParry" data-dtype="Boolean" />{{ localize 'MGT2.RollPrompt.Parry' }}</label>
<select name="meleeParryDM">
<option value="0" selected>MD 0</option>
<option value="-1">MD 1</option>
<option value="-2">MD 2</option>
<option value="-3">MD 3</option>
<option value="-4">MD 4</option>
<option value="-5">MD 5</option>
<option value="-6">MD 6</option>
</select>
</div>
</fieldset>
{{/if}}
{{/if}}
<div class="form-group">