Gestion du dé d'usure des degats des armes, gestion des munitions, gestion de la limute dégats vs DV
Release Creation / build (release) Successful in 1m11s
Release Creation / build (release) Successful in 1m11s
This commit is contained in:
@@ -201,12 +201,17 @@ export class DonjonEtCieRollDialog {
|
||||
}
|
||||
|
||||
static async createDamage(actor, item) {
|
||||
const damageContext = DonjonEtCieUtility.getMartialDamageContext(actor, item);
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-donjon-et-cie/templates/dialogs/damage-roll.hbs",
|
||||
{
|
||||
actorName: actor?.name ?? item.actor?.name ?? "",
|
||||
item,
|
||||
actorBonus: actor?.system?.combat?.degatsBonus ?? 0
|
||||
actorBonus: actor?.system?.combat?.degatsBonus ?? 0,
|
||||
damageFormula: damageContext.effectiveFormula || item.system.degats,
|
||||
damageBase: damageContext.baseFormula || item.system.degats,
|
||||
damageCapped: damageContext.capped,
|
||||
martialDvLabel: damageContext.martialDvSides ? `d${damageContext.martialDvSides}` : damageContext.martialDvFormula
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -57,6 +57,9 @@ export default class DonjonEtCieItemSheet extends HandlebarsApplicationMixin(fou
|
||||
isCapacity: item.type === "capacite",
|
||||
isLanguage: item.type === "langue",
|
||||
isTrait: item.type === "trait",
|
||||
ammunitionUsageLabel: item.type === "arme" && Number(item.system.munitionsDelta ?? 0) > 0
|
||||
? DonjonEtCieUtility.formatUsageDie(item.system.munitionsDelta)
|
||||
: "—",
|
||||
canResetUsage: item.type === "entrainement" && Number(item.system.deltaMax ?? 0) > 0 && Number(item.system.delta ?? 0) !== Number(item.system.deltaMax ?? 0),
|
||||
armorProtectionDisplay: Number(item.system.resultatProtection ?? 0) > 0 ? item.system.resultatProtection : "—",
|
||||
weaponCharacteristicLabel: item.type === "arme" ? DonjonEtCieUtility.getWeaponCharacteristicLabel(item.system.categorie) : null,
|
||||
|
||||
@@ -31,6 +31,14 @@ export class DonjonEtCieItem extends Item {
|
||||
return Number(this.system.deltaMax ?? this.system.delta ?? 0);
|
||||
}
|
||||
|
||||
get ammunitionUsageDie() {
|
||||
return Number(this.system.munitionsDelta ?? 0);
|
||||
}
|
||||
|
||||
get damageUsageDie() {
|
||||
return Number(this.system.degatsDelta ?? 0);
|
||||
}
|
||||
|
||||
async roll() {
|
||||
if (this.type === "arme") return DonjonEtCieRollDialog.createWeapon(this.actor, this);
|
||||
if (this.type === "sortilege") return DonjonEtCieRollDialog.createSpell(this.actor, this);
|
||||
@@ -43,10 +51,27 @@ export class DonjonEtCieItem extends Item {
|
||||
}
|
||||
|
||||
async rollDamage() {
|
||||
if (!this.system.degats) return null;
|
||||
if (this.system.degatsEstUsageDe) {
|
||||
if (!Number(this.system.degatsDelta ?? 0)) {
|
||||
ui.notifications.warn(game.i18n.localize("DNC.Warn.DamageExhausted"));
|
||||
return null;
|
||||
}
|
||||
} else if (!this.system.degats) {
|
||||
return null;
|
||||
}
|
||||
return DonjonEtCieRollDialog.createDamage(this.actor, this);
|
||||
}
|
||||
|
||||
async rollAmmoUsage() {
|
||||
if (this.type !== "arme") return null;
|
||||
return game.system.donjonEtCie.rolls.rollWeaponAmmoUsage(this);
|
||||
}
|
||||
|
||||
async rollDamageUsage() {
|
||||
if (this.type !== "arme") return null;
|
||||
return game.system.donjonEtCie.rolls.rollWeaponDamageUsage(this);
|
||||
}
|
||||
|
||||
async postToChat() {
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-donjon-et-cie/templates/chat/item-card.hbs",
|
||||
|
||||
@@ -46,7 +46,7 @@ function injectActorDirectoryMissionPackButton(app, element) {
|
||||
}
|
||||
|
||||
function onChatActionClick(event) {
|
||||
const button = event.target.closest("[data-action='rollChatDamage'], [data-action='rollSpellChaos'], [data-action='applyDamage']");
|
||||
const button = event.target.closest("[data-action='rollChatDamage'], [data-action='rollSpellChaos'], [data-action='applyDamage'], [data-action='rollAmmoUsage'], [data-action='rollDamageUsage']");
|
||||
if (!(button instanceof HTMLElement)) return;
|
||||
|
||||
event.preventDefault();
|
||||
@@ -85,6 +85,12 @@ function onChatActionClick(event) {
|
||||
const itemUuid = button.dataset.itemUuid;
|
||||
if (!itemUuid) return;
|
||||
const item = await fromUuid(itemUuid);
|
||||
if (button.dataset.action === "rollAmmoUsage") {
|
||||
return item?.rollAmmoUsage?.();
|
||||
}
|
||||
if (button.dataset.action === "rollDamageUsage") {
|
||||
return item?.rollDamageUsage?.();
|
||||
}
|
||||
return item?.rollDamage?.();
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -308,12 +308,14 @@ export class DonjonEtCieRolls {
|
||||
favorLabel: favor?.label ?? null,
|
||||
favorNote: favor?.note ?? null,
|
||||
showDamageButton: result.success && Boolean(item.system.degats),
|
||||
showAmmoButton: Number(item.system.munitionsDelta ?? 0) > 0,
|
||||
itemUuid: item.uuid,
|
||||
details: [
|
||||
{ label: game.i18n.localize("DNC.UI.Weapon"), value: item.name },
|
||||
{ label: game.i18n.localize("DNC.UI.Characteristic"), value: characteristicLabel },
|
||||
{ label: `Valeur de ${characteristicLabel}`, value: result.target },
|
||||
{ label: game.i18n.localize("DNC.UI.Damage"), value: item.system.degats || "—" },
|
||||
{ label: game.i18n.localize("DNC.UI.Ammunition"), value: Number(item.system.munitionsDelta ?? 0) > 0 ? DonjonEtCieUtility.formatUsageDie(item.system.munitionsDelta) : "—" },
|
||||
{ label: game.i18n.localize("DNC.UI.Range"), value: item.system.portee || "—" },
|
||||
...(favor ? [
|
||||
{ label: game.i18n.localize("DNC.Chat.Favor"), value: favor.label },
|
||||
@@ -327,11 +329,54 @@ export class DonjonEtCieRolls {
|
||||
return { ...result, favor, mode: effectiveMode };
|
||||
}
|
||||
|
||||
static async rollWeaponAmmoUsage(item, { mode = "normal" } = {}) {
|
||||
const before = Number(item?.system?.munitionsDelta ?? 0);
|
||||
if (!before) {
|
||||
ui.notifications.warn(game.i18n.localize("DNC.Warn.NoAmmunitionAvailable"));
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolved = await this.#resolveFormulaRoll(`1d${before}`, {}, { mode, favorable: "high" });
|
||||
const result = resolved.kept;
|
||||
const degraded = result <= 3;
|
||||
const after = degraded ? DonjonEtCieUtility.degradeUsageDie(before) : before;
|
||||
|
||||
if (after !== before) {
|
||||
await item.update({ "system.munitionsDelta": after });
|
||||
}
|
||||
|
||||
await this.#createChatCard(item.actor, "systems/fvtt-donjon-et-cie/templates/chat/usage-card.hbs", {
|
||||
title: `${game.i18n.localize("DNC.Roll.Ammunition")} : ${item.name}`,
|
||||
value: result,
|
||||
values: resolved.values,
|
||||
mode: resolved.mode,
|
||||
modeLabel: this.#getModeLabel(resolved.mode),
|
||||
before: DonjonEtCieUtility.formatUsageDie(before),
|
||||
after: DonjonEtCieUtility.formatUsageDie(after),
|
||||
protectionStored: null,
|
||||
degraded,
|
||||
exhausted: after === 0,
|
||||
isAmmunition: true
|
||||
}, { rolls: resolved.rolls });
|
||||
|
||||
return { result, values: resolved.values, mode: resolved.mode, before, after, degraded };
|
||||
}
|
||||
|
||||
static async rollDamage(actor, item, { mode = "normal" } = {}) {
|
||||
if (!item.system.degats) return null;
|
||||
const isUsageDie = Boolean(item.system.degatsEstUsageDe);
|
||||
const degatsDelta = Number(item.system.degatsDelta ?? 0);
|
||||
|
||||
if (isUsageDie && !degatsDelta) {
|
||||
ui.notifications.warn(game.i18n.localize("DNC.Warn.DamageExhausted"));
|
||||
return null;
|
||||
}
|
||||
if (!isUsageDie && !item.system.degats) return null;
|
||||
|
||||
const damageContext = DonjonEtCieUtility.getMartialDamageContext(actor, item);
|
||||
const actorBonus = Number(actor?.system?.combat?.degatsBonus ?? 0);
|
||||
const totalBonus = actorBonus;
|
||||
const formula = totalBonus ? `${item.system.degats} + ${totalBonus}` : item.system.degats;
|
||||
const effectiveDamage = damageContext.effectiveFormula || (isUsageDie ? `1d${degatsDelta}` : item.system.degats);
|
||||
const formula = totalBonus ? `${effectiveDamage} + ${totalBonus}` : effectiveDamage;
|
||||
const result = await this.#resolveFormulaRoll(formula, {}, { mode, favorable: "high" });
|
||||
const targets = DonjonEtCieUtility.getSceneDamageTargets();
|
||||
const rollDieLabels = result.rolls.map((roll) => {
|
||||
@@ -339,6 +384,7 @@ export class DonjonEtCieRolls {
|
||||
return dieValues.length ? dieValues.join(" + ") : String(roll.total ?? "—");
|
||||
});
|
||||
const keptDieLabel = rollDieLabels[result.keptIndex] ?? rollDieLabels[0] ?? String(result.kept);
|
||||
const baseDamageDisplay = isUsageDie ? DonjonEtCieUtility.formatUsageDie(degatsDelta) : item.system.degats;
|
||||
|
||||
await this.#createChatCard(actor ?? item.actor, "systems/fvtt-donjon-et-cie/templates/chat/damage-card.hbs", {
|
||||
title: `${game.i18n.localize("DNC.Roll.Damage")} : ${item.name}`,
|
||||
@@ -351,13 +397,60 @@ export class DonjonEtCieRolls {
|
||||
values: result.values,
|
||||
total: result.kept,
|
||||
bonus: totalBonus,
|
||||
baseDamage: item.system.degats,
|
||||
baseDamage: baseDamageDisplay,
|
||||
effectiveDamage,
|
||||
damageCapped: damageContext.capped,
|
||||
martialDvLabel: damageContext.martialDvSides ? `d${damageContext.martialDvSides}` : damageContext.martialDvFormula,
|
||||
sourceLabel: item.name,
|
||||
targets,
|
||||
hasTargets: targets.length > 0
|
||||
hasTargets: targets.length > 0,
|
||||
showDamageUsageButton: isUsageDie && degatsDelta > 0,
|
||||
itemUuid: item.uuid
|
||||
}, { rolls: result.rolls });
|
||||
|
||||
return { total: result.kept, formula: result.formula, bonus: totalBonus, values: result.values, mode: result.mode };
|
||||
return {
|
||||
total: result.kept,
|
||||
formula: result.formula,
|
||||
baseDamage: baseDamageDisplay,
|
||||
effectiveDamage,
|
||||
damageCapped: damageContext.capped,
|
||||
bonus: totalBonus,
|
||||
values: result.values,
|
||||
mode: result.mode
|
||||
};
|
||||
}
|
||||
|
||||
static async rollWeaponDamageUsage(item, { mode = "normal" } = {}) {
|
||||
const before = Number(item?.system?.degatsDelta ?? 0);
|
||||
if (!before) {
|
||||
ui.notifications.warn(game.i18n.localize("DNC.Warn.DamageExhausted"));
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolved = await this.#resolveFormulaRoll(`1d${before}`, {}, { mode, favorable: "high" });
|
||||
const result = resolved.kept;
|
||||
const degraded = result <= 3;
|
||||
const after = degraded ? DonjonEtCieUtility.degradeUsageDie(before) : before;
|
||||
|
||||
if (after !== before) {
|
||||
await item.update({ "system.degatsDelta": after });
|
||||
}
|
||||
|
||||
await this.#createChatCard(item.actor, "systems/fvtt-donjon-et-cie/templates/chat/usage-card.hbs", {
|
||||
title: `${game.i18n.localize("DNC.Roll.DamageUsage")} : ${item.name}`,
|
||||
value: result,
|
||||
values: resolved.values,
|
||||
mode: resolved.mode,
|
||||
modeLabel: this.#getModeLabel(resolved.mode),
|
||||
before: DonjonEtCieUtility.formatUsageDie(before),
|
||||
after: DonjonEtCieUtility.formatUsageDie(after),
|
||||
protectionStored: null,
|
||||
degraded,
|
||||
exhausted: after === 0,
|
||||
isDamageUsage: true
|
||||
}, { rolls: resolved.rolls });
|
||||
|
||||
return { result, values: resolved.values, mode: resolved.mode, before, after, degraded };
|
||||
}
|
||||
|
||||
static async applyDamage(target, { damage = 0, useArmor = false, sourceLabel = "" } = {}) {
|
||||
|
||||
@@ -119,6 +119,87 @@ export class DonjonEtCieUtility {
|
||||
};
|
||||
}
|
||||
|
||||
static parseDieFormula(formula) {
|
||||
const normalized = String(formula ?? "").trim();
|
||||
if (!normalized) return null;
|
||||
|
||||
const dieMatch = normalized.match(/(?<count>\d*)d(?<sides>\d+)/i);
|
||||
if (dieMatch?.groups?.sides) {
|
||||
return {
|
||||
formula: normalized,
|
||||
count: Number(dieMatch.groups.count || 1),
|
||||
countRaw: dieMatch.groups.count ?? "",
|
||||
sides: Number(dieMatch.groups.sides),
|
||||
match: dieMatch[0],
|
||||
index: dieMatch.index ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
if (/^\d+$/.test(normalized)) {
|
||||
const sides = Number(normalized);
|
||||
if ([4, 6, 8, 10, 12, 20].includes(sides)) {
|
||||
return {
|
||||
formula: normalized,
|
||||
count: 1,
|
||||
countRaw: "",
|
||||
sides,
|
||||
match: normalized,
|
||||
index: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static getMartialDamageContext(actor, item) {
|
||||
const isUsageDie = Boolean(item?.system?.degatsEstUsageDe);
|
||||
const degatsDelta = Number(item?.system?.degatsDelta ?? 0);
|
||||
const baseFormula = isUsageDie
|
||||
? (degatsDelta > 0 ? `1d${degatsDelta}` : "")
|
||||
: String(item?.system?.degats ?? "").trim();
|
||||
|
||||
const baseContext = {
|
||||
baseFormula,
|
||||
effectiveFormula: baseFormula,
|
||||
capped: false,
|
||||
martialDvFormula: String(actor?.system?.sante?.dv ?? "").trim(),
|
||||
martialDvSides: 0,
|
||||
weaponSides: 0,
|
||||
isUsageDie
|
||||
};
|
||||
|
||||
if (actor?.type !== "employe" || item?.type !== "arme" || !baseFormula) {
|
||||
return baseContext;
|
||||
}
|
||||
|
||||
const damageDie = this.parseDieFormula(baseFormula);
|
||||
const martialDie = this.parseDieFormula(actor.system.sante?.dv);
|
||||
if (!damageDie || !martialDie?.sides) return baseContext;
|
||||
|
||||
const cappedSides = Math.min(damageDie.sides, martialDie.sides);
|
||||
if (cappedSides >= damageDie.sides) {
|
||||
return {
|
||||
...baseContext,
|
||||
martialDvSides: martialDie.sides,
|
||||
weaponSides: damageDie.sides
|
||||
};
|
||||
}
|
||||
|
||||
const replacement = `${damageDie.countRaw || ""}d${cappedSides}`;
|
||||
const effectiveFormula = damageDie.match === damageDie.formula
|
||||
? replacement
|
||||
: `${damageDie.formula.slice(0, damageDie.index)}${replacement}${damageDie.formula.slice(damageDie.index + damageDie.match.length)}`;
|
||||
|
||||
return {
|
||||
...baseContext,
|
||||
effectiveFormula,
|
||||
capped: true,
|
||||
martialDvSides: martialDie.sides,
|
||||
weaponSides: damageDie.sides
|
||||
};
|
||||
}
|
||||
|
||||
static getFavorLabel(key) {
|
||||
return DONJON_ET_CIE.favorDepartments[key] ?? key;
|
||||
}
|
||||
@@ -173,12 +254,20 @@ export class DonjonEtCieUtility {
|
||||
const system = item.system;
|
||||
const delta = Number(system.delta ?? 0);
|
||||
const deltaMax = Number(system.deltaMax ?? delta ?? 0);
|
||||
const ammunitionDelta = Number(system.munitionsDelta ?? 0);
|
||||
const isUsageDie = Boolean(system.degatsEstUsageDe);
|
||||
const degatsDelta = Number(system.degatsDelta ?? 0);
|
||||
const usageLabel = item.type === "entrainement" && deltaMax > 0
|
||||
? `${this.formatUsageDie(delta)} / ${this.formatUsageDie(deltaMax)}`
|
||||
: delta > 0
|
||||
? this.formatUsageDie(delta)
|
||||
: null;
|
||||
|
||||
const damageUsageLabel = isUsageDie
|
||||
? (degatsDelta > 0 ? this.formatUsageDie(degatsDelta) : game.i18n.localize("DNC.UI.DamageExhausted"))
|
||||
: null;
|
||||
const damageLabel = isUsageDie ? damageUsageLabel : (system.degats || null);
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
@@ -187,11 +276,15 @@ export class DonjonEtCieUtility {
|
||||
system,
|
||||
uuid: item.uuid,
|
||||
usageLabel,
|
||||
ammunitionUsageLabel: item.type === "arme" && ammunitionDelta > 0 ? this.formatUsageDie(ammunitionDelta) : null,
|
||||
protectionLabel: item.type === "armure" && Number(system.resultatProtection ?? 0) > 0 ? `Protection ${system.resultatProtection}` : null,
|
||||
weaponCharacteristicLabel: item.type === "arme" ? this.getWeaponCharacteristicLabel(system.categorie) : null,
|
||||
canRoll: ["arme", "sortilege"].includes(item.type),
|
||||
canUse: delta > 0,
|
||||
canRollDamage: Boolean(system.degats),
|
||||
hasTrackedAmmunition: item.type === "arme" && ammunitionDelta > 0,
|
||||
damageUsageLabel,
|
||||
damageLabel,
|
||||
canRollDamage: item.type === "arme" && (isUsageDie ? degatsDelta > 0 : Boolean(system.degats)),
|
||||
rollAction: item.type === "sortilege" ? "rollSpell" : "rollWeapon",
|
||||
damageAction: "rollDamage",
|
||||
isEquipped: Boolean(system.equipee),
|
||||
|
||||
@@ -20,6 +20,9 @@ export default class ArmeDataModel extends BaseItemDataModel {
|
||||
categorie: new fields.StringField({ initial: "melee" }),
|
||||
caracteristique: new fields.StringField({ initial: "force" }),
|
||||
degats: new fields.StringField({ initial: "1d6" }),
|
||||
degatsEstUsageDe: new fields.BooleanField({ initial: false }),
|
||||
degatsDelta: new fields.NumberField({ initial: 0, integer: true, min: 0 }),
|
||||
munitionsDelta: new fields.NumberField({ initial: 0, integer: true, min: 0 }),
|
||||
portee: new fields.StringField({ initial: "" }),
|
||||
mains: new fields.NumberField({ initial: 1, integer: true }),
|
||||
equipee: new fields.BooleanField({ initial: false })
|
||||
|
||||
Reference in New Issue
Block a user