6 Commits
Author SHA1 Message Date
uberwald ea3322e843 feat: fiche véhicule (caractéristiques) et capacités de totem de groupe
Release Creation / build (release) Failing after 1m24s
- véhicule : fiabilité 1-10, type véhicule/monture, distance, vitesse (croisière/pointe), entretien, réserve d'effort
- nettoyage du tableau d'entretien à la soumission
- fiche de groupe : section capacités de totem (création type totem + liste)
2026-08-04 17:20:37 +02:00
uberwald 6247f65590 feat: fiche de Communauté (socle)
- acteur community (identité, géographie, histoire, objectifs, taille/effectif)
- 4 Domaines x 2 jauges avec états (Crise/Bas/Stable/Opulent), moral, réserves
- onglets Identité / Domaines / Ressources, édition ProseMirror (formInput)
- header avec fond, totem, niveau, effectif ; icône par défaut
- libellé du type via typeLabels (sans écraser TYPES.* du core)
2026-08-04 17:20:29 +02:00
uberwald 8d8428cad3 feat: gérer les armes des créatures (morsure, griffes, piqûre…)
- gestion des armes dans l'onglet Combat des créatures (équipement + liste)
- dégâts de l'arme équipée utilisés à la place de computed.damage (+ Vigueur+)
- correction du libellé Vigueur+ (ADVERSITY.vigor au lieu de VERMINE.vigor)
2026-08-04 14:25:27 +02:00
uberwald 9cf7d9ac68 feat: système d'expérience et transferts de dés d'expérience
Release Creation / build (release) Failing after 1m20s
- phase d'expérience (récompenses collectives et individuelles, objectifs, traumatisme)
- jets d'apprentissage (compétences, spécialités, caractéristiques, réserves, évolution)
- bonus de réserve acquis par apprentissage intégré au max dérivé
- transferts de dés individuels <-> groupe dans le dialogue de jet
- onglet expérience sur la fiche personnage
- guide utilisateur combat (docs/user/COMBAT.md)
2026-08-04 14:14:09 +02:00
uberwald f2c8cfe246 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)
2026-08-04 14:14:03 +02:00
uberwald 31f2f4530c feat: améliorer les fiches (tests PNJ/créature, message de jet, compétences, créatures)
Release Creation / build (release) Failing after 1m23s
2026-08-04 00:49:37 +02:00
80 changed files with 5330 additions and 324 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

+126
View File
@@ -0,0 +1,126 @@
# Combat — Échanges de coups (assisté)
## Principe
Chaque échange de coups se déroule dans le chat : un jet d'attaque, un éventuel
jet de défense, puis une résolution assistée. Tous les calculs (réussites,
dégâts, protection, blessure) sont effectués automatiquement ; la blessure
s'applique ensuite d'un simple clic.
```
Attaque → (Défense) → Résoudre → Appliquer
```
## 1. L'attaque
Ouvrir le dialogue d'attaque :
- **Personnage / PNJ** : cliquer l'icône cible (`fa-crosshairs`) sur une arme
dans l'onglet **Combat** (liste des armes équipées) ou dans l'onglet
**Matériel**.
- **Créature** : cliquer la valeur « Attaque » de l'onglet **Stats**.
Le dialogue est prérempli selon le type d'acteur :
| Type | Pool | Dégâts de base | Difficulté |
|------|------|----------------|------------|
| **Personnage** | Caractéristique + compétence (verrouillées) | Arme (+ Vigueur si « Vigueur+ ») | Selon maîtrise / portée |
| **PNJ** | Pool de menace + réserves de rôle | Arme équipée, sinon `Vigueur` de menace | Selon niveau d'expérience |
| **Créature** | `Attaque` calculée | `Dégâts` calculés | Son statut de combat (verrouillée) |
### Difficulté
- **Contact** : dépend du niveau de compétence de l'attaquant.
- Incompétent / Débutant : `7`
- Confirmé : `5 ou 7`
- Expert : `5, 7 ou 9`
- Maître / Légende : `3, 5, 7 ou 9`
- **Distance** : par portée.
- Courte : `5` — Moyenne : `7` — Longue : `9`
- **Créature ciblée** : la difficulté est forcée au statut de combat de la
créature (toute réussite ≥ 1 touche).
### Options du dialogue
- **Deuxième action** : +2 à la difficulté.
- **Handicap** : augmente les succès requis.
- **Sang-Froid** : dés bonus (débités du personnage).
- **Totems** : dés de totem (+ garder un totem si les deux sont actifs).
- **Consommation de munitions** : pour les armes à distance (coche cochée par
défaut, la munition est décomptée au jet).
- Les **cibles** sélectionnées sur la carte (`Ctrl` + clic / outil de ciblage)
sont affichées et reprises dans l'échange.
### La carte de chat
Le message d'attaque affiche l'arme (dégâts, portées, munitions) puis le bloc
**Échange de coups** avec, **par cible**, les boutons **Esquive** et **Parade**
(contact) ou **Esquive** seule (distance), ainsi que le bouton **Résoudre**.
## 2. La défense
Seuls les acteurs **capables de se défendre** (personnages et PNJ) ont des
boutons de défense — pas les créatures (défense passive).
- Cliquer **Esquive** ou **Parade** sur la carte d'attaque.
- Le dialogue de défense est **verrouillé** :
- **Esquive** : `Réflexes` + `Corps-à-corps` (attaque au contact) ou
`Vigilance` (attaque à distance).
- **Parade** : `Vigueur` + `Mêlée` (ou `Corps-à-corps`).
- **Difficulté = celle de l'attaque** (non modifiable).
- Le jet de défense est lié au message d'attaque (même échange).
> Permission : Esquive / Parade ne sont utilisables que par le **propriétaire
> de la cible** ou le **MJ**.
## 3. La résolution
Cliquer **Résoudre** (auteur du message ou MJ). Le système compare les
**réussites** des jets d'attaque et de défense :
- **Attaquant > Défenseur** → **Touché** (parade insuffisante si parade).
- **Attaquant ≤ Défenseur** → **Esquivé / Paré** : aucun dégât.
- **Sans défense** (abstention ou créature) : toute réussite ≥ 1 touche.
Les totaux sont lus en direct : une **relance** de dé effectuée avant la
résolution est prise en compte.
### Dégâts
```
Dégâts = dégâts de base de l'arme (+ Vigueur si « Vigueur+ ») + réussites d'attaque
```
### Protection
- **Parade** : seule la protection utilisée pour la parade réduit.
- **Sinon** : somme des **protections équipées** de la cible
(créature : protection de rôle).
### Blessure
La blessure correspond au **seuil le plus élevé** inférieur ou égal aux dégâts
nets : **Légère**, **Grave** ou **Mortelle**. Un bloc de résultat est affiché
**par cible** (attaque multi-cibles = un bloc par cible).
## 4. L'application de la blessure
Cliquer **« Appliquer la blessure »** sur le bloc de résultat (propriétaire de
la cible ou MJ) : le cercle de blessure correspondant est coché sur l'acteur.
## Rappels et permissions
| Action | Qui peut la faire |
|--------|-------------------|
| Esquive / Parade | Propriétaire de la cible ou MJ |
| Résoudre | Auteur du message d'attaque ou MJ |
| Appliquer la blessure | Propriétaire de la cible ou MJ |
- Les **créatures** ne lancent pas de défense : leur difficulté d'attaque et de
défense est leur **statut de combat**.
- Les **PNJ** lancent leur défense comme un personnage (capacités + compétences).
- Les **réserves de rôle** des PNJ sont à dépenser à la main (gérées par le MJ).
---
*Guide utilisateur — Vermine2047 pour FoundryVTT.*
+220 -3
View File
@@ -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",
@@ -172,6 +174,15 @@
"range": "Range", "range": "Range",
"ranges": "Ranges", "ranges": "Ranges",
"mobility": "Mobility", "mobility": "Mobility",
"vehicle_specs": "Vehicle specifications",
"kind": "Kind",
"kind_vehicle": "Vehicle",
"kind_mount": "Mount",
"distance": "Distance",
"speed": "Speed",
"speed_duration_ph": "top speed duration (e.g. 10 min)",
"effort_reserve": "Effort reserve",
"maintenance": "Maintenance",
"rarity": "Rarity", "rarity": "Rarity",
"rarity_sm": "Rar.", "rarity_sm": "Rar.",
"reliability": "Reliability", "reliability": "Reliability",
@@ -202,6 +213,7 @@
"type_character": "Character", "type_character": "Character",
"type_npc": "NPC", "type_npc": "NPC",
"type_creature": "Creature", "type_creature": "Creature",
"type_community": "Community",
"type": "Type", "type": "Type",
"group_members": "Members", "group_members": "Members",
"encounters": "Encounters", "encounters": "Encounters",
@@ -241,6 +253,29 @@
"success_count": "Success Count", "success_count": "Success Count",
"success_required": "Success Required", "success_required": "Success Required",
"test_of": "Test of", "test_of": "Test of",
"roll_success": "Success",
"roll_failure": "Failure",
"roll_context": "Roll context",
"roll_pool": "Dice pool",
"roll_dice": "Dice detail",
"pool_ability": "Ability",
"pool_skill": "Skill",
"pool_specialty": "Specialty",
"pool_help": "Help",
"pool_tooling": "Equipment",
"pool_group": "Group",
"pool_selfControl": "Self-Control",
"pool_pools": "Pools",
"pool_total": "Total",
"totem_used": "Totem",
"totem_keep": "kept",
"domain_bonus": "Domain bonus",
"domain_penalty": "Domain penalty",
"used": "used",
"die_regular": "dice",
"die_human": "human",
"die_adapted": "adapted",
"die_successes": "successes",
"total": "Total", "total": "Total",
"totem_dice": "Totem Dice", "totem_dice": "Totem Dice",
"totem_hint": "Totem Hint", "totem_hint": "Totem Hint",
@@ -258,12 +293,108 @@
"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"
},
"Sheet": {
"character": "Character",
"npc": "NPC",
"group": "Group",
"creature": "Creature",
"community": "Community"
}, },
"base_values": "Base Values", "base_values": "Base Values",
"computed_values": "Computed Values", "computed_values": "Computed Values",
@@ -327,6 +458,91 @@
"effects.passive": "Passive", "effects.passive": "Passive",
"effects.temporary": "Temporary" "effects.temporary": "Temporary"
}, },
"COMMUNITY": {
"tabs": {
"identity": "Identity",
"domains": "Domains",
"resources": "Resources"
},
"identity": "Identity",
"name_placeholder": "Community name",
"choose_totem": "Choose totem",
"origin": "Origin",
"theme": "Theme",
"age_label": "Community age",
"age": {
"recent": "A few days",
"established": "A few months",
"old": "A few years"
},
"instincts": "Instincts",
"prohibits": "Prohibitions",
"notes": "Notes",
"location": "Location",
"sector": "Sector",
"placeType": "Place type",
"landmarks": "Landmarks",
"territory": "Territory",
"history": "History",
"founding": "Founding",
"events": "Key events",
"history_relations": "Relations",
"objectives": "Objectives",
"objective_major": "Major objective",
"objectives_minor": "Minor objectives",
"add_objective": "Add objective",
"size_title": "Size & Population",
"size_label": "Size",
"sizeLevels": {
"1": "Size 1",
"2": "Size 2",
"3": "Size 3"
},
"population": "Population",
"population_range": "Recommended population",
"figures": "Figures",
"companies": "Companies",
"domains": {
"resources": "Resources",
"security": "Security",
"dynamism": "Dynamism",
"relations": "Relations"
},
"sections": {
"vivre": "Food & Water",
"supplies": "Supplies",
"defense": "Defense",
"care": "Care",
"cohesion": "Cohesion",
"productivity": "Productivity",
"human": "Human",
"ecology": "Ecology"
},
"states": {
"crisis": "Crisis",
"low": "Low",
"stable": "Stable",
"opulent": "Opulent"
},
"reserves": "Reserves",
"group_reserve": "Group reserve",
"material_dice": "Material dice",
"experience_dice": "Experience dice",
"morale": "Morale",
"morale_value": "Value",
"morale_state": "State",
"morale_levels": {
"high": "High",
"normal": "Normal",
"low": "Low",
"crisis": "Crisis"
},
"morale_blocked": "All four Domains are Low: the Morale bonus is inactive.",
"habitants_title": "Inhabitants",
"habitants_coming": "Figures and Companies management comes in a future version.",
"linked_group": "Linked group",
"profile_hint": "Community profile"
},
"ITEMS": { "ITEMS": {
"item": "Item", "item": "Item",
"items": "Items", "items": "Items",
@@ -574,7 +790,7 @@
"name": "Repulsion" "name": "Repulsion"
}, },
"tracks": { "tracks": {
"name": "Tracks" "name": "Tracking"
}, },
"crafting": { "crafting": {
"name": "Crafting" "name": "Crafting"
@@ -604,7 +820,7 @@
"name": "Melee" "name": "Melee"
}, },
"archery": { "archery": {
"name": "Ranged" "name": "Ranged Weapons"
}, },
"throwing": { "throwing": {
"name": "Throwing" "name": "Throwing"
@@ -656,6 +872,7 @@
"Round": "Round" "Round": "Round"
}, },
"SIZE_LEVELS": { "SIZE_LEVELS": {
"tiny": "Insect",
"small": "Small", "small": "Small",
"medium": "Medium", "medium": "Medium",
"large": "Large" "large": "Large"
+216 -5
View File
@@ -93,7 +93,7 @@
"AGE_TYPES": { "AGE_TYPES": {
"young": "Jeune", "young": "Jeune",
"adult": "Adulte", "adult": "Adulte",
"old": "Vieux" "old": "Ancien"
}, },
"IDENTITY": { "IDENTITY": {
"biography": "Biographie", "biography": "Biographie",
@@ -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",
@@ -188,6 +190,29 @@
"grant_reroll": "accorder des relances", "grant_reroll": "accorder des relances",
"success_count": "nombre de succès", "success_count": "nombre de succès",
"success_required": "succès requis", "success_required": "succès requis",
"roll_success": "Réussite",
"roll_failure": "Échec",
"roll_context": "Contexte du jet",
"roll_pool": "Composition du jet",
"roll_dice": "Détail des dés",
"pool_ability": "Caractéristique",
"pool_skill": "Compétence",
"pool_specialty": "Spécialité",
"pool_help": "Entraide",
"pool_tooling": "Équipement",
"pool_group": "Groupe",
"pool_selfControl": "Sang-Froid",
"pool_pools": "Réserves",
"pool_total": "Total",
"totem_used": "Totem",
"totem_keep": "gardé",
"domain_bonus": "Bonus de domaine",
"domain_penalty": "Malus de domaine",
"used": "utilisé",
"die_regular": "dés",
"die_human": "humains",
"die_adapted": "adaptés",
"die_successes": "succès",
"error_no_actor_selected": "Vous n'avez pas de personnage attitré ou de token sélectionné", "error_no_actor_selected": "Vous n'avez pas de personnage attitré ou de token sélectionné",
"error_cannot_reroll": "Vous ne pouvez pas relancer un dés sur ce jet", "error_cannot_reroll": "Vous ne pouvez pas relancer un dés sur ce jet",
"error_no_rerolls_left": "Plus de relances possibles", "error_no_rerolls_left": "Plus de relances possibles",
@@ -230,6 +255,15 @@
"range": "Portée", "range": "Portée",
"ranges": "Portées", "ranges": "Portées",
"mobility": "Mobilité", "mobility": "Mobilité",
"vehicle_specs": "Caractéristiques du véhicule",
"kind": "Type",
"kind_vehicle": "Véhicule",
"kind_mount": "Monture",
"distance": "Distance",
"speed": "Vitesse",
"speed_duration_ph": "durée de pointe (ex: 10 min)",
"effort_reserve": "Réserve d'Effort",
"maintenance": "Entretien",
"rarity": "Rareté", "rarity": "Rareté",
"rarity_sm": "Rar.", "rarity_sm": "Rar.",
"reliability": "Fiabilité", "reliability": "Fiabilité",
@@ -244,6 +278,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",
@@ -259,7 +381,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",
@@ -281,6 +404,7 @@
"type_character": "Personnage", "type_character": "Personnage",
"type_npc": "PNJ", "type_npc": "PNJ",
"type_creature": "Créature", "type_creature": "Créature",
"type_community": "Communauté",
"type": "Type", "type": "Type",
"group_members": "Membres", "group_members": "Membres",
"encounters": "Rencontres", "encounters": "Rencontres",
@@ -326,7 +450,8 @@
"character": "Personnage", "character": "Personnage",
"npc": "PNJ", "npc": "PNJ",
"group": "Groupe", "group": "Groupe",
"creature": "Créature" "creature": "Créature",
"community": "Communauté"
} }
}, },
"UI": { "UI": {
@@ -344,6 +469,91 @@
"inactive": "Effets inactifs" "inactive": "Effets inactifs"
} }
}, },
"COMMUNITY": {
"tabs": {
"identity": "Identité",
"domains": "Domaines",
"resources": "Ressources"
},
"identity": "Identité",
"name_placeholder": "Nom de la Communauté",
"choose_totem": "Choisir le totem",
"origin": "Origine",
"theme": "Thème",
"age_label": "Âge de la Communauté",
"age": {
"recent": "Quelques jours",
"established": "Quelques mois",
"old": "Quelques années"
},
"instincts": "Instincts",
"prohibits": "Interdits",
"notes": "Notes",
"location": "Localisation",
"sector": "Secteur",
"placeType": "Type de lieu",
"landmarks": "Éléments marquants",
"territory": "Territoire",
"history": "Histoire",
"founding": "Fondation",
"events": "Événements marquants",
"history_relations": "Relations",
"objectives": "Objectifs",
"objective_major": "Objectif majeur",
"objectives_minor": "Objectifs mineurs",
"add_objective": "Ajouter un objectif",
"size_title": "Taille & Effectif",
"size_label": "Taille",
"sizeLevels": {
"1": "Taille 1",
"2": "Taille 2",
"3": "Taille 3"
},
"population": "Effectif",
"population_range": "Population recommandée",
"figures": "Figures",
"companies": "Compagnies",
"domains": {
"resources": "Ressources",
"security": "Sécurité",
"dynamism": "Dynamisme",
"relations": "Rapports"
},
"sections": {
"vivre": "Vivres",
"supplies": "Fournitures",
"defense": "Défense",
"care": "Soins",
"cohesion": "Cohésion",
"productivity": "Productivité",
"human": "Humain",
"ecology": "Écologie"
},
"states": {
"crisis": "Crise",
"low": "Bas",
"stable": "Stable",
"opulent": "Opulent"
},
"reserves": "Réserves",
"group_reserve": "Réserve de Groupe",
"material_dice": "Dés de Matériel",
"experience_dice": "Dés d'Expérience",
"morale": "Moral",
"morale_value": "Valeur",
"morale_state": "État",
"morale_levels": {
"high": "Haut",
"normal": "Normal",
"low": "Bas",
"crisis": "Crise"
},
"morale_blocked": "Les quatre Domaines sont Bas : le Bonus de Moral est inactif.",
"habitants_title": "Habitants",
"habitants_coming": "La gestion des Figures et des Compagnies arrive dans une prochaine version.",
"linked_group": "Groupe rattaché",
"profile_hint": "Profil de la Communauté"
},
"ITEMS": { "ITEMS": {
"item": "Objet", "item": "Objet",
"items": "Objets", "items": "Objets",
@@ -433,6 +643,7 @@
"major": "Majeur" "major": "Majeur"
}, },
"SIZE_LEVELS": { "SIZE_LEVELS": {
"tiny": "Insecte",
"small": "Petit", "small": "Petit",
"medium": "Moyen", "medium": "Moyen",
"large": "Grand" "large": "Grand"
@@ -596,7 +807,7 @@
"name": "Répulsion" "name": "Répulsion"
}, },
"tracks": { "tracks": {
"name": "Pistage" "name": "Traces"
}, },
"crafting": { "crafting": {
"name": "Artisanat" "name": "Artisanat"
@@ -617,7 +828,7 @@
"name": "Armes à feu" "name": "Armes à feu"
}, },
"archery": { "archery": {
"name": "Tir à l'arc" "name": "Armes de Tir"
}, },
"armory": { "armory": {
"name": "Armurerie" "name": "Armurerie"
+152
View File
@@ -0,0 +1,152 @@
@import "../variables";
@import "../utilities";
.system-vermine2047 .vermine2047.actor.community {
.char-header {
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
max-height: 130px;
.community-portrait {
display: flex;
align-items: center;
justify-content: center;
}
.char-name,
.char-vermine2047 {
border-bottom: none;
line-height: 2rem;
}
.char-vermine2047 {
font-size: 1.1rem;
}
.profile,
.origin,
.level,
.population {
width: 100%;
}
input[type="text"] {
flex: 1;
min-width: 0;
}
.level input,
.population input {
width: 60px;
text-align: center;
}
}
.community-section {
padding: 8px;
border: 1px solid @color-border-dark-4;
background: fade(@color-chitin-dark, 20%);
h3 {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-13;
color: @theme-color-dark;
margin: 0 0 8px;
text-align: left;
border-bottom: none;
}
}
.community-linked-group {
display: flex;
gap: 6px;
margin-bottom: 8px;
padding: 6px 8px;
border: 1px dashed @color-hemolymph;
background: fade(@color-hemolymph, 8%);
.label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
color: @color-amber;
}
}
.community-size-info {
margin: 6px 0 0;
font-size: @font-size-11;
color: @color-text-light-0;
opacity: 0.85;
}
.community-warning {
margin: 6px 0 0;
padding: 4px 8px;
font-size: @font-size-11;
font-weight: 700;
color: @color-hemolymph;
background: fade(@color-hemolymph, 12%);
border: 1px solid @color-hemolymph;
}
.community-hint {
margin: 0;
font-size: @font-size-11;
font-style: italic;
color: @color-text-light-0;
opacity: 0.7;
}
// Jauges de Domaines
.gauge {
display: flex;
flex-direction: column;
gap: 2px;
padding: 6px;
border: 1px solid @color-border-dark-4;
border-left: 4px solid @color-border-dark-4;
label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
color: @color-amber;
}
input[type="number"] {
width: 48px;
text-align: center;
padding: 2px;
}
.gauge-value {
font-family: "DistressBlack", sans-serif;
font-size: @font-size-16;
color: @color-text-light-0;
}
.gauge-state {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
font-weight: 700;
text-transform: uppercase;
}
&.crisis { border-left-color: @color-hemolymph; .gauge-state { color: @color-hemolymph; } }
&.low { border-left-color: @color-amber; .gauge-state { color: @color-amber; } }
&.stable { border-left-color: @color-acid-green; .gauge-state { color: @color-acid-green; } }
&.opulent { border-left-color: @theme-color-accent; .gauge-state { color: @theme-color-accent; } }
}
.morale-level {
font-family: "Roboto", sans-serif;
font-size: @font-size-12;
font-weight: 700;
&.high { color: @color-acid-green; }
&.normal { color: @color-text-light-0; }
&.low { color: @color-amber; }
&.crisis { color: @color-hemolymph; }
}
}
+461 -3
View File
@@ -44,6 +44,24 @@ body.system-vermine2047.theme-dark {
color: #000000; color: #000000;
} }
// Core sheets the system doesn't restyle (RollTable, Journal, …) inherit the
// gray text overrides above, which are unreadable on their dark backgrounds.
// Restore Foundry's default light-on-dark text colors for those apps (dark theme only).
body.system-vermine2047.theme-dark {
.roll-table-sheet,
.journal-sheet,
.document-sheet,
.settings-sheet {
--color-text-primary: var(--color-light-2);
--color-text-emphatic: var(--color-light-1);
--color-text-secondary: var(--color-light-3);
--color-text-subtle: var(--color-light-5);
--color-text-light-highlight: #f0f0e0;
--color-text-light-heading: #c9c7b8;
--color-text-light-primary: #b5b3a4;
}
}
body.system-vermine2047.theme-dark .form-group > label { body.system-vermine2047.theme-dark .form-group > label {
color: #777777; color: #777777;
} }
@@ -97,6 +115,22 @@ ul.unstyled {
} }
} }
// Les fenêtres de choix (totem, acteur) peuvent dépasser la hauteur fixe :
// on autorise le défilement pour accéder à la totalité du contenu.
.application#TOTEM_PICKER .window-content,
.application#ACTOR_PICKER .window-content,
.application#TRAIT_SELECTOR .window-content {
overflow-y: auto;
}
// La fiche de groupe contient beaucoup de zones (membres, objectifs, totem…) :
// dès que le contenu dépasse la hauteur de la fenêtre, on autorise le défilement
// pour ne pas perdre l'accès aux zones du bas.
.application.sheet.vermine2047.group .window-content,
.application.sheet.vermine2047.community .window-content {
overflow-y: auto;
}
.window-content .row { .window-content .row {
&.smb { margin-bottom: 0.25rem; } &.smb { margin-bottom: 0.25rem; }
&.mdb { margin-bottom: 0.5rem; } &.mdb { margin-bottom: 0.5rem; }
@@ -113,6 +147,29 @@ ul.unstyled {
.rollable { .rollable {
.transition(); .transition();
// Affordance légère au repos : la zone est cliquable.
i.fa-dice-d10,
i.fa-dice {
font-size: 0.8em;
color: @color-acid-green;
}
}
// Sur la fiche créature, les valeurs lançables (Attaque…) sont fortement
// matérialisées : pastille de dé + bordure pointillée.
.application.sheet.vermine2047.creature {
.rollable {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 1px 5px;
border-radius: 3px;
color: @theme-color-light;
border: 1px dashed fade(@color-acid-green, 45%);
background: fade(@color-acid-green, 8%);
cursor: pointer;
}
} }
img.profile-img { img.profile-img {
@@ -420,7 +477,7 @@ body.system-vermine2047 img#logo {
} }
.item-name { .item-name {
padding-left: 5px; padding: 0 2px 0 5px;
text-align: left; text-align: left;
} }
} }
@@ -674,6 +731,296 @@ ol.chat-log {
color: @color-amber; color: @color-amber;
} }
} }
// ── Verdict ───────────────────────────────────────────────────────
.roll-verdict {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin: 6px 0;
padding: 6px 12px;
border-radius: 3px;
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-16;
letter-spacing: 0.5px;
&.success {
color: @color-acid-green;
background: fade(@color-acid-green, 15%);
border: 1px solid @color-acid-green;
}
&.failure {
color: @color-hemolymph;
background: fade(@color-hemolymph, 15%);
border: 1px solid @color-hemolymph;
}
}
// ── Contexte du jet ───────────────────────────────────────────────
div.roll-context {
gap: 6px 12px;
margin: 4px 0 6px;
padding: 6px 8px;
background: rgba(0, 0, 0, 0.06);
.context-item {
display: inline-flex;
align-items: center;
gap: 5px;
.context-label {
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-11;
opacity: 0.8;
}
.context-value {
font-family: "Roboto", sans-serif;
font-size: @font-size-12;
font-weight: 600;
}
&.totem-human .context-value { color: @totem-human-color; }
&.totem-adapted .context-value { color: @totem-adapted-color; }
}
}
// ── Arme du jet ───────────────────────────────────────────────────
div.weapon-block {
margin: 4px 0 6px;
padding: 6px 8px;
border: 1px solid @color-hemolymph;
background: rgba(0, 0, 0, 0.1);
.weapon-block-header {
align-items: center;
gap: 6px;
margin-bottom: 2px;
.weapon-icon {
color: @color-hemolymph;
}
.weapon-name {
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-12;
font-weight: 900;
color: @theme-color-dark;
}
.weapon-targets {
margin-left: auto;
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
color: @theme-color-accent;
}
}
.weapon-details {
gap: 4px 12px;
.weapon-detail {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
strong {
text-transform: uppercase;
}
}
}
}
// ── Échange de coups ─────────────────────────────────────────────
div.exchange-actions {
margin: 4px 0 6px;
padding: 6px 8px;
border: 1px dashed @color-hemolymph;
background: fade(@color-hemolymph, 8%);
.exchange-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-12;
font-weight: 900;
color: @theme-color-dark;
i { color: @color-hemolymph; }
}
.exchange-target {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
padding: 3px 0;
.exchange-target-name {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
color: @theme-color-accent;
}
button.exchange-defend {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
padding: 2px 8px;
cursor: pointer;
color: @color-chitin-dark;
background: @color-text-light-0;
border: 1px solid @color-border-dark-4;
border-radius: 3px;
.transition();
&:hover {
color: @color-text-light-0;
background: @color-hemolymph;
border-color: @color-hemolymph;
}
}
.exchange-passive {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
color: @color-amber;
opacity: 0.85;
font-style: italic;
}
}
button.exchange-resolve {
margin-top: 4px;
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-11;
padding: 3px 12px;
cursor: pointer;
color: @color-text-light-0;
background: fade(@color-hemolymph, 25%);
border: 1px solid @color-hemolymph;
border-radius: 3px;
.transition();
&:hover {
color: @theme-color-dark;
background: @color-hemolymph;
}
}
}
div.exchange-resolution {
margin: 4px 0 6px;
padding: 6px 8px;
border: 1px solid @color-amber;
background: fade(@color-amber, 8%);
.resolution-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-12;
font-weight: 900;
color: @theme-color-dark;
i { color: @color-amber; }
}
.resolution-line {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
padding: 1px 0;
strong { font-weight: 700; }
.res-hit { color: @color-hemolymph; font-weight: 700; }
.res-dodged,
.res-parried { color: @color-acid-green; font-weight: 700; }
&.res-wound { color: @color-hemolymph; font-weight: 700; }
&.res-noshock,
&.res-nodamage { color: @color-acid-green; }
}
button.exchange-apply {
margin-top: 6px;
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-11;
padding: 3px 12px;
cursor: pointer;
color: @color-text-light-0;
background: fade(@color-hemolymph, 25%);
border: 1px solid @color-hemolymph;
border-radius: 3px;
.transition();
&:hover:not(:disabled) {
color: @theme-color-dark;
background: @color-hemolymph;
}
&:disabled {
opacity: 0.7;
cursor: default;
color: @color-acid-green;
border-color: @color-acid-green;
}
}
}
// ── Composition du jet ────────────────────────────────────────────
div.roll-pool {
margin: 4px 0;
h4 {
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-11;
margin: 4px 0;
border-bottom: none;
}
.pool-breakdown {
gap: 4px 10px;
.pool-item {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
&.pool-total {
font-weight: 700;
color: @theme-color-light;
}
}
}
}
// ── Résumé des dés ────────────────────────────────────────────────
div.roll-dice-summary {
gap: 6px 12px;
justify-content: center;
margin: 4px 0 2px;
padding: 4px 8px;
background: rgba(0, 0, 0, 0.06);
.die-summary {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
&.human { color: @totem-human-color; }
&.adapted { color: @totem-adapted-color; }
}
}
} }
div.item-card header img { div.item-card header img {
@@ -681,11 +1028,122 @@ ol.chat-log {
} }
} }
// Padding supplémentaire pour les fiches groupe, npc et créature // Padding supplémentaire pour les fiches groupe, npc, créature et communauté
.system-vermine2047 { .system-vermine2047 {
.group-content, .group-content,
.npc-content, .npc-content,
.creature-content { .creature-content,
.community-content {
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-11;
color: @color-amber;
}
}
.exp-learned {
display: flex;
flex-wrap: wrap;
gap: 6px;
span {
padding: 2px 8px;
font-size: @font-size-11;
border: 1px solid @color-border-dark-4;
border-radius: 3px;
opacity: 0.6;
&.learned {
opacity: 1;
color: @color-acid-green;
border-color: @color-acid-green;
}
}
}
}
}
+248
View File
@@ -399,6 +399,254 @@ iframe {
} }
} }
// ── Combat dialog (attaque) ──────────────────────────────────────
.combat-mode-label {
margin: 0;
text-align: center;
font-size: @font-size-14;
color: @color-hemolymph;
text-transform: uppercase;
}
.locked-value {
flex: 1;
display: flex;
align-items: center;
min-height: 28px;
padding: 0 8px;
font-size: @font-size-12;
color: @color-chitin-dark;
background: @color-text-light-0;
border-radius: 3px;
.custom-select-style();
}
.combat-dialog-weapon {
border: 1px solid @color-hemolymph;
padding: 6px 8px;
background: fade(@color-chitin-dark, 40%);
.weapon-block-header {
align-items: center;
gap: 6px;
.weapon-icon { color: @color-hemolymph; }
.weapon-name {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-12;
font-weight: 900;
color: @theme-color-dark;
}
}
.weapon-details {
gap: 4px 12px;
flex-wrap: wrap;
.weapon-detail {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
color: @color-text-light-0;
strong { text-transform: uppercase; }
}
}
}
.combat-difficulty-options {
flex-wrap: wrap;
gap: 8px;
.difficulty-option {
display: flex;
align-items: center;
gap: 4px;
font-family: "Roboto", sans-serif;
text-transform: none;
font-size: @font-size-12;
color: @color-text-light-0;
cursor: pointer;
input[type="radio"] { flex: none; }
&.locked {
opacity: 0.8;
font-style: italic;
}
}
}
.combat-targets {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
padding: 4px 2px;
.label {
font-size: @font-size-11;
color: @color-amber;
}
.combat-target-tag {
padding: 2px 8px;
font-size: @font-size-11;
font-family: "Roboto", sans-serif;
color: @color-text-light-0;
background: fade(@color-hemolymph, 20%);
border: 1px solid fade(@color-hemolymph, 50%);
border-radius: 3px;
}
}
// ── Phase d'Expérience ───────────────────────────────────────────
.exp-phase-characters {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
min-height: 0;
details > div:not(summary) {
padding: 8px;
background: fade(@color-chitin-dark, 30%);
}
}
// ── Dialogue d'apprentissage ─────────────────────────────────────
.learning-summary {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border: 1px solid @color-hemolymph;
background: fade(@color-hemolymph, 10%);
.label {
font-size: @font-size-12;
color: @color-amber;
}
.exp-value {
font-family: "DistressBlack", sans-serif;
font-size: @font-size-20;
color: @color-acid-green;
}
}
.learning-type {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 6px 2px;
label {
display: flex;
align-items: center;
gap: 4px;
font-family: "Roboto", sans-serif;
text-transform: none;
font-size: @font-size-12;
color: @color-text-light-0;
cursor: pointer;
input[type="radio"] { flex: none; }
&:hover { color: @color-amber; }
}
}
.learning-targets {
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px;
border: 1px solid @color-border-dark-4;
background: fade(@color-chitin-dark, 30%);
.learning-hint {
margin: 0;
font-size: @font-size-11;
font-style: italic;
color: @color-text-light-0;
opacity: 0.75;
}
}
.learning-result {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
padding: 8px;
border: 1px solid @color-amber;
background: fade(@color-amber, 8%);
.context-item {
display: flex;
align-items: center;
gap: 6px;
.context-label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
color: @color-amber;
}
.context-value {
font-family: "Roboto", sans-serif;
font-size: @font-size-13;
font-weight: 700;
color: @color-text-light-0;
}
}
}
// ── Transfert de Dés d'Expérience ─────────────────────────────────
.experience-section {
.experience-controls {
gap: 8px;
padding: 8px;
.experience-group {
font-size: @font-size-11;
color: @color-text-light-0;
opacity: 0.8;
#exp-group {
font-weight: 700;
color: @color-acid-green;
}
}
button[data-action="giveToGroup"] {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
padding: 3px 12px;
cursor: pointer;
color: @color-text-light-0;
background: fade(@color-acid-green, 15%);
border: 1px solid @color-acid-green;
border-radius: 3px;
.transition();
&:hover:not(:disabled) {
color: @theme-color-dark;
background: @color-acid-green;
}
&:disabled {
opacity: 0.6;
cursor: default;
}
}
}
}
// ── Total dice pool section ───────────────────────────────────── // ── Total dice pool section ─────────────────────────────────────
.total-section { .total-section {
align-items: center; align-items: center;
+1
View File
@@ -22,6 +22,7 @@
@import "actor/npc"; @import "actor/npc";
@import "actor/group"; @import "actor/group";
@import "actor/creature"; @import "actor/creature";
@import "actor/community";
// 6. Styles des items // 6. Styles des items
@import "items"; @import "items";
+1
View File
@@ -4,6 +4,7 @@ export { default as VermineCharacterSheetV2 } from "./character-sheet.mjs"
export { default as VermineNpcSheetV2 } from "./npc-sheet.mjs" export { default as VermineNpcSheetV2 } from "./npc-sheet.mjs"
export { default as VermineGroupSheetV2 } from "./group-sheet.mjs" export { default as VermineGroupSheetV2 } from "./group-sheet.mjs"
export { default as VermineCreatureSheetV2 } from "./creature-sheet.mjs" export { default as VermineCreatureSheetV2 } from "./creature-sheet.mjs"
export { default as VermineCommunitySheetV2 } from "./community-sheet.mjs"
export { export {
VermineItemSheetV2, VermineItemSheetV2,
VermineWeaponSheetV2, VermineWeaponSheetV2,
@@ -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()
+37 -8
View File
@@ -7,18 +7,21 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
position: { width: 860, height: 720 }, position: { width: 860, height: 720 },
window: { contentClasses: ["character-content"] }, window: { contentClasses: ["character-content"] },
actions: { actions: {
addSpecialty: VermineCharacterSheetV2.#onAddSpecialty addSpecialty: VermineCharacterSheetV2.#onAddSpecialty,
openExperiencePhase: VermineCharacterSheetV2.#onOpenExperiencePhase,
openLearning: VermineCharacterSheetV2.#onOpenLearning
} }
} }
static PARTS = { static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/character-main.hbs" }, main: { template: "systems/vermine2047/templates/actor/appv2/character-main.hbs", scrollable: [""] },
tabs: { template: "templates/generic/tab-navigation.hbs" }, tabs: { template: "templates/generic/tab-navigation.hbs" },
abilities: { template: "systems/vermine2047/templates/actor/appv2/character-abilities.hbs" }, abilities: { template: "systems/vermine2047/templates/actor/appv2/character-abilities.hbs", scrollable: [""] },
totem: { template: "systems/vermine2047/templates/actor/appv2/character-totem.hbs" }, totem: { template: "systems/vermine2047/templates/actor/appv2/character-totem.hbs", scrollable: [""] },
equipment: { template: "systems/vermine2047/templates/actor/appv2/character-equipment.hbs" }, equipment: { template: "systems/vermine2047/templates/actor/appv2/character-equipment.hbs", scrollable: [""] },
stories: { template: "systems/vermine2047/templates/actor/appv2/character-stories.hbs" }, stories: { template: "systems/vermine2047/templates/actor/appv2/character-stories.hbs", scrollable: [""] },
combat: { template: "systems/vermine2047/templates/actor/appv2/character-combat.hbs" } combat: { template: "systems/vermine2047/templates/actor/appv2/character-combat.hbs", scrollable: [""] },
experience: { template: "systems/vermine2047/templates/actor/appv2/character-experience.hbs", scrollable: [""] }
} }
tabGroups = { sheet: "abilities" } tabGroups = { sheet: "abilities" }
@@ -29,7 +32,8 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
totem: { id: "totem", group: "sheet", icon: "fas fa-star", label: "VERMINE.tabs.totem" }, totem: { id: "totem", group: "sheet", icon: "fas fa-star", label: "VERMINE.tabs.totem" },
equipment: { id: "equipment", group: "sheet", icon: "fas fa-hammer", label: "VERMINE.tabs.equipment" }, equipment: { id: "equipment", group: "sheet", icon: "fas fa-hammer", label: "VERMINE.tabs.equipment" },
stories: { id: "stories", group: "sheet", icon: "fas fa-book-open-reader", label: "VERMINE.tabs.stories" }, stories: { id: "stories", group: "sheet", icon: "fas fa-book-open-reader", label: "VERMINE.tabs.stories" },
combat: { id: "combat", group: "sheet", icon: "fas fa-medal", label: "VERMINE.tabs.combat" } combat: { id: "combat", group: "sheet", icon: "fas fa-medal", label: "VERMINE.tabs.combat" },
experience: { id: "experience", group: "sheet", icon: "fas fa-graduation-cap", label: "VERMINE.tabs.experience" }
} }
for (const v of Object.values(tabs)) { for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id v.active = this.tabGroups[v.group] === v.id
@@ -51,6 +55,7 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
case "main": break case "main": break
case "abilities": case "abilities":
context.tab = context.tabs.abilities context.tab = context.tabs.abilities
context.specialties = doc.itemTypes.specialty
break break
case "totem": case "totem":
context.tab = context.tabs.totem context.tab = context.tabs.totem
@@ -73,9 +78,21 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
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
case "experience":
context.tab = context.tabs.experience
context.specialtyCount = doc.itemTypes.specialty.length
const ex = doc.system.experience
context.experience = ex
context.collectiveTotal = (ex.collective.danger + ex.collective.discoveries + ex.collective.duration)
context.individualTotal = (ex.individual.implication + ex.individual.influence + ex.individual.interpretation)
context.currentPhase = ex.phase
context.learnedCurrent = ex.learned.phase === ex.phase
break
} }
return context return context
} }
@@ -95,4 +112,16 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
if (skillKey) itemData.system = { skill: skillKey } if (skillKey) itemData.system = { skill: skillKey }
await this.document.createEmbeddedDocuments("Item", [itemData]) await this.document.createEmbeddedDocuments("Item", [itemData])
} }
static async #onOpenExperiencePhase(event, target) {
const { default: ExperiencePhaseDialog } = await import("../../system/dialogs/experiencePhaseDialog.mjs")
const dialog = await ExperiencePhaseDialog.create()
if (dialog) dialog.render(true)
}
static async #onOpenLearning(event, target) {
const { default: LearningDialog } = await import("../../system/dialogs/learningDialog.mjs")
const dialog = await LearningDialog.create({ actorId: this.document.id })
if (dialog) dialog.render(true)
}
} }
@@ -0,0 +1,123 @@
import VermineBaseActorSheet from "./base-actor-sheet.mjs"
export default class VermineCommunitySheetV2 extends VermineBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ["community"],
position: { width: 760, height: 640 },
window: { contentClasses: ["community-content"] },
actions: {
addObjective: VermineCommunitySheetV2.#onAddObjective,
deleteObjective: VermineCommunitySheetV2.#onDeleteObjective
}
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/community-main.hbs", scrollable: [""] },
tabs: { template: "templates/generic/tab-navigation.hbs" },
identity: { template: "systems/vermine2047/templates/actor/appv2/community-identity.hbs", scrollable: [""] },
domains: { template: "systems/vermine2047/templates/actor/appv2/community-domains.hbs", scrollable: [""] },
resources: { template: "systems/vermine2047/templates/actor/appv2/community-resources.hbs", scrollable: [""] }
}
tabGroups = { sheet: "identity" }
#getTabs() {
const tabs = {
identity: { id: "identity", group: "sheet", icon: "fas fa-info-circle", label: "COMMUNITY.tabs.identity" },
domains: { id: "domains", group: "sheet", icon: "fas fa-chart-simple", label: "COMMUNITY.tabs.domains" },
resources: { id: "resources", group: "sheet", icon: "fas fa-boxes-stacked", label: "COMMUNITY.tabs.resources" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
// Groupe de personnages rattaché
context.resolvedGroup = null
if (this.document.system.groupId) {
const g = game.actors.get(this.document.system.groupId)
if (g) context.resolvedGroup = { id: g.id, name: g.name }
}
// Enrichissement HTML des champs longs (même logique que la fiche PNJ)
const enrich = async (path) => {
const val = foundry.utils.getProperty(this.document.system, path)
return val ? await foundry.applications.ux.TextEditor.implementation.enrichHTML(val, { async: true }) : ""
}
context.enrichedNotes = await enrich("identity.notes")
context.enrichedLandmarks = await enrich("location.landmarks")
context.enrichedTerritory = await enrich("location.territory")
context.enrichedEvents = await enrich("history.events")
context.enrichedRelations = await enrich("history.relations")
return context
}
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main": break
case "identity":
context.tab = context.tabs.identity
context.ageOptions = Object.entries(CONFIG.VERMINE.communityAges).map(([key, label]) => ({ key, label: game.i18n.localize(label) }))
break
case "domains":
context.tab = context.tabs.domains
context.sizeOptions = CONFIG.VERMINE.communitySizeLevels
context.sizeLabel = doc.system.size?.value ? game.i18n.localize(CONFIG.VERMINE.communitySizeLevels[doc.system.size.value]?.label ?? "") : ""
context.sizeInfo = doc.system.size?.value ? CONFIG.VERMINE.communitySizeLevels[doc.system.size.value] : null
// Préparer les 4 Domaines avec leurs 2 sections (jauges) et leur état
context.domains = []
for (const [dkey, dcfg] of Object.entries(CONFIG.VERMINE.communityDomains)) {
const domain = doc.system.domains?.[dkey]
context.domains.push({
key: dkey,
label: game.i18n.localize(dcfg.label),
sections: Object.entries(dcfg.sections).map(([skey, label]) => {
const gauge = domain?.[skey] ?? { value: 0 }
const state = gauge.state || "crisis"
return {
domainKey: dkey,
key: skey,
label: game.i18n.localize(label),
gauge,
state,
stateLabel: game.i18n.localize(CONFIG.VERMINE.communityGaugeStates[state]?.label ?? "COMMUNITY.states.crisis")
}
})
})
}
break
case "resources":
context.tab = context.tabs.resources
break
}
return context
}
changeTab(tab, group, options = {}) {
super.changeTab(tab, group, options)
if (group === "sheet") {
const main = this.element?.querySelector('[data-group="sheet"][data-tab="main"]')
if (main) main.classList.add("active")
}
}
static #onDeleteObjective(event, target) {
const index = parseInt(target.dataset.index)
if (isNaN(index)) return
const objectives = foundry.utils.duplicate(this.document.system.objectives || { major: "", minor: [] })
objectives.minor.splice(index, 1)
this.document.update({ "system.objectives": objectives })
}
static #onAddObjective(event, target) {
const objectives = foundry.utils.duplicate(this.document.system.objectives || { major: "", minor: [] })
objectives.minor.push("")
this.document.update({ "system.objectives": objectives })
}
}
+38 -8
View File
@@ -2,6 +2,10 @@ import VermineBaseActorSheet from "./base-actor-sheet.mjs"
export default class VermineCreatureSheetV2 extends VermineBaseActorSheet { export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
// La fiche créature s'ouvre en mode édition : Gabarit, Taille, Rôle et Meute
// se règlent dès l'ouverture (le mode jeu reste accessible via le toggle).
_sheetMode = this.constructor.SHEET_MODES.EDIT
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ["creature"], classes: ["creature"],
position: { width: 700, height: 650 }, position: { width: 700, height: 650 },
@@ -9,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" }
@@ -61,13 +65,28 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
break break
case "stats": case "stats":
context.tab = context.tabs.stats context.tab = context.tabs.stats
context.patternLabel = doc.system.pattern?.value ? CONFIG.VERMINE.creaturePatternLevels[doc.system.pattern.value]?.label : "" context.patternLabel = doc.system.pattern?.value ? game.i18n.localize(CONFIG.VERMINE.creaturePatternLevels[doc.system.pattern.value]?.label ?? "") : ""
context.sizeLabel = doc.system.size?.value || "" context.sizeLabel = doc.system.size?.value !== undefined ? game.i18n.localize(CONFIG.VERMINE.creatureSizeLevels[doc.system.size.value]?.label ?? "") : ""
context.roleLabel = doc.system.role?.value ? CONFIG.VERMINE.creatureRoleLevels[doc.system.role.value]?.label : "" context.roleLabel = doc.system.role?.value ? game.i18n.localize(CONFIG.VERMINE.creatureRoleLevels[doc.system.role.value]?.label ?? "") : ""
context.packLabel = doc.system.pack?.value || game.i18n.localize("VERMINE.none") context.packLabel = doc.system.pack?.value || game.i18n.localize("VERMINE.none")
// Contribution cumulée de la Taille (les modificateurs se cumulent : Taille N = somme 1..N)
const sizeLevels = CONFIG.VERMINE.creatureSizeLevels || {}
const sizeLevel = doc.system.size?.value ?? 0
context.sizeCumulative = { attack: 0, vigor: 0, minorWound: 0, majorWound: 0, deadlyWound: 0 }
for (let i = 1; i <= sizeLevel; i++) {
const cfg = sizeLevels[i] || {}
context.sizeCumulative.attack += cfg.attack || 0
context.sizeCumulative.vigor += cfg.vigor || 0
context.sizeCumulative.minorWound += cfg.minorWound || 0
context.sizeCumulative.majorWound += cfg.majorWound || 0
context.sizeCumulative.deadlyWound += cfg.deadlyWound || 0
}
break break
case "combat": case "combat":
context.tab = context.tabs.combat context.tab = context.tabs.combat
context.weapons = doc.itemTypes.weapon
context.equippedWeapons = doc.itemTypes.weapon.filter(i => i.system.equipped)
context.equippedDefenses = doc.itemTypes.defense.filter(i => i.system.equipped)
break break
case "effects": case "effects":
context.tab = context.tabs.effects context.tab = context.tabs.effects
@@ -77,4 +96,15 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
} }
return context return context
} }
/** @override - Les créatures n'ont pas de caractéristiques/compétences : on ouvre le dialogue d'attaque (pool calculé). */
async _onRoll(event) {
event.preventDefault()
const el = event.currentTarget
const type = el.dataset.type
if (type !== "creature-attack") return super._onRoll(event)
const { default: CombatDialog } = await import("../../system/dialogs/combatDialog.mjs")
const dialog = await CombatDialog.create({ actorId: this.document.id })
if (dialog) dialog.render(true)
}
} }
+14 -7
View File
@@ -11,17 +11,18 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
deleteMember: VermineGroupSheetV2.#onDeleteMember, deleteMember: VermineGroupSheetV2.#onDeleteMember,
deleteEncounter: VermineGroupSheetV2.#onDeleteEncounter, deleteEncounter: VermineGroupSheetV2.#onDeleteEncounter,
deleteObjective: VermineGroupSheetV2.#onDeleteObjective, deleteObjective: VermineGroupSheetV2.#onDeleteObjective,
addObjective: VermineGroupSheetV2.#onAddObjective addObjective: VermineGroupSheetV2.#onAddObjective,
createTotemAbility: VermineGroupSheetV2.#onCreateTotemAbility
} }
} }
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" }
@@ -67,7 +68,8 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
case "main": break case "main": break
case "info": case "info":
context.tab = context.tabs.info context.tab = context.tabs.info
context.abilities = doc.itemTypes.ability context.abilities = doc.itemTypes.ability.filter(i => i.system.type !== "totem")
context.totem_abilities = doc.itemTypes.ability.filter(i => i.system.type === "totem")
context.specialties = doc.itemTypes.specialty context.specialties = doc.itemTypes.specialty
context.backgrounds = doc.itemTypes.background context.backgrounds = doc.itemTypes.background
context.traumas = doc.itemTypes.trauma context.traumas = doc.itemTypes.trauma
@@ -135,4 +137,9 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
objectives[type].push("") objectives[type].push("")
this.document.update({ "system.objectives": objectives }) this.document.update({ "system.objectives": objectives })
} }
static async #onCreateTotemAbility(event, target) {
const name = game.i18n.localize("ITEMS.new_ability")
await this.document.createEmbeddedDocuments("Item", [{ name, type: "ability", system: { type: "totem" } }])
}
} }
+34 -12
View File
@@ -3,71 +3,93 @@ 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: [""] } }
async _prepareContext() {
const context = await super._prepareContext()
// Compétences d'Entretien possibles (mécanique, technologie, artisanat, animalisme…)
const maintenanceKeys = ["mecanical", "technology", "crafting", "animalism", "environment", "wildlife", "flora", "repulsion", "toxics"]
const current = this.document.system.maintenance || []
context.maintenanceSkills = maintenanceKeys.map(key => ({
key,
label: game.i18n.localize("SKILLS." + key + ".name"),
checked: current.includes(key)
}))
return context
}
/** @override - nettoie le tableau d'Entretien (retire les cases non cochées). */
_prepareSubmitData(event, form, formData, updateData) {
const result = super._prepareSubmitData(event, form, formData, updateData)
if (Array.isArray(result?.system?.maintenance)) {
result.system.maintenance = result.system.maintenance.filter(v => typeof v === "string" && v.trim() !== "")
}
return result
}
} }
// ── 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: [""] } }
} }
+19 -6
View File
@@ -2,6 +2,10 @@ import VermineBaseActorSheet from "./base-actor-sheet.mjs"
export default class VermineNpcSheetV2 extends VermineBaseActorSheet { export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
// La fiche PNJ s'ouvre en mode édition : caractéristiques, compétences et
// niveaux se règlent dès l'ouverture (le mode jeu reste accessible via le toggle).
_sheetMode = this.constructor.SHEET_MODES.EDIT
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ["npc"], classes: ["npc"],
position: { width: 750, height: 680 }, position: { width: 750, height: 680 },
@@ -9,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" }
@@ -24,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" }
@@ -57,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
+1
View File
@@ -2,6 +2,7 @@ export { default as VermineCharacterData } from "./character.mjs"
export { default as VermineNpcData } from "./npc.mjs" export { default as VermineNpcData } from "./npc.mjs"
export { default as VermineGroupData } from "./group.mjs" export { default as VermineGroupData } from "./group.mjs"
export { default as VermineCreatureData } from "./creature.mjs" export { default as VermineCreatureData } from "./creature.mjs"
export { default as VermineCommunityData } from "./community.mjs"
export { default as VermineItemData } from "./item.mjs" export { default as VermineItemData } from "./item.mjs"
export { default as VermineWeaponData } from "./weapon.mjs" export { default as VermineWeaponData } from "./weapon.mjs"
export { default as VermineDefenseData } from "./defense.mjs" export { default as VermineDefenseData } from "./defense.mjs"
+1 -1
View File
@@ -219,7 +219,7 @@ const reqInt = { required: true, nullable: false, integer: true }
export function raritySchema() { export function raritySchema() {
const fields = foundry.data.fields const fields = foundry.data.fields
return new fields.SchemaField({ return new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 3, min: 1, max: 5 }), value: new fields.NumberField({ ...reqInt, initial: 3, min: 1, max: 10 }),
handicap: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }) handicap: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
}) })
} }
+38 -4
View File
@@ -72,8 +72,40 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
attributes: new fields.SchemaField({ attributes: new fields.SchemaField({
xp: attributeSchema(0, 0, 10), xp: attributeSchema(0, 0, 10),
reputation: attributeSchema(0, 0, 10), reputation: attributeSchema(0, 0, 10),
self_control: attributeSchema(0, 0, 5), self_control: attributeSchema(0, 0, 10),
effort: attributeSchema(0, 0, 5) effort: attributeSchema(0, 0, 10)
}),
// Expérience (dés, récompenses, évolution, apprentissage)
experience: new fields.SchemaField({
dice: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
phase: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
collective: new fields.SchemaField({
danger: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
discoveries: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
duration: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 })
}),
individual: new fields.SchemaField({
implication: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
influence: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
interpretation: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 })
}),
evolution: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
traumaReward: new fields.BooleanField({ required: true, initial: false }),
// Bonus de Réserve acquis par apprentissage (s'ajoute au max dérivé)
reserveBonus: new fields.SchemaField({
self_control: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
effort: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 })
}),
// Limites d'apprentissage par phase
learned: new fields.SchemaField({
phase: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
skill: new fields.BooleanField({ required: true, initial: false }),
specialty: new fields.BooleanField({ required: true, initial: false }),
reserve: new fields.BooleanField({ required: true, initial: false }),
characteristic: new fields.BooleanField({ required: true, initial: false }),
evolution: new fields.BooleanField({ required: true, initial: false })
})
}), }),
// Rencontres // Rencontres
@@ -127,7 +159,8 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
const sum = abilities const sum = abilities
.filter(a => a.category === "mental" || a.category === "social") .filter(a => a.category === "mental" || a.category === "social")
.reduce((acc, a) => acc + a.value, 0) .reduce((acc, a) => acc + a.value, 0)
this.attributes.self_control.max = sum + modFromAge const bonus = this.experience?.reserveBonus?.self_control || 0
this.attributes.self_control.max = Math.min(10, sum + modFromAge + bonus)
} }
/** /**
@@ -140,7 +173,8 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
const sum = abilities const sum = abilities
.filter(a => a.category === "physical" || a.category === "manual") .filter(a => a.category === "physical" || a.category === "manual")
.reduce((acc, a) => acc + a.value, 0) .reduce((acc, a) => acc + a.value, 0)
this.attributes.effort.max = sum + modFromAge const bonus = this.experience?.reserveBonus?.effort || 0
this.attributes.effort.max = Math.min(10, sum + modFromAge + bonus)
} }
/** /**
+195
View File
@@ -0,0 +1,195 @@
/**
* DataModel pour les acteurs de type "community" (Communauté).
* La Communauté est l'extension sédentarisée d'un Groupe : elle englobe le
* Groupe de personnages, des Figures et des Habitants répartis en Compagnies.
* Le socle couvre l'identité, la géographie, l'histoire, les objectifs, la
* taille/l'effectif, les quatre Domaines (8 jauges), le Moral et les réserves.
*/
import {
attributeSchema,
levelSchema,
combatStatusSchema
} from "./_shared.mjs"
export default class VermineCommunityData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.community"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
const gauge = () => attributeSchema(0, 0, 10)
return {
// Statut de combat (base, peu utilisé pour une Communauté)
combatStatus: combatStatusSchema("9"),
// Identité
identity: new fields.SchemaField({
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
profile: new fields.StringField({ required: true, nullable: false, initial: "" }),
origin: new fields.StringField({ required: true, nullable: false, initial: "" }),
theme: new fields.StringField({ required: true, nullable: false, initial: "" }),
instincts: new fields.StringField({ required: true, nullable: false, initial: "" }),
prohibits: new fields.StringField({ required: true, nullable: false, initial: "" }),
// Âge de la Communauté : recent | established | old
age: new fields.StringField({ required: true, nullable: false, initial: "recent" }),
notes: new fields.HTMLField({ required: true, initial: "", textSearch: true })
}),
// Géographie / localisation
location: new fields.SchemaField({
sector: new fields.StringField({ required: true, nullable: false, initial: "" }),
placeType: new fields.StringField({ required: true, nullable: false, initial: "" }),
landmarks: new fields.HTMLField({ required: true, initial: "", textSearch: true }),
territory: new fields.HTMLField({ required: true, initial: "", textSearch: true })
}),
// Histoire
history: new fields.SchemaField({
founding: new fields.StringField({ required: true, nullable: false, initial: "" }),
events: new fields.HTMLField({ required: true, initial: "", textSearch: true }),
relations: new fields.HTMLField({ required: true, initial: "", textSearch: true })
}),
// Objectifs : un majeur + des mineurs
objectives: new fields.SchemaField({
major: new fields.StringField({ required: true, nullable: false, initial: "" }),
minor: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" }))
}),
// Taille (1-3) et effectif
size: attributeSchema(1, 1, 3),
population: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
// Domaines : 4 Domaines × 2 sections (jauges en Dés)
domains: new fields.SchemaField({
resources: new fields.SchemaField({
vivre: gauge(),
supplies: gauge()
}),
security: new fields.SchemaField({
defense: gauge(),
care: gauge()
}),
dynamism: new fields.SchemaField({
cohesion: gauge(),
productivity: gauge()
}),
relations: new fields.SchemaField({
human: gauge(),
ecology: gauge()
})
}),
// Moral (comme le Groupe) : value 0-7, level dérivé
morale: new fields.SchemaField({
level: new fields.StringField({ required: true, nullable: false, initial: "normal" }),
value: new fields.NumberField({ ...reqInt, initial: 7, min: 0, max: 7 }),
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: 7, min: 0 })
}),
// Réserve de Groupe
reserve: attributeSchema(0, 0, 10),
// Dés collectifs de la Communauté
material: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
experience: new fields.SchemaField({
dice: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
}),
// Niveau de la Communauté (pour l'expérience des habitants)
level: levelSchema(1, 1, 10),
// Groupe de personnages rattaché (id d'acteur)
groupId: new fields.StringField({ required: true, nullable: false, initial: "" }),
// Habitants (phase 2) : Figures et Compagnies
figures: new fields.ArrayField(new fields.SchemaField({
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
role: new fields.StringField({ required: true, nullable: false, initial: "" }),
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
specialty: new fields.SchemaField({
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
score: new fields.NumberField({ ...reqInt, initial: 3, min: 0, max: 10 })
})
})),
companies: new fields.ArrayField(new fields.SchemaField({
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
specialty: new fields.SchemaField({
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
score: new fields.NumberField({ ...reqInt, initial: 3, min: 0, max: 10 })
}),
level: levelSchema(1, 1, 4),
members: new fields.NumberField({ ...reqInt, initial: 1, min: 0 }),
figureName: new fields.StringField({ required: true, nullable: false, initial: "" })
}))
}
}
/** @override */
prepareDerivedData() {
super.prepareDerivedData()
// 1. État (Crise/Bas/Stable/Opulent) de chaque jauge de Domaine
this._computeGaugeStates()
// 2. Les quatre Domaines sont-ils Bas ? (Moral inactif)
this._computeAllDomainsLow()
// 3. Mettre à jour le niveau de Moral selon la valeur
this._updateMorale()
}
/**
* État d'une jauge selon sa valeur en Dés.
* Crise 0D / Bas 1-2D / Stable 3-5D / Opulent 6D+.
* @param {number} value
* @returns {string}
*/
static gaugeState(value) {
const v = parseInt(value, 10) || 0
if (v >= 6) return "opulent"
if (v >= 3) return "stable"
if (v >= 1) return "low"
return "crisis"
}
_computeGaugeStates() {
for (const domain of Object.values(this.domains)) {
for (const gauge of Object.values(domain)) {
gauge.state = VermineCommunityData.gaugeState(gauge.value)
}
}
}
/**
* Un Domaine est "Bas" lorsque ses deux sections sont à 2D ou moins.
* Si les quatre Domaines sont Bas, le Bonus de Moral est inactif.
*/
_computeAllDomainsLow() {
const domainLow = (domain) => Object.values(domain).every(g => (parseInt(g.value, 10) || 0) <= 2)
this.allDomainsLow = Object.values(this.domains).every(domainLow)
}
/**
* Met à jour le niveau de Moral en fonction de la valeur de dés.
* Règles : 7D = Haut, 6-3D = Normal, 2-1D = Bas, 0D = Crise.
*/
_updateMorale() {
const moraleValue = parseInt(this.morale?.value, 10) || 0
if (this.morale.level && this.morale.level !== "normal") return
if (moraleValue >= 7) {
this.morale.level = "high"
} else if (moraleValue >= 3) {
this.morale.level = "normal"
} else if (moraleValue >= 1) {
this.morale.level = "low"
} else {
this.morale.level = "crisis"
}
}
}
+32 -11
View File
@@ -48,7 +48,7 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
// Niveaux de créature (patron, taille, rôle, meute) // Niveaux de créature (patron, taille, rôle, meute)
pattern: attributeSchema(1, 1, 4), pattern: attributeSchema(1, 1, 4),
size: attributeSchema(1, 1, 3), size: attributeSchema(0, 0, 3),
role: attributeSchema(1, 1, 4), role: attributeSchema(1, 1, 4),
pack: attributeSchema(0, 0, 3), pack: attributeSchema(0, 0, 3),
@@ -97,28 +97,38 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
*/ */
_calculateCreatureComputedValues() { _calculateCreatureComputedValues() {
const patternLevel = this.pattern?.value || 1 const patternLevel = this.pattern?.value || 1
const sizeLevel = this.size?.value || 1 const sizeLevel = this.size?.value || 0
const roleLevel = this.role?.value || 1 const roleLevel = this.role?.value || 1
const packLevel = this.pack?.value || 0 const packLevel = this.pack?.value || 0
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {} const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {}
const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {}
const roleConfig = CONFIG.VERMINE.creatureRoleLevels[roleLevel] || {} const roleConfig = CONFIG.VERMINE.creatureRoleLevels[roleLevel] || {}
const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {} const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {}
// Les modificateurs de Taille se cumulent : une créature de Taille N applique
// les modificateurs de chaque Taille de 1 à N (Taille 0 = insecte, aucun).
const sizeLevels = CONFIG.VERMINE.creatureSizeLevels || {}
let sizeAttack = 0
let sizeVigor = 0
for (let i = 1; i <= sizeLevel; i++) {
const cfg = sizeLevels[i] || {}
sizeAttack += cfg.attack || 0
sizeVigor += cfg.vigor || 0
}
// Attaque : patron + taille + meute + réaction du rôle // Attaque : patron + taille + meute + réaction du rôle
this.computed.attack = (patternConfig.attack || 0) this.computed.attack = (patternConfig.attack || 0)
+ (sizeConfig.attack || 0) + sizeAttack
+ (packConfig.attack || 0) + (packConfig.attack || 0)
+ (roleConfig.reaction || 0) + (roleConfig.reaction || 0)
// Dégâts : patron + vigueur de taille + meute // Dégâts : patron + vigueur de taille + meute
this.computed.damage = (patternConfig.damage || 0) this.computed.damage = (patternConfig.damage || 0)
+ (sizeConfig.vigor || 0) + sizeVigor
+ (packConfig.damage || 0) + (packConfig.damage || 0)
// Vigueur : taille + meute // Vigueur : taille + meute
this.computed.vigor = (sizeConfig.vigor || 0) + (packConfig.damage || 0) this.computed.vigor = sizeVigor + (packConfig.damage || 0)
// Réaction : rôle // Réaction : rôle
this.computed.reaction = (roleConfig.reaction || 0) + (roleConfig.reaction_bonus || 0) this.computed.reaction = (roleConfig.reaction || 0) + (roleConfig.reaction_bonus || 0)
@@ -141,21 +151,32 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
*/ */
_calculateCreatureWoundThresholds() { _calculateCreatureWoundThresholds() {
const patternLevel = this.pattern?.value || 1 const patternLevel = this.pattern?.value || 1
const sizeLevel = this.size?.value || 1 const sizeLevel = this.size?.value || 0
const packLevel = this.pack?.value || 0 const packLevel = this.pack?.value || 0
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {} const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {}
const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {}
const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {} const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {}
// Les modificateurs de Taille se cumulent (Taille N = somme des tailles 1..N).
const sizeLevels = CONFIG.VERMINE.creatureSizeLevels || {}
let sizeMinor = 0
let sizeMajor = 0
let sizeDeadly = 0
for (let i = 1; i <= sizeLevel; i++) {
const cfg = sizeLevels[i] || {}
sizeMinor += cfg.minorWound || 0
sizeMajor += cfg.majorWound || 0
sizeDeadly += cfg.deadlyWound || 0
}
this.minorWound.threshold = (patternConfig.minorWound || 0) this.minorWound.threshold = (patternConfig.minorWound || 0)
+ (sizeConfig.minorWound || 0) + sizeMinor
+ (packConfig.minorWound || 0) + (packConfig.minorWound || 0)
this.majorWound.threshold = (patternConfig.majorWound || 0) this.majorWound.threshold = (patternConfig.majorWound || 0)
+ (sizeConfig.majorWound || 0) + sizeMajor
+ (packConfig.majorWound || 0) + (packConfig.majorWound || 0)
this.deadlyWound.threshold = (patternConfig.deadlyWound || 0) this.deadlyWound.threshold = (patternConfig.deadlyWound || 0)
+ (sizeConfig.deadlyWound || 0) + sizeDeadly
+ (packConfig.deadlyWound || 0) + (packConfig.deadlyWound || 0)
// Max de blessures // Max de blessures
+1
View File
@@ -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 })
+5
View File
@@ -75,6 +75,11 @@ export default class VermineGroupData extends foundry.abstract.TypeDataModel {
// Membres (IDs d'acteurs) // Membres (IDs d'acteurs)
members: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })), members: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
// Dés d'Expérience collectifs (réserve de Groupe)
experience: new fields.SchemaField({
dice: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 })
}),
// Rencontres // Rencontres
encounters: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })) encounters: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" }))
} }
+3 -1
View File
@@ -10,9 +10,11 @@ export default class VermineSpecialtyData extends foundry.abstract.TypeDataModel
/** @override */ /** @override */
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return { return {
name: new fields.StringField({ required: true, nullable: false, initial: "" }), name: new fields.StringField({ required: true, nullable: false, initial: "" }),
skill: new fields.StringField({ required: true, nullable: false, initial: "" }) skill: new fields.StringField({ required: true, nullable: false, initial: "" }),
level: new fields.NumberField({ ...reqInt, initial: 1, min: 0, max: 5 })
} }
} }
} }
+24 -1
View File
@@ -1,7 +1,9 @@
import { baseItemSchema } from "./_shared.mjs" import { baseItemSchema } from "./_shared.mjs"
/** /**
* DataModel pour les items de type "vehicle" (véhicules). * DataModel pour les items de type "vehicle" (véhicules et montures).
* Caractéristiques : Rareté, Fiabilité, Distance, Vitesse (croisière/pointe),
* Entretien (compétences) et Réserve d'Effort (montures).
* @augments {foundry.abstract.TypeDataModel} * @augments {foundry.abstract.TypeDataModel}
*/ */
export default class VermineVehicleData extends foundry.abstract.TypeDataModel { export default class VermineVehicleData extends foundry.abstract.TypeDataModel {
@@ -14,6 +16,27 @@ export default class VermineVehicleData extends foundry.abstract.TypeDataModel {
const reqInt = { required: true, nullable: false, integer: true } const reqInt = { required: true, nullable: false, integer: true }
return { return {
...baseItemSchema(), ...baseItemSchema(),
// Fiabilité étendue (1-10) pour les véhicules
reliability: new fields.NumberField({ ...reqInt, initial: 5, min: 1, max: 10 }),
// Type : "vehicle" (véhicule) ou "mount" (monture)
kind: new fields.StringField({ required: true, nullable: false, initial: "vehicle" }),
// Distance : km parcourus avec un "plein"
distance: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
// Vitesse : croisière / pointe (durée de la pointe)
speed: new fields.SchemaField({
cruise: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
top: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
topDuration: new fields.StringField({ required: true, nullable: false, initial: "10 min" })
}),
// Entretien : compétences nécessaires (clés de CONFIG.VERMINE.skills)
maintenance: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
// Réserve d'Effort (montures)
effortReserve: new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
}),
// Mobilité (héritée)
mobility: new fields.NumberField({ ...reqInt, initial: 3, min: 0 }) mobility: new fields.NumberField({ ...reqInt, initial: 3, min: 0 })
} }
} }
+1
View File
@@ -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({
+55 -3
View File
@@ -139,9 +139,10 @@ VERMINE.creaturePatternLevels = {
* Creature Size Levels configuration * Creature Size Levels configuration
*/ */
VERMINE.creatureSizeLevels = { VERMINE.creatureSizeLevels = {
1: { "label": "SIZE_LEVELS.small", "attack": 2, "vigor": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 }, 0: { "label": "SIZE_LEVELS.tiny", "attack": 0, "vigor": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 },
2: { "label": "SIZE_LEVELS.medium", "attack": 3, "vigor": 2, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 }, 1: { "label": "SIZE_LEVELS.small", "attack": 1, "vigor": 1, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
3: { "label": "SIZE_LEVELS.large", "attack": 4, "vigor": 3, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 } 2: { "label": "SIZE_LEVELS.medium", "attack": 2, "vigor": 2, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 },
3: { "label": "SIZE_LEVELS.large", "attack": 3, "vigor": 3, "minorWound": 3, "majorWound": 3, "deadlyWound": 3 }
} }
/** /**
@@ -468,3 +469,54 @@ VERMINE.totemEffects = {
} }
} }
/**
* Communauté — Domaines et sections (jauges).
*/
VERMINE.communityDomains = {
resources: {
label: "COMMUNITY.domains.resources",
sections: { vivre: "COMMUNITY.sections.vivre", supplies: "COMMUNITY.sections.supplies" }
},
security: {
label: "COMMUNITY.domains.security",
sections: { defense: "COMMUNITY.sections.defense", care: "COMMUNITY.sections.care" }
},
dynamism: {
label: "COMMUNITY.domains.dynamism",
sections: { cohesion: "COMMUNITY.sections.cohesion", productivity: "COMMUNITY.sections.productivity" }
},
relations: {
label: "COMMUNITY.domains.relations",
sections: { human: "COMMUNITY.sections.human", ecology: "COMMUNITY.sections.ecology" }
}
}
/**
* Communauté — Tailles (effectif, Figures, Compagnies recommandés).
*/
VERMINE.communitySizeLevels = {
1: { label: "COMMUNITY.sizeLevels.1", population: "7-50", figures: 3, companies: 5 },
2: { label: "COMMUNITY.sizeLevels.2", population: "51-75", figures: 5, companies: 7 },
3: { label: "COMMUNITY.sizeLevels.3", population: "100+", figures: 7, companies: 9 }
}
/**
* Communauté — États d'une jauge de Domaine.
* Crise 0D / Bas 1-2D / Stable 3-5D / Opulent 6D+.
*/
VERMINE.communityGaugeStates = {
crisis: { label: "COMMUNITY.states.crisis" },
low: { label: "COMMUNITY.states.low" },
stable: { label: "COMMUNITY.states.stable" },
opulent: { label: "COMMUNITY.states.opulent" }
}
/**
* Communauté — Âge (notion de temps).
*/
VERMINE.communityAges = {
recent: "COMMUNITY.age.recent",
established: "COMMUNITY.age.established",
old: "COMMUNITY.age.old"
}
+399
View File
@@ -0,0 +1,399 @@
import { VermineUtils } from "../roll.mjs";
import { VermineExchange } from "../exchange.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
/**
* Dialogue d'attaque pour les échanges de coups.
* Prépare le pool, la difficulté (contact / distance) et le contexte
* d'échange (cibles, dégâts de base) selon le type d'acteur :
* - Personnage : capacité + compétence de l'arme.
* - PNJ : pool d'attaque du niveau de menace + réserves de rôle.
* - Créature : pool d'attaque calculé, difficulté du statut de combat.
*/
export default class CombatDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
static DEFAULT_OPTIONS = {
classes: ["vermine-roll"],
tag: "form",
window: {
icon: "fas fa-crosshairs",
resizable: false
},
position: {
width: 520,
height: 600
},
actions: {
roll: CombatDialog.#onRoll,
cancel: CombatDialog.#onCancel
}
};
static PARTS = {
main: { template: "systems/vermine2047/templates/dialogs/combat-dialog.hbs" }
};
static async create({ actorId, weapon = null }) {
const actor = await game.actors.get(actorId);
if (!actor) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
return null;
}
const mode = actor.type === "creature" ? "contact" : VermineExchange.modeForWeapon(weapon);
return new CombatDialog({ actor, weapon, mode });
}
constructor(options = {}) {
super(options);
this.actor = options.actor;
this.weapon = options.weapon ?? null;
this.mode = options.mode ?? "contact";
this.abilityKey = VermineExchange.attackAbility(this.mode);
this.skillKey = this.weapon?.system?.skill || null;
this.skillValue = this.skillKey ? (this.actor.system?.skills?.[this.skillKey]?.value || 0) : 0;
this.skillPool = this.skillValue ? (CONFIG.VERMINE.SkillLevels?.[this.skillValue]?.dicePool || 0) : 0;
this.skillReroll = this.skillValue ? (CONFIG.VERMINE.SkillLevels?.[this.skillValue]?.reroll || 0) : 0;
this.basePool = VermineExchange.baseAttackPool(this.actor, this.weapon, this.mode);
this.baseDamage = VermineExchange.baseDamage(this.actor, this.weapon);
}
get title() {
return game.i18n.localize("VERMINE.attack");
}
async _prepareContext() {
const actor = this.actor;
const isCharacter = actor.type === "character";
const isNpc = actor.type === "npc";
const isCreature = actor.type === "creature";
const targets = [...game.user.targets].map(t => ({
id: t.actor?.id ?? null,
uuid: t.actor?.uuid ?? null,
name: t.name,
canDefend: t.actor?.type !== "creature"
}));
let difficultyOptions = [];
let lockedDifficulty = null;
let defaultDifficulty = null;
if (isCreature) {
const d = parseInt(actor.system?.combatStatus?.difficulty, 10) || 9;
lockedDifficulty = d;
difficultyOptions = [{ difficulty: d, label: "", locked: true }];
defaultDifficulty = d;
} else if (this.mode === "ranged") {
difficultyOptions = VermineExchange.rangeDifficultyOptions().map(o => ({
difficulty: o.difficulty,
label: game.i18n.localize(o.label),
rangeKey: o.key
}));
defaultDifficulty = difficultyOptions[0]?.difficulty ?? 7;
} else if (isNpc) {
difficultyOptions = VermineExchange.npcDifficultyOptions(actor).map(d => ({
difficulty: d,
label: String(d)
}));
defaultDifficulty = difficultyOptions[0]?.difficulty ?? 7;
} else {
difficultyOptions = VermineExchange.contactDifficultyOptions(this.skillValue).map(d => ({
difficulty: d,
label: String(d)
}));
defaultDifficulty = difficultyOptions[0]?.difficulty ?? 7;
}
// Cible de type créature : défense passive, la difficulté d'attaque est
// forcée au statut de combat de la créature (toute réussite ≥1 touche).
const creatureTargets = targets.filter(t => t.id && !t.canDefend);
const creatureLocked = !isCreature && creatureTargets.length && creatureTargets.length === targets.length;
if (creatureLocked) {
const ref = creatureTargets[0];
const creature = ref.id ? game.actors.get(ref.id) : null;
const actor = creature ?? (ref.uuid ? await fromUuid(ref.uuid) : null);
const d = parseInt(actor?.system?.combatStatus?.difficulty, 10) || 7;
lockedDifficulty = d;
difficultyOptions = [{ difficulty: d, label: "", locked: true }];
defaultDifficulty = d;
}
const threat = actor.system?.threat?.value || 1;
const role = actor.system?.role?.value || 1;
return {
actor,
system: actor.system,
config: CONFIG.VERMINE,
actorType: actor.type,
mode: this.mode,
weapon: this.weapon,
abilityKey: this.abilityKey,
abilityValue: actor.system?.abilities?.[this.abilityKey]?.value || 0,
skillKey: this.skillKey,
skillValue: this.skillValue,
basePool: this.basePool,
baseDamage: this.baseDamage,
isCharacter,
isNpc,
isCreature,
creatureLocked,
npcThreatPool: isNpc ? (CONFIG.VERMINE.npcThreatLevels?.[threat]?.attack || 0) : 0,
npcPoolMax: isNpc ? (CONFIG.VERMINE.npcRoleLevels?.[role]?.pools || 0) : 0,
difficultyOptions,
lockedDifficulty,
defaultDifficulty,
hasAmmo: this.mode === "ranged" && (this.weapon?.system?.ammo ?? 0) > 0,
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
availableItems: actor.items.filter(i => i.type === "item"),
targets
};
}
async _onRender(context, options) {
this.element.dataset.actorId = this.actor.id;
this.element.dataset.weaponId = this.weapon?.id ?? "";
this.element.dataset.mode = this.mode;
for (const inp of this.element.querySelectorAll("[data-roll]")) {
inp.addEventListener("change", this.#onInputChange.bind(this));
}
const selfControl = this.element.querySelector("#self_control");
if (selfControl) {
selfControl.addEventListener("change", this.#onChangeSelfControl.bind(this));
}
this.element.querySelector("#second-action")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#human-totem")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#adapted-totem")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#pools-spent")?.addEventListener("change", () => this.#updateUI());
const ability = this.element.querySelector("#ability");
if (ability && this.actor.type === "character") {
const sc = this.element.querySelector("#self_control");
if (sc) sc.max = ability.value;
}
this.#updateUI();
}
// ── Getters ──────────────────────────────────────────────────────────
get #el() { return this.element; }
#getDifficultyBase() {
const el = this.#el.querySelector('input[name="combat-difficulty"]:checked');
return parseInt(el?.value, 10) || 7;
}
#getSecondAction() {
return this.#el.querySelector("#second-action")?.checked ?? false;
}
#getHandicap() {
const sel = this.#el.querySelector("#handicap");
return Math.max(0, (parseInt(sel?.value, 10) || 1) - 1);
}
#getSelfCtrl() {
return parseInt(this.#el.querySelector("#self_control")?.value, 10) || 0;
}
#getPoolsSpent() {
return parseInt(this.#el.querySelector("#pools-spent")?.value, 10) || 0;
}
#getUseAmmo() {
return this.#el.querySelector("#use-ammo")?.checked ?? false;
}
#getTotems() {
return {
human: this.#el.querySelector("#human-totem")?.checked ?? false,
adapted: this.#el.querySelector("#adapted-totem")?.checked ?? false
};
}
#getKeepTotem() {
return this.#el.querySelector("#keep-totem-select")?.value ?? null;
}
getDifficultySelect() {
return this.#getDifficultyBase() + (this.#getSecondAction() ? 2 : 0);
}
getSkillCategory() {
if (this.actor.type !== "character" || !this.skillKey) return null;
return this.actor.system.skills[this.skillKey]?.category ?? null;
}
getSkillLevel() {
return this.actor.type === "character" && this.skillValue ? this.skillValue : null;
}
getReroll() {
if (this.actor.type === "npc") {
const exp = this.actor.system?.experience?.value || 1;
return CONFIG.VERMINE.npcExperienceLevels?.[exp]?.rerolls || 0;
}
if (this.actor.type === "creature") return 0;
return this.skillReroll;
}
getMaxEffort() {
if (this.actor.type === "character") return this.actor.system?.abilities?.[this.abilityKey]?.value || 0;
return 0;
}
getPoolBreakdown() {
const ability = this.actor.type === "character"
? (this.actor.system?.abilities?.[this.abilityKey]?.value || 0)
: 0;
const skill = this.actor.type === "character" ? this.skillPool : 0;
const selfControl = this.#getSelfCtrl();
const pools = this.actor.type === "npc" ? this.#getPoolsSpent() : 0;
const specialty = this.#hasSpecialtySelected() ? 1 : 0;
const help = this.#el.querySelector("#helped")?.checked ? 1 : 0;
const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked");
const tooling = toolsChecked && toolsChecked.value !== "0" ? 1 : 0;
const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
const total = this.basePool + selfControl + pools + specialty + help + tooling + group;
return { ability, skill, selfControl, pools, specialty, help, tooling, group, total };
}
getDicePool() {
return this.getPoolBreakdown().total;
}
#hasSpecialtySelected() {
const checked = this.#el.querySelector("input[name='usingSpecialization']:checked");
return Boolean(checked && checked.value !== "aucune");
}
getSpecialtyName() {
const checked = this.#el.querySelector("input[name='usingSpecialization']:checked");
return checked && checked.value !== "aucune" ? checked.value : null;
}
getLabel() {
const base = this.mode === "ranged"
? game.i18n.localize("VERMINE.attack_ranged")
: game.i18n.localize("VERMINE.attack_contact");
return this.weapon ? `${base} (${this.weapon.name})` : base;
}
// ── UI ───────────────────────────────────────────────────────────────
#updateUI() {
const total = this.getDicePool();
const totalEl = this.#el.querySelector("#dice-pool-total");
if (totalEl) totalEl.textContent = `${total}D`;
const finalEls = this.#el.querySelectorAll("#final-difficulty");
for (const el of finalEls) el.textContent = this.getDifficultySelect();
const b = this.getPoolBreakdown();
const bonusEl = this.#el.querySelector("#total-bonus");
if (bonusEl) bonusEl.textContent = b.selfControl + b.pools + b.specialty + b.help + b.tooling + b.group;
const handSel = this.#el.querySelector("#handicap");
const handEl = this.#el.querySelector("#current-handicap");
if (handSel && handEl) {
handEl.textContent = handSel.options[handSel.selectedIndex]?.text ?? "";
}
}
#onInputChange() {
this.#updateUI();
}
#onChangeSelfControl(ev) {
const valEl = this.#el.querySelector("#self_control_value");
if (valEl) valEl.textContent = ev.currentTarget.value;
this.#updateUI();
}
static async #onCancel(event, target) {
this.close();
}
static async #onRoll(event, target) {
const selfCtrl = this.getPoolBreakdown().selfControl;
if (selfCtrl > 0) {
const current = this.actor?.system?.attributes?.self_control?.value ?? 0;
if (current < selfCtrl) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_not_enough_self_control"));
return;
}
}
const finalDifficulty = this.getDifficultySelect();
// Consommation de munitions (armes à distance)
if (this.mode === "ranged" && this.#getUseAmmo() && this.weapon) {
const ammo = this.weapon.system.ammo || 0;
if (ammo > 0) {
await this.weapon.update({ "system.ammo": ammo - 1 });
}
}
if (selfCtrl > 0) {
const newVal = this.actor.system.attributes.self_control.value - selfCtrl;
await this.actor.update({ "system.attributes.self_control.value": newVal });
}
const targets = [...game.user.targets].map(t => ({
id: t.actor?.id ?? null,
uuid: t.actor?.uuid ?? null,
name: t.name,
canDefend: t.actor?.type !== "creature"
}));
const attack = {
id: foundry.utils.randomID(),
attackerId: this.actor.id,
attackerName: this.actor.name,
mode: this.mode,
difficulty: finalDifficulty,
range: this.mode === "ranged" ? this.#getRangeKey() : null,
secondAction: this.#getSecondAction(),
baseDamage: this.baseDamage,
targets,
weapon: this.weapon
? { name: this.weapon.name, img: this.weapon.img, damage: foundry.utils.duplicate(this.weapon.system.damage) }
: null
};
await VermineUtils.roll({
actor: this.actor,
NoD: this.getDicePool(),
Reroll: this.getReroll(),
difficulty: finalDifficulty,
handicap: this.#getHandicap(),
rollLabel: this.getLabel(),
totems: this.#getTotems(),
self_control: selfCtrl,
max_effort: this.getMaxEffort(),
keepTotem: this.#getKeepTotem(),
skillCategory: this.getSkillCategory(),
skillLevel: this.getSkillLevel(),
hasSpecialty: this.#hasSpecialtySelected(),
poolBreakdown: this.getPoolBreakdown(),
specialtyName: this.getSpecialtyName(),
weapon: this.weapon?.toObject() ?? null,
targets: targets.map(t => t.name),
attack,
messageFlags: { "vermine-exchange": attack }
});
this.close();
}
#getRangeKey() {
const el = this.#el.querySelector('input[name="combat-difficulty"]:checked');
return el?.dataset?.rangeKey ?? null;
}
}
@@ -0,0 +1,150 @@
const { HandlebarsApplicationMixin } = foundry.applications.api;
/**
* Dialogue du meneur pour ouvrir une phase d'Expérience.
* Permet d'attribuer les Dés d'Expérience collectifs (Danger, Découvertes,
* Durée) au Groupe et aux personnages, ainsi que les Dés individuels
* (Implication, Influence, Interprétation), les récompenses d'Objectifs et
* la récompense de Traumatisme choisi.
*/
export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
static DEFAULT_OPTIONS = {
classes: ["vermine-roll"],
tag: "form",
window: {
icon: "fas fa-star",
resizable: true
},
position: {
width: 620,
height: 700
},
actions: {
apply: ExperiencePhaseDialog.#onApply,
cancel: ExperiencePhaseDialog.#onCancel
}
};
static PARTS = {
main: { template: "systems/vermine2047/templates/dialogs/experience-phase-dialog.hbs", scrollable: [""] }
};
static async create() {
return new ExperiencePhaseDialog();
}
get title() {
return game.i18n.localize("VERMINE.experience_phase");
}
async _prepareContext() {
const characters = game.actors.filter(a => a.type === "character");
const groups = game.actors.filter(a => a.type === "group");
return {
characters: characters.map(a => ({
id: a.id,
name: a.name,
dice: a.system?.experience?.dice || 0
})),
groups: groups.map(a => ({
id: a.id,
name: a.name,
dice: a.system?.experience?.dice || 0
})),
rewards: [
{ value: 0, label: game.i18n.localize("VERMINE.reward_anecdotic") },
{ value: 1, label: game.i18n.localize("VERMINE.reward_minor") },
{ value: 2, label: game.i18n.localize("VERMINE.reward_major") },
{ value: 3, label: game.i18n.localize("VERMINE.reward_exceptionnal") }
]
};
}
async _onRender(context, options) {
this.element.querySelectorAll("[data-roll]").forEach(inp => inp.addEventListener("change", () => this.#updateUI()));
this.#updateUI();
}
#updateUI() {
const danger = parseInt(this.element.querySelector("#collective-danger")?.value, 10) || 0;
const discoveries = parseInt(this.element.querySelector("#collective-discoveries")?.value, 10) || 0;
const duration = parseInt(this.element.querySelector("#collective-duration")?.value, 10) || 0;
const el = this.element.querySelector("#collective-total");
if (el) el.textContent = `${danger + discoveries + duration}D`;
}
static async #onCancel(event, target) {
this.close();
}
static async #onApply(event, target) {
const collective = (parseInt(this.element.querySelector("#collective-danger")?.value, 10) || 0)
+ (parseInt(this.element.querySelector("#collective-discoveries")?.value, 10) || 0)
+ (parseInt(this.element.querySelector("#collective-duration")?.value, 10) || 0);
const groupId = this.element.querySelector("#group-select")?.value || null;
if (groupId) {
const group = game.actors.get(groupId);
if (group && collective > 0) {
const current = group.system?.experience?.dice || 0;
await group.update({ "system.experience.dice": current + collective });
}
}
const results = [];
for (const row of this.element.querySelectorAll("[data-character-id]")) {
const id = row.dataset.characterId;
const actor = game.actors.get(id);
if (!actor) continue;
const implication = parseInt(row.querySelector(`[name="implication-${id}"]`)?.value, 10) || 0;
const influence = parseInt(row.querySelector(`[name="influence-${id}"]`)?.value, 10) || 0;
const interpretation = parseInt(row.querySelector(`[name="interpretation-${id}"]`)?.value, 10) || 0;
const objectiveMinor = row.querySelector(`[name="objective-minor-${id}"]`)?.checked ? 3 : 0;
const objectiveMajor = row.querySelector(`[name="objective-major-${id}"]`)?.checked ? 5 : 0;
const trauma = row.querySelector(`[name="trauma-${id}"]`)?.checked ? 5 : 0;
const individual = implication + influence + interpretation;
const bonus = objectiveMinor + objectiveMajor + trauma;
const gained = collective + individual + bonus;
const ex = actor.system?.experience;
const nextPhase = (ex?.phase || 0) + 1;
const update = {
"system.experience.phase": nextPhase,
"system.experience.dice": (ex?.dice || 0) + gained,
"system.experience.collective.danger": parseInt(this.element.querySelector("#collective-danger")?.value, 10) || 0,
"system.experience.collective.discoveries": parseInt(this.element.querySelector("#collective-discoveries")?.value, 10) || 0,
"system.experience.collective.duration": parseInt(this.element.querySelector("#collective-duration")?.value, 10) || 0,
"system.experience.individual.implication": implication,
"system.experience.individual.influence": influence,
"system.experience.individual.interpretation": interpretation,
"system.experience.traumaReward": trauma > 0,
"system.experience.learned.phase": nextPhase,
"system.experience.learned.skill": false,
"system.experience.learned.specialty": false,
"system.experience.learned.reserve": false,
"system.experience.learned.characteristic": false,
"system.experience.learned.evolution": false
};
await actor.update(update);
results.push({ name: actor.name, gained });
}
const lines = [`${game.i18n.localize("VERMINE.experience_phase")} +1`];
if (groupId) {
const group = game.actors.get(groupId);
lines.push(`${group?.name ?? ""} (${game.i18n.localize("VERMINE.collective")}): +${collective}D`);
}
for (const r of results) {
lines.push(`${r.name}: +${r.gained}D`);
}
await ChatMessage.create({
user: game.user?._id,
speaker: ChatMessage.getSpeaker(),
content: `<div class="vermine-roll-message"><h3>${game.i18n.localize("VERMINE.experience_rewards")}</h3><div class="roll-context flexrow flex-wrap">${lines.map(l => `<span class="context-item">${l}</span>`).join("")}</div></div>`
});
this.close();
}
}
+327
View File
@@ -0,0 +1,327 @@
import { VermineUtils } from "../roll.mjs";
import { VermineExperience } from "../experience.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
/**
* Dialogue d'apprentissage (phase d'Expérience).
* Permet de dépenser des Dés d'Expérience pour tenter de développer une
* Compétence, une Spécialité, une Caractéristique, une Réserve ou un
* Dé d'Évolution. La progression est appliquée automatiquement en cas de
* réussite.
*/
export default class LearningDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
#actor;
static DEFAULT_OPTIONS = {
classes: ["vermine-roll"],
tag: "form",
window: {
icon: "fas fa-graduation-cap",
resizable: false
},
position: {
width: 560,
height: 640
},
actions: {
roll: LearningDialog.#onRoll,
cancel: LearningDialog.#onCancel
}
};
static PARTS = {
main: { template: "systems/vermine2047/templates/dialogs/learning-dialog.hbs", scrollable: [""] }
};
static async create({ actorId }) {
const actor = await game.actors.get(actorId);
if (!actor) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
return null;
}
return new LearningDialog({ actor });
}
constructor(options = {}) {
super(options);
this.#actor = options.actor;
}
get title() {
return game.i18n.localize("VERMINE.open_learning");
}
async _prepareContext() {
const actor = this.#actor;
const skills = Object.entries(CONFIG.VERMINE.skills).map(([key, cfg]) => ({
key,
name: game.i18n.localize("SKILLS." + key + ".name"),
category: cfg.category,
value: actor.system?.skills?.[key]?.value || 0,
target: Math.min(5, (actor.system?.skills?.[key]?.value || 0) + 1),
rarity: actor.system?.skills?.[key]?.rarity || 0,
isDomain: VermineExperience.isPreferredDomain(actor, key)
}));
return {
actor,
system: actor.system,
config: CONFIG.VERMINE,
dice: actor.system?.experience?.dice || 0,
ageType: actor.system?.identity?.ageType || 2,
abilities: Object.entries(CONFIG.VERMINE.abilities).map(([key, label]) => ({
key,
label: game.i18n.localize(label),
value: actor.system?.abilities?.[key]?.value || 0
})),
reserves: [
{ key: "self_control", label: game.i18n.localize("VERMINE.self_control"), value: actor.system?.attributes?.self_control?.value || 0, max: actor.system?.attributes?.self_control?.max || 0 },
{ key: "effort", label: game.i18n.localize("VERMINE.effort"), value: actor.system?.attributes?.effort?.value || 0, max: actor.system?.attributes?.effort?.max || 0 }
],
specialties: actor.itemTypes?.specialty || [],
skills,
specialtyCount: (actor.itemTypes?.specialty || []).length
};
}
async _onRender(context, options) {
this.element.dataset.actorId = this.#actor.id;
for (const inp of this.element.querySelectorAll("[data-roll]")) {
inp.addEventListener("change", () => this.#updateUI());
}
this.element.querySelector("#learning-dice")?.addEventListener("input", () => this.#updateUI());
this.#updateUI();
}
get #el() { return this.element; }
#getType() {
return this.#el.querySelector('input[name="learning-type"]:checked')?.value ?? "skill";
}
#getTargetKey() {
const type = this.#getType();
if (type === "characteristic") return this.#el.querySelector("#learning-ability")?.value ?? "";
if (type === "reserve") return this.#el.querySelector("#learning-reserve")?.value ?? "";
return this.#el.querySelector("#learning-target")?.value ?? "";
}
#getDiceSpent() {
return Math.max(1, parseInt(this.#el.querySelector("#learning-dice")?.value, 10) || 1);
}
#getSpecialtyName() {
return this.#el.querySelector("#specialty-name")?.value?.trim() || game.i18n.localize("ITEMS.new_specialty");
}
#compute() {
const actor = this.#actor;
const type = this.#getType();
const key = this.#getTargetKey();
const difficulty = 7;
let handicap = 0;
let poolModifier = 0;
let label = "";
let targetLevel = null;
if (type === "skill") {
const current = key ? actor.system?.skills?.[key]?.value || 0 : 0;
targetLevel = Math.min(5, current + 1);
const rarity = key ? actor.system?.skills?.[key]?.rarity || 0 : 0;
label = game.i18n.localize(VermineExperience.skillLevelLabelKey(targetLevel));
const diff = VermineExperience.skillLearningDifficulty(targetLevel);
const domain = key ? VermineExperience.isPreferredDomain(actor, key) : false;
return {
type,
key,
targetLevel,
difficulty: domain ? Math.max(3, diff - 2) : diff,
handicap: VermineExperience.skillLearningHandicap(targetLevel, rarity),
poolModifier: 0,
label,
targetLabel: key ? game.i18n.localize("SKILLS." + key + ".name") : ""
};
}
if (type === "specialty") {
const current = key ? actor.system?.skills?.[key]?.value || 0 : 0;
const domain = key ? VermineExperience.isPreferredDomain(actor, key) : false;
label = game.i18n.localize("VERMINE.specialty");
return {
type,
key,
targetLevel: current,
difficulty: VermineExperience.specialtyLearningDifficulty(current, domain),
handicap: VermineExperience.specialtyLearningHandicap(actor.itemTypes?.specialty?.length || 0),
poolModifier: 0,
label,
targetLabel: key ? game.i18n.localize("SKILLS." + key + ".name") : ""
};
}
if (type === "characteristic") {
const current = key ? actor.system?.abilities?.[key]?.value || 0 : 0;
label = game.i18n.localize("VERMINE.characteristic");
return {
type,
key,
targetLevel: current,
difficulty: VermineExperience.characteristicLearningDifficulty(),
handicap: current,
poolModifier: 0,
label,
targetLabel: key ? game.i18n.localize(CONFIG.VERMINE.abilities[key]) : ""
};
}
if (type === "reserve") {
const current = key ? actor.system?.attributes?.[key]?.value || 0 : 0;
label = game.i18n.localize("VERMINE.reserve");
return {
type,
key,
targetLevel: current,
difficulty: VermineExperience.reserveLearningDifficulty(current),
handicap: VermineExperience.ageHandicap(actor.system?.identity?.ageType || 2),
poolModifier: 0,
label,
targetLabel: key ? game.i18n.localize(key === "self_control" ? "VERMINE.self_control" : "VERMINE.effort") : ""
};
}
// evolution
const human = actor.system?.adaptation?.totems?.human?.value || 0;
const adapted = actor.system?.adaptation?.totems?.adapted?.value || 0;
label = game.i18n.localize("VERMINE.evolution_dice");
return {
type,
key,
targetLevel: actor.system?.experience?.evolution || 0,
difficulty: VermineExperience.evolutionDifficulty(actor.system?.identity?.ageType || 2),
handicap: actor.system?.experience?.evolution || 0,
poolModifier: VermineExperience.evolutionTotemModifier(human, adapted),
label,
targetLabel: ""
};
}
#updateUI() {
const type = this.#getType();
const show = (id, visible) => {
const el = this.#el.querySelector(id);
if (el) el.style.display = visible ? "" : "none";
};
show("#learning-skill-section", type === "skill" || type === "specialty");
show("#learning-specialty-section", type === "specialty");
show("#learning-ability-section", type === "characteristic");
show("#learning-reserve-section", type === "reserve");
show("#learning-evolution-section", type === "evolution");
// Pour une Spécialité, seule une compétence au moins Confirmée (≥2) est valable.
const skillSelect = this.#el.querySelector("#learning-target");
if (skillSelect) {
for (const opt of skillSelect.options) {
const value = parseInt(opt.dataset.value, 10) || 0;
opt.hidden = (type === "specialty") ? value < 2 : false;
}
}
const c = this.#compute();
const set = (sel, txt) => { const el = this.#el.querySelector(sel); if (el) el.textContent = txt; };
set("#diff-value", c.difficulty);
set("#handicap-value", c.handicap);
set("#required-value", 1 + c.handicap);
const pool = Math.max(0, this.#getDiceSpent() + c.poolModifier);
set("#pool-value", `${pool}D${c.poolModifier ? ` (${c.poolModifier >= 0 ? "+" : ""}${c.poolModifier}D)` : ""}`);
const poolInput = this.#el.querySelector("#learning-dice");
if (poolInput) poolInput.max = this.#actor.system?.experience?.dice || 0;
}
static async #onCancel(event, target) {
this.close();
}
static async #onRoll(event, target) {
const actor = this.#actor;
const ex = actor.system?.experience;
const dice = ex?.dice || 0;
const spent = Math.min(this.#getDiceSpent(), dice);
if (spent < 1) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_experience_dice"));
return;
}
const c = this.#compute();
// Limite : une seule compétence, spécialité, réserve, caractéristique et
// un seul Dé d'Évolution par phase.
if (ex?.learned?.phase === ex?.phase) {
const flagMap = { skill: "skill", specialty: "specialty", reserve: "reserve", characteristic: "characteristic", evolution: "evolution" };
if (ex.learned[flagMap[c.type]]) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_learning_limit"));
return;
}
}
if (c.type === "skill" && c.targetLevel !== null && (actor.system?.skills?.[c.key]?.value || 0) >= 5) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_skill_max"));
return;
}
if (c.type === "characteristic" && c.targetLevel !== null && (actor.system?.abilities?.[c.key]?.value || 0) >= 3) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_characteristic_max"));
return;
}
if (c.type === "specialty" && c.targetLevel !== null && c.targetLevel < 2) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_specialty_requirement"));
return;
}
if (c.type === "reserve" && c.targetLevel !== null && (actor.system?.attributes?.[c.key]?.max || 0) >= 10) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_reserve_max"));
return;
}
const pool = Math.max(0, spent + c.poolModifier);
const rollLabel = `${game.i18n.localize("VERMINE.open_learning")}${c.label}${c.targetLabel ? " (" + c.targetLabel + ")" : ""}`;
const roll = await VermineUtils.roll({
actor,
NoD: pool,
Reroll: 0,
difficulty: c.difficulty,
handicap: c.handicap,
rollLabel,
poolBreakdown: { ability: 0, skill: 0, selfControl: 0, specialty: 0, help: 0, tooling: 0, group: 0, total: pool },
messageFlags: { "vermine-learning": { type: c.type, key: c.key, spent, success: false } }
});
const required = 1 + c.handicap;
const specialtyName = this.#getSpecialtyName();
if ((roll._total ?? 0) >= required) {
await this.#applySuccess(c, spent, specialtyName);
}
this.close();
}
async #applySuccess(c, spent, specialtyName) {
const actor = this.#actor;
const updates = { "system.experience.dice": (actor.system.experience.dice || 0) - spent };
if (c.type === "skill") {
const current = actor.system.skills[c.key].value || 0;
updates["system.skills." + c.key + ".value"] = Math.min(5, current + 1);
updates["system.experience.learned.skill"] = true;
} else if (c.type === "specialty") {
await actor.createEmbeddedDocuments("Item", [{ name: specialtyName, type: "specialty", system: { skill: c.key } }]);
updates["system.experience.learned.specialty"] = true;
} else if (c.type === "characteristic") {
const current = actor.system.abilities[c.key].value || 0;
updates["system.abilities." + c.key + ".value"] = Math.min(3, current + 1);
updates["system.experience.learned.characteristic"] = true;
} else if (c.type === "reserve") {
const bonus = actor.system.experience.reserveBonus?.[c.key] || 0;
updates["system.experience.reserveBonus." + c.key] = bonus + 1;
updates["system.experience.learned.reserve"] = true;
} else {
updates["system.experience.evolution"] = (actor.system.experience.evolution || 0) + 1;
updates["system.experience.learned.evolution"] = true;
}
updates["system.experience.learned.phase"] = actor.system.experience.phase;
await actor.update(updates);
ui.notifications.info(game.i18n.localize("VERMINE.learning_success"));
}
}
+170 -19
View File
@@ -23,7 +23,8 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
}, },
actions: { actions: {
roll: RollDialog.#onRoll, roll: RollDialog.#onRoll,
cancel: RollDialog.#onCancel cancel: RollDialog.#onCancel,
giveToGroup: RollDialog.#onGiveToGroup
} }
}; };
@@ -32,6 +33,9 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
}; };
static async create(data = {}) { static async create(data = {}) {
if (data.actor instanceof Actor) {
return new RollDialog({ actor: data.actor, label: data.label, rolltype: data.rolltype, weapon: data.weapon, locks: data.locks, defense: data.defense });
}
const actorId = data.actorId ?? game.user.character?.id ?? canvas.tokens.controlled[0]?.actor?.id; const actorId = data.actorId ?? game.user.character?.id ?? canvas.tokens.controlled[0]?.actor?.id;
if (!actorId || typeof actorId !== "string") { if (!actorId || typeof actorId !== "string") {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected")); ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
@@ -42,7 +46,7 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected")); ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
return null; return null;
} }
return new RollDialog({ actor, label: data.label, rolltype: data.rolltype }); return new RollDialog({ actor, label: data.label, rolltype: data.rolltype, weapon: data.weapon, locks: data.locks, defense: data.defense });
} }
constructor(options = {}) { constructor(options = {}) {
@@ -50,10 +54,16 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
this.#actor = options.actor; this.#actor = options.actor;
this.label = options.label ?? null; this.label = options.label ?? null;
this.rolltype = options.rolltype ?? null; this.rolltype = options.rolltype ?? null;
this.weapon = options.weapon ?? null;
this.locks = options.locks ?? null;
this.defense = options.defense ?? null;
} }
async _prepareContext() { async _prepareContext() {
const actor = this.#actor; const actor = this.#actor;
const isCharacter = actor.type === "character";
const group = isCharacter ? RollDialog.#findGroup(actor) : null;
const groupPool = group?.system?.experience?.dice || 0;
return { return {
actor, actor,
system: actor.system, system: actor.system,
@@ -66,13 +76,28 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
help: false, help: false,
specialty: false, specialty: false,
availableSpecialties: actor.items.filter(i => i.type === "specialty"), availableSpecialties: actor.items.filter(i => i.type === "specialty"),
availableItems: actor.items.filter(i => i.type === "item") availableItems: actor.items.filter(i => i.type === "item"),
locks: this.locks,
experience: {
hasExperience: isCharacter,
individual: isCharacter ? actor.system?.experience?.dice || 0 : 0,
groupPool,
pullOptions: [0, 1, 2].filter(v => v <= Math.min(2, groupPool)),
canPull: Boolean(group) && (game.user.isGM || group.isOwner),
canGive: isCharacter && (game.user.isGM || actor.isOwner)
}
}; };
} }
async _onRender(context, options) { async _onRender(context, options) {
this.element.dataset.actorId = this.#actor.id; this.element.dataset.actorId = this.#actor.id;
// Mode verrouillé (défense en échange de coups) : présélectionne et bloque
// caractéristique + difficulté, présélectionne la compétence.
if (this.locks) {
this.#applyLocks(this.locks);
}
for (const inp of this.element.querySelectorAll("[data-roll]")) { for (const inp of this.element.querySelectorAll("[data-roll]")) {
inp.addEventListener("change", this.#onInputChange.bind(this)); inp.addEventListener("change", this.#onInputChange.bind(this));
} }
@@ -95,6 +120,7 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
this.element.querySelector("#adapted-totem")?.addEventListener("change", () => this.#updateUI()); this.element.querySelector("#adapted-totem")?.addEventListener("change", () => this.#updateUI());
this.#displaySpecialties(); this.#displaySpecialties();
this.#refreshExperienceUI();
this.#updateUI(); this.#updateUI();
if (ability?.value !== "0") { if (ability?.value !== "0") {
@@ -111,19 +137,44 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
#getDifficulty() { return this.#el.querySelector("#difficulty"); } #getDifficulty() { return this.#el.querySelector("#difficulty"); }
#getHandicap() { return this.#el.querySelector("#handicap"); } #getHandicap() { return this.#el.querySelector("#handicap"); }
#getSelfCtrl() { return this.#el.querySelector("#self_control"); } #getSelfCtrl() { return this.#el.querySelector("#self_control"); }
#getExperiencePull() { return parseInt(this.#el.querySelector("#experience-pull")?.value, 10) || 0; }
getDicePool() { /**
* Trouve le premier Groupe dont le personnage est membre.
* @param {Actor} actor
* @returns {Actor|null}
*/
static #findGroup(actor) {
return game.actors.filter(a => a.type === "group" && (a.system?.members ?? []).includes(actor.id))[0] ?? null;
}
/** Décomposition du pool de dés pour affichage dans le message. */
getPoolBreakdown() {
const abil = this.#getAbility(); const abil = this.#getAbility();
const abilVal = parseInt(abil?.options[abil?.selectedIndex]?.value, 10) || 0; const ability = parseInt(abil?.options[abil?.selectedIndex]?.value, 10) || 0;
const skill = this.#getSkill(); const skill = this.#getSkill();
const skillPool = parseInt(skill?.options[skill?.selectedIndex]?.dataset?.pool, 10) || 0; const skillPool = parseInt(skill?.options[skill?.selectedIndex]?.dataset?.pool, 10) || 0;
const sc = parseInt(this.#getSelfCtrl()?.value, 10) || 0; const selfControl = parseInt(this.#getSelfCtrl()?.value, 10) || 0;
const specChecked = this.#el.querySelector("#usingSpecialization")?.checked; const specChecked = this.hasSpecialtySelected();
const helped = this.#el.querySelector("#helped")?.checked; const helped = this.#el.querySelector("#helped")?.checked;
const tools = this.#el.querySelector("input[name='usingTools']:checked")?.value !== "0"; const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked");
const tools = toolsChecked && toolsChecked.value !== "0";
const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0; const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
const bonuses = (specChecked ? 1 : 0) + (helped ? 1 : 0) + (tools ? 1 : 0) + group; const experience = this.#getExperiencePull();
return (abilVal + sc + skillPool + bonuses) || 0; const specialty = specChecked ? 1 : 0;
const help = helped ? 1 : 0;
const tooling = tools ? 1 : 0;
const total = ability + skillPool + selfControl + specialty + help + tooling + group + experience;
return { ability, skill: skillPool, selfControl, specialty, help, tooling, group, experience, total };
}
getDicePool() {
return this.getPoolBreakdown().total;
}
getSpecialtyName() {
const checked = this.#el.querySelector("input[name='usingSpecialization']:checked");
return checked && checked.value !== "aucune" ? checked.value : null;
} }
getDifficultySelect() { getDifficultySelect() {
@@ -200,6 +251,40 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
// ── UI ─────────────────────────────────────────────────────────────── // ── UI ───────────────────────────────────────────────────────────────
#applyLocks(locks) {
const ability = this.#getAbility();
if (ability && locks.ability) {
const opt = [...ability.options].find(o => o.dataset.label === locks.ability);
if (opt) {
opt.selected = true;
ability.disabled = true;
const scoreEl = this.#el.querySelector("#abilityScore");
if (scoreEl) scoreEl.value = opt.value;
const sc = this.#getSelfCtrl();
if (sc) sc.max = opt.value;
}
}
const skill = this.#getSkill();
if (skill && locks.skill) {
const opt = [...skill.options].find(o => o.dataset.label === locks.skill);
if (opt) opt.selected = true;
}
const difficulty = this.#getDifficulty();
if (difficulty && locks.difficulty !== undefined && locks.difficulty !== null) {
const locked = parseInt(locks.difficulty, 10);
let opt = [...difficulty.options].find(o => parseInt(o.value, 10) === locked);
if (!opt) {
// Difficulté hors des options (ex: 9 + 2) : ajoute une option dédiée.
opt = document.createElement("option");
opt.value = String(locked);
opt.textContent = `${game.i18n.localize("VERMINE.difficulty")} (${locked})`;
difficulty.appendChild(opt);
}
opt.selected = true;
difficulty.disabled = true;
}
}
#displaySpecialties() { #displaySpecialties() {
for (const el of this.#el.querySelectorAll("[data-spec-skill]")) { for (const el of this.#el.querySelectorAll("[data-spec-skill]")) {
el.style.display = "inline"; el.style.display = "inline";
@@ -207,14 +292,32 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
} }
#calculateBonusCount() { #calculateBonusCount() {
let b = 0; const b = this.getPoolBreakdown();
if (this.#el.querySelector("#helped")?.checked) b += 1; return b.specialty + b.help + b.tooling + b.group + b.selfControl + b.experience;
b += parseInt(this.#el.querySelector("#group")?.value, 10) || 0; }
b += parseInt(this.#getSelfCtrl()?.value, 10) || 0;
const tools = this.#el.querySelector("input[name='usingTools']:checked"); #refreshExperienceUI() {
if (tools && tools.value !== "0") b += 1; const actor = this.#actor;
if (this.hasSpecialtySelected()) b += 1; const group = RollDialog.#findGroup(actor);
return b; const individual = actor.system?.experience?.dice || 0;
const groupPool = group?.system?.experience?.dice || 0;
const indEl = this.#el.querySelector("#exp-individual");
if (indEl) indEl.textContent = individual;
const grpEl = this.#el.querySelector("#exp-group");
if (grpEl) grpEl.textContent = groupPool;
const pull = this.#el.querySelector("#experience-pull");
if (pull) {
const max = Math.min(2, groupPool);
const current = Math.min(parseInt(pull.value, 10) || 0, max);
pull.innerHTML = "";
for (let i = 0; i <= max; i++) {
const opt = document.createElement("option");
opt.value = String(i);
opt.textContent = `${i}D`;
if (i === current) opt.selected = true;
pull.appendChild(opt);
}
}
} }
#updateUI() { #updateUI() {
@@ -281,6 +384,29 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
this.close(); this.close();
} }
static async #onGiveToGroup(event, target) {
const actor = this.#actor;
if (actor.type !== "character") return;
const individual = actor.system?.experience?.dice || 0;
if (individual < 1) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_experience_dice"));
return;
}
if (!game.user.isGM && !actor.isOwner) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_permission"));
return;
}
const group = RollDialog.#findGroup(actor);
if (!group) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_group"));
return;
}
const groupPool = group.system?.experience?.dice || 0;
await actor.update({ "system.experience.dice": individual - 1 });
await group.update({ "system.experience.dice": groupPool + 1 });
this.#refreshExperienceUI();
}
static async #onRoll(event, target) { static async #onRoll(event, target) {
const selfCtrl = this.getSelfControl(); const selfCtrl = this.getSelfControl();
if (selfCtrl > 0) { if (selfCtrl > 0) {
@@ -302,6 +428,26 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
await this.#actor.update({ "system.attributes.self_control.value": newVal }); await this.#actor.update({ "system.attributes.self_control.value": newVal });
} }
// Transfert d'Expérience : puiser dans la Réserve de Groupe (1-2D).
const pulled = this.#getExperiencePull();
if (pulled > 0) {
const group = RollDialog.#findGroup(this.#actor);
if (!group) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_group"));
return;
}
if (!game.user.isGM && !group.isOwner) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_permission"));
return;
}
const groupPool = group.system?.experience?.dice || 0;
if (groupPool < pulled) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_group_dice"));
return;
}
await group.update({ "system.experience.dice": groupPool - pulled });
}
await VermineUtils.roll({ await VermineUtils.roll({
actor: this.#actor, actor: this.#actor,
NoD: this.getDicePool(), NoD: this.getDicePool(),
@@ -315,7 +461,12 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
keepTotem: this.getKeepTotem(), keepTotem: this.getKeepTotem(),
skillCategory: this.getSkillCategory(), skillCategory: this.getSkillCategory(),
skillLevel: this.getSkillLevel(), skillLevel: this.getSkillLevel(),
hasSpecialty: this.hasSpecialtySelected() hasSpecialty: this.hasSpecialtySelected(),
poolBreakdown: this.getPoolBreakdown(),
specialtyName: this.getSpecialtyName(),
weapon: this.weapon?.toObject() ?? null,
targets: [...game.user.targets].map(t => t.name),
messageFlags: this.defense ? { "vermine-defense": this.defense } : null
}); });
this.close(); this.close();
+495
View File
@@ -0,0 +1,495 @@
/**
* 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. Une créature
* équipée d'une arme (morsure, griffes…) utilise les dégâts de celle-ci.
* @param {Actor} actor L'attaquant
* @param {Item|null} weapon
* @returns {number}
*/
static baseDamage(actor, weapon) {
const type = actor.type
if (type === 'creature') {
if (weapon?.system?.damage) {
let base = weapon.system.damage.value || 0
if (weapon.system.damage.addVigor) base += actor.system?.computed?.vigor || 0
return base
}
return actor.system?.computed?.damage || 0
}
if (weapon?.system?.damage) {
let base = weapon.system.damage.value || 0
if (weapon.system.damage.addVigor) base += this.#vigor(actor)
return base
}
return this.#vigor(actor)
}
static #vigor(actor) {
if (actor.type === 'npc') {
const threat = actor.system?.threat?.value || 1
return CONFIG.VERMINE.npcThreatLevels?.[threat]?.vigor || 0
}
if (actor.type === 'creature') return actor.system?.computed?.vigor || 0
return actor.system?.abilities?.vigor?.value || 0
}
// ── Protection et blessures ─────────────────────────────────────────
/**
* Protection du défenseur.
* Parade : seule la protection utilisée compte.
* Sinon : somme des protections équipées (créature : protection de rôle).
* @param {Actor} defender
* @param {number|null} parryLevel Protection utilisée lors d'une parade
* @returns {number}
*/
static protectionTotal(defender, parryLevel = null) {
if (parryLevel !== null) return Math.max(0, parryLevel)
if (defender.type === 'creature') {
return defender.system?.computed?.protection || 0
}
const defenses = defender.itemTypes?.defense?.filter(d => d.system.equipped) || []
return defenses.reduce((sum, d) => sum + (d.system.level || 0), 0)
}
/**
* Catégorie de blessure : le seuil le plus élevé inférieur ou égal
* aux dégâts nets subis.
* @param {number} netDamage
* @param {Actor} defender
* @returns {string|null} 'minorWound', 'majorWound', 'deadlyWound' ou null
*/
static highestWound(netDamage, defender) {
const sys = defender.system
const deadly = sys?.deadlyWound?.threshold ?? Number.POSITIVE_INFINITY
const major = sys?.majorWound?.threshold ?? Number.POSITIVE_INFINITY
const minor = sys?.minorWound?.threshold ?? Number.POSITIVE_INFINITY
if (netDamage >= deadly) return 'deadlyWound'
if (netDamage >= major) return 'majorWound'
if (netDamage >= minor) return 'minorWound'
return null
}
/**
* Clé i18n du libellé d'une catégorie de blessure.
* @param {string|null} category
* @returns {string}
*/
static woundLabelKey(category) {
if (category === 'deadlyWound') return 'VERMINE.wounds.deadly_wounds'
if (category === 'majorWound') return 'VERMINE.wounds.heavy_wounds'
if (category === 'minorWound') return 'VERMINE.wounds.light_wounds'
return 'VERMINE.no_wound'
}
/**
* Vérifie que l'utilisateur peut appliquer une blessure sur l'acteur.
* @param {Actor} actor
* @returns {boolean}
*/
static canApplyWound(actor) {
return Boolean(actor) && (game.user.isGM || actor.isOwner)
}
/**
* Applique +1 dans la catégorie de blessure indiquée (plafonné au max).
* @param {Actor} actor
* @param {string} woundCategory 'minorWound' | 'majorWound' | 'deadlyWound'
* @returns {Promise<number|boolean>} nouvelle valeur, ou false si refusé
*/
static async applyWound(actor, woundCategory) {
if (!actor || !woundCategory) return null
if (!this.canApplyWound(actor)) return false
const current = actor.system?.[woundCategory]?.value || 0
const max = actor.system?.[woundCategory]?.max ?? Number.POSITIVE_INFINITY
const next = Math.min(current + 1, max)
await actor.update({ [`system.${woundCategory}.value`]: next })
return next
}
// ── Résolution ──────────────────────────────────────────────────────
/**
* Résout l'échange pour une cible donnée (défense la plus récente liée
* à cette cible, ou abstention si aucune défense).
* @param {ChatMessage} attackMessage
* @param {string|null} targetId id d'acteur de la cible (défaut : première cible)
* @returns {Promise<Object|null>} résultat de la résolution
*/
static async resolve(attackMessage, targetId = null) {
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
if (!exchange) return null
const target = (targetId && exchange.targets?.find(t => t.id === targetId)) || exchange.targets?.[0] || null
if (!target) return null
const defs = this.#defenseMessagesFor(exchange.id, target.id)
const defenderMessage = defs[defs.length - 1]
const defense = defenderMessage ? defenderMessage.getFlag('world', FLAG_DEFENSE) : null
const attackerSuccesses = this.#messageTotal(attackMessage)
const defenderSuccesses = defense ? this.#messageTotal(defenderMessage) : 0
const hit = attackerSuccesses > defenderSuccesses
// Le flag de défense porte actorId/actorUuid (clés différentes de celles
// des cibles) : on normalise la référence avant résolution de l'acteur.
const defenderRef = defense
? { id: defense.actorId ?? null, uuid: defense.actorUuid ?? null, name: defense.actorName ?? '' }
: target
const defender = await this.#resolveDefender(defenderRef)
const damage = (exchange.baseDamage || 0) + attackerSuccesses
const parryLevel = defense?.type === 'parade' ? (defense.parryLevel ?? 0) : null
const protection = hit && parryLevel !== null
? parryLevel
: (defender ? this.protectionTotal(defender, hit ? null : parryLevel) : 0)
const netDamage = hit ? Math.max(0, damage - protection) : 0
const woundCategory = hit && defender ? this.highestWound(netDamage, defender) : null
return {
exchangeId: exchange.id,
defenderId: defenderRef.id ?? null,
defenderUuid: defenderRef.uuid ?? null,
defenderName: defense?.actorName || target.name || '',
defenseType: defense?.type || null,
attackerSuccesses,
defenderSuccesses,
hit,
damage,
protection,
netDamage,
woundCategory,
woundThreshold: woundCategory && defender ? defender.system[woundCategory].threshold : null,
canApply: Boolean(defender) && this.canApplyWound(defender)
}
}
/**
* Rend le bloc de résolution (HTML) pour insertion dans le message.
* @param {Object} r Résultat de VermineExchange.resolve
* @returns {string}
*/
static renderResolution(r) {
const t = (key) => game.i18n.localize(key)
const verdict = !r.hit
? (r.defenseType === 'parade'
? `<span class="res-parried">${t('VERMINE.missed_parade')}</span>`
: `<span class="res-dodged">${t('VERMINE.missed_esquive')}</span>`)
: (r.defenseType === 'parade'
? `<span class="res-hit">${t('VERMINE.hit_parade')}</span>`
: `<span class="res-hit">${t('VERMINE.hit')}</span>`)
const lines = [
`<div class="resolution-line">${t('VERMINE.attacker_successes')}: ${r.attackerSuccesses} &middot; ${t('VERMINE.defender_successes')}: ${r.defenderSuccesses} &rarr; ${verdict}</div>`
]
if (r.hit) {
lines.push(`<div class="resolution-line">${t('VERMINE.raw_damage')}: <strong>${r.damage}</strong>${r.protection ? ` &minus; ${t('ADVERSITY.protection')}: ${r.protection}` : ''} = ${t('VERMINE.net_damage')}: <strong>${r.netDamage}</strong></div>`)
if (r.woundCategory) {
lines.push(`<div class="resolution-line res-wound">${t('VERMINE.wound')}: <strong>${t(this.woundLabelKey(r.woundCategory))}</strong> (${t('VERMINE.wounds.threshold')} ${r.woundThreshold}D)</div>`)
} else {
lines.push(`<div class="resolution-line res-noshock">${t('VERMINE.no_wound')}</div>`)
}
} else {
lines.push(`<div class="resolution-line res-nodamage">${t('VERMINE.no_damage')}</div>`)
}
const apply = (r.hit && r.woundCategory && r.canApply)
? `<button type="button" class="exchange-apply" data-action="exchange-apply" data-defender-id="${r.defenderId}" data-defender-uuid="${r.defenderUuid}" data-wound="${r.woundCategory}"><i class="fas fa-briefcase-medical"></i> ${t('VERMINE.apply_wound')}</button>`
: ''
return `<div class="exchange-resolution">
<div class="resolution-header"><i class="fas fa-exchange-alt"></i> ${t('VERMINE.resolve_result')}</div>
${lines.join('')}
${apply}
</div>`
}
// ── Liaison chat ────────────────────────────────────────────────────
/**
* Lie les interactions d'échange de coups sur un message de chat.
* @param {HTMLElement} html élément du message
* @param {ChatMessage} message
*/
static bindChat(html, message) {
const exchange = message.getFlag('world', FLAG_EXCHANGE)
if (exchange) {
html.querySelectorAll('.exchange-defend').forEach(btn => {
btn.addEventListener('click', ev => this.#onDefense(ev, message))
})
html.querySelectorAll('.exchange-resolve').forEach(btn => {
btn.addEventListener('click', ev => this.#onResolve(ev, message))
})
}
html.querySelectorAll('[data-action="exchange-apply"]').forEach(btn => {
btn.addEventListener('click', ev => this.#onApply(ev, message))
})
}
static async #onDefense(ev, attackMessage) {
ev.preventDefault()
ev.stopPropagation()
const btn = ev.currentTarget
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
if (!exchange) return
const type = btn.dataset.type
const actor = await this.#resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
if (!actor) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
return
}
if (!game.user.isGM && !actor.isOwner) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
return
}
if (actor.type === 'creature') {
ui.notifications.warn(game.i18n.localize('VERMINE.error_creature_no_defense'))
return
}
const { default: RollDialog } = await import('./dialogs/rollDialog.mjs')
const locks = this.#defenseLocks(actor, exchange, type)
const dialog = await RollDialog.create({
actor,
rolltype: 'skill',
label: locks.skill,
locks,
defense: {
exchangeId: exchange.id,
type,
actorId: actor.id,
actorUuid: actor.uuid,
actorName: actor.name,
difficulty: exchange.difficulty,
parryLevel: type === 'parade' ? this.#parryLevel(actor) : null
}
})
if (dialog) dialog.render(true)
}
static #defenseLocks(actor, exchange, type) {
if (type === 'parade') {
return { ability: 'vigor', skill: this.#parrySkill(actor), difficulty: exchange.difficulty }
}
return {
ability: 'reflexes',
skill: exchange.mode === 'ranged' ? 'alertness' : 'close',
difficulty: exchange.difficulty
}
}
static #parrySkill(actor) {
const hasMelee = actor.itemTypes?.weapon?.some(w => w.system.equipped && w.system.skill === 'melee')
return hasMelee ? 'melee' : 'close'
}
static #parryLevel(actor) {
const defenses = actor.itemTypes?.defense?.filter(d => d.system.equipped) || []
return defenses.reduce((best, d) => Math.max(best, d.system.level || 0), 0)
}
static async #onResolve(ev, attackMessage) {
ev.preventDefault()
const btn = ev.currentTarget
if (!game.user.isGM && !attackMessage.isOwner) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
return
}
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
if (!exchange) return
if (!exchange.targets?.length) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_target'))
return
}
const wrapper = btn.closest('.exchange-actions')
const container = wrapper?.parentElement
if (!container) return
container.querySelectorAll('.exchange-resolution').forEach(el => el.remove())
const targets = exchange.targets?.length ? exchange.targets : [null]
for (const target of targets) {
const result = await this.resolve(attackMessage, target?.id ?? null)
if (!result) continue
const block = document.createElement('div')
block.innerHTML = this.renderResolution(result)
const node = block.firstElementChild
container.appendChild(node)
this.bindChat(node, attackMessage)
}
const msgEl = btn.closest('.vermine-roll-message')
if (msgEl) await attackMessage.update({ content: msgEl.outerHTML })
}
static async #onApply(ev, attackMessage) {
ev.preventDefault()
const btn = ev.currentTarget
const defender = await this.#resolveDefender({ id: btn.dataset.defenderId, uuid: btn.dataset.defenderUuid })
const wound = btn.dataset.wound
if (!defender || !wound) return
if (!this.canApplyWound(defender)) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
return
}
const next = await this.applyWound(defender, wound)
if (next === false) return
btn.disabled = true
btn.innerHTML = `<i class="fas fa-check"></i> ${game.i18n.localize('VERMINE.wound_applied')}`
// Persiste l'état du bouton seulement si l'utilisateur peut modifier le message
if (game.user.isGM || attackMessage.isOwner) {
const msgEl = btn.closest('.vermine-roll-message')
if (msgEl) await attackMessage.update({ content: msgEl.outerHTML })
}
}
// ── Helpers internes ────────────────────────────────────────────────
/**
* Résout un acteur depuis son id d'acteur monde (acteurs liés) ou son
* uuid (tokens non liés via fromUuid).
* @param {{id: string|null, uuid: string|null}} ref
* @returns {Promise<Actor|null>}
*/
static async #resolveDefender({ id = null, uuid = null }) {
if (id) {
const actor = game.actors.get(id)
if (actor) return actor
}
if (uuid) {
const doc = await fromUuid(uuid)
if (!doc) return null
if (doc.documentType === 'Actor') return doc
if (doc.documentType === 'Token') return doc.actor ?? null
}
return null
}
static #defenseMessagesFor(exchangeId, targetId = null) {
return game.messages.contents.filter(m => {
const f = m.getFlag('world', FLAG_DEFENSE)
if (!f || f.exchangeId !== exchangeId) return false
if (targetId) return f.actorId === targetId
return true
})
}
static #messageTotal(message) {
const el = document.createElement('div')
el.innerHTML = message.content || ''
const totalEl = el.querySelector('#total')
const val = parseInt(totalEl?.innerText, 10)
return Number.isNaN(val) ? 0 : val
}
}
+146
View File
@@ -0,0 +1,146 @@
/**
* Règles d'apprentissage (phase d'Expérience).
* Helpers purs de difficultés, handicaps et calculs de jets d'Expérience.
*/
export class VermineExperience {
/**
* Difficulté d'un jet d'apprentissage de Compétence selon le niveau visé.
* @param {number} targetLevel niveau visé (1-5)
* @returns {number}
*/
static skillLearningDifficulty(targetLevel) {
switch (parseInt(targetLevel, 10)) {
case 1: return 5 // Débutant
case 2: return 7 // Confirmé
case 3: return 9 // Expert
case 4: return 10 // Maître
case 5: return 10 // Légende
default: return 7
}
}
/**
* Handicap d'un jet d'apprentissage de Compétence.
* Rareté (1-3) + handicap (I) supplémentaire pour le niveau Légende.
* @param {number} targetLevel niveau visé (1-5)
* @param {number} rarity rareté de la compétence (0-3)
* @returns {number}
*/
static skillLearningHandicap(targetLevel, rarity) {
let handicap = parseInt(rarity, 10) || 0
if (parseInt(targetLevel, 10) === 5) handicap += 1 // Légende : (I)
return handicap
}
/**
* Difficulté d'un jet d'apprentissage de Spécialité selon le niveau actuel
* de la compétence associée. Domaine de prédilection : -2.
* @param {number} skillLevel niveau actuel de la compétence (2-5)
* @param {boolean} isDomain la compétence est dans le domaine de prédilection
* @returns {number}
*/
static specialtyLearningDifficulty(skillLevel, isDomain) {
let difficulty
switch (parseInt(skillLevel, 10)) {
case 2: difficulty = 9 // Confirmé
break
case 3: difficulty = 7 // Expert
break
case 4: difficulty = 5 // Maître
break
case 5: difficulty = 5 // Légende (relance automatique)
break
default: difficulty = 9
}
return isDomain ? difficulty - 2 : difficulty
}
/**
* Handicap d'un jet d'apprentissage de Spécialité : nombre de spécialités
* déjà possédées (requiert au moins Confirmé dans la compétence associée).
* @param {number} specialtyCount
* @returns {number}
*/
static specialtyLearningHandicap(specialtyCount) {
return Math.max(0, parseInt(specialtyCount, 10) || 0)
}
/**
* Difficulté d'un jet d'apprentissage de Caractéristique.
* @returns {number}
*/
static characteristicLearningDifficulty() {
return 9
}
/**
* Difficulté d'un jet d'apprentissage de Réserve : valeur actuelle.
* @param {number} currentValue
* @returns {number}
*/
static reserveLearningDifficulty(currentValue) {
return Math.max(0, parseInt(currentValue, 10) || 0)
}
/**
* Handicap d'âge pour l'apprentissage des Réserves et autres jets liés à l'âge.
* @param {number} ageType 1=Jeune, 2=Adulte, 3=Ancien
* @returns {number}
*/
static ageHandicap(ageType) {
switch (parseInt(ageType, 10)) {
case 1: return 1 // Jeune (I)
case 3: return 3 // Ancien (III)
default: return 2 // Adulte (II)
}
}
/**
* Difficulté d'un jet d'acquisition d'un Dé d'Évolution selon l'âge.
* @param {number} ageType 1=Jeune, 2=Adulte, 3=Ancien
* @returns {number}
*/
static evolutionDifficulty(ageType) {
switch (parseInt(ageType, 10)) {
case 1: return 5
case 3: return 9
default: return 7
}
}
/**
* Bonus/Malus de Totem sur le jet de Dé d'Évolution.
* Plus de Dés Adaptés que Humains : +1D. Inversement : -1D.
* @param {number} human human totem dice
* @param {number} adapted adapted totem dice
* @returns {number}
*/
static evolutionTotemModifier(human, adapted) {
if (adapted > human) return 1
if (human > adapted) return -1
return 0
}
/**
* Clé i18n du libellé du niveau de compétence visé.
* @param {number} level
* @returns {string}
*/
static skillLevelLabelKey(level) {
return CONFIG.VERMINE.SkillLevels?.[level]?.label ?? "SKILL_LEVELS.incompetent"
}
/**
* Détermine si une compétence appartient au domaine de prédilection.
* @param {Actor} actor
* @param {string} skillKey
* @returns {boolean}
*/
static isPreferredDomain(actor, skillKey) {
const category = actor.system?.skills?.[skillKey]?.category
const preferred = actor.system?.skill_categories?.preferred
return Boolean(category && preferred && category === preferred)
}
}
+20
View File
@@ -12,6 +12,7 @@ export const preloadHandlebarsTemplates = async function () {
"systems/vermine2047/templates/actor/parts/actor-items.hbs", "systems/vermine2047/templates/actor/parts/actor-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",
@@ -32,6 +33,7 @@ export const preloadHandlebarsTemplates = async function () {
// npc partials // npc partials
"systems/vermine2047/templates/actor/npc/npc-combat.hbs", "systems/vermine2047/templates/actor/npc/npc-combat.hbs",
"systems/vermine2047/templates/actor/parts/npc-skill-category.hbs", "systems/vermine2047/templates/actor/parts/npc-skill-category.hbs",
"systems/vermine2047/templates/actor/parts/npc-skill-item.hbs",
// creature partials // creature partials
"systems/vermine2047/templates/actor/creature/creature-combat.hbs", "systems/vermine2047/templates/actor/creature/creature-combat.hbs",
@@ -100,6 +102,21 @@ export const registerHandlebarsHelpers = function () {
return game.i18n.localize(arrayLabel + "." + objectLabel + ".name"); return game.i18n.localize(arrayLabel + "." + objectLabel + ".name");
}); });
// Return an object of skills ordered alphabetically by their localized
// display name (easier to find a skill quickly). Use with `{{#each}}`.
Handlebars.registerHelper('sortedSkills', function (skills) {
if (!skills || typeof skills !== 'object') return skills;
const sorted = {};
Object.keys(skills)
.sort((a, b) => {
const na = game.i18n.localize(`SKILLS.${a}.name`).toLocaleLowerCase();
const nb = game.i18n.localize(`SKILLS.${b}.name`).toLocaleLowerCase();
return na.localeCompare(nb, game.i18n.lang);
})
.forEach(k => { sorted[k] = skills[k]; });
return sorted;
});
Handlebars.registerHelper('smarttlk', function (arrayLabel, objectLabel, key) { Handlebars.registerHelper('smarttlk', function (arrayLabel, objectLabel, key) {
return game.i18n.localize(arrayLabel + "." + objectLabel + "." + key); return game.i18n.localize(arrayLabel + "." + objectLabel + "." + key);
}); });
@@ -347,6 +364,9 @@ export const registerHandlebarsHelpers = function () {
Handlebars.registerHelper('ifincludes', function (arg1, arg2, options) { Handlebars.registerHelper('ifincludes', function (arg1, arg2, options) {
return (arg1.includes(arg2)) ? options.fn(this) : options.inverse(this); return (arg1.includes(arg2)) ? options.fn(this) : options.inverse(this);
}); });
Handlebars.registerHelper('ifStartsWith', function (arg1, arg2, options) {
return (typeof arg1 === 'string' && arg1.startsWith(arg2)) ? options.fn(this) : options.inverse(this);
});
//math operations //math operations
+4
View File
@@ -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') })
+124 -27
View File
@@ -29,7 +29,13 @@ export class VermineUtils {
keepTotem = null, keepTotem = null,
skillLevel = null, skillLevel = null,
hasSpecialty = false, hasSpecialty = false,
handicap = 0 handicap = 0,
poolBreakdown = null,
specialtyName = null,
weapon = null,
targets = [],
attack = null,
messageFlags = null
}) { }) {
// Validate inputs // Validate inputs
if (!actor) { if (!actor) {
@@ -105,6 +111,27 @@ export class VermineUtils {
// Build final formula // Build final formula
formula = modFormula !== null ? `${baseFormula}+${modFormula}` : baseFormula; formula = modFormula !== null ? `${baseFormula}+${modFormula}` : baseFormula;
// Reconcile the breakdown displayed in chat with the dice actually rolled.
// Domain bonuses/malus are only applied when the corresponding totem die
// is used, and totem dice replace (not add to) base-pool dice.
if (poolBreakdown) {
// Count totem dice present in the final modifier formula.
let totemDice = 0;
if (modFormula) {
if (modFormula.includes("human_")) totemDice += 1;
if (modFormula.includes("adapted_")) totemDice += 1;
}
// Only count the domain bonus of totems actually activated in the roll.
const domainBonus = (totems.human ? totemBonus.human : 0)
+ (totems.adapted ? totemBonus.adapted : 0);
const basePool = poolBreakdown.total;
poolBreakdown.totemDice = totemDice;
poolBreakdown.domainBonus = domainBonus;
// Total = nombre réel de dés lancés (NoD réguliers + dés de totem).
// Équivaut à basePool + domainBonus sauf si le pool régulier clampe à 0.
poolBreakdown.total = NoD + totemDice;
}
// Create the roll // Create the roll
const roll = new Roll(formula, actor.getRollData()); const roll = new Roll(formula, actor.getRollData());
@@ -143,7 +170,13 @@ export class VermineUtils {
keepTotem, keepTotem,
skillLevel, skillLevel,
hasSpecialty, hasSpecialty,
handicap handicap,
poolBreakdown,
specialtyName,
weapon,
targets,
attack,
messageFlags
}); });
return roll; return roll;
@@ -199,10 +232,10 @@ export class VermineUtils {
/** /**
* Handles reroll events on dice in chat messages. * Handles reroll events on dice in chat messages.
* @param {Object} message - The chat message containing the reroll event * @param {Object} message - The chat message containing the reroll event
* @param {Object} ev - The reroll event * @param {HTMLElement} target - The clicked die element
* @returns {Promise<boolean>} Whether the reroll was successful * @returns {Promise<boolean>} Whether the reroll was successful
*/ */
static async onReroll(message, ev) { static async onReroll(message, target) {
// Verify user permissions // Verify user permissions
const msgUserId = message.user?.id ?? message.user; const msgUserId = message.user?.id ?? message.user;
if (msgUserId !== game.user?.id && !game.user?.isGM) { if (msgUserId !== game.user?.id && !game.user?.isGM) {
@@ -211,7 +244,7 @@ export class VermineUtils {
} }
// Get reroll count // Get reroll count
const rollMessage = ev.currentTarget.closest('div.vermine-roll-message'); const rollMessage = target.closest('div.vermine-roll-message');
if (!rollMessage) { if (!rollMessage) {
return false; return false;
} }
@@ -221,22 +254,22 @@ export class VermineUtils {
// Check if rerolls are available // Check if rerolls are available
if (!rerollCount || parseInt(rerollCount, 10) < 1) { if (!rerollCount || parseInt(rerollCount, 10) < 1) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_rerolls_left')); ui.notifications.warn(game.i18n.localize('VERMINE.error_no_rerolls_left'));
const rerollables = ev.currentTarget.closest('ul')?.querySelectorAll('.rerollable'); const rerollables = target.closest('ul')?.querySelectorAll('.rerollable');
if (rerollables) { if (rerollables) {
rerollables.forEach(el => el.classList.remove('rerollable')); rerollables.forEach(el => el.classList.remove('rerollable'));
} }
return false; return false;
} }
ev.currentTarget.classList.add('rerolled'); target.classList.add('rerolled');
// Set reroll flag // Set reroll flag
await message.setFlag("world", "reroll", true); await message.setFlag("world", "reroll", true);
// Get difficulty and dice type // Get difficulty and dice type
const ulElement = ev.currentTarget.closest('ul'); const ulElement = target.closest('ul');
const difficulty = ulElement?.dataset.difficulty ?? 7; const difficulty = ulElement?.dataset.difficulty ?? 7;
let diceType = ev.currentTarget.dataset.diceType; let diceType = target.dataset.diceType;
// Sanitize user name // Sanitize user name
const safeUserName = (game.user?.name ?? "user").replace(/[^a-zA-Z0-9_]/g, '_'); const safeUserName = (game.user?.name ?? "user").replace(/[^a-zA-Z0-9_]/g, '_');
@@ -265,30 +298,68 @@ export class VermineUtils {
// Update die display // Update die display
const result = reroll.dice[0]?.results[0]?.result ?? 0; const result = reroll.dice[0]?.results[0]?.result ?? 0;
const dieSpan = ev.currentTarget.querySelector('span'); const dieSpan = target.querySelector('span');
if (dieSpan) { if (dieSpan) {
dieSpan.innerText = result; dieSpan.innerText = result;
} }
// Update the face used for the CSS background image.
target.dataset.result = result;
// Update total if successful // Set/clear the success class from the rerolled die (a previous success
// must be removed if the new result fails).
const success = reroll.dice[0]?.results[0]?.success; const success = reroll.dice[0]?.results[0]?.success;
if (success) { target.classList.toggle('success', !!success);
ev.currentTarget.classList.add('success'); target.classList.remove("rerollable");
const totalElement = rollMessage.querySelector('#total');
if (totalElement) { // Recompute the total from the live dice in the DOM. This avoids the
const currentTotal = parseInt(totalElement.innerText, 10) || 0; // cumulative-increment drift of the old logic (which never removed a
totalElement.innerText = currentTotal + reroll.total; // removed success from #total). Totem dice (human/adapted) count double.
} const totalEl = rollMessage.querySelector('#total');
if (totalEl) {
let total = 0;
rollMessage.querySelectorAll('li.die.success').forEach(die => {
const isTotem = die.classList.contains('human') || die.classList.contains('adapted');
total += isTotem ? 2 : 1;
});
totalEl.innerText = total;
} }
// Update message content // Update message content
ev.currentTarget.classList.remove("rerollable"); const messageContent = target.closest('div.message-content');
const messageContent = ev.currentTarget.closest('div.message-content');
if (messageContent) { if (messageContent) {
const newRerollCount = parseInt(rerollCount, 10) - 1; const newRerollCount = parseInt(rerollCount, 10) - 1;
rollMessage.querySelector('#allowed_reroll').innerText = newRerollCount; rollMessage.querySelector('#allowed_reroll').innerText = newRerollCount;
// Recompute the verdict banner from the recomputed total.
const verdictEl = rollMessage.querySelector('.roll-verdict');
const requiredEl = rollMessage.querySelector('#required');
if (verdictEl && totalEl && requiredEl) {
const total = parseInt(totalEl.innerText, 10) || 0;
const required = parseInt(requiredEl.innerText, 10) || 1;
const won = total >= required;
verdictEl.classList.toggle('success', won);
verdictEl.classList.toggle('failure', !won);
const label = won
? game.i18n.localize('VERMINE.roll_success')
: game.i18n.localize('VERMINE.roll_failure');
verdictEl.innerHTML = `${won ? '<i class="fas fa-check-circle"></i>' : '<i class="fas fa-times-circle"></i>'}<span>${label}</span>`;
}
// Recompute the dice summary from the current die list.
const summaryEl = rollMessage.querySelector('.roll-dice-summary');
if (summaryEl) {
const stats = { regular: { count: 0, success: 0 }, human: { count: 0, success: 0 }, adapted: { count: 0, success: 0 } };
rollMessage.querySelectorAll('li.die').forEach(die => {
const type = die.classList.contains('human') ? 'human' : die.classList.contains('adapted') ? 'adapted' : 'regular';
stats[type].count += 1;
if (die.classList.contains('success')) stats[type].success += 1;
});
const mk = (key, count, s) => count ? `<span class="die-summary ${key}">${count} ${game.i18n.localize(`VERMINE.die_${key}`)} · ${s} ${game.i18n.localize('VERMINE.die_successes')}</span>` : '';
summaryEl.innerHTML = mk('regular', stats.regular.count, stats.regular.success)
+ mk('human', stats.human.count, stats.human.success)
+ mk('adapted', stats.adapted.count, stats.adapted.success);
}
await message.update({ await message.update({
content: messageContent.outerHTML content: messageContent.outerHTML
}); });
@@ -314,10 +385,11 @@ export class VermineUtils {
html.querySelectorAll('.rerollable').forEach(el => { html.querySelectorAll('.rerollable').forEach(el => {
el.addEventListener('click', async (ev) => { el.addEventListener('click', async (ev) => {
ev.preventDefault(); ev.preventDefault();
const msgId = ev.currentTarget.closest("li.message")?.dataset?.messageId; const dieEl = ev.currentTarget;
const msgId = dieEl.closest("li.message")?.dataset?.messageId;
if (msgId) { if (msgId) {
const message = await game.messages.get(msgId); const message = await game.messages.get(msgId);
await VermineUtils.onReroll(message, ev); await VermineUtils.onReroll(message, dieEl);
} }
}); });
}); });
@@ -336,6 +408,7 @@ export class VermineUtils {
// Add click event for granting rerolls // Add click event for granting rerolls
html.querySelectorAll("button.grant-reroll").forEach(el => { html.querySelectorAll("button.grant-reroll").forEach(el => {
el.addEventListener('click', async (ev) => { el.addEventListener('click', async (ev) => {
const btn = ev.currentTarget;
const grantedRerollElement = html.querySelector('#granted-reroll'); const grantedRerollElement = html.querySelector('#granted-reroll');
const allowedRerollElement = html.querySelector("#allowed_reroll"); const allowedRerollElement = html.querySelector("#allowed_reroll");
@@ -343,13 +416,13 @@ export class VermineUtils {
allowedRerollElement.innerText = grantedRerollElement.innerText; allowedRerollElement.innerText = grantedRerollElement.innerText;
} }
const mesEl = ev.currentTarget.closest('[data-message-id]'); const mesEl = btn.closest('[data-message-id]');
const messageId = mesEl?.dataset?.messageId; const messageId = mesEl?.dataset?.messageId;
if (messageId) { if (messageId) {
ev.currentTarget.closest('.reroll-from-effort').style.display = "none"; btn.closest('.reroll-from-effort').style.display = "none";
const rollMessage = ev.currentTarget.closest(".vermine-roll-message"); const rollMessage = btn.closest(".vermine-roll-message");
if (rollMessage) { if (rollMessage) {
const content = rollMessage.outerHTML; const content = rollMessage.outerHTML;
const message = await game.messages.get(messageId); const message = await game.messages.get(messageId);
@@ -401,12 +474,36 @@ export class VermineUtils {
* @returns {Promise<ChatMessage>} The created chat message * @returns {Promise<ChatMessage>} The created chat message
*/ */
static async diplayChatRoll(roll, param) { static async diplayChatRoll(roll, param) {
const content = await foundry.applications.handlebars.renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param }); // Aggregate dice stats per type (regular / human / adapted)
const diceStats = { regular: { count: 0, success: 0 }, human: { count: 0, success: 0 }, adapted: { count: 0, success: 0 } };
for (const die of roll.dice ?? []) {
// Flavors look like "<type>_<user>"; classify from the leading token.
const flavor = die.options?.flavor ?? "";
const prefix = flavor.split("_")[0];
const type = prefix === "human" ? "human" : prefix === "adapted" ? "adapted" : "regular";
for (const r of die.results ?? []) {
diceStats[type].count += 1;
if (r.success) diceStats[type].success += 1;
}
}
// Verdict
const required = 1 + (param.handicap ?? 0);
const total = roll._total ?? 0;
const verdict = {
success: total >= required,
total,
required
};
const content = await foundry.applications.handlebars.renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param, diceStats, verdict });
const chatData = { const chatData = {
user: game.user?._id, user: game.user?._id,
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;
} }
+17 -1
View File
@@ -37,6 +37,14 @@ Hooks.once('init', async function () {
CONFIG.Actor.dataModels.npc = models.VermineNpcData; CONFIG.Actor.dataModels.npc = models.VermineNpcData;
CONFIG.Actor.dataModels.group = models.VermineGroupData; CONFIG.Actor.dataModels.group = models.VermineGroupData;
CONFIG.Actor.dataModels.creature = models.VermineCreatureData; CONFIG.Actor.dataModels.creature = models.VermineCreatureData;
CONFIG.Actor.dataModels.community = models.VermineCommunityData;
// Libellé du type "community" (TYPES.Actor.* du core ne le connaît pas)
CONFIG.Actor.typeLabels ??= {};
CONFIG.Actor.typeLabels.community = "VERMINE.type_community";
Hooks.once('i18nInit', () => {
CONFIG.Actor.typeLabels.community = "VERMINE.type_community";
});
CONFIG.Item.dataModels.item = models.VermineItemData; CONFIG.Item.dataModels.item = models.VermineItemData;
CONFIG.Item.dataModels.weapon = models.VermineWeaponData; CONFIG.Item.dataModels.weapon = models.VermineWeaponData;
@@ -89,6 +97,9 @@ Hooks.once('init', async function () {
foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineGroupSheetV2, { foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineGroupSheetV2, {
types: ["group"], makeDefault: true, label: "VERMINE.Sheet.group" types: ["group"], makeDefault: true, label: "VERMINE.Sheet.group"
}) })
foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineCommunitySheetV2, {
types: ["community"], makeDefault: true, label: "VERMINE.Sheet.community"
})
// Item sheets — un par type // Item sheets — un par type
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineItemSheetV2, { foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineItemSheetV2, {
@@ -180,6 +191,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",
@@ -192,7 +204,11 @@ Hooks.once('init', async function () {
"systems/vermine2047/templates/actor/appv2/creature-info.hbs", "systems/vermine2047/templates/actor/appv2/creature-info.hbs",
"systems/vermine2047/templates/actor/appv2/creature-stats.hbs", "systems/vermine2047/templates/actor/appv2/creature-stats.hbs",
"systems/vermine2047/templates/actor/appv2/creature-combat.hbs", "systems/vermine2047/templates/actor/appv2/creature-combat.hbs",
"systems/vermine2047/templates/actor/appv2/creature-effects.hbs" "systems/vermine2047/templates/actor/appv2/creature-effects.hbs",
"systems/vermine2047/templates/actor/appv2/community-main.hbs",
"systems/vermine2047/templates/actor/appv2/community-identity.hbs",
"systems/vermine2047/templates/actor/appv2/community-domains.hbs",
"systems/vermine2047/templates/actor/appv2/community-resources.hbs"
]); ]);
}); });
+2 -1
View File
@@ -34,7 +34,8 @@
"character": { "htmlFields": ["equipment.description", "identity.biography", "identity.relations"] }, "character": { "htmlFields": ["equipment.description", "identity.biography", "identity.relations"] },
"npc": { "htmlFields": ["equipment.description", "identity.notes"] }, "npc": { "htmlFields": ["equipment.description", "identity.notes"] },
"group": { "htmlFields": ["equipment.description", "identity.notes"] }, "group": { "htmlFields": ["equipment.description", "identity.notes"] },
"creature": { "htmlFields": ["equipment.description", "identity.notes", "identity.biography"] } "creature": { "htmlFields": ["equipment.description", "identity.notes", "identity.biography"] },
"community": { "htmlFields": ["identity.notes", "location.landmarks", "location.territory", "history.events", "history.relations"] }
}, },
"Item": { "Item": {
"item": { "htmlFields": ["description"] }, "item": { "htmlFields": ["description"] },
+1
View File
@@ -221,6 +221,7 @@
{{> "systems/vermine2047/templates/actor/parts/npc-skill-category.hbs" {{> "systems/vermine2047/templates/actor/parts/npc-skill-category.hbs"
categoryKey=key categoryKey=key
categoryLabel=(concat "VERMINE.skill_category." key) categoryLabel=(concat "VERMINE.skill_category." key)
skills=@root.system.skills
}} }}
{{/if}} {{/if}}
{{/each}} {{/each}}
@@ -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">
@@ -55,7 +55,7 @@
> >
{{/if}} {{/if}}
</h4> </h4>
{{#each @root.system.skills as |skill skey|}} {{#each (sortedSkills @root.system.skills) as |skill skey|}}
{{#if (eq skill.category sckey) }} {{#if (eq skill.category sckey) }}
<div class="ability flexrow "> <div class="ability flexrow ">
@@ -79,12 +79,10 @@
data-action="addSpecialty" data-action="addSpecialty"
></i> ></i>
{{/ifgt}} {{/ifgt}}
{{#each skill.specialties as |spe ind|}} {{#each @root.specialties as |spe|}}
{{#ife spe.system.skill skey}} {{#ife spe.system.skill skey}}
<i <span class="specialty" data-tooltip="spécialité utilisable"
class="specialty" >{{spe.name}}<sup>{{spe.system.level}}</sup></span>
data-tooltip="spécialité utilisable"
>{{spe.name}}</i>
{{/ife}} {{/ife}}
{{/each}} {{/each}}
</div> </div>
+2 -11
View File
@@ -1,4 +1,4 @@
<div class="tab combat sheet-part" data-group="sheet" data-tab="combat"> <div class="tab combat sheet-part {{#if @root.tabs.combat.active}}active{{/if}}" data-group="sheet" data-tab="combat">
<div class="align-center"> <div class="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}}
+84 -87
View File
@@ -1,90 +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="grid grid-2col">
<div>
<ol class="items-list">
<li class="item flexrow items-header">
<div
class="item-name"
style="flex:4;"
>{{ localize
'IDENTITY.name'}}</div>
<div class="item-quantity">{{ localize 'VERMINE.qty'}}</div>
<div class="item-weight">{{ localize 'VERMINE.weight'}}</div>
<div class="item-controls">
{{#if isEditMode}}
<a
class="item-control item-create"
title="Create item"
data-action="create"
data-type="item"
><i class="fas fa-plus"></i></a>
{{/if}}
</div>
</li>
{{#each gear as |item id|}}
<li
class="item flexrow flex-group-center"
data-item-id="{{item._id}}"
>
<div
class="item-name"
style="flex:4;"
>
<div class="item-image">
<a
class="item-control item-edit"
data-action="edit"
data-item-id="{{item._id}}"
data-roll-type="item"
><img
src="{{item.img}}"
title="{{item.name}}"
width="24"
height="24"
/></a>
</div>
<a
class="item-control item-edit"
data-action="edit"
data-item-id="{{item._id}}"
data-tooltip="Edit Item"
>{{item.name}} </a>
{{#if item.system.damages.value}}
/
<span data-tooltip="{{item.damagedLabel}}">{{{item.damagedIcon}}}</span>
{{/if}}
</div>
<div>
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">
{{item.system.quantity}}
</a>
</div>
<div>
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">
{{item.system.weight}}
</a>
</div>
<div class="item-controls">
{{#if @root.isEditMode}}
<a
class="item-control item-delete"
data-action="delete"
data-item-id="{{item._id}}"
title="Delete Item"
><i class="fas fa-trash"></i></a>
{{/if}}
</div>
</li>
{{/each}}
</ol>
</div>
<div>
<h4>{{ localize 'IDENTITY.notes'}}</h4>
{{formInput systemFields.equipment.fields.description enriched=enrichedEquipmentNotes value=system.equipment.description name="system.equipment.description" toggled=true}}
</div>
</div>
<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"}}
@@ -93,4 +7,87 @@
<h4>{{localize 'ITEMS.defenses'}}</h4> <h4>{{localize 'ITEMS.defenses'}}</h4>
{{> "systems/vermine2047/templates/actor/parts/actor-defenses.hbs"}} {{> "systems/vermine2047/templates/actor/parts/actor-defenses.hbs"}}
</div> </div>
<div class="objects">
<h4>{{localize 'ITEMS.items'}}</h4>
<ol class="items-list">
<li class="item flexrow items-header">
<div
class="item-name"
style="flex:4;"
>{{ localize
'IDENTITY.name'}}</div>
<div class="item-quantity">{{ localize 'VERMINE.qty'}}</div>
<div class="item-weight">{{ localize 'VERMINE.weight'}}</div>
<div class="item-controls">
<a
class="item-control item-create"
title="Create item"
data-action="create"
data-type="item"
><i class="fas fa-plus"></i></a>
</div>
</li>
{{#each gear as |item id|}}
<li
class="item flexrow flex-group-center"
data-item-id="{{item._id}}"
>
<div
class="item-name"
style="flex:4;"
>
<div class="item-image">
<a
class="item-control item-edit"
data-action="edit"
data-item-id="{{item._id}}"
data-roll-type="item"
><img
src="{{item.img}}"
title="{{item.name}}"
width="24"
height="24"
/></a>
</div>
<a
class="item-control item-edit"
data-action="edit"
data-item-id="{{item._id}}"
data-tooltip="Edit Item"
>{{item.name}} </a>
{{#if item.system.damages.value}}
/
<span data-tooltip="{{item.damagedLabel}}">{{{item.damagedIcon}}}</span>
{{/if}}
</div>
<div>
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">
{{item.system.quantity}}
</a>
</div>
<div>
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">
{{item.system.weight}}
</a>
</div>
<div class="item-controls">
{{#if @root.isEditMode}}
<a
class="item-control item-delete"
data-action="delete"
data-item-id="{{item._id}}"
title="Delete Item"
><i class="fas fa-trash"></i></a>
{{/if}}
</div>
</li>
{{/each}}
</ol>
</div>
<div class="equipment-notes">
<h4>{{ localize 'IDENTITY.notes'}}</h4>
{{formInput systemFields.equipment.fields.description enriched=enrichedEquipmentNotes value=system.equipment.description name="system.equipment.description" toggled=true}}
</div>
</div> </div>
@@ -0,0 +1,71 @@
<div class="tab experience sheet-part {{#if @root.tabs.experience.active}}active{{/if}}" data-group="sheet" data-tab="experience">
<h3>{{ localize 'VERMINE.experience' }}</h3>
<div class="experience-summary">
<div class="exp-stat">
<label>{{ localize 'VERMINE.experience_dice' }}</label>
<span class="exp-value">{{ system.experience.dice }}D</span>
</div>
<div class="exp-stat">
<label>{{ localize 'VERMINE.experience_phase' }}</label>
<span class="exp-value">{{ system.experience.phase }}</span>
</div>
<div class="exp-stat">
<label>{{ localize 'VERMINE.evolution_dice' }}</label>
<span class="exp-value">{{ system.experience.evolution }}D</span>
</div>
</div>
<div class="flexrow exp-actions">
{{#if isGM}}
<button type="button" data-action="openExperiencePhase">
<i class="fas fa-star"></i> {{ localize 'VERMINE.open_experience_phase' }}
</button>
{{/if}}
<button type="button" data-action="openLearning">
<i class="fas fa-graduation-cap"></i> {{ localize 'VERMINE.open_learning' }}
</button>
</div>
<section class="exp-rewards">
<h4>{{ localize 'VERMINE.collective_rewards' }} <span class="exp-total">({{ collectiveTotal }}D)</span></h4>
<div class="exp-reward-row">
<label>{{ localize 'VERMINE.danger' }}</label>
<span>{{ system.experience.collective.danger }}D</span>
<label>{{ localize 'VERMINE.discoveries' }}</label>
<span>{{ system.experience.collective.discoveries }}D</span>
<label>{{ localize 'VERMINE.duration' }}</label>
<span>{{ system.experience.collective.duration }}D</span>
</div>
</section>
<section class="exp-rewards">
<h4>{{ localize 'VERMINE.individual_rewards' }} <span class="exp-total">({{ individualTotal }}D)</span></h4>
<div class="exp-reward-row">
<label>{{ localize 'VERMINE.implication' }}</label>
<span>{{ system.experience.individual.implication }}D</span>
<label>{{ localize 'VERMINE.influence' }}</label>
<span>{{ system.experience.individual.influence }}D</span>
<label>{{ localize 'VERMINE.interpretation' }}</label>
<span>{{ system.experience.individual.interpretation }}D</span>
</div>
</section>
<section class="exp-rewards">
<h4>{{ localize 'VERMINE.learning_limits' }}</h4>
<div class="exp-learned">
<span class="{{#if system.experience.learned.skill}}learned{{/if}}">{{ localize 'VERMINE.learned_skill' }}</span>
<span class="{{#if system.experience.learned.specialty}}learned{{/if}}">{{ localize 'VERMINE.learned_specialty' }}</span>
<span class="{{#if system.experience.learned.reserve}}learned{{/if}}">{{ localize 'VERMINE.learned_reserve' }}</span>
<span class="{{#if system.experience.learned.characteristic}}learned{{/if}}">{{ localize 'VERMINE.learned_characteristic' }}</span>
<span class="{{#if system.experience.learned.evolution}}learned{{/if}}">{{ localize 'VERMINE.learned_evolution' }}</span>
</div>
</section>
<section class="exp-rewards">
<h4>{{ localize 'VERMINE.trauma_reward' }}</h4>
<div class="exp-learned">
<span class="{{#if system.experience.traumaReward}}learned{{/if}}">{{ localize 'VERMINE.trauma_reward_5' }}</span>
</div>
</section>
</div>
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab stories sheet-part" data-group="sheet" data-tab="stories"> <div class="tab stories sheet-part {{#if @root.tabs.stories.active}}active{{/if}}" data-group="sheet" data-tab="stories">
<section class="flexrow flex-group-left flex-align-left gap-md"> <section class="flexrow flex-group-left flex-align-left gap-md">
<div> <div>
<h4>{{ localize 'IDENTITY.theme'}}</h4> <h4>{{ localize 'IDENTITY.theme'}}</h4>
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab totem sheet-part" data-group="sheet" data-tab="totem"> <div class="tab totem sheet-part {{#if @root.tabs.totem.active}}active{{/if}}" data-group="sheet" data-tab="totem">
{{#if system.identity.totem}} {{#if system.identity.totem}}
<div class="totem-details"> <div class="totem-details">
<img <img
@@ -0,0 +1,54 @@
<div class="tab domains sheet-part {{#if @root.tabs.domains.active}}active{{/if}}" data-group="sheet" data-tab="domains">
<section class="community-section">
<h3>{{ localize 'COMMUNITY.size_title' }}</h3>
<div class="flexrow community-size">
<div class="form-group" style="flex: 1;">
<label for="system.size.value">{{ localize 'COMMUNITY.size_label' }}</label>
{{#if isEditMode}}
<select name="system.size.value" data-dtype="Number">
{{#each sizeOptions as |cfg level|}}
<option value="{{ level }}" {{#ife @root.system.size.value level}}selected{{/ife}}>{{ localize cfg.label }}</option>
{{/each}}
</select>
{{else}}
<span>{{ sizeLabel }}</span>
{{/if}}
</div>
<div class="form-group" style="flex: 1;">
<label for="system.population">{{ localize 'COMMUNITY.population' }}</label>
{{#if isEditMode}}
<input type="number" name="system.population" value="{{system.population}}" data-dtype="Number" min="0" />
{{else}}
<span>{{system.population}}</span>
{{/if}}
</div>
</div>
{{#if sizeInfo}}
<p class="community-size-info">
{{ localize 'COMMUNITY.population_range' }}: <strong>{{ sizeInfo.population }}</strong> ·
{{ localize 'COMMUNITY.figures' }}: <strong>{{ sizeInfo.figures }}+</strong> ·
{{ localize 'COMMUNITY.companies' }}: <strong>{{ sizeInfo.companies }}+</strong>
</p>
{{/if}}
</section>
{{#each domains}}
<section class="community-section">
<h3>{{ this.label }}</h3>
<div class="grid grid-2col">
{{#each this.sections}}
<div class="gauge {{ this.state }}">
<label for="system.domains.{{ this.domainKey }}.{{ this.key }}.value">{{ this.label }}</label>
{{#if @root.isEditMode}}
<input type="number" name="system.domains.{{ this.domainKey }}.{{ this.key }}.value"
value="{{ this.gauge.value }}" data-dtype="Number" min="0" />
{{else}}
<span class="gauge-value">{{ this.gauge.value }}D</span>
{{/if}}
<span class="gauge-state {{ this.state }}">{{ this.stateLabel }}</span>
</div>
{{/each}}
</div>
</section>
{{/each}}
</div>
@@ -0,0 +1,137 @@
<div class="tab identity sheet-part {{#if @root.tabs.identity.active}}active{{/if}}" data-group="sheet" data-tab="identity">
<section class="community-section">
<h3>{{ localize 'COMMUNITY.identity' }}</h3>
<div class="grid grid-2col">
<div class="form-group">
<label for="system.identity.origin">{{ localize 'COMMUNITY.origin' }}</label>
{{#if isEditMode}}
<input type="text" name="system.identity.origin" value="{{system.identity.origin}}" data-dtype="String" />
{{else}}
<span>{{system.identity.origin}}</span>
{{/if}}
</div>
<div class="form-group">
<label for="system.identity.theme">{{ localize 'COMMUNITY.theme' }}</label>
{{#if isEditMode}}
<input type="text" name="system.identity.theme" value="{{system.identity.theme}}" data-dtype="String" />
{{else}}
<span>{{system.identity.theme}}</span>
{{/if}}
</div>
<div class="form-group">
<label for="system.identity.age">{{ localize 'COMMUNITY.age_label' }}</label>
{{#if isEditMode}}
<select name="system.identity.age" data-dtype="String">
{{#each ageOptions}}
<option value="{{ this.key }}" {{#ife @root.system.identity.age this.key}}selected{{/ife}}>{{ this.label }}</option>
{{/each}}
</select>
{{else}}
<span>{{#each ageOptions}}{{#ife @root.system.identity.age this.key}}{{ this.label }}{{/ife}}{{/each}}</span>
{{/if}}
</div>
<div class="form-group">
<label>{{ localize 'COMMUNITY.instincts' }}</label>
{{#if isEditMode}}
<input type="text" name="system.identity.instincts" value="{{system.identity.instincts}}" data-dtype="String" />
{{else}}
<span>{{system.identity.instincts}}</span>
{{/if}}
</div>
<div class="form-group">
<label>{{ localize 'COMMUNITY.prohibits' }}</label>
{{#if isEditMode}}
<input type="text" name="system.identity.prohibits" value="{{system.identity.prohibits}}" data-dtype="String" />
{{else}}
<span>{{system.identity.prohibits}}</span>
{{/if}}
</div>
</div>
<div class="form-group">
<label for="system.identity.notes">{{ localize 'COMMUNITY.notes' }}</label>
{{formInput systemFields.identity.fields.notes enriched=enrichedNotes value=system.identity.notes name="system.identity.notes" toggled=true}}
</div>
</section>
<section class="community-section">
<h3>{{ localize 'COMMUNITY.location' }}</h3>
<div class="grid grid-2col">
<div class="form-group">
<label for="system.location.sector">{{ localize 'COMMUNITY.sector' }}</label>
{{#if isEditMode}}
<input type="text" name="system.location.sector" value="{{system.location.sector}}" data-dtype="String" />
{{else}}
<span>{{system.location.sector}}</span>
{{/if}}
</div>
<div class="form-group">
<label for="system.location.placeType">{{ localize 'COMMUNITY.placeType' }}</label>
{{#if isEditMode}}
<input type="text" name="system.location.placeType" value="{{system.location.placeType}}" data-dtype="String" />
{{else}}
<span>{{system.location.placeType}}</span>
{{/if}}
</div>
</div>
<div class="form-group">
<label for="system.location.landmarks">{{ localize 'COMMUNITY.landmarks' }}</label>
{{formInput systemFields.location.fields.landmarks enriched=enrichedLandmarks value=system.location.landmarks name="system.location.landmarks" toggled=true}}
</div>
<div class="form-group">
<label for="system.location.territory">{{ localize 'COMMUNITY.territory' }}</label>
{{formInput systemFields.location.fields.territory enriched=enrichedTerritory value=system.location.territory name="system.location.territory" toggled=true}}
</div>
</section>
<section class="community-section">
<h3>{{ localize 'COMMUNITY.history' }}</h3>
<div class="form-group">
<label for="system.history.founding">{{ localize 'COMMUNITY.founding' }}</label>
{{#if isEditMode}}
<input type="text" name="system.history.founding" value="{{system.history.founding}}" data-dtype="String" />
{{else}}
<span>{{system.history.founding}}</span>
{{/if}}
</div>
<div class="form-group">
<label for="system.history.events">{{ localize 'COMMUNITY.events' }}</label>
{{formInput systemFields.history.fields.events enriched=enrichedEvents value=system.history.events name="system.history.events" toggled=true}}
</div>
<div class="form-group">
<label for="system.history.relations">{{ localize 'COMMUNITY.history_relations' }}</label>
{{formInput systemFields.history.fields.relations enriched=enrichedRelations value=system.history.relations name="system.history.relations" toggled=true}}
</div>
</section>
<section class="community-section">
<h3>{{ localize 'COMMUNITY.objectives' }}</h3>
<div class="form-group">
<label for="system.objectives.major">{{ localize 'COMMUNITY.objective_major' }}</label>
{{#if isEditMode}}
<input type="text" name="system.objectives.major" value="{{system.objectives.major}}" data-dtype="String" />
{{else}}
<span>{{system.objectives.major}}</span>
{{/if}}
</div>
<div class="form-group">
<label>{{ localize 'COMMUNITY.objectives_minor' }}</label>
<ol class="items-list">
{{#each system.objectives.minor as |obj index|}}
<li class="item flexrow" data-index="{{index}}">
{{#if @root.isEditMode}}
<input type="text" name="system.objectives.minor.{{index}}" value="{{obj}}" data-dtype="String" style="flex: 1;" />
<a data-action="deleteObjective" data-index="{{index}}" title="{{ localize 'VERMINE.delete' }}"><i class="fas fa-trash"></i></a>
{{else}}
<span style="flex: 1;">{{obj}}</span>
{{/if}}
</li>
{{/each}}
{{#if isEditMode}}
<li class="item flexrow">
<a data-action="addObjective" title="{{ localize 'VERMINE.add' }}"><i class="fas fa-plus"></i> {{ localize 'COMMUNITY.add_objective' }}</a>
</li>
{{/if}}
</ol>
</div>
</section>
</div>
+79
View File
@@ -0,0 +1,79 @@
<div class="tab main sheet-part" data-group="sheet" data-tab="main">
{{!-- Toggle Edit/Play --}}
<div class="sheet-header-toggle">
{{#if isEditMode}}
<button type="button" data-action="toggleSheet">{{localize "VERMINE.playMode"}}</button>
{{else}}
<button type="button" data-action="toggleSheet">{{localize "VERMINE.editMode"}}</button>
{{/if}}
</div>
{{!-- HEADER --}}
<header class="char-header grid grid-12col w-full">
<div class="grid-span-2 community-portrait">
<img class="profile-img" src="{{actor.img}}" data-action="editImage" data-edit="img" width="110"
height="110" title="{{actor.name}}" />
</div>
<section class="char-details grid-span-6">
<h1 class="char-name flexrow flex-group-left w-full">
<label>{{ localize 'IDENTITY.name' }}</label>
{{#if isEditMode}}
<input name="name" type="text" value="{{actor.name}}" placeholder="{{ localize 'COMMUNITY.name_placeholder' }}" />
{{else}}
<span>{{actor.name}}</span>
{{/if}}
</h1>
<div class="profile flexrow flex-group-center">
<label>{{ localize 'IDENTITY.profile' }}</label>
{{#if isEditMode}}
<input name="system.identity.profile" type="text" value="{{system.identity.profile}}" data-dtype="String" />
{{else}}
<span>{{#if system.identity.profile}}{{system.identity.profile}}{{else}}{{ localize 'COMMUNITY.profile_hint' }}{{/if}}</span>
{{/if}}
</div>
<div class="origin flexrow flex-group-center">
<label>{{ localize 'COMMUNITY.origin' }}</label>
{{#if isEditMode}}
<input name="system.identity.origin" type="text" value="{{system.identity.origin}}" data-dtype="String" />
{{else}}
<span>{{system.identity.origin}}</span>
{{/if}}
</div>
</section>
<section class="char-level grid-span-4">
<h1 class="char-vermine2047 flexrow flex-group-left w-full">
{{#if (eq system.identity.totem "")}}
<a data-action="chooseTotem" class="chooseTotem">{{ localize 'COMMUNITY.choose_totem' }}&hellip;</a>
{{ else }}
<a data-action="chooseTotem" class="chooseTotem">{{ smarttl "TOTEMS" system.identity.totem }}</a>
{{/if}}
</h1>
<div class="level flexrow flex-group-center">
<label>{{ localize 'VERMINE.level' }}</label>
{{#if isEditMode}}
<input name="system.level.value" type="number" value="{{system.level.value}}" data-dtype="Number" min="{{system.level.min}}" max="{{system.level.max}}" />
{{else}}
<span>{{system.level.value}}</span>
{{/if}}
<span>/ {{system.level.max}}</span>
</div>
<div class="population flexrow flex-group-center">
<label>{{ localize 'COMMUNITY.population' }}</label>
{{#if isEditMode}}
<input name="system.population" type="number" value="{{system.population}}" data-dtype="Number" min="0" />
{{else}}
<span>{{system.population}}</span>
{{/if}}
</div>
</section>
</header>
{{#if resolvedGroup}}
<div class="community-linked-group">
<span class="label">{{ localize 'COMMUNITY.linked_group' }}:</span>
<span class="value">{{ resolvedGroup.name }}</span>
</div>
{{/if}}
</div>
@@ -0,0 +1,63 @@
<div class="tab resources sheet-part {{#if @root.tabs.resources.active}}active{{/if}}" data-group="sheet" data-tab="resources">
<section class="community-section">
<h3>{{ localize 'COMMUNITY.reserves' }}</h3>
<div class="grid grid-3col">
<div class="form-group">
<label for="system.reserve.value">{{ localize 'COMMUNITY.group_reserve' }}</label>
{{#if isEditMode}}
<input type="number" name="system.reserve.value" value="{{system.reserve.value}}" data-dtype="Number" min="0" max="{{system.reserve.max}}" />
{{else}}
<span>{{system.reserve.value}} / {{system.reserve.max}}</span>
{{/if}}
</div>
<div class="form-group">
<label for="system.material">{{ localize 'COMMUNITY.material_dice' }}</label>
{{#if isEditMode}}
<input type="number" name="system.material" value="{{system.material}}" data-dtype="Number" min="0" />
{{else}}
<span>{{system.material}}D</span>
{{/if}}
</div>
<div class="form-group">
<label for="system.experience.dice">{{ localize 'COMMUNITY.experience_dice' }}</label>
{{#if isEditMode}}
<input type="number" name="system.experience.dice" value="{{system.experience.dice}}" data-dtype="Number" min="0" />
{{else}}
<span>{{system.experience.dice}}D</span>
{{/if}}
</div>
</div>
</section>
<section class="community-section">
<h3>{{ localize 'COMMUNITY.morale' }}</h3>
<div class="flexrow">
<div class="form-group" style="flex: 1;">
<label for="system.morale.value">{{ localize 'COMMUNITY.morale_value' }}</label>
{{#if isEditMode}}
<input type="number" name="system.morale.value" value="{{system.morale.value}}" data-dtype="Number" min="0" max="7" />
{{else}}
<span>{{system.morale.value}}</span>
{{/if}}
</div>
<div class="form-group" style="flex: 1;">
<label>{{ localize 'COMMUNITY.morale_state' }}</label>
<span class="morale-level {{ system.morale.level }}">{{ localize (concat 'COMMUNITY.morale_levels.' system.morale.level) }}</span>
</div>
</div>
{{#if system.allDomainsLow}}
<p class="community-warning">{{ localize 'COMMUNITY.morale_blocked' }}</p>
{{/if}}
</section>
<section class="community-section">
<h3>{{ localize 'COMMUNITY.habitants_title' }}</h3>
<p class="community-hint">{{ localize 'COMMUNITY.habitants_coming' }}</p>
{{#if resolvedGroup}}
<p class="community-linked-group">
<span class="label">{{ localize 'COMMUNITY.linked_group' }}:</span>
<span class="value">{{ resolvedGroup.name }}</span>
</p>
{{/if}}
</section>
</div>
+9 -1
View File
@@ -1,4 +1,6 @@
<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">
{{> "systems/vermine2047/templates/actor/parts/actor-equipped-gear.hbs"}}
<div class="grid grid-2col"> <div class="grid grid-2col">
{{!-- Computed Values --}} {{!-- Computed Values --}}
<div> <div>
@@ -67,6 +69,12 @@
</div> </div>
</div> </div>
{{!-- Armes --}}
<div class="grid-span-2 weapons">
<h4 class="item-name flexrow">{{ localize 'ITEMS.weapons' }}</h4>
{{> "systems/vermine2047/templates/actor/parts/actor-weapons.hbs"}}
</div>
{{!-- Equipment --}} {{!-- Equipment --}}
<div class="grid-span-2"> <div class="grid-span-2">
<h4>{{ localize 'VERMINE.gear' }}</h4> <h4>{{ localize 'VERMINE.gear' }}</h4>
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab effects sheet-part" data-group="sheet" data-tab="effects"> <div class="tab effects sheet-part {{#if @root.tabs.effects.active}}active{{/if}}" data-group="sheet" data-tab="effects">
<h4 class="item-name effect-name flexrow">{{ localize "UI.effects.name" }}</h4> <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 -1
View File
@@ -1,4 +1,4 @@
<div class="tab info sheet-part" data-group="sheet" data-tab="info"> <div class="tab info sheet-part {{#if @root.tabs.info.active}}active{{/if}}" data-group="sheet" data-tab="info">
<div class="grid grid-2col"> <div class="grid grid-2col">
<div> <div>
<h4>{{ localize 'IDENTITY.profile' }}</h4> <h4>{{ localize 'IDENTITY.profile' }}</h4>
+13 -6
View File
@@ -1,5 +1,12 @@
<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) --}}
<div class="mdb align-center">
<h4>{{ localize 'VERMINE.total_attack' }}</h4>
<a class="rollable" data-type="creature-attack" data-label="attack" title="{{ localize 'VERMINE.roll' }}">{{ system.computed.attack }}<i class="fas fa-dice-d10"></i></a>
</div>
<section class="grid grid-3col gap-md"> <section class="grid grid-3col gap-md">
<div class="mdb"> <div class="mdb">
<h4 class="align-center">{{ localize 'ADVERSITY.pattern' }}: {{ patternLabel }}</h4> <h4 class="align-center">{{ localize 'ADVERSITY.pattern' }}: {{ patternLabel }}</h4>
@@ -16,12 +23,12 @@
<div class="mdb"> <div class="mdb">
<h4 class="align-center">{{ localize 'ADVERSITY.size' }}: {{ sizeLabel }}</h4> <h4 class="align-center">{{ localize 'ADVERSITY.size' }}: {{ sizeLabel }}</h4>
<ul class="unstyled"> <ul class="unstyled">
<li>{{ localize 'ADVERSITY.attack' }}: {{ creatureSizeLevel "attack" system.size.value }}</li> <li>{{ localize 'ADVERSITY.attack' }}: {{ sizeCumulative.attack }}</li>
<li>{{ localize 'ADVERSITY.vigor' }}: {{ creatureSizeLevel "vigor" system.size.value }}</li> <li>{{ localize 'ADVERSITY.vigor' }}: {{ sizeCumulative.vigor }}</li>
<li>{{ localize 'ADVERSITY.wounds' }}: <li>{{ localize 'ADVERSITY.wounds' }}:
{{ creatureSizeLevel "minorWound" system.size.value }}/ {{ sizeCumulative.minorWound }}/
{{ creatureSizeLevel "majorWound" system.size.value }}/ {{ sizeCumulative.majorWound }}/
{{ creatureSizeLevel "deadlyWound" system.size.value }} {{ sizeCumulative.deadlyWound }}
</li> </li>
</ul> </ul>
</div> </div>
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab gear sheet-part" data-group="sheet" data-tab="gear"> <div class="tab gear sheet-part {{#if @root.tabs.gear.active}}active{{/if}}" data-group="sheet" data-tab="gear">
<div class="grid grid-2col"> <div class="grid grid-2col">
<div> <div>
<ol class="items-list"> <ol class="items-list">
+28 -2
View File
@@ -1,4 +1,4 @@
<div class="tab info sheet-part" data-group="sheet" data-tab="info"> <div class="tab info sheet-part {{#if @root.tabs.info.active}}active{{/if}}" data-group="sheet" data-tab="info">
<div class="grid grid-2col"> <div class="grid grid-2col">
{{!-- Group Identity --}} {{!-- Group Identity --}}
@@ -44,7 +44,7 @@
</h4> </h4>
<ol class="list-item"> <ol class="list-item">
{{#each abilities as |item id|}} {{#each abilities as |item id|}}
{{#if (ne item.type 'totem')}} {{#if (ne item.system.type 'totem')}}
<li class="item flexrow" data-item-id="{{item._id}}"> <li class="item flexrow" data-item-id="{{item._id}}">
<div class="item-name" style="flex:4;"> <div class="item-name" style="flex:4;">
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="{{ localize 'UI.effect_edit' }}">{{item.name}}</a> <a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="{{ localize 'UI.effect_edit' }}">{{item.name}}</a>
@@ -60,6 +60,32 @@
</ol> </ol>
</div> </div>
{{!-- Group Totem Abilities --}}
<div class="grid-span-2">
<h4>
<span>{{ localize 'VERMINE.totem_abilities' }}</span>
{{#if isEditMode}}
<a class="item-control item-create" data-action="createTotemAbility" title="{{ localize 'ITEMS.new_ability' }}"><i class="fas fa-plus"></i></a>
{{/if}}
</h4>
<ol class="list-item">
{{#each totem_abilities as |item id|}}
<li class="item flexrow" data-item-id="{{item._id}}">
<div class="item-name" style="flex:4;">
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="{{ localize 'UI.effect_edit' }}">{{item.name}}</a>
<span class="hexa" title="{{ localize 'VERMINE.level' }}">{{item.system.level.value}}</span>
<span title="{{ localize 'VERMINE.learning' }}">{{item.system.learn.threshold}}/{{item.system.learn.hindrance}}</span>
</div>
<div class="item-controls flexrow">
{{#if @root.isEditMode}}
<a class="item-control item-delete" data-action="delete" data-item-id="{{item._id}}" title="{{ localize 'UI.effect_delete' }}"><i class="fas fa-trash"></i></a>
{{/if}}
</div>
</li>
{{/each}}
</ol>
</div>
{{!-- Specialties --}} {{!-- Specialties --}}
<div> <div>
<h4> <h4>
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab reserve sheet-part" data-group="sheet" data-tab="reserve"> <div class="tab reserve sheet-part {{#if @root.tabs.reserve.active}}active{{/if}}" data-group="sheet" data-tab="reserve">
<div class="grid grid-2col"> <div class="grid grid-2col">
{{!-- Group Reserve and Morale --}} {{!-- Group Reserve and Morale --}}
<div class="grid-span-2"> <div class="grid-span-2">
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab road sheet-part" data-group="sheet" data-tab="road"> <div class="tab road sheet-part {{#if @root.tabs.road.active}}active{{/if}}" data-group="sheet" data-tab="road">
<ol class="items-list"> <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 --}}
@@ -6,7 +6,7 @@
<h4 class="ability-category-header">{{ localize 'VERMINE.ability_category.physical' }}</h4> <h4 class="ability-category-header">{{ localize 'VERMINE.ability_category.physical' }}</h4>
<div class="grid grid-2col"> <div class="grid grid-2col">
<div class="ability-item"> <div class="ability-item">
<label for="system.abilities.vigor.value">{{ localize 'ABILITIES.vigor.name' }}</label> <label for="system.abilities.vigor.value" class="resource-label rollable" data-type="ability" data-label="vigor">{{ localize 'ABILITIES.vigor.name' }}</label>
<div class="ability-control"> <div class="ability-control">
{{#if isEditMode}} {{#if isEditMode}}
<input type="range" name="system.abilities.vigor.value" value="{{system.abilities.vigor.value}}" min="{{system.abilities.vigor.min}}" max="{{system.abilities.vigor.max}}" data-dtype="Number"/> <input type="range" name="system.abilities.vigor.value" value="{{system.abilities.vigor.value}}" min="{{system.abilities.vigor.min}}" max="{{system.abilities.vigor.max}}" data-dtype="Number"/>
@@ -15,7 +15,7 @@
</div> </div>
</div> </div>
<div class="ability-item"> <div class="ability-item">
<label for="system.abilities.health.value">{{ localize 'ABILITIES.health.name' }}</label> <label for="system.abilities.health.value" class="resource-label rollable" data-type="ability" data-label="health">{{ localize 'ABILITIES.health.name' }}</label>
<div class="ability-control"> <div class="ability-control">
{{#if isEditMode}} {{#if isEditMode}}
<input type="range" name="system.abilities.health.value" value="{{system.abilities.health.value}}" min="{{system.abilities.health.min}}" max="{{system.abilities.health.max}}" data-dtype="Number"/> <input type="range" name="system.abilities.health.value" value="{{system.abilities.health.value}}" min="{{system.abilities.health.min}}" max="{{system.abilities.health.max}}" data-dtype="Number"/>
@@ -31,7 +31,7 @@
<h4 class="ability-category-header">{{ localize 'VERMINE.ability_category.manual' }}</h4> <h4 class="ability-category-header">{{ localize 'VERMINE.ability_category.manual' }}</h4>
<div class="grid grid-2col"> <div class="grid grid-2col">
<div class="ability-item"> <div class="ability-item">
<label for="system.abilities.precision.value">{{ localize 'ABILITIES.precision.name' }}</label> <label for="system.abilities.precision.value" class="resource-label rollable" data-type="ability" data-label="precision">{{ localize 'ABILITIES.precision.name' }}</label>
<div class="ability-control"> <div class="ability-control">
{{#if isEditMode}} {{#if isEditMode}}
<input type="range" name="system.abilities.precision.value" value="{{system.abilities.precision.value}}" min="{{system.abilities.precision.min}}" max="{{system.abilities.precision.max}}" data-dtype="Number"/> <input type="range" name="system.abilities.precision.value" value="{{system.abilities.precision.value}}" min="{{system.abilities.precision.min}}" max="{{system.abilities.precision.max}}" data-dtype="Number"/>
@@ -40,7 +40,7 @@
</div> </div>
</div> </div>
<div class="ability-item"> <div class="ability-item">
<label for="system.abilities.reflexes.value">{{ localize 'ABILITIES.reflexes.name' }}</label> <label for="system.abilities.reflexes.value" class="resource-label rollable" data-type="ability" data-label="reflexes">{{ localize 'ABILITIES.reflexes.name' }}</label>
<div class="ability-control"> <div class="ability-control">
{{#if isEditMode}} {{#if isEditMode}}
<input type="range" name="system.abilities.reflexes.value" value="{{system.abilities.reflexes.value}}" min="{{system.abilities.reflexes.min}}" max="{{system.abilities.reflexes.max}}" data-dtype="Number"/> <input type="range" name="system.abilities.reflexes.value" value="{{system.abilities.reflexes.value}}" min="{{system.abilities.reflexes.min}}" max="{{system.abilities.reflexes.max}}" data-dtype="Number"/>
@@ -56,7 +56,7 @@
<h4 class="ability-category-header">{{ localize 'VERMINE.ability_category.mental' }}</h4> <h4 class="ability-category-header">{{ localize 'VERMINE.ability_category.mental' }}</h4>
<div class="grid grid-2col"> <div class="grid grid-2col">
<div class="ability-item"> <div class="ability-item">
<label for="system.abilities.knowledge.value">{{ localize 'ABILITIES.knowledge.name' }}</label> <label for="system.abilities.knowledge.value" class="resource-label rollable" data-type="ability" data-label="knowledge">{{ localize 'ABILITIES.knowledge.name' }}</label>
<div class="ability-control"> <div class="ability-control">
{{#if isEditMode}} {{#if isEditMode}}
<input type="range" name="system.abilities.knowledge.value" value="{{system.abilities.knowledge.value}}" min="{{system.abilities.knowledge.min}}" max="{{system.abilities.knowledge.max}}" data-dtype="Number"/> <input type="range" name="system.abilities.knowledge.value" value="{{system.abilities.knowledge.value}}" min="{{system.abilities.knowledge.min}}" max="{{system.abilities.knowledge.max}}" data-dtype="Number"/>
@@ -65,7 +65,7 @@
</div> </div>
</div> </div>
<div class="ability-item"> <div class="ability-item">
<label for="system.abilities.perception.value">{{ localize 'ABILITIES.perception.name' }}</label> <label for="system.abilities.perception.value" class="resource-label rollable" data-type="ability" data-label="perception">{{ localize 'ABILITIES.perception.name' }}</label>
<div class="ability-control"> <div class="ability-control">
{{#if isEditMode}} {{#if isEditMode}}
<input type="range" name="system.abilities.perception.value" value="{{system.abilities.perception.value}}" min="{{system.abilities.perception.min}}" max="{{system.abilities.perception.max}}" data-dtype="Number"/> <input type="range" name="system.abilities.perception.value" value="{{system.abilities.perception.value}}" min="{{system.abilities.perception.min}}" max="{{system.abilities.perception.max}}" data-dtype="Number"/>
@@ -81,7 +81,7 @@
<h4 class="ability-category-header">{{ localize 'VERMINE.ability_category.social' }}</h4> <h4 class="ability-category-header">{{ localize 'VERMINE.ability_category.social' }}</h4>
<div class="grid grid-2col"> <div class="grid grid-2col">
<div class="ability-item"> <div class="ability-item">
<label for="system.abilities.will.value">{{ localize 'ABILITIES.will.name' }}</label> <label for="system.abilities.will.value" class="resource-label rollable" data-type="ability" data-label="will">{{ localize 'ABILITIES.will.name' }}</label>
<div class="ability-control"> <div class="ability-control">
{{#if isEditMode}} {{#if isEditMode}}
<input type="range" name="system.abilities.will.value" value="{{system.abilities.will.value}}" min="{{system.abilities.will.min}}" max="{{system.abilities.will.max}}" data-dtype="Number"/> <input type="range" name="system.abilities.will.value" value="{{system.abilities.will.value}}" min="{{system.abilities.will.min}}" max="{{system.abilities.will.max}}" data-dtype="Number"/>
@@ -90,7 +90,7 @@
</div> </div>
</div> </div>
<div class="ability-item"> <div class="ability-item">
<label for="system.abilities.empathy.value">{{ localize 'ABILITIES.empathy.name' }}</label> <label for="system.abilities.empathy.value" class="resource-label rollable" data-type="ability" data-label="empathy">{{ localize 'ABILITIES.empathy.name' }}</label>
<div class="ability-control"> <div class="ability-control">
{{#if isEditMode}} {{#if isEditMode}}
<input type="range" name="system.abilities.empathy.value" value="{{system.abilities.empathy.value}}" min="{{system.abilities.empathy.min}}" max="{{system.abilities.empathy.max}}" data-dtype="Number"/> <input type="range" name="system.abilities.empathy.value" value="{{system.abilities.empathy.value}}" min="{{system.abilities.empathy.min}}" max="{{system.abilities.empathy.max}}" data-dtype="Number"/>
+2 -1
View File
@@ -1,4 +1,4 @@
<div class="tab combat sheet-part" data-group="sheet" data-tab="combat"> <div class="tab combat sheet-part {{#if @root.tabs.combat.active}}active{{/if}}" data-group="sheet" data-tab="combat">
<div class="grid grid-2col"> <div 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">
+10
View File
@@ -0,0 +1,10 @@
<div class="tab equipment sheet-part {{#if @root.tabs.equipment.active}}active{{/if}}" data-group="sheet" data-tab="equipment">
<div class="weapons">
<h4>{{localize 'ITEMS.weapons'}}</h4>
{{> "systems/vermine2047/templates/actor/parts/actor-weapons.hbs"}}
</div>
<div class="protections">
<h4>{{localize 'ITEMS.defenses'}}</h4>
{{> "systems/vermine2047/templates/actor/parts/actor-defenses.hbs"}}
</div>
</div>
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab notes sheet-part" data-group="sheet" data-tab="notes"> <div class="tab notes sheet-part {{#if @root.tabs.notes.active}}active{{/if}}" data-group="sheet" data-tab="notes">
<h3>{{ localize 'IDENTITY.notes' }}</h3> <h3>{{ localize 'IDENTITY.notes' }}</h3>
<div class="grid grid-1col"> <div class="grid grid-1col">
<div class="form-group"> <div class="form-group">
+2 -1
View File
@@ -1,4 +1,4 @@
<div class="tab skills sheet-part" data-group="sheet" data-tab="skills"> <div class="tab skills sheet-part {{#if @root.tabs.skills.active}}active{{/if}}" data-group="sheet" data-tab="skills">
<h3>{{ localize 'VERMINE.skills' }}</h3> <h3>{{ localize 'VERMINE.skills' }}</h3>
{{!-- Category Selector --}} {{!-- Category Selector --}}
@@ -27,6 +27,7 @@
{{> "systems/vermine2047/templates/actor/parts/npc-skill-category.hbs" {{> "systems/vermine2047/templates/actor/parts/npc-skill-category.hbs"
categoryKey=key categoryKey=key
categoryLabel=(concat "VERMINE.skill_category." key) categoryLabel=(concat "VERMINE.skill_category." key)
skills=@root.system.skills
}} }}
{{/ife}} {{/ife}}
{{/each}} {{/each}}
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="tab threat sheet-part" data-group="sheet" data-tab="threat"> <div class="tab threat sheet-part {{#if @root.tabs.threat.active}}active{{/if}}" data-group="sheet" data-tab="threat">
<h3>{{ localize 'ADVERSITY.threat_details' }}</h3> <h3>{{ localize 'ADVERSITY.threat_details' }}</h3>
<div class="grid grid-3col"> <div class="grid grid-3col">
{{!-- Menace --}} {{!-- Menace --}}
@@ -48,7 +48,7 @@
{{/ife}} {{/ife}}
> >
</h4> </h4>
{{#each @root.system.skills as |skill skey|}} {{#each (sortedSkills @root.system.skills) as |skill skey|}}
{{#if (eq skill.category sckey) }} {{#if (eq skill.category sckey) }}
<div class="ability flexrow "> <div class="ability flexrow ">
@@ -70,14 +70,16 @@
data-tooltip="ajout de spécialité" data-tooltip="ajout de spécialité"
></i> ></i>
{{/ifgt}} {{/ifgt}}
{{#each skill.specialties as |spe ind|}} {{#if @root.specialties}}
{{#each @root.specialties as |spe|}}
{{#ife spe.system.skill skey}} {{#ife spe.system.skill skey}}
<i <span
class="specialty" class="specialty"
data-tooltip="spécialité utilisable" data-tooltip="spécialité utilisable"
>{{spe.name}}</i> >{{spe.name}}<sup>{{spe.system.level}}</sup></span>
{{/ife}} {{/ife}}
{{/each}} {{/each}}
{{/if}}
</div> </div>
<select <select
name="system.skills.{{skey}}.value" name="system.skills.{{skey}}.value"
+9
View File
@@ -15,6 +15,8 @@
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>
</div> </div>
</li> </li>
{{#each defenses as |item id|}} {{#each defenses as |item id|}}
@@ -41,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 'ADVERSITY.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}}
+8
View File
@@ -9,6 +9,9 @@
<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-edit"></i></a>
</div> </div>
</li> </li>
{{#each weapons as |item id|}} {{#each weapons as |item id|}}
@@ -35,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>
+2 -2
View File
@@ -2,8 +2,8 @@
<div class="skill-category"> <div class="skill-category">
<h4>{{ localize categoryLabel }}</h4> <h4>{{ localize categoryLabel }}</h4>
<div class="grid grid-2col"> <div class="grid grid-2col">
{{#each system.skills as |skill key|}} {{#each (sortedSkills skills) as |skill key|}}
{{#ife skill.category categoryKey}} {{#ife skill.category ../categoryKey}}
{{> "systems/vermine2047/templates/actor/parts/npc-skill-item.hbs" skill=skill key=key}} {{> "systems/vermine2047/templates/actor/parts/npc-skill-item.hbs" skill=skill key=key}}
{{/ife}} {{/ife}}
{{/each}} {{/each}}
+4 -2
View File
@@ -1,6 +1,6 @@
<div class="skill-item"> <div class="skill-item">
<label for="system.skills.{{key}}.value" title="{{ localize (concat 'SKILLS.' key) }} - {{ localize (concat 'VERMINE.rarity_' skill.rarity) }}"> <label for="system.skills.{{key}}.value" class="resource-label rollable" data-type="skill" data-label="{{key}}" title="{{ smarttlk 'SKILLS' key 'name' }} - {{ localize (concat 'VERMINE.rarity_' skill.rarity) }}">
{{ localize (concat 'SKILLS.' key) }} {{ smarttlk 'SKILLS' key 'name' }}
{{#if skill.rarity}} {{#if skill.rarity}}
<span class="rarity-badge rarity-{{skill.rarity}}" title="{{ localize (concat 'VERMINE.rarity_' skill.rarity) }}"> <span class="rarity-badge rarity-{{skill.rarity}}" title="{{ localize (concat 'VERMINE.rarity_' skill.rarity) }}">
{{skill.rarity}} {{skill.rarity}}
@@ -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>
+4 -4
View File
@@ -7,18 +7,18 @@
{{#if (ne tk "adapted")}} {{#if (ne tk "adapted")}}
<div class="flexrow row mdb flex-group-center"> <div class="flexrow row mdb flex-group-center">
<h4 style="flex:33%;"> <h4 style="flex:30%;text-align:center;">
<a class="totem" data-totem="{{ tk }}" <a class="totem" data-totem="{{ tk }}"
title="{{ smarttlk "TOTEMS" title="{{ smarttlk "TOTEMS"
tk "description"}}"> tk "description"}}">
<img <img
src="/systems/vermine2047/assets/images/ui/totems/{{ tk }}.webp" src="/systems/vermine2047/assets/images/ui/totems/{{ tk }}.webp"
alt="{{ localize totem }}" width="80" height="80" /> alt="{{ localize totem }}" width="60" height="60" />
<h2>{{ smarttlk "TOTEMS" <h2 style="font-size:1rem;">{{ smarttlk "TOTEMS"
tk "name"}}</h2> tk "name"}}</h2>
</a> </a>
</h4> </h4>
<p style="flex:66%;text-align:left;"> <p style="flex:70%;text-align:left;margin:0;font-size:0.8rem;">
<strong>Instincts</strong> : {{ smarttlk "TOTEMS" tk <strong>Instincts</strong> : {{ smarttlk "TOTEMS" tk
"instincts"}}<br> "instincts"}}<br>
<strong>Interdits</strong> : {{ smarttlk "TOTEMS" tk <strong>Interdits</strong> : {{ smarttlk "TOTEMS" tk
+234
View File
@@ -0,0 +1,234 @@
<div class="roll-dialog-content">
<!-- HIDDEN DATA -->
<input type="hidden" name="ability" id="ability" value="{{ abilityValue }}" />
<input type="hidden" name="speakerId" value="{{ actor.id }}" />
{{#if weapon}}
<div class="weapon-block combat-dialog-weapon">
<div class="weapon-block-header flexrow">
<span class="weapon-icon"><i class="fas fa-crosshairs"></i></span>
<span class="weapon-name">{{ weapon.name }}</span>
</div>
<div class="flexrow flex-wrap weapon-details">
<span class="weapon-detail">
<strong>{{localize "VERMINE.damages"}}:</strong>
{{#if weapon.system.damage.addVigor}}{{localize "ADVERSITY.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>
@@ -0,0 +1,109 @@
<div class="roll-dialog-content">
<h3 class="combat-mode-label">{{ localize 'VERMINE.experience_phase' }}</h3>
<details class="difficulty-section" open>
<summary class="flexrow">
<span class="label">{{ localize 'VERMINE.collective_rewards' }}</span>
<span class="current-values"><span id="collective-total">0D</span></span>
</summary>
<div class="flexrow difficulty-controls">
<div class="flexcol">
<label class="label" for="collective-danger">{{ localize 'VERMINE.danger' }}</label>
<select class="info-value" data-roll="true" id="collective-danger" name="collective-danger">
{{#each rewards}}
<option value="{{ this.value }}">{{ this.label }} ({{ this.value }}D)</option>
{{/each}}
</select>
</div>
<div class="flexcol">
<label class="label" for="collective-discoveries">{{ localize 'VERMINE.discoveries' }}</label>
<select class="info-value" data-roll="true" id="collective-discoveries" name="collective-discoveries">
{{#each rewards}}
<option value="{{ this.value }}">{{ this.label }} ({{ this.value }}D)</option>
{{/each}}
</select>
</div>
<div class="flexcol">
<label class="label" for="collective-duration">{{ localize 'VERMINE.duration' }}</label>
<select class="info-value" data-roll="true" id="collective-duration" name="collective-duration">
{{#each rewards}}
<option value="{{ this.value }}">{{ this.label }} ({{ this.value }}D)</option>
{{/each}}
</select>
</div>
</div>
{{#if groups.length}}
<div class="flexrow bonus-item">
<label class="label" for="group-select">{{ localize 'VERMINE.collective_group' }}</label>
<select class="info-value" id="group-select" name="group-select">
<option value="">{{ localize 'VERMINE.none' }}</option>
{{#each groups}}
<option value="{{ this.id }}">{{ this.name }} ({{ this.dice }}D)</option>
{{/each}}
</select>
</div>
{{/if}}
</details>
<div class="exp-phase-characters">
{{#each characters}}
<details class="difficulty-section" data-character-id="{{ this.id }}" open>
<summary class="flexrow">
<span class="label">{{ this.name }}</span>
<span class="current-values">{{ localize 'VERMINE.experience_dice' }}: {{ this.dice }}D</span>
</summary>
<div class="flexrow difficulty-controls">
<div class="flexcol">
<label class="label" for="implication-{{ this.id }}">{{ localize 'VERMINE.implication' }}</label>
<select class="info-value" data-roll="true" id="implication-{{ this.id }}" name="implication-{{ this.id }}">
{{#each @root.rewards}}
<option value="{{ this.value }}">{{ this.label }} ({{ this.value }}D)</option>
{{/each}}
</select>
</div>
<div class="flexcol">
<label class="label" for="influence-{{ this.id }}">{{ localize 'VERMINE.influence' }}</label>
<select class="info-value" data-roll="true" id="influence-{{ this.id }}" name="influence-{{ this.id }}">
{{#each @root.rewards}}
<option value="{{ this.value }}">{{ this.label }} ({{ this.value }}D)</option>
{{/each}}
</select>
</div>
<div class="flexcol">
<label class="label" for="interpretation-{{ this.id }}">{{ localize 'VERMINE.interpretation' }}</label>
<select class="info-value" data-roll="true" id="interpretation-{{ this.id }}" name="interpretation-{{ this.id }}">
{{#each @root.rewards}}
<option value="{{ this.value }}">{{ this.label }} ({{ this.value }}D)</option>
{{/each}}
</select>
</div>
</div>
<div class="flexrow bonus-item">
<label class="flexrow">
<input type="checkbox" data-roll="true" id="objective-minor-{{ this.id }}" name="objective-minor-{{ this.id }}" />
<span class="label">{{ localize 'VERMINE.objective_minor' }} (+3D)</span>
</label>
<label class="flexrow">
<input type="checkbox" data-roll="true" id="objective-major-{{ this.id }}" name="objective-major-{{ this.id }}" />
<span class="label">{{ localize 'VERMINE.objective_major' }} (+5D)</span>
</label>
<label class="flexrow">
<input type="checkbox" data-roll="true" id="trauma-{{ this.id }}" name="trauma-{{ this.id }}" />
<span class="label">{{ localize 'VERMINE.trauma_reward' }} (+5D)</span>
</label>
</div>
</details>
{{else}}
<p>{{ localize 'VERMINE.no_character' }}</p>
{{/each}}
</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="apply">
<i class="fas fa-check"></i> {{localize "VERMINE.apply"}}
</button>
</footer>
</div>
+94
View File
@@ -0,0 +1,94 @@
<div class="roll-dialog-content">
<h3 class="combat-mode-label">{{ localize 'VERMINE.open_learning' }}</h3>
<div class="learning-summary">
<span class="label">{{ localize 'VERMINE.experience_dice' }}:</span>
<span class="exp-value">{{ dice }}D</span>
</div>
<div class="learning-type">
<label><input type="radio" data-roll="true" name="learning-type" value="skill" checked /><span>{{ localize 'VERMINE.learn_skill' }}</span></label>
<label><input type="radio" data-roll="true" name="learning-type" value="specialty" /><span>{{ localize 'VERMINE.learn_specialty' }}</span></label>
<label><input type="radio" data-roll="true" name="learning-type" value="characteristic" /><span>{{ localize 'VERMINE.learn_characteristic' }}</span></label>
<label><input type="radio" data-roll="true" name="learning-type" value="reserve" /><span>{{ localize 'VERMINE.learn_reserve' }}</span></label>
<label><input type="radio" data-roll="true" name="learning-type" value="evolution" /><span>{{ localize 'VERMINE.learn_evolution' }}</span></label>
</div>
<div id="learning-skill-section" class="learning-targets">
<label class="label" for="learning-target">{{ localize 'VERMINE.learn_skill' }}</label>
<select class="info-value" data-roll="true" id="learning-target" name="learning-target">
{{#each skills}}
<option value="{{ this.key }}" data-value="{{ this.value }}" data-rarity="{{ this.rarity }}" data-domain="{{ this.isDomain }}">
{{ this.name }} ({{ this.value }}{{ this.target }})
</option>
{{/each}}
</select>
</div>
<div id="learning-specialty-section" class="learning-targets" style="display:none;">
<div class="flexrow bonus-item">
<label class="label" for="specialty-name">{{ localize 'VERMINE.learn_specialty_name' }}</label>
<input type="text" data-roll="true" id="specialty-name" name="specialty-name" value="{{ localize 'ITEMS.new_specialty' }}" />
</div>
<p class="learning-hint">{{ localize 'VERMINE.specialty_hint' }}</p>
</div>
<div id="learning-ability-section" class="learning-targets" style="display:none;">
<label class="label" for="learning-ability">{{ localize 'VERMINE.learn_characteristic' }}</label>
<select class="info-value" data-roll="true" id="learning-ability" name="learning-ability">
{{#each abilities}}
<option value="{{ this.key }}">{{ this.label }} ({{ this.value }})</option>
{{/each}}
</select>
<p class="learning-hint">{{ localize 'VERMINE.characteristic_hint' }}</p>
</div>
<div id="learning-reserve-section" class="learning-targets" style="display:none;">
<label class="label" for="learning-reserve">{{ localize 'VERMINE.learn_reserve' }}</label>
<select class="info-value" data-roll="true" id="learning-reserve" name="learning-reserve">
{{#each reserves}}
<option value="{{ this.key }}">{{ this.label }} ({{ this.value }}/{{ this.max }})</option>
{{/each}}
</select>
<p class="learning-hint">{{ localize 'VERMINE.reserve_hint' }}</p>
</div>
<div id="learning-evolution-section" class="learning-targets" style="display:none;">
<p class="learning-hint">{{ localize 'VERMINE.evolution_hint' }}</p>
</div>
<div class="flexrow bonus-item full-width">
<label class="label" for="learning-dice">{{ localize 'VERMINE.dice_spent' }}</label>
<input type="number" class="numeric-entry" style="text-align:center;" data-roll="true"
id="learning-dice" name="learning-dice" min="1" max="{{ dice }}" value="1" />
<span>D</span>
</div>
<div class="learning-result">
<div class="context-item">
<span class="context-label">{{ localize 'VERMINE.difficulty' }}</span>
<span class="context-value" id="diff-value">5</span>
</div>
<div class="context-item">
<span class="context-label">{{ localize 'VERMINE.handicap' }}</span>
<span class="context-value" id="handicap-value">0</span>
</div>
<div class="context-item">
<span class="context-label">{{ localize 'VERMINE.success_required' }}</span>
<span class="context-value" id="required-value">1</span>
</div>
<div class="context-item">
<span class="context-label">{{ localize 'VERMINE.pool_total' }}</span>
<span class="context-value" id="pool-value">1D</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-graduation-cap"></i> {{localize "VERMINE.roll"}}
</button>
</footer>
</div>
+27
View File
@@ -335,6 +335,33 @@
</div> </div>
</details> </details>
<!-- EXPERIENCE DICE -->
{{#if experience.hasExperience}}
<details class="experience-section">
<summary class="flexrow">
<span class="label">{{localize 'VERMINE.experience_transfer'}}</span>
<span class="current-values">{{localize 'VERMINE.experience_dice'}}: <span id="exp-individual">{{experience.individual}}</span>D</span>
</summary>
<div class="flexcol experience-controls">
<div class="flexrow bonus-item">
<label class="label" for="experience-pull">{{localize 'VERMINE.experience_pull'}}</label>
<select class="info-value" data-roll="true" id="experience-pull" name="experience-pull"
{{#unless experience.canPull}}disabled{{/unless}}>
{{#each experience.pullOptions}}
<option value="{{this}}">{{this}}D</option>
{{/each}}
</select>
<span class="experience-group">{{localize 'VERMINE.collective_group'}}: <span id="exp-group">{{experience.groupPool}}</span>D</span>
</div>
<div class="flexrow bonus-item">
<button type="button" data-action="giveToGroup" {{#unless experience.canGive}}disabled{{/unless}}>
<i class="fas fa-share"></i> {{localize 'VERMINE.experience_give'}}
</button>
</div>
</div>
</details>
{{/if}}
<!-- TOTAL DICE POOL --> <!-- TOTAL DICE POOL -->
<div class="flexrow total-section"> <div class="flexrow total-section">
<label class="label">{{localize 'VERMINE.total'}}:</label> <label class="label">{{localize 'VERMINE.total'}}:</label>
+13 -6
View File
@@ -3,12 +3,19 @@
{{!-- Specialty specific content --}} {{!-- Specialty specific content --}}
<div class="grid grid-2col"> <div class="grid grid-2col">
{{#if item.system.skill}} {{#if item.system.skill}}
<div class="resource flexrow"> <div class="resource flexrow">
<span class="resource-label">{{ localize 'VERMINE.skill_title' }}:</span> <span class="resource-label">{{ localize 'VERMINE.skill_title' }}:</span>
<span>{{ smarttlk 'SKILLS' item.system.skill 'name' }}</span> <span>{{ smarttlk 'SKILLS' item.system.skill 'name' }}</span>
</div> </div>
{{/if}} {{/if}}
{{#if item.system.level}}
<div class="resource flexrow">
<span class="resource-label">{{ localize 'VERMINE.level' }}:</span>
<span>{{ item.system.level }}</span>
</div>
{{/if}}
{{#if item.system.description}} {{#if item.system.description}}
<div class="resource flexcol full-width"> <div class="resource flexcol full-width">
+1 -1
View File
@@ -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>
+28 -19
View File
@@ -13,25 +13,34 @@
value="{{item.name}}" value="{{item.name}}"
placeholder="Name" placeholder="Name"
/></h1> /></h1>
<select <div class="flexrow">
name="system.skill" <select
class="skill-select" name="system.skill"
> class="skill-select"
{{#each @root.config.skillCategories as |skillCategory sckey|}} >
<optgroup label="{{ smarttlk 'SKILLS_CATEGORIES' sckey 'name' }}"> {{#each @root.config.skillCategories as |skillCategory sckey|}}
{{#each @root.config.skills as |skill key|}} <optgroup label="{{ smarttlk 'SKILLS_CATEGORIES' sckey 'name' }}">
{{#ife skill.category sckey}} {{#each (sortedSkills @root.config.skills) as |skill key|}}
<option {{#ife skill.category sckey}}
value="{{key}}" <option
{{#ife key @root.item.system.skill}} value="{{key}}"
selected {{#ife key @root.item.system.skill}}
{{/ife}} selected
>{{ smarttlk 'SKILLS' key 'name' }}</option> {{/ife}}
{{/ife}} >{{ smarttlk 'SKILLS' key 'name' }}</option>
{{/each}} {{/ife}}
</optgroup> {{/each}}
{{/each}} </optgroup>
</select> {{/each}}
</select>
<select
name="system.level"
class="skill-select"
title="{{ localize 'VERMINE.level' }}"
>
{{selectOptions @root.config.SkillLevels selected=item.system.level localize=true}}
</select>
</div>
</div> </div>
</header> </header>
+56 -15
View File
@@ -1,23 +1,64 @@
<div class="{{cssClass}}"> <div class="{{cssClass}}">
{{> "systems/vermine2047/templates/item/partials/header.hbs"}} {{> "systems/vermine2047/templates/item/partials/header.hbs"}}
<section class="sheet-body">
{{> "systems/vermine2047/templates/item/partials/traits.hbs"}}
<section class="sheet-body"> <section class="vehicle-fields">
{{> "systems/vermine2047/templates/item/partials/traits.hbs"}} <h3>{{ localize "VERMINE.vehicle_specs" }}</h3>
<div class="flexrow">
<div class="resource align-center flexcol">
<label class="resource-label">{{ localize "VERMINE.kind" }}</label>
<select name="system.kind" data-dtype="String">
<option value="vehicle" {{#ife system.kind "vehicle"}}selected{{/ife}}>{{ localize "VERMINE.kind_vehicle" }}</option>
<option value="mount" {{#ife system.kind "mount"}}selected{{/ife}}>{{ localize "VERMINE.kind_mount" }}</option>
</select>
</div>
<div class="resource align-center flexcol">
<label class="resource-label">{{ localize "VERMINE.distance" }}</label>
<div class="hexa"><input type="number" name="system.distance" value="{{system.distance}}" data-dtype="Number" /></div>
<span class="unit">km</span>
</div>
<div class="resource align-center flexcol">
<label class="resource-label">{{ localize "VERMINE.speed" }}</label>
<div class="flexrow align-center">
<input type="number" name="system.speed.cruise" value="{{system.speed.cruise}}" data-dtype="Number" />
<span>/</span>
<input type="number" name="system.speed.top" value="{{system.speed.top}}" data-dtype="Number" />
</div>
<input type="text" name="system.speed.topDuration" value="{{system.speed.topDuration}}" placeholder="{{ localize 'VERMINE.speed_duration_ph' }}" />
</div>
</div>
<div class="flexrow"> <div class="flexrow">
<div class="resource flexrow align-center"> <div class="resource align-center flexcol">
<label class="resource-label">{{ localize "VERMINE.mobility"}}</label> <label class="resource-label">{{ localize "VERMINE.effort_reserve" }}</label>
<div class="hexa"> <div class="flexrow align-center">
<input type="number" name="system.mobility" value="{{system.mobility}}" data-dtype="Number"/> <div class="hexa"><input type="number" name="system.effortReserve.value" value="{{system.effortReserve.value}}" data-dtype="Number" /></div>
</div> <span>/</span>
</div> <div class="hexa"><input type="number" name="system.effortReserve.max" value="{{system.effortReserve.max}}" data-dtype="Number" /></div>
</div>
</div>
<div class="resource align-center flexcol">
<label class="resource-label">{{ localize "VERMINE.mobility" }}</label>
<div class="hexa"><input type="number" name="system.mobility" value="{{system.mobility}}" data-dtype="Number" /></div>
</div>
</div>
<div class="maintenance">
<label class="resource-label">{{ localize "VERMINE.maintenance" }}</label>
<div class="flexrow flex-wrap">
{{#each maintenanceSkills}}
<label class="flexrow align-center">
<input type="checkbox" name="system.maintenance" value="{{ this.key }}" {{#if this.checked}}checked{{/if}} />
<span>{{ this.label }}</span>
</label>
{{/each}}
</div>
</div>
</section>
</div> {{> "systems/vermine2047/templates/item/partials/physicalItems.hbs"}}
{{> "systems/vermine2047/templates/item/partials/physicalItems.hbs"}} </section>
</section>
</div> </div>
+1 -1
View File
@@ -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}}"
+164 -8
View File
@@ -1,10 +1,151 @@
<div class="vermine-roll-message"> <div class="vermine-roll-message">
{{!-- Verdict --}}
<div class="roll-verdict {{#if verdict.success}}success{{else}}failure{{/if}}">
{{#if verdict.success}}
<i class="fas fa-check-circle"></i>
<span>{{localize "VERMINE.roll_success"}}</span>
{{else}}
<i class="fas fa-times-circle"></i>
<span>{{localize "VERMINE.roll_failure"}}</span>
{{/if}}
</div>
<h3>{{param.actor.name}} : {{localize "VERMINE.test_of"}} {{param.rollLabel}}</h3> <h3>{{param.actor.name}} : {{localize "VERMINE.test_of"}} {{param.rollLabel}}</h3>
<div class="flexrow"> {{#if param.weapon}}
<h4>{{localize "VERMINE.difficulty"}}:</h4> <div class="weapon-block">
<span id="difficulty">{{param.difficulty}}</span> <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 "ADVERSITY.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> </div>
{{/if}}
{{#if param.attack}}
<div class="exchange-actions" data-exchange-id="{{param.attack.id}}">
<div class="exchange-header"><i class="fas fa-exchange-alt"></i> {{localize "VERMINE.exchange"}}</div>
{{#each param.attack.targets}}
<div class="exchange-target">
<span class="exchange-target-name"><i class="fas fa-bullseye"></i> {{this.name}}</span>
{{#if this.canDefend}}
<button type="button" class="exchange-defend" data-type="esquive" data-target-id="{{this.id}}" data-target-uuid="{{this.uuid}}" title="{{localize 'VERMINE.defense_esquive'}}"><i class="fas fa-running"></i> {{localize "VERMINE.defense_esquive"}}</button>
{{#ife @root.param.attack.mode "contact"}}
<button type="button" class="exchange-defend" data-type="parade" data-target-id="{{this.id}}" data-target-uuid="{{this.uuid}}" title="{{localize 'VERMINE.defense_parade'}}"><i class="fas fa-shield-alt"></i> {{localize "VERMINE.defense_parade"}}</button>
{{/ife}}
{{else}}
<span class="exchange-passive"><i class="fas fa-shield-virus"></i> {{localize "VERMINE.passive_defense"}}</span>
{{/if}}
</div>
{{/each}}
<button type="button" class="exchange-resolve" data-exchange-id="{{param.attack.id}}"><i class="fas fa-balance-scale"></i> {{localize "VERMINE.resolve"}}</button>
</div>
{{/if}}
{{!-- Context --}}
<div class="roll-context flexrow flex-wrap">
<div class="context-item">
<span class="context-label">{{localize "VERMINE.difficulty"}}</span>
<span class="context-value" id="difficulty">{{param.difficulty}}</span>
</div>
<div class="context-item">
<span class="context-label">{{localize "VERMINE.success_required"}}</span>
<span class="context-value">{{verdict.required}}</span>
</div>
{{#if param.handicap}}
<div class="context-item">
<span class="context-label">{{localize "VERMINE.handicap"}}</span>
<span class="context-value">+{{param.handicap}}</span>
</div>
{{/if}}
{{#if param.specialtyName}}
<div class="context-item">
<span class="context-label">{{localize "VERMINE.specialty"}}</span>
<span class="context-value">+1D {{param.specialtyName}}</span>
</div>
{{/if}}
{{#if param.self_control}}
<div class="context-item">
<span class="context-label">{{localize "VERMINE.self_control"}}</span>
<span class="context-value">+{{param.self_control}}D</span>
</div>
{{/if}}
{{#if param.totems.human}}
<div class="context-item totem-human">
<span class="context-label">{{localize "VERMINE.totem_used"}}</span>
<span class="context-value">{{localize "TOTEMS.human.name"}}{{#ife param.keepTotem "human"}} ({{localize "VERMINE.totem_keep"}}){{/ife}}</span>
</div>
{{/if}}
{{#if param.totems.adapted}}
<div class="context-item totem-adapted">
<span class="context-label">{{localize "VERMINE.totem_used"}}</span>
<span class="context-value">{{localize "TOTEMS.adapted.name"}}{{#ife param.keepTotem "adapted"}} ({{localize "VERMINE.totem_keep"}}){{/ife}}</span>
</div>
{{/if}}
{{#if param.skillLevel}}
<div class="context-item">
<span class="context-label">{{localize "VERMINE.skill_mastery"}}</span>
<span class="context-value">{{skillLevel "label" param.skillLevel}}</span>
</div>
{{/if}}
</div>
{{!-- Dice pool breakdown --}}
{{#if param.poolBreakdown}}
<div class="roll-pool">
<h4>{{localize "VERMINE.roll_pool"}}</h4>
<div class="flexrow flex-wrap pool-breakdown">
{{#if param.poolBreakdown.ability}}
<span class="pool-item">{{localize "VERMINE.pool_ability"}} {{param.poolBreakdown.ability}}D</span>
{{/if}}
{{#if param.poolBreakdown.skill}}
<span class="pool-item">{{localize "VERMINE.pool_skill"}} {{param.poolBreakdown.skill}}D</span>
{{/if}}
{{#if param.poolBreakdown.specialty}}
<span class="pool-item">{{localize "VERMINE.pool_specialty"}} +{{param.poolBreakdown.specialty}}D</span>
{{/if}}
{{#if param.poolBreakdown.help}}
<span class="pool-item">{{localize "VERMINE.pool_help"}} +{{param.poolBreakdown.help}}D</span>
{{/if}}
{{#if param.poolBreakdown.tooling}}
<span class="pool-item">{{localize "VERMINE.pool_tooling"}} +{{param.poolBreakdown.tooling}}D</span>
{{/if}}
{{#if param.poolBreakdown.group}}
<span class="pool-item">{{localize "VERMINE.pool_group"}} +{{param.poolBreakdown.group}}D</span>
{{/if}}
{{#if param.poolBreakdown.pools}}
<span class="pool-item">{{localize "VERMINE.pool_pools"}} +{{param.poolBreakdown.pools}}D</span>
{{/if}}
{{#if param.poolBreakdown.experience}}
<span class="pool-item">{{localize "VERMINE.pool_experience"}} +{{param.poolBreakdown.experience}}D</span>
{{/if}}
{{#if param.poolBreakdown.selfControl}}
<span class="pool-item">{{localize "VERMINE.pool_selfControl"}} +{{param.poolBreakdown.selfControl}}D</span>
{{/if}}
{{#if param.poolBreakdown.domainBonus}}
<span class="pool-item">{{#ifgt param.poolBreakdown.domainBonus 0}}{{localize "VERMINE.domain_bonus"}}{{else}}{{localize "VERMINE.domain_penalty"}}{{/ifgt}} {{param.poolBreakdown.domainBonus}}D</span>
{{/if}}
<span class="pool-item pool-total">{{localize "VERMINE.pool_total"}} {{param.poolBreakdown.total}}D</span>
</div>
</div>
{{/if}}
<div class="reroll-fromroll"> <div class="reroll-fromroll">
<h4>{{localize "VERMINE.rerolls_possible"}} : <span id="allowed_reroll">{{param.Reroll}}</span></h4> <h4>{{localize "VERMINE.rerolls_possible"}} : <span id="allowed_reroll">{{param.Reroll}}</span></h4>
@@ -26,20 +167,21 @@
</div> </div>
</div> </div>
{{!-- Dice results --}}
<ul class="flexrow roll-results initial-roll" data-difficulty="{{param.difficulty}}"> <ul class="flexrow roll-results initial-roll" data-difficulty="{{param.difficulty}}">
{{#each roll.dice as |dieType index|}} {{#each roll.dice as |dieType index|}}
{{#each dieType.results as |die index|}} {{#each dieType.results as |die index|}}
{{! Determine dice class and type based on flavor }} {{! Determine dice class and type based on flavor (leading token) }}
{{#ifincludes dieType.options.flavor "human"}} {{#ifStartsWith dieType.options.flavor "human_"}}
{{set diceClass="human"}} {{set diceClass="human"}}
{{set diceTypeVal="human"}} {{set diceTypeVal="human"}}
{{else ifincludes dieType.options.flavor "adapted"}} {{else ifStartsWith dieType.options.flavor "adapted_"}}
{{set diceClass="adapted"}} {{set diceClass="adapted"}}
{{set diceTypeVal="adapted"}} {{set diceTypeVal="adapted"}}
{{else}} {{else}}
{{set diceClass="regular"}} {{set diceClass="regular"}}
{{set diceTypeVal="regular"}} {{set diceTypeVal="regular"}}
{{/ifincludes}} {{/ifStartsWith}}
<li class="roll die flexcol {{diceClass}} {{#if die.success}}success{{/if}}" <li class="roll die flexcol {{diceClass}} {{#if die.success}}success{{/if}}"
data-dice-type="{{diceTypeVal}}" data-dice-type="{{diceTypeVal}}"
data-result="{{die.result}}"> data-result="{{die.result}}">
@@ -49,6 +191,20 @@
{{/each}} {{/each}}
</ul> </ul>
{{!-- Dice summary --}}
<div class="roll-dice-summary flexrow">
{{#if diceStats.regular.count}}
<span class="die-summary regular">{{diceStats.regular.count}} {{localize "VERMINE.die_regular"}} · {{diceStats.regular.success}} {{localize "VERMINE.die_successes"}}</span>
{{/if}}
{{#if diceStats.human.count}}
<span class="die-summary human">{{diceStats.human.count}} {{localize "VERMINE.die_human"}} · {{diceStats.human.success}} {{localize "VERMINE.die_successes"}}</span>
{{/if}}
{{#if diceStats.adapted.count}}
<span class="die-summary adapted">{{diceStats.adapted.count}} {{localize "VERMINE.die_adapted"}} · {{diceStats.adapted.success}} {{localize "VERMINE.die_successes"}}</span>
{{/if}}
</div>
{{!-- Total --}}
<div class="roll-total flexrow"> <div class="roll-total flexrow">
<div class="flexcol"> <div class="flexcol">
<h4>{{localize "VERMINE.success_count"}}:</h4> <h4>{{localize "VERMINE.success_count"}}:</h4>
@@ -56,7 +212,7 @@
</div> </div>
<div class="flexcol"> <div class="flexcol">
<h4>{{localize "VERMINE.success_required"}}:</h4> <h4>{{localize "VERMINE.success_required"}}:</h4>
<span id="required">{{math_add 1 param.handicap}}</span> <span id="required">{{verdict.required}}</span>
</div> </div>
</div> </div>
</div> </div>