2 Commits
Author SHA1 Message Date
uberwald b0a3aff648 feat: gestion des Dés de Totems (influence, instincts, interdits)
Release Creation / build (release) Failing after 1m32s
- VermineTotemDice : influence Humain/Adapté (+1D/-1D par domaine, règles
  p. 121), gains/pertes via Instincts/Interdits avec limites (3D/totem,
  5D total) et règles d'échange.
- roll.mjs applique l'influence selon le Totem dominant (au lieu de
  l'ancienne logique basée sur identity.totem).
- Dialogue TotemDiceDialog (instinct Humain/autre totem, interdit Humain,
  acte grave ±2D) + bouton et ligne d'influence sur la fiche personnage.
- Dés de Totem gagnés en cas d'échec à l'apprentissage Dé d'Évolution
  (dés dépensés, garde capacité vide).
- Couleur de texte @color-text-light-2 assombrie (#c9e0c0 → #3f9b55)
  pour la lisibilité dans tous les dialogues.

fix: robustesse et visibilité des évolutions

- buyEvolutionDialog : n'efface les Dés d'Évolution que si l'item est créé.
- Flag mutation éditable sur la fiche item evolution + badge
  Adaptation/Mutation dans la liste du personnage.
- loseDie : applique aussi la limite totale de 5 dés au gain Adapté.
- Case "acte grave" conservée entre les rendus du dialogue.
2026-08-06 15:32:55 +02:00
uberwald 3cb96410b9 fix: réserve de Groupe calculée selon les membres (3D + 1D/personnage)
- _calculateGroupReserve utilisait une règle simplifiée (niveau × 2) au
  lieu du nombre de personnages membres. Désormais max = 3 + membres
  (personnages uniquement), plafonné à 10D, conforme aux règles p. 68-69.
- Un Groupe nouvellement créé démarre avec sa Réserve à son maximum.
2026-08-06 09:37:52 +02:00
25 changed files with 899 additions and 104 deletions
+35 -1
View File
@@ -304,7 +304,20 @@
"die_successes": "successes", "die_successes": "successes",
"total": "Total", "total": "Total",
"totem_dice": "Totem Dice", "totem_dice": "Totem Dice",
"totem_hint": "Totem Hint", "totem_hint": "Check to use totem dice (double success possible)",
"totem_neutral": "Neutral",
"totem_manage": "Manage Totem Dice (Instincts / Interdits)",
"totem_serious": "Serious act (2 dice)",
"totem_manage_hint": "Instincts: obeying the Human's Instincts gains 1 Human die; another Totem's Instincts gain 1 Adapted die. Interdit: breaking a Human Interdit loses 1 Human die (or gains 1 Adapted die if none). Max 3 dice of the same Totem and 5 dice total.",
"totem_instinct_human": "Human Instinct (+1 Human die)",
"totem_instinct_adapted": "Another Totem's Instinct (+1 Adapted die)",
"totem_interdit_human": "Human Interdit (-1 Human die)",
"totem_gained": "{totem} +1D",
"totem_gained_adapted": "+1D Adapted",
"totem_exchanged": "{totem} gained in exchange (1 opposite die removed)",
"totem_replaced": "Opposite die removed instead (max 3D reached)",
"totem_lost": "-1 Human die",
"totem_limit_reached": "Totem dice limit reached.",
"FightTool": "Fight Tool", "FightTool": "Fight Tool",
"RollTool": "Roll Tool", "RollTool": "Roll Tool",
"Roll4Fight": "Roll for Fight", "Roll4Fight": "Roll for Fight",
@@ -374,6 +387,27 @@
"trauma_reward": "Chosen trauma", "trauma_reward": "Chosen trauma",
"trauma_reward_5": "Trauma reward (+5D)", "trauma_reward_5": "Trauma reward (+5D)",
"evolution_dice": "Evolution dice", "evolution_dice": "Evolution dice",
"buy_evolution": "Buy an Adaptation/Mutation",
"evolution_name": "Name",
"evolution_level": "Level (cost in Evolution dice)",
"evolution_mutation": "Mutation",
"evolution_adaptation": "Adaptation",
"evolution_available_dice": "Accumulated Evolution dice",
"evolution_total": "Total levels (after purchase)",
"evolution_mode_max": "Game mode maximum",
"game_mode": "Game mode",
"mode_survie": "Survival",
"mode_cauchemar": "Nightmare",
"mode_apocalypse": "Apocalypse",
"evolution_buy_hint": "The purchase consumes and erases all accumulated Evolution dice. Limits: Survival 5D, Nightmare 6D, Apocalypse 7D total. Level 3/4 Mutations are forbidden in Survival; only one level 4 Mutation.",
"buy_evolution_done": "Adaptation/Mutation acquired. Evolution dice erased.",
"error_evolution_no_name": "Provide a name for the Adaptation/Mutation.",
"error_evolution_invalid_level": "Invalid level (1 to 4).",
"error_evolution_not_enough_dice": "Not enough Evolution dice for this level.",
"error_evolution_mode_limit": "Game mode maximum Evolution dice reached.",
"error_evolution_mutation_mode": "Level 3/4 Mutations require Nightmare or Apocalypse modes.",
"error_evolution_mutation_4d": "The character already has a level 4 Mutation.",
"error_evolution_create": "Could not create the Adaptation/Mutation.",
"open_experience_phase": "Experience phase", "open_experience_phase": "Experience phase",
"open_learning": "Learning", "open_learning": "Learning",
"validate_phase": "Validate phase", "validate_phase": "Validate phase",
+34
View File
@@ -302,6 +302,19 @@
"totem_dice": "Dés de totem", "totem_dice": "Dés de totem",
"keep_totem": "Garder le totem", "keep_totem": "Garder le totem",
"totem_hint": "Cochez pour utiliser les dés de totem (double réussite possible)", "totem_hint": "Cochez pour utiliser les dés de totem (double réussite possible)",
"totem_neutral": "Neutre",
"totem_manage": "Gérer les Dés de Totems (Instincts / Interdits)",
"totem_serious": "Acte grave (2 dés)",
"totem_manage_hint": "Instincts : obéir aux Instincts de l'Humain gagne 1D Humain ; ceux d'un autre Totem gagnent 1D Adapté. Interdit : briser un Interdit de l'Humain fait perdre 1D Humain (ou gagner 1D Adapté si aucun). Max 3 Dés du même Totem et 5 Dés au total.",
"totem_instinct_human": "Instinct de l'Humain (+1D Humain)",
"totem_instinct_adapted": "Instinct d'un autre Totem (+1D Adapté)",
"totem_interdit_human": "Interdit de l'Humain (-1D Humain)",
"totem_gained": "{totem} +1D",
"totem_gained_adapted": "+1D Adapté",
"totem_exchanged": "{totem} gagné en échange (1D opposé retiré)",
"totem_replaced": "Dé opposé retiré à la place (max 3D atteint)",
"totem_lost": "-1D Humain",
"totem_limit_reached": "Limite de Dés de Totems atteinte.",
"attack": "Attaque", "attack": "Attaque",
"attack_contact": "Attaque au contact", "attack_contact": "Attaque au contact",
"attack_ranged": "Attaque à distance", "attack_ranged": "Attaque à distance",
@@ -357,6 +370,27 @@
"trauma_reward": "Traumatisme choisi", "trauma_reward": "Traumatisme choisi",
"trauma_reward_5": "Récompense de Traumatisme (+5D)", "trauma_reward_5": "Récompense de Traumatisme (+5D)",
"evolution_dice": "Dés d'Évolution", "evolution_dice": "Dés d'Évolution",
"buy_evolution": "Acheter une Adaptation/Mutation",
"evolution_name": "Nom",
"evolution_level": "Niveau (coût en Dés d'Évolution)",
"evolution_mutation": "Mutation",
"evolution_adaptation": "Adaptation",
"evolution_available_dice": "Dés d'Évolution accumulés",
"evolution_total": "Total des niveaux (après achat)",
"evolution_mode_max": "Maximum du Mode de jeu",
"game_mode": "Mode de jeu",
"mode_survie": "Survie",
"mode_cauchemar": "Cauchemar",
"mode_apocalypse": "Apocalypse",
"evolution_buy_hint": "L'achat consomme et efface tous les Dés d'Évolution accumulés. Limites : Survie 5D, Cauchemar 6D, Apocalypse 7D au total. Mutations 3D/4D interdites en Mode Survie ; une seule Mutation à 4D.",
"buy_evolution_done": "Adaptation/Mutation acquise. Dés d'Évolution effacés.",
"error_evolution_no_name": "Indiquez un nom pour l'Adaptation/Mutation.",
"error_evolution_invalid_level": "Niveau invalide (1 à 4).",
"error_evolution_not_enough_dice": "Dés d'Évolution insuffisants pour ce niveau.",
"error_evolution_mode_limit": "Maximum de Dés d'Évolution du Mode de jeu atteint.",
"error_evolution_mutation_mode": "Les Mutations de Niveau 3/4 nécessitent les Modes Cauchemar ou Apocalypse.",
"error_evolution_mutation_4d": "Le personnage possède déjà une Mutation à 4D.",
"error_evolution_create": "Impossible de créer l'Adaptation/Mutation.",
"open_experience_phase": "Phase d'Expérience", "open_experience_phase": "Phase d'Expérience",
"open_learning": "Apprentissage", "open_learning": "Apprentissage",
"validate_phase": "Valider la phase", "validate_phase": "Valider la phase",
+23 -2
View File
@@ -696,6 +696,27 @@
&:last-child { border-bottom: none; } &:last-child { border-bottom: none; }
} }
.evolution-badge {
margin-left: 6px;
padding: 1px 6px;
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
text-transform: uppercase;
border-radius: 3px;
&.adaptation {
color: @color-text-light-0;
background: fade(@color-acid-green, 25%);
border: 1px solid @color-acid-green;
}
&.mutation {
color: @color-text-light-0;
background: fade(@color-hemolymph, 30%);
border: 1px solid @color-hemolymph;
}
}
} }
// ── V2 tab navigation — chitin-plate style ───────────────────────── // ── V2 tab navigation — chitin-plate style ─────────────────────────
@@ -706,7 +727,7 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
background: @color-chitin-dark; background: @color-chitin-dark;
border-bottom: 2px solid @color-amber; border-bottom: 2px solid @color-hemolymph;
min-height: 36px; min-height: 36px;
> a { > a {
@@ -730,7 +751,7 @@
} }
&.active { &.active {
background: linear-gradient(180deg, @color-amber 0%, @color-honeycomb 100%); background: @color-honeycomb;
color: @color-chitin-dark; color: @color-chitin-dark;
&::after { &::after {
+4 -4
View File
@@ -69,7 +69,7 @@
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
text-transform: uppercase; text-transform: uppercase;
font-size: @font-size-11; font-size: @font-size-11;
color: @color-amber; color: @color-text-light-2;
} }
} }
@@ -111,7 +111,7 @@
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
text-transform: uppercase; text-transform: uppercase;
font-size: @font-size-11; font-size: @font-size-11;
color: @color-amber; color: @color-text-light-2;
} }
input[type="number"] { input[type="number"] {
@@ -134,7 +134,7 @@
} }
&.crisis { border-left-color: @color-hemolymph; .gauge-state { color: @color-hemolymph; } } &.crisis { border-left-color: @color-hemolymph; .gauge-state { color: @color-hemolymph; } }
&.low { border-left-color: @color-amber; .gauge-state { color: @color-amber; } } &.low { border-left-color: @color-honeycomb; .gauge-state { color: @color-honeycomb; } }
&.stable { border-left-color: @color-acid-green; .gauge-state { color: @color-acid-green; } } &.stable { border-left-color: @color-acid-green; .gauge-state { color: @color-acid-green; } }
&.opulent { border-left-color: @theme-color-accent; .gauge-state { color: @theme-color-accent; } } &.opulent { border-left-color: @theme-color-accent; .gauge-state { color: @theme-color-accent; } }
} }
@@ -146,7 +146,7 @@
&.high { color: @color-acid-green; } &.high { color: @color-acid-green; }
&.normal { color: @color-text-light-0; } &.normal { color: @color-text-light-0; }
&.low { color: @color-amber; } &.low { color: @color-honeycomb; }
&.crisis { color: @color-hemolymph; } &.crisis { color: @color-hemolymph; }
} }
} }
+1 -1
View File
@@ -188,7 +188,7 @@
.capacity-type { .capacity-type {
flex: 0 0 auto; flex: 0 0 auto;
font-size: @font-size-12; font-size: @font-size-12;
color: @color-amber; color: @color-text-light-2;
text-transform: uppercase; text-transform: uppercase;
} }
+32
View File
@@ -124,5 +124,37 @@
} }
} }
} }
.totem-influence-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
margin-top: 2px;
font-size: 0.75rem;
.totem-influence-label {
color: @color-text-light-0;
strong.totem-human { color: @totem-human-color; }
strong.totem-adapted { color: @totem-adapted-color; }
}
.totem-manage-btn {
flex: none;
padding: 2px 8px;
cursor: pointer;
color: @color-text-light-0;
background: fade(@color-hemolymph, 20%);
border: 1px solid @color-hemolymph;
border-radius: 3px;
.transition();
&:hover {
color: @theme-color-dark;
background: @color-hemolymph;
}
}
}
} }
} }
+2 -2
View File
@@ -124,7 +124,7 @@
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
text-transform: uppercase; text-transform: uppercase;
font-size: @font-size-11; font-size: @font-size-11;
color: @color-amber; color: @color-text-light-2;
} }
} }
@@ -171,7 +171,7 @@
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
text-transform: uppercase; text-transform: uppercase;
font-size: @font-size-11; font-size: @font-size-11;
color: @color-amber; color: @color-text-light-2;
} }
} }
} }
+8 -8
View File
@@ -732,7 +732,7 @@ ol.chat-log {
} }
#required { #required {
color: @color-amber; color: @color-text-light-2;
} }
} }
@@ -894,7 +894,7 @@ ol.chat-log {
.exchange-passive { .exchange-passive {
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
font-size: @font-size-11; font-size: @font-size-11;
color: @color-amber; color: @color-text-light-2;
opacity: 0.85; opacity: 0.85;
font-style: italic; font-style: italic;
} }
@@ -923,8 +923,8 @@ ol.chat-log {
div.exchange-resolution { div.exchange-resolution {
margin: 4px 0 6px; margin: 4px 0 6px;
padding: 6px 8px; padding: 6px 8px;
border: 1px solid @color-amber; border: 1px solid @color-hemolymph;
background: fade(@color-amber, 8%); background: fade(@color-hemolymph, 8%);
.resolution-header { .resolution-header {
display: flex; display: flex;
@@ -937,7 +937,7 @@ ol.chat-log {
font-weight: 900; font-weight: 900;
color: @theme-color-dark; color: @theme-color-dark;
i { color: @color-amber; } i { color: @color-hemolymph; }
} }
.resolution-line { .resolution-line {
@@ -1063,7 +1063,7 @@ ol.chat-log {
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
text-transform: uppercase; text-transform: uppercase;
font-size: @font-size-11; font-size: @font-size-11;
color: @color-amber; color: @color-text-light-2;
} }
.exp-value { .exp-value {
@@ -1111,7 +1111,7 @@ ol.chat-log {
margin: 0 0 6px; margin: 0 0 6px;
.exp-total { .exp-total {
color: @color-amber; color: @color-text-light-2;
} }
} }
@@ -1126,7 +1126,7 @@ ol.chat-log {
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
text-transform: uppercase; text-transform: uppercase;
font-size: @font-size-11; font-size: @font-size-11;
color: @color-amber; color: @color-text-light-2;
} }
} }
+99 -10
View File
@@ -188,7 +188,7 @@ iframe {
> label { > label {
font-size: @font-size-13; font-size: @font-size-13;
color: @color-amber; color: @color-text-light-2;
white-space: nowrap; white-space: nowrap;
} }
@@ -294,7 +294,7 @@ iframe {
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: @color-amber; color: @color-text-light-highlight;
} }
} }
@@ -347,7 +347,7 @@ iframe {
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: @color-amber; color: @color-text-light-highlight;
} }
} }
} }
@@ -381,7 +381,7 @@ iframe {
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: @color-amber; color: @color-text-light-highlight;
} }
small { small {
@@ -487,7 +487,7 @@ iframe {
.label { .label {
font-size: @font-size-11; font-size: @font-size-11;
color: @color-amber; color: @color-text-light-2;
} }
.combat-target-tag { .combat-target-tag {
@@ -527,7 +527,7 @@ iframe {
.label { .label {
font-size: @font-size-12; font-size: @font-size-12;
color: @color-amber; color: @color-text-light-2;
} }
.exp-value { .exp-value {
@@ -555,7 +555,7 @@ iframe {
input[type="radio"] { flex: none; } input[type="radio"] { flex: none; }
&:hover { color: @color-amber; } &:hover { color: @color-text-light-highlight; }
} }
} }
@@ -581,8 +581,8 @@ iframe {
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px 16px; gap: 8px 16px;
padding: 8px; padding: 8px;
border: 1px solid @color-amber; border: 1px solid @color-hemolymph;
background: fade(@color-amber, 8%); background: fade(@color-hemolymph, 8%);
.context-item { .context-item {
display: flex; display: flex;
@@ -593,7 +593,7 @@ iframe {
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
text-transform: uppercase; text-transform: uppercase;
font-size: @font-size-11; font-size: @font-size-11;
color: @color-amber; color: @color-text-light-2;
} }
.context-value { .context-value {
@@ -721,3 +721,92 @@ iframe {
} }
} }
} }
// ── Achat d'Adaptation/Mutation ───────────────────────────────────────
.application.buy-evolution .bonus-item {
align-items: center;
gap: 8px;
> label.label {
flex: none;
font-size: @font-size-12;
color: @color-text-light-2;
white-space: nowrap;
}
input[type="text"] {
flex: 1 1 auto;
min-width: 0;
}
select {
flex: 1 1 auto;
min-width: 0;
width: auto;
}
input[type="checkbox"] {
flex: none;
width: 18px;
height: 18px;
}
}
// ── Gestion des Dés de Totems ─────────────────────────────────────────
.application.totem-dice .totem-dice-state {
gap: 12px;
padding: 6px 8px;
border: 1px solid @color-hemolymph;
background: fade(@color-hemolymph, 8%);
.totem-state {
display: flex;
align-items: center;
gap: 6px;
.label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-12;
color: @color-text-light-2;
white-space: nowrap;
}
&.totem-human .label { color: @color-text-light-2; }
&.totem-adapted .label { color: @color-text-light-2; }
.value {
font-family: "Roboto", sans-serif;
font-size: @font-size-14;
font-weight: 700;
color: @color-text-light-0;
}
}
}
.application.totem-dice .totem-influence {
font-size: @font-size-16;
}
.application.totem-dice .totem-actions {
gap: 8px;
button {
flex: 1 1 100%;
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-12;
padding: 6px 10px;
cursor: pointer;
color: @color-text-light-0;
background: fade(@color-hemolymph, 15%);
border: 1px solid @color-hemolymph;
border-radius: 3px;
.transition();
&:hover {
color: @theme-color-dark;
background: @color-hemolymph;
}
}
}
+1 -2
View File
@@ -20,7 +20,7 @@
// Niveaux de gris pour le texte clair // Niveaux de gris pour le texte clair
@color-text-light-0: #fff; @color-text-light-0: #fff;
@color-text-light-1: #e0f0f0; @color-text-light-1: #e0f0f0;
@color-text-light-2: #c9e0c0; @color-text-light-2: #3f9b55;
@color-text-light-3: #90c4a4; @color-text-light-3: #90c4a4;
@color-text-light-4: #80c08b; @color-text-light-4: #80c08b;
@color-text-light-5: #60b06b; @color-text-light-5: #60b06b;
@@ -139,7 +139,6 @@
// Couleurs insecte / organique // Couleurs insecte / organique
@color-acid-green: #1e7d32; @color-acid-green: #1e7d32;
@color-amber: #e8b84b;
@color-chitin-dark: #2a2520; @color-chitin-dark: #2a2520;
@color-hemolymph: #8b4513; @color-hemolymph: #8b4513;
@color-membrane: rgba(106, 176, 76, 0.1); @color-membrane: rgba(106, 176, 76, 0.1);
+17 -1
View File
@@ -10,7 +10,9 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
addSpecialty: VermineCharacterSheetV2.#onAddSpecialty, addSpecialty: VermineCharacterSheetV2.#onAddSpecialty,
openExperiencePhase: VermineCharacterSheetV2.#onOpenExperiencePhase, openExperiencePhase: VermineCharacterSheetV2.#onOpenExperiencePhase,
openLearning: VermineCharacterSheetV2.#onOpenLearning, openLearning: VermineCharacterSheetV2.#onOpenLearning,
validatePhase: VermineCharacterSheetV2.#onValidatePhase validatePhase: VermineCharacterSheetV2.#onValidatePhase,
buyEvolution: VermineCharacterSheetV2.#onBuyEvolution,
openTotemDice: VermineCharacterSheetV2.#onOpenTotemDice
} }
} }
@@ -133,4 +135,18 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
await VermineExperience.resetPhaseLimits(this.document) await VermineExperience.resetPhaseLimits(this.document)
ui.notifications.info(game.i18n.localize("VERMINE.validate_phase_done")) ui.notifications.info(game.i18n.localize("VERMINE.validate_phase_done"))
} }
/** Ouvre le dialogue d'achat d'une Adaptation/Mutation (Dés d'Évolution). */
static async #onBuyEvolution(event, target) {
const { default: BuyEvolutionDialog } = await import("../../system/dialogs/buyEvolutionDialog.mjs")
const dialog = await BuyEvolutionDialog.create({ actorId: this.document.id })
if (dialog) dialog.render(true)
}
/** Ouvre le dialogue de gestion des Dés de Totems. */
static async #onOpenTotemDice(event, target) {
const { default: TotemDiceDialog } = await import("../../system/dialogs/totemDiceDialog.mjs")
const dialog = await TotemDiceDialog.create({ actorId: this.document.id })
if (dialog) dialog.render(true)
}
} }
+2 -1
View File
@@ -13,7 +13,8 @@ export default class VermineEvolutionData extends foundry.abstract.TypeDataModel
const fields = foundry.data.fields const fields = foundry.data.fields
return { return {
...listItemSchema(), ...listItemSchema(),
level: levelSchema(1, 1, 4) level: levelSchema(1, 1, 4),
mutation: new fields.BooleanField({ required: true, initial: false })
} }
} }
} }
+6 -4
View File
@@ -129,12 +129,14 @@ export default class VermineGroupData extends foundry.abstract.TypeDataModel {
} }
/** /**
* Calcule la réserve max en fonction du niveau du groupe. * Calcule la réserve max selon le nombre de membres (personnages).
* Règle simplifiée : niveau × 2, plafonné à 10. * Règles p. 68-69 : la Réserve débute à 3D et gagne 1D par personnage,
* avec un maximum de 10D.
*/ */
_calculateGroupReserve() { _calculateGroupReserve() {
const level = this.level?.value || 1 const members = Array.isArray(this.members) ? this.members : []
this.reserve.max = Math.min(10, level * 2) const characterCount = members.filter((id) => game.actors?.get(id)?.type === "character").length
this.reserve.max = Math.min(10, 3 + characterCount)
if (this.reserve.value > this.reserve.max) { if (this.reserve.value > this.reserve.max) {
this.reserve.value = this.reserve.max this.reserve.value = this.reserve.max
+17
View File
@@ -288,6 +288,23 @@ VERMINE.totems = {
"loner": "TOTEMS.loner.name", "loner": "TOTEMS.loner.name",
"adapted": "TOTEMS.adapted.name" "adapted": "TOTEMS.adapted.name"
} }
/**
* Influence des Dés de Totems (règles p. 121).
* Selon le Totem dominant (strictement plus de Dés Humains ou Adaptés),
* le personnage subit un Bonus/Malus de 1D sur les Domaines de Compétences.
* Neutre : aucun modificateur.
*/
VERMINE.totemInfluence = {
"human": {
"bonus": ["man", "tool", "world"],
"malus": ["animal", "survival", "weapon"]
},
"adapted": {
"bonus": ["animal", "survival", "weapon"],
"malus": ["man", "tool", "world"]
}
}
VERMINE.totem_opposites = { VERMINE.totem_opposites = {
"human": "TOTEMS.adapted.name", "human": "TOTEMS.adapted.name",
"predator": "TOTEMS.scavenger.name", "predator": "TOTEMS.scavenger.name",
@@ -0,0 +1,115 @@
import { VermineExperience } from "../experience.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
/**
* Dialogue d'achat d'une Adaptation/Mutation avec les Dés d'Évolution
* accumulés. Vérifie le coût (niveau 1-4), la limite du Mode de jeu et les
* restrictions sur les Mutations ; en cas de succès, crée l'item et efface
* les Dés d'Évolution accumulés.
*/
export default class BuyEvolutionDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
#actor;
static DEFAULT_OPTIONS = {
classes: ["vermine-roll", "buy-evolution"],
tag: "form",
window: {
icon: "fas fa-cart-shopping",
resizable: false
},
position: {
width: 480,
height: 460
},
actions: {
buy: BuyEvolutionDialog.#onBuy,
cancel: BuyEvolutionDialog.#onCancel
}
};
static PARTS = {
main: { template: "systems/vermine2047/templates/dialogs/buy-evolution-dialog.hbs", scrollable: [""] }
};
static async create({ actorId }) {
const actor = await game.actors.get(actorId);
if (!actor) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
return null;
}
return new BuyEvolutionDialog({ actor });
}
constructor(options = {}) {
super(options);
this.#actor = options.actor;
}
get title() {
return game.i18n.localize("VERMINE.buy_evolution");
}
async _prepareContext() {
const actor = this.#actor;
const mode = VermineExperience.gameMode();
const modeLabels = {
1: game.i18n.localize("VERMINE.mode_survie"),
2: game.i18n.localize("VERMINE.mode_cauchemar"),
3: game.i18n.localize("VERMINE.mode_apocalypse")
};
const levels = [];
for (let value = 1; value <= 4; value++) {
levels.push({ value, cost: value });
}
return {
evolutionDice: actor.system?.experience?.evolution || 0,
defaultName: game.i18n.localize("ITEMS.new_evolution"),
levels,
totalLevels: VermineExperience.evolutionTotalLevels(actor),
modeMax: VermineExperience.evolutionMaxForMode(mode),
modeLabel: modeLabels[mode] || ""
};
}
static async #onCancel(event, target) {
this.close();
}
static async #onBuy(event, target) {
const actor = this.#actor;
const name = this.element.querySelector("#evolution-name")?.value?.trim() || "";
const cost = parseInt(this.element.querySelector("#evolution-level")?.value, 10) || 0;
const mutation = this.element.querySelector("#evolution-mutation")?.checked || false;
if (!name) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_evolution_no_name"));
return;
}
const error = VermineExperience.validateEvolutionPurchase(actor, cost, mutation);
if (error) {
ui.notifications.warn(game.i18n.localize(error));
return;
}
const created = await actor.createEmbeddedDocuments("Item", [{
name,
type: "evolution",
system: {
level: { value: cost },
mutation
}
}]);
// N'efface les Dés d'Évolution que si l'item a bien été créé.
if (!created || created.length === 0) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_evolution_create"));
return;
}
await actor.update({ "system.experience.evolution": 0 });
ui.notifications.info(game.i18n.localize("VERMINE.buy_evolution_done"));
this.close();
}
}
+125
View File
@@ -0,0 +1,125 @@
import { VermineTotemDice } from "../totem-dice.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
/**
* Dialogue de gestion des Dés de Totems (Humain / Adapté).
* Permet d'appliquer les gains et pertes liés aux Instincts et Interdits
* (1D ou 2D selon la gravité de l'acte), en respectant les limites
* (3 Dés du même Totem, 5 Dés au maximum) et les règles d'échange.
*/
export default class TotemDiceDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
#actor;
#serious = false;
static DEFAULT_OPTIONS = {
classes: ["vermine-roll", "totem-dice"],
tag: "form",
window: {
icon: "fas fa-dice-d10",
resizable: false
},
position: {
width: 480,
height: 420
},
actions: {
gainHuman: TotemDiceDialog.#onGainHuman,
gainAdapted: TotemDiceDialog.#onGainAdapted,
loseHuman: TotemDiceDialog.#onLoseHuman,
close: TotemDiceDialog.#onClose
}
};
static PARTS = {
main: { template: "systems/vermine2047/templates/dialogs/totem-dice-dialog.hbs", scrollable: [""] }
};
static async create({ actorId }) {
const actor = await game.actors.get(actorId);
if (!actor) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
return null;
}
return new TotemDiceDialog({ actor });
}
constructor(options = {}) {
super(options);
this.#actor = options.actor;
}
get title() {
return game.i18n.localize("VERMINE.totem_dice");
}
async _prepareContext() {
const actor = this.#actor;
const influence = VermineTotemDice.getInfluence(actor);
const influenceLabels = {
human: game.i18n.localize("TOTEMS.human.name"),
adapted: game.i18n.localize("TOTEMS.adapted.name"),
neutral: game.i18n.localize("VERMINE.totem_neutral")
};
return {
humanValue: actor.system?.adaptation?.totems?.human?.value ?? 0,
humanMax: actor.system?.adaptation?.totems?.human?.max ?? 3,
adaptedValue: actor.system?.adaptation?.totems?.adapted?.value ?? 0,
adaptedMax: actor.system?.adaptation?.totems?.adapted?.max ?? 3,
influenceLabel: influenceLabels[influence] || "",
serious: this.#serious
};
}
_onRender(context, options) {
super._onRender(context, options);
const checkbox = this.element.querySelector("#totem-serious");
if (checkbox) {
checkbox.checked = this.#serious;
checkbox.addEventListener("change", () => { this.#serious = checkbox.checked; });
}
}
#getCount() {
return this.element.querySelector("#totem-serious")?.checked ? 2 : 1;
}
#notify(result) {
const key = result.totem === "human" ? "TOTEMS.human.name" : "TOTEMS.adapted.name";
const totemName = game.i18n.localize(key);
const parts = [];
if (result.gained) parts.push(game.i18n.localize("VERMINE.totem_gained").replace("{totem}", totemName));
if (result.exchanged) parts.push(game.i18n.localize("VERMINE.totem_exchanged").replace("{totem}", totemName));
if (result.replaced) parts.push(game.i18n.localize("VERMINE.totem_replaced").replace("{totem}", totemName));
if (result.lost) parts.push(game.i18n.localize("VERMINE.totem_lost"));
if (result.gainedAdapted) parts.push(game.i18n.localize("VERMINE.totem_gained_adapted"));
if (result.refused) parts.push(game.i18n.localize("VERMINE.totem_limit_reached"));
const text = parts.join(" ");
if (result.refused && !result.gained && !result.lost && !result.exchanged && !result.replaced) {
ui.notifications.warn(text || game.i18n.localize("VERMINE.totem_limit_reached"));
} else if (text) {
ui.notifications.info(text);
}
this.render();
}
static async #onGainHuman() {
const result = await VermineTotemDice.gainDie(this.#actor, "human", this.#getCount());
this.#notify(result);
}
static async #onGainAdapted() {
const result = await VermineTotemDice.gainDie(this.#actor, "adapted", this.#getCount());
this.#notify(result);
}
static async #onLoseHuman() {
const result = await VermineTotemDice.loseDie(this.#actor, "human", this.#getCount());
this.#notify(result);
}
static #onClose() {
this.close();
}
}
+58
View File
@@ -233,4 +233,62 @@ export class VermineExperience {
} }
if (Object.keys(update).length) await actor.update(update) if (Object.keys(update).length) await actor.update(update)
} }
/**
* Mode de jeu actif (1=Survie, 2=Cauchemar, 3=Apocalypse).
* @returns {number}
*/
static gameMode() {
return parseInt(game.settings.get("vermine2047", "game-mode"), 10) || 1
}
/**
* Total de Dés d'Évolution maximum selon le Mode de jeu.
* @param {number} mode 1=Survie, 2=Cauchemar, 3=Apocalypse
* @returns {number}
*/
static evolutionMaxForMode(mode) {
switch (parseInt(mode, 10)) {
case 2: return 6
case 3: return 7
default: return 5
}
}
/**
* Somme des niveaux des Adaptations/Mutations déjà possédées.
* @param {foundry.abstract.Document} actor personnage
* @returns {number}
*/
static evolutionTotalLevels(actor) {
return (actor?.itemTypes?.evolution || []).reduce((sum, item) => sum + (item.system?.level?.value || 0), 0)
}
/**
* Valide l'achat d'une Adaptation/Mutation contre les Dés d'Évolution
* accumulés. Retourne une clé i18n d'erreur, ou null si l'achat est permis.
* Règles p. 116-117 : coût = niveau ; total limité par le Mode ; Mutations
* 3D/4D interdites en Mode Survie ; une seule Mutation à 4D.
* @param {foundry.abstract.Document} actor personnage
* @param {number} cost niveau de l'Adaptation/Mutation (1-4)
* @param {boolean} mutation vrai si c'est une Mutation
* @returns {string|null}
*/
static validateEvolutionPurchase(actor, cost, mutation) {
const level = parseInt(cost, 10) || 0
const accumulated = actor.system?.experience?.evolution || 0
const mode = this.gameMode()
if (accumulated < level) return "VERMINE.error_evolution_not_enough_dice"
if (level < 1 || level > 4) return "VERMINE.error_evolution_invalid_level"
const total = this.evolutionTotalLevels(actor) + level
if (total > this.evolutionMaxForMode(mode)) return "VERMINE.error_evolution_mode_limit"
if (mutation && level >= 3 && mode < 2) return "VERMINE.error_evolution_mutation_mode"
if (mutation && level === 4) {
const hasFourDMutation = (actor?.itemTypes?.evolution || []).some(
(item) => item.system?.mutation && (item.system?.level?.value || 0) === 4
)
if (hasFourDMutation) return "VERMINE.error_evolution_mutation_4d"
}
return null
}
} }
+9
View File
@@ -131,6 +131,15 @@ export const registerHooks = function () {
} }
}); });
// Un Groupe démarre avec une Réserve à son maximum (3D + 1D par membre).
Hooks.on("createActor", async function (actor) {
if (actor.type !== "group") return;
const reserve = actor.system?.reserve;
if (reserve && reserve.value === 0 && reserve.max > 0) {
await actor.update({ "system.reserve.value": reserve.max });
}
});
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Combat Hooks */ /* Combat Hooks */
/* -------------------------------------------- */ /* -------------------------------------------- */
+16 -64
View File
@@ -1,3 +1,5 @@
import { VermineTotemDice } from "./totem-dice.mjs";
export class VermineUtils { export class VermineUtils {
/** /**
* Rolls dice with Vermine2047-specific rules. * Rolls dice with Vermine2047-specific rules.
@@ -49,12 +51,12 @@ export class VermineUtils {
// Declare variables // Declare variables
let formula = ""; let formula = "";
let modFormula = null; let modFormula = null;
let totemBonus = { human: 0, adapted: 0 };
// Calculate domain bonuses for totems // Influence des Dés de Totems : Bonus/Malus de 1D selon le Totem dominant
if (skillCategory) { // et le Domaine de la Compétence (règles p. 121). S'applique même sans
totemBonus = this._calculateTotemDomainBonuses(skillCategory, actor); // utiliser un Dé de Totem.
} const influenceMod = this._calculateTotemInfluence(skillCategory, actor);
NoD += influenceMod;
// Apply automatic successes from skill mastery // Apply automatic successes from skill mastery
let adjustedDifficulty = difficulty; let adjustedDifficulty = difficulty;
@@ -65,11 +67,6 @@ export class VermineUtils {
const humanDifficulty = adjustedDifficulty; const humanDifficulty = adjustedDifficulty;
const humanFormula = `(1D10cs>=${humanDifficulty}[human_${safeUserName}]*2)`; const humanFormula = `(1D10cs>=${humanDifficulty}[human_${safeUserName}]*2)`;
// Apply domain bonus/malus
if (totemBonus.human !== 0) {
NoD += totemBonus.human;
}
modFormula = humanFormula; modFormula = humanFormula;
} }
@@ -79,11 +76,6 @@ export class VermineUtils {
const adaptedDifficulty = adjustedDifficulty; const adaptedDifficulty = adjustedDifficulty;
const adaptedFormula = `(1D10cs>=${adaptedDifficulty}[adapted_${safeUserName}]*2)`; const adaptedFormula = `(1D10cs>=${adaptedDifficulty}[adapted_${safeUserName}]*2)`;
// Apply domain bonus/malus
if (totemBonus.adapted !== 0) {
NoD += totemBonus.adapted;
}
// Build combined formula // Build combined formula
if (modFormula !== null) { if (modFormula !== null) {
modFormula = `${modFormula}+${adaptedFormula}`; modFormula = `${modFormula}+${adaptedFormula}`;
@@ -113,8 +105,8 @@ export class VermineUtils {
formula = modFormula !== null ? `${baseFormula}+${modFormula}` : baseFormula; formula = modFormula !== null ? `${baseFormula}+${modFormula}` : baseFormula;
// Reconcile the breakdown displayed in chat with the dice actually rolled. // Reconcile the breakdown displayed in chat with the dice actually rolled.
// Domain bonuses/malus are only applied when the corresponding totem die // L'influence s'applique au pool ; les Dés de Totem remplacent (n'ajoutent
// is used, and totem dice replace (not add to) base-pool dice. // pas) des dés du pool de base.
if (poolBreakdown) { if (poolBreakdown) {
// Count totem dice present in the final modifier formula. // Count totem dice present in the final modifier formula.
let totemDice = 0; let totemDice = 0;
@@ -122,14 +114,9 @@ export class VermineUtils {
if (modFormula.includes("human_")) totemDice += 1; if (modFormula.includes("human_")) totemDice += 1;
if (modFormula.includes("adapted_")) totemDice += 1; if (modFormula.includes("adapted_")) totemDice += 1;
} }
// Only count the domain bonus of totems actually activated in the roll.
const domainBonus = (totems.human ? totemBonus.human : 0)
+ (totems.adapted ? totemBonus.adapted : 0);
const basePool = poolBreakdown.total;
poolBreakdown.totemDice = totemDice; poolBreakdown.totemDice = totemDice;
poolBreakdown.domainBonus = domainBonus; poolBreakdown.domainBonus = influenceMod;
// Total = nombre réel de dés lancés (NoD réguliers + dés de totem). // Total = nombre réel de dés lancés (NoD réguliers + dés de totem).
// Équivaut à basePool + domainBonus sauf si le pool régulier clampe à 0.
poolBreakdown.total = NoD + totemDice; poolBreakdown.total = NoD + totemDice;
} }
@@ -145,7 +132,7 @@ export class VermineUtils {
skillCategory: skillCategory, skillCategory: skillCategory,
skillLevel: skillLevel, skillLevel: skillLevel,
hasSpecialty: hasSpecialty, hasSpecialty: hasSpecialty,
totemBonuses: { ...totemBonus }, totemInfluence: influenceMod,
baseNoD: NoD, baseNoD: NoD,
rerolls: Reroll, rerolls: Reroll,
selfControl: self_control, selfControl: self_control,
@@ -188,47 +175,12 @@ export class VermineUtils {
* Calculates domain bonuses/penalties for totems. * Calculates domain bonuses/penalties for totems.
* @param {string} skillCategory - The skill category * @param {string} skillCategory - The skill category
* @param {Actor} actor - The actor * @param {Actor} actor - The actor
* @returns {Object} Bonuses for each totem {human: number, adapted: number} * @returns {number} Modificateur d'influence (-1, 0 ou +1)
*/ */
static _calculateTotemDomainBonuses(skillCategory, actor) { static _calculateTotemInfluence(skillCategory, actor) {
const bonuses = { human: 0, adapted: 0 }; // Les Dés de Totems n'existent que sur les personnages.
if (actor?.type !== "character" || !skillCategory) return 0;
// Validate inputs return VermineTotemDice.influenceModifier(actor, skillCategory);
if (!CONFIG.VERMINE?.totemDomains || !actor?.system?.identity?.totem) {
return bonuses;
}
const actorTotem = actor.system.identity.totem;
// Check if actor's totem exists in configuration
if (!CONFIG.VERMINE.totemDomains[actorTotem]) {
return bonuses;
}
const totemConfig = CONFIG.VERMINE.totemDomains[actorTotem];
if (!totemConfig?.domains) {
return bonuses;
}
// Get actor's preferred skill category
const preferredCategory = actor.system.skill_categories?.preferred;
// Bonus for actor's totem if preferred category is in its domains
if (preferredCategory && totemConfig.domains.includes(preferredCategory)) {
bonuses[actorTotem] = totemConfig.bonus || 1;
}
// Penalty for opposite totem if preferred category is in its domains
const oppositeTotem = CONFIG.VERMINE.totem_opposites?.[actorTotem];
if (oppositeTotem && CONFIG.VERMINE.totemDomains[oppositeTotem]) {
const oppositeConfig = CONFIG.VERMINE.totemDomains[oppositeTotem];
if (preferredCategory && oppositeConfig?.domains?.includes(preferredCategory)) {
bonuses[oppositeTotem] = -(oppositeConfig.bonus || 1);
}
}
return bonuses;
} }
/** /**
+171
View File
@@ -0,0 +1,171 @@
/**
* VermineTotemDice - Gestion des Dés de Totems (Humain / Adapté).
*
* Règles p. 121 :
* - Influence : si un joueur possède strictement plus de Dés d'un Totem que
* de l'autre, ce Totem prend l'ascendant (+1D/-1D sur certains Domaines).
* - Gains et pertes via les Instincts et les Interdits (1D ou 2D selon la
* gravité), avec des limites (3 Dés du même Totem, 5 Dés au maximum).
*/
export class VermineTotemDice {
/**
* Totem dominant d'un personnage.
* @param {foundry.abstract.Document} actor
* @returns {'human'|'adapted'|'neutral'}
*/
static getInfluence(actor) {
const human = actor.system?.adaptation?.totems?.human?.value || 0;
const adapted = actor.system?.adaptation?.totems?.adapted?.value || 0;
if (human > adapted) return "human";
if (adapted > human) return "adapted";
return "neutral";
}
/**
* Modificateur (Bonus/Malus) d'influence pour une catégorie de compétence.
* @param {foundry.abstract.Document} actor
* @param {string} skillCategory catégorie (man, animal, tool, weapon, survival, world)
* @returns {number} -1, 0 ou +1
*/
static influenceModifier(actor, skillCategory) {
const influence = this.getInfluence(actor);
if (influence === "neutral" || !skillCategory) return 0;
const cfg = CONFIG.VERMINE?.totemInfluence?.[influence];
if (!cfg) return 0;
if ((cfg.bonus || []).includes(skillCategory)) return 1;
if ((cfg.malus || []).includes(skillCategory)) return -1;
return 0;
}
/**
* Applique un gain d'un Dé de Totem selon les règles d'échange et de limites.
* Retourne un objet décrivant ce qui s'est passé (pour notification).
* @param {foundry.abstract.Document} actor
* @param {'human'|'adapted'} totem
* @param {number} [count=1] 1 ou 2 dés (acte grave)
* @returns {Promise<Object>}
*/
static async gainDie(actor, totem, count = 1) {
const human = actor.system.adaptation.totems.human;
const adapted = actor.system.adaptation.totems.adapted;
const actions = [];
for (let i = 0; i < count; i++) {
const step = this._gainOneDie(human.value, adapted.value, totem);
if (step.unchanged) {
actions.push({ ok: false, reason: "max" });
break;
}
human.value = step.human;
adapted.value = step.adapted;
actions.push({ ok: true, exchanged: step.exchanged, replaced: step.replaced });
}
await actor.update({
"system.adaptation.totems.human.value": human.value,
"system.adaptation.totems.adapted.value": adapted.value
});
return this._summarize(actions, totem);
}
/**
* Applique une perte d'un Dé de Totem.
* Règle : seul l'Humain a des Interdits. Briser un Interdit Humain fait
* perdre 1D Humain ; si le personnage n'en possède pas, il gagne 1D Adapté.
* @param {foundry.abstract.Document} actor
* @param {'human'} totem
* @param {number} [count=1] 1 ou 2 dés (acte grave)
* @returns {Promise<Object>}
*/
static async loseDie(actor, totem, count = 1) {
const human = actor.system.adaptation.totems.human;
const adapted = actor.system.adaptation.totems.adapted;
const actions = [];
for (let i = 0; i < count; i++) {
if (human.value > 0) {
human.value -= 1;
actions.push({ ok: true, lost: true });
} else if (adapted.value < adapted.max && (human.value + adapted.value) < 5) {
// Pas de Dé Humain : gagne un Dé Adapté à la place (limites appliquées).
adapted.value += 1;
actions.push({ ok: true, gainedAdapted: true });
} else {
actions.push({ ok: false, reason: "max" });
break;
}
}
await actor.update({
"system.adaptation.totems.human.value": human.value,
"system.adaptation.totems.adapted.value": adapted.value
});
return this._summarize(actions, totem);
}
/**
* Ajoute un Dé du Totem donné en respectant les limites et les échanges.
* Règles :
* - 3 Dés max du même Totem, 5 Dés max au total.
* - Si le joueur gagne un Dé qu'il ne peut conserver, il peut supprimer un
* Dé du Totem opposé à la place ; sinon le gain est annulé.
* - Gagner un 4e Dé Adapté est impossible, mais supprime un Dé Humain.
* @param {number} human
* @param {number} adapted
* @param {'human'|'adapted'} totem
* @returns {Object}
*/
static _gainOneDie(human, adapted, totem) {
const limitTotem = 3;
const opposite = totem === "human" ? "adapted" : "human";
const total = human + adapted;
const current = totem === "human" ? human : adapted;
const oppositeVal = totem === "human" ? adapted : human;
// Le Totem cible est déjà au maximum (3) : impossible d'empiler.
if (current >= limitTotem) {
if (oppositeVal > 0) {
// Supprime un Dé du Totem opposé à la place (le gain n'augmente pas).
const result = { human, adapted, unchanged: false, replaced: true };
result[opposite] -= 1;
return result;
}
return { human, adapted, unchanged: true };
}
// Limite totale atteinte (5) : peut remplacer un Dé opposé.
if (total >= 5) {
if (oppositeVal > 0) {
const result = { human, adapted, unchanged: false, exchanged: true };
result[totem] += 1;
result[opposite] -= 1;
return result;
}
return { human, adapted, unchanged: true };
}
// Gain normal.
const result = { human, adapted, unchanged: false };
result[totem] += 1;
return result;
}
/**
* Résume les actions appliquées en un libellé localisé.
* @param {Array} actions
* @param {'human'|'adapted'} totem
* @returns {Object}
*/
static _summarize(actions, totem) {
const gained = actions.filter(a => a.ok && !a.exchanged && !a.replaced && !a.lost && !a.gainedAdapted).length;
const exchanged = actions.filter(a => a.exchanged).length;
const replaced = actions.filter(a => a.replaced).length;
const lost = actions.filter(a => a.lost).length;
const gainedAdapted = actions.filter(a => a.gainedAdapted).length;
const refused = actions.some(a => !a.ok);
return { totem, gained, exchanged, replaced, lost, gainedAdapted, refused };
}
}
+16
View File
@@ -74,6 +74,22 @@
</div> </div>
</div> </div>
{{!-- Influence + gestion des Dés de Totems --}}
<div class="totem-influence-row">
<span class="totem-influence-label">
{{ localize "VERMINE.influence" }}:
{{#ifgt system.adaptation.totems.human.value system.adaptation.totems.adapted.value}}
<strong class="totem-human">{{ localize "TOTEMS.human.name" }}</strong>
{{else ifgt system.adaptation.totems.adapted.value system.adaptation.totems.human.value}}
<strong class="totem-adapted">{{ localize "TOTEMS.adapted.name" }}</strong>
{{else}}
<strong>{{ localize "VERMINE.totem_neutral" }}</strong>
{{/ifgt}}
</span>
<button type="button" class="totem-manage-btn" data-action="openTotemDice"
title="{{ localize 'VERMINE.totem_manage' }}"><i class="fas fa-sliders"></i></button>
</div>
</div> </div>
<ul class="padding-with-frieze unstyled paper"> <ul class="padding-with-frieze unstyled paper">
{{#if system.identity.totem }} {{#if system.identity.totem }}
+7 -4
View File
@@ -97,7 +97,6 @@
data-action="edit" data-action="edit"
data-item-id="{{item._id}}" data-item-id="{{item._id}}"
title="Edit Item">{{item.name}}</a> title="Edit Item">{{item.name}}</a>
<span data-tooltip="Compétence">{{item.system.skill}}</span>
</div> </div>
<div class="item-controls flexrow"> <div class="item-controls flexrow">
{{#if item.system.description}} {{#if item.system.description}}
@@ -192,9 +191,8 @@
{{#if isEditMode}} {{#if isEditMode}}
<a <a
class="item-control item-create" class="item-control item-create"
title="Create item" title="Buy evolution"
data-action="create" data-action="buyEvolution"><i class="fas fa-cart-shopping"></i></a>
data-type="evolution"><i class="fas fa-plus"></i></a>
{{/if}} {{/if}}
</h4> </h4>
<ol class="list-item"> <ol class="list-item">
@@ -206,6 +204,11 @@
data-action="edit" data-action="edit"
data-item-id="{{item._id}}" data-item-id="{{item._id}}"
title="Edit Item">{{item.name}}</a> title="Edit Item">{{item.name}}</a>
{{#if item.system.mutation}}
<span class="evolution-badge mutation">{{ localize 'VERMINE.evolution_mutation' }}</span>
{{else}}
<span class="evolution-badge adaptation">{{ localize 'VERMINE.evolution_adaptation' }}</span>
{{/if}}
</div> </div>
<div class="item-controls flexrow"> <div class="item-controls flexrow">
{{#if item.system.description}} {{#if item.system.description}}
@@ -0,0 +1,53 @@
<div class="roll-dialog-content">
<h3 class="combat-mode-label">{{ localize 'VERMINE.buy_evolution' }}</h3>
<div class="learning-summary">
<span class="label">{{ localize 'VERMINE.evolution_available_dice' }}:</span>
<span class="exp-value">{{ evolutionDice }}D</span>
</div>
<div class="flexrow bonus-item full-width">
<label class="label" for="evolution-name">{{ localize 'VERMINE.evolution_name' }}</label>
<input type="text" data-roll="true" id="evolution-name" name="evolution-name" value="{{ defaultName }}" />
</div>
<div class="flexrow bonus-item full-width">
<label class="label" for="evolution-level">{{ localize 'VERMINE.evolution_level' }}</label>
<select class="info-value" data-roll="true" id="evolution-level" name="evolution-level">
{{#each levels}}
<option value="{{ this.value }}">{{ this.value }}D — {{ this.cost }}D</option>
{{/each}}
</select>
</div>
<div class="flexrow bonus-item full-width">
<label class="label" for="evolution-mutation">{{ localize 'VERMINE.evolution_mutation' }}</label>
<input type="checkbox" data-roll="true" id="evolution-mutation" name="evolution-mutation" />
</div>
<div class="learning-result">
<div class="context-item">
<span class="context-label">{{ localize 'VERMINE.evolution_total' }}</span>
<span class="context-value" id="evolution-total-value">{{ totalLevels }}D</span>
</div>
<div class="context-item">
<span class="context-label">{{ localize 'VERMINE.evolution_mode_max' }}</span>
<span class="context-value">{{ modeMax }}D</span>
</div>
<div class="context-item">
<span class="context-label">{{ localize 'VERMINE.game_mode' }}</span>
<span class="context-value">{{ modeLabel }}</span>
</div>
</div>
<p class="learning-hint">{{ localize 'VERMINE.evolution_buy_hint' }}</p>
<footer class="sheet-footer flexrow">
<button type="button" data-action="cancel">
<i class="fas fa-times"></i> {{localize "VERMINE.cancel"}}
</button>
<button type="button" data-action="buy">
<i class="fas fa-cart-shopping"></i> {{localize "VERMINE.buy_evolution"}}
</button>
</footer>
</div>
+44
View File
@@ -0,0 +1,44 @@
<div class="roll-dialog-content">
<h3 class="combat-mode-label">{{ localize 'VERMINE.totem_dice' }}</h3>
<div class="learning-summary">
<span class="label">{{ localize 'VERMINE.influence' }}:</span>
<span class="exp-value totem-influence">{{ influenceLabel }}</span>
</div>
<div class="totem-dice-state flexrow flex-group-center">
<div class="totem-state">
<span class="label totem-human">{{ localize 'TOTEMS.human.name' }}:</span>
<span class="value">{{ humanValue }} / {{ humanMax }}</span>
</div>
<div class="totem-state">
<span class="label totem-adapted">{{ localize 'TOTEMS.adapted.name' }}:</span>
<span class="value">{{ adaptedValue }} / {{ adaptedMax }}</span>
</div>
</div>
<div class="flexrow bonus-item full-width">
<label class="label" for="totem-serious">{{ localize 'VERMINE.totem_serious' }}</label>
<input type="checkbox" data-roll="true" id="totem-serious" name="totem-serious" {{checked serious}} />
</div>
<p class="learning-hint">{{ localize 'VERMINE.totem_manage_hint' }}</p>
<div class="totem-actions flexrow flex-wrap">
<button type="button" data-action="gainHuman">
<i class="fas fa-hand-holding-heart"></i> {{ localize 'VERMINE.totem_instinct_human' }}
</button>
<button type="button" data-action="gainAdapted">
<i class="fas fa-paw"></i> {{ localize 'VERMINE.totem_instinct_adapted' }}
</button>
<button type="button" data-action="loseHuman">
<i class="fas fa-ban"></i> {{ localize 'VERMINE.totem_interdit_human' }}
</button>
</div>
<footer class="sheet-footer flexrow">
<button type="button" data-action="close">
<i class="fas fa-times"></i> {{localize "VERMINE.close"}}
</button>
</footer>
</div>
+4
View File
@@ -13,6 +13,10 @@
<label class="resource-label ">{{ localize "VERMINE.level"}}</label> <label class="resource-label ">{{ localize "VERMINE.level"}}</label>
<span class="hexa"><input type="number" name="system.level.value" value="{{system.level.value }}" data-dtype="Number" min="{{system.level.min }}" max="{{system.level.max }}"/></span> <span class="hexa"><input type="number" name="system.level.value" value="{{system.level.value }}" data-dtype="Number" min="{{system.level.min }}" max="{{system.level.max }}"/></span>
</h4> </h4>
<div class="flexrow bonus-item full-width">
<label class="resource-label" for="system.mutation">{{ localize "VERMINE.evolution_mutation"}}</label>
<input type="checkbox" name="system.mutation" {{checked system.mutation}}/>
</div>
<h2>description</h2> <h2>description</h2>
<fieldset> <fieldset>