feat: équipement, échanges de coups et améliorations des fiches

- équipement des armes et protections (champ equipped, liste des équipés, toggle)
- échanges de coups automatisés (attaque/défense/résolution/blessures dans le chat)
- dialogue d'attaque par type (personnage/PNJ/créature), portées et difficultés
- conservation du scroll des fiches lors des modifs (option scrollable + classe active)
- verrouillage des compétences PNJ en mode jeu
- corrections review combat (défenseur, difficulté >10, multi-cibles, permissions)
This commit is contained in:
2026-08-04 14:14:03 +02:00
parent 31f2f4530c
commit f2c8cfe246
45 changed files with 2105 additions and 78 deletions
+93 -1
View File
@@ -147,6 +147,8 @@
"pools": "Reserves",
"self_control": "Self-control",
"effort": "Effort",
"equip": "Equip",
"unequip": "Unequip",
"group": "Group",
"abilities": "Characteristics",
"ability": "Characteristic",
@@ -253,6 +255,7 @@
"pool_tooling": "Equipment",
"pool_group": "Group",
"pool_selfControl": "Self-Control",
"pool_pools": "Pools",
"pool_total": "Total",
"totem_used": "Totem",
"totem_keep": "kept",
@@ -280,12 +283,101 @@
"error_no_actor_selected": "No actor selected",
"error_not_enough_self_control": "Not enough self control",
"error_select_ability": "Select an ability",
"attack": "Attack",
"attack_contact": "Melee attack",
"attack_ranged": "Ranged attack",
"defense_esquive": "Dodge",
"defense_parade": "Parry",
"passive_defense": "Passive defense",
"range_short": "Short",
"range_mid": "Medium",
"range_long": "Long",
"second_action": "Second action (+2 difficulty)",
"consume_ammo": "Consume ammo",
"npc_pool": "Pools used",
"exchange": "Combat exchange",
"resolve": "Resolve",
"resolve_result": "Exchange resolution",
"attacker_successes": "Attack successes",
"defender_successes": "Defense successes",
"raw_damage": "Raw damage",
"net_damage": "Net damage",
"hit": "Hit",
"hit_parade": "Hit (insufficient parry)",
"missed_esquive": "Dodged",
"missed_parade": "Parried",
"no_damage": "No damage",
"wound": "Wound",
"no_wound": "No wound",
"apply_wound": "Apply wound",
"wound_applied": "Wound applied",
"error_creature_no_defense": "Creatures cannot defend (passive defense)",
"error_no_permission": "Insufficient permissions",
"error_no_target": "No target selected for this exchange",
"experience_phase": "Experience phase",
"experience_dice": "Experience dice",
"experience_rewards": "Experience rewards",
"collective": "collective",
"collective_rewards": "Collective rewards",
"individual_rewards": "Individual rewards",
"collective_group": "Group (collective pool)",
"danger": "Danger",
"discoveries": "Discoveries",
"duration": "Duration",
"implication": "Implication",
"influence": "Influence",
"interpretation": "Interpretation",
"reward_anecdotic": "Anecdotal",
"reward_minor": "Minor",
"reward_major": "Major",
"reward_exceptionnal": "Exceptional",
"objective_minor": "Minor objective",
"objective_major": "Major objective",
"trauma_reward": "Chosen trauma",
"trauma_reward_5": "Trauma reward (+5D)",
"evolution_dice": "Evolution dice",
"open_experience_phase": "Experience phase",
"open_learning": "Learning",
"learning_limits": "Learning limits (per phase)",
"learned_skill": "Skill",
"learned_specialty": "Specialty",
"learned_reserve": "Reserve",
"learned_characteristic": "Characteristic",
"learned_evolution": "Evolution",
"no_character": "No character in this world.",
"apply": "Apply",
"characteristic": "Characteristic",
"learn_skill": "Skill",
"learn_specialty": "Specialty",
"learn_characteristic": "Characteristic",
"learn_reserve": "Reserve",
"learn_evolution": "Evolution die",
"learn_specialty_name": "Specialty name",
"specialty_hint": "Prerequisite: skill at least Proficient (≥2).",
"characteristic_hint": "Difficulty 9 + Handicap = current value. Max 3 (except Superhuman).",
"reserve_hint": "Difficulty = current value + age Handicap. Max 10.",
"evolution_hint": "Difficulty by age + Handicap = accumulated Evolution dice. Adapted totem +1D, Human -1D.",
"dice_spent": "Dice spent",
"learning_success": "Learning successful!",
"error_no_experience_dice": "No Experience dice available.",
"error_learning_limit": "Limit reached for this type of learning in this phase.",
"error_skill_max": "This skill is already at maximum level.",
"error_characteristic_max": "Characteristic already at maximum (3).",
"error_specialty_requirement": "Specialty requires: skill at least Proficient (≥2).",
"error_reserve_max": "Reserve already at maximum (10).",
"experience_transfer": "Experience dice",
"experience_pull": "Pull from group",
"experience_give": "Give 1D to group",
"pool_experience": "Experience",
"error_no_group": "No group associated with this character.",
"error_no_group_dice": "Not enough group pool dice.",
"tabs": {
"abilities": "Abilities",
"combat": "Combat",
"equipment": "Equipment",
"stories": "Stories",
"totem": "Totem"
"totem": "Totem",
"experience": "Experience"
},
"base_values": "Base Values",
"computed_values": "Computed Values",
+93 -1
View File
@@ -154,6 +154,8 @@
"reserves": "Réserves",
"self_control": "Sang-Froid",
"effort": "Effort",
"equip": "Équiper",
"unequip": "Déséquiper",
"preferred_category": "Catégorie préférée",
"wounds": {
"name": "Blessures",
@@ -200,6 +202,7 @@
"pool_tooling": "Équipement",
"pool_group": "Groupe",
"pool_selfControl": "Sang-Froid",
"pool_pools": "Réserves",
"pool_total": "Total",
"totem_used": "Totem",
"totem_keep": "gardé",
@@ -266,6 +269,94 @@
"totem_dice": "Dés de totem",
"keep_totem": "Garder le totem",
"totem_hint": "Cochez pour utiliser les dés de totem (double réussite possible)",
"attack": "Attaque",
"attack_contact": "Attaque au contact",
"attack_ranged": "Attaque à distance",
"defense_esquive": "Esquive",
"defense_parade": "Parade",
"passive_defense": "Défense passive",
"range_short": "Courte",
"range_mid": "Moyenne",
"range_long": "Longue",
"second_action": "Deuxième action (+2 difficulté)",
"consume_ammo": "Consommer des munitions",
"npc_pool": "Réserves utilisées",
"exchange": "Échange de coups",
"resolve": "Résoudre",
"resolve_result": "Résolution de l'échange",
"attacker_successes": "Réussites d'attaque",
"defender_successes": "Réussites de défense",
"raw_damage": "Dégâts bruts",
"net_damage": "Dégâts nets",
"hit": "Touché",
"hit_parade": "Touché (parade insuffisante)",
"missed_esquive": "Esquivé",
"missed_parade": "Paré",
"no_damage": "Aucun dégât",
"wound": "Blessure",
"no_wound": "Aucune blessure",
"apply_wound": "Appliquer la blessure",
"wound_applied": "Blessure appliquée",
"error_creature_no_defense": "Les créatures ne peuvent pas se défendre (défense passive)",
"error_no_permission": "Permissions insuffisantes",
"error_no_target": "Aucune cible sélectionnée pour cet échange",
"experience_phase": "Phase d'Expérience",
"experience_dice": "Dés d'Expérience",
"experience_rewards": "Récompenses d'Expérience",
"collective": "collectifs",
"collective_rewards": "Récompenses collectives",
"individual_rewards": "Récompenses individuelles",
"collective_group": "Groupe (réserve collective)",
"danger": "Danger",
"discoveries": "Découvertes",
"duration": "Durée",
"implication": "Implication",
"influence": "Influence",
"interpretation": "Interprétation",
"reward_anecdotic": "Anecdotique",
"reward_minor": "Mineure",
"reward_major": "Majeure",
"reward_exceptionnal": "Exceptionnelle",
"objective_minor": "Objectif mineur",
"objective_major": "Objectif majeur",
"trauma_reward": "Traumatisme choisi",
"trauma_reward_5": "Récompense de Traumatisme (+5D)",
"evolution_dice": "Dés d'Évolution",
"open_experience_phase": "Phase d'Expérience",
"open_learning": "Apprentissage",
"learning_limits": "Limites d'apprentissage (par phase)",
"learned_skill": "Compétence",
"learned_specialty": "Spécialité",
"learned_reserve": "Réserve",
"learned_characteristic": "Caractéristique",
"learned_evolution": "Évolution",
"no_character": "Aucun personnage dans ce monde.",
"apply": "Appliquer",
"characteristic": "Caractéristique",
"learn_skill": "Compétence",
"learn_specialty": "Spécialité",
"learn_characteristic": "Caractéristique",
"learn_reserve": "Réserve",
"learn_evolution": "Dé d'Évolution",
"learn_specialty_name": "Nom de la spécialité",
"specialty_hint": "Prérequis : compétence au moins Confirmée (≥2).",
"characteristic_hint": "Difficulté 9 + Handicap = valeur actuelle. Max 3 (Surhumain excepté).",
"reserve_hint": "Difficulté = valeur actuelle + Handicap d'âge. Max 10.",
"evolution_hint": "Difficulté selon l'âge + Handicap = Dés d'Évolution accumulés. Totem Adapté +1D, Humain -1D.",
"dice_spent": "Dés dépensés",
"learning_success": "Apprentissage réussi !",
"error_no_experience_dice": "Pas de Dés d'Expérience disponibles.",
"error_learning_limit": "Limite atteinte pour ce type d'apprentissage dans cette phase.",
"error_skill_max": "Cette compétence est déjà au niveau maximal.",
"error_characteristic_max": "Caractéristique déjà au maximum (3).",
"error_specialty_requirement": "Spécialité requise : compétence au moins Confirmée (≥2).",
"error_reserve_max": "Réserve déjà au maximum (10).",
"experience_transfer": "Dés d'Expérience",
"experience_pull": "Puiser du Groupe",
"experience_give": "Donner 1D au Groupe",
"pool_experience": "Expérience",
"error_no_group": "Aucun Groupe associé à ce personnage.",
"error_no_group_dice": "Réserve de Groupe insuffisante.",
"error_select_skill": "Veuillez sélectionner une compétence",
"needed": "nécessaire",
"cost": "Coût",
@@ -281,7 +372,8 @@
"totem": "Totem et ajustements",
"combat": "Combat et reserves",
"equipment": "Matériel",
"stories": "Histoire"
"stories": "Histoire",
"experience": "Expérience"
},
"ammo": "Munitions",
"ammo_sm": "Mun",
+299
View File
@@ -788,6 +788,195 @@ ol.chat-log {
}
}
// ── Arme du jet ───────────────────────────────────────────────────
div.weapon-block {
margin: 4px 0 6px;
padding: 6px 8px;
border: 1px solid @color-hemolymph;
background: rgba(0, 0, 0, 0.1);
.weapon-block-header {
align-items: center;
gap: 6px;
margin-bottom: 2px;
.weapon-icon {
color: @color-hemolymph;
}
.weapon-name {
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-12;
font-weight: 900;
color: @theme-color-dark;
}
.weapon-targets {
margin-left: auto;
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
color: @theme-color-accent;
}
}
.weapon-details {
gap: 4px 12px;
.weapon-detail {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
strong {
text-transform: uppercase;
}
}
}
}
// ── Échange de coups ─────────────────────────────────────────────
div.exchange-actions {
margin: 4px 0 6px;
padding: 6px 8px;
border: 1px dashed @color-hemolymph;
background: fade(@color-hemolymph, 8%);
.exchange-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-12;
font-weight: 900;
color: @theme-color-dark;
i { color: @color-hemolymph; }
}
.exchange-target {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
padding: 3px 0;
.exchange-target-name {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
color: @theme-color-accent;
}
button.exchange-defend {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
padding: 2px 8px;
cursor: pointer;
color: @color-chitin-dark;
background: @color-text-light-0;
border: 1px solid @color-border-dark-4;
border-radius: 3px;
.transition();
&:hover {
color: @color-text-light-0;
background: @color-hemolymph;
border-color: @color-hemolymph;
}
}
.exchange-passive {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
color: @color-amber;
opacity: 0.85;
font-style: italic;
}
}
button.exchange-resolve {
margin-top: 4px;
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-11;
padding: 3px 12px;
cursor: pointer;
color: @color-text-light-0;
background: fade(@color-hemolymph, 25%);
border: 1px solid @color-hemolymph;
border-radius: 3px;
.transition();
&:hover {
color: @theme-color-dark;
background: @color-hemolymph;
}
}
}
div.exchange-resolution {
margin: 4px 0 6px;
padding: 6px 8px;
border: 1px solid @color-amber;
background: fade(@color-amber, 8%);
.resolution-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-12;
font-weight: 900;
color: @theme-color-dark;
i { color: @color-amber; }
}
.resolution-line {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
padding: 1px 0;
strong { font-weight: 700; }
.res-hit { color: @color-hemolymph; font-weight: 700; }
.res-dodged,
.res-parried { color: @color-acid-green; font-weight: 700; }
&.res-wound { color: @color-hemolymph; font-weight: 700; }
&.res-noshock,
&.res-nodamage { color: @color-acid-green; }
}
button.exchange-apply {
margin-top: 6px;
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-11;
padding: 3px 12px;
cursor: pointer;
color: @color-text-light-0;
background: fade(@color-hemolymph, 25%);
border: 1px solid @color-hemolymph;
border-radius: 3px;
.transition();
&:hover:not(:disabled) {
color: @theme-color-dark;
background: @color-hemolymph;
}
&:disabled {
opacity: 0.7;
cursor: default;
color: @color-acid-green;
border-color: @color-acid-green;
}
}
}
// ── Composition du jet ────────────────────────────────────────────
div.roll-pool {
margin: 4px 0;
@@ -846,3 +1035,113 @@ ol.chat-log {
padding: 15px;
}
}
// ── Onglet Expérience ─────────────────────────────────────────────────
.tab.experience {
.experience-summary {
display: flex;
gap: 10px;
margin-bottom: 10px;
.exp-stat {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding: 8px;
border: 1px solid @color-border-dark-4;
background: fade(@color-chitin-dark, 30%);
label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
color: @color-amber;
}
.exp-value {
font-family: "DistressBlack", sans-serif;
font-size: @font-size-24;
color: @color-acid-green;
}
}
}
.exp-actions {
gap: 8px;
margin-bottom: 10px;
button {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-12;
padding: 5px 14px;
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;
}
}
}
.exp-rewards {
margin-bottom: 10px;
padding: 8px;
border: 1px solid @color-border-dark-4;
background: fade(@color-chitin-dark, 20%);
h4 {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-12;
color: @theme-color-dark;
margin: 0 0 6px;
.exp-total {
color: @color-amber;
}
}
.exp-reward-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
font-size: @font-size-12;
label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-10;
color: @color-amber;
}
}
.exp-learned {
display: flex;
flex-wrap: wrap;
gap: 6px;
span {
padding: 2px 8px;
font-size: @font-size-11;
border: 1px solid @color-border-dark-4;
border-radius: 3px;
opacity: 0.6;
&.learned {
opacity: 1;
color: @color-acid-green;
border-color: @color-acid-green;
}
}
}
}
}
+248
View File
@@ -399,6 +399,254 @@ iframe {
}
}
// ── Combat dialog (attaque) ──────────────────────────────────────
.combat-mode-label {
margin: 0;
text-align: center;
font-size: @font-size-14;
color: @color-hemolymph;
text-transform: uppercase;
}
.locked-value {
flex: 1;
display: flex;
align-items: center;
min-height: 28px;
padding: 0 8px;
font-size: @font-size-12;
color: @color-chitin-dark;
background: @color-text-light-0;
border-radius: 3px;
.custom-select-style();
}
.combat-dialog-weapon {
border: 1px solid @color-hemolymph;
padding: 6px 8px;
background: fade(@color-chitin-dark, 40%);
.weapon-block-header {
align-items: center;
gap: 6px;
.weapon-icon { color: @color-hemolymph; }
.weapon-name {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-12;
font-weight: 900;
color: @theme-color-dark;
}
}
.weapon-details {
gap: 4px 12px;
flex-wrap: wrap;
.weapon-detail {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
color: @color-text-light-0;
strong { text-transform: uppercase; }
}
}
}
.combat-difficulty-options {
flex-wrap: wrap;
gap: 8px;
.difficulty-option {
display: flex;
align-items: center;
gap: 4px;
font-family: "Roboto", sans-serif;
text-transform: none;
font-size: @font-size-12;
color: @color-text-light-0;
cursor: pointer;
input[type="radio"] { flex: none; }
&.locked {
opacity: 0.8;
font-style: italic;
}
}
}
.combat-targets {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
padding: 4px 2px;
.label {
font-size: @font-size-11;
color: @color-amber;
}
.combat-target-tag {
padding: 2px 8px;
font-size: @font-size-11;
font-family: "Roboto", sans-serif;
color: @color-text-light-0;
background: fade(@color-hemolymph, 20%);
border: 1px solid fade(@color-hemolymph, 50%);
border-radius: 3px;
}
}
// ── Phase d'Expérience ───────────────────────────────────────────
.exp-phase-characters {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
min-height: 0;
details > div:not(summary) {
padding: 8px;
background: fade(@color-chitin-dark, 30%);
}
}
// ── Dialogue d'apprentissage ─────────────────────────────────────
.learning-summary {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border: 1px solid @color-hemolymph;
background: fade(@color-hemolymph, 10%);
.label {
font-size: @font-size-12;
color: @color-amber;
}
.exp-value {
font-family: "DistressBlack", sans-serif;
font-size: @font-size-20;
color: @color-acid-green;
}
}
.learning-type {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 6px 2px;
label {
display: flex;
align-items: center;
gap: 4px;
font-family: "Roboto", sans-serif;
text-transform: none;
font-size: @font-size-12;
color: @color-text-light-0;
cursor: pointer;
input[type="radio"] { flex: none; }
&:hover { color: @color-amber; }
}
}
.learning-targets {
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px;
border: 1px solid @color-border-dark-4;
background: fade(@color-chitin-dark, 30%);
.learning-hint {
margin: 0;
font-size: @font-size-11;
font-style: italic;
color: @color-text-light-0;
opacity: 0.75;
}
}
.learning-result {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
padding: 8px;
border: 1px solid @color-amber;
background: fade(@color-amber, 8%);
.context-item {
display: flex;
align-items: center;
gap: 6px;
.context-label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
color: @color-amber;
}
.context-value {
font-family: "Roboto", sans-serif;
font-size: @font-size-13;
font-weight: 700;
color: @color-text-light-0;
}
}
}
// ── Transfert de Dés d'Expérience ─────────────────────────────────
.experience-section {
.experience-controls {
gap: 8px;
padding: 8px;
.experience-group {
font-size: @font-size-11;
color: @color-text-light-0;
opacity: 0.8;
#exp-group {
font-weight: 700;
color: @color-acid-green;
}
}
button[data-action="giveToGroup"] {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
padding: 3px 12px;
cursor: pointer;
color: @color-text-light-0;
background: fade(@color-acid-green, 15%);
border: 1px solid @color-acid-green;
border-radius: 3px;
.transition();
&:hover:not(:disabled) {
color: @theme-color-dark;
background: @color-acid-green;
}
&:disabled {
opacity: 0.6;
cursor: default;
}
}
}
}
// ── Total dice pool section ─────────────────────────────────────
.total-section {
align-items: center;
@@ -30,8 +30,10 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
toggleSheet: VermineBaseActorSheet.#onToggleSheet,
edit: VermineBaseActorSheet.#onItemEdit,
delete: VermineBaseActorSheet.#onItemDelete,
toggleEquip: VermineBaseActorSheet.#onToggleEquip,
create: VermineBaseActorSheet.#onItemCreate,
roll: VermineBaseActorSheet.#onRollItem,
attack: VermineBaseActorSheet.#onAttack,
clickRadio: VermineBaseActorSheet.#onClickRadioHexa,
effectControl: VermineBaseActorSheet.#onEffectControl,
chooseTotem: VermineBaseActorSheet.#onChooseTotem
@@ -237,7 +239,8 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
const dialog = await RollDialog.create({
actorId: this.document.id,
rolltype: "skill",
label: item.system.skill
label: item.system.skill,
weapon: item
})
if (dialog) dialog.render(true)
return
@@ -245,6 +248,24 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
item?.roll()
}
static async #onAttack(event, target) {
const id = target.closest("[data-item-id]")?.dataset?.itemId
if (!id) return
const item = this.document.items.get(id)
if (!item) return
const { default: CombatDialog } = await import("../../system/dialogs/combatDialog.mjs")
const dialog = await CombatDialog.create({ actorId: this.document.id, weapon: item })
if (dialog) dialog.render(true)
}
static async #onToggleEquip(event, target) {
const id = target.closest("[data-item-id]")?.dataset?.itemId
if (!id) return
const item = this.document.items.get(id)
if (!item) return
await item.update({ "system.equipped": !item.system.equipped })
}
static #onClickRadioHexa(event, target) {
event.preventDefault()
event.stopPropagation()
+9 -19
View File
@@ -13,12 +13,12 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/creature-main.hbs" },
main: { template: "systems/vermine2047/templates/actor/appv2/creature-main.hbs", scrollable: [""] },
tabs: { template: "templates/generic/tab-navigation.hbs" },
info: { template: "systems/vermine2047/templates/actor/appv2/creature-info.hbs" },
stats: { template: "systems/vermine2047/templates/actor/appv2/creature-stats.hbs" },
combat: { template: "systems/vermine2047/templates/actor/appv2/creature-combat.hbs" },
effects: { template: "systems/vermine2047/templates/actor/appv2/creature-effects.hbs" }
info: { template: "systems/vermine2047/templates/actor/appv2/creature-info.hbs", scrollable: [""] },
stats: { template: "systems/vermine2047/templates/actor/appv2/creature-stats.hbs", scrollable: [""] },
combat: { template: "systems/vermine2047/templates/actor/appv2/creature-combat.hbs", scrollable: [""] },
effects: { template: "systems/vermine2047/templates/actor/appv2/creature-effects.hbs", scrollable: [""] }
}
tabGroups = { sheet: "info" }
@@ -94,24 +94,14 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
return context
}
/** @override - Les créatures n'ont pas de caractéristiques/compétences : on lance directement le pool d'attaque. */
/** @override - Les créatures n'ont pas de caractéristiques/compétences : on ouvre le dialogue d'attaque (pool calculé). */
async _onRoll(event) {
event.preventDefault()
const el = event.currentTarget
const type = el.dataset.type
if (type !== "creature-attack") return super._onRoll(event)
const pool = this.document.system.computed?.attack ?? 0
if (!pool) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_select_ability"))
return
}
const difficulty = this.document.system.combatStatus?.difficulty ?? 7
const { VermineUtils } = await import("../../system/roll.mjs")
await VermineUtils.roll({
actor: this.document,
NoD: pool,
difficulty: parseInt(difficulty, 10),
rollLabel: game.i18n.localize("ADVERSITY.attack")
})
const { default: CombatDialog } = await import("../../system/dialogs/combatDialog.mjs")
const dialog = await CombatDialog.create({ actorId: this.document.id })
if (dialog) dialog.render(true)
}
}
+5 -5
View File
@@ -16,12 +16,12 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/group-main.hbs" },
main: { template: "systems/vermine2047/templates/actor/appv2/group-main.hbs", scrollable: [""] },
tabs: { template: "templates/generic/tab-navigation.hbs" },
info: { template: "systems/vermine2047/templates/actor/appv2/group-info.hbs" },
gear: { template: "systems/vermine2047/templates/actor/appv2/group-gear.hbs" },
road: { template: "systems/vermine2047/templates/actor/appv2/group-road.hbs" },
reserve: { template: "systems/vermine2047/templates/actor/appv2/group-reserve.hbs" }
info: { template: "systems/vermine2047/templates/actor/appv2/group-info.hbs", scrollable: [""] },
gear: { template: "systems/vermine2047/templates/actor/appv2/group-gear.hbs", scrollable: [""] },
road: { template: "systems/vermine2047/templates/actor/appv2/group-road.hbs", scrollable: [""] },
reserve: { template: "systems/vermine2047/templates/actor/appv2/group-reserve.hbs", scrollable: [""] }
}
tabGroups = { sheet: "info" }
+12 -12
View File
@@ -3,71 +3,71 @@ import { VermineBaseItemSheet } from "./base-item-sheet.mjs"
// ── Item générique ────────────────────────────────────────────────────
export class VermineItemSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["item-gear"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-item-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-item-sheet.hbs", scrollable: [""] } }
}
// ── Arme ──────────────────────────────────────────────────────────────
export class VermineWeaponSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["weapon"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-weapon-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-weapon-sheet.hbs", scrollable: [""] } }
}
// ── Défense ───────────────────────────────────────────────────────────
export class VermineDefenseSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["defense"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-defense-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-defense-sheet.hbs", scrollable: [""] } }
}
// ── Véhicule ──────────────────────────────────────────────────────────
export class VermineVehicleSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["vehicle"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-vehicle-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-vehicle-sheet.hbs", scrollable: [""] } }
}
// ── Capacité ──────────────────────────────────────────────────────────
export class VermineAbilitySheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["ability"], position: { width: 560 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-ability-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-ability-sheet.hbs", scrollable: [""] } }
}
// ── Spécialité ────────────────────────────────────────────────────────
export class VermineSpecialtySheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["specialty"], position: { width: 400 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-specialty-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-specialty-sheet.hbs", scrollable: [""] } }
}
// ── Historique ────────────────────────────────────────────────────────
export class VermineBackgroundSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["background"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-background-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-background-sheet.hbs", scrollable: [""] } }
}
// ── Traumatisme ───────────────────────────────────────────────────────
export class VermineTraumaSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["trauma"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-trauma-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-trauma-sheet.hbs", scrollable: [""] } }
}
// ── Évolution ─────────────────────────────────────────────────────────
export class VermineEvolutionSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["evolution"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-evolution-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-evolution-sheet.hbs", scrollable: [""] } }
}
// ── Rumeur ────────────────────────────────────────────────────────────
export class VermineRumorSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["rumor"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rumor-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rumor-sheet.hbs", scrollable: [""] } }
}
// ── Cible ─────────────────────────────────────────────────────────────
export class VermineTargetSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["target"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-target-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-target-sheet.hbs", scrollable: [""] } }
}
// ── Rite ──────────────────────────────────────────────────────────────
export class VermineRiteSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["rite"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rite-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rite-sheet.hbs", scrollable: [""] } }
}
+15 -6
View File
@@ -13,13 +13,14 @@ export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/npc-main.hbs" },
main: { template: "systems/vermine2047/templates/actor/appv2/npc-main.hbs", scrollable: [""] },
tabs: { template: "templates/generic/tab-navigation.hbs" },
characteristics: { template: "systems/vermine2047/templates/actor/appv2/npc-characteristics.hbs" },
skills: { template: "systems/vermine2047/templates/actor/appv2/npc-skills.hbs" },
threat: { template: "systems/vermine2047/templates/actor/appv2/npc-threat.hbs" },
combat: { template: "systems/vermine2047/templates/actor/appv2/npc-combat.hbs" },
notes: { template: "systems/vermine2047/templates/actor/appv2/npc-notes.hbs" }
characteristics: { template: "systems/vermine2047/templates/actor/appv2/npc-characteristics.hbs", scrollable: [""] },
skills: { template: "systems/vermine2047/templates/actor/appv2/npc-skills.hbs", scrollable: [""] },
equipment: { template: "systems/vermine2047/templates/actor/appv2/npc-equipment.hbs", scrollable: [""] },
threat: { template: "systems/vermine2047/templates/actor/appv2/npc-threat.hbs", scrollable: [""] },
combat: { template: "systems/vermine2047/templates/actor/appv2/npc-combat.hbs", scrollable: [""] },
notes: { template: "systems/vermine2047/templates/actor/appv2/npc-notes.hbs", scrollable: [""] }
}
tabGroups = { sheet: "characteristics" }
@@ -28,6 +29,7 @@ export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
const tabs = {
characteristics: { id: "characteristics", group: "sheet", icon: "fas fa-dice", label: "VERMINE.abilities" },
skills: { id: "skills", group: "sheet", icon: "fas fa-brain", label: "VERMINE.skills" },
equipment: { id: "equipment", group: "sheet", icon: "fas fa-hammer", label: "VERMINE.tabs.equipment" },
threat: { id: "threat", group: "sheet", icon: "fas fa-exclamation-triangle", label: "ADVERSITY.threat" },
combat: { id: "combat", group: "sheet", icon: "fas fa-sword", label: "VERMINE.combat" },
notes: { id: "notes", group: "sheet", icon: "fas fa-sticky-note", label: "IDENTITY.notes" }
@@ -61,11 +63,18 @@ export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
case "skills":
context.tab = context.tabs.skills
break
case "equipment":
context.tab = context.tabs.equipment
context.weapons = doc.itemTypes.weapon
context.defenses = doc.itemTypes.defense
break
case "threat":
context.tab = context.tabs.threat
break
case "combat":
context.tab = context.tabs.combat
context.equippedWeapons = doc.itemTypes.weapon.filter(i => i.system.equipped)
context.equippedDefenses = doc.itemTypes.defense.filter(i => i.system.equipped)
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
context.effects = prepareActiveEffectCategories(doc.effects)
break
+1
View File
@@ -15,6 +15,7 @@ export default class VermineDefenseData extends foundry.abstract.TypeDataModel {
return {
...baseItemSchema(),
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
equipped: new fields.BooleanField({ required: true, initial: false }),
specificLevel: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "" }),
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
+1
View File
@@ -15,6 +15,7 @@ export default class VermineWeaponData extends foundry.abstract.TypeDataModel {
return {
...baseItemSchema(),
skill: new fields.StringField({ required: true, nullable: false, initial: "" }),
equipped: new fields.BooleanField({ required: true, initial: false }),
min_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
damage: new fields.SchemaField({
+399
View File
@@ -0,0 +1,399 @@
import { VermineUtils } from "../roll.mjs";
import { VermineExchange } from "../exchange.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
/**
* Dialogue d'attaque pour les échanges de coups.
* Prépare le pool, la difficulté (contact / distance) et le contexte
* d'échange (cibles, dégâts de base) selon le type d'acteur :
* - Personnage : capacité + compétence de l'arme.
* - PNJ : pool d'attaque du niveau de menace + réserves de rôle.
* - Créature : pool d'attaque calculé, difficulté du statut de combat.
*/
export default class CombatDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
static DEFAULT_OPTIONS = {
classes: ["vermine-roll"],
tag: "form",
window: {
icon: "fas fa-crosshairs",
resizable: false
},
position: {
width: 520,
height: 600
},
actions: {
roll: CombatDialog.#onRoll,
cancel: CombatDialog.#onCancel
}
};
static PARTS = {
main: { template: "systems/vermine2047/templates/dialogs/combat-dialog.hbs" }
};
static async create({ actorId, weapon = null }) {
const actor = await game.actors.get(actorId);
if (!actor) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
return null;
}
const mode = actor.type === "creature" ? "contact" : VermineExchange.modeForWeapon(weapon);
return new CombatDialog({ actor, weapon, mode });
}
constructor(options = {}) {
super(options);
this.actor = options.actor;
this.weapon = options.weapon ?? null;
this.mode = options.mode ?? "contact";
this.abilityKey = VermineExchange.attackAbility(this.mode);
this.skillKey = this.weapon?.system?.skill || null;
this.skillValue = this.skillKey ? (this.actor.system?.skills?.[this.skillKey]?.value || 0) : 0;
this.skillPool = this.skillValue ? (CONFIG.VERMINE.SkillLevels?.[this.skillValue]?.dicePool || 0) : 0;
this.skillReroll = this.skillValue ? (CONFIG.VERMINE.SkillLevels?.[this.skillValue]?.reroll || 0) : 0;
this.basePool = VermineExchange.baseAttackPool(this.actor, this.weapon, this.mode);
this.baseDamage = VermineExchange.baseDamage(this.actor, this.weapon);
}
get title() {
return game.i18n.localize("VERMINE.attack");
}
async _prepareContext() {
const actor = this.actor;
const isCharacter = actor.type === "character";
const isNpc = actor.type === "npc";
const isCreature = actor.type === "creature";
const targets = [...game.user.targets].map(t => ({
id: t.actor?.id ?? null,
uuid: t.actor?.uuid ?? null,
name: t.name,
canDefend: t.actor?.type !== "creature"
}));
let difficultyOptions = [];
let lockedDifficulty = null;
let defaultDifficulty = null;
if (isCreature) {
const d = parseInt(actor.system?.combatStatus?.difficulty, 10) || 9;
lockedDifficulty = d;
difficultyOptions = [{ difficulty: d, label: "", locked: true }];
defaultDifficulty = d;
} else if (this.mode === "ranged") {
difficultyOptions = VermineExchange.rangeDifficultyOptions().map(o => ({
difficulty: o.difficulty,
label: game.i18n.localize(o.label),
rangeKey: o.key
}));
defaultDifficulty = difficultyOptions[0]?.difficulty ?? 7;
} else if (isNpc) {
difficultyOptions = VermineExchange.npcDifficultyOptions(actor).map(d => ({
difficulty: d,
label: String(d)
}));
defaultDifficulty = difficultyOptions[0]?.difficulty ?? 7;
} else {
difficultyOptions = VermineExchange.contactDifficultyOptions(this.skillValue).map(d => ({
difficulty: d,
label: String(d)
}));
defaultDifficulty = difficultyOptions[0]?.difficulty ?? 7;
}
// Cible de type créature : défense passive, la difficulté d'attaque est
// forcée au statut de combat de la créature (toute réussite ≥1 touche).
const creatureTargets = targets.filter(t => t.id && !t.canDefend);
const creatureLocked = !isCreature && creatureTargets.length && creatureTargets.length === targets.length;
if (creatureLocked) {
const ref = creatureTargets[0];
const creature = ref.id ? game.actors.get(ref.id) : null;
const actor = creature ?? (ref.uuid ? await fromUuid(ref.uuid) : null);
const d = parseInt(actor?.system?.combatStatus?.difficulty, 10) || 7;
lockedDifficulty = d;
difficultyOptions = [{ difficulty: d, label: "", locked: true }];
defaultDifficulty = d;
}
const threat = actor.system?.threat?.value || 1;
const role = actor.system?.role?.value || 1;
return {
actor,
system: actor.system,
config: CONFIG.VERMINE,
actorType: actor.type,
mode: this.mode,
weapon: this.weapon,
abilityKey: this.abilityKey,
abilityValue: actor.system?.abilities?.[this.abilityKey]?.value || 0,
skillKey: this.skillKey,
skillValue: this.skillValue,
basePool: this.basePool,
baseDamage: this.baseDamage,
isCharacter,
isNpc,
isCreature,
creatureLocked,
npcThreatPool: isNpc ? (CONFIG.VERMINE.npcThreatLevels?.[threat]?.attack || 0) : 0,
npcPoolMax: isNpc ? (CONFIG.VERMINE.npcRoleLevels?.[role]?.pools || 0) : 0,
difficultyOptions,
lockedDifficulty,
defaultDifficulty,
hasAmmo: this.mode === "ranged" && (this.weapon?.system?.ammo ?? 0) > 0,
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
availableItems: actor.items.filter(i => i.type === "item"),
targets
};
}
async _onRender(context, options) {
this.element.dataset.actorId = this.actor.id;
this.element.dataset.weaponId = this.weapon?.id ?? "";
this.element.dataset.mode = this.mode;
for (const inp of this.element.querySelectorAll("[data-roll]")) {
inp.addEventListener("change", this.#onInputChange.bind(this));
}
const selfControl = this.element.querySelector("#self_control");
if (selfControl) {
selfControl.addEventListener("change", this.#onChangeSelfControl.bind(this));
}
this.element.querySelector("#second-action")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#human-totem")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#adapted-totem")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#pools-spent")?.addEventListener("change", () => this.#updateUI());
const ability = this.element.querySelector("#ability");
if (ability && this.actor.type === "character") {
const sc = this.element.querySelector("#self_control");
if (sc) sc.max = ability.value;
}
this.#updateUI();
}
// ── Getters ──────────────────────────────────────────────────────────
get #el() { return this.element; }
#getDifficultyBase() {
const el = this.#el.querySelector('input[name="combat-difficulty"]:checked');
return parseInt(el?.value, 10) || 7;
}
#getSecondAction() {
return this.#el.querySelector("#second-action")?.checked ?? false;
}
#getHandicap() {
const sel = this.#el.querySelector("#handicap");
return Math.max(0, (parseInt(sel?.value, 10) || 1) - 1);
}
#getSelfCtrl() {
return parseInt(this.#el.querySelector("#self_control")?.value, 10) || 0;
}
#getPoolsSpent() {
return parseInt(this.#el.querySelector("#pools-spent")?.value, 10) || 0;
}
#getUseAmmo() {
return this.#el.querySelector("#use-ammo")?.checked ?? false;
}
#getTotems() {
return {
human: this.#el.querySelector("#human-totem")?.checked ?? false,
adapted: this.#el.querySelector("#adapted-totem")?.checked ?? false
};
}
#getKeepTotem() {
return this.#el.querySelector("#keep-totem-select")?.value ?? null;
}
getDifficultySelect() {
return this.#getDifficultyBase() + (this.#getSecondAction() ? 2 : 0);
}
getSkillCategory() {
if (this.actor.type !== "character" || !this.skillKey) return null;
return this.actor.system.skills[this.skillKey]?.category ?? null;
}
getSkillLevel() {
return this.actor.type === "character" && this.skillValue ? this.skillValue : null;
}
getReroll() {
if (this.actor.type === "npc") {
const exp = this.actor.system?.experience?.value || 1;
return CONFIG.VERMINE.npcExperienceLevels?.[exp]?.rerolls || 0;
}
if (this.actor.type === "creature") return 0;
return this.skillReroll;
}
getMaxEffort() {
if (this.actor.type === "character") return this.actor.system?.abilities?.[this.abilityKey]?.value || 0;
return 0;
}
getPoolBreakdown() {
const ability = this.actor.type === "character"
? (this.actor.system?.abilities?.[this.abilityKey]?.value || 0)
: 0;
const skill = this.actor.type === "character" ? this.skillPool : 0;
const selfControl = this.#getSelfCtrl();
const pools = this.actor.type === "npc" ? this.#getPoolsSpent() : 0;
const specialty = this.#hasSpecialtySelected() ? 1 : 0;
const help = this.#el.querySelector("#helped")?.checked ? 1 : 0;
const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked");
const tooling = toolsChecked && toolsChecked.value !== "0" ? 1 : 0;
const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
const total = this.basePool + selfControl + pools + specialty + help + tooling + group;
return { ability, skill, selfControl, pools, specialty, help, tooling, group, total };
}
getDicePool() {
return this.getPoolBreakdown().total;
}
#hasSpecialtySelected() {
const checked = this.#el.querySelector("input[name='usingSpecialization']:checked");
return Boolean(checked && checked.value !== "aucune");
}
getSpecialtyName() {
const checked = this.#el.querySelector("input[name='usingSpecialization']:checked");
return checked && checked.value !== "aucune" ? checked.value : null;
}
getLabel() {
const base = this.mode === "ranged"
? game.i18n.localize("VERMINE.attack_ranged")
: game.i18n.localize("VERMINE.attack_contact");
return this.weapon ? `${base} (${this.weapon.name})` : base;
}
// ── UI ───────────────────────────────────────────────────────────────
#updateUI() {
const total = this.getDicePool();
const totalEl = this.#el.querySelector("#dice-pool-total");
if (totalEl) totalEl.textContent = `${total}D`;
const finalEls = this.#el.querySelectorAll("#final-difficulty");
for (const el of finalEls) el.textContent = this.getDifficultySelect();
const b = this.getPoolBreakdown();
const bonusEl = this.#el.querySelector("#total-bonus");
if (bonusEl) bonusEl.textContent = b.selfControl + b.pools + b.specialty + b.help + b.tooling + b.group;
const handSel = this.#el.querySelector("#handicap");
const handEl = this.#el.querySelector("#current-handicap");
if (handSel && handEl) {
handEl.textContent = handSel.options[handSel.selectedIndex]?.text ?? "";
}
}
#onInputChange() {
this.#updateUI();
}
#onChangeSelfControl(ev) {
const valEl = this.#el.querySelector("#self_control_value");
if (valEl) valEl.textContent = ev.currentTarget.value;
this.#updateUI();
}
static async #onCancel(event, target) {
this.close();
}
static async #onRoll(event, target) {
const selfCtrl = this.getPoolBreakdown().selfControl;
if (selfCtrl > 0) {
const current = this.actor?.system?.attributes?.self_control?.value ?? 0;
if (current < selfCtrl) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_not_enough_self_control"));
return;
}
}
const finalDifficulty = this.getDifficultySelect();
// Consommation de munitions (armes à distance)
if (this.mode === "ranged" && this.#getUseAmmo() && this.weapon) {
const ammo = this.weapon.system.ammo || 0;
if (ammo > 0) {
await this.weapon.update({ "system.ammo": ammo - 1 });
}
}
if (selfCtrl > 0) {
const newVal = this.actor.system.attributes.self_control.value - selfCtrl;
await this.actor.update({ "system.attributes.self_control.value": newVal });
}
const targets = [...game.user.targets].map(t => ({
id: t.actor?.id ?? null,
uuid: t.actor?.uuid ?? null,
name: t.name,
canDefend: t.actor?.type !== "creature"
}));
const attack = {
id: foundry.utils.randomID(),
attackerId: this.actor.id,
attackerName: this.actor.name,
mode: this.mode,
difficulty: finalDifficulty,
range: this.mode === "ranged" ? this.#getRangeKey() : null,
secondAction: this.#getSecondAction(),
baseDamage: this.baseDamage,
targets,
weapon: this.weapon
? { name: this.weapon.name, img: this.weapon.img, damage: foundry.utils.duplicate(this.weapon.system.damage) }
: null
};
await VermineUtils.roll({
actor: this.actor,
NoD: this.getDicePool(),
Reroll: this.getReroll(),
difficulty: finalDifficulty,
handicap: this.#getHandicap(),
rollLabel: this.getLabel(),
totems: this.#getTotems(),
self_control: selfCtrl,
max_effort: this.getMaxEffort(),
keepTotem: this.#getKeepTotem(),
skillCategory: this.getSkillCategory(),
skillLevel: this.getSkillLevel(),
hasSpecialty: this.#hasSpecialtySelected(),
poolBreakdown: this.getPoolBreakdown(),
specialtyName: this.getSpecialtyName(),
weapon: this.weapon?.toObject() ?? null,
targets: targets.map(t => t.name),
attack,
messageFlags: { "vermine-exchange": attack }
});
this.close();
}
#getRangeKey() {
const el = this.#el.querySelector('input[name="combat-difficulty"]:checked');
return el?.dataset?.rangeKey ?? null;
}
}
+489
View File
@@ -0,0 +1,489 @@
/**
* Moteur de résolution des échanges de coups.
*
* Gère l'attaque (contact / distance), la défense (esquive / parade),
* le calcul des dégâts, la déduction des protections et l'application
* des blessures pour les personnages, PNJ et créatures.
*
* Le flux est assisté : les cartes de chat calculent et affichent tout,
* un bouton "Appliquer" coche la blessure sur l'acteur cible.
*
* Liens entre messages (flags "world") :
* - "vermine-exchange" sur le message d'attaque
* - "vermine-defense" sur le message de défense
*/
const FLAG_EXCHANGE = 'vermine-exchange'
const FLAG_DEFENSE = 'vermine-defense'
export class VermineExchange {
// ── Tables de difficulté ────────────────────────────────────────────
/**
* Capacité d'attaque selon le mode.
* @param {string} mode 'contact' ou 'ranged'
* @returns {string} clé de capacité (vigor / precision)
*/
static attackAbility(mode) {
return mode === 'ranged' ? 'precision' : 'vigor'
}
/**
* Difficultés d'attaque au contact selon le niveau de compétence (0-5).
* Incompétent/Débutant = 7, Confirmé = 5/7, Expert = 5/7/9,
* Maître/Légende = 3/5/7/9.
* @param {number} skillLevel valeur de compétence (0-5)
* @returns {number[]}
*/
static contactDifficultyOptions(skillLevel) {
switch (parseInt(skillLevel, 10) || 0) {
case 2: return [5, 7]
case 3: return [5, 7, 9]
case 4:
case 5: return [3, 5, 7, 9]
default: return [7]
}
}
/**
* Difficultés d'attaque à distance par portée.
* @returns {{key: string, label: string, difficulty: number}[]}
*/
static rangeDifficultyOptions() {
return [
{ key: 'short', label: 'VERMINE.range_short', difficulty: 5 },
{ key: 'mid', label: 'VERMINE.range_mid', difficulty: 7 },
{ key: 'long', label: 'VERMINE.range_long', difficulty: 9 }
]
}
/**
* Difficultés proposées au PNJ selon son niveau d'expérience
* (champ "contact" : "7", "5 ou 7", "5,7 ou 9", "3,5,7 ou 9").
* @param {Actor} actor
* @returns {number[]}
*/
static npcDifficultyOptions(actor) {
const level = actor.system?.experience?.value || 1
const cfg = CONFIG.VERMINE.npcExperienceLevels?.[level] || {}
const raw = cfg.contact || '7'
return raw.split(',').flatMap(part => part.trim().split(/\s+ou\s+/).map(Number))
}
/**
* Mode d'attaque déduit de la compétence de l'arme.
* @param {Item|null} weapon
* @returns {string} 'contact' ou 'ranged'
*/
static modeForWeapon(weapon) {
const skill = weapon?.system?.skill || ''
return ['firearms', 'archery', 'throwing'].includes(skill) ? 'ranged' : 'contact'
}
// ── Pool et dégâts ──────────────────────────────────────────────────
/**
* Pool d'attaque de base (sans les bonus optionnels du dialogue).
* Personnage : caractéristique + pool de compétence.
* PNJ : pool d'attaque du niveau de menace.
* Créature : pool d'attaque calculé.
* @param {Actor} actor L'attaquant
* @param {Item|null} weapon
* @param {string} mode
* @returns {number}
*/
static baseAttackPool(actor, weapon, mode) {
const type = actor.type
if (type === 'creature') {
return actor.system?.computed?.attack || 0
}
if (type === 'npc') {
const threat = actor.system?.threat?.value || 1
return CONFIG.VERMINE.npcThreatLevels?.[threat]?.attack || 0
}
const abilityKey = this.attackAbility(mode)
const ability = actor.system?.abilities?.[abilityKey]?.value || 0
const skillKey = weapon?.system?.skill
const skillValue = skillKey ? actor.system?.skills?.[skillKey]?.value || 0 : 0
const skillPool = skillValue ? CONFIG.VERMINE.SkillLevels?.[skillValue]?.dicePool || 0 : 0
return ability + skillPool
}
/**
* Dégâts de base de l'arme (hors réussites de l'attaque).
* Le trait "Vigueur+" ajoute la vigueur de l'attaquant.
* PNJ sans arme et créatures utilisent leurs dégâts propres.
* @param {Actor} actor L'attaquant
* @param {Item|null} weapon
* @returns {number}
*/
static baseDamage(actor, weapon) {
const type = actor.type
if (type === 'creature') {
return actor.system?.computed?.damage || 0
}
if (weapon?.system?.damage) {
let base = weapon.system.damage.value || 0
if (weapon.system.damage.addVigor) base += this.#vigor(actor)
return base
}
return this.#vigor(actor)
}
static #vigor(actor) {
if (actor.type === 'npc') {
const threat = actor.system?.threat?.value || 1
return CONFIG.VERMINE.npcThreatLevels?.[threat]?.vigor || 0
}
if (actor.type === 'creature') return actor.system?.computed?.vigor || 0
return actor.system?.abilities?.vigor?.value || 0
}
// ── Protection et blessures ─────────────────────────────────────────
/**
* Protection du défenseur.
* Parade : seule la protection utilisée compte.
* Sinon : somme des protections équipées (créature : protection de rôle).
* @param {Actor} defender
* @param {number|null} parryLevel Protection utilisée lors d'une parade
* @returns {number}
*/
static protectionTotal(defender, parryLevel = null) {
if (parryLevel !== null) return Math.max(0, parryLevel)
if (defender.type === 'creature') {
return defender.system?.computed?.protection || 0
}
const defenses = defender.itemTypes?.defense?.filter(d => d.system.equipped) || []
return defenses.reduce((sum, d) => sum + (d.system.level || 0), 0)
}
/**
* Catégorie de blessure : le seuil le plus élevé inférieur ou égal
* aux dégâts nets subis.
* @param {number} netDamage
* @param {Actor} defender
* @returns {string|null} 'minorWound', 'majorWound', 'deadlyWound' ou null
*/
static highestWound(netDamage, defender) {
const sys = defender.system
const deadly = sys?.deadlyWound?.threshold ?? Number.POSITIVE_INFINITY
const major = sys?.majorWound?.threshold ?? Number.POSITIVE_INFINITY
const minor = sys?.minorWound?.threshold ?? Number.POSITIVE_INFINITY
if (netDamage >= deadly) return 'deadlyWound'
if (netDamage >= major) return 'majorWound'
if (netDamage >= minor) return 'minorWound'
return null
}
/**
* Clé i18n du libellé d'une catégorie de blessure.
* @param {string|null} category
* @returns {string}
*/
static woundLabelKey(category) {
if (category === 'deadlyWound') return 'VERMINE.wounds.deadly_wounds'
if (category === 'majorWound') return 'VERMINE.wounds.heavy_wounds'
if (category === 'minorWound') return 'VERMINE.wounds.light_wounds'
return 'VERMINE.no_wound'
}
/**
* Vérifie que l'utilisateur peut appliquer une blessure sur l'acteur.
* @param {Actor} actor
* @returns {boolean}
*/
static canApplyWound(actor) {
return Boolean(actor) && (game.user.isGM || actor.isOwner)
}
/**
* Applique +1 dans la catégorie de blessure indiquée (plafonné au max).
* @param {Actor} actor
* @param {string} woundCategory 'minorWound' | 'majorWound' | 'deadlyWound'
* @returns {Promise<number|boolean>} nouvelle valeur, ou false si refusé
*/
static async applyWound(actor, woundCategory) {
if (!actor || !woundCategory) return null
if (!this.canApplyWound(actor)) return false
const current = actor.system?.[woundCategory]?.value || 0
const max = actor.system?.[woundCategory]?.max ?? Number.POSITIVE_INFINITY
const next = Math.min(current + 1, max)
await actor.update({ [`system.${woundCategory}.value`]: next })
return next
}
// ── Résolution ──────────────────────────────────────────────────────
/**
* Résout l'échange pour une cible donnée (défense la plus récente liée
* à cette cible, ou abstention si aucune défense).
* @param {ChatMessage} attackMessage
* @param {string|null} targetId id d'acteur de la cible (défaut : première cible)
* @returns {Promise<Object|null>} résultat de la résolution
*/
static async resolve(attackMessage, targetId = null) {
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
if (!exchange) return null
const target = (targetId && exchange.targets?.find(t => t.id === targetId)) || exchange.targets?.[0] || null
if (!target) return null
const defs = this.#defenseMessagesFor(exchange.id, target.id)
const defenderMessage = defs[defs.length - 1]
const defense = defenderMessage ? defenderMessage.getFlag('world', FLAG_DEFENSE) : null
const attackerSuccesses = this.#messageTotal(attackMessage)
const defenderSuccesses = defense ? this.#messageTotal(defenderMessage) : 0
const hit = attackerSuccesses > defenderSuccesses
// Le flag de défense porte actorId/actorUuid (clés différentes de celles
// des cibles) : on normalise la référence avant résolution de l'acteur.
const defenderRef = defense
? { id: defense.actorId ?? null, uuid: defense.actorUuid ?? null, name: defense.actorName ?? '' }
: target
const defender = await this.#resolveDefender(defenderRef)
const damage = (exchange.baseDamage || 0) + attackerSuccesses
const parryLevel = defense?.type === 'parade' ? (defense.parryLevel ?? 0) : null
const protection = hit && parryLevel !== null
? parryLevel
: (defender ? this.protectionTotal(defender, hit ? null : parryLevel) : 0)
const netDamage = hit ? Math.max(0, damage - protection) : 0
const woundCategory = hit && defender ? this.highestWound(netDamage, defender) : null
return {
exchangeId: exchange.id,
defenderId: defenderRef.id ?? null,
defenderUuid: defenderRef.uuid ?? null,
defenderName: defense?.actorName || target.name || '',
defenseType: defense?.type || null,
attackerSuccesses,
defenderSuccesses,
hit,
damage,
protection,
netDamage,
woundCategory,
woundThreshold: woundCategory && defender ? defender.system[woundCategory].threshold : null,
canApply: Boolean(defender) && this.canApplyWound(defender)
}
}
/**
* Rend le bloc de résolution (HTML) pour insertion dans le message.
* @param {Object} r Résultat de VermineExchange.resolve
* @returns {string}
*/
static renderResolution(r) {
const t = (key) => game.i18n.localize(key)
const verdict = !r.hit
? (r.defenseType === 'parade'
? `<span class="res-parried">${t('VERMINE.missed_parade')}</span>`
: `<span class="res-dodged">${t('VERMINE.missed_esquive')}</span>`)
: (r.defenseType === 'parade'
? `<span class="res-hit">${t('VERMINE.hit_parade')}</span>`
: `<span class="res-hit">${t('VERMINE.hit')}</span>`)
const lines = [
`<div class="resolution-line">${t('VERMINE.attacker_successes')}: ${r.attackerSuccesses} &middot; ${t('VERMINE.defender_successes')}: ${r.defenderSuccesses} &rarr; ${verdict}</div>`
]
if (r.hit) {
lines.push(`<div class="resolution-line">${t('VERMINE.raw_damage')}: <strong>${r.damage}</strong>${r.protection ? ` &minus; ${t('ADVERSITY.protection')}: ${r.protection}` : ''} = ${t('VERMINE.net_damage')}: <strong>${r.netDamage}</strong></div>`)
if (r.woundCategory) {
lines.push(`<div class="resolution-line res-wound">${t('VERMINE.wound')}: <strong>${t(this.woundLabelKey(r.woundCategory))}</strong> (${t('VERMINE.wounds.threshold')} ${r.woundThreshold}D)</div>`)
} else {
lines.push(`<div class="resolution-line res-noshock">${t('VERMINE.no_wound')}</div>`)
}
} else {
lines.push(`<div class="resolution-line res-nodamage">${t('VERMINE.no_damage')}</div>`)
}
const apply = (r.hit && r.woundCategory && r.canApply)
? `<button type="button" class="exchange-apply" data-action="exchange-apply" data-defender-id="${r.defenderId}" data-defender-uuid="${r.defenderUuid}" data-wound="${r.woundCategory}"><i class="fas fa-briefcase-medical"></i> ${t('VERMINE.apply_wound')}</button>`
: ''
return `<div class="exchange-resolution">
<div class="resolution-header"><i class="fas fa-exchange-alt"></i> ${t('VERMINE.resolve_result')}</div>
${lines.join('')}
${apply}
</div>`
}
// ── Liaison chat ────────────────────────────────────────────────────
/**
* Lie les interactions d'échange de coups sur un message de chat.
* @param {HTMLElement} html élément du message
* @param {ChatMessage} message
*/
static bindChat(html, message) {
const exchange = message.getFlag('world', FLAG_EXCHANGE)
if (exchange) {
html.querySelectorAll('.exchange-defend').forEach(btn => {
btn.addEventListener('click', ev => this.#onDefense(ev, message))
})
html.querySelectorAll('.exchange-resolve').forEach(btn => {
btn.addEventListener('click', ev => this.#onResolve(ev, message))
})
}
html.querySelectorAll('[data-action="exchange-apply"]').forEach(btn => {
btn.addEventListener('click', ev => this.#onApply(ev, message))
})
}
static async #onDefense(ev, attackMessage) {
ev.preventDefault()
ev.stopPropagation()
const btn = ev.currentTarget
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
if (!exchange) return
const type = btn.dataset.type
const actor = await this.#resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
if (!actor) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
return
}
if (!game.user.isGM && !actor.isOwner) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
return
}
if (actor.type === 'creature') {
ui.notifications.warn(game.i18n.localize('VERMINE.error_creature_no_defense'))
return
}
const { default: RollDialog } = await import('./dialogs/rollDialog.mjs')
const locks = this.#defenseLocks(actor, exchange, type)
const dialog = await RollDialog.create({
actor,
rolltype: 'skill',
label: locks.skill,
locks,
defense: {
exchangeId: exchange.id,
type,
actorId: actor.id,
actorUuid: actor.uuid,
actorName: actor.name,
difficulty: exchange.difficulty,
parryLevel: type === 'parade' ? this.#parryLevel(actor) : null
}
})
if (dialog) dialog.render(true)
}
static #defenseLocks(actor, exchange, type) {
if (type === 'parade') {
return { ability: 'vigor', skill: this.#parrySkill(actor), difficulty: exchange.difficulty }
}
return {
ability: 'reflexes',
skill: exchange.mode === 'ranged' ? 'alertness' : 'close',
difficulty: exchange.difficulty
}
}
static #parrySkill(actor) {
const hasMelee = actor.itemTypes?.weapon?.some(w => w.system.equipped && w.system.skill === 'melee')
return hasMelee ? 'melee' : 'close'
}
static #parryLevel(actor) {
const defenses = actor.itemTypes?.defense?.filter(d => d.system.equipped) || []
return defenses.reduce((best, d) => Math.max(best, d.system.level || 0), 0)
}
static async #onResolve(ev, attackMessage) {
ev.preventDefault()
const btn = ev.currentTarget
if (!game.user.isGM && !attackMessage.isOwner) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
return
}
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
if (!exchange) return
if (!exchange.targets?.length) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_target'))
return
}
const wrapper = btn.closest('.exchange-actions')
const container = wrapper?.parentElement
if (!container) return
container.querySelectorAll('.exchange-resolution').forEach(el => el.remove())
const targets = exchange.targets?.length ? exchange.targets : [null]
for (const target of targets) {
const result = await this.resolve(attackMessage, target?.id ?? null)
if (!result) continue
const block = document.createElement('div')
block.innerHTML = this.renderResolution(result)
const node = block.firstElementChild
container.appendChild(node)
this.bindChat(node, attackMessage)
}
const msgEl = btn.closest('.vermine-roll-message')
if (msgEl) await attackMessage.update({ content: msgEl.outerHTML })
}
static async #onApply(ev, attackMessage) {
ev.preventDefault()
const btn = ev.currentTarget
const defender = await this.#resolveDefender({ id: btn.dataset.defenderId, uuid: btn.dataset.defenderUuid })
const wound = btn.dataset.wound
if (!defender || !wound) return
if (!this.canApplyWound(defender)) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
return
}
const next = await this.applyWound(defender, wound)
if (next === false) return
btn.disabled = true
btn.innerHTML = `<i class="fas fa-check"></i> ${game.i18n.localize('VERMINE.wound_applied')}`
// Persiste l'état du bouton seulement si l'utilisateur peut modifier le message
if (game.user.isGM || attackMessage.isOwner) {
const msgEl = btn.closest('.vermine-roll-message')
if (msgEl) await attackMessage.update({ content: msgEl.outerHTML })
}
}
// ── Helpers internes ────────────────────────────────────────────────
/**
* Résout un acteur depuis son id d'acteur monde (acteurs liés) ou son
* uuid (tokens non liés via fromUuid).
* @param {{id: string|null, uuid: string|null}} ref
* @returns {Promise<Actor|null>}
*/
static async #resolveDefender({ id = null, uuid = null }) {
if (id) {
const actor = game.actors.get(id)
if (actor) return actor
}
if (uuid) {
const doc = await fromUuid(uuid)
if (!doc) return null
if (doc.documentType === 'Actor') return doc
if (doc.documentType === 'Token') return doc.actor ?? null
}
return null
}
static #defenseMessagesFor(exchangeId, targetId = null) {
return game.messages.contents.filter(m => {
const f = m.getFlag('world', FLAG_DEFENSE)
if (!f || f.exchangeId !== exchangeId) return false
if (targetId) return f.actorId === targetId
return true
})
}
static #messageTotal(message) {
const el = document.createElement('div')
el.innerHTML = message.content || ''
const totalEl = el.querySelector('#total')
const val = parseInt(totalEl?.innerText, 10)
return Number.isNaN(val) ? 0 : val
}
}
+1
View File
@@ -12,6 +12,7 @@ export const preloadHandlebarsTemplates = async function () {
"systems/vermine2047/templates/actor/parts/actor-items.hbs",
"systems/vermine2047/templates/actor/parts/actor-weapons.hbs",
"systems/vermine2047/templates/actor/parts/actor-defenses.hbs",
"systems/vermine2047/templates/actor/parts/actor-equipped-gear.hbs",
// Character partials.
"systems/vermine2047/templates/actor/character/character-id.hbs",
+4
View File
@@ -41,6 +41,10 @@ export const registerHooks = function () {
});
Hooks.on('renderChatMessageHTML', async (message, html, data) => {
// Échanges de coups : liaisons ouvertes à tous (propriétaires de cibles inclus)
const { VermineExchange } = await import('./exchange.mjs');
VermineExchange.bindChat(html, message);
let rerollTitle = html.querySelector(".reroll-fromroll h4");
if (rerollTitle) {
rerollTitle.addEventListener("click", () => { html.querySelector(".reroll").classList.toggle('visible') })
+13 -2
View File
@@ -31,7 +31,11 @@ export class VermineUtils {
hasSpecialty = false,
handicap = 0,
poolBreakdown = null,
specialtyName = null
specialtyName = null,
weapon = null,
targets = [],
attack = null,
messageFlags = null
}) {
// Validate inputs
if (!actor) {
@@ -168,7 +172,11 @@ export class VermineUtils {
hasSpecialty,
handicap,
poolBreakdown,
specialtyName
specialtyName,
weapon,
targets,
attack,
messageFlags
});
return roll;
@@ -493,6 +501,9 @@ export class VermineUtils {
speaker: ChatMessage.getSpeaker(),
content: content
};
if (param.messageFlags) {
chatData.flags = { world: param.messageFlags };
}
const msg = await ChatMessage.create(chatData);
return msg;
}
+1
View File
@@ -180,6 +180,7 @@ Hooks.once('init', async function () {
"systems/vermine2047/templates/actor/appv2/npc-main.hbs",
"systems/vermine2047/templates/actor/appv2/npc-characteristics.hbs",
"systems/vermine2047/templates/actor/appv2/npc-skills.hbs",
"systems/vermine2047/templates/actor/appv2/npc-equipment.hbs",
"systems/vermine2047/templates/actor/appv2/npc-threat.hbs",
"systems/vermine2047/templates/actor/appv2/npc-combat.hbs",
"systems/vermine2047/templates/actor/appv2/npc-notes.hbs",
@@ -1,4 +1,4 @@
<div class="tab abilities sheet-part" data-group="sheet" data-tab="abilities">
<div class="tab abilities sheet-part {{#if @root.tabs.abilities.active}}active{{/if}}" data-group="sheet" data-tab="abilities">
{{!-- Character --}}
<h3>{{ localize 'VERMINE.abilities' }}</h3>
<div class="grid grid-4col">
+2 -11
View File
@@ -1,4 +1,4 @@
<div class="tab combat sheet-part" data-group="sheet" data-tab="combat">
<div class="tab combat sheet-part {{#if @root.tabs.combat.active}}active{{/if}}" data-group="sheet" data-tab="combat">
<div class="align-center">
<div class="shadow">
<h4>situation de combat par défaut</h4>
@@ -20,6 +20,7 @@
</div>
</div>
</div>
{{> "systems/vermine2047/templates/actor/parts/actor-equipped-gear.hbs"}}
<div class="flexrow">
<div class="self-control-container flexcol align-center">
<h4 class="align-center">
@@ -42,7 +43,6 @@
unavailable
{{/ifgt}}">
{{#iflteq @row @root.system.attributes.self_control.max }}
{{#if @root.isEditMode}}
<input
type="radio"
data-dtype="Number"
@@ -56,7 +56,6 @@
"
/>
{{/if}}
{{/iflteq}}
</div>
@@ -88,7 +87,6 @@
unavailable
{{/ifgt}}">
{{#iflteq @row @root.system.attributes.effort.max }}
{{#if @root.isEditMode}}
<input
type="radio"
data-dtype="Number"
@@ -102,7 +100,6 @@
"
/>
{{/if}}
{{/iflteq}}
</div>
@@ -124,7 +121,6 @@
<div class="hexa ability{{#iflteq @minorwoundmax @root.system.minorWound.value }}
checked
{{/iflteq}}">
{{#if @root.isEditMode}}
<input
type="radio"
data-dtype="Number"
@@ -141,7 +137,6 @@
checked="true"
{{/ife}}
/>
{{/if}}
</div>
{{/repeat}}
</div>
@@ -155,7 +150,6 @@
<div class="hexa ability{{#iflteq @majourwoundmax @root.system.majorWound.value }}
checked
{{/iflteq}}">
{{#if @root.isEditMode}}
<input
type="radio"
data-dtype="Number"
@@ -173,7 +167,6 @@
checked="true"
{{/ife}}
/>
{{/if}}
</div>
{{/repeat}}
@@ -189,7 +182,6 @@
<div class="hexa ability{{#iflteq @deadlywoundmax @root.system.deadlyWound.value }}
checked
{{/iflteq}}">
{{#if @root.isEditMode}}
<input
type="radio"
data-dtype="Number"
@@ -206,7 +198,6 @@
checked="true"
{{/ife}}
/>
{{/if}}
</div>
{{/repeat}}
@@ -1,4 +1,4 @@
<div class="tab equipment sheet-part" data-group="sheet" data-tab="equipment">
<div class="tab equipment sheet-part {{#if @root.tabs.equipment.active}}active{{/if}}" data-group="sheet" data-tab="equipment">
<div class="weapons">
<h4>{{localize 'ITEMS.weapons'}}</h4>
{{> "systems/vermine2047/templates/actor/parts/actor-weapons.hbs"}}
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab stories sheet-part" data-group="sheet" data-tab="stories">
<div class="tab stories sheet-part {{#if @root.tabs.stories.active}}active{{/if}}" data-group="sheet" data-tab="stories">
<section class="flexrow flex-group-left flex-align-left gap-md">
<div>
<h4>{{ localize 'IDENTITY.theme'}}</h4>
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab totem sheet-part" data-group="sheet" data-tab="totem">
<div class="tab totem sheet-part {{#if @root.tabs.totem.active}}active{{/if}}" data-group="sheet" data-tab="totem">
{{#if system.identity.totem}}
<div class="totem-details">
<img
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab combat sheet-part" data-group="sheet" data-tab="combat">
<div class="tab combat sheet-part {{#if @root.tabs.combat.active}}active{{/if}}" data-group="sheet" data-tab="combat">
<div class="grid grid-2col">
{{!-- Computed Values --}}
<div>
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab effects sheet-part" data-group="sheet" data-tab="effects">
<div class="tab effects sheet-part {{#if @root.tabs.effects.active}}active{{/if}}" data-group="sheet" data-tab="effects">
<h4 class="item-name effect-name flexrow">{{ localize "UI.effects.name" }}</h4>
<ol class="items-list effects-list">
{{#each effects as |section sid|}}
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab info sheet-part" data-group="sheet" data-tab="info">
<div class="tab info sheet-part {{#if @root.tabs.info.active}}active{{/if}}" data-group="sheet" data-tab="info">
<div class="grid grid-2col">
<div>
<h4>{{ localize 'IDENTITY.profile' }}</h4>
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab stats sheet-part" data-group="sheet" data-tab="stats">
<div class="tab stats sheet-part {{#if @root.tabs.stats.active}}active{{/if}}" data-group="sheet" data-tab="stats">
<h3>{{ localize 'VERMINE.base_values' }}</h3>
{{!-- Total attaque (lancable) --}}
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab gear sheet-part" data-group="sheet" data-tab="gear">
<div class="tab gear sheet-part {{#if @root.tabs.gear.active}}active{{/if}}" data-group="sheet" data-tab="gear">
<div class="grid grid-2col">
<div>
<ol class="items-list">
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab info sheet-part" data-group="sheet" data-tab="info">
<div class="tab info sheet-part {{#if @root.tabs.info.active}}active{{/if}}" data-group="sheet" data-tab="info">
<div class="grid grid-2col">
{{!-- Group Identity --}}
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab reserve sheet-part" data-group="sheet" data-tab="reserve">
<div class="tab reserve sheet-part {{#if @root.tabs.reserve.active}}active{{/if}}" data-group="sheet" data-tab="reserve">
<div class="grid grid-2col">
{{!-- Group Reserve and Morale --}}
<div class="grid-span-2">
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab road sheet-part" data-group="sheet" data-tab="road">
<div class="tab road sheet-part {{#if @root.tabs.road.active}}active{{/if}}" data-group="sheet" data-tab="road">
<ol class="items-list">
<li class="item flexrow items-header">
<div class="item-name" style="flex:4;">{{ localize 'IDENTITY.name'}}</div>
@@ -1,4 +1,4 @@
<div class="tab characteristics sheet-part" data-group="sheet" data-tab="characteristics">
<div class="tab characteristics sheet-part {{#if @root.tabs.characteristics.active}}active{{/if}}" data-group="sheet" data-tab="characteristics">
<h3>{{ localize 'VERMINE.abilities' }}</h3>
{{!-- Caracteristiques physiques --}}
+2 -1
View File
@@ -1,4 +1,4 @@
<div class="tab combat sheet-part" data-group="sheet" data-tab="combat">
<div class="tab combat sheet-part {{#if @root.tabs.combat.active}}active{{/if}}" data-group="sheet" data-tab="combat">
<div class="grid grid-2col">
<div>
<h4 class="align-center">{{ localize "VERMINE.self_control"}}</h4>
@@ -27,6 +27,7 @@
</p>
</div>
</div>
{{> "systems/vermine2047/templates/actor/parts/actor-equipped-gear.hbs"}}
<h4 class="item-name effect-name flexrow">{{ localize
"VERMINE.wounds.name"}}</h4>
<ul class="unstyled">
+10
View File
@@ -0,0 +1,10 @@
<div class="tab equipment sheet-part {{#if @root.tabs.equipment.active}}active{{/if}}" data-group="sheet" data-tab="equipment">
<div class="weapons">
<h4>{{localize 'ITEMS.weapons'}}</h4>
{{> "systems/vermine2047/templates/actor/parts/actor-weapons.hbs"}}
</div>
<div class="protections">
<h4>{{localize 'ITEMS.defenses'}}</h4>
{{> "systems/vermine2047/templates/actor/parts/actor-defenses.hbs"}}
</div>
</div>
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab notes sheet-part" data-group="sheet" data-tab="notes">
<div class="tab notes sheet-part {{#if @root.tabs.notes.active}}active{{/if}}" data-group="sheet" data-tab="notes">
<h3>{{ localize 'IDENTITY.notes' }}</h3>
<div class="grid grid-1col">
<div class="form-group">
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab skills sheet-part" data-group="sheet" data-tab="skills">
<div class="tab skills sheet-part {{#if @root.tabs.skills.active}}active{{/if}}" data-group="sheet" data-tab="skills">
<h3>{{ localize 'VERMINE.skills' }}</h3>
{{!-- Category Selector --}}
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab threat sheet-part" data-group="sheet" data-tab="threat">
<div class="tab threat sheet-part {{#if @root.tabs.threat.active}}active{{/if}}" data-group="sheet" data-tab="threat">
<h3>{{ localize 'ADVERSITY.threat_details' }}</h3>
<div class="grid grid-3col">
{{!-- Menace --}}
+8
View File
@@ -15,6 +15,7 @@
data-type="defense"
data-action="create"
><i class="fas fa-plus"></i></a>
<a style="visibility:hidden;" aria-hidden="true"><i class="fas fa-circle"></i></a>
<a style="visibility:hidden;" aria-hidden="true"><i class="fas fa-trash"></i></a>
</div>
</li>
@@ -42,6 +43,13 @@
<div><a data-action="edit" title="Edit Item">{{item.system.rarity.value}}</a></div>
<div><a data-action="edit" title="Edit Item">{{item.system.reliability}}</a></div>
<div class="item-controls">
<a
data-action="toggleEquip"
data-item-id="{{item._id}}"
title="{{#if item.system.equipped}}{{ localize 'VERMINE.unequip'}}{{else}}{{ localize 'VERMINE.equip'}}{{/if}}"
><i
class="fas {{#if item.system.equipped}}fa-circle-check{{else}}fa-circle{{/if}}"
></i></a>
<a
data-action="delete"
title="Delete Item"
@@ -0,0 +1,64 @@
{{#if equippedWeapons.length}}
<div class="equipped-gear">
<h4 class="item-name flexrow">{{ localize 'ITEMS.weapons'}}</h4>
<ol class="items-list">
{{#each equippedWeapons as |item|}}
<li
class="item flexrow flex-group-center align-center"
data-item-id="{{item._id}}"
>
<div class="item-name flexrow" style="flex:4;">
<div class="item-image">
<a data-action="roll"><img
src="{{item.img}}"
title="{{item.name}}"
width="24"
height="24"
/></a>
</div>
<a data-action="roll">{{item.name}}</a>
</div>
<div style="flex:1;"><a data-action="attack" title="{{ localize 'VERMINE.attack' }}"><i class="fas fa-crosshairs"></i></a></div>
<div style="flex:2;"><a data-action="edit" title="Edit Item">
{{#if item.system.damage.addVigor}}
{{localize 'VERMINE.vigor'}}+
{{/if}}
{{item.system.damage.value}}
({{item.system.damage.type}})
</a></div>
<div style="flex:2;">{{#if item.system.skill}}{{ smarttlk 'SKILLS' item.system.skill 'name' }}{{/if}}</div>
</li>
{{/each}}
</ol>
</div>
{{/if}}
{{#if equippedDefenses.length}}
<div class="equipped-gear">
<h4 class="item-name flexrow">{{ localize 'ITEMS.defenses'}}</h4>
<ol class="items-list">
{{#each equippedDefenses as |item|}}
<li
class="item flexrow flex-group-center align-center"
data-item-id="{{item._id}}"
>
<div class="item-name flexrow" style="flex:4;">
<div class="item-image">
<a data-action="roll"><img
src="{{item.img}}"
title="{{item.name}}"
width="24"
height="24"
/></a>
</div>
<a data-action="edit" title="Edit Item">{{item.name}}</a>
</div>
<div style="flex:2;"><a data-action="edit" title="Edit Item">{{item.system.level}}</a></div>
<div style="flex:2;"><a data-action="edit" title="Edit Item">
{{#if item.system.isShield}}{{ localize 'ITEMS.shield' }}{{/if}}
</a></div>
</li>
{{/each}}
</ol>
</div>
{{/if}}
+6
View File
@@ -9,6 +9,7 @@
<div class="item-controls">
<a class="item-control item-create" title="Create item"
data-type="weapon" data-action="create"><i class="fas fa-plus"></i></a>
<a style="visibility:hidden;" aria-hidden="true"><i class="fas fa-circle"></i></a>
<a style="visibility:hidden;" aria-hidden="true"><i class="fas fa-trash"></i></a>
<a style="visibility:hidden;" aria-hidden="true"><i class="fas fa-edit"></i></a>
</div>
@@ -37,10 +38,15 @@
<div style="flex:1;"><a data-action="edit" title="Edit Item">{{item.system.rarity.value}}</a></div>
<div style="flex:1;"><a data-action="edit" title="Edit Item">{{item.system.reliability}}</a></div>
<div class="item-controls">
<a data-action="toggleEquip" data-item-id="{{item._id}}"
title="{{#if item.system.equipped}}{{ localize 'VERMINE.unequip'}}{{else}}{{ localize 'VERMINE.equip'}}{{/if}}"><i
class="fas {{#if item.system.equipped}}fa-circle-check{{else}}fa-circle{{/if}}"></i></a>
<a data-action="delete" title="Delete Item"><i
class="fas fa-trash"></i></a>
<a data-action="edit" title="Edit Item"><i
class="fas fa-edit"></i></a>
<a data-action="attack" title="{{ localize 'VERMINE.attack' }}"><i
class="fas fa-crosshairs"></i></a>
<a data-action="roll" title="Roll Item"><i
class="fas fa-dice"></i></a>
</div>
+2
View File
@@ -8,6 +8,7 @@
{{/if}}
</label>
<div class="skill-control">
{{#if @root.isEditMode}}
<input
type="range"
name="system.skills.{{key}}.value"
@@ -16,6 +17,7 @@
max="{{skill.max}}"
data-dtype="Number"
/>
{{/if}}
<span class="skill-value">{{skill.value}}D</span>
</div>
</div>
+234
View File
@@ -0,0 +1,234 @@
<div class="roll-dialog-content">
<!-- HIDDEN DATA -->
<input type="hidden" name="ability" id="ability" value="{{ abilityValue }}" />
<input type="hidden" name="speakerId" value="{{ actor.id }}" />
{{#if weapon}}
<div class="weapon-block combat-dialog-weapon">
<div class="weapon-block-header flexrow">
<span class="weapon-icon"><i class="fas fa-crosshairs"></i></span>
<span class="weapon-name">{{ weapon.name }}</span>
</div>
<div class="flexrow flex-wrap weapon-details">
<span class="weapon-detail">
<strong>{{localize "VERMINE.damages"}}:</strong>
{{#if weapon.system.damage.addVigor}}{{localize "VERMINE.vigor"}}+ {{/if}}
{{ weapon.system.damage.value }} ({{ weapon.system.damage.type }})
</span>
<span class="weapon-detail">
<strong>{{localize "VERMINE.ranges"}}:</strong>
{{ weapon.system.min_range }} / {{ weapon.system.max_range }}
</span>
<span class="weapon-detail">
<strong>{{localize "VERMINE.ammo"}}:</strong>
{{ weapon.system.ammo }}
</span>
</div>
</div>
{{/if}}
<h4 class="combat-mode-label">{{#ife mode "ranged"}}{{localize 'VERMINE.attack_ranged'}}{{else}}{{localize 'VERMINE.attack_contact'}}{{/ife}}</h4>
<div class="dice-pool">
<!-- ATTACKER POOL -->
<div class="flexrow main-roll-section">
<div class="flexcol characteristic-section">
<label class="label">{{localize 'VERMINE.ability'}}</label>
{{#if isCharacter}}
<div class="locked-value">{{ smarttlk 'ABILITIES' abilityKey 'name' }} / {{ abilityValue }}</div>
{{else if isNpc}}
<div class="locked-value">{{localize 'ADVERSITY.threat'}} / {{ npcThreatPool }}</div>
{{else}}
<div class="locked-value">{{localize 'ADVERSITY.attack'}} / {{ basePool }}</div>
{{/if}}
</div>
<div class="flexcol skill-section">
<label class="label">{{localize 'VERMINE.skill_title'}}</label>
{{#if isCharacter}}
<div class="locked-value">{{ smarttlk 'SKILLS' skillKey 'name' }} / {{ skillValue }}</div>
{{else}}
<div class="locked-value">{{localize 'VERMINE.pool_total'}}</div>
{{/if}}
</div>
</div>
<!-- DIFFICULTY -->
<details class="difficulty-section" open>
<summary class="flexrow">
<span class="label">{{localize 'VERMINE.difficulty'}}</span>
<span class="current-values"><span id="final-difficulty">{{ defaultDifficulty }}</span></span>
</summary>
<div class="flexcol difficulty-controls">
<div class="flexrow combat-difficulty-options">
{{#each difficultyOptions}}
<label class="flexrow difficulty-option {{#if this.locked}}locked{{/if}}">
<input
type="radio"
data-roll="true"
name="combat-difficulty"
value="{{ this.difficulty }}"
{{#if this.rangeKey}}data-range-key="{{ this.rangeKey }}"{{/if}}
{{#ife @root.defaultDifficulty this.difficulty}}checked{{/ife}}
{{#if this.locked}}disabled{{/if}}
/>
<span>{{#if this.label}}{{ this.label }}{{else}}{{ this.difficulty }}{{/if}} ({{ this.difficulty }})</span>
</label>
{{/each}}
</div>
{{#unless isCreature}}
{{#unless creatureLocked}}
<label class="flexrow bonus-item">
<input type="checkbox" data-roll="true" name="second-action" id="second-action" value="1" />
<span class="label">{{localize 'VERMINE.second_action'}}</span>
</label>
{{/unless}}
{{/unless}}
{{#if hasAmmo}}
<label class="flexrow bonus-item">
<input type="checkbox" data-roll="true" name="use-ammo" id="use-ammo" value="1" checked />
<span class="label">{{localize 'VERMINE.consume_ammo'}}</span>
</label>
{{/if}}
<div class="flexcol handicap-section">
<label class="label" for="handicap">{{localize 'VERMINE.handicap'}}</label>
<select class="info-value" data-roll="true" data-dtype="String" type="number"
name="handicap" id="handicap" min="0" max="2">
<option value="1" selected>{{localize 'VERMINE.none'}}</option>
<option value="2">(I)</option>
<option value="3">(II)</option>
</select>
</div>
</div>
</details>
<!-- NPC POOLS -->
{{#if isNpc}}
<div class="flexrow bonus-item full-width">
<label class="label" for="pools-spent">{{localize 'VERMINE.npc_pool'}}</label>
<input
type="number"
data-roll="true"
class="numeric-entry"
style="text-align: center;"
name="pools-spent"
id="pools-spent"
min="0"
max="{{ npcPoolMax }}"
value="0"
/>
<span>/ {{ npcPoolMax }}</span>
</div>
{{/if}}
<!-- TARGETS -->
{{#if targets.length}}
<div class="combat-targets">
<span class="label">{{localize 'ITEMS.targets'}}:</span>
{{#each targets}}
<span class="combat-target-tag">{{ this.name }}</span>
{{/each}}
</div>
{{/if}}
<!-- BONUSES -->
<details class="bonuses-section">
<summary class="flexrow">
<span class="label">{{localize 'VERMINE.bonuses'}}</span>
<span class="bonus-count">+<span id="total-bonus">0</span>D</span>
</summary>
<div class="grid grid-2col bonus-grid">
<div class="flexrow bonus-item">
<input type="checkbox" data-roll="true" name="helped" id="helped" value="1" />
<label class="label" for="helped">{{localize 'VERMINE.help'}} (+1D)</label>
</div>
<div class="flexrow bonus-item">
<label class="label" for="group">{{localize 'VERMINE.group'}}</label>
<input type="number" data-roll="true" class="numeric-entry" style="text-align: center;"
name="group" id="group" min="0" max="5" value="0" />
<span>D</span>
</div>
{{#if isCharacter}}
<div class="flexrow bonus-item full-width">
<label class="label" for="self_control">
{{localize 'VERMINE.self_control'}} <span>(+<span id="self_control_value">0</span>D)</span>
</label>
<input type="range" id="self_control" data-roll="true" name="self_control" min="0" max="0" value="0" />
</div>
{{/if}}
{{#if availableItems.length}}
<div class="flexrow bonus-item full-width">
<label class="label">{{localize 'VERMINE.tooling'}} (+1D)</label>
<div class="item-list grid grid-4col">
<label>
<input type="radio" data-roll="true" name="usingTools" id="usingTools" value="0" checked />
<i>{{localize 'VERMINE.none'}}</i>
</label>
{{#each availableItems as |item ind|}}
<label>
<input type="radio" data-roll="true" name="usingTools" id="usingTools" value="{{ item.name }}" />
<i>{{ item.name }}</i>
</label>
{{/each}}
</div>
</div>
{{/if}}
{{#ifgt @root.system.adaptation.totems.human.value 0}}
<div class="flexrow bonus-item full-width totems-section">
<label class="label">{{localize 'VERMINE.totem_dice'}}</label>
<div class="flexrow totem-options">
<label class="totem-option">
<input type="checkbox" data-roll="true" name="human-totem" id="human-totem" value="1" class="totem-checkbox" />
<span class="totem-label">{{localize 'TOTEMS.human.name'}}</span>
<small>({{@root.system.adaptation.totems.human.value}}D)</small>
</label>
{{#ifgt @root.system.adaptation.totems.adapted.value 0}}
<label class="totem-option">
<input type="checkbox" data-roll="true" name="adapted-totem" id="adapted-totem" value="1" class="totem-checkbox" />
<span class="totem-label">{{localize 'TOTEMS.adapted.name'}}</span>
<small>({{@root.system.adaptation.totems.adapted.value}}D)</small>
</label>
{{/ifgt}}
</div>
</div>
{{/ifgt}}
</div>
</details>
<!-- TOTAL -->
<div class="flexrow total-section">
<label class="label">{{localize 'VERMINE.total'}}:</label>
<span id="dice-pool-total" class="total-value">0D</span>
{{#ifgt @root.system.adaptation.totems.human.value 0}}
{{#ifgt @root.system.adaptation.totems.adapted.value 0}}
<span class="totem-selector">
({{localize 'VERMINE.keep_totem'}}
<select id="keep-totem-select" name="keep_totem">
<option value="human">{{localize 'TOTEMS.human.name'}}</option>
<option value="adapted">{{localize 'TOTEMS.adapted.name'}}</option>
</select>)
</span>
{{/ifgt}}
{{/ifgt}}
<span class="totem-selector">
({{localize 'VERMINE.difficulty'}}: <strong id="final-difficulty">{{ defaultDifficulty }}</strong>)
</span>
</div>
</div>
<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="roll">
<i class="fas fa-crosshairs"></i> {{localize "VERMINE.attack"}}
</button>
</footer>
</div>
+1 -1
View File
@@ -15,7 +15,7 @@
<option value="">aucune</option>
{{#each @root.config.skillCategories as |skillCategory sckey|}}
<optgroup label="{{ smarttlk 'SKILLS_CATEGORIES' sckey 'name' }}">
{{#each @root.config.skills as |skill key|}}
{{#each (sortedSkills @root.config.skills) as |skill key|}}
{{#ife skill.category sckey}}
<option value="{{key}}" {{#ife key @root.system.needSkill.skill}} selected {{/ife}}>{{ smarttlk 'SKILLS' key 'name' }}</option>
+1 -1
View File
@@ -20,7 +20,7 @@
>
{{#each @root.config.skillCategories as |skillCategory sckey|}}
<optgroup label="{{ smarttlk 'SKILLS_CATEGORIES' sckey 'name' }}">
{{#each @root.config.skills as |skill key|}}
{{#each (sortedSkills @root.config.skills) as |skill key|}}
{{#ife skill.category sckey}}
<option
value="{{key}}"
+1 -1
View File
@@ -11,7 +11,7 @@
>
{{#each @root.config.skillCategories as |skillCategory sckey|}}
<optgroup label="{{ smarttlk 'SKILLS_CATEGORIES' sckey 'name' }}">
{{#each @root.config.skills as |skill key|}}
{{#each (sortedSkills @root.config.skills) as |skill key|}}
{{#ife skill.category sckey}}
<option
value="{{key}}"
+53
View File
@@ -12,6 +12,53 @@
<h3>{{param.actor.name}} : {{localize "VERMINE.test_of"}} {{param.rollLabel}}</h3>
{{#if param.weapon}}
<div class="weapon-block">
<div class="weapon-block-header flexrow">
<span class="weapon-icon"><i class="fas fa-crosshairs"></i></span>
<span class="weapon-name">{{param.weapon.name}}</span>
{{#if param.targets.length}}
<span class="weapon-targets"><i class="fas fa-bullseye"></i> {{param.targets}}</span>
{{/if}}
</div>
<div class="flexrow flex-wrap weapon-details">
<span class="weapon-detail">
<strong>{{localize "VERMINE.damages"}}:</strong>
{{#if param.weapon.system.damage.addVigor}}{{localize "VERMINE.vigor"}}+ {{/if}}
{{param.weapon.system.damage.value}} ({{param.weapon.system.damage.type}})
</span>
<span class="weapon-detail">
<strong>{{localize "VERMINE.ranges"}}:</strong>
{{param.weapon.system.min_range}} / {{param.weapon.system.max_range}}
</span>
<span class="weapon-detail">
<strong>{{localize "VERMINE.ammo"}}:</strong>
{{param.weapon.system.ammo}}
</span>
</div>
</div>
{{/if}}
{{#if param.attack}}
<div class="exchange-actions" data-exchange-id="{{param.attack.id}}">
<div class="exchange-header"><i class="fas fa-exchange-alt"></i> {{localize "VERMINE.exchange"}}</div>
{{#each param.attack.targets}}
<div class="exchange-target">
<span class="exchange-target-name"><i class="fas fa-bullseye"></i> {{this.name}}</span>
{{#if this.canDefend}}
<button type="button" class="exchange-defend" data-type="esquive" data-target-id="{{this.id}}" data-target-uuid="{{this.uuid}}" title="{{localize 'VERMINE.defense_esquive'}}"><i class="fas fa-running"></i> {{localize "VERMINE.defense_esquive"}}</button>
{{#ife @root.param.attack.mode "contact"}}
<button type="button" class="exchange-defend" data-type="parade" data-target-id="{{this.id}}" data-target-uuid="{{this.uuid}}" title="{{localize 'VERMINE.defense_parade'}}"><i class="fas fa-shield-alt"></i> {{localize "VERMINE.defense_parade"}}</button>
{{/ife}}
{{else}}
<span class="exchange-passive"><i class="fas fa-shield-virus"></i> {{localize "VERMINE.passive_defense"}}</span>
{{/if}}
</div>
{{/each}}
<button type="button" class="exchange-resolve" data-exchange-id="{{param.attack.id}}"><i class="fas fa-balance-scale"></i> {{localize "VERMINE.resolve"}}</button>
</div>
{{/if}}
{{!-- Context --}}
<div class="roll-context flexrow flex-wrap">
<div class="context-item">
@@ -83,6 +130,12 @@
{{#if param.poolBreakdown.group}}
<span class="pool-item">{{localize "VERMINE.pool_group"}} +{{param.poolBreakdown.group}}D</span>
{{/if}}
{{#if param.poolBreakdown.pools}}
<span class="pool-item">{{localize "VERMINE.pool_pools"}} +{{param.poolBreakdown.pools}}D</span>
{{/if}}
{{#if param.poolBreakdown.experience}}
<span class="pool-item">{{localize "VERMINE.pool_experience"}} +{{param.poolBreakdown.experience}}D</span>
{{/if}}
{{#if param.poolBreakdown.selfControl}}
<span class="pool-item">{{localize "VERMINE.pool_selfControl"}} +{{param.poolBreakdown.selfControl}}D</span>
{{/if}}