5 Commits

Author SHA1 Message Date
uberwald 09f2349bab Fix PV boxes
Release Creation / build (release) Successful in 56s
2026-05-09 23:32:43 +02:00
uberwald d8eb23289e Fix PV boxes 2026-05-09 23:32:29 +02:00
uberwald 820a7d61cf Corrections sur focus
Release Creation / build (release) Successful in 1m4s
2026-05-07 00:14:20 +02:00
uberwald 755c349078 DsN OK, objets uniques, règles de campagne
Release Creation / build (release) Successful in 1m26s
2026-05-03 21:51:35 +02:00
uberwald 8fb27c2e82 UPdate sys helper
Release Creation / build (release) Successful in 2m29s
2026-05-01 09:33:41 +02:00
32 changed files with 1215 additions and 178 deletions
+11
View File
@@ -27,6 +27,17 @@ jobs:
manifest: https://www.uberwald.me/gitea/uberwald/fvtt-donjon-et-cie/releases/download/latest/system.json manifest: https://www.uberwald.me/gitea/uberwald/fvtt-donjon-et-cie/releases/download/latest/system.json
download: https://www.uberwald.me/gitea/${{ gitea.repository }}/releases/download/${{ github.event.release.tag_name }}/fvtt-donjon-et-cie.zip download: https://www.uberwald.me/gitea/${{ gitea.repository }}/releases/download/${{ github.event.release.tag_name }}/fvtt-donjon-et-cie.zip
- name: Setup Node
uses: https://github.com/actions/setup-node@v4
with:
node-version: 24
- name: Install dependencies
run: npm ci
- name: Build system and pack compendiums
run: npm run build
- run: | - run: |
apt update -y apt update -y
apt install -y zip apt install -y zip
+37
View File
@@ -103,6 +103,10 @@
"DNC.Warn.SpellInsufficientResources": "Le lanceur ne dispose pas d'assez de PV et de focus pour payer ce sort.", "DNC.Warn.SpellInsufficientResources": "Le lanceur ne dispose pas d'assez de PV et de focus pour payer ce sort.",
"DNC.Warn.ChaosUnavailable": "Le Chaos n'est pas disponible pour ce sort.", "DNC.Warn.ChaosUnavailable": "Le Chaos n'est pas disponible pour ce sort.",
"DNC.Warn.TrainingExhausted": "Cet entrainement est epuise pour aujourd'hui. Reinitialisez son delta quotidien pour le lendemain.", "DNC.Warn.TrainingExhausted": "Cet entrainement est epuise pour aujourd'hui. Reinitialisez son delta quotidien pour le lendemain.",
"DNC.Settings.MissionPackMode.Name": "Mode de paquetage de mission",
"DNC.Settings.MissionPackMode.Hint": "Choisit entre le tirage classique rapide et la regle optionnelle de campagne avec allocation des des par les joueurs.",
"DNC.Settings.MissionPackMode.Classic": "Classique",
"DNC.Settings.MissionPackMode.Campaign": "Campagne",
"DNC.Macro.MissionPack.Title": "Paquetage de debut de mission", "DNC.Macro.MissionPack.Title": "Paquetage de debut de mission",
"DNC.Macro.MissionPack.ItemsAdded": "Objets ajoutes", "DNC.Macro.MissionPack.ItemsAdded": "Objets ajoutes",
"DNC.Macro.MissionPack.ItemsMissing": "Objets manquants", "DNC.Macro.MissionPack.ItemsMissing": "Objets manquants",
@@ -121,13 +125,46 @@
"DNC.Macro.MissionPack.Partial": "{actor} recoit {count} objet(s), mais {missing} entree(s) du paquetage sont introuvables.", "DNC.Macro.MissionPack.Partial": "{actor} recoit {count} objet(s), mais {missing} entree(s) du paquetage sont introuvables.",
"DNC.Macro.MissionPack.DialogTitle": "Paquetage de mission", "DNC.Macro.MissionPack.DialogTitle": "Paquetage de mission",
"DNC.Macro.MissionPack.DialogIntro": "Selectionnez l'employe qui recevra le paquetage de debut de mission, puis lancez le tirage.", "DNC.Macro.MissionPack.DialogIntro": "Selectionnez l'employe qui recevra le paquetage de debut de mission, puis lancez le tirage.",
"DNC.Macro.MissionPack.ActiveMode": "Mode :",
"DNC.Macro.MissionPack.ModeClassic": "Classique",
"DNC.Macro.MissionPack.ModeClassicHint": "Le systeme tire directement les 4 tables d equipement, comme dans les one-shots.",
"DNC.Macro.MissionPack.ModeCampaign": "Campagne",
"DNC.Macro.MissionPack.ModeCampaignHint": "Le joueur alloue 1d20, 1d12, 1d10 et 1d8 aux 4 categories, puis l anciennete est ajoutee a chaque resultat.",
"DNC.Macro.MissionPack.DialogActor": "Employe", "DNC.Macro.MissionPack.DialogActor": "Employe",
"DNC.Macro.MissionPack.DialogAction": "Generer le paquetage", "DNC.Macro.MissionPack.DialogAction": "Generer le paquetage",
"DNC.Macro.MissionPack.SidebarButton": "Paquetage", "DNC.Macro.MissionPack.SidebarButton": "Paquetage",
"DNC.Macro.MissionPack.CampaignDialogLead": "En mode campagne, le joueur proprietaire choisira l affectation de ses des dans un dialogue dedie.",
"DNC.Macro.MissionPack.CampaignDialogTitle": "Paquetage de campagne",
"DNC.Macro.MissionPack.CampaignDialogSubtitle": "Repartition logistique",
"DNC.Macro.MissionPack.CampaignDialogIntro": "{actor} prepare son paquetage de campagne. Repartissez les des entre les categories, puis indiquez la relation a l econome.",
"DNC.Macro.MissionPack.CampaignDialogAction": "Valider l allocation",
"DNC.Macro.MissionPack.CampaignDialogPlayer": "Joueur",
"DNC.Macro.MissionPack.CampaignDialogRequester": "Demande de",
"DNC.Macro.MissionPack.CampaignDialogRank": "Anciennete",
"DNC.Macro.MissionPack.CampaignDialogRelation": "Relation a l econome",
"DNC.Macro.MissionPack.CampaignDialogAssignHelp": "Attribuez un de different a chaque categorie de paquetage.",
"DNC.Macro.MissionPack.CampaignDialogHelp": "Le d20 alloue servira aussi pour verifier l equipement unique. L anciennete est ajoutee a chaque jet apres avantage ou desavantage.",
"DNC.Macro.MissionPack.CampaignController": "Choix joueur :",
"DNC.Macro.MissionPack.CampaignRelation": "Relation :",
"DNC.Macro.MissionPack.CampaignRequestCanceled": "Le paquetage de campagne pour {actor} n a pas ete valide par {player}.",
"DNC.Macro.MissionPack.WarnDiceRequired": "Attribuez un de a chacune des 4 categories du paquetage.",
"DNC.Macro.MissionPack.WarnDiceUnique": "Chaque de ne peut etre utilise qu une seule fois.",
"DNC.Macro.MissionPack.Relation.positive": "Positive",
"DNC.Macro.MissionPack.Relation.neutral": "Neutre",
"DNC.Macro.MissionPack.Relation.negative": "Negative",
"DNC.Macro.MissionPack.RollDetail": "{die} · {mode} · lancers {values} · garde {kept} · + anciennete {rank} = {total}",
"DNC.Macro.MissionPack.TotalClamped": "Total retenu sur la table : {clamped} (au lieu de {total}).",
"DNC.Macro.MissionPack.melee": "Arme de corps a corps", "DNC.Macro.MissionPack.melee": "Arme de corps a corps",
"DNC.Macro.MissionPack.ranged": "Arme a distance", "DNC.Macro.MissionPack.ranged": "Arme a distance",
"DNC.Macro.MissionPack.armor": "Armure", "DNC.Macro.MissionPack.armor": "Armure",
"DNC.Macro.MissionPack.misc": "Encas et equipement divers", "DNC.Macro.MissionPack.misc": "Encas et equipement divers",
"DNC.Macro.MissionPack.UniqueReference": "d20 de reference",
"DNC.Macro.MissionPack.UniqueActorRoll": "d20 du joueur",
"DNC.Macro.MissionPack.UniqueMatch": "Objet unique",
"DNC.Macro.MissionPack.UniqueMiss": "Pas d objet unique",
"DNC.Macro.MissionPack.UniqueGranted": "Objet unique ajoute :",
"DNC.Macro.MissionPack.UniqueTableRoll": "tirage d objet unique",
"DNC.Macro.MissionPack.UniqueRuleReminder": "Aucun objet unique supplementaire cette fois ci.",
"DNC.Welcome.Kicker": "Accueil", "DNC.Welcome.Kicker": "Accueil",
"DNC.Welcome.Title": "Bienvenue dans Donjon & Cie", "DNC.Welcome.Title": "Bienvenue dans Donjon & Cie",
"DNC.Welcome.Subtitle": "Systeme FoundryVTT · version {version}", "DNC.Welcome.Subtitle": "Systeme FoundryVTT · version {version}",
+16 -5
View File
@@ -135,12 +135,18 @@
gap: 0.25rem; gap: 0.25rem;
} }
.dnc-employe-sheet .sheet-header.compact .hp-field {
grid-column: span 2;
}
.dnc-employe-sheet .sheet-header.compact input[type="number"] { .dnc-employe-sheet .sheet-header.compact input[type="number"] {
max-width: 4.75rem; max-width: 4.75rem;
} }
.dnc-employe-sheet .sheet-header.compact .counter-field input[type="number"] { .dnc-employe-sheet .sheet-header.compact .counter-field input[name="system.sante.pv.value"],
max-width: 4rem; .dnc-employe-sheet .sheet-header.compact .counter-field input[name="system.sante.pv.max"] {
width: 5.25rem;
max-width: 5.25rem;
} }
.dnc-pnj-sheet .sheet-header.compact .identity-grid { .dnc-pnj-sheet .sheet-header.compact .identity-grid {
@@ -152,14 +158,19 @@
gap: 0.25rem; gap: 0.25rem;
} }
.dnc-pnj-sheet .sheet-header.compact .hp-field {
grid-column: span 2;
}
.dnc-pnj-sheet .sheet-header.compact input[type="number"] { .dnc-pnj-sheet .sheet-header.compact input[type="number"] {
width: 4.5rem; width: 4.5rem;
max-width: 4.5rem; max-width: 4.5rem;
} }
.dnc-pnj-sheet .sheet-header.compact .counter-field input[type="number"] { .dnc-pnj-sheet .sheet-header.compact .counter-field input[name="system.sante.pv.value"],
width: 3.75rem; .dnc-pnj-sheet .sheet-header.compact .counter-field input[name="system.sante.pv.max"] {
max-width: 3.75rem; width: 5.25rem;
max-width: 5.25rem;
} }
.dnc-sheet label { .dnc-sheet label {
+146
View File
@@ -34,3 +34,149 @@
.dnc-roll-dialog .window-content { .dnc-roll-dialog .window-content {
background: linear-gradient(180deg, #f7efe0 0%, #e3d0b1 100%); background: linear-gradient(180deg, #f7efe0 0%, #e3d0b1 100%);
} }
.dnc-mission-pack-mode,
.dnc-mission-pack-note {
font-size: 0.9rem;
}
.dnc-mission-pack-campaign {
gap: @spacing-lg;
}
.dnc-mission-pack-hero {
padding: @spacing-lg;
border: 1px solid fade(@color-border, 35%);
border-radius: @radius-md;
background:
linear-gradient(180deg, fade(#ffffff, 65%) 0%, fade(@color-panel, 68%) 100%),
linear-gradient(135deg, fade(@color-accent, 8%) 0%, fade(@color-accent, 0%) 100%);
box-shadow: 0 8px 18px fade(@color-shadow, 10%);
}
.dnc-mission-pack-kicker {
margin: 0 0 0.25rem;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: fade(@color-accent, 82%);
}
.dnc-mission-pack-hero h2 {
margin: 0;
font-family: @font-display;
font-size: 1.4rem;
line-height: 1.1;
color: @color-accent;
}
.dnc-mission-pack-subtitle {
margin-top: 0.2rem;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
color: fade(@color-border, 78%);
}
.dnc-mission-pack-intro {
margin-top: @spacing-sm;
color: @color-muted;
}
.dnc-mission-pack-meta-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: @spacing-md;
}
.dnc-mission-pack-meta-card {
display: grid;
gap: 0.2rem;
padding: @spacing-md;
border: 1px solid fade(@color-border, 30%);
border-radius: @radius-md;
background: fade(#ffffff, 42%);
}
.dnc-mission-pack-meta-card span {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: @color-muted;
}
.dnc-mission-pack-meta-card strong {
font-size: 1rem;
}
.dnc-mission-pack-section {
display: grid;
gap: @spacing-md;
}
.dnc-mission-pack-select {
padding: @spacing-md;
border: 1px solid fade(@color-border, 32%);
border-radius: @radius-md;
background: fade(#ffffff, 36%);
}
.dnc-mission-pack-assignments {
display: grid;
gap: @spacing-md;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.dnc-mission-pack-assignment {
display: grid;
gap: @spacing-sm;
padding: @spacing-md;
border: 1px solid fade(@color-border, 35%);
border-radius: @radius-md;
background: fade(#ffffff, 38%);
box-shadow: inset 0 1px 0 fade(#ffffff, 55%);
}
.dnc-mission-pack-assignment span {
font-size: 0.82rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.dnc-mission-pack-note {
color: @color-muted;
}
.dnc-mission-pack-note-foot {
padding: @spacing-md;
border-top: 1px solid fade(@color-border, 24%);
}
.dnc-mission-pack-dialog .window-header {
background:
linear-gradient(180deg, fade(#fdf6e7, 96%) 0%, fade(#ead4aa, 96%) 100%),
linear-gradient(90deg, fade(@color-accent, 12%) 0%, fade(@color-accent, 0%) 100%);
border-bottom: 1px solid fade(@color-border, 35%);
}
.dnc-mission-pack-dialog .window-title {
color: @color-accent;
font-family: @font-display;
letter-spacing: 0.03em;
text-shadow: none;
}
.dnc-mission-pack-dialog .window-header button {
color: @color-ink;
}
@media (max-width: 640px) {
.dnc-mission-pack-meta-grid,
.dnc-mission-pack-assignments {
grid-template-columns: 1fr;
}
}
+13
View File
@@ -29,6 +29,19 @@ export class DonjonEtCieActor extends Actor {
} }
} }
async _preUpdate(changed, options, user) {
if (
foundry.utils.hasProperty(changed, "system.magie.focus.delta")
&& !foundry.utils.hasProperty(changed, "system.magie.focus.resultat")
&& !foundry.utils.hasProperty(changed, "system.magie.focus.sceneId")
) {
foundry.utils.setProperty(changed, "system.magie.focus.resultat", 0);
foundry.utils.setProperty(changed, "system.magie.focus.sceneId", "");
}
return super._preUpdate(changed, options, user);
}
getCharacteristicEntries() { getCharacteristicEntries() {
return DonjonEtCieUtility.getCharacteristicEntries(this.system); return DonjonEtCieUtility.getCharacteristicEntries(this.system);
} }
+685 -100
View File
@@ -13,6 +13,18 @@
import { DonjonEtCieUtility } from "./donjon-et-cie-utility.mjs"; import { DonjonEtCieUtility } from "./donjon-et-cie-utility.mjs";
export class DonjonEtCieMacros { export class DonjonEtCieMacros {
static MISSION_PACK_MODE_SETTING = "missionPackMode";
static MISSION_PACK_SOCKET_SCOPE = "missionPackCampaign";
static MISSION_PACK_REQUEST_TIMEOUT = 300000;
static MISSION_PACK_CAMPAIGN_DICE = [20, 12, 10, 8];
static MISSION_PACK_STEWARD_MODES = {
positive: "avantage",
neutral: "normal",
negative: "desavantage"
};
static #campaignRequests = new Map();
static #socketRegistered = false;
static MISSION_PACK_TABLES = [ static MISSION_PACK_TABLES = [
{ key: "melee", name: "Armes de corps a corps", multiple: false }, { key: "melee", name: "Armes de corps a corps", multiple: false },
{ key: "ranged", name: "Armes a distance", multiple: false }, { key: "ranged", name: "Armes a distance", multiple: false },
@@ -20,6 +32,132 @@ export class DonjonEtCieMacros {
{ key: "misc", name: "Encas et equipement divers", multiple: true } { key: "misc", name: "Encas et equipement divers", multiple: true }
]; ];
static MISSION_PACK_UNIQUE_ITEMS = [
{
name: "L epee de Monsieur Noir",
type: "arme",
system: {
categorie: "melee",
caracteristique: "force",
degats: "1d6",
portee: "",
mains: 1,
equipee: false,
description: "<p>Elle n a rien de particulier mais confere un prestige important.</p><p>Avantage a tous les jets d interaction avec les employes de Donjon &amp; Cie.</p>",
notes: ""
}
},
{
name: "Fil a plomb d Arnezon",
type: "equipement",
system: {
quantite: 1,
equipee: false,
emplacement: "",
description: "<p>Il oscille de maniere clairement etrange en presence d un passage secret.</p>",
notes: ""
}
},
{
name: "Boussole de Drize Durban",
type: "equipement",
system: {
quantite: 1,
equipee: false,
emplacement: "",
description: "<p>Trois reglages : pointe vers le client, le resident ou l employe le plus proche.</p>",
notes: ""
}
},
{
name: "Boule de cristal de la supervision",
type: "equipement",
system: {
quantite: 1,
equipee: false,
emplacement: "",
description: "<p>Permet de voir toute personne ou lieu qu on connait deja.</p><p>Jet de SAG pour la controler. La plupart des huiles sentent quand on les observe.</p>",
notes: ""
}
},
{
name: "Dent du dragon Leogradonardicus III",
type: "equipement",
system: {
quantite: 1,
equipee: false,
emplacement: "",
description: "<p>Un jet de CHA reussi permet de controler les reptiles inintelligents et de charmer les humanoides reptiliens.</p>",
notes: ""
}
},
{
name: "Doigt d Aarcarcerax",
type: "consommable",
system: {
quantite: 1,
delta: 4,
effet: "Tue la creature vers qui on pointe le doigt.",
description: "<p>Tue la creature vers qui on pointe le doigt.</p><p>La liche sait instantanement qu on a retrouve son doigt.</p>",
notes: ""
}
},
{
name: "Cape de Vlad von Drakovitch",
type: "consommable",
system: {
quantite: 1,
delta: 6,
effet: "Permet de se transformer en 1-3 rats, 4-5 chauve-souris, 6 forme gazeuse.",
description: "<p>Delta 6 charges.</p><p>Permet de se transformer en 1-3 rats, 4-5 chauve-souris, 6 forme gazeuse.</p>",
notes: ""
}
},
{
name: "Vieux carnet de notes d Affalella",
type: "consommable",
system: {
quantite: 1,
delta: 6,
effet: "Fonctionne comme des faveurs de la Mercatique utilisables uniquement dans les aires client.",
description: "<p>Vieux carnet de notes d Affalella, directrice de la Mercatique.</p><p>Fonctionne comme Delta 6 faveurs de la Mercatique utilisables uniquement dans les aires client.</p>",
notes: ""
}
},
{
name: "Ancienne cle universelle de Paiji",
type: "consommable",
system: {
quantite: 1,
delta: 8,
effet: "Ouvre tous les coffres, portes, armoires et autres serrures du Donjon.",
description: "<p>Ancienne cle universelle de Paiji, directeur de la Maintenance.</p><p>Delta 8 usages. Ouvre tous les coffres, portes, armoires et autres serrures du Donjon.</p>",
notes: ""
}
},
{
name: "Baguette d urgence",
type: "equipement",
system: {
quantite: 1,
equipee: false,
emplacement: "",
description: "<p>Teleporte l utilisateur au palier des huiles lorsque la baguette est brisee.</p>",
notes: ""
}
}
];
static registerSocketListeners() {
if (this.#socketRegistered || !game.socket) return;
game.socket.on(`system.${game.system.id}`, (payload) => {
void this.#handleSocketMessage(payload);
});
this.#socketRegistered = true;
}
static #normalizeName(value) { static #normalizeName(value) {
return String(value ?? "") return String(value ?? "")
.normalize("NFD") .normalize("NFD")
@@ -34,6 +172,50 @@ export class DonjonEtCieMacros {
return game.i18n.localize(`DNC.Macro.MissionPack.${key}`); return game.i18n.localize(`DNC.Macro.MissionPack.${key}`);
} }
static #getModeLabel(mode) {
if (mode === "avantage") return game.i18n.localize("DNC.UI.ModeAdvantage");
if (mode === "desavantage") return game.i18n.localize("DNC.UI.ModeDisadvantage");
return game.i18n.localize("DNC.UI.ModeNormal");
}
static #getMissionPackMode() {
return String(game.settings.get("fvtt-donjon-et-cie", this.MISSION_PACK_MODE_SETTING) ?? "classic");
}
static #getMissionPackModeLabel(mode) {
return game.i18n.localize(mode === "campaign"
? "DNC.Macro.MissionPack.ModeCampaign"
: "DNC.Macro.MissionPack.ModeClassic");
}
static #getMissionPackModeDescription(mode) {
return game.i18n.localize(mode === "campaign"
? "DNC.Macro.MissionPack.ModeCampaignHint"
: "DNC.Macro.MissionPack.ModeClassicHint");
}
static #getStewardRelationLabel(relation) {
return game.i18n.localize(`DNC.Macro.MissionPack.Relation.${relation ?? "neutral"}`);
}
static #getStewardRelationOptions() {
return ["positive", "neutral", "negative"].map((relation) => ({
value: relation,
label: this.#getStewardRelationLabel(relation)
}));
}
static #getCampaignDiceOptions() {
return this.MISSION_PACK_CAMPAIGN_DICE.map((sides) => ({
value: String(sides),
label: `1d${sides}`
}));
}
static #getDefaultCampaignAssignments() {
return Object.fromEntries(this.MISSION_PACK_TABLES.map((spec, index) => [spec.key, String(this.MISSION_PACK_CAMPAIGN_DICE[index])]));
}
static #getDefaultMissionPackActorId(actors) { static #getDefaultMissionPackActorId(actors) {
const controlledActor = canvas?.tokens?.controlled?.[0]?.actor ?? null; const controlledActor = canvas?.tokens?.controlled?.[0]?.actor ?? null;
if (controlledActor?.type === "employe") return controlledActor.id; if (controlledActor?.type === "employe") return controlledActor.id;
@@ -50,6 +232,16 @@ export class DonjonEtCieMacros {
})); }));
} }
static #getMissionPackOwnerUsers(actor) {
return game.users
.filter((user) => !user.isGM && actor.testUserPermission(user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER))
.sort((a, b) => Number(b.active) - Number(a.active) || a.name.localeCompare(b.name, "fr", { sensitivity: "base" }));
}
static #getCampaignTargetUser(actor) {
return this.#getMissionPackOwnerUsers(actor).find((user) => user.active) ?? null;
}
static async #resolveMissionPackActor(target = null) { static async #resolveMissionPackActor(target = null) {
if (target?.documentName === "Actor") return target; if (target?.documentName === "Actor") return target;
if (target?.actor?.documentName === "Actor") return target.actor; if (target?.actor?.documentName === "Actor") return target.actor;
@@ -121,6 +313,127 @@ export class DonjonEtCieMacros {
.filter((entry) => !/^dotation\s+\d+$/i.test(entry)); .filter((entry) => !/^dotation\s+\d+$/i.test(entry));
} }
static async #evaluateRoll(formula) {
const roll = await (new Roll(formula)).evaluate();
const values = roll.dice
.flatMap((die) => die.results.map((result) => Number(result.result ?? result.count ?? 0)))
.filter((value) => Number.isFinite(value));
return {
roll,
total: Number(roll.total ?? 0),
values
};
}
static #formatRollValues(values, kept) {
if (!values.length) return String(kept ?? 0);
if (values.length === 1) return String(values[0]);
return `${values.join(" / ")} -> ${kept}`;
}
static async #rollPackDie(sides, { mode = "normal" } = {}) {
const formula = mode === "avantage"
? `2d${sides}kh`
: mode === "desavantage"
? `2d${sides}kl`
: `1d${sides}`;
const evaluated = await this.#evaluateRoll(formula);
return {
sides: Number(sides),
dieLabel: `1d${sides}`,
roll: evaluated.roll,
values: evaluated.values,
valuesLabel: this.#formatRollValues(evaluated.values, evaluated.total),
kept: evaluated.total,
mode,
modeLabel: this.#getModeLabel(mode)
};
}
static #createInlineItemData(spec) {
return {
name: spec.name,
type: spec.type,
img: DonjonEtCieUtility.getDefaultItemIcon(spec.type),
system: foundry.utils.deepClone(spec.system)
};
}
static #getTableResults(table) {
return table?.results?.contents ?? [];
}
static #getTableRange(result) {
const rawRange = Array.isArray(result?.range)
? result.range
: result?._source?.range ?? [];
return [Number(rawRange[0] ?? 0), Number(rawRange[1] ?? 0)];
}
static #findTableResultByTotal(table, total) {
const entries = this.#getTableResults(table)
.map((result) => ({
result,
range: this.#getTableRange(result)
}))
.filter(({ range }) => Number.isFinite(range[0]) && Number.isFinite(range[1]) && range[1] >= range[0])
.sort((a, b) => a.range[0] - b.range[0]);
if (!entries.length) {
return {
result: null,
clampedTotal: total,
range: null
};
}
const minimum = entries[0].range[0];
const maximum = entries.at(-1).range[1];
const clampedTotal = Math.max(minimum, Math.min(maximum, total));
const match = entries.find(({ range }) => clampedTotal >= range[0] && clampedTotal <= range[1]) ?? entries.at(-1);
return {
result: match.result,
clampedTotal,
range: match.range
};
}
static async #resolveUniqueMissionPackEntry({ comparisonRoll = null } = {}) {
const reference = await this.#rollPackDie(20);
const actorEvaluation = comparisonRoll == null ? await this.#rollPackDie(20) : null;
const actorRoll = Number(comparisonRoll ?? actorEvaluation?.kept ?? 0);
if (reference.kept !== actorRoll) {
return {
matched: false,
referenceRoll: reference.kept,
actorRoll,
uniqueRoll: null,
itemName: "",
itemData: null,
rolls: [reference.roll, ...(actorEvaluation ? [actorEvaluation.roll] : [])]
};
}
const uniqueRoll = await this.#rollPackDie(this.MISSION_PACK_UNIQUE_ITEMS.length);
const spec = this.MISSION_PACK_UNIQUE_ITEMS[Math.max(0, uniqueRoll.kept - 1)] ?? this.MISSION_PACK_UNIQUE_ITEMS[0];
const document = await this.#findItemByName(spec.name);
const itemData = document ? this.#toEmbeddedItemData(document) : this.#createInlineItemData(spec);
return {
matched: true,
referenceRoll: reference.kept,
actorRoll,
uniqueRoll: uniqueRoll.kept,
itemName: document?.name ?? spec.name,
itemData,
rolls: [reference.roll, ...(actorEvaluation ? [actorEvaluation.roll] : []), uniqueRoll.roll]
};
}
static async #resolveTableResultEntries(result, { multiple = false } = {}) { static async #resolveTableResultEntries(result, { multiple = false } = {}) {
if (!result) { if (!result) {
return { return {
@@ -129,19 +442,27 @@ export class DonjonEtCieMacros {
}; };
} }
if (result.type === "document" && result.documentCollection && result.documentId) { const source = result._source ?? {};
const uuid = result.documentCollection.includes(".") const resultName = String(result.name ?? source.name ?? "").trim();
? `Compendium.${result.documentCollection}.Item.${result.documentId}` const resultDescription = String(result.description ?? source.description ?? "").trim();
: `Item.${result.documentId}`; const resultText = String(source.text ?? "").trim();
const document = await fromUuid(uuid); const resultLabel = resultName || resultDescription || resultText;
const label = document?.name ?? result.text ?? "";
const documentUuid = result.type === "document"
? result.documentUuid ?? source.documentUuid ?? null
: null;
if (documentUuid) {
const document = await fromUuid(documentUuid);
const label = document?.name ?? resultLabel;
return { return {
display: label, display: label,
entries: label ? [{ name: label, document }] : [] entries: label ? [{ name: label, document }] : []
}; };
} }
const uuidTargets = this.#extractUuidTargets(result.text); const sourceText = resultDescription || resultText || resultLabel;
const uuidTargets = this.#extractUuidTargets(sourceText);
if (uuidTargets.length) { if (uuidTargets.length) {
const entries = []; const entries = [];
for (const target of uuidTargets) { for (const target of uuidTargets) {
@@ -158,8 +479,8 @@ export class DonjonEtCieMacros {
} }
const names = multiple const names = multiple
? this.#extractPlainTextEntries(result.text) ? this.#extractPlainTextEntries(sourceText || resultLabel)
: [result.text].map((entry) => String(entry ?? "").trim()).filter(Boolean); : [resultLabel].filter(Boolean);
return { return {
display: names.join(", "), display: names.join(", "),
@@ -175,6 +496,353 @@ export class DonjonEtCieMacros {
return data; return data;
} }
static async #materializeMissionPackEntries(entries) {
const embeddedItems = [];
const addedNames = [];
const missingNames = [];
for (const entry of entries) {
const item = entry.document ?? await this.#findItemByName(entry.name);
if (!item) {
missingNames.push(entry.name);
continue;
}
embeddedItems.push(this.#toEmbeddedItemData(item));
addedNames.push(item.name);
}
return {
embeddedItems,
addedNames,
missingNames
};
}
static async #openCampaignAllocationDialog(actor, { playerName = "", requesterName = "" } = {}) {
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-donjon-et-cie/templates/dialogs/mission-pack-campaign-dialog.hbs",
{
actorName: actor.name,
playerName,
requesterName,
rank: Number(actor.system.anciennete?.rang ?? actor.system.sante?.dv ?? 0),
assignments: this.MISSION_PACK_TABLES.map((spec) => ({
key: spec.key,
label: this.#getMissionPackLabel(spec.key),
fieldName: `${spec.key}Die`,
selectedDie: this.#getDefaultCampaignAssignments()[spec.key]
})),
diceOptions: this.#getCampaignDiceOptions(),
relationOptions: this.#getStewardRelationOptions(),
selectedRelation: "neutral"
}
);
return foundry.applications.api.DialogV2.wait({
window: {
title: game.i18n.localize("DNC.Macro.MissionPack.CampaignDialogTitle"),
icon: "fa-solid fa-dice"
},
classes: ["dnc-roll-dialog", "dnc-mission-pack-dialog"],
content,
modal: true,
buttons: [
{
action: "confirm",
label: game.i18n.localize("DNC.Macro.MissionPack.CampaignDialogAction"),
icon: "fa-solid fa-check",
default: true,
callback: async (event, button) => {
const assignments = Object.fromEntries(this.MISSION_PACK_TABLES.map((spec) => [
spec.key,
Number(button.form.elements[`${spec.key}Die`]?.value ?? 0)
]));
const selectedDice = Object.values(assignments).filter((value) => value > 0);
if (selectedDice.length !== this.MISSION_PACK_TABLES.length) {
ui.notifications.warn(game.i18n.localize("DNC.Macro.MissionPack.WarnDiceRequired"));
return null;
}
if (new Set(selectedDice).size !== selectedDice.length) {
ui.notifications.warn(game.i18n.localize("DNC.Macro.MissionPack.WarnDiceUnique"));
return null;
}
return {
assignments,
stewardRelation: button.form.elements.stewardRelation?.value ?? "neutral"
};
}
}
],
rejectClose: false
});
}
static async #handleSocketMessage(payload) {
if (payload?.scope !== this.MISSION_PACK_SOCKET_SCOPE) return;
if (payload.type === "campaign-response") {
const pending = this.#campaignRequests.get(payload.requestId);
if (!pending) return;
clearTimeout(pending.timeoutId);
this.#campaignRequests.delete(payload.requestId);
pending.resolve(payload.result ?? null);
return;
}
if (payload.type !== "campaign-request" || payload.targetUserId !== game.user.id) return;
const actor = await fromUuid(payload.actorUuid);
const allocation = actor
? await this.#openCampaignAllocationDialog(actor, {
playerName: game.user.name,
requesterName: payload.requesterName ?? ""
})
: null;
game.socket.emit(`system.${game.system.id}`, {
scope: this.MISSION_PACK_SOCKET_SCOPE,
type: "campaign-response",
requestId: payload.requestId,
result: allocation
? {
allocation,
responderUserId: game.user.id,
responderName: game.user.name
}
: {
allocation: null,
responderUserId: game.user.id,
responderName: game.user.name
}
});
}
static async #requestCampaignAllocation(actor, targetUser) {
if (!targetUser || targetUser.id === game.user.id) {
const allocation = await this.#openCampaignAllocationDialog(actor, {
playerName: game.user.name,
requesterName: game.user.name
});
return allocation ? { allocation, responderName: game.user.name } : null;
}
const requestId = foundry.utils.randomID();
const responsePromise = new Promise((resolve) => {
const timeoutId = globalThis.setTimeout(() => {
this.#campaignRequests.delete(requestId);
resolve(null);
}, this.MISSION_PACK_REQUEST_TIMEOUT);
this.#campaignRequests.set(requestId, { resolve, timeoutId });
});
game.socket.emit(`system.${game.system.id}`, {
scope: this.MISSION_PACK_SOCKET_SCOPE,
type: "campaign-request",
requestId,
targetUserId: targetUser.id,
actorUuid: actor.uuid,
requesterName: game.user.name
});
const response = await responsePromise;
if (!response?.allocation) {
ui.notifications.warn(game.i18n.format("DNC.Macro.MissionPack.CampaignRequestCanceled", {
actor: actor.name,
player: targetUser.name
}));
return null;
}
return {
allocation: response.allocation,
responderName: response.responderName ?? targetUser.name
};
}
static async #finalizeMissionPack(actor, drawPlans, {
generationMode = "classic",
controllerName = "",
stewardRelation = "neutral",
uniqueComparisonRoll = null
} = {}) {
const draws = [];
const embeddedItems = [];
const rolls = [];
let missingCount = 0;
for (const plan of drawPlans) {
rolls.push(...(plan.rolls ?? []).filter((roll) => roll instanceof Roll));
if (plan.failed || !plan.resolved) {
draws.push({
label: this.#getMissionPackLabel(plan.spec.key),
display: game.i18n.format("DNC.Macro.MissionPack.TableMissing", { table: plan.spec.name }),
addedNames: [],
addedSummary: "",
missingNames: [],
missingSummary: "",
failed: true
});
continue;
}
const materialized = await this.#materializeMissionPackEntries(plan.resolved.entries);
embeddedItems.push(...materialized.embeddedItems);
missingCount += materialized.missingNames.length;
draws.push({
label: this.#getMissionPackLabel(plan.spec.key),
display: plan.resolved.display || game.i18n.localize("DNC.Macro.MissionPack.NoResult"),
addedNames: materialized.addedNames,
addedSummary: materialized.addedNames.join(", "),
missingNames: materialized.missingNames,
missingSummary: materialized.missingNames.join(", "),
failed: false,
...plan.detail
});
}
const uniqueEntry = await this.#resolveUniqueMissionPackEntry({ comparisonRoll: uniqueComparisonRoll });
rolls.push(...(uniqueEntry.rolls ?? []).filter((roll) => roll instanceof Roll));
if (uniqueEntry.itemData) {
embeddedItems.push(uniqueEntry.itemData);
}
const createdItems = embeddedItems.length
? await actor.createEmbeddedDocuments("Item", embeddedItems, { renderSheet: false })
: [];
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-donjon-et-cie/templates/chat/mission-pack-card.hbs",
{
title: game.i18n.localize("DNC.Macro.MissionPack.Title"),
actorName: actor.name,
createdCount: createdItems.length,
missingCount,
draws,
uniqueEntry,
isCampaign: generationMode === "campaign",
generationModeLabel: this.#getMissionPackModeLabel(generationMode),
controllerName,
stewardRelationLabel: generationMode === "campaign" ? this.#getStewardRelationLabel(stewardRelation) : ""
}
);
await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor }),
user: game.user.id,
content,
rolls
});
if (createdItems.length && !missingCount) {
ui.notifications.info(game.i18n.format("DNC.Macro.MissionPack.Success", {
actor: actor.name,
count: createdItems.length
}));
} else if (createdItems.length) {
ui.notifications.warn(game.i18n.format("DNC.Macro.MissionPack.Partial", {
actor: actor.name,
count: createdItems.length,
missing: missingCount
}));
} else {
ui.notifications.warn(game.i18n.localize("DNC.Macro.MissionPack.WarnNothingAdded"));
}
return {
actor,
createdItems,
missingCount,
draws,
uniqueEntry,
generationMode,
controllerName,
stewardRelation
};
}
static async #grantClassicMissionPack(actor) {
const drawPlans = [];
for (const spec of this.MISSION_PACK_TABLES) {
const table = await this.#findRollTableByName(spec.name);
if (!table) {
drawPlans.push({ spec, failed: true, resolved: null, rolls: [] });
continue;
}
const draw = await table.draw({ displayChat: false });
const result = draw.results?.[0] ?? null;
drawPlans.push({
spec,
failed: false,
resolved: await this.#resolveTableResultEntries(result, { multiple: spec.multiple }),
detail: null,
rolls: draw.roll instanceof Roll ? [draw.roll] : []
});
}
return this.#finalizeMissionPack(actor, drawPlans, {
generationMode: "classic"
});
}
static async #grantCampaignMissionPack(actor) {
const targetUser = this.#getCampaignTargetUser(actor);
const allocationResult = await this.#requestCampaignAllocation(actor, targetUser);
if (!allocationResult?.allocation) return null;
const rankBonus = Number(actor.system.anciennete?.rang ?? actor.system.sante?.dv ?? 0);
const stewardRelation = allocationResult.allocation.stewardRelation ?? "neutral";
const mode = this.MISSION_PACK_STEWARD_MODES[stewardRelation] ?? "normal";
const drawPlans = [];
let uniqueComparisonRoll = null;
for (const spec of this.MISSION_PACK_TABLES) {
const table = await this.#findRollTableByName(spec.name);
if (!table) {
drawPlans.push({ spec, failed: true, resolved: null, rolls: [] });
continue;
}
const sides = Number(allocationResult.allocation.assignments?.[spec.key] ?? 0);
const rollData = await this.#rollPackDie(sides, { mode });
if (sides === 20) uniqueComparisonRoll = rollData.kept;
const total = rollData.kept + rankBonus;
const tableResult = this.#findTableResultByTotal(table, total);
drawPlans.push({
spec,
failed: false,
resolved: await this.#resolveTableResultEntries(tableResult.result, { multiple: spec.multiple }),
detail: {
dieLabel: rollData.dieLabel,
modeLabel: rollData.modeLabel,
rollValuesLabel: rollData.valuesLabel,
kept: rollData.kept,
rankBonus,
total,
resolvedTotal: tableResult.clampedTotal,
clamped: tableResult.clampedTotal !== total
},
rolls: [rollData.roll]
});
}
return this.#finalizeMissionPack(actor, drawPlans, {
generationMode: "campaign",
controllerName: allocationResult.responderName ?? targetUser?.name ?? game.user.name,
stewardRelation,
uniqueComparisonRoll
});
}
/** /**
* Open the GM-only mission pack dialog. * Open the GM-only mission pack dialog.
* @returns {Promise<object|null>} * @returns {Promise<object|null>}
@@ -191,12 +859,16 @@ export class DonjonEtCieMacros {
return null; return null;
} }
const mode = this.#getMissionPackMode();
const selectedActorId = this.#getDefaultMissionPackActorId(actorOptions.map((option) => game.actors.get(option.value)).filter(Boolean)); const selectedActorId = this.#getDefaultMissionPackActorId(actorOptions.map((option) => game.actors.get(option.value)).filter(Boolean));
const content = await foundry.applications.handlebars.renderTemplate( const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-donjon-et-cie/templates/dialogs/mission-pack-dialog.hbs", "systems/fvtt-donjon-et-cie/templates/dialogs/mission-pack-dialog.hbs",
{ {
actorOptions, actorOptions,
selectedActorId selectedActorId,
modeLabel: this.#getMissionPackModeLabel(mode),
modeDescription: this.#getMissionPackModeDescription(mode),
isCampaign: mode === "campaign"
} }
); );
@@ -246,95 +918,8 @@ export class DonjonEtCieMacros {
return null; return null;
} }
const draws = []; return this.#getMissionPackMode() === "campaign"
const embeddedItems = []; ? this.#grantCampaignMissionPack(actor)
let missingCount = 0; : this.#grantClassicMissionPack(actor);
for (const spec of this.MISSION_PACK_TABLES) {
const table = await this.#findRollTableByName(spec.name);
if (!table) {
draws.push({
label: this.#getMissionPackLabel(spec.key),
display: game.i18n.format("DNC.Macro.MissionPack.TableMissing", { table: spec.name }),
addedNames: [],
addedSummary: "",
missingNames: [],
missingSummary: "",
failed: true
});
continue;
}
const draw = await table.draw({ displayChat: false });
const result = draw.results?.[0] ?? null;
const resolved = await this.#resolveTableResultEntries(result, { multiple: spec.multiple });
const addedNames = [];
const missingNames = [];
for (const entry of resolved.entries) {
const item = entry.document ?? await this.#findItemByName(entry.name);
if (!item) {
missingNames.push(entry.name);
missingCount += 1;
continue;
}
embeddedItems.push(this.#toEmbeddedItemData(item));
addedNames.push(item.name);
}
draws.push({
label: this.#getMissionPackLabel(spec.key),
display: resolved.display || game.i18n.localize("DNC.Macro.MissionPack.NoResult"),
addedNames,
addedSummary: addedNames.join(", "),
missingNames,
missingSummary: missingNames.join(", "),
failed: false
});
}
const createdItems = embeddedItems.length
? await actor.createEmbeddedDocuments("Item", embeddedItems, { renderSheet: false })
: [];
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-donjon-et-cie/templates/chat/mission-pack-card.hbs",
{
title: game.i18n.localize("DNC.Macro.MissionPack.Title"),
actorName: actor.name,
createdCount: createdItems.length,
missingCount,
draws
}
);
await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor }),
user: game.user.id,
content
});
if (createdItems.length && !missingCount) {
ui.notifications.info(game.i18n.format("DNC.Macro.MissionPack.Success", {
actor: actor.name,
count: createdItems.length
}));
} else if (createdItems.length) {
ui.notifications.warn(game.i18n.format("DNC.Macro.MissionPack.Partial", {
actor: actor.name,
count: createdItems.length,
missing: missingCount
}));
} else {
ui.notifications.warn(game.i18n.localize("DNC.Macro.MissionPack.WarnNothingAdded"));
}
return {
actor,
createdItems,
missingCount,
draws
};
} }
} }
+14
View File
@@ -98,6 +98,19 @@ function registerSystemSettings() {
type: String, type: String,
default: "" default: ""
}); });
game.settings.register("fvtt-donjon-et-cie", DonjonEtCieMacros.MISSION_PACK_MODE_SETTING, {
name: "Mode de paquetage de mission",
hint: "Choisit entre le tirage classique rapide et la regle optionnelle de campagne avec allocation des des par les joueurs.",
scope: "world",
config: true,
type: String,
default: "classic",
choices: {
classic: "Classique",
campaign: "Campagne"
}
});
} }
async function getHelpJournalLink() { async function getHelpJournalLink() {
@@ -215,6 +228,7 @@ Hooks.once("init", async () => {
}); });
Hooks.once("ready", () => { Hooks.once("ready", () => {
DonjonEtCieMacros.registerSocketListeners();
document.addEventListener("click", onChatActionClick); document.addEventListener("click", onChatActionClick);
void maybeCreateWelcomeMessage(); void maybeCreateWelcomeMessage();
}); });
+26 -16
View File
@@ -14,12 +14,14 @@ import { DonjonEtCieUtility } from "./donjon-et-cie-utility.mjs";
import { DONJON_ET_CIE } from "./donjon-et-cie-config.mjs"; import { DONJON_ET_CIE } from "./donjon-et-cie-config.mjs";
export class DonjonEtCieRolls { export class DonjonEtCieRolls {
static async #createChatCard(actor, template, context) { static async #createChatCard(actor, template, context, { rolls = [] } = {}) {
const content = await foundry.applications.handlebars.renderTemplate(template, context); const content = await foundry.applications.handlebars.renderTemplate(template, context);
const validRolls = rolls.filter((roll) => roll instanceof Roll);
await ChatMessage.create({ await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor }), speaker: ChatMessage.getSpeaker({ actor }),
user: game.user.id, user: game.user.id,
content content,
rolls: validRolls
}); });
} }
@@ -68,7 +70,7 @@ export class DonjonEtCieRolls {
const kept = this.#selectKeptValue(values, mode, "low"); const kept = this.#selectKeptValue(values, mode, "low");
const success = kept <= target; const success = kept <= target;
return { characteristic, characteristicKey, target, values, kept, success, mode, isNaturalOne: kept === 1, isNaturalTwenty: kept === 20 }; return { characteristic, characteristicKey, target, values, kept, success, mode, roll, isNaturalOne: kept === 1, isNaturalTwenty: kept === 20 };
} }
static async #resolveFavorBoost(actor, favorKey, mode = "normal") { static async #resolveFavorBoost(actor, favorKey, mode = "normal") {
@@ -101,6 +103,7 @@ export class DonjonEtCieRolls {
effectiveMode: this.#applyFavorMode(mode), effectiveMode: this.#applyFavorMode(mode),
modeBefore: mode, modeBefore: mode,
modeAfter: this.#applyFavorMode(mode), modeAfter: this.#applyFavorMode(mode),
rolls: resolved.rolls,
note: degraded note: degraded
? "Le coup de pouce reste anonyme : un collegue du departement a donne l'info utile." ? "Le coup de pouce reste anonyme : un collegue du departement a donne l'info utile."
: "Le coup de pouce tient bon : nommez le collegue, ses trois traits et la relation pour le trombinoscope." : "Le coup de pouce tient bon : nommez le collegue, ses trois traits et la relation pour le trombinoscope."
@@ -129,7 +132,7 @@ export class DonjonEtCieRolls {
after: DonjonEtCieUtility.formatUsageDie(after), after: DonjonEtCieUtility.formatUsageDie(after),
autoSpent: true, autoSpent: true,
note: "La faveur est brulee pour obtenir directement l'aide souhaitee, a la discretion du MJ." note: "La faveur est brulee pour obtenir directement l'aide souhaitee, a la discretion du MJ."
}); }, { rolls: [] });
return { key: favorKey, label, before, after }; return { key: favorKey, label, before, after };
} }
@@ -172,6 +175,7 @@ export class DonjonEtCieRolls {
before: focusDelta, before: focusDelta,
after, after,
degraded, degraded,
rolls: resolved.rolls,
values: resolved.values values: resolved.values
}; };
} }
@@ -208,7 +212,7 @@ export class DonjonEtCieRolls {
{ label: game.i18n.localize("DNC.Chat.After"), value: DonjonEtCieUtility.formatUsageDie(favor.after) } { label: game.i18n.localize("DNC.Chat.After"), value: DonjonEtCieUtility.formatUsageDie(favor.after) }
] : []) ] : [])
] ]
}); }, { rolls: [...(favor?.rolls ?? []), result.roll] });
return { ...result, favor, mode: effectiveMode }; return { ...result, favor, mode: effectiveMode };
} }
@@ -246,7 +250,7 @@ export class DonjonEtCieRolls {
mode: result.mode, mode: result.mode,
modeLabel: this.#getModeLabel(result.mode), modeLabel: this.#getModeLabel(result.mode),
syncedCombat syncedCombat
}); }, { rolls: result.rolls });
return { total: result.kept, die, dieValues, dex, bonus: sheetBonus, mode: result.mode, syncedCombat }; return { total: result.kept, die, dieValues, dex, bonus: sheetBonus, mode: result.mode, syncedCombat };
} }
@@ -271,7 +275,7 @@ export class DonjonEtCieRolls {
formula: roll.formula, formula: roll.formula,
total: roll.total, total: roll.total,
dieValues dieValues
}); }, { rolls: [roll] });
return { formula: roll.formula, total: roll.total, dieValues }; return { formula: roll.formula, total: roll.total, dieValues };
} }
@@ -318,7 +322,7 @@ export class DonjonEtCieRolls {
{ label: game.i18n.localize("DNC.Chat.After"), value: DonjonEtCieUtility.formatUsageDie(favor.after) } { label: game.i18n.localize("DNC.Chat.After"), value: DonjonEtCieUtility.formatUsageDie(favor.after) }
] : []) ] : [])
] ]
}); }, { rolls: [...(favor?.rolls ?? []), result.roll] });
return { ...result, favor, mode: effectiveMode }; return { ...result, favor, mode: effectiveMode };
} }
@@ -351,7 +355,7 @@ export class DonjonEtCieRolls {
sourceLabel: item.name, sourceLabel: item.name,
targets, targets,
hasTargets: targets.length > 0 hasTargets: targets.length > 0
}); }, { 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, bonus: totalBonus, values: result.values, mode: result.mode };
} }
@@ -389,6 +393,7 @@ export class DonjonEtCieRolls {
static async rollSpell(actor, item, { mode = "normal", favorKey = "" } = {}) { static async rollSpell(actor, item, { mode = "normal", favorKey = "" } = {}) {
const characteristicKey = item.system.caracteristique || "intelligence"; const characteristicKey = item.system.caracteristique || "intelligence";
const focus = await this.#ensureFocus(actor); const focus = await this.#ensureFocus(actor);
const currentSceneId = DonjonEtCieUtility.getCurrentSceneId();
const rank = Number(actor.system.anciennete?.rang ?? actor.system.sante?.dv ?? 0); const rank = Number(actor.system.anciennete?.rang ?? actor.system.sante?.dv ?? 0);
const cost = Number(item.system.coutPv ?? 0); const cost = Number(item.system.coutPv ?? 0);
const autoDisadvantage = cost > rank; const autoDisadvantage = cost > rank;
@@ -399,8 +404,13 @@ export class DonjonEtCieRolls {
if (!result) return null; if (!result) return null;
const currentPv = Number(actor.system.sante?.pv?.value ?? 0); const liveActor = game.actors.get(actor.id) ?? actor;
const availableMagicHp = currentPv + focus.activeValue; const currentPv = Number(liveActor.system.sante?.pv?.value ?? 0);
const currentFocusSceneId = liveActor.system.magie?.focus?.sceneId ?? "";
const currentFocusValue = currentFocusSceneId === currentSceneId
? Number(liveActor.system.magie?.focus?.resultat ?? 0)
: 0;
const availableMagicHp = currentPv + currentFocusValue;
if (cost > availableMagicHp) { if (cost > availableMagicHp) {
ui.notifications.warn(game.i18n.localize("DNC.Warn.SpellInsufficientResources")); ui.notifications.warn(game.i18n.localize("DNC.Warn.SpellInsufficientResources"));
@@ -409,8 +419,8 @@ export class DonjonEtCieRolls {
const characteristicShort = DONJON_ET_CIE.characteristics[characteristicKey]?.short ?? characteristicKey; const characteristicShort = DONJON_ET_CIE.characteristics[characteristicKey]?.short ?? characteristicKey;
const success = result.isNaturalTwenty ? false : result.success; const success = result.isNaturalTwenty ? false : result.success;
const focusSpent = result.isNaturalOne ? 0 : Math.min(cost, focus.activeValue); const focusSpent = result.isNaturalOne ? 0 : Math.min(cost, currentFocusValue);
const focusRemaining = Math.max(focus.activeValue - focusSpent, 0); const focusRemaining = Math.max(currentFocusValue - focusSpent, 0);
const spentPv = result.isNaturalOne ? 0 : Math.max(cost - focusSpent, 0); const spentPv = result.isNaturalOne ? 0 : Math.max(cost - focusSpent, 0);
const remainingPv = Math.max(currentPv - spentPv, 0); const remainingPv = Math.max(currentPv - spentPv, 0);
const updateData = {}; const updateData = {};
@@ -483,7 +493,7 @@ export class DonjonEtCieRolls {
focusDegraded: focus.degraded, focusDegraded: focus.degraded,
spentPv, spentPv,
remainingPv remainingPv
}); }, { rolls: [...(favor?.rolls ?? []), ...(focus.rolls ?? []), result.roll] });
return { ...result, success, spentPv, remainingPv, cost, focus, focusSpent, focusRemaining, favor, mode: effectiveMode }; return { ...result, success, spentPv, remainingPv, cost, focus, focusSpent, focusRemaining, favor, mode: effectiveMode };
} }
@@ -514,7 +524,7 @@ export class DonjonEtCieRolls {
degraded, degraded,
exhausted: after < 4, exhausted: after < 4,
itemName: item.name itemName: item.name
}); }, { rolls: resolved.rolls });
return { result, before, after, degraded, chaosEntry }; return { result, before, after, degraded, chaosEntry };
} }
@@ -552,7 +562,7 @@ export class DonjonEtCieRolls {
protectionStored: item.type === "armure" ? result : null, protectionStored: item.type === "armure" ? result : null,
degraded, degraded,
exhausted: after === 0 exhausted: after === 0
}); }, { rolls: resolved.rolls });
return { result, values: resolved.values, mode: resolved.mode, before, after, degraded }; return { result, values: resolved.values, mode: resolved.mode, before, after, degraded };
} }
+1
View File
@@ -35,6 +35,7 @@ export class DonjonEtCieUtility {
"systems/fvtt-donjon-et-cie/templates/dialogs/spell-roll.hbs", "systems/fvtt-donjon-et-cie/templates/dialogs/spell-roll.hbs",
"systems/fvtt-donjon-et-cie/templates/dialogs/usage-roll.hbs", "systems/fvtt-donjon-et-cie/templates/dialogs/usage-roll.hbs",
"systems/fvtt-donjon-et-cie/templates/dialogs/mission-pack-dialog.hbs", "systems/fvtt-donjon-et-cie/templates/dialogs/mission-pack-dialog.hbs",
"systems/fvtt-donjon-et-cie/templates/dialogs/mission-pack-campaign-dialog.hbs",
"systems/fvtt-donjon-et-cie/templates/chat/roll-card.hbs", "systems/fvtt-donjon-et-cie/templates/chat/roll-card.hbs",
"systems/fvtt-donjon-et-cie/templates/chat/spell-card.hbs", "systems/fvtt-donjon-et-cie/templates/chat/spell-card.hbs",
"systems/fvtt-donjon-et-cie/templates/chat/chaos-card.hbs", "systems/fvtt-donjon-et-cie/templates/chat/chaos-card.hbs",
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000061 MANIFEST-000077
+7 -7
View File
@@ -1,7 +1,7 @@
2026/05/01-09:12:26.343249 7f3f34bfd6c0 Recovering log #59 2026/05/09-23:26:00.662378 7fe6d3fff6c0 Recovering log #75
2026/05/01-09:12:26.359113 7f3f34bfd6c0 Delete type=3 #57 2026/05/09-23:26:00.714642 7fe6d3fff6c0 Delete type=3 #73
2026/05/01-09:12:26.359184 7f3f34bfd6c0 Delete type=0 #59 2026/05/09-23:26:00.714705 7fe6d3fff6c0 Delete type=0 #75
2026/05/01-09:12:52.776973 7f3ee77fe6c0 Level-0 table #64: started 2026/05/09-23:32:35.906147 7fe6d37fe6c0 Level-0 table #80: started
2026/05/01-09:12:52.777005 7f3ee77fe6c0 Level-0 table #64: 0 bytes OK 2026/05/09-23:32:35.906185 7fe6d37fe6c0 Level-0 table #80: 0 bytes OK
2026/05/01-09:12:52.786696 7f3ee77fe6c0 Delete type=0 #62 2026/05/09-23:32:35.912511 7fe6d37fe6c0 Delete type=0 #78
2026/05/01-09:12:52.798282 7f3ee77fe6c0 Manual compaction at level-0 from '!folders!K9aiFu0dE6UYiXBd' @ 72057594037927935 : 1 .. '!items!zyqLzmpbHxK3jt5q' @ 0 : 0; will stop at (end) 2026/05/09-23:32:35.920264 7fe6d37fe6c0 Manual compaction at level-0 from '!folders!K9aiFu0dE6UYiXBd' @ 72057594037927935 : 1 .. '!items!zyqLzmpbHxK3jt5q' @ 0 : 0; will stop at (end)
+7 -7
View File
@@ -1,7 +1,7 @@
2026/05/01-00:45:47.927508 7f3f34bfd6c0 Recovering log #55 2026/05/09-23:24:27.244461 7fe6d3fff6c0 Recovering log #71
2026/05/01-00:45:47.937371 7f3f34bfd6c0 Delete type=3 #53 2026/05/09-23:24:27.261516 7fe6d3fff6c0 Delete type=3 #69
2026/05/01-00:45:47.937437 7f3f34bfd6c0 Delete type=0 #55 2026/05/09-23:24:27.261564 7fe6d3fff6c0 Delete type=0 #71
2026/05/01-00:49:08.816625 7f3ee77fe6c0 Level-0 table #60: started 2026/05/09-23:25:01.848661 7fe6d37fe6c0 Level-0 table #76: started
2026/05/01-00:49:08.816656 7f3ee77fe6c0 Level-0 table #60: 0 bytes OK 2026/05/09-23:25:01.848715 7fe6d37fe6c0 Level-0 table #76: 0 bytes OK
2026/05/01-00:49:08.823102 7f3ee77fe6c0 Delete type=0 #58 2026/05/09-23:25:01.854909 7fe6d37fe6c0 Delete type=0 #74
2026/05/01-00:49:08.830613 7f3ee77fe6c0 Manual compaction at level-0 from '!folders!K9aiFu0dE6UYiXBd' @ 72057594037927935 : 1 .. '!items!zyqLzmpbHxK3jt5q' @ 0 : 0; will stop at (end) 2026/05/09-23:25:01.877929 7fe6d37fe6c0 Manual compaction at level-0 from '!folders!K9aiFu0dE6UYiXBd' @ 72057594037927935 : 1 .. '!items!zyqLzmpbHxK3jt5q' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000048 MANIFEST-000064
+7 -7
View File
@@ -1,7 +1,7 @@
2026/05/01-09:12:26.362764 7f3ee7fff6c0 Recovering log #46 2026/05/09-23:26:00.734399 7fe7211fe6c0 Recovering log #62
2026/05/01-09:12:26.377202 7f3ee7fff6c0 Delete type=3 #44 2026/05/09-23:26:00.785619 7fe7211fe6c0 Delete type=3 #60
2026/05/01-09:12:26.377270 7f3ee7fff6c0 Delete type=0 #46 2026/05/09-23:26:00.785680 7fe7211fe6c0 Delete type=0 #62
2026/05/01-09:12:52.767020 7f3ee77fe6c0 Level-0 table #51: started 2026/05/09-23:32:35.893861 7fe6d37fe6c0 Level-0 table #67: started
2026/05/01-09:12:52.767054 7f3ee77fe6c0 Level-0 table #51: 0 bytes OK 2026/05/09-23:32:35.893901 7fe6d37fe6c0 Level-0 table #67: 0 bytes OK
2026/05/01-09:12:52.776818 7f3ee77fe6c0 Delete type=0 #49 2026/05/09-23:32:35.899905 7fe6d37fe6c0 Delete type=0 #65
2026/05/01-09:12:52.798270 7f3ee77fe6c0 Manual compaction at level-0 from '!tables!PPsxQgHwLCQ2gjSW' @ 72057594037927935 : 1 .. '!tables.results!wJZXUo4q5b5vE3Dy.zFTPLMc9zOl5hISV' @ 0 : 0; will stop at (end) 2026/05/09-23:32:35.920244 7fe6d37fe6c0 Manual compaction at level-0 from '!tables!PPsxQgHwLCQ2gjSW' @ 72057594037927935 : 1 .. '!tables.results!wJZXUo4q5b5vE3Dy.zFTPLMc9zOl5hISV' @ 0 : 0; will stop at (end)
+7 -7
View File
@@ -1,7 +1,7 @@
2026/05/01-00:45:47.940542 7f3f35bff6c0 Recovering log #42 2026/05/09-23:24:27.266509 7fe7211fe6c0 Recovering log #58
2026/05/01-00:45:47.951337 7f3f35bff6c0 Delete type=3 #40 2026/05/09-23:24:27.282592 7fe7211fe6c0 Delete type=3 #56
2026/05/01-00:45:47.951402 7f3f35bff6c0 Delete type=0 #42 2026/05/09-23:24:27.282660 7fe7211fe6c0 Delete type=0 #58
2026/05/01-00:49:08.810118 7f3ee77fe6c0 Level-0 table #47: started 2026/05/09-23:25:01.864644 7fe6d37fe6c0 Level-0 table #63: started
2026/05/01-00:49:08.810197 7f3ee77fe6c0 Level-0 table #47: 0 bytes OK 2026/05/09-23:25:01.864678 7fe6d37fe6c0 Level-0 table #63: 0 bytes OK
2026/05/01-00:49:08.816466 7f3ee77fe6c0 Delete type=0 #45 2026/05/09-23:25:01.871922 7fe6d37fe6c0 Delete type=0 #61
2026/05/01-00:49:08.830594 7f3ee77fe6c0 Manual compaction at level-0 from '!tables!PPsxQgHwLCQ2gjSW' @ 72057594037927935 : 1 .. '!tables.results!wJZXUo4q5b5vE3Dy.zFTPLMc9zOl5hISV' @ 0 : 0; will stop at (end) 2026/05/09-23:25:01.877952 7fe6d37fe6c0 Manual compaction at level-0 from '!tables!PPsxQgHwLCQ2gjSW' @ 72057594037927935 : 1 .. '!tables.results!wJZXUo4q5b5vE3Dy.zFTPLMc9zOl5hISV' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000030 MANIFEST-000002
+5 -7
View File
@@ -1,7 +1,5 @@
2026/05/01-09:12:26.379797 7f3f353fe6c0 Recovering log #28 2026/05/09-23:29:14.636398 7f54217ed6c0 Delete type=3 #1
2026/05/01-09:12:26.395236 7f3f353fe6c0 Delete type=3 #26 2026/05/09-23:29:14.638741 7f5403fff6c0 Level-0 table #5: started
2026/05/01-09:12:26.395290 7f3f353fe6c0 Delete type=0 #28 2026/05/09-23:29:14.666649 7f5403fff6c0 Level-0 table #5: 3056 bytes OK
2026/05/01-09:12:52.786838 7f3ee77fe6c0 Level-0 table #33: started 2026/05/09-23:29:14.731617 7f5403fff6c0 Delete type=0 #3
2026/05/01-09:12:52.786863 7f3ee77fe6c0 Level-0 table #33: 0 bytes OK 2026/05/09-23:29:14.731832 7f5403fff6c0 Manual compaction at level-0 from '!journal!69Da9YvF9BfOV7oK' @ 72057594037927935 : 1 .. '!journal.pages!69Da9YvF9BfOV7oK.XM0eLkgKXPyskV65' @ 0 : 0; will stop at (end)
2026/05/01-09:12:52.798150 7f3ee77fe6c0 Delete type=0 #31
2026/05/01-09:12:52.798293 7f3ee77fe6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
-5
View File
@@ -1,5 +0,0 @@
2026/05/01-00:45:48.110687 7f3f35bff6c0 Delete type=3 #1
2026/05/01-00:49:08.823197 7f3ee77fe6c0 Level-0 table #29: started
2026/05/01-00:49:08.823234 7f3ee77fe6c0 Level-0 table #29: 0 bytes OK
2026/05/01-00:49:08.830411 7f3ee77fe6c0 Delete type=0 #27
2026/05/01-00:49:08.830628 7f3ee77fe6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
Binary file not shown.
View File
+134 -5
View File
@@ -121,11 +121,16 @@
.dnc-employe-sheet .sheet-header.compact label { .dnc-employe-sheet .sheet-header.compact label {
gap: 0.25rem; gap: 0.25rem;
} }
.dnc-employe-sheet .sheet-header.compact .hp-field {
grid-column: span 2;
}
.dnc-employe-sheet .sheet-header.compact input[type="number"] { .dnc-employe-sheet .sheet-header.compact input[type="number"] {
max-width: 4.75rem; max-width: 4.75rem;
} }
.dnc-employe-sheet .sheet-header.compact .counter-field input[type="number"] { .dnc-employe-sheet .sheet-header.compact .counter-field input[name="system.sante.pv.value"],
max-width: 4rem; .dnc-employe-sheet .sheet-header.compact .counter-field input[name="system.sante.pv.max"] {
width: 5.25rem;
max-width: 5.25rem;
} }
.dnc-pnj-sheet .sheet-header.compact .identity-grid { .dnc-pnj-sheet .sheet-header.compact .identity-grid {
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -134,13 +139,17 @@
.dnc-pnj-sheet .sheet-header.compact label { .dnc-pnj-sheet .sheet-header.compact label {
gap: 0.25rem; gap: 0.25rem;
} }
.dnc-pnj-sheet .sheet-header.compact .hp-field {
grid-column: span 2;
}
.dnc-pnj-sheet .sheet-header.compact input[type="number"] { .dnc-pnj-sheet .sheet-header.compact input[type="number"] {
width: 4.5rem; width: 4.5rem;
max-width: 4.5rem; max-width: 4.5rem;
} }
.dnc-pnj-sheet .sheet-header.compact .counter-field input[type="number"] { .dnc-pnj-sheet .sheet-header.compact .counter-field input[name="system.sante.pv.value"],
width: 3.75rem; .dnc-pnj-sheet .sheet-header.compact .counter-field input[name="system.sante.pv.max"] {
max-width: 3.75rem; width: 5.25rem;
max-width: 5.25rem;
} }
.dnc-sheet label { .dnc-sheet label {
display: flex; display: flex;
@@ -556,6 +565,126 @@
.dnc-roll-dialog .window-content { .dnc-roll-dialog .window-content {
background: linear-gradient(180deg, #f7efe0 0%, #e3d0b1 100%); background: linear-gradient(180deg, #f7efe0 0%, #e3d0b1 100%);
} }
.dnc-mission-pack-mode,
.dnc-mission-pack-note {
font-size: 0.9rem;
}
.dnc-mission-pack-campaign {
gap: 1rem;
}
.dnc-mission-pack-hero {
padding: 1rem;
border: 1px solid rgba(91, 70, 52, 0.35);
border-radius: 10px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.65) 0%, rgba(241, 229, 208, 0.68) 100%), linear-gradient(135deg, rgba(139, 46, 23, 0.08) 0%, rgba(139, 46, 23, 0) 100%);
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.1);
}
.dnc-mission-pack-kicker {
margin: 0 0 0.25rem;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: rgba(139, 46, 23, 0.82);
}
.dnc-mission-pack-hero h2 {
margin: 0;
font-family: "IM Fell English SC", "Palatino Linotype", "Book Antiqua", Palatino, serif;
font-size: 1.4rem;
line-height: 1.1;
color: #8b2e17;
}
.dnc-mission-pack-subtitle {
margin-top: 0.2rem;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
color: rgba(91, 70, 52, 0.78);
}
.dnc-mission-pack-intro {
margin-top: 0.4rem;
color: #6d5a4f;
}
.dnc-mission-pack-meta-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.75rem;
}
.dnc-mission-pack-meta-card {
display: grid;
gap: 0.2rem;
padding: 0.75rem;
border: 1px solid rgba(91, 70, 52, 0.3);
border-radius: 10px;
background: rgba(255, 255, 255, 0.42);
}
.dnc-mission-pack-meta-card span {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #6d5a4f;
}
.dnc-mission-pack-meta-card strong {
font-size: 1rem;
}
.dnc-mission-pack-section {
display: grid;
gap: 0.75rem;
}
.dnc-mission-pack-select {
padding: 0.75rem;
border: 1px solid rgba(91, 70, 52, 0.32);
border-radius: 10px;
background: rgba(255, 255, 255, 0.36);
}
.dnc-mission-pack-assignments {
display: grid;
gap: 0.75rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.dnc-mission-pack-assignment {
display: grid;
gap: 0.4rem;
padding: 0.75rem;
border: 1px solid rgba(91, 70, 52, 0.35);
border-radius: 10px;
background: rgba(255, 255, 255, 0.38);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55);
}
.dnc-mission-pack-assignment span {
font-size: 0.82rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.dnc-mission-pack-note {
color: #6d5a4f;
}
.dnc-mission-pack-note-foot {
padding: 0.75rem;
border-top: 1px solid rgba(91, 70, 52, 0.24);
}
.dnc-mission-pack-dialog .window-header {
background: linear-gradient(180deg, rgba(253, 246, 231, 0.96) 0%, rgba(234, 212, 170, 0.96) 100%), linear-gradient(90deg, rgba(139, 46, 23, 0.12) 0%, rgba(139, 46, 23, 0) 100%);
border-bottom: 1px solid rgba(91, 70, 52, 0.35);
}
.dnc-mission-pack-dialog .window-title {
color: #8b2e17;
font-family: "IM Fell English SC", "Palatino Linotype", "Book Antiqua", Palatino, serif;
letter-spacing: 0.03em;
text-shadow: none;
}
.dnc-mission-pack-dialog .window-header button {
color: #221b18;
}
@media (max-width: 640px) {
.dnc-mission-pack-meta-grid,
.dnc-mission-pack-assignments {
grid-template-columns: 1fr;
}
}
.dnc-chat-card { .dnc-chat-card {
position: relative; position: relative;
border: 2px solid #5b4634; border: 2px solid #5b4634;
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
<button type="button" data-action="rollHitDice" aria-label="Lancer le de de vie" title="Lancer le de de vie"><i class="fa-solid fa-dice-d20"></i></button> <button type="button" data-action="rollHitDice" aria-label="Lancer le de de vie" title="Lancer le de de vie"><i class="fa-solid fa-dice-d20"></i></button>
</div> </div>
</label> </label>
<label class="with-controls"> <label class="with-controls hp-field" style="grid-column: span 2;">
<span>PV</span> <span>PV</span>
<div class="counter-field"> <div class="counter-field">
<button type="button" data-action="adjustCounter" data-path="system.sante.pv.value" data-delta="-1">-</button> <button type="button" data-action="adjustCounter" data-path="system.sante.pv.value" data-delta="-1">-</button>
+1 -1
View File
@@ -32,7 +32,7 @@
<button type="button" data-action="rollHitDice" aria-label="Lancer le de de vie" title="Lancer le de de vie"><i class="fa-solid fa-dice-d20"></i></button> <button type="button" data-action="rollHitDice" aria-label="Lancer le de de vie" title="Lancer le de de vie"><i class="fa-solid fa-dice-d20"></i></button>
</div> </div>
</label> </label>
<label class="with-controls"> <label class="with-controls hp-field" style="grid-column: span 2;">
<span>PV</span> <span>PV</span>
<div class="counter-field"> <div class="counter-field">
<button type="button" data-action="adjustCounter" data-path="system.sante.pv.value" data-delta="-1">-</button> <button type="button" data-action="adjustCounter" data-path="system.sante.pv.value" data-delta="-1">-</button>
+28
View File
@@ -11,6 +11,9 @@
</header> </header>
<div class="chat-pill-row"> <div class="chat-pill-row">
<span class="chat-pill">{{localize "DNC.Macro.MissionPack.ActiveMode"}} {{generationModeLabel}}</span>
{{#if isCampaign}}<span class="chat-pill">{{localize "DNC.Macro.MissionPack.CampaignController"}} {{controllerName}}</span>{{/if}}
{{#if isCampaign}}<span class="chat-pill">{{localize "DNC.Macro.MissionPack.CampaignRelation"}} {{stewardRelationLabel}}</span>{{/if}}
<span class="chat-pill success">{{localize "DNC.Macro.MissionPack.ItemsAdded"}} {{createdCount}}</span> <span class="chat-pill success">{{localize "DNC.Macro.MissionPack.ItemsAdded"}} {{createdCount}}</span>
{{#if missingCount}}<span class="chat-pill failure">{{localize "DNC.Macro.MissionPack.ItemsMissing"}} {{missingCount}}</span>{{/if}} {{#if missingCount}}<span class="chat-pill failure">{{localize "DNC.Macro.MissionPack.ItemsMissing"}} {{missingCount}}</span>{{/if}}
</div> </div>
@@ -27,8 +30,33 @@
{{#each draws}} {{#each draws}}
<li> <li>
<strong>{{this.label}}</strong> : {{this.display}} <strong>{{this.label}}</strong> : {{this.display}}
{{#if this.dieLabel}}
<br>{{localize "DNC.Macro.MissionPack.RollDetail" die=this.dieLabel mode=this.modeLabel values=this.rollValuesLabel kept=this.kept rank=this.rankBonus total=this.total}}
{{#if this.clamped}}<br>{{localize "DNC.Macro.MissionPack.TotalClamped" total=this.total clamped=this.resolvedTotal}}{{/if}}
{{/if}}
{{#if this.missingSummary}}<br>{{localize "DNC.Macro.MissionPack.Missing"}} : {{this.missingSummary}}{{/if}} {{#if this.missingSummary}}<br>{{localize "DNC.Macro.MissionPack.Missing"}} : {{this.missingSummary}}{{/if}}
</li> </li>
{{/each}} {{/each}}
</ul> </ul>
<div class="chat-pill-row">
<span class="chat-pill">{{localize "DNC.Macro.MissionPack.UniqueReference"}} {{uniqueEntry.referenceRoll}}</span>
<span class="chat-pill">{{localize "DNC.Macro.MissionPack.UniqueActorRoll"}} {{uniqueEntry.actorRoll}}</span>
<span class="chat-pill {{#if uniqueEntry.matched}}success{{else}}failure{{/if}}">
{{#if uniqueEntry.matched}}
{{localize "DNC.Macro.MissionPack.UniqueMatch"}}
{{else}}
{{localize "DNC.Macro.MissionPack.UniqueMiss"}}
{{/if}}
</span>
</div>
{{#if uniqueEntry.matched}}
<p class="chat-note">
<strong>{{localize "DNC.Macro.MissionPack.UniqueGranted"}}</strong> {{uniqueEntry.itemName}}
{{#if uniqueEntry.uniqueRoll}}({{localize "DNC.Macro.MissionPack.UniqueTableRoll"}} {{uniqueEntry.uniqueRoll}}){{/if}}
</p>
{{else}}
<p class="chat-note">{{localize "DNC.Macro.MissionPack.UniqueRuleReminder"}}</p>
{{/if}}
</section> </section>
@@ -0,0 +1,54 @@
<div class="dnc-dialog-form dnc-mission-pack-campaign">
<header class="dnc-mission-pack-hero">
<p class="dnc-mission-pack-kicker">{{localize "DNC.Chat.Kicker.Logistics"}}</p>
<h2>{{localize "DNC.Macro.MissionPack.CampaignDialogTitle"}}</h2>
<p class="dnc-mission-pack-subtitle">{{localize "DNC.Macro.MissionPack.CampaignDialogSubtitle"}}</p>
<p class="dnc-mission-pack-intro">{{localize "DNC.Macro.MissionPack.CampaignDialogIntro" actor=actorName}}</p>
</header>
<section class="dnc-mission-pack-meta-grid">
<article class="dnc-mission-pack-meta-card">
<span>{{localize "DNC.Macro.MissionPack.DialogActor"}}</span>
<strong>{{actorName}}</strong>
</article>
<article class="dnc-mission-pack-meta-card">
<span>{{localize "DNC.Macro.MissionPack.CampaignDialogPlayer"}}</span>
<strong>{{playerName}}</strong>
</article>
{{#if requesterName}}
<article class="dnc-mission-pack-meta-card">
<span>{{localize "DNC.Macro.MissionPack.CampaignDialogRequester"}}</span>
<strong>{{requesterName}}</strong>
</article>
{{/if}}
<article class="dnc-mission-pack-meta-card">
<span>{{localize "DNC.Macro.MissionPack.CampaignDialogRank"}}</span>
<strong>+{{rank}}</strong>
</article>
</section>
<section class="dnc-mission-pack-section">
<label class="dnc-mission-pack-select">
<span>{{localize "DNC.Macro.MissionPack.CampaignDialogRelation"}}</span>
<select name="stewardRelation">
{{selectOptions relationOptions selected=selectedRelation labelAttr="label" valueAttr="value" localize=false}}
</select>
</label>
</section>
<section class="dnc-mission-pack-section">
<p class="dnc-mission-pack-note">{{localize "DNC.Macro.MissionPack.CampaignDialogAssignHelp"}}</p>
<div class="dnc-mission-pack-assignments">
{{#each assignments}}
<label class="dnc-mission-pack-assignment">
<span>{{this.label}}</span>
<select name="{{this.fieldName}}">
{{selectOptions ../diceOptions selected=this.selectedDie labelAttr="label" valueAttr="value" localize=false}}
</select>
</label>
{{/each}}
</div>
</section>
<p class="dnc-mission-pack-note dnc-mission-pack-note-foot">{{localize "DNC.Macro.MissionPack.CampaignDialogHelp"}}</p>
</div>
@@ -1,9 +1,14 @@
<div class="dnc-dialog-form"> <div class="dnc-dialog-form">
<p>{{localize "DNC.Macro.MissionPack.DialogIntro"}}</p> <p>{{localize "DNC.Macro.MissionPack.DialogIntro"}}</p>
<p class="dnc-mission-pack-mode"><strong>{{localize "DNC.Macro.MissionPack.ActiveMode"}}</strong> {{modeLabel}}</p>
<p>{{modeDescription}}</p>
<label> <label>
<span>{{localize "DNC.Macro.MissionPack.DialogActor"}}</span> <span>{{localize "DNC.Macro.MissionPack.DialogActor"}}</span>
<select name="actorId"> <select name="actorId">
{{selectOptions actorOptions selected=selectedActorId labelAttr="label" valueAttr="value" localize=false}} {{selectOptions actorOptions selected=selectedActorId labelAttr="label" valueAttr="value" localize=false}}
</select> </select>
</label> </label>
{{#if isCampaign}}
<p class="dnc-mission-pack-note">{{localize "DNC.Macro.MissionPack.CampaignDialogLead"}}</p>
{{/if}}
</div> </div>