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:
+93
-1
@@ -147,6 +147,8 @@
|
|||||||
"pools": "Reserves",
|
"pools": "Reserves",
|
||||||
"self_control": "Self-control",
|
"self_control": "Self-control",
|
||||||
"effort": "Effort",
|
"effort": "Effort",
|
||||||
|
"equip": "Equip",
|
||||||
|
"unequip": "Unequip",
|
||||||
"group": "Group",
|
"group": "Group",
|
||||||
"abilities": "Characteristics",
|
"abilities": "Characteristics",
|
||||||
"ability": "Characteristic",
|
"ability": "Characteristic",
|
||||||
@@ -253,6 +255,7 @@
|
|||||||
"pool_tooling": "Equipment",
|
"pool_tooling": "Equipment",
|
||||||
"pool_group": "Group",
|
"pool_group": "Group",
|
||||||
"pool_selfControl": "Self-Control",
|
"pool_selfControl": "Self-Control",
|
||||||
|
"pool_pools": "Pools",
|
||||||
"pool_total": "Total",
|
"pool_total": "Total",
|
||||||
"totem_used": "Totem",
|
"totem_used": "Totem",
|
||||||
"totem_keep": "kept",
|
"totem_keep": "kept",
|
||||||
@@ -280,12 +283,101 @@
|
|||||||
"error_no_actor_selected": "No actor selected",
|
"error_no_actor_selected": "No actor selected",
|
||||||
"error_not_enough_self_control": "Not enough self control",
|
"error_not_enough_self_control": "Not enough self control",
|
||||||
"error_select_ability": "Select an ability",
|
"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": {
|
"tabs": {
|
||||||
"abilities": "Abilities",
|
"abilities": "Abilities",
|
||||||
"combat": "Combat",
|
"combat": "Combat",
|
||||||
"equipment": "Equipment",
|
"equipment": "Equipment",
|
||||||
"stories": "Stories",
|
"stories": "Stories",
|
||||||
"totem": "Totem"
|
"totem": "Totem",
|
||||||
|
"experience": "Experience"
|
||||||
},
|
},
|
||||||
"base_values": "Base Values",
|
"base_values": "Base Values",
|
||||||
"computed_values": "Computed Values",
|
"computed_values": "Computed Values",
|
||||||
|
|||||||
+93
-1
@@ -154,6 +154,8 @@
|
|||||||
"reserves": "Réserves",
|
"reserves": "Réserves",
|
||||||
"self_control": "Sang-Froid",
|
"self_control": "Sang-Froid",
|
||||||
"effort": "Effort",
|
"effort": "Effort",
|
||||||
|
"equip": "Équiper",
|
||||||
|
"unequip": "Déséquiper",
|
||||||
"preferred_category": "Catégorie préférée",
|
"preferred_category": "Catégorie préférée",
|
||||||
"wounds": {
|
"wounds": {
|
||||||
"name": "Blessures",
|
"name": "Blessures",
|
||||||
@@ -200,6 +202,7 @@
|
|||||||
"pool_tooling": "Équipement",
|
"pool_tooling": "Équipement",
|
||||||
"pool_group": "Groupe",
|
"pool_group": "Groupe",
|
||||||
"pool_selfControl": "Sang-Froid",
|
"pool_selfControl": "Sang-Froid",
|
||||||
|
"pool_pools": "Réserves",
|
||||||
"pool_total": "Total",
|
"pool_total": "Total",
|
||||||
"totem_used": "Totem",
|
"totem_used": "Totem",
|
||||||
"totem_keep": "gardé",
|
"totem_keep": "gardé",
|
||||||
@@ -266,6 +269,94 @@
|
|||||||
"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)",
|
||||||
|
"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",
|
"error_select_skill": "Veuillez sélectionner une compétence",
|
||||||
"needed": "nécessaire",
|
"needed": "nécessaire",
|
||||||
"cost": "Coût",
|
"cost": "Coût",
|
||||||
@@ -281,7 +372,8 @@
|
|||||||
"totem": "Totem et ajustements",
|
"totem": "Totem et ajustements",
|
||||||
"combat": "Combat et reserves",
|
"combat": "Combat et reserves",
|
||||||
"equipment": "Matériel",
|
"equipment": "Matériel",
|
||||||
"stories": "Histoire"
|
"stories": "Histoire",
|
||||||
|
"experience": "Expérience"
|
||||||
},
|
},
|
||||||
"ammo": "Munitions",
|
"ammo": "Munitions",
|
||||||
"ammo_sm": "Mun",
|
"ammo_sm": "Mun",
|
||||||
|
|||||||
+299
@@ -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 ────────────────────────────────────────────
|
// ── Composition du jet ────────────────────────────────────────────
|
||||||
div.roll-pool {
|
div.roll-pool {
|
||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
@@ -846,3 +1035,113 @@ ol.chat-log {
|
|||||||
padding: 15px;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 dice pool section ─────────────────────────────────────
|
||||||
.total-section {
|
.total-section {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
|
|||||||
toggleSheet: VermineBaseActorSheet.#onToggleSheet,
|
toggleSheet: VermineBaseActorSheet.#onToggleSheet,
|
||||||
edit: VermineBaseActorSheet.#onItemEdit,
|
edit: VermineBaseActorSheet.#onItemEdit,
|
||||||
delete: VermineBaseActorSheet.#onItemDelete,
|
delete: VermineBaseActorSheet.#onItemDelete,
|
||||||
|
toggleEquip: VermineBaseActorSheet.#onToggleEquip,
|
||||||
create: VermineBaseActorSheet.#onItemCreate,
|
create: VermineBaseActorSheet.#onItemCreate,
|
||||||
roll: VermineBaseActorSheet.#onRollItem,
|
roll: VermineBaseActorSheet.#onRollItem,
|
||||||
|
attack: VermineBaseActorSheet.#onAttack,
|
||||||
clickRadio: VermineBaseActorSheet.#onClickRadioHexa,
|
clickRadio: VermineBaseActorSheet.#onClickRadioHexa,
|
||||||
effectControl: VermineBaseActorSheet.#onEffectControl,
|
effectControl: VermineBaseActorSheet.#onEffectControl,
|
||||||
chooseTotem: VermineBaseActorSheet.#onChooseTotem
|
chooseTotem: VermineBaseActorSheet.#onChooseTotem
|
||||||
@@ -237,7 +239,8 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
|
|||||||
const dialog = await RollDialog.create({
|
const dialog = await RollDialog.create({
|
||||||
actorId: this.document.id,
|
actorId: this.document.id,
|
||||||
rolltype: "skill",
|
rolltype: "skill",
|
||||||
label: item.system.skill
|
label: item.system.skill,
|
||||||
|
weapon: item
|
||||||
})
|
})
|
||||||
if (dialog) dialog.render(true)
|
if (dialog) dialog.render(true)
|
||||||
return
|
return
|
||||||
@@ -245,6 +248,24 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
|
|||||||
item?.roll()
|
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) {
|
static #onClickRadioHexa(event, target) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static PARTS = {
|
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" },
|
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||||
info: { template: "systems/vermine2047/templates/actor/appv2/creature-info.hbs" },
|
info: { template: "systems/vermine2047/templates/actor/appv2/creature-info.hbs", scrollable: [""] },
|
||||||
stats: { template: "systems/vermine2047/templates/actor/appv2/creature-stats.hbs" },
|
stats: { template: "systems/vermine2047/templates/actor/appv2/creature-stats.hbs", scrollable: [""] },
|
||||||
combat: { template: "systems/vermine2047/templates/actor/appv2/creature-combat.hbs" },
|
combat: { template: "systems/vermine2047/templates/actor/appv2/creature-combat.hbs", scrollable: [""] },
|
||||||
effects: { template: "systems/vermine2047/templates/actor/appv2/creature-effects.hbs" }
|
effects: { template: "systems/vermine2047/templates/actor/appv2/creature-effects.hbs", scrollable: [""] }
|
||||||
}
|
}
|
||||||
|
|
||||||
tabGroups = { sheet: "info" }
|
tabGroups = { sheet: "info" }
|
||||||
@@ -94,24 +94,14 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
|
|||||||
return context
|
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) {
|
async _onRoll(event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const el = event.currentTarget
|
const el = event.currentTarget
|
||||||
const type = el.dataset.type
|
const type = el.dataset.type
|
||||||
if (type !== "creature-attack") return super._onRoll(event)
|
if (type !== "creature-attack") return super._onRoll(event)
|
||||||
const pool = this.document.system.computed?.attack ?? 0
|
const { default: CombatDialog } = await import("../../system/dialogs/combatDialog.mjs")
|
||||||
if (!pool) {
|
const dialog = await CombatDialog.create({ actorId: this.document.id })
|
||||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_select_ability"))
|
if (dialog) dialog.render(true)
|
||||||
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")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static PARTS = {
|
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" },
|
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||||
info: { template: "systems/vermine2047/templates/actor/appv2/group-info.hbs" },
|
info: { template: "systems/vermine2047/templates/actor/appv2/group-info.hbs", scrollable: [""] },
|
||||||
gear: { template: "systems/vermine2047/templates/actor/appv2/group-gear.hbs" },
|
gear: { template: "systems/vermine2047/templates/actor/appv2/group-gear.hbs", scrollable: [""] },
|
||||||
road: { template: "systems/vermine2047/templates/actor/appv2/group-road.hbs" },
|
road: { template: "systems/vermine2047/templates/actor/appv2/group-road.hbs", scrollable: [""] },
|
||||||
reserve: { template: "systems/vermine2047/templates/actor/appv2/group-reserve.hbs" }
|
reserve: { template: "systems/vermine2047/templates/actor/appv2/group-reserve.hbs", scrollable: [""] }
|
||||||
}
|
}
|
||||||
|
|
||||||
tabGroups = { sheet: "info" }
|
tabGroups = { sheet: "info" }
|
||||||
|
|||||||
@@ -3,71 +3,71 @@ import { VermineBaseItemSheet } from "./base-item-sheet.mjs"
|
|||||||
// ── Item générique ────────────────────────────────────────────────────
|
// ── Item générique ────────────────────────────────────────────────────
|
||||||
export class VermineItemSheetV2 extends VermineBaseItemSheet {
|
export class VermineItemSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["item-gear"], position: { width: 520 } }
|
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 ──────────────────────────────────────────────────────────────
|
// ── Arme ──────────────────────────────────────────────────────────────
|
||||||
export class VermineWeaponSheetV2 extends VermineBaseItemSheet {
|
export class VermineWeaponSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["weapon"], position: { width: 520 } }
|
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 ───────────────────────────────────────────────────────────
|
// ── Défense ───────────────────────────────────────────────────────────
|
||||||
export class VermineDefenseSheetV2 extends VermineBaseItemSheet {
|
export class VermineDefenseSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["defense"], position: { width: 520 } }
|
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 ──────────────────────────────────────────────────────────
|
// ── Véhicule ──────────────────────────────────────────────────────────
|
||||||
export class VermineVehicleSheetV2 extends VermineBaseItemSheet {
|
export class VermineVehicleSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["vehicle"], position: { width: 520 } }
|
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é ──────────────────────────────────────────────────────────
|
// ── Capacité ──────────────────────────────────────────────────────────
|
||||||
export class VermineAbilitySheetV2 extends VermineBaseItemSheet {
|
export class VermineAbilitySheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["ability"], position: { width: 560 } }
|
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é ────────────────────────────────────────────────────────
|
// ── Spécialité ────────────────────────────────────────────────────────
|
||||||
export class VermineSpecialtySheetV2 extends VermineBaseItemSheet {
|
export class VermineSpecialtySheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["specialty"], position: { width: 400 } }
|
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 ────────────────────────────────────────────────────────
|
// ── Historique ────────────────────────────────────────────────────────
|
||||||
export class VermineBackgroundSheetV2 extends VermineBaseItemSheet {
|
export class VermineBackgroundSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["background"], position: { width: 520 } }
|
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 ───────────────────────────────────────────────────────
|
// ── Traumatisme ───────────────────────────────────────────────────────
|
||||||
export class VermineTraumaSheetV2 extends VermineBaseItemSheet {
|
export class VermineTraumaSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["trauma"], position: { width: 520 } }
|
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 ─────────────────────────────────────────────────────────
|
// ── Évolution ─────────────────────────────────────────────────────────
|
||||||
export class VermineEvolutionSheetV2 extends VermineBaseItemSheet {
|
export class VermineEvolutionSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["evolution"], position: { width: 520 } }
|
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 ────────────────────────────────────────────────────────────
|
// ── Rumeur ────────────────────────────────────────────────────────────
|
||||||
export class VermineRumorSheetV2 extends VermineBaseItemSheet {
|
export class VermineRumorSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["rumor"], position: { width: 520 } }
|
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 ─────────────────────────────────────────────────────────────
|
// ── Cible ─────────────────────────────────────────────────────────────
|
||||||
export class VermineTargetSheetV2 extends VermineBaseItemSheet {
|
export class VermineTargetSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["target"], position: { width: 520 } }
|
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 ──────────────────────────────────────────────────────────────
|
// ── Rite ──────────────────────────────────────────────────────────────
|
||||||
export class VermineRiteSheetV2 extends VermineBaseItemSheet {
|
export class VermineRiteSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["rite"], position: { width: 520 } }
|
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: [""] } }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static PARTS = {
|
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" },
|
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||||
characteristics: { template: "systems/vermine2047/templates/actor/appv2/npc-characteristics.hbs" },
|
characteristics: { template: "systems/vermine2047/templates/actor/appv2/npc-characteristics.hbs", scrollable: [""] },
|
||||||
skills: { template: "systems/vermine2047/templates/actor/appv2/npc-skills.hbs" },
|
skills: { template: "systems/vermine2047/templates/actor/appv2/npc-skills.hbs", scrollable: [""] },
|
||||||
threat: { template: "systems/vermine2047/templates/actor/appv2/npc-threat.hbs" },
|
equipment: { template: "systems/vermine2047/templates/actor/appv2/npc-equipment.hbs", scrollable: [""] },
|
||||||
combat: { template: "systems/vermine2047/templates/actor/appv2/npc-combat.hbs" },
|
threat: { template: "systems/vermine2047/templates/actor/appv2/npc-threat.hbs", scrollable: [""] },
|
||||||
notes: { template: "systems/vermine2047/templates/actor/appv2/npc-notes.hbs" }
|
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" }
|
tabGroups = { sheet: "characteristics" }
|
||||||
@@ -28,6 +29,7 @@ export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
|
|||||||
const tabs = {
|
const tabs = {
|
||||||
characteristics: { id: "characteristics", group: "sheet", icon: "fas fa-dice", label: "VERMINE.abilities" },
|
characteristics: { id: "characteristics", group: "sheet", icon: "fas fa-dice", label: "VERMINE.abilities" },
|
||||||
skills: { id: "skills", group: "sheet", icon: "fas fa-brain", label: "VERMINE.skills" },
|
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" },
|
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" },
|
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" }
|
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":
|
case "skills":
|
||||||
context.tab = context.tabs.skills
|
context.tab = context.tabs.skills
|
||||||
break
|
break
|
||||||
|
case "equipment":
|
||||||
|
context.tab = context.tabs.equipment
|
||||||
|
context.weapons = doc.itemTypes.weapon
|
||||||
|
context.defenses = doc.itemTypes.defense
|
||||||
|
break
|
||||||
case "threat":
|
case "threat":
|
||||||
context.tab = context.tabs.threat
|
context.tab = context.tabs.threat
|
||||||
break
|
break
|
||||||
case "combat":
|
case "combat":
|
||||||
context.tab = context.tabs.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")
|
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
|
||||||
context.effects = prepareActiveEffectCategories(doc.effects)
|
context.effects = prepareActiveEffectCategories(doc.effects)
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default class VermineDefenseData extends foundry.abstract.TypeDataModel {
|
|||||||
return {
|
return {
|
||||||
...baseItemSchema(),
|
...baseItemSchema(),
|
||||||
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||||
|
equipped: new fields.BooleanField({ required: true, initial: false }),
|
||||||
specificLevel: new fields.SchemaField({
|
specificLevel: new fields.SchemaField({
|
||||||
label: new fields.StringField({ required: true, initial: "" }),
|
label: new fields.StringField({ required: true, initial: "" }),
|
||||||
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
|
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default class VermineWeaponData extends foundry.abstract.TypeDataModel {
|
|||||||
return {
|
return {
|
||||||
...baseItemSchema(),
|
...baseItemSchema(),
|
||||||
skill: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
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 }),
|
min_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||||
max_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
max_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||||
damage: new fields.SchemaField({
|
damage: new fields.SchemaField({
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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} · ${t('VERMINE.defender_successes')}: ${r.defenderSuccesses} → ${verdict}</div>`
|
||||||
|
]
|
||||||
|
if (r.hit) {
|
||||||
|
lines.push(`<div class="resolution-line">${t('VERMINE.raw_damage')}: <strong>${r.damage}</strong>${r.protection ? ` − ${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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ export const preloadHandlebarsTemplates = async function () {
|
|||||||
"systems/vermine2047/templates/actor/parts/actor-items.hbs",
|
"systems/vermine2047/templates/actor/parts/actor-items.hbs",
|
||||||
"systems/vermine2047/templates/actor/parts/actor-weapons.hbs",
|
"systems/vermine2047/templates/actor/parts/actor-weapons.hbs",
|
||||||
"systems/vermine2047/templates/actor/parts/actor-defenses.hbs",
|
"systems/vermine2047/templates/actor/parts/actor-defenses.hbs",
|
||||||
|
"systems/vermine2047/templates/actor/parts/actor-equipped-gear.hbs",
|
||||||
|
|
||||||
// Character partials.
|
// Character partials.
|
||||||
"systems/vermine2047/templates/actor/character/character-id.hbs",
|
"systems/vermine2047/templates/actor/character/character-id.hbs",
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ export const registerHooks = function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Hooks.on('renderChatMessageHTML', async (message, html, data) => {
|
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");
|
let rerollTitle = html.querySelector(".reroll-fromroll h4");
|
||||||
if (rerollTitle) {
|
if (rerollTitle) {
|
||||||
rerollTitle.addEventListener("click", () => { html.querySelector(".reroll").classList.toggle('visible') })
|
rerollTitle.addEventListener("click", () => { html.querySelector(".reroll").classList.toggle('visible') })
|
||||||
|
|||||||
+13
-2
@@ -31,7 +31,11 @@ export class VermineUtils {
|
|||||||
hasSpecialty = false,
|
hasSpecialty = false,
|
||||||
handicap = 0,
|
handicap = 0,
|
||||||
poolBreakdown = null,
|
poolBreakdown = null,
|
||||||
specialtyName = null
|
specialtyName = null,
|
||||||
|
weapon = null,
|
||||||
|
targets = [],
|
||||||
|
attack = null,
|
||||||
|
messageFlags = null
|
||||||
}) {
|
}) {
|
||||||
// Validate inputs
|
// Validate inputs
|
||||||
if (!actor) {
|
if (!actor) {
|
||||||
@@ -168,7 +172,11 @@ export class VermineUtils {
|
|||||||
hasSpecialty,
|
hasSpecialty,
|
||||||
handicap,
|
handicap,
|
||||||
poolBreakdown,
|
poolBreakdown,
|
||||||
specialtyName
|
specialtyName,
|
||||||
|
weapon,
|
||||||
|
targets,
|
||||||
|
attack,
|
||||||
|
messageFlags
|
||||||
});
|
});
|
||||||
|
|
||||||
return roll;
|
return roll;
|
||||||
@@ -493,6 +501,9 @@ export class VermineUtils {
|
|||||||
speaker: ChatMessage.getSpeaker(),
|
speaker: ChatMessage.getSpeaker(),
|
||||||
content: content
|
content: content
|
||||||
};
|
};
|
||||||
|
if (param.messageFlags) {
|
||||||
|
chatData.flags = { world: param.messageFlags };
|
||||||
|
}
|
||||||
const msg = await ChatMessage.create(chatData);
|
const msg = await ChatMessage.create(chatData);
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ Hooks.once('init', async function () {
|
|||||||
"systems/vermine2047/templates/actor/appv2/npc-main.hbs",
|
"systems/vermine2047/templates/actor/appv2/npc-main.hbs",
|
||||||
"systems/vermine2047/templates/actor/appv2/npc-characteristics.hbs",
|
"systems/vermine2047/templates/actor/appv2/npc-characteristics.hbs",
|
||||||
"systems/vermine2047/templates/actor/appv2/npc-skills.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-threat.hbs",
|
||||||
"systems/vermine2047/templates/actor/appv2/npc-combat.hbs",
|
"systems/vermine2047/templates/actor/appv2/npc-combat.hbs",
|
||||||
"systems/vermine2047/templates/actor/appv2/npc-notes.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 --}}
|
{{!-- Character --}}
|
||||||
<h3>{{ localize 'VERMINE.abilities' }}</h3>
|
<h3>{{ localize 'VERMINE.abilities' }}</h3>
|
||||||
<div class="grid grid-4col">
|
<div class="grid grid-4col">
|
||||||
|
|||||||
@@ -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="align-center">
|
||||||
<div class="shadow">
|
<div class="shadow">
|
||||||
<h4>situation de combat par défaut</h4>
|
<h4>situation de combat par défaut</h4>
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{> "systems/vermine2047/templates/actor/parts/actor-equipped-gear.hbs"}}
|
||||||
<div class="flexrow">
|
<div class="flexrow">
|
||||||
<div class="self-control-container flexcol align-center">
|
<div class="self-control-container flexcol align-center">
|
||||||
<h4 class="align-center">
|
<h4 class="align-center">
|
||||||
@@ -42,7 +43,6 @@
|
|||||||
unavailable
|
unavailable
|
||||||
{{/ifgt}}">
|
{{/ifgt}}">
|
||||||
{{#iflteq @row @root.system.attributes.self_control.max }}
|
{{#iflteq @row @root.system.attributes.self_control.max }}
|
||||||
{{#if @root.isEditMode}}
|
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
data-dtype="Number"
|
data-dtype="Number"
|
||||||
@@ -56,7 +56,6 @@
|
|||||||
|
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
|
||||||
{{/iflteq}}
|
{{/iflteq}}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -88,7 +87,6 @@
|
|||||||
unavailable
|
unavailable
|
||||||
{{/ifgt}}">
|
{{/ifgt}}">
|
||||||
{{#iflteq @row @root.system.attributes.effort.max }}
|
{{#iflteq @row @root.system.attributes.effort.max }}
|
||||||
{{#if @root.isEditMode}}
|
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
data-dtype="Number"
|
data-dtype="Number"
|
||||||
@@ -102,7 +100,6 @@
|
|||||||
|
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
|
||||||
{{/iflteq}}
|
{{/iflteq}}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -124,7 +121,6 @@
|
|||||||
<div class="hexa ability{{#iflteq @minorwoundmax @root.system.minorWound.value }}
|
<div class="hexa ability{{#iflteq @minorwoundmax @root.system.minorWound.value }}
|
||||||
checked
|
checked
|
||||||
{{/iflteq}}">
|
{{/iflteq}}">
|
||||||
{{#if @root.isEditMode}}
|
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
data-dtype="Number"
|
data-dtype="Number"
|
||||||
@@ -141,7 +137,6 @@
|
|||||||
checked="true"
|
checked="true"
|
||||||
{{/ife}}
|
{{/ife}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
|
||||||
</div>
|
</div>
|
||||||
{{/repeat}}
|
{{/repeat}}
|
||||||
</div>
|
</div>
|
||||||
@@ -155,7 +150,6 @@
|
|||||||
<div class="hexa ability{{#iflteq @majourwoundmax @root.system.majorWound.value }}
|
<div class="hexa ability{{#iflteq @majourwoundmax @root.system.majorWound.value }}
|
||||||
checked
|
checked
|
||||||
{{/iflteq}}">
|
{{/iflteq}}">
|
||||||
{{#if @root.isEditMode}}
|
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
data-dtype="Number"
|
data-dtype="Number"
|
||||||
@@ -173,7 +167,6 @@
|
|||||||
checked="true"
|
checked="true"
|
||||||
{{/ife}}
|
{{/ife}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{{/repeat}}
|
{{/repeat}}
|
||||||
@@ -189,7 +182,6 @@
|
|||||||
<div class="hexa ability{{#iflteq @deadlywoundmax @root.system.deadlyWound.value }}
|
<div class="hexa ability{{#iflteq @deadlywoundmax @root.system.deadlyWound.value }}
|
||||||
checked
|
checked
|
||||||
{{/iflteq}}">
|
{{/iflteq}}">
|
||||||
{{#if @root.isEditMode}}
|
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
data-dtype="Number"
|
data-dtype="Number"
|
||||||
@@ -206,7 +198,6 @@
|
|||||||
checked="true"
|
checked="true"
|
||||||
{{/ife}}
|
{{/ife}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
|
||||||
</div>
|
</div>
|
||||||
{{/repeat}}
|
{{/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">
|
<div class="weapons">
|
||||||
<h4>{{localize 'ITEMS.weapons'}}</h4>
|
<h4>{{localize 'ITEMS.weapons'}}</h4>
|
||||||
{{> "systems/vermine2047/templates/actor/parts/actor-weapons.hbs"}}
|
{{> "systems/vermine2047/templates/actor/parts/actor-weapons.hbs"}}
|
||||||
|
|||||||
@@ -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">
|
<section class="flexrow flex-group-left flex-align-left gap-md">
|
||||||
<div>
|
<div>
|
||||||
<h4>{{ localize 'IDENTITY.theme'}}</h4>
|
<h4>{{ localize 'IDENTITY.theme'}}</h4>
|
||||||
|
|||||||
@@ -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}}
|
{{#if system.identity.totem}}
|
||||||
<div class="totem-details">
|
<div class="totem-details">
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -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 class="grid grid-2col">
|
||||||
{{!-- Computed Values --}}
|
{{!-- Computed Values --}}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -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>
|
<h4 class="item-name effect-name flexrow">{{ localize "UI.effects.name" }}</h4>
|
||||||
<ol class="items-list effects-list">
|
<ol class="items-list effects-list">
|
||||||
{{#each effects as |section sid|}}
|
{{#each effects as |section sid|}}
|
||||||
|
|||||||
@@ -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 class="grid grid-2col">
|
||||||
<div>
|
<div>
|
||||||
<h4>{{ localize 'IDENTITY.profile' }}</h4>
|
<h4>{{ localize 'IDENTITY.profile' }}</h4>
|
||||||
|
|||||||
@@ -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>
|
<h3>{{ localize 'VERMINE.base_values' }}</h3>
|
||||||
|
|
||||||
{{!-- Total attaque (lancable) --}}
|
{{!-- Total attaque (lancable) --}}
|
||||||
|
|||||||
@@ -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 class="grid grid-2col">
|
||||||
<div>
|
<div>
|
||||||
<ol class="items-list">
|
<ol class="items-list">
|
||||||
|
|||||||
@@ -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 class="grid grid-2col">
|
||||||
{{!-- Group Identity --}}
|
{{!-- Group Identity --}}
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
<div class="grid grid-2col">
|
||||||
{{!-- Group Reserve and Morale --}}
|
{{!-- Group Reserve and Morale --}}
|
||||||
<div class="grid-span-2">
|
<div class="grid-span-2">
|
||||||
|
|||||||
@@ -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">
|
<ol class="items-list">
|
||||||
<li class="item flexrow items-header">
|
<li class="item flexrow items-header">
|
||||||
<div class="item-name" style="flex:4;">{{ localize 'IDENTITY.name'}}</div>
|
<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>
|
<h3>{{ localize 'VERMINE.abilities' }}</h3>
|
||||||
|
|
||||||
{{!-- Caracteristiques physiques --}}
|
{{!-- Caracteristiques physiques --}}
|
||||||
|
|||||||
@@ -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 class="grid grid-2col">
|
||||||
<div>
|
<div>
|
||||||
<h4 class="align-center">{{ localize "VERMINE.self_control"}}</h4>
|
<h4 class="align-center">{{ localize "VERMINE.self_control"}}</h4>
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{> "systems/vermine2047/templates/actor/parts/actor-equipped-gear.hbs"}}
|
||||||
<h4 class="item-name effect-name flexrow">{{ localize
|
<h4 class="item-name effect-name flexrow">{{ localize
|
||||||
"VERMINE.wounds.name"}}</h4>
|
"VERMINE.wounds.name"}}</h4>
|
||||||
<ul class="unstyled">
|
<ul class="unstyled">
|
||||||
|
|||||||
@@ -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,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>
|
<h3>{{ localize 'IDENTITY.notes' }}</h3>
|
||||||
<div class="grid grid-1col">
|
<div class="grid grid-1col">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -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>
|
<h3>{{ localize 'VERMINE.skills' }}</h3>
|
||||||
|
|
||||||
{{!-- Category Selector --}}
|
{{!-- Category Selector --}}
|
||||||
|
|||||||
@@ -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>
|
<h3>{{ localize 'ADVERSITY.threat_details' }}</h3>
|
||||||
<div class="grid grid-3col">
|
<div class="grid grid-3col">
|
||||||
{{!-- Menace --}}
|
{{!-- Menace --}}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
data-type="defense"
|
data-type="defense"
|
||||||
data-action="create"
|
data-action="create"
|
||||||
><i class="fas fa-plus"></i></a>
|
><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-trash"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</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.rarity.value}}</a></div>
|
||||||
<div><a data-action="edit" title="Edit Item">{{item.system.reliability}}</a></div>
|
<div><a data-action="edit" title="Edit Item">{{item.system.reliability}}</a></div>
|
||||||
<div class="item-controls">
|
<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
|
<a
|
||||||
data-action="delete"
|
data-action="delete"
|
||||||
title="Delete Item"
|
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}}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
<div class="item-controls">
|
<div class="item-controls">
|
||||||
<a class="item-control item-create" title="Create item"
|
<a class="item-control item-create" title="Create item"
|
||||||
data-type="weapon" data-action="create"><i class="fas fa-plus"></i></a>
|
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-trash"></i></a>
|
||||||
<a style="visibility:hidden;" aria-hidden="true"><i class="fas fa-edit"></i></a>
|
<a style="visibility:hidden;" aria-hidden="true"><i class="fas fa-edit"></i></a>
|
||||||
</div>
|
</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.rarity.value}}</a></div>
|
||||||
<div style="flex:1;"><a data-action="edit" title="Edit Item">{{item.system.reliability}}</a></div>
|
<div style="flex:1;"><a data-action="edit" title="Edit Item">{{item.system.reliability}}</a></div>
|
||||||
<div class="item-controls">
|
<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
|
<a data-action="delete" title="Delete Item"><i
|
||||||
class="fas fa-trash"></i></a>
|
class="fas fa-trash"></i></a>
|
||||||
<a data-action="edit" title="Edit Item"><i
|
<a data-action="edit" title="Edit Item"><i
|
||||||
class="fas fa-edit"></i></a>
|
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
|
<a data-action="roll" title="Roll Item"><i
|
||||||
class="fas fa-dice"></i></a>
|
class="fas fa-dice"></i></a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
</label>
|
</label>
|
||||||
<div class="skill-control">
|
<div class="skill-control">
|
||||||
|
{{#if @root.isEditMode}}
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
name="system.skills.{{key}}.value"
|
name="system.skills.{{key}}.value"
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
max="{{skill.max}}"
|
max="{{skill.max}}"
|
||||||
data-dtype="Number"
|
data-dtype="Number"
|
||||||
/>
|
/>
|
||||||
|
{{/if}}
|
||||||
<span class="skill-value">{{skill.value}}D</span>
|
<span class="skill-value">{{skill.value}}D</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<option value="">aucune</option>
|
<option value="">aucune</option>
|
||||||
{{#each @root.config.skillCategories as |skillCategory sckey|}}
|
{{#each @root.config.skillCategories as |skillCategory sckey|}}
|
||||||
<optgroup label="{{ smarttlk 'SKILLS_CATEGORIES' sckey 'name' }}">
|
<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}}
|
{{#ife skill.category sckey}}
|
||||||
<option value="{{key}}" {{#ife key @root.system.needSkill.skill}} selected {{/ife}}>{{ smarttlk 'SKILLS' key 'name' }}</option>
|
<option value="{{key}}" {{#ife key @root.system.needSkill.skill}} selected {{/ife}}>{{ smarttlk 'SKILLS' key 'name' }}</option>
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
>
|
>
|
||||||
{{#each @root.config.skillCategories as |skillCategory sckey|}}
|
{{#each @root.config.skillCategories as |skillCategory sckey|}}
|
||||||
<optgroup label="{{ smarttlk 'SKILLS_CATEGORIES' sckey 'name' }}">
|
<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}}
|
{{#ife skill.category sckey}}
|
||||||
<option
|
<option
|
||||||
value="{{key}}"
|
value="{{key}}"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
>
|
>
|
||||||
{{#each @root.config.skillCategories as |skillCategory sckey|}}
|
{{#each @root.config.skillCategories as |skillCategory sckey|}}
|
||||||
<optgroup label="{{ smarttlk 'SKILLS_CATEGORIES' sckey 'name' }}">
|
<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}}
|
{{#ife skill.category sckey}}
|
||||||
<option
|
<option
|
||||||
value="{{key}}"
|
value="{{key}}"
|
||||||
|
|||||||
@@ -12,6 +12,53 @@
|
|||||||
|
|
||||||
<h3>{{param.actor.name}} : {{localize "VERMINE.test_of"}} {{param.rollLabel}}</h3>
|
<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 --}}
|
{{!-- Context --}}
|
||||||
<div class="roll-context flexrow flex-wrap">
|
<div class="roll-context flexrow flex-wrap">
|
||||||
<div class="context-item">
|
<div class="context-item">
|
||||||
@@ -83,6 +130,12 @@
|
|||||||
{{#if param.poolBreakdown.group}}
|
{{#if param.poolBreakdown.group}}
|
||||||
<span class="pool-item">{{localize "VERMINE.pool_group"}} +{{param.poolBreakdown.group}}D</span>
|
<span class="pool-item">{{localize "VERMINE.pool_group"}} +{{param.poolBreakdown.group}}D</span>
|
||||||
{{/if}}
|
{{/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}}
|
{{#if param.poolBreakdown.selfControl}}
|
||||||
<span class="pool-item">{{localize "VERMINE.pool_selfControl"}} +{{param.poolBreakdown.selfControl}}D</span>
|
<span class="pool-item">{{localize "VERMINE.pool_selfControl"}} +{{param.poolBreakdown.selfControl}}D</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
Reference in New Issue
Block a user