Gestion des traits d'arme et des munitions
This commit is contained in:
80
lang/en.json
80
lang/en.json
@@ -353,7 +353,33 @@
|
|||||||
"Bane": "Bane",
|
"Bane": "Bane",
|
||||||
"CreatureSkill": "Skill",
|
"CreatureSkill": "Skill",
|
||||||
"NoSkill": "No 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": {
|
"Timeframes": {
|
||||||
"Normal": "Normal",
|
"Normal": "Normal",
|
||||||
@@ -369,7 +395,22 @@
|
|||||||
"Effect": "Effect",
|
"Effect": "Effect",
|
||||||
"Dice": "Dice",
|
"Dice": "Dice",
|
||||||
"Result": "Result",
|
"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": {
|
"Items": {
|
||||||
@@ -458,6 +499,29 @@
|
|||||||
"Weightless": "Weightless",
|
"Weightless": "Weightless",
|
||||||
"Quantity": "Quantity"
|
"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": {
|
"Durations": {
|
||||||
"Seconds": "Seconds",
|
"Seconds": "Seconds",
|
||||||
"Minutes": "Minutes",
|
"Minutes": "Minutes",
|
||||||
@@ -565,10 +629,18 @@
|
|||||||
},
|
},
|
||||||
"Notifications": {
|
"Notifications": {
|
||||||
"HealingApplied": "{name} has been healed for {amount} points.",
|
"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": {
|
"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."
|
"InvalidRollFormula": "Invalid roll formula."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
80
lang/fr.json
80
lang/fr.json
@@ -353,7 +353,33 @@
|
|||||||
"Bane": "Désavantage",
|
"Bane": "Désavantage",
|
||||||
"CreatureSkill": "Compétence",
|
"CreatureSkill": "Compétence",
|
||||||
"NoSkill": "Aucune 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": {
|
"Timeframes": {
|
||||||
"Normal": "Normal",
|
"Normal": "Normal",
|
||||||
@@ -369,7 +395,22 @@
|
|||||||
"Effect": "Effet",
|
"Effect": "Effet",
|
||||||
"Dice": "Dés",
|
"Dice": "Dés",
|
||||||
"Result": "Résultat",
|
"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": {
|
"Items": {
|
||||||
@@ -458,6 +499,29 @@
|
|||||||
"Weightless": "Aucun Poids",
|
"Weightless": "Aucun Poids",
|
||||||
"Quantity": "Quantité"
|
"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": {
|
"Durations": {
|
||||||
"Seconds": "Secondes",
|
"Seconds": "Secondes",
|
||||||
"Minutes": "Minutes",
|
"Minutes": "Minutes",
|
||||||
@@ -565,10 +629,18 @@
|
|||||||
},
|
},
|
||||||
"Notifications": {
|
"Notifications": {
|
||||||
"HealingApplied": "{name} a été soigné(e) de {amount} points.",
|
"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": {
|
"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."
|
"InvalidRollFormula": "Formule de jet invalide."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
475
mgt2.bundle.js
475
mgt2.bundle.js
@@ -403,12 +403,18 @@ class WeaponData extends PhysicalItemData {
|
|||||||
schema.damage = new fields$4.StringField({ required: false, blank: true, trim: true });
|
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.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.magazineCost = new fields$4.NumberField({ required: false, initial: 0, min: 0, integer: true });
|
||||||
schema.traits = new fields$4.ArrayField(
|
schema.traits = new fields$4.SchemaField({
|
||||||
new fields$4.SchemaField({
|
ap: new fields$4.NumberField({ required: false, initial: 0, min: 0, integer: true }),
|
||||||
name: new fields$4.StringField({ required: true, blank: true, trim: true }),
|
auto: new fields$4.NumberField({ required: false, initial: 0, min: 0, integer: true }),
|
||||||
description: new fields$4.StringField({ required: false, blank: true, trim: 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(
|
schema.options = new fields$4.ArrayField(
|
||||||
new fields$4.SchemaField({
|
new fields$4.SchemaField({
|
||||||
name: new fields$4.StringField({ required: true, blank: true, trim: true }),
|
name: new fields$4.StringField({ required: true, blank: true, trim: true }),
|
||||||
@@ -417,6 +423,23 @@ class WeaponData extends PhysicalItemData {
|
|||||||
);
|
);
|
||||||
return schema;
|
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;
|
const fields$3 = foundry.data.fields;
|
||||||
@@ -943,7 +966,10 @@ class ActorCharacter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateData["system.inventory.weight"] = onHandWeight;
|
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);
|
await $this.update(updateData);
|
||||||
|
|
||||||
@@ -964,6 +990,8 @@ class ActorCharacter {
|
|||||||
let heavy = normal * 2;
|
let heavy = normal * 2;
|
||||||
foundry.utils.setProperty(changed, "system.inventory.encumbrance.normal", normal);
|
foundry.utils.setProperty(changed, "system.inventory.encumbrance.normal", normal);
|
||||||
foundry.utils.setProperty(changed, "system.inventory.encumbrance.heavy", heavy);
|
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"));
|
//console.log(foundry.utils.getProperty(changed, "system.characteristics.strength.value"));
|
||||||
@@ -1017,25 +1045,39 @@ class ActorCharacter {
|
|||||||
// $this.update({ system: { characteristics: data } });
|
// $this.update({ system: { characteristics: data } });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
static applyDamage($this, amount, { ignoreArmor = false } = {}) {
|
static async applyDamage($this, amount, { ignoreArmor = false, ap = 0, stun = false } = {}) {
|
||||||
if (isNaN(amount) || amount === 0) return;
|
if (isNaN(amount) || amount === 0) return { incapRounds: 0 };
|
||||||
const rank1 = $this.system.config.damages.rank1;
|
const rank1 = $this.system.config.damages.rank1;
|
||||||
const rank2 = $this.system.config.damages.rank2;
|
const rank2 = $this.system.config.damages.rank2;
|
||||||
const rank3 = $this.system.config.damages.rank3;
|
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 = {};
|
const data = {};
|
||||||
data[rank1] = { value: $this.system.characteristics[rank1].value };
|
data[rank1] = { value: $this.system.characteristics[rank1].value };
|
||||||
data[rank2] = { value: $this.system.characteristics[rank2].value };
|
data[rank2] = { value: $this.system.characteristics[rank2].value };
|
||||||
data[rank3] = { value: $this.system.characteristics[rank3].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)) {
|
for (const [key, rank] of Object.entries(data)) {
|
||||||
if (rank.value > 0) {
|
if (rank.value > 0) {
|
||||||
if (rank.value >= amount) {
|
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) {
|
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") {
|
if (this.type === "character") {
|
||||||
return ActorCharacter.applyDamage(this, amount, { ignoreArmor });
|
return ActorCharacter.applyDamage(this, amount, { ignoreArmor, ap, stun });
|
||||||
} else if (this.type === "creature") {
|
} 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);
|
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);
|
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);
|
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,
|
skillLevel: options.skillLevel ?? 0,
|
||||||
// Healing fields
|
// Healing fields
|
||||||
showHeal: options.showHeal ?? false,
|
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({
|
return await DialogV2$1.wait({
|
||||||
@@ -1816,13 +1867,14 @@ class CharacterPrompts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async openEditorFullView(title, html) {
|
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", {
|
const htmlContent = await renderTemplate$2("systems/mgt2/templates/editor-fullview.html", {
|
||||||
config: CONFIG.MGT2,
|
config: CONFIG.MGT2,
|
||||||
html
|
html: html ?? ""
|
||||||
});
|
});
|
||||||
game.settings.get("mgt2", "theme");
|
game.settings.get("mgt2", "theme");
|
||||||
await DialogV2.wait({
|
await DialogV2.wait({
|
||||||
window: { title },
|
window: { title: safeTitle },
|
||||||
content: htmlContent,
|
content: htmlContent,
|
||||||
rejectClose: false,
|
rejectClose: false,
|
||||||
buttons: [
|
buttons: [
|
||||||
@@ -2064,8 +2116,7 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
|
|||||||
i._range = i.system.range.isMelee
|
i._range = i.system.range.isMelee
|
||||||
? game.i18n.localize("MGT2.Melee")
|
? game.i18n.localize("MGT2.Melee")
|
||||||
: MGT2Helper.getRangeDisplay(i.system.range);
|
: MGT2Helper.getRangeDisplay(i.system.range);
|
||||||
if (i.system.traits?.length > 0)
|
i._subInfo = WeaponData.getTraitsSummary(i.system.traits);
|
||||||
i._subInfo = i.system.traits.map(x => x.name).join(", ");
|
|
||||||
weapons.push(i);
|
weapons.push(i);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -2453,7 +2504,14 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
|
|||||||
encumbrance: this.actor.system.states.encumbrance,
|
encumbrance: this.actor.system.states.encumbrance,
|
||||||
difficulty: null,
|
difficulty: null,
|
||||||
damageFormula: null,
|
damageFormula: null,
|
||||||
|
damageAP: 0,
|
||||||
|
blastRadius: 0,
|
||||||
|
stun: false,
|
||||||
|
radiation: false,
|
||||||
isMelee: false,
|
isMelee: false,
|
||||||
|
isRanged: false,
|
||||||
|
bulky: false,
|
||||||
|
veryBulky: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cardButtons = [];
|
const cardButtons = [];
|
||||||
@@ -2523,6 +2581,18 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
|
|||||||
rollOptions.damageFormula = itemObj.system.damage;
|
rollOptions.damageFormula = itemObj.system.damage;
|
||||||
if (itemObj.type === "weapon") {
|
if (itemObj.type === "weapon") {
|
||||||
rollOptions.isMelee = itemObj.system.range?.isMelee === true;
|
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.type === "disease") {
|
||||||
if (itemObj.system.subType === "disease")
|
if (itemObj.system.subType === "disease")
|
||||||
@@ -2544,6 +2614,10 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
|
|||||||
const rollModifiers = [];
|
const rollModifiers = [];
|
||||||
const rollFormulaParts = [];
|
const rollFormulaParts = [];
|
||||||
|
|
||||||
|
// Auto trait — fire mode
|
||||||
|
const autoLevel = rollOptions.autoLevel ?? 0;
|
||||||
|
const autoMode = autoLevel > 0 ? (userRollData.autoMode ?? "single") : "single";
|
||||||
|
|
||||||
if (userRollData.diceModifier) {
|
if (userRollData.diceModifier) {
|
||||||
rollFormulaParts.push("3d6", userRollData.diceModifier);
|
rollFormulaParts.push("3d6", userRollData.diceModifier);
|
||||||
} else {
|
} else {
|
||||||
@@ -2594,14 +2668,127 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
|
|||||||
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.CustomDM") + " " + (customDMVal > 0 ? `+${customDMVal}` : `${customDMVal}`));
|
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;
|
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("");
|
const rollFormula = rollFormulaParts.join("");
|
||||||
if (!Roll.validate(rollFormula)) {
|
if (!Roll.validate(rollFormula)) {
|
||||||
ui.notifications.error(game.i18n.localize("MGT2.Errors.InvalidRollFormula"));
|
ui.notifications.error(game.i18n.localize("MGT2.Errors.InvalidRollFormula"));
|
||||||
return;
|
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 });
|
let roll = await new Roll(rollFormula, this.actor.getRollData()).roll({ rollMode: userRollData.rollMode });
|
||||||
|
|
||||||
if (isInitiative && this.token?.combatant) {
|
if (isInitiative && this.token?.combatant) {
|
||||||
@@ -2623,7 +2810,7 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
|
|||||||
rollFailure = !rollSuccess;
|
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;
|
let effectiveDamageFormula = rollOptions.damageFormula || null;
|
||||||
if (effectiveDamageFormula) {
|
if (effectiveDamageFormula) {
|
||||||
if (rollEffect !== undefined && rollEffect !== 0) {
|
if (rollEffect !== undefined && rollEffect !== 0) {
|
||||||
@@ -2633,6 +2820,18 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
|
|||||||
const strDm = this.actor.system.characteristics.strength?.dm ?? 0;
|
const strDm = this.actor.system.characteristics.strength?.dm ?? 0;
|
||||||
if (strDm !== 0) effectiveDamageFormula += (strDm >= 0 ? "+" : "") + strDm;
|
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 ─────────────────────────────────────
|
// ── 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)
|
// Show damage button only if there's a formula AND (no difficulty check OR roll succeeded)
|
||||||
showRollDamage: !!effectiveDamageFormula && (!difficultyValue || rollSuccess),
|
showRollDamage: !!effectiveDamageFormula && (!difficultyValue || rollSuccess),
|
||||||
cardButtons: cardButtons,
|
cardButtons: cardButtons,
|
||||||
|
autoInfo,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (MGT2Helper.hasValue(rollOptions, "difficulty")) {
|
if (MGT2Helper.hasValue(rollOptions, "difficulty")) {
|
||||||
@@ -2675,7 +2875,7 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
|
|||||||
|
|
||||||
let flags = null;
|
let flags = null;
|
||||||
if (effectiveDamageFormula) {
|
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 (cardButtons.length > 0) {
|
||||||
if (!flags) flags = { mgt2: {} };
|
if (!flags) flags = { mgt2: {} };
|
||||||
@@ -2759,17 +2959,18 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
|
|||||||
|
|
||||||
static async #onOpenEditor(event) {
|
static async #onOpenEditor(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
await CharacterPrompts.openEditorFullView(
|
const title = this.actor.system.personal.species
|
||||||
this.actor.system.personal.species,
|
|| game.i18n.localize("MGT2.Actor.Species")
|
||||||
this.actor.system.personal.speciesText.descriptionLong
|
|| "Species";
|
||||||
);
|
const html = this.actor.system.personal.speciesText?.descriptionLong ?? "";
|
||||||
|
await CharacterPrompts.openEditorFullView(title, html);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #onHeal(event, target) {
|
static async #onHeal(event, target) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const healType = target.dataset.healType;
|
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"));
|
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3214,7 +3415,7 @@ class TravellerCreatureSheet extends MGT2ActorSheet {
|
|||||||
|
|
||||||
// ───────────────────────────────────────────────────────── Roll Helpers
|
// ───────────────────────────────────────────────────────── 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 diffTarget = MGT2Helper.getDifficultyValue(difficulty ?? "Average");
|
||||||
const hasDifficulty = !!difficulty;
|
const hasDifficulty = !!difficulty;
|
||||||
const success = hasDifficulty ? roll.total >= diffTarget : true;
|
const success = hasDifficulty ? roll.total >= diffTarget : true;
|
||||||
@@ -3228,6 +3429,8 @@ class TravellerCreatureSheet extends MGT2ActorSheet {
|
|||||||
if (extraTooltip) breakdownParts.push(extraTooltip);
|
if (extraTooltip) breakdownParts.push(extraTooltip);
|
||||||
const rollBreakdown = breakdownParts.join(" | ");
|
const rollBreakdown = breakdownParts.join(" | ");
|
||||||
|
|
||||||
|
const showRollDamage = success && !!damageFormula;
|
||||||
|
|
||||||
const chatData = {
|
const chatData = {
|
||||||
creatureName: actor.name,
|
creatureName: actor.name,
|
||||||
creatureImg: actor.img,
|
creatureImg: actor.img,
|
||||||
@@ -3243,6 +3446,7 @@ class TravellerCreatureSheet extends MGT2ActorSheet {
|
|||||||
effect: hasDifficulty ? effect : null,
|
effect: hasDifficulty ? effect : null,
|
||||||
effectStr: hasDifficulty ? effectStr : null,
|
effectStr: hasDifficulty ? effectStr : null,
|
||||||
modifiers: dm !== 0 ? [`DM ${dm >= 0 ? "+" : ""}${dm}`] : [],
|
modifiers: dm !== 0 ? [`DM ${dm >= 0 ? "+" : ""}${dm}`] : [],
|
||||||
|
showRollDamage,
|
||||||
};
|
};
|
||||||
|
|
||||||
const chatContent = await renderTemplate$1(
|
const chatContent = await renderTemplate$1(
|
||||||
@@ -3250,11 +3454,23 @@ class TravellerCreatureSheet extends MGT2ActorSheet {
|
|||||||
chatData
|
chatData
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const flags = showRollDamage ? {
|
||||||
|
mgt2: {
|
||||||
|
damage: {
|
||||||
|
formula: normalizeDice(damageFormula),
|
||||||
|
effect,
|
||||||
|
rollObjectName: actor.name,
|
||||||
|
rollTypeName: rollLabel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} : {};
|
||||||
|
|
||||||
await ChatMessage.create({
|
await ChatMessage.create({
|
||||||
content: chatContent,
|
content: chatContent,
|
||||||
speaker: ChatMessage.getSpeaker({ actor }),
|
speaker: ChatMessage.getSpeaker({ actor }),
|
||||||
rolls: [roll],
|
rolls: [roll],
|
||||||
rollMode: rollMode ?? game.settings.get("core", "rollMode"),
|
rollMode: rollMode ?? game.settings.get("core", "rollMode"),
|
||||||
|
flags,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success, effect, total: roll.total };
|
return { success, effect, total: roll.total };
|
||||||
@@ -3355,24 +3571,14 @@ class TravellerCreatureSheet extends MGT2ActorSheet {
|
|||||||
if (chosenSkill) tooltipParts.push(`${chosenSkill.name} ${skillLevel >= 0 ? "+" : ""}${skillLevel}`);
|
if (chosenSkill) tooltipParts.push(`${chosenSkill.name} ${skillLevel >= 0 ? "+" : ""}${skillLevel}`);
|
||||||
if (customDM !== 0) tooltipParts.push(`MD perso ${customDM >= 0 ? "+" : ""}${customDM}`);
|
if (customDM !== 0) tooltipParts.push(`MD perso ${customDM >= 0 ? "+" : ""}${customDM}`);
|
||||||
|
|
||||||
const { success } = await TravellerCreatureSheet.#postCreatureRoll({
|
await TravellerCreatureSheet.#postCreatureRoll({
|
||||||
actor, roll, rollLabel,
|
actor, roll, rollLabel,
|
||||||
dm,
|
dm,
|
||||||
difficulty: result.difficulty,
|
difficulty: result.difficulty,
|
||||||
rollMode: result.rollMode,
|
rollMode: result.rollMode,
|
||||||
extraTooltip: tooltipParts.join(" | "),
|
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
|
// ───────────────────────────────────────────────────────── CRUD Handlers
|
||||||
@@ -3716,6 +3922,7 @@ const preloadHandlebarsTemplates = async function() {
|
|||||||
"systems/mgt2/templates/roll-prompt.html",
|
"systems/mgt2/templates/roll-prompt.html",
|
||||||
"systems/mgt2/templates/chat/roll.html",
|
"systems/mgt2/templates/chat/roll.html",
|
||||||
//"systems/mgt2/templates/chat/roll-characteristic.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-sheet.html",
|
||||||
"systems/mgt2/templates/actors/actor-config-characteristic-sheet.html",
|
"systems/mgt2/templates/actors/actor-config-characteristic-sheet.html",
|
||||||
"systems/mgt2/templates/actors/trait-sheet.html",
|
"systems/mgt2/templates/actors/trait-sheet.html",
|
||||||
@@ -3734,12 +3941,45 @@ class ChatHelper {
|
|||||||
if (!message || !element) {
|
if (!message || !element) {
|
||||||
return;
|
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 => {
|
element.querySelectorAll('button[data-action="rollDamage"]').forEach(el => {
|
||||||
el.addEventListener('click', async event => {
|
el.addEventListener('click', async event => {
|
||||||
await this._processRollDamageButtonEvent(message, 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 => {
|
element.querySelectorAll('button[data-action="damage"]').forEach(el => {
|
||||||
el.addEventListener('click', async event => {
|
el.addEventListener('click', async event => {
|
||||||
await this._applyChatCardDamage(message, event);
|
await this._applyChatCardDamage(message, event);
|
||||||
@@ -3791,75 +4031,144 @@ class ChatHelper {
|
|||||||
static async _processRollDamageButtonEvent(message, event) {
|
static async _processRollDamageButtonEvent(message, event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
let rollFormula = message.flags.mgt2.damage.formula;
|
const damageFlags = message.flags?.mgt2?.damage;
|
||||||
|
if (!damageFlags?.formula) {
|
||||||
let roll = await new Roll(rollFormula, {}).roll();
|
ui.notifications.warn(game.i18n.localize("MGT2.Errors.InvalidRollFormula"));
|
||||||
|
return;
|
||||||
let speaker;
|
|
||||||
let selectTokens = canvas.tokens.controlled;
|
|
||||||
if (selectTokens.length > 0) {
|
|
||||||
speaker = selectTokens[0].actor;
|
|
||||||
} else {
|
|
||||||
speaker = game.user.character;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = {
|
const chatData = {
|
||||||
user: game.user.id,
|
user: game.user.id,
|
||||||
speaker: ChatMessage.getSpeaker({ actor: speaker }),
|
speaker: message.speaker,
|
||||||
formula: roll._formula,
|
formula: roll._formula,
|
||||||
tooltip: await roll.getTooltip(),
|
tooltip: await roll.getTooltip(),
|
||||||
total: Math.round(roll.total * 100) / 100,
|
total: Math.round(roll.total * 100) / 100,
|
||||||
showButtons: true,
|
showButtons: true,
|
||||||
hasDamage: true,
|
hasDamage: true,
|
||||||
rollTypeName: rollTypeName,
|
rollTypeName,
|
||||||
rollObjectName: message.flags.mgt2.damage.rollObjectName
|
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);
|
const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/roll.html", chatData);
|
||||||
chatData.content = html;
|
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);
|
return roll.toMessage(chatData);
|
||||||
}
|
}
|
||||||
|
|
||||||
static _applyChatCardDamage(message, event) {
|
static async #markButtonApplied(message, btn, action) {
|
||||||
if (canvas.tokens.controlled.length === 0) {
|
const existing = message.flags?.mgt2?.appliedActions ?? [];
|
||||||
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
|
if (!existing.includes(action)) {
|
||||||
return;
|
await message.setFlag("mgt2", "appliedActions", [...existing, action]);
|
||||||
}
|
}
|
||||||
const roll = message.rolls[0];
|
if (btn) btn.disabled = true;
|
||||||
return Promise.all(canvas.tokens.controlled.map(t => {
|
|
||||||
const a = t.actor;
|
|
||||||
return a.applyDamage(roll.total);
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static _applyChatCardHealing(message, event) {
|
static async _applyChatCardDamage(message, event) {
|
||||||
if (canvas.tokens.controlled.length === 0) {
|
if (game.user.targets.size === 0) {
|
||||||
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
|
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
|
||||||
return;
|
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
|
const amount = message.flags?.mgt2?.healing?.amount
|
||||||
?? message.flags?.mgt2?.surgery?.healing
|
?? message.flags?.mgt2?.surgery?.healing
|
||||||
?? Math.max(1, message.rolls[0].total);
|
?? Math.max(1, message.rolls[0].total);
|
||||||
return Promise.all(canvas.tokens.controlled.map(t => {
|
await Promise.all([...game.user.targets].map(async t => {
|
||||||
const a = t.actor;
|
await t.actor.applyHealing(amount);
|
||||||
return a.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) {
|
static async _applyChatCardSurgeryDamage(message, event) {
|
||||||
if (canvas.tokens.controlled.length === 0) {
|
if (game.user.targets.size === 0) {
|
||||||
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
|
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const btn = event.currentTarget;
|
||||||
const amount = message.flags?.mgt2?.surgery?.surgeryDamage ?? 3;
|
const amount = message.flags?.mgt2?.surgery?.surgeryDamage ?? 3;
|
||||||
return Promise.all(canvas.tokens.controlled.map(t => {
|
await Promise.all([...game.user.targets].map(async t => {
|
||||||
const a = t.actor;
|
await t.actor.applyDamage(amount, { ignoreArmor: true });
|
||||||
return a.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
@@ -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") {
|
if (this.type === "character") {
|
||||||
return ActorCharacter.applyDamage(this, amount, { ignoreArmor });
|
return ActorCharacter.applyDamage(this, amount, { ignoreArmor, ap, stun });
|
||||||
} else if (this.type === "creature") {
|
} 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);
|
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);
|
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);
|
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 }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,13 +51,14 @@ export class CharacterPrompts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async openEditorFullView(title, html) {
|
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", {
|
const htmlContent = await renderTemplate("systems/mgt2/templates/editor-fullview.html", {
|
||||||
config: CONFIG.MGT2,
|
config: CONFIG.MGT2,
|
||||||
html
|
html: html ?? ""
|
||||||
});
|
});
|
||||||
const theme = game.settings.get("mgt2", "theme");
|
const theme = game.settings.get("mgt2", "theme");
|
||||||
await DialogV2.wait({
|
await DialogV2.wait({
|
||||||
window: { title },
|
window: { title: safeTitle },
|
||||||
content: htmlContent,
|
content: htmlContent,
|
||||||
rejectClose: false,
|
rejectClose: false,
|
||||||
buttons: [
|
buttons: [
|
||||||
|
|||||||
@@ -254,7 +254,10 @@ export class ActorCharacter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateData["system.inventory.weight"] = onHandWeight;
|
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);
|
await $this.update(updateData);
|
||||||
|
|
||||||
@@ -275,6 +278,8 @@ export class ActorCharacter {
|
|||||||
let heavy = normal * 2;
|
let heavy = normal * 2;
|
||||||
foundry.utils.setProperty(changed, "system.inventory.encumbrance.normal", normal);
|
foundry.utils.setProperty(changed, "system.inventory.encumbrance.normal", normal);
|
||||||
foundry.utils.setProperty(changed, "system.inventory.encumbrance.heavy", heavy);
|
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"));
|
//console.log(foundry.utils.getProperty(changed, "system.characteristics.strength.value"));
|
||||||
@@ -331,25 +336,39 @@ export class ActorCharacter {
|
|||||||
// $this.update({ system: { characteristics: data } });
|
// $this.update({ system: { characteristics: data } });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
static applyDamage($this, amount, { ignoreArmor = false } = {}) {
|
static async applyDamage($this, amount, { ignoreArmor = false, ap = 0, stun = false } = {}) {
|
||||||
if (isNaN(amount) || amount === 0) return;
|
if (isNaN(amount) || amount === 0) return { incapRounds: 0 };
|
||||||
const rank1 = $this.system.config.damages.rank1;
|
const rank1 = $this.system.config.damages.rank1;
|
||||||
const rank2 = $this.system.config.damages.rank2;
|
const rank2 = $this.system.config.damages.rank2;
|
||||||
const rank3 = $this.system.config.damages.rank3;
|
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 = {};
|
const data = {};
|
||||||
data[rank1] = { value: $this.system.characteristics[rank1].value };
|
data[rank1] = { value: $this.system.characteristics[rank1].value };
|
||||||
data[rank2] = { value: $this.system.characteristics[rank2].value };
|
data[rank2] = { value: $this.system.characteristics[rank2].value };
|
||||||
data[rank3] = { value: $this.system.characteristics[rank3].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)) {
|
for (const [key, rank] of Object.entries(data)) {
|
||||||
if (rank.value > 0) {
|
if (rank.value > 0) {
|
||||||
if (rank.value >= amount) {
|
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) {
|
static applyHealing($this, amount, type) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { MGT2 } from "../../config.js";
|
|||||||
import { MGT2Helper } from "../../helper.js";
|
import { MGT2Helper } from "../../helper.js";
|
||||||
import { RollPromptHelper } from "../../roll-prompt.js";
|
import { RollPromptHelper } from "../../roll-prompt.js";
|
||||||
import { CharacterPrompts } from "../../actors/character-prompts.js";
|
import { CharacterPrompts } from "../../actors/character-prompts.js";
|
||||||
|
import WeaponData from "../../models/items/weapon.mjs";
|
||||||
|
|
||||||
export default class TravellerCharacterSheet extends MGT2ActorSheet {
|
export default class TravellerCharacterSheet extends MGT2ActorSheet {
|
||||||
|
|
||||||
@@ -200,8 +201,7 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
|
|||||||
i._range = i.system.range.isMelee
|
i._range = i.system.range.isMelee
|
||||||
? game.i18n.localize("MGT2.Melee")
|
? game.i18n.localize("MGT2.Melee")
|
||||||
: MGT2Helper.getRangeDisplay(i.system.range);
|
: MGT2Helper.getRangeDisplay(i.system.range);
|
||||||
if (i.system.traits?.length > 0)
|
i._subInfo = WeaponData.getTraitsSummary(i.system.traits);
|
||||||
i._subInfo = i.system.traits.map(x => x.name).join(", ");
|
|
||||||
weapons.push(i);
|
weapons.push(i);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -589,7 +589,14 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
|
|||||||
encumbrance: this.actor.system.states.encumbrance,
|
encumbrance: this.actor.system.states.encumbrance,
|
||||||
difficulty: null,
|
difficulty: null,
|
||||||
damageFormula: null,
|
damageFormula: null,
|
||||||
|
damageAP: 0,
|
||||||
|
blastRadius: 0,
|
||||||
|
stun: false,
|
||||||
|
radiation: false,
|
||||||
isMelee: false,
|
isMelee: false,
|
||||||
|
isRanged: false,
|
||||||
|
bulky: false,
|
||||||
|
veryBulky: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cardButtons = [];
|
const cardButtons = [];
|
||||||
@@ -659,6 +666,18 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
|
|||||||
rollOptions.damageFormula = itemObj.system.damage;
|
rollOptions.damageFormula = itemObj.system.damage;
|
||||||
if (itemObj.type === "weapon") {
|
if (itemObj.type === "weapon") {
|
||||||
rollOptions.isMelee = itemObj.system.range?.isMelee === true;
|
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.type === "disease") {
|
||||||
if (itemObj.system.subType === "disease")
|
if (itemObj.system.subType === "disease")
|
||||||
@@ -680,6 +699,10 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
|
|||||||
const rollModifiers = [];
|
const rollModifiers = [];
|
||||||
const rollFormulaParts = [];
|
const rollFormulaParts = [];
|
||||||
|
|
||||||
|
// Auto trait — fire mode
|
||||||
|
const autoLevel = rollOptions.autoLevel ?? 0;
|
||||||
|
const autoMode = autoLevel > 0 ? (userRollData.autoMode ?? "single") : "single";
|
||||||
|
|
||||||
if (userRollData.diceModifier) {
|
if (userRollData.diceModifier) {
|
||||||
rollFormulaParts.push("3d6", userRollData.diceModifier);
|
rollFormulaParts.push("3d6", userRollData.diceModifier);
|
||||||
} else {
|
} else {
|
||||||
@@ -730,14 +753,127 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
|
|||||||
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.CustomDM") + " " + (customDMVal > 0 ? `+${customDMVal}` : `${customDMVal}`));
|
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;
|
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("");
|
const rollFormula = rollFormulaParts.join("");
|
||||||
if (!Roll.validate(rollFormula)) {
|
if (!Roll.validate(rollFormula)) {
|
||||||
ui.notifications.error(game.i18n.localize("MGT2.Errors.InvalidRollFormula"));
|
ui.notifications.error(game.i18n.localize("MGT2.Errors.InvalidRollFormula"));
|
||||||
return;
|
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 });
|
let roll = await new Roll(rollFormula, this.actor.getRollData()).roll({ rollMode: userRollData.rollMode });
|
||||||
|
|
||||||
if (isInitiative && this.token?.combatant) {
|
if (isInitiative && this.token?.combatant) {
|
||||||
@@ -759,7 +895,7 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
|
|||||||
rollFailure = !rollSuccess;
|
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;
|
let effectiveDamageFormula = rollOptions.damageFormula || null;
|
||||||
if (effectiveDamageFormula) {
|
if (effectiveDamageFormula) {
|
||||||
if (rollEffect !== undefined && rollEffect !== 0) {
|
if (rollEffect !== undefined && rollEffect !== 0) {
|
||||||
@@ -769,6 +905,18 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
|
|||||||
const strDm = this.actor.system.characteristics.strength?.dm ?? 0;
|
const strDm = this.actor.system.characteristics.strength?.dm ?? 0;
|
||||||
if (strDm !== 0) effectiveDamageFormula += (strDm >= 0 ? "+" : "") + strDm;
|
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 ─────────────────────────────────────
|
// ── 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)
|
// Show damage button only if there's a formula AND (no difficulty check OR roll succeeded)
|
||||||
showRollDamage: !!effectiveDamageFormula && (!difficultyValue || rollSuccess),
|
showRollDamage: !!effectiveDamageFormula && (!difficultyValue || rollSuccess),
|
||||||
cardButtons: cardButtons,
|
cardButtons: cardButtons,
|
||||||
|
autoInfo,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (MGT2Helper.hasValue(rollOptions, "difficulty")) {
|
if (MGT2Helper.hasValue(rollOptions, "difficulty")) {
|
||||||
@@ -811,7 +960,7 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
|
|||||||
|
|
||||||
let flags = null;
|
let flags = null;
|
||||||
if (effectiveDamageFormula) {
|
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 (cardButtons.length > 0) {
|
||||||
if (!flags) flags = { mgt2: {} };
|
if (!flags) flags = { mgt2: {} };
|
||||||
@@ -895,17 +1044,18 @@ export default class TravellerCharacterSheet extends MGT2ActorSheet {
|
|||||||
|
|
||||||
static async #onOpenEditor(event) {
|
static async #onOpenEditor(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
await CharacterPrompts.openEditorFullView(
|
const title = this.actor.system.personal.species
|
||||||
this.actor.system.personal.species,
|
|| game.i18n.localize("MGT2.Actor.Species")
|
||||||
this.actor.system.personal.speciesText.descriptionLong
|
|| "Species";
|
||||||
);
|
const html = this.actor.system.personal.speciesText?.descriptionLong ?? "";
|
||||||
|
await CharacterPrompts.openEditorFullView(title, html);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #onHeal(event, target) {
|
static async #onHeal(event, target) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const healType = target.dataset.healType;
|
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"));
|
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export default class TravellerCreatureSheet extends MGT2ActorSheet {
|
|||||||
|
|
||||||
// ───────────────────────────────────────────────────────── Roll Helpers
|
// ───────────────────────────────────────────────────────── 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 diffTarget = MGT2Helper.getDifficultyValue(difficulty ?? "Average");
|
||||||
const hasDifficulty = !!difficulty;
|
const hasDifficulty = !!difficulty;
|
||||||
const success = hasDifficulty ? roll.total >= diffTarget : true;
|
const success = hasDifficulty ? roll.total >= diffTarget : true;
|
||||||
@@ -111,6 +111,8 @@ export default class TravellerCreatureSheet extends MGT2ActorSheet {
|
|||||||
if (extraTooltip) breakdownParts.push(extraTooltip);
|
if (extraTooltip) breakdownParts.push(extraTooltip);
|
||||||
const rollBreakdown = breakdownParts.join(" | ");
|
const rollBreakdown = breakdownParts.join(" | ");
|
||||||
|
|
||||||
|
const showRollDamage = success && !!damageFormula;
|
||||||
|
|
||||||
const chatData = {
|
const chatData = {
|
||||||
creatureName: actor.name,
|
creatureName: actor.name,
|
||||||
creatureImg: actor.img,
|
creatureImg: actor.img,
|
||||||
@@ -126,6 +128,7 @@ export default class TravellerCreatureSheet extends MGT2ActorSheet {
|
|||||||
effect: hasDifficulty ? effect : null,
|
effect: hasDifficulty ? effect : null,
|
||||||
effectStr: hasDifficulty ? effectStr : null,
|
effectStr: hasDifficulty ? effectStr : null,
|
||||||
modifiers: dm !== 0 ? [`DM ${dm >= 0 ? "+" : ""}${dm}`] : [],
|
modifiers: dm !== 0 ? [`DM ${dm >= 0 ? "+" : ""}${dm}`] : [],
|
||||||
|
showRollDamage,
|
||||||
};
|
};
|
||||||
|
|
||||||
const chatContent = await renderTemplate(
|
const chatContent = await renderTemplate(
|
||||||
@@ -133,11 +136,23 @@ export default class TravellerCreatureSheet extends MGT2ActorSheet {
|
|||||||
chatData
|
chatData
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const flags = showRollDamage ? {
|
||||||
|
mgt2: {
|
||||||
|
damage: {
|
||||||
|
formula: normalizeDice(damageFormula),
|
||||||
|
effect,
|
||||||
|
rollObjectName: actor.name,
|
||||||
|
rollTypeName: rollLabel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} : {};
|
||||||
|
|
||||||
await ChatMessage.create({
|
await ChatMessage.create({
|
||||||
content: chatContent,
|
content: chatContent,
|
||||||
speaker: ChatMessage.getSpeaker({ actor }),
|
speaker: ChatMessage.getSpeaker({ actor }),
|
||||||
rolls: [roll],
|
rolls: [roll],
|
||||||
rollMode: rollMode ?? game.settings.get("core", "rollMode"),
|
rollMode: rollMode ?? game.settings.get("core", "rollMode"),
|
||||||
|
flags,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success, effect, total: roll.total };
|
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 (chosenSkill) tooltipParts.push(`${chosenSkill.name} ${skillLevel >= 0 ? "+" : ""}${skillLevel}`);
|
||||||
if (customDM !== 0) tooltipParts.push(`MD perso ${customDM >= 0 ? "+" : ""}${customDM}`);
|
if (customDM !== 0) tooltipParts.push(`MD perso ${customDM >= 0 ? "+" : ""}${customDM}`);
|
||||||
|
|
||||||
const { success } = await TravellerCreatureSheet.#postCreatureRoll({
|
await TravellerCreatureSheet.#postCreatureRoll({
|
||||||
actor, roll, rollLabel,
|
actor, roll, rollLabel,
|
||||||
dm,
|
dm,
|
||||||
difficulty: result.difficulty,
|
difficulty: result.difficulty,
|
||||||
rollMode: result.rollMode,
|
rollMode: result.rollMode,
|
||||||
extraTooltip: tooltipParts.join(" | "),
|
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
|
// ───────────────────────────────────────────────────────── CRUD Handlers
|
||||||
|
|||||||
@@ -6,12 +6,45 @@ export class ChatHelper {
|
|||||||
if (!message || !element) {
|
if (!message || !element) {
|
||||||
return;
|
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 => {
|
element.querySelectorAll('button[data-action="rollDamage"]').forEach(el => {
|
||||||
el.addEventListener('click', async event => {
|
el.addEventListener('click', async event => {
|
||||||
await this._processRollDamageButtonEvent(message, 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 => {
|
element.querySelectorAll('button[data-action="damage"]').forEach(el => {
|
||||||
el.addEventListener('click', async event => {
|
el.addEventListener('click', async event => {
|
||||||
await this._applyChatCardDamage(message, event);
|
await this._applyChatCardDamage(message, event);
|
||||||
@@ -63,74 +96,143 @@ export class ChatHelper {
|
|||||||
static async _processRollDamageButtonEvent(message, event) {
|
static async _processRollDamageButtonEvent(message, event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
let rollFormula = message.flags.mgt2.damage.formula;
|
const damageFlags = message.flags?.mgt2?.damage;
|
||||||
|
if (!damageFlags?.formula) {
|
||||||
let roll = await new Roll(rollFormula, {}).roll();
|
ui.notifications.warn(game.i18n.localize("MGT2.Errors.InvalidRollFormula"));
|
||||||
|
return;
|
||||||
let speaker;
|
|
||||||
let selectTokens = canvas.tokens.controlled;
|
|
||||||
if (selectTokens.length > 0) {
|
|
||||||
speaker = selectTokens[0].actor;
|
|
||||||
} else {
|
|
||||||
speaker = game.user.character;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = {
|
const chatData = {
|
||||||
user: game.user.id,
|
user: game.user.id,
|
||||||
speaker: ChatMessage.getSpeaker({ actor: speaker }),
|
speaker: message.speaker,
|
||||||
formula: roll._formula,
|
formula: roll._formula,
|
||||||
tooltip: await roll.getTooltip(),
|
tooltip: await roll.getTooltip(),
|
||||||
total: Math.round(roll.total * 100) / 100,
|
total: Math.round(roll.total * 100) / 100,
|
||||||
showButtons: true,
|
showButtons: true,
|
||||||
hasDamage: true,
|
hasDamage: true,
|
||||||
rollTypeName: rollTypeName,
|
rollTypeName,
|
||||||
rollObjectName: message.flags.mgt2.damage.rollObjectName
|
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);
|
const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/roll.html", chatData);
|
||||||
chatData.content = html;
|
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);
|
return roll.toMessage(chatData);
|
||||||
}
|
}
|
||||||
|
|
||||||
static _applyChatCardDamage(message, event) {
|
static async #markButtonApplied(message, btn, action) {
|
||||||
if (canvas.tokens.controlled.length === 0) {
|
const existing = message.flags?.mgt2?.appliedActions ?? [];
|
||||||
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
|
if (!existing.includes(action)) {
|
||||||
return;
|
await message.setFlag("mgt2", "appliedActions", [...existing, action]);
|
||||||
}
|
}
|
||||||
const roll = message.rolls[0];
|
if (btn) btn.disabled = true;
|
||||||
return Promise.all(canvas.tokens.controlled.map(t => {
|
|
||||||
const a = t.actor;
|
|
||||||
return a.applyDamage(roll.total);
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static _applyChatCardHealing(message, event) {
|
static async _applyChatCardDamage(message, event) {
|
||||||
if (canvas.tokens.controlled.length === 0) {
|
if (game.user.targets.size === 0) {
|
||||||
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
|
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
|
||||||
return;
|
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
|
const amount = message.flags?.mgt2?.healing?.amount
|
||||||
?? message.flags?.mgt2?.surgery?.healing
|
?? message.flags?.mgt2?.surgery?.healing
|
||||||
?? Math.max(1, message.rolls[0].total);
|
?? Math.max(1, message.rolls[0].total);
|
||||||
return Promise.all(canvas.tokens.controlled.map(t => {
|
await Promise.all([...game.user.targets].map(async t => {
|
||||||
const a = t.actor;
|
await t.actor.applyHealing(amount);
|
||||||
return a.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) {
|
static async _applyChatCardSurgeryDamage(message, event) {
|
||||||
if (canvas.tokens.controlled.length === 0) {
|
if (game.user.targets.size === 0) {
|
||||||
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
|
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const btn = event.currentTarget;
|
||||||
const amount = message.flags?.mgt2?.surgery?.surgeryDamage ?? 3;
|
const amount = message.flags?.mgt2?.surgery?.surgeryDamage ?? 3;
|
||||||
return Promise.all(canvas.tokens.controlled.map(t => {
|
await Promise.all([...game.user.targets].map(async t => {
|
||||||
const a = t.actor;
|
await t.actor.applyDamage(amount, { ignoreArmor: true });
|
||||||
return a.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,12 +13,18 @@ export default class WeaponData extends PhysicalItemData {
|
|||||||
schema.damage = new fields.StringField({ required: false, blank: true, trim: true });
|
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.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.magazineCost = new fields.NumberField({ required: false, initial: 0, min: 0, integer: true });
|
||||||
schema.traits = new fields.ArrayField(
|
schema.traits = new fields.SchemaField({
|
||||||
new fields.SchemaField({
|
ap: new fields.NumberField({ required: false, initial: 0, min: 0, integer: true }),
|
||||||
name: new fields.StringField({ required: true, blank: true, trim: true }),
|
auto: new fields.NumberField({ required: false, initial: 0, min: 0, integer: true }),
|
||||||
description: new fields.StringField({ required: false, blank: true, trim: 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(
|
schema.options = new fields.ArrayField(
|
||||||
new fields.SchemaField({
|
new fields.SchemaField({
|
||||||
name: new fields.StringField({ required: true, blank: true, trim: true }),
|
name: new fields.StringField({ required: true, blank: true, trim: true }),
|
||||||
@@ -27,4 +33,21 @@ export default class WeaponData extends PhysicalItemData {
|
|||||||
);
|
);
|
||||||
return schema;
|
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(", ");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,14 @@ export class RollPromptHelper {
|
|||||||
skillLevel: options.skillLevel ?? 0,
|
skillLevel: options.skillLevel ?? 0,
|
||||||
// Healing fields
|
// Healing fields
|
||||||
showHeal: options.showHeal ?? false,
|
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({
|
return await DialogV2.wait({
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export const preloadHandlebarsTemplates = async function() {
|
|||||||
"systems/mgt2/templates/roll-prompt.html",
|
"systems/mgt2/templates/roll-prompt.html",
|
||||||
"systems/mgt2/templates/chat/roll.html",
|
"systems/mgt2/templates/chat/roll.html",
|
||||||
//"systems/mgt2/templates/chat/roll-characteristic.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-sheet.html",
|
||||||
"systems/mgt2/templates/actors/actor-config-characteristic-sheet.html",
|
"systems/mgt2/templates/actors/actor-config-characteristic-sheet.html",
|
||||||
"systems/mgt2/templates/actors/trait-sheet.html",
|
"systems/mgt2/templates/actors/trait-sheet.html",
|
||||||
|
|||||||
@@ -329,6 +329,114 @@ li.chat-message
|
|||||||
.mgt2-effect-value
|
.mgt2-effect-value
|
||||||
color: #EE4050
|
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 ──
|
// ── Action buttons ──
|
||||||
.mgt2-buttons
|
.mgt2-buttons
|
||||||
display: flex
|
display: flex
|
||||||
@@ -363,4 +471,12 @@ li.chat-message
|
|||||||
background: #EE4050
|
background: #EE4050
|
||||||
border-color: #EE4050
|
border-color: #EE4050
|
||||||
color: #fff
|
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
|
||||||
@@ -108,6 +108,39 @@
|
|||||||
width: 14px !important
|
width: 14px !important
|
||||||
height: 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
|
// Footer buttons
|
||||||
.dialog-buttons, .form-footer, footer
|
.dialog-buttons, .form-footer, footer
|
||||||
background: #f5eeee !important
|
background: #f5eeee !important
|
||||||
@@ -148,3 +181,95 @@
|
|||||||
&:hover
|
&:hover
|
||||||
background: #ff5060 !important
|
background: #ff5060 !important
|
||||||
box-shadow: 0 4px 18px rgba(238,64,80,0.45) !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
|
||||||
|
|||||||
@@ -416,3 +416,44 @@
|
|||||||
.editor,
|
.editor,
|
||||||
.editor-container
|
.editor-container
|
||||||
min-height: 200px !important
|
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
2
styles/mgt2.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -40,4 +40,10 @@
|
|||||||
{{ localize 'MGT2.Chat.Roll.Effect' }} <span class="mgt2-effect-value">{{effectStr}}</span>
|
{{ localize 'MGT2.Chat.Roll.Effect' }} <span class="mgt2-effect-value">{{effectStr}}</span>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if showRollDamage}}
|
||||||
|
<div class="mgt2-buttons">
|
||||||
|
<button data-action="rollDamage">{{ localize 'MGT2.Chat.Roll.Damages' }}</button>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
24
templates/chat/radiation.html
Normal file
24
templates/chat/radiation.html
Normal 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>
|
||||||
@@ -34,6 +34,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</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}}
|
{{#if rollSuccess}}
|
||||||
<div class="mgt2-outcome is-success"><i class="fa-solid fa-check"></i> {{ localize 'MGT2.Chat.Roll.Success' }}</div>
|
<div class="mgt2-outcome is-success"><i class="fa-solid fa-check"></i> {{ localize 'MGT2.Chat.Roll.Success' }}</div>
|
||||||
{{else if rollFailure}}
|
{{else if rollFailure}}
|
||||||
@@ -56,6 +90,11 @@
|
|||||||
{{#if showRollDamage}}
|
{{#if showRollDamage}}
|
||||||
<button data-action="rollDamage">{{ localize 'MGT2.Chat.Roll.Damages' }}</button>
|
<button data-action="rollDamage">{{ localize 'MGT2.Chat.Roll.Damages' }}</button>
|
||||||
{{/if}}
|
{{/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|}}
|
{{#each cardButtons as |cardButton|}}
|
||||||
<button data-action="{{cardButton.action}}" title="{{cardButton.label}}">{{cardButton.label}}</button>
|
<button data-action="{{cardButton.action}}" title="{{cardButton.label}}">{{cardButton.label}}</button>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|||||||
@@ -78,24 +78,44 @@
|
|||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
<div class="table-container">
|
<fieldset class="mgt2-weapon-traits">
|
||||||
<div class="table-row heading">
|
<legend>{{ localize 'MGT2.WeaponTraits.SectionTitle' }}</legend>
|
||||||
<div class="row-item row-item-left">{{ localize 'MGT2.Items.Trait' }}</div>
|
<div class="mgt2-weapon-traits-grid">
|
||||||
<div class="row-item row-item-left">{{ localize 'MGT2.Items.Description' }}</div>
|
<div class="mgt2-trait-num" data-tooltip="{{ localize 'MGT2.WeaponTraits.APHint' }}">
|
||||||
<div class="row-item row-item-right"><a class="options-create" data-property="traits"><i class="fas fa-plus"></i></a></div>
|
<label>{{ localize 'MGT2.WeaponTraits.AP' }}</label>
|
||||||
</div>
|
<input type="number" name="system.traits.ap" value="{{system.traits.ap}}" data-dtype="Number" min="0" class="short" />
|
||||||
{{#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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row-item row-item-right item-controls">
|
<div class="mgt2-trait-num" data-tooltip="{{ localize 'MGT2.WeaponTraits.AutoHint' }}">
|
||||||
<a class="item-control options-delete" title="Delete Trait"><i class="fas fa-trash"></i></a>
|
<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>
|
||||||
|
<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>
|
</div>
|
||||||
{{/each}}
|
</fieldset>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tab" data-group="primary" data-tab="tab3">
|
<div class="tab" data-group="primary" data-tab="tab3">
|
||||||
{{> systems/mgt2/templates/items/parts/sheet-configuration.html }}
|
{{> systems/mgt2/templates/items/parts/sheet-configuration.html }}
|
||||||
|
|||||||
@@ -40,19 +40,138 @@
|
|||||||
{{selectOptions skills selected=skill valueAttr="_id" labelAttr="name"}}
|
{{selectOptions skills selected=skill valueAttr="_id" labelAttr="name"}}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
{{#unless isAttack}}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{ localize 'MGT2.RollPrompt.Timeframes' }}</label>
|
<label>{{ localize 'MGT2.RollPrompt.Timeframes' }}</label>
|
||||||
<select name="timeframes">
|
<select name="timeframes">
|
||||||
{{selectOptions config.Timeframes selected = timeframe localize = true}}
|
{{selectOptions config.Timeframes selected = timeframe localize = true}}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
{{/unless}}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{{ localize 'MGT2.RollPrompt.States' }}</legend>
|
<legend>{{ localize 'MGT2.RollPrompt.States' }}</legend>
|
||||||
<div class="form-group">
|
{{!-- Hidden checkboxes preserve form values for the roll calculation --}}
|
||||||
<label class="mgt2-checkbox"><input type="checkbox" name="encumbrance" data-dtype="Boolean" {{checked encumbrance}} />{{ localize 'MGT2.RollPrompt.EncumbranceDM' }}</label>
|
<input type="checkbox" name="encumbrance" data-dtype="Boolean" {{checked encumbrance}} style="display:none" />
|
||||||
<label class="mgt2-checkbox"><input type="checkbox" name="fatigue" data-dtype="Boolean" {{checked fatigue}} />{{ localize 'MGT2.RollPrompt.FatigueDM' }}</label>
|
<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>
|
</div>
|
||||||
</fieldset>
|
</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}}
|
{{/if}}
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
Reference in New Issue
Block a user