Gestion des traits d'arme et des munitions
This commit is contained in:
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.magazine = new fields$4.NumberField({ required: false, initial: 0, min: 0, integer: true });
|
||||
schema.magazineCost = new fields$4.NumberField({ required: false, initial: 0, min: 0, integer: true });
|
||||
schema.traits = new fields$4.ArrayField(
|
||||
new fields$4.SchemaField({
|
||||
name: new fields$4.StringField({ required: true, blank: true, trim: true }),
|
||||
description: new fields$4.StringField({ required: false, blank: true, trim: true })
|
||||
})
|
||||
);
|
||||
schema.traits = new fields$4.SchemaField({
|
||||
ap: new fields$4.NumberField({ required: false, initial: 0, min: 0, integer: true }),
|
||||
auto: new fields$4.NumberField({ required: false, initial: 0, min: 0, integer: true }),
|
||||
blast: new fields$4.NumberField({ required: false, initial: 0, min: 0, integer: true }),
|
||||
bulky: new fields$4.BooleanField({ required: false, initial: false }),
|
||||
veryBulky: new fields$4.BooleanField({ required: false, initial: false }),
|
||||
stun: new fields$4.BooleanField({ required: false, initial: false }),
|
||||
smart: new fields$4.BooleanField({ required: false, initial: false }),
|
||||
radiation: new fields$4.BooleanField({ required: false, initial: false }),
|
||||
scope: new fields$4.BooleanField({ required: false, initial: false }),
|
||||
zeroG: new fields$4.BooleanField({ required: false, initial: false })
|
||||
});
|
||||
schema.options = new fields$4.ArrayField(
|
||||
new fields$4.SchemaField({
|
||||
name: new fields$4.StringField({ required: true, blank: true, trim: true }),
|
||||
@@ -417,6 +423,23 @@ class WeaponData extends PhysicalItemData {
|
||||
);
|
||||
return schema;
|
||||
}
|
||||
|
||||
/** Returns a compact display string of active traits (e.g. "AP 2, Auto 3, Blast 5, Bulky") */
|
||||
static getTraitsSummary(traits) {
|
||||
if (!traits) return "";
|
||||
const parts = [];
|
||||
if (traits.ap > 0) parts.push(`AP ${traits.ap}`);
|
||||
if (traits.auto > 0) parts.push(`Auto ${traits.auto}`);
|
||||
if (traits.blast > 0) parts.push(`Blast ${traits.blast}`);
|
||||
if (traits.bulky) parts.push(game.i18n.localize("MGT2.WeaponTraits.Bulky"));
|
||||
if (traits.veryBulky) parts.push(game.i18n.localize("MGT2.WeaponTraits.VeryBulky"));
|
||||
if (traits.stun) parts.push(game.i18n.localize("MGT2.WeaponTraits.Stun"));
|
||||
if (traits.smart) parts.push(game.i18n.localize("MGT2.WeaponTraits.Smart"));
|
||||
if (traits.radiation) parts.push(game.i18n.localize("MGT2.WeaponTraits.Radiation"));
|
||||
if (traits.scope) parts.push(game.i18n.localize("MGT2.WeaponTraits.Scope"));
|
||||
if (traits.zeroG) parts.push(game.i18n.localize("MGT2.WeaponTraits.ZeroG"));
|
||||
return parts.join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
const fields$3 = foundry.data.fields;
|
||||
@@ -943,7 +966,10 @@ class ActorCharacter {
|
||||
}
|
||||
|
||||
updateData["system.inventory.weight"] = onHandWeight;
|
||||
updateData["system.states.encumbrance"] = onHandWeight > $this.system.inventory.encumbrance.normal;
|
||||
// Use the threshold from updateData if it was just recalculated (e.g. STR/END talent change),
|
||||
// otherwise fall back to the persisted value.
|
||||
const encumbranceThreshold = updateData["system.inventory.encumbrance.normal"] ?? $this.system.inventory.encumbrance.normal;
|
||||
updateData["system.states.encumbrance"] = onHandWeight > encumbranceThreshold;
|
||||
|
||||
await $this.update(updateData);
|
||||
|
||||
@@ -964,6 +990,8 @@ class ActorCharacter {
|
||||
let heavy = normal * 2;
|
||||
foundry.utils.setProperty(changed, "system.inventory.encumbrance.normal", normal);
|
||||
foundry.utils.setProperty(changed, "system.inventory.encumbrance.heavy", heavy);
|
||||
// Also update the encumbrance state flag against the new threshold
|
||||
foundry.utils.setProperty(changed, "system.states.encumbrance", $this.system.inventory.weight > normal);
|
||||
}
|
||||
|
||||
//console.log(foundry.utils.getProperty(changed, "system.characteristics.strength.value"));
|
||||
@@ -1017,25 +1045,39 @@ class ActorCharacter {
|
||||
// $this.update({ system: { characteristics: data } });
|
||||
// }
|
||||
|
||||
static applyDamage($this, amount, { ignoreArmor = false } = {}) {
|
||||
if (isNaN(amount) || amount === 0) return;
|
||||
static async applyDamage($this, amount, { ignoreArmor = false, ap = 0, stun = false } = {}) {
|
||||
if (isNaN(amount) || amount === 0) return { incapRounds: 0 };
|
||||
const rank1 = $this.system.config.damages.rank1;
|
||||
const rank2 = $this.system.config.damages.rank2;
|
||||
const rank3 = $this.system.config.damages.rank3;
|
||||
|
||||
if (amount < 0) amount = Math.abs(amount);
|
||||
|
||||
if (!ignoreArmor) {
|
||||
const rawArmor = $this.system.inventory?.armor ?? 0;
|
||||
const armorValue = Math.max(0, rawArmor - ap);
|
||||
amount = Math.max(0, amount - armorValue);
|
||||
if (amount === 0) return { incapRounds: 0 };
|
||||
}
|
||||
|
||||
// ── Stun / Incapacitating: only deduct from endurance (rank3) ─────────
|
||||
if (stun) {
|
||||
const endKey = rank3; // "endurance" by default
|
||||
const prevEnd = $this.system.characteristics[endKey].value;
|
||||
const newEnd = Math.max(0, prevEnd - amount);
|
||||
const incapRounds = newEnd === 0 ? Math.max(0, amount - prevEnd) : 0;
|
||||
await $this.update({
|
||||
system: { characteristics: { [endKey]: { value: newEnd, dm: this.getModifier(newEnd) } } }
|
||||
});
|
||||
return { incapRounds };
|
||||
}
|
||||
|
||||
// ── Normal damage cascade: rank1 → rank2 → rank3 ────────────────────
|
||||
const data = {};
|
||||
data[rank1] = { value: $this.system.characteristics[rank1].value };
|
||||
data[rank2] = { value: $this.system.characteristics[rank2].value };
|
||||
data[rank3] = { value: $this.system.characteristics[rank3].value };
|
||||
|
||||
if (amount < 0) amount = Math.abs(amount);
|
||||
|
||||
if (!ignoreArmor) {
|
||||
const armorValue = $this.system.inventory?.armor ?? 0;
|
||||
amount = Math.max(0, amount - armorValue);
|
||||
if (amount === 0) return;
|
||||
}
|
||||
|
||||
for (const [key, rank] of Object.entries(data)) {
|
||||
if (rank.value > 0) {
|
||||
if (rank.value >= amount) {
|
||||
@@ -1050,7 +1092,8 @@ class ActorCharacter {
|
||||
}
|
||||
}
|
||||
|
||||
$this.update({ system: { characteristics: data } });
|
||||
await $this.update({ system: { characteristics: data } });
|
||||
return { incapRounds: 0 };
|
||||
}
|
||||
|
||||
static applyHealing($this, amount, type) {
|
||||
@@ -1237,17 +1280,18 @@ class TravellerActor extends Actor {
|
||||
}
|
||||
}
|
||||
|
||||
applyDamage(amount, { ignoreArmor = false } = {}) {
|
||||
applyDamage(amount, { ignoreArmor = false, ap = 0, stun = false } = {}) {
|
||||
if (this.type === "character") {
|
||||
return ActorCharacter.applyDamage(this, amount, { ignoreArmor });
|
||||
return ActorCharacter.applyDamage(this, amount, { ignoreArmor, ap, stun });
|
||||
} else if (this.type === "creature") {
|
||||
if (isNaN(amount) || amount === 0) return;
|
||||
if (isNaN(amount) || amount === 0) return Promise.resolve({ incapRounds: 0 });
|
||||
if (amount < 0) amount = Math.abs(amount);
|
||||
const armorValue = ignoreArmor ? 0 : (this.system.armor ?? 0);
|
||||
const rawArmor = ignoreArmor ? 0 : (this.system.armor ?? 0);
|
||||
const armorValue = Math.max(0, rawArmor - ap);
|
||||
const effective = Math.max(0, amount - armorValue);
|
||||
if (effective === 0) return;
|
||||
if (effective === 0) return Promise.resolve({ incapRounds: 0 });
|
||||
const newValue = Math.max(0, (this.system.life.value ?? 0) - effective);
|
||||
return this.update({ "system.life.value": newValue });
|
||||
return this.update({ "system.life.value": newValue }).then(() => ({ incapRounds: 0 }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1722,7 +1766,14 @@ class RollPromptHelper {
|
||||
skillLevel: options.skillLevel ?? 0,
|
||||
// Healing fields
|
||||
showHeal: options.showHeal ?? false,
|
||||
healType: options.healType ?? null
|
||||
healType: options.healType ?? null,
|
||||
// Ranged/Melee weapon flags
|
||||
isRanged: options.isRanged ?? false,
|
||||
isMelee: options.isMelee ?? false,
|
||||
isAttack: (options.isRanged ?? false) || (options.isMelee ?? false),
|
||||
autoLevel: options.autoLevel ?? 0,
|
||||
hasScope: options.scope ?? false,
|
||||
hasZeroG: options.zeroG ?? false,
|
||||
});
|
||||
|
||||
return await DialogV2$1.wait({
|
||||
@@ -1816,13 +1867,14 @@ class CharacterPrompts {
|
||||
}
|
||||
|
||||
static async openEditorFullView(title, html) {
|
||||
const safeTitle = title || game.i18n.localize("MGT2.Actor.Species") || "Species";
|
||||
const htmlContent = await renderTemplate$2("systems/mgt2/templates/editor-fullview.html", {
|
||||
config: CONFIG.MGT2,
|
||||
html
|
||||
html: html ?? ""
|
||||
});
|
||||
game.settings.get("mgt2", "theme");
|
||||
await DialogV2.wait({
|
||||
window: { title },
|
||||
window: { title: safeTitle },
|
||||
content: htmlContent,
|
||||
rejectClose: false,
|
||||
buttons: [
|
||||
@@ -2064,8 +2116,7 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
|
||||
i._range = i.system.range.isMelee
|
||||
? game.i18n.localize("MGT2.Melee")
|
||||
: MGT2Helper.getRangeDisplay(i.system.range);
|
||||
if (i.system.traits?.length > 0)
|
||||
i._subInfo = i.system.traits.map(x => x.name).join(", ");
|
||||
i._subInfo = WeaponData.getTraitsSummary(i.system.traits);
|
||||
weapons.push(i);
|
||||
break;
|
||||
|
||||
@@ -2453,7 +2504,14 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
|
||||
encumbrance: this.actor.system.states.encumbrance,
|
||||
difficulty: null,
|
||||
damageFormula: null,
|
||||
damageAP: 0,
|
||||
blastRadius: 0,
|
||||
stun: false,
|
||||
radiation: false,
|
||||
isMelee: false,
|
||||
isRanged: false,
|
||||
bulky: false,
|
||||
veryBulky: false,
|
||||
};
|
||||
|
||||
const cardButtons = [];
|
||||
@@ -2523,6 +2581,18 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
|
||||
rollOptions.damageFormula = itemObj.system.damage;
|
||||
if (itemObj.type === "weapon") {
|
||||
rollOptions.isMelee = itemObj.system.range?.isMelee === true;
|
||||
rollOptions.isRanged = itemObj.type === "weapon" && !itemObj.system.range?.isMelee;
|
||||
rollOptions.damageAP = itemObj.system.traits?.ap ?? 0;
|
||||
rollOptions.blastRadius = itemObj.system.traits?.blast ?? 0;
|
||||
rollOptions.stun = itemObj.system.traits?.stun === true;
|
||||
rollOptions.radiation = itemObj.system.traits?.radiation === true;
|
||||
rollOptions.scope = itemObj.system.traits?.scope === true;
|
||||
rollOptions.zeroG = itemObj.system.traits?.zeroG === true;
|
||||
rollOptions.autoLevel = itemObj.system.traits?.auto ?? 0;
|
||||
rollOptions.itemId = itemObj._id;
|
||||
rollOptions.magazine = itemObj.system.magazine ?? -1; // -1 = not tracked
|
||||
rollOptions.bulky = itemObj.system.traits?.bulky === true;
|
||||
rollOptions.veryBulky = itemObj.system.traits?.veryBulky === true;
|
||||
}
|
||||
if (itemObj.type === "disease") {
|
||||
if (itemObj.system.subType === "disease")
|
||||
@@ -2544,6 +2614,10 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
|
||||
const rollModifiers = [];
|
||||
const rollFormulaParts = [];
|
||||
|
||||
// Auto trait — fire mode
|
||||
const autoLevel = rollOptions.autoLevel ?? 0;
|
||||
const autoMode = autoLevel > 0 ? (userRollData.autoMode ?? "single") : "single";
|
||||
|
||||
if (userRollData.diceModifier) {
|
||||
rollFormulaParts.push("3d6", userRollData.diceModifier);
|
||||
} else {
|
||||
@@ -2594,14 +2668,127 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
|
||||
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.CustomDM") + " " + (customDMVal > 0 ? `+${customDMVal}` : `${customDMVal}`));
|
||||
}
|
||||
|
||||
if (rollOptions.isRanged) {
|
||||
const rangedRange = parseInt(userRollData.rangedRange ?? "0", 10);
|
||||
if (rangedRange === 1) {
|
||||
rollFormulaParts.push("+1");
|
||||
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.RangeShort") + " +1");
|
||||
} else if (rangedRange === -2) {
|
||||
rollFormulaParts.push("-2");
|
||||
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.RangeLong") + " −2");
|
||||
} else if (rangedRange === -4) {
|
||||
rollFormulaParts.push("-4");
|
||||
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.RangeExtreme") + " −4");
|
||||
}
|
||||
|
||||
const rangedAim = parseInt(userRollData.rangedAim ?? "0", 10);
|
||||
// Auto: burst/full-auto cancels all aiming advantages (rules p.75)
|
||||
if (rangedAim > 0 && autoMode === "single") {
|
||||
rollFormulaParts.push(`+${rangedAim}`);
|
||||
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.Aim") + ` +${rangedAim}`);
|
||||
if (userRollData.rangedLaserSight === true || userRollData.rangedLaserSight === "true") {
|
||||
rollFormulaParts.push("+1");
|
||||
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.LaserSight") + " +1");
|
||||
}
|
||||
}
|
||||
|
||||
const rangedFastTarget = parseInt(userRollData.rangedFastTarget ?? "0", 10);
|
||||
if (rangedFastTarget < 0) {
|
||||
rollFormulaParts.push(`${rangedFastTarget}`);
|
||||
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.FastTarget") + ` ${rangedFastTarget}`);
|
||||
}
|
||||
|
||||
if (userRollData.rangedCover === true || userRollData.rangedCover === "true") {
|
||||
rollFormulaParts.push("-2");
|
||||
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.Cover") + " −2");
|
||||
}
|
||||
|
||||
if (userRollData.rangedProne === true || userRollData.rangedProne === "true") {
|
||||
rollFormulaParts.push("-1");
|
||||
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.Prone") + " −1");
|
||||
}
|
||||
|
||||
if (userRollData.rangedDodge === true || userRollData.rangedDodge === "true") {
|
||||
const dodgeDM = parseInt(userRollData.rangedDodgeDM ?? "0", 10);
|
||||
if (dodgeDM < 0) {
|
||||
rollFormulaParts.push(`${dodgeDM}`);
|
||||
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.Dodge") + ` ${dodgeDM}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rollOptions.isMelee) {
|
||||
if (userRollData.meleeDodge === true || userRollData.meleeDodge === "true") {
|
||||
const dodgeDM = parseInt(userRollData.meleeDodgeDM ?? "0", 10);
|
||||
if (dodgeDM < 0) {
|
||||
rollFormulaParts.push(`${dodgeDM}`);
|
||||
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.Dodge") + ` ${dodgeDM}`);
|
||||
}
|
||||
}
|
||||
if (userRollData.meleeParry === true || userRollData.meleeParry === "true") {
|
||||
const parryDM = parseInt(userRollData.meleeParryDM ?? "0", 10);
|
||||
if (parryDM < 0) {
|
||||
rollFormulaParts.push(`${parryDM}`);
|
||||
rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.Parry") + ` ${parryDM}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (MGT2Helper.hasValue(userRollData, "difficulty") && userRollData.difficulty !== "") rollOptions.difficulty = userRollData.difficulty;
|
||||
|
||||
// ── Bulky / Very Bulky trait: STR penalty ────────────────────────────
|
||||
const strDm = this.actor.system.characteristics.strength?.dm ?? 0;
|
||||
if (rollOptions.veryBulky) {
|
||||
// Very Bulky: requires STR DM ≥ +2
|
||||
if (strDm < 2) {
|
||||
const penalty = strDm - 2;
|
||||
rollFormulaParts.push(`${penalty}`);
|
||||
rollModifiers.push(game.i18n.localize("MGT2.WeaponTraits.VeryBulky") + ` ${penalty}`);
|
||||
ui.notifications.warn(game.i18n.format("MGT2.Notifications.VeryBulkyPenalty", { penalty }));
|
||||
}
|
||||
} else if (rollOptions.bulky) {
|
||||
// Bulky: requires STR DM ≥ +1
|
||||
if (strDm < 1) {
|
||||
const penalty = strDm - 1;
|
||||
rollFormulaParts.push(`${penalty}`);
|
||||
rollModifiers.push(game.i18n.localize("MGT2.WeaponTraits.Bulky") + ` ${penalty}`);
|
||||
ui.notifications.warn(game.i18n.format("MGT2.Notifications.BulkyPenalty", { penalty }));
|
||||
}
|
||||
}
|
||||
|
||||
const rollFormula = rollFormulaParts.join("");
|
||||
if (!Roll.validate(rollFormula)) {
|
||||
ui.notifications.error(game.i18n.localize("MGT2.Errors.InvalidRollFormula"));
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Ammo decrement (ranged weapons with magazine tracking) ───────────
|
||||
if (rollOptions.isRanged && rollOptions.itemId && rollOptions.magazine >= 0) {
|
||||
let ammoUsed = 1;
|
||||
if (autoMode === "burst" && autoLevel > 0) ammoUsed = autoLevel;
|
||||
else if (autoMode === "fullAuto" && autoLevel > 0) ammoUsed = autoLevel * 3;
|
||||
|
||||
const currentMag = rollOptions.magazine;
|
||||
if (currentMag <= 0) {
|
||||
ui.notifications.warn(
|
||||
game.i18n.format("MGT2.Notifications.NoAmmo", { weapon: rollOptions.rollObjectName })
|
||||
);
|
||||
} else {
|
||||
const newMag = Math.max(0, currentMag - ammoUsed);
|
||||
const weaponItem = this.actor.getEmbeddedDocument("Item", rollOptions.itemId);
|
||||
if (weaponItem) await weaponItem.update({ "system.magazine": newMag });
|
||||
if (newMag === 0) {
|
||||
ui.notifications.warn(
|
||||
game.i18n.format("MGT2.Notifications.AmmoEmpty", { weapon: rollOptions.rollObjectName })
|
||||
);
|
||||
} else {
|
||||
ui.notifications.info(
|
||||
game.i18n.format("MGT2.Notifications.AmmoUsed", { used: ammoUsed, remaining: newMag, weapon: rollOptions.rollObjectName })
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let roll = await new Roll(rollFormula, this.actor.getRollData()).roll({ rollMode: userRollData.rollMode });
|
||||
|
||||
if (isInitiative && this.token?.combatant) {
|
||||
@@ -2623,7 +2810,7 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
|
||||
rollFailure = !rollSuccess;
|
||||
}
|
||||
|
||||
// Build effective damage formula: base + effect + STR DM (melee)
|
||||
// Build effective damage formula: base + effect + STR DM (melee) + Auto burst
|
||||
let effectiveDamageFormula = rollOptions.damageFormula || null;
|
||||
if (effectiveDamageFormula) {
|
||||
if (rollEffect !== undefined && rollEffect !== 0) {
|
||||
@@ -2633,6 +2820,18 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
|
||||
const strDm = this.actor.system.characteristics.strength?.dm ?? 0;
|
||||
if (strDm !== 0) effectiveDamageFormula += (strDm >= 0 ? "+" : "") + strDm;
|
||||
}
|
||||
// Burst: add Auto level to damage
|
||||
if (autoMode === "burst" && autoLevel > 0) {
|
||||
effectiveDamageFormula += `+${autoLevel}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto fire mode chat info
|
||||
let autoInfo = null;
|
||||
if (autoMode === "burst" && autoLevel > 0) {
|
||||
autoInfo = game.i18n.format("MGT2.RollPrompt.AutoBurstInfo", { level: autoLevel, ammo: autoLevel });
|
||||
} else if (autoMode === "fullAuto" && autoLevel > 0) {
|
||||
autoInfo = game.i18n.format("MGT2.RollPrompt.AutoFullInfo", { level: autoLevel, ammo: autoLevel * 3 });
|
||||
}
|
||||
|
||||
// ── Build roll breakdown tooltip ─────────────────────────────────────
|
||||
@@ -2659,6 +2858,7 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
|
||||
// Show damage button only if there's a formula AND (no difficulty check OR roll succeeded)
|
||||
showRollDamage: !!effectiveDamageFormula && (!difficultyValue || rollSuccess),
|
||||
cardButtons: cardButtons,
|
||||
autoInfo,
|
||||
};
|
||||
|
||||
if (MGT2Helper.hasValue(rollOptions, "difficulty")) {
|
||||
@@ -2675,7 +2875,7 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
|
||||
|
||||
let flags = null;
|
||||
if (effectiveDamageFormula) {
|
||||
flags = { mgt2: { damage: { formula: effectiveDamageFormula, rollObjectName: rollOptions.rollObjectName, rollTypeName: rollOptions.rollTypeName } } };
|
||||
flags = { mgt2: { damage: { formula: effectiveDamageFormula, ap: rollOptions.damageAP ?? 0, blast: rollOptions.blastRadius ?? 0, stun: rollOptions.stun ?? false, radiation: rollOptions.radiation ?? false, rollObjectName: rollOptions.rollObjectName, rollTypeName: rollOptions.rollTypeName } } };
|
||||
}
|
||||
if (cardButtons.length > 0) {
|
||||
if (!flags) flags = { mgt2: {} };
|
||||
@@ -2759,17 +2959,18 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
|
||||
|
||||
static async #onOpenEditor(event) {
|
||||
event.preventDefault();
|
||||
await CharacterPrompts.openEditorFullView(
|
||||
this.actor.system.personal.species,
|
||||
this.actor.system.personal.speciesText.descriptionLong
|
||||
);
|
||||
const title = this.actor.system.personal.species
|
||||
|| game.i18n.localize("MGT2.Actor.Species")
|
||||
|| "Species";
|
||||
const html = this.actor.system.personal.speciesText?.descriptionLong ?? "";
|
||||
await CharacterPrompts.openEditorFullView(title, html);
|
||||
}
|
||||
|
||||
static async #onHeal(event, target) {
|
||||
event.preventDefault();
|
||||
const healType = target.dataset.healType;
|
||||
|
||||
if (canvas.tokens.controlled.length === 0) {
|
||||
if (game.user.targets.size === 0) {
|
||||
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
|
||||
return;
|
||||
}
|
||||
@@ -3214,7 +3415,7 @@ class TravellerCreatureSheet extends MGT2ActorSheet {
|
||||
|
||||
// ───────────────────────────────────────────────────────── Roll Helpers
|
||||
|
||||
static async #postCreatureRoll({ actor, roll, rollLabel, dm, difficulty, difficultyLabel, rollMode, extraTooltip }) {
|
||||
static async #postCreatureRoll({ actor, roll, rollLabel, dm, difficulty, difficultyLabel, rollMode, extraTooltip, damageFormula }) {
|
||||
const diffTarget = MGT2Helper.getDifficultyValue(difficulty ?? "Average");
|
||||
const hasDifficulty = !!difficulty;
|
||||
const success = hasDifficulty ? roll.total >= diffTarget : true;
|
||||
@@ -3228,6 +3429,8 @@ class TravellerCreatureSheet extends MGT2ActorSheet {
|
||||
if (extraTooltip) breakdownParts.push(extraTooltip);
|
||||
const rollBreakdown = breakdownParts.join(" | ");
|
||||
|
||||
const showRollDamage = success && !!damageFormula;
|
||||
|
||||
const chatData = {
|
||||
creatureName: actor.name,
|
||||
creatureImg: actor.img,
|
||||
@@ -3243,6 +3446,7 @@ class TravellerCreatureSheet extends MGT2ActorSheet {
|
||||
effect: hasDifficulty ? effect : null,
|
||||
effectStr: hasDifficulty ? effectStr : null,
|
||||
modifiers: dm !== 0 ? [`DM ${dm >= 0 ? "+" : ""}${dm}`] : [],
|
||||
showRollDamage,
|
||||
};
|
||||
|
||||
const chatContent = await renderTemplate$1(
|
||||
@@ -3250,11 +3454,23 @@ class TravellerCreatureSheet extends MGT2ActorSheet {
|
||||
chatData
|
||||
);
|
||||
|
||||
const flags = showRollDamage ? {
|
||||
mgt2: {
|
||||
damage: {
|
||||
formula: normalizeDice(damageFormula),
|
||||
effect,
|
||||
rollObjectName: actor.name,
|
||||
rollTypeName: rollLabel,
|
||||
}
|
||||
}
|
||||
} : {};
|
||||
|
||||
await ChatMessage.create({
|
||||
content: chatContent,
|
||||
speaker: ChatMessage.getSpeaker({ actor }),
|
||||
rolls: [roll],
|
||||
rollMode: rollMode ?? game.settings.get("core", "rollMode"),
|
||||
flags,
|
||||
});
|
||||
|
||||
return { success, effect, total: roll.total };
|
||||
@@ -3355,24 +3571,14 @@ class TravellerCreatureSheet extends MGT2ActorSheet {
|
||||
if (chosenSkill) tooltipParts.push(`${chosenSkill.name} ${skillLevel >= 0 ? "+" : ""}${skillLevel}`);
|
||||
if (customDM !== 0) tooltipParts.push(`MD perso ${customDM >= 0 ? "+" : ""}${customDM}`);
|
||||
|
||||
const { success } = await TravellerCreatureSheet.#postCreatureRoll({
|
||||
await TravellerCreatureSheet.#postCreatureRoll({
|
||||
actor, roll, rollLabel,
|
||||
dm,
|
||||
difficulty: result.difficulty,
|
||||
rollMode: result.rollMode,
|
||||
extraTooltip: tooltipParts.join(" | "),
|
||||
damageFormula: attack.damage || null,
|
||||
});
|
||||
|
||||
// Roll damage only on success
|
||||
if (success && attack.damage) {
|
||||
const dmgFormula = normalizeDice(attack.damage);
|
||||
const dmgRoll = await new Roll(dmgFormula).evaluate();
|
||||
await dmgRoll.toMessage({
|
||||
speaker: ChatMessage.getSpeaker({ actor }),
|
||||
flavor: `<strong>${actor.name}</strong> — ${game.i18n.localize("MGT2.Chat.Weapon.Damage")}: ${attack.name} (${attack.damage})`,
|
||||
rollMode: result.rollMode ?? game.settings.get("core", "rollMode"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────── CRUD Handlers
|
||||
@@ -3716,6 +3922,7 @@ const preloadHandlebarsTemplates = async function() {
|
||||
"systems/mgt2/templates/roll-prompt.html",
|
||||
"systems/mgt2/templates/chat/roll.html",
|
||||
//"systems/mgt2/templates/chat/roll-characteristic.html",
|
||||
"systems/mgt2/templates/chat/radiation.html",
|
||||
"systems/mgt2/templates/actors/actor-config-sheet.html",
|
||||
"systems/mgt2/templates/actors/actor-config-characteristic-sheet.html",
|
||||
"systems/mgt2/templates/actors/trait-sheet.html",
|
||||
@@ -3734,12 +3941,45 @@ class ChatHelper {
|
||||
if (!message || !element) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore disabled state for already-applied buttons
|
||||
const appliedActions = message.flags?.mgt2?.appliedActions ?? [];
|
||||
if (appliedActions.length > 0) {
|
||||
appliedActions.forEach(action => {
|
||||
element.querySelectorAll(`button[data-action="${action}"]`).forEach(btn => {
|
||||
btn.disabled = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Apply buttons are GM-only: hide them for players
|
||||
const GM_ACTIONS = ["damage", "healing", "surgeryDamage"];
|
||||
if (!game.user.isGM) {
|
||||
GM_ACTIONS.forEach(action => {
|
||||
element.querySelectorAll(`button[data-action="${action}"]`).forEach(btn => {
|
||||
btn.style.display = "none";
|
||||
});
|
||||
});
|
||||
// Hide the buttons container if no visible buttons remain
|
||||
element.querySelectorAll(".mgt2-buttons").forEach(container => {
|
||||
const hasVisible = [...container.querySelectorAll("button")]
|
||||
.some(b => b.style.display !== "none");
|
||||
if (!hasVisible) container.style.display = "none";
|
||||
});
|
||||
}
|
||||
|
||||
element.querySelectorAll('button[data-action="rollDamage"]').forEach(el => {
|
||||
el.addEventListener('click', async event => {
|
||||
await this._processRollDamageButtonEvent(message, event);
|
||||
});
|
||||
});
|
||||
|
||||
element.querySelectorAll('button[data-action="rollRadiation"]').forEach(el => {
|
||||
el.addEventListener('click', async event => {
|
||||
await this._rollRadiationDamage(message, event);
|
||||
});
|
||||
});
|
||||
|
||||
element.querySelectorAll('button[data-action="damage"]').forEach(el => {
|
||||
el.addEventListener('click', async event => {
|
||||
await this._applyChatCardDamage(message, event);
|
||||
@@ -3791,75 +4031,144 @@ class ChatHelper {
|
||||
static async _processRollDamageButtonEvent(message, event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let rollFormula = message.flags.mgt2.damage.formula;
|
||||
|
||||
let roll = await new Roll(rollFormula, {}).roll();
|
||||
|
||||
let speaker;
|
||||
let selectTokens = canvas.tokens.controlled;
|
||||
if (selectTokens.length > 0) {
|
||||
speaker = selectTokens[0].actor;
|
||||
} else {
|
||||
speaker = game.user.character;
|
||||
const damageFlags = message.flags?.mgt2?.damage;
|
||||
if (!damageFlags?.formula) {
|
||||
ui.notifications.warn(game.i18n.localize("MGT2.Errors.InvalidRollFormula"));
|
||||
return;
|
||||
}
|
||||
|
||||
let rollTypeName = message.flags.mgt2.damage.rollTypeName ? message.flags.mgt2.damage.rollTypeName + " " + game.i18n.localize("MGT2.Actor.Damage") : null;
|
||||
const effect = damageFlags.effect ?? 0;
|
||||
const effectPart = effect > 0 ? `+${effect}` : effect < 0 ? `${effect}` : "";
|
||||
const rollFormula = damageFlags.formula + effectPart;
|
||||
|
||||
let roll;
|
||||
try {
|
||||
roll = await new Roll(rollFormula, {}).roll();
|
||||
} catch (e) {
|
||||
ui.notifications.warn(game.i18n.localize("MGT2.Errors.InvalidRollFormula"));
|
||||
return;
|
||||
}
|
||||
|
||||
const rollTypeName = damageFlags.rollTypeName
|
||||
? damageFlags.rollTypeName + " " + game.i18n.localize("MGT2.Actor.Damage")
|
||||
: null;
|
||||
|
||||
const ap = damageFlags.ap ?? 0;
|
||||
const blast = damageFlags.blast ?? 0;
|
||||
const stun = damageFlags.stun ?? false;
|
||||
const radiation = damageFlags.radiation ?? false;
|
||||
|
||||
const chatData = {
|
||||
user: game.user.id,
|
||||
speaker: ChatMessage.getSpeaker({ actor: speaker }),
|
||||
speaker: message.speaker,
|
||||
formula: roll._formula,
|
||||
tooltip: await roll.getTooltip(),
|
||||
total: Math.round(roll.total * 100) / 100,
|
||||
showButtons: true,
|
||||
hasDamage: true,
|
||||
rollTypeName: rollTypeName,
|
||||
rollObjectName: message.flags.mgt2.damage.rollObjectName
|
||||
rollTypeName,
|
||||
rollObjectName: damageFlags.rollObjectName,
|
||||
apValue: ap > 0 ? ap : null,
|
||||
blastRadius: blast > 0 ? blast : null,
|
||||
stunWeapon: stun || null,
|
||||
radiationWeapon: radiation || null,
|
||||
};
|
||||
|
||||
const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/roll.html", chatData);
|
||||
chatData.content = html;
|
||||
|
||||
// Persist ap, blast, stun, radiation in damage message flags so handlers can read them
|
||||
if (ap > 0 || blast > 0 || stun || radiation) chatData.flags = { mgt2: { damage: { ap, blast, stun, radiation, rollObjectName: damageFlags.rollObjectName } } };
|
||||
|
||||
return roll.toMessage(chatData);
|
||||
}
|
||||
|
||||
static _applyChatCardDamage(message, event) {
|
||||
if (canvas.tokens.controlled.length === 0) {
|
||||
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
|
||||
return;
|
||||
static async #markButtonApplied(message, btn, action) {
|
||||
const existing = message.flags?.mgt2?.appliedActions ?? [];
|
||||
if (!existing.includes(action)) {
|
||||
await message.setFlag("mgt2", "appliedActions", [...existing, action]);
|
||||
}
|
||||
const roll = message.rolls[0];
|
||||
return Promise.all(canvas.tokens.controlled.map(t => {
|
||||
const a = t.actor;
|
||||
return a.applyDamage(roll.total);
|
||||
}));
|
||||
if (btn) btn.disabled = true;
|
||||
}
|
||||
|
||||
static _applyChatCardHealing(message, event) {
|
||||
if (canvas.tokens.controlled.length === 0) {
|
||||
static async _applyChatCardDamage(message, event) {
|
||||
if (game.user.targets.size === 0) {
|
||||
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
|
||||
return;
|
||||
}
|
||||
// For First Aid/Surgery healing, use amount from flags; otherwise use roll total
|
||||
const btn = event.currentTarget;
|
||||
const roll = message.rolls[0];
|
||||
const amount = Math.round(roll.total * 100) / 100;
|
||||
const ap = message.flags?.mgt2?.damage?.ap ?? 0;
|
||||
const stun = message.flags?.mgt2?.damage?.stun ?? false;
|
||||
await Promise.all([...game.user.targets].map(async t => {
|
||||
const result = await t.actor.applyDamage(amount, { ap, stun });
|
||||
if (stun) {
|
||||
const incapRounds = result?.incapRounds ?? 0;
|
||||
if (incapRounds > 0) {
|
||||
ui.notifications.warn(game.i18n.format("MGT2.Notifications.StunIncapacitated", { name: t.actor.name, rounds: incapRounds }));
|
||||
} else {
|
||||
ui.notifications.info(game.i18n.format("MGT2.Notifications.StunDamageApplied", { name: t.actor.name, amount }));
|
||||
}
|
||||
} else {
|
||||
const notifKey = ap > 0 ? "MGT2.Notifications.DamageAppliedAP" : "MGT2.Notifications.DamageApplied";
|
||||
ui.notifications.info(game.i18n.format(notifKey, { name: t.actor.name, amount, ap }));
|
||||
}
|
||||
}));
|
||||
await ChatHelper.#markButtonApplied(message, btn, "damage");
|
||||
}
|
||||
|
||||
static async _applyChatCardHealing(message, event) {
|
||||
if (game.user.targets.size === 0) {
|
||||
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
|
||||
return;
|
||||
}
|
||||
const btn = event.currentTarget;
|
||||
const amount = message.flags?.mgt2?.healing?.amount
|
||||
?? message.flags?.mgt2?.surgery?.healing
|
||||
?? Math.max(1, message.rolls[0].total);
|
||||
return Promise.all(canvas.tokens.controlled.map(t => {
|
||||
const a = t.actor;
|
||||
return a.applyHealing(amount);
|
||||
await Promise.all([...game.user.targets].map(async t => {
|
||||
await t.actor.applyHealing(amount);
|
||||
ui.notifications.info(game.i18n.format("MGT2.Notifications.HealingApplied", { name: t.actor.name, amount }));
|
||||
}));
|
||||
await ChatHelper.#markButtonApplied(message, btn, "healing");
|
||||
}
|
||||
|
||||
static _applyChatCardSurgeryDamage(message, event) {
|
||||
if (canvas.tokens.controlled.length === 0) {
|
||||
static async _applyChatCardSurgeryDamage(message, event) {
|
||||
if (game.user.targets.size === 0) {
|
||||
ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected"));
|
||||
return;
|
||||
}
|
||||
const btn = event.currentTarget;
|
||||
const amount = message.flags?.mgt2?.surgery?.surgeryDamage ?? 3;
|
||||
return Promise.all(canvas.tokens.controlled.map(t => {
|
||||
const a = t.actor;
|
||||
return a.applyDamage(amount, { ignoreArmor: true });
|
||||
await Promise.all([...game.user.targets].map(async t => {
|
||||
await t.actor.applyDamage(amount, { ignoreArmor: true });
|
||||
ui.notifications.info(game.i18n.format("MGT2.Notifications.DamageApplied", { name: t.actor.name, amount }));
|
||||
}));
|
||||
await ChatHelper.#markButtonApplied(message, btn, "surgeryDamage");
|
||||
}
|
||||
|
||||
static async _rollRadiationDamage(message, event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const damageFlags = message.flags?.mgt2?.damage;
|
||||
const rollObjectName = damageFlags?.rollObjectName ?? "";
|
||||
|
||||
// 2D × 20 rads (MGT2 rule: 2d6 × 20)
|
||||
const roll = await new Roll("2d6 * 20", {}).roll();
|
||||
|
||||
const chatData = {
|
||||
user: game.user.id,
|
||||
speaker: message.speaker,
|
||||
formula: roll._formula,
|
||||
tooltip: await roll.getTooltip(),
|
||||
total: Math.round(roll.total),
|
||||
rollObjectName,
|
||||
};
|
||||
|
||||
const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/radiation.html", chatData);
|
||||
chatData.content = html;
|
||||
return roll.toMessage(chatData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user