Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf905cdcfb | ||
|
|
ea3322e843 | ||
|
|
6247f65590 | ||
|
|
8d8428cad3 | ||
|
|
9cf7d9ac68 | ||
|
|
f2c8cfe246 | ||
|
|
31f2f4530c | ||
|
|
9c7ebc6df9 | ||
|
|
05f668acf0 | ||
|
|
10b01614e6 |
@@ -36,6 +36,10 @@ jobs:
|
|||||||
- name: Build CSS
|
- name: Build CSS
|
||||||
run: npm install && npm run build
|
run: npm install && npm run build
|
||||||
|
|
||||||
|
# Recompile les compendiums LevelDB depuis les sources YAML (src/packs/ → packs/)
|
||||||
|
- name: Compile Compendiums
|
||||||
|
run: npm run pullYAMLtoLDB
|
||||||
|
|
||||||
# Crée le zip de release avec tous les fichiers nécessaires au système
|
# Crée le zip de release avec tous les fichiers nécessaires au système
|
||||||
- run: |
|
- run: |
|
||||||
apt update -y
|
apt update -y
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
@@ -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.*
|
||||||
+229
-4
@@ -16,7 +16,7 @@
|
|||||||
"TYPES.Item.item": "Item",
|
"TYPES.Item.item": "Item",
|
||||||
"TYPES.Item.weapon": "Weapon",
|
"TYPES.Item.weapon": "Weapon",
|
||||||
"TYPES.Item.defense": "Defense",
|
"TYPES.Item.defense": "Defense",
|
||||||
"TYPES.Item.vehicle": "Vehicle",
|
"TYPES.Actor.vehicle": "Vehicle",
|
||||||
"TYPES.Item.ability": "Ability",
|
"TYPES.Item.ability": "Ability",
|
||||||
"TYPES.Item.specialty": "Specialty",
|
"TYPES.Item.specialty": "Specialty",
|
||||||
"TYPES.Item.background": "Background",
|
"TYPES.Item.background": "Background",
|
||||||
@@ -145,8 +145,12 @@
|
|||||||
},
|
},
|
||||||
"pool": "Reserve",
|
"pool": "Reserve",
|
||||||
"pools": "Reserves",
|
"pools": "Reserves",
|
||||||
|
"dice_pool": "Dice pool",
|
||||||
|
"auto_successes": "automatic successes",
|
||||||
"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 +176,18 @@
|
|||||||
"range": "Range",
|
"range": "Range",
|
||||||
"ranges": "Ranges",
|
"ranges": "Ranges",
|
||||||
"mobility": "Mobility",
|
"mobility": "Mobility",
|
||||||
|
"vehicle": "Vehicle",
|
||||||
|
"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",
|
||||||
|
"owner": "Owner",
|
||||||
|
"no_vehicles": "No vehicles",
|
||||||
"rarity": "Rarity",
|
"rarity": "Rarity",
|
||||||
"rarity_sm": "Rar.",
|
"rarity_sm": "Rar.",
|
||||||
"reliability": "Reliability",
|
"reliability": "Reliability",
|
||||||
@@ -202,6 +218,8 @@
|
|||||||
"type_character": "Character",
|
"type_character": "Character",
|
||||||
"type_npc": "NPC",
|
"type_npc": "NPC",
|
||||||
"type_creature": "Creature",
|
"type_creature": "Creature",
|
||||||
|
"type_community": "Community",
|
||||||
|
"type_vehicle": "Vehicle",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"group_members": "Members",
|
"group_members": "Members",
|
||||||
"encounters": "Encounters",
|
"encounters": "Encounters",
|
||||||
@@ -223,6 +241,7 @@
|
|||||||
"world": "World"
|
"world": "World"
|
||||||
},
|
},
|
||||||
"CharacterNamePlaceholder": "Character Name",
|
"CharacterNamePlaceholder": "Character Name",
|
||||||
|
"ProfilePlaceholder": "Profile",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"bonuses": "Bonuses",
|
"bonuses": "Bonuses",
|
||||||
"choose_ability": "Choose Ability",
|
"choose_ability": "Choose Ability",
|
||||||
@@ -240,6 +259,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",
|
||||||
@@ -257,12 +299,109 @@
|
|||||||
"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",
|
||||||
|
"vehicle": "Vehicle"
|
||||||
},
|
},
|
||||||
"base_values": "Base Values",
|
"base_values": "Base Values",
|
||||||
"computed_values": "Computed Values",
|
"computed_values": "Computed Values",
|
||||||
@@ -326,6 +465,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",
|
||||||
@@ -573,7 +797,7 @@
|
|||||||
"name": "Repulsion"
|
"name": "Repulsion"
|
||||||
},
|
},
|
||||||
"tracks": {
|
"tracks": {
|
||||||
"name": "Tracks"
|
"name": "Tracking"
|
||||||
},
|
},
|
||||||
"crafting": {
|
"crafting": {
|
||||||
"name": "Crafting"
|
"name": "Crafting"
|
||||||
@@ -603,7 +827,7 @@
|
|||||||
"name": "Melee"
|
"name": "Melee"
|
||||||
},
|
},
|
||||||
"archery": {
|
"archery": {
|
||||||
"name": "Ranged"
|
"name": "Ranged Weapons"
|
||||||
},
|
},
|
||||||
"throwing": {
|
"throwing": {
|
||||||
"name": "Throwing"
|
"name": "Throwing"
|
||||||
@@ -655,6 +879,7 @@
|
|||||||
"Round": "Round"
|
"Round": "Round"
|
||||||
},
|
},
|
||||||
"SIZE_LEVELS": {
|
"SIZE_LEVELS": {
|
||||||
|
"tiny": "Insect",
|
||||||
"small": "Small",
|
"small": "Small",
|
||||||
"medium": "Medium",
|
"medium": "Medium",
|
||||||
"large": "Large"
|
"large": "Large"
|
||||||
|
|||||||
+225
-6
@@ -8,7 +8,7 @@
|
|||||||
"TYPES.Item.item": "Objet",
|
"TYPES.Item.item": "Objet",
|
||||||
"TYPES.Item.weapon": "Arme",
|
"TYPES.Item.weapon": "Arme",
|
||||||
"TYPES.Item.defense": "Protection",
|
"TYPES.Item.defense": "Protection",
|
||||||
"TYPES.Item.vehicle": "Véhicule",
|
"TYPES.Actor.vehicle": "Véhicule",
|
||||||
"TYPES.Item.ability": "Capacité",
|
"TYPES.Item.ability": "Capacité",
|
||||||
"TYPES.Item.specialty": "Spécialité",
|
"TYPES.Item.specialty": "Spécialité",
|
||||||
"TYPES.Item.background": "Historique",
|
"TYPES.Item.background": "Historique",
|
||||||
@@ -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",
|
||||||
@@ -152,8 +152,12 @@
|
|||||||
"pool": "Réserve",
|
"pool": "Réserve",
|
||||||
"pools": "Réserves",
|
"pools": "Réserves",
|
||||||
"reserves": "Réserves",
|
"reserves": "Réserves",
|
||||||
|
"dice_pool": "Pool de dés",
|
||||||
|
"auto_successes": "réussites automatiques",
|
||||||
"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 +192,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 +257,18 @@
|
|||||||
"range": "Portée",
|
"range": "Portée",
|
||||||
"ranges": "Portées",
|
"ranges": "Portées",
|
||||||
"mobility": "Mobilité",
|
"mobility": "Mobilité",
|
||||||
|
"vehicle": "Véhicule",
|
||||||
|
"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",
|
||||||
|
"owner": "Propriétaire",
|
||||||
|
"no_vehicles": "Aucun véhicule",
|
||||||
"rarity": "Rareté",
|
"rarity": "Rareté",
|
||||||
"rarity_sm": "Rar.",
|
"rarity_sm": "Rar.",
|
||||||
"reliability": "Fiabilité",
|
"reliability": "Fiabilité",
|
||||||
@@ -244,6 +283,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 +386,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 +409,8 @@
|
|||||||
"type_character": "Personnage",
|
"type_character": "Personnage",
|
||||||
"type_npc": "PNJ",
|
"type_npc": "PNJ",
|
||||||
"type_creature": "Créature",
|
"type_creature": "Créature",
|
||||||
|
"type_community": "Communauté",
|
||||||
|
"type_vehicle": "Véhicule",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"group_members": "Membres",
|
"group_members": "Membres",
|
||||||
"encounters": "Rencontres",
|
"encounters": "Rencontres",
|
||||||
@@ -314,6 +444,7 @@
|
|||||||
"total_damage": "Dégâts totaux",
|
"total_damage": "Dégâts totaux",
|
||||||
"wound_thresholds": "Seuils de blessures",
|
"wound_thresholds": "Seuils de blessures",
|
||||||
"CharacterNamePlaceholder": "Nom du personnage",
|
"CharacterNamePlaceholder": "Nom du personnage",
|
||||||
|
"ProfilePlaceholder": "Profil",
|
||||||
"sexes": {
|
"sexes": {
|
||||||
"female": "Féminin",
|
"female": "Féminin",
|
||||||
"male": "Masculin"
|
"male": "Masculin"
|
||||||
@@ -325,7 +456,9 @@
|
|||||||
"character": "Personnage",
|
"character": "Personnage",
|
||||||
"npc": "PNJ",
|
"npc": "PNJ",
|
||||||
"group": "Groupe",
|
"group": "Groupe",
|
||||||
"creature": "Créature"
|
"creature": "Créature",
|
||||||
|
"community": "Communauté",
|
||||||
|
"vehicle": "Véhicule"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"UI": {
|
"UI": {
|
||||||
@@ -343,6 +476,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",
|
||||||
@@ -432,6 +650,7 @@
|
|||||||
"major": "Majeur"
|
"major": "Majeur"
|
||||||
},
|
},
|
||||||
"SIZE_LEVELS": {
|
"SIZE_LEVELS": {
|
||||||
|
"tiny": "Insecte",
|
||||||
"small": "Petit",
|
"small": "Petit",
|
||||||
"medium": "Moyen",
|
"medium": "Moyen",
|
||||||
"large": "Grand"
|
"large": "Grand"
|
||||||
@@ -595,7 +814,7 @@
|
|||||||
"name": "Répulsion"
|
"name": "Répulsion"
|
||||||
},
|
},
|
||||||
"tracks": {
|
"tracks": {
|
||||||
"name": "Pistage"
|
"name": "Traces"
|
||||||
},
|
},
|
||||||
"crafting": {
|
"crafting": {
|
||||||
"name": "Artisanat"
|
"name": "Artisanat"
|
||||||
@@ -616,7 +835,7 @@
|
|||||||
"name": "Armes à feu"
|
"name": "Armes à feu"
|
||||||
},
|
},
|
||||||
"archery": {
|
"archery": {
|
||||||
"name": "Tir à l'arc"
|
"name": "Armes de Tir"
|
||||||
},
|
},
|
||||||
"armory": {
|
"armory": {
|
||||||
"name": "Armurerie"
|
"name": "Armurerie"
|
||||||
|
|||||||
+29
-7
@@ -106,6 +106,10 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> nav.tabs {
|
> nav.tabs {
|
||||||
@@ -331,21 +335,39 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.padding-with-frieze {
|
.padding-with-frieze {
|
||||||
margin-left: 18% !important;
|
padding: 0.25rem;
|
||||||
margin-right: 10% !important;
|
|
||||||
|
|
||||||
li {
|
li {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resource-label {
|
||||||
|
h4 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.major-totem {
|
.major-totem {
|
||||||
position: relative;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
padding: 0.6rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.12);
|
||||||
|
border: 2px groove #eeede0;
|
||||||
|
|
||||||
|
.flexcol {
|
||||||
|
gap: 0.3rem;
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
position: absolute;
|
margin: 0;
|
||||||
transform: rotate(-8deg);
|
font-size: 1.1em;
|
||||||
opacity: 1;
|
}
|
||||||
transition: 0.2s;
|
|
||||||
|
.img-totem {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
filter: drop-shadow(0px 0px 10px rgb(0, 0, 0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
-89
@@ -30,77 +30,70 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.skills-container {
|
.npc-trait-list {
|
||||||
display: flex;
|
li {
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-category {
|
|
||||||
.card-style();
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
font-family: "DistressBlack", sans-serif;
|
|
||||||
font-size: @font-size-14;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-item {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
padding: 5px;
|
padding: 4px 0;
|
||||||
border-bottom: 1px solid @color-border-dark-3;
|
border-bottom: 1px solid @color-border-dark-3;
|
||||||
|
font-size: @font-size-12;
|
||||||
|
|
||||||
&:last-child { border-bottom: none; }
|
&:last-child { border-bottom: none; }
|
||||||
|
|
||||||
label {
|
strong { flex: 1; }
|
||||||
flex: 1;
|
|
||||||
font-size: @font-size-12;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
.trait-value {
|
||||||
box-shadow: 0 0 10px @theme-color-highlight inset;
|
min-width: 30px;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-control {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-value {
|
|
||||||
min-width: 25px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rarity-badge {
|
.npc-roll-btn {
|
||||||
display: inline-block;
|
color: @color-text-light-primary;
|
||||||
padding: 1px 4px;
|
opacity: 0.7;
|
||||||
border-radius: 3px;
|
transition: opacity 0.15s ease;
|
||||||
font-size: 10px;
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: @color-text-light-1;
|
||||||
|
text-shadow: 0 0 6px @theme-color-highlight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.npc-wounds {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.wound-cat {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-left: 5px;
|
|
||||||
|
|
||||||
&.rarity-0 { .rarity-badge(@rarity-0-bg, @rarity-0-text); }
|
|
||||||
&.rarity-1 { .rarity-badge(@rarity-1-bg, @rarity-1-text); }
|
|
||||||
&.rarity-2 { .rarity-badge(@rarity-2-bg, @rarity-2-text); }
|
|
||||||
&.rarity-3 { .rarity-badge(@rarity-3-bg, @rarity-3-text); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.skill-category-selector {
|
.wound-boxes {
|
||||||
margin-bottom: 15px;
|
display: inline-flex;
|
||||||
.card-style();
|
gap: 2px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 8px;
|
||||||
|
color: @color-border-dark-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.npc-free-skills {
|
||||||
|
margin-top: 15px;
|
||||||
|
.form-group-style();
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: @font-size-12;
|
||||||
|
font-weight: bold;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wounds-card {
|
.wounds-card {
|
||||||
@@ -133,39 +126,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ability-section {
|
|
||||||
.card-style();
|
|
||||||
}
|
|
||||||
|
|
||||||
.ability-category-header {
|
|
||||||
font-family: "DistressBlack", sans-serif;
|
|
||||||
font-size: @font-size-14;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
border-bottom: 1px solid @color-border-dark-3;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ability-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 5px 0;
|
|
||||||
|
|
||||||
label {
|
|
||||||
flex: 1;
|
|
||||||
font-size: @font-size-12;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ability-control {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -213,6 +173,17 @@
|
|||||||
padding: 3px 5px;
|
padding: 3px 5px;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
font-size: @font-size-12;
|
||||||
|
padding: 3px 5px;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.npc-profile-grid {
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor {
|
.editor {
|
||||||
|
|||||||
+29
-35
@@ -19,26 +19,39 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
div.minor-totems {
|
div.minor-totems {
|
||||||
position: relative;
|
display: flex;
|
||||||
height: 8.5rem;
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.25rem;
|
||||||
|
min-height: 8.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
background-color: rgba(146, 156, 111, 0.5215686275);
|
background-color: rgba(146, 156, 111, 0.5215686275);
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
position: relative;
|
text-align: center;
|
||||||
z-index: 1;
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.totem-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
h5 {
|
h5 {
|
||||||
position: absolute;
|
|
||||||
top: 2.6rem;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
z-index: 1;
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: 2rem;
|
max-width: 1.5rem;
|
||||||
position: absolute;
|
height: auto;
|
||||||
top: 1.4rem;
|
|
||||||
bottom: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.human, &.adapted {
|
&.human, &.adapted {
|
||||||
@@ -62,50 +75,31 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.totem-dice {
|
.totem-dice {
|
||||||
position: absolute;
|
display: flex;
|
||||||
left: 0;
|
justify-content: space-between;
|
||||||
right: 0;
|
align-items: center;
|
||||||
bottom: 0;
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
.human-dice,
|
.human-dice,
|
||||||
.adapted-dice {
|
.adapted-dice {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
margin-left: 2rem;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.human-dice i,
|
.human-dice i,
|
||||||
.adapted-dice i {
|
.adapted-dice i {
|
||||||
padding-top: 0.5rem;
|
|
||||||
color: @totem-human-color;
|
color: @totem-human-color;
|
||||||
position: relative;
|
|
||||||
z-index: 0;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.adapted-dice {
|
.adapted-dice {
|
||||||
justify-content: flex-end;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 2rem;
|
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
|
||||||
|
|
||||||
.adapted-dice i {
|
i {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
padding-top: 0.5rem;
|
|
||||||
color: @totem-adapted-color;
|
color: @totem-adapted-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.human {
|
|
||||||
left: 0;
|
|
||||||
img { left: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.adapted {
|
|
||||||
right: 0;
|
|
||||||
img { right: 0; left: auto; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
@import "../variables";
|
||||||
|
@import "../utilities";
|
||||||
|
|
||||||
|
.system-vermine2047 .vermine2047.actor.vehicle {
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-columns: 1fr !important;
|
||||||
|
grid-template-rows: auto 1fr auto !important;
|
||||||
|
|
||||||
|
> header.window-header {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
> section.window-content {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div.window-resize-handle {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto 1fr;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
> .tab.main {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
> nav.tabs {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .tab:not(.main) {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 3;
|
||||||
|
display: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet-header {
|
||||||
|
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
|
||||||
|
padding: 10px;
|
||||||
|
max-height: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-fields { flex: 1; }
|
||||||
|
|
||||||
|
.resource {
|
||||||
|
.card-style();
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-label {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 8px;
|
||||||
|
min-width: 60px;
|
||||||
|
font-size: @font-size-12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
select {
|
||||||
|
margin-right: 8px;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"] {
|
||||||
|
width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.charname {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
font-size: @font-size-18;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-owner {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-section {
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid @color-border-dark-4;
|
||||||
|
background: fade(@color-chitin-dark, 20%);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-label {
|
||||||
|
display: block;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
font-size: @font-size-11;
|
||||||
|
color: @color-text-light-0;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maintenance {
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.flexrow label {
|
||||||
|
gap: 4px;
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: @font-size-12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
label {
|
||||||
|
font-family: "DistressBlack", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
color: @color-amber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
+461
-3
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
@import "actor/npc";
|
@import "actor/npc";
|
||||||
@import "actor/group";
|
@import "actor/group";
|
||||||
@import "actor/creature";
|
@import "actor/creature";
|
||||||
|
@import "actor/community";
|
||||||
|
@import "actor/vehicle";
|
||||||
|
|
||||||
// 6. Styles des items
|
// 6. Styles des items
|
||||||
@import "items";
|
@import "items";
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ 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 { default as VermineVehicleSheetV2 } from "./vehicle-sheet.mjs"
|
||||||
export {
|
export {
|
||||||
VermineItemSheetV2,
|
VermineItemSheetV2,
|
||||||
VermineWeaponSheetV2,
|
VermineWeaponSheetV2,
|
||||||
VermineDefenseSheetV2,
|
VermineDefenseSheetV2,
|
||||||
VermineVehicleSheetV2,
|
|
||||||
VermineAbilitySheetV2,
|
VermineAbilitySheetV2,
|
||||||
VermineSpecialtySheetV2,
|
VermineSpecialtySheetV2,
|
||||||
VermineBackgroundSheetV2,
|
VermineBackgroundSheetV2,
|
||||||
|
|||||||
@@ -30,8 +30,13 @@ 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,
|
||||||
|
createVehicle: VermineBaseActorSheet.#onCreateVehicle,
|
||||||
|
editVehicle: VermineBaseActorSheet.#onEditVehicle,
|
||||||
|
deleteVehicle: VermineBaseActorSheet.#onDeleteVehicle,
|
||||||
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
|
||||||
@@ -114,7 +119,8 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
|
|||||||
isEditable: this.isEditable,
|
isEditable: this.isEditable,
|
||||||
enrichedNotes: await enrich("identity.notes"),
|
enrichedNotes: await enrich("identity.notes"),
|
||||||
enrichedBiography: await enrich("identity.biography"),
|
enrichedBiography: await enrich("identity.biography"),
|
||||||
enrichedRelations: await enrich("identity.relations")
|
enrichedRelations: await enrich("identity.relations"),
|
||||||
|
enrichedEquipmentNotes: await enrich("equipment.description")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,17 +232,53 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
|
|||||||
await this.document.createEmbeddedDocuments("Item", [{ name, type }])
|
await this.document.createEmbeddedDocuments("Item", [{ name, type }])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Véhicules (acteurs) ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Crée un acteur "vehicle" rattaché à cet acteur (groupe ou personnage). */
|
||||||
|
static async #onCreateVehicle(event, target) {
|
||||||
|
const name = game.i18n.localize("ITEMS.new_vehicle")
|
||||||
|
const vehicle = await Actor.create({
|
||||||
|
name,
|
||||||
|
type: "vehicle",
|
||||||
|
system: { ownerId: this.document.id },
|
||||||
|
// Le véhicule est un bien partagé : reprendre la propriété du propriétaire
|
||||||
|
ownership: foundry.utils.deepClone(this.document.ownership)
|
||||||
|
})
|
||||||
|
vehicle?.sheet.render(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onEditVehicle(event, target) {
|
||||||
|
const id = target.closest("[data-vehicle-id]")?.dataset?.vehicleId
|
||||||
|
const vehicle = game.actors.get(id)
|
||||||
|
vehicle?.sheet.render(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onDeleteVehicle(event, target) {
|
||||||
|
const id = target.closest("[data-vehicle-id]")?.dataset?.vehicleId
|
||||||
|
const vehicle = game.actors.get(id)
|
||||||
|
await vehicle?.deleteDialog()
|
||||||
|
}
|
||||||
|
|
||||||
static async #onRollItem(event, target) {
|
static async #onRollItem(event, target) {
|
||||||
const id = target.closest("[data-item-id]")?.dataset?.itemId
|
const id = target.closest("[data-item-id]")?.dataset?.itemId
|
||||||
if (!id) return
|
if (!id) return
|
||||||
const item = this.document.items.get(id)
|
const item = this.document.items.get(id)
|
||||||
if (!item) return
|
if (!item) return
|
||||||
|
// Armes : PNJ → dialogue de combat (valeur d'Attaque) ; personnage →
|
||||||
|
// dialogue de compétence. Les PNJ n'ont ni caractéristiques ni compétences.
|
||||||
|
if (item.type === "weapon" && this.document.type === 'npc') {
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (item.type === "weapon" && this.document.system.abilities && this.document.system.skills) {
|
if (item.type === "weapon" && this.document.system.abilities && this.document.system.skills) {
|
||||||
const { default: RollDialog } = await import("../../system/dialogs/rollDialog.mjs")
|
const { default: RollDialog } = await import("../../system/dialogs/rollDialog.mjs")
|
||||||
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
|
||||||
@@ -244,6 +286,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()
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -66,16 +71,28 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
|||||||
context.gear = doc.itemTypes.item
|
context.gear = doc.itemTypes.item
|
||||||
context.weapons = doc.itemTypes.weapon
|
context.weapons = doc.itemTypes.weapon
|
||||||
context.defenses = doc.itemTypes.defense
|
context.defenses = doc.itemTypes.defense
|
||||||
context.vehicles = doc.itemTypes.vehicle
|
context.vehicles = game.actors.filter(a => a.type === "vehicle" && a.system.ownerId === doc.id)
|
||||||
break
|
break
|
||||||
case "stories":
|
case "stories":
|
||||||
context.tab = context.tabs.stories
|
context.tab = context.tabs.stories
|
||||||
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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -78,10 +80,11 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
|||||||
context.gear = doc.itemTypes.item
|
context.gear = doc.itemTypes.item
|
||||||
context.weapons = doc.itemTypes.weapon
|
context.weapons = doc.itemTypes.weapon
|
||||||
context.defenses = doc.itemTypes.defense
|
context.defenses = doc.itemTypes.defense
|
||||||
|
context.vehicles = this.#getOwnedVehicles()
|
||||||
break
|
break
|
||||||
case "road":
|
case "road":
|
||||||
context.tab = context.tabs.road
|
context.tab = context.tabs.road
|
||||||
context.vehicles = doc.itemTypes.vehicle
|
context.vehicles = this.#getOwnedVehicles()
|
||||||
break
|
break
|
||||||
case "reserve":
|
case "reserve":
|
||||||
context.tab = context.tabs.reserve
|
context.tab = context.tabs.reserve
|
||||||
@@ -90,6 +93,11 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
|||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Véhicules (acteurs) appartenant à ce groupe via system.ownerId. */
|
||||||
|
#getOwnedVehicles() {
|
||||||
|
return game.actors.filter(a => a.type === "vehicle" && a.system.ownerId === this.document.id)
|
||||||
|
}
|
||||||
|
|
||||||
// Actions : délégation aux applications AppV1 existantes pour TotemPicker/ActorPicker
|
// Actions : délégation aux applications AppV1 existantes pour TotemPicker/ActorPicker
|
||||||
static async #onChooseTotem(event, target) {
|
static async #onChooseTotem(event, target) {
|
||||||
const { TotemPicker } = await import("../../system/applications.mjs")
|
const { TotemPicker } = await import("../../system/applications.mjs")
|
||||||
@@ -135,4 +143,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" } }])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,71 +3,65 @@ 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 ──────────────────────────────────────────────────────────
|
|
||||||
export class VermineVehicleSheetV2 extends VermineBaseItemSheet {
|
|
||||||
static DEFAULT_OPTIONS = { classes: ["vehicle"], position: { width: 520 } }
|
|
||||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-vehicle-sheet.hbs" } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 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: [""] } }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ 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 : profil et niveaux (Menace,
|
||||||
|
// Expérience, Rôle) 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,22 +14,20 @@ 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" },
|
traits: { template: "systems/vermine2047/templates/actor/appv2/npc-traits.hbs", scrollable: [""] },
|
||||||
skills: { template: "systems/vermine2047/templates/actor/appv2/npc-skills.hbs" },
|
equipment: { template: "systems/vermine2047/templates/actor/appv2/npc-equipment.hbs", scrollable: [""] },
|
||||||
threat: { template: "systems/vermine2047/templates/actor/appv2/npc-threat.hbs" },
|
combat: { template: "systems/vermine2047/templates/actor/appv2/npc-combat.hbs", scrollable: [""] },
|
||||||
combat: { template: "systems/vermine2047/templates/actor/appv2/npc-combat.hbs" },
|
notes: { template: "systems/vermine2047/templates/actor/appv2/npc-notes.hbs", scrollable: [""] }
|
||||||
notes: { template: "systems/vermine2047/templates/actor/appv2/npc-notes.hbs" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tabGroups = { sheet: "characteristics" }
|
tabGroups = { sheet: "traits" }
|
||||||
|
|
||||||
#getTabs() {
|
#getTabs() {
|
||||||
const tabs = {
|
const tabs = {
|
||||||
characteristics: { id: "characteristics", group: "sheet", icon: "fas fa-dice", label: "VERMINE.abilities" },
|
traits: { id: "traits", group: "sheet", icon: "fas fa-dice", label: "VERMINE.traits" },
|
||||||
skills: { id: "skills", group: "sheet", icon: "fas fa-brain", label: "VERMINE.skills" },
|
equipment: { id: "equipment", group: "sheet", icon: "fas fa-hammer", label: "VERMINE.tabs.equipment" },
|
||||||
threat: { id: "threat", group: "sheet", icon: "fas fa-exclamation-triangle", label: "ADVERSITY.threat" },
|
|
||||||
combat: { id: "combat", group: "sheet", icon: "fas fa-sword", label: "VERMINE.combat" },
|
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" }
|
||||||
}
|
}
|
||||||
@@ -51,17 +54,18 @@ export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
|
|||||||
context.totemOptions = CONFIG.VERMINE.totems
|
context.totemOptions = CONFIG.VERMINE.totems
|
||||||
context.originOptions = CONFIG.VERMINE.origins
|
context.originOptions = CONFIG.VERMINE.origins
|
||||||
break
|
break
|
||||||
case "characteristics":
|
case "traits":
|
||||||
context.tab = context.tabs.characteristics
|
context.tab = context.tabs.traits
|
||||||
break
|
break
|
||||||
case "skills":
|
case "equipment":
|
||||||
context.tab = context.tabs.skills
|
context.tab = context.tabs.equipment
|
||||||
break
|
context.weapons = doc.itemTypes.weapon
|
||||||
case "threat":
|
context.defenses = doc.itemTypes.defense
|
||||||
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
|
||||||
@@ -71,4 +75,27 @@ export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
|
|||||||
}
|
}
|
||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @override - Le PNJ n'a ni caractéristiques ni compétences : les jets
|
||||||
|
* rapides utilisent la valeur d'Attaque (dialogue de combat) ou les valeurs
|
||||||
|
* dérivées Action / Spécialité / Réaction (dialogue PNJ simplifié). */
|
||||||
|
async _onRoll(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
const el = event.currentTarget
|
||||||
|
const type = el.dataset.type
|
||||||
|
if (!type) return
|
||||||
|
if (type === "attack") {
|
||||||
|
const { default: CombatDialog } = await import("../../system/dialogs/combatDialog.mjs")
|
||||||
|
const dialog = await CombatDialog.create({ actorId: this.document.id })
|
||||||
|
if (dialog) dialog.render(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (["action", "specialty", "reaction"].includes(type)) {
|
||||||
|
const { default: NpcRollDialog } = await import("../../system/dialogs/npcRollDialog.mjs")
|
||||||
|
const dialog = await NpcRollDialog.create({ actorId: this.document.id, rolltype: type })
|
||||||
|
if (dialog) dialog.render(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return super._onRoll(event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import VermineBaseActorSheet from "./base-actor-sheet.mjs"
|
||||||
|
|
||||||
|
export default class VermineVehicleSheetV2 extends VermineBaseActorSheet {
|
||||||
|
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
classes: ["vehicle"],
|
||||||
|
position: { width: 640, height: 580 },
|
||||||
|
window: { contentClasses: ["vehicle-content"] }
|
||||||
|
}
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
main: { template: "systems/vermine2047/templates/actor/appv2/vehicle-main.hbs", scrollable: [""] },
|
||||||
|
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||||
|
specs: { template: "systems/vermine2047/templates/actor/appv2/vehicle-specs.hbs", scrollable: [""] }
|
||||||
|
}
|
||||||
|
|
||||||
|
tabGroups = { sheet: "specs" }
|
||||||
|
|
||||||
|
#getTabs() {
|
||||||
|
const tabs = {
|
||||||
|
specs: { id: "specs", group: "sheet", icon: "fas fa-car-side", label: "VERMINE.vehicle_specs" }
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
// 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)
|
||||||
|
}))
|
||||||
|
// Propriétaire (groupe ou personnage) résolu
|
||||||
|
context.resolvedOwner = null
|
||||||
|
if (this.document.system.ownerId) {
|
||||||
|
const owner = game.actors.get(this.document.system.ownerId)
|
||||||
|
if (owner) context.resolvedOwner = { id: owner.id, name: owner.name }
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
async _preparePartContext(partId, context) {
|
||||||
|
switch (partId) {
|
||||||
|
case "main": break
|
||||||
|
case "specs":
|
||||||
|
context.tab = context.tabs.specs
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
@@ -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: "" }))
|
||||||
}
|
}
|
||||||
|
|||||||
+75
-62
@@ -2,17 +2,22 @@
|
|||||||
* DataModel pour les acteurs de type "npc" (PNJ).
|
* DataModel pour les acteurs de type "npc" (PNJ).
|
||||||
* Étend foundry.abstract.TypeDataModel.
|
* Étend foundry.abstract.TypeDataModel.
|
||||||
*
|
*
|
||||||
* Note : le champ libre de compétences (texte descriptif) est nommé "freeSkills"
|
* Conforme aux règles des PNJ (regles/regles_pnj.txt) : un PNJ n'a ni
|
||||||
* pour éviter le conflit avec le SchemaField "skills" qui contient les 30 compétences.
|
* caractéristiques chiffrées, ni réserves détaillées, ni liste de compétences.
|
||||||
|
* Il est défini uniquement par son Profil (identité) et trois critères :
|
||||||
|
* Menace, Expérience et Rôle. Toutes les valeurs utiles en jeu sont dérivées
|
||||||
|
* des tableaux de référence (CONFIG.VERMINE.npc*Levels) et exposées dans
|
||||||
|
* "system.computed".
|
||||||
|
*
|
||||||
|
* Note : le champ libre de description des Spécialités est nommé "freeSkills"
|
||||||
|
* pour éviter le conflit avec le champ "skills" que le template.json historique
|
||||||
|
* définissait comme texte libre pour les PNJ.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
woundSchema,
|
woundSchema,
|
||||||
combatStatusSchema,
|
combatStatusSchema,
|
||||||
equipmentSchema,
|
equipmentSchema,
|
||||||
attributeSchema,
|
attributeSchema
|
||||||
abilitiesSchema,
|
|
||||||
skillCategoriesSchema,
|
|
||||||
skillsSchema
|
|
||||||
} from "./_shared.mjs"
|
} from "./_shared.mjs"
|
||||||
|
|
||||||
export default class VermineNpcData extends foundry.abstract.TypeDataModel {
|
export default class VermineNpcData extends foundry.abstract.TypeDataModel {
|
||||||
@@ -23,8 +28,7 @@ export default class VermineNpcData extends foundry.abstract.TypeDataModel {
|
|||||||
/**
|
/**
|
||||||
* Migration des données avant traitement par le schéma.
|
* Migration des données avant traitement par le schéma.
|
||||||
* Avant DataModel, template.json définissait "skills" comme un champ texte libre
|
* Avant DataModel, template.json définissait "skills" comme un champ texte libre
|
||||||
* pour les PNJ. Le DataModel réserve "skills" pour les 30 compétences individuelles
|
* pour les PNJ. Le DataModel utilise "freeSkills" pour cette description.
|
||||||
* (SchemaField) et utilise "freeSkills" pour le texte libre.
|
|
||||||
* @param {Object} source Données brutes avant validation du schéma
|
* @param {Object} source Données brutes avant validation du schéma
|
||||||
* @returns {Object} Données migrées
|
* @returns {Object} Données migrées
|
||||||
*/
|
*/
|
||||||
@@ -48,9 +52,10 @@ export default class VermineNpcData extends foundry.abstract.TypeDataModel {
|
|||||||
// Statut de combat (base, difficulté par défaut 9 pour PNJ)
|
// Statut de combat (base, difficulté par défaut 9 pour PNJ)
|
||||||
combatStatus: combatStatusSchema("9"),
|
combatStatus: combatStatusSchema("9"),
|
||||||
|
|
||||||
// Identité
|
// Identité (Profil du PNJ)
|
||||||
identity: new fields.SchemaField({
|
identity: new fields.SchemaField({
|
||||||
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
age: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
profile: 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: "" }),
|
origin: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
@@ -58,30 +63,29 @@ export default class VermineNpcData extends foundry.abstract.TypeDataModel {
|
|||||||
notes: new fields.HTMLField({ required: true, initial: "" })
|
notes: new fields.HTMLField({ required: true, initial: "" })
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Attributs (XP, réputation, sang-froid, effort)
|
// Critères de définition du PNJ (Menace, Expérience, Rôle)
|
||||||
attributes: new fields.SchemaField({
|
|
||||||
xp: attributeSchema(0, 0, 10),
|
|
||||||
reputation: attributeSchema(0, 0, 10),
|
|
||||||
self_control: attributeSchema(0, 0, 5),
|
|
||||||
effort: attributeSchema(0, 0, 5)
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Niveaux PNJ (menace, expérience, rôle)
|
|
||||||
threat: attributeSchema(1, 1, 4),
|
threat: attributeSchema(1, 1, 4),
|
||||||
experience: attributeSchema(1, 1, 4),
|
experience: attributeSchema(1, 1, 4),
|
||||||
role: attributeSchema(1, 1, 4),
|
role: attributeSchema(1, 1, 4),
|
||||||
|
|
||||||
// Compétences (les 30 compétences individuelles)
|
// Description libre des Spécialités (champ texte PNJ)
|
||||||
skills: skillsSchema(),
|
|
||||||
|
|
||||||
// Description libre des compétences (champ texte PNJ)
|
|
||||||
freeSkills: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
freeSkills: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
|
||||||
// Catégories de compétences
|
// Valeurs calculées (dérivées des niveaux de menace / expérience / rôle)
|
||||||
skill_categories: skillCategoriesSchema(),
|
computed: new fields.SchemaField({
|
||||||
|
attack: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
|
||||||
// Caractéristiques (8)
|
vigor: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
|
||||||
abilities: abilitiesSchema(),
|
action: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 3 }),
|
||||||
|
specialties: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 4 }),
|
||||||
|
rerolls: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
|
||||||
|
contact: new fields.StringField({ required: true, nullable: false, initial: "7" }),
|
||||||
|
reaction: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 3 }),
|
||||||
|
reactionBonus: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
|
||||||
|
pools: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
|
||||||
|
gear: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 9 }),
|
||||||
|
gearHindrance: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
|
||||||
|
protection: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1 })
|
||||||
|
}),
|
||||||
|
|
||||||
// Équipement
|
// Équipement
|
||||||
equipment: equipmentSchema()
|
equipment: equipmentSchema()
|
||||||
@@ -92,58 +96,67 @@ export default class VermineNpcData extends foundry.abstract.TypeDataModel {
|
|||||||
prepareDerivedData() {
|
prepareDerivedData() {
|
||||||
super.prepareDerivedData()
|
super.prepareDerivedData()
|
||||||
|
|
||||||
// 1. Calculer les seuils de blessures selon le niveau de menace
|
// 1. Calculer les valeurs dérivées selon les niveaux de menace/expérience/rôle
|
||||||
|
this._setNpcComputedValues()
|
||||||
|
|
||||||
|
// 2. Calculer les seuils de blessures selon le niveau de menace
|
||||||
this._setNpcWoundThresholds()
|
this._setNpcWoundThresholds()
|
||||||
|
|
||||||
// 2. Calculer les réserves selon le niveau de rôle
|
// 3. Mettre à jour le statut de combat
|
||||||
this._setNpcAttributes()
|
|
||||||
|
|
||||||
// 3. Définir les libellés des caractéristiques
|
|
||||||
this._setAbilityLabels()
|
|
||||||
|
|
||||||
// 4. Mettre à jour le statut de combat
|
|
||||||
this._updateCombatStatus()
|
this._updateCombatStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calcule les seuils de blessures à partir du niveau de menace.
|
* Calcule les valeurs dérivées à partir des niveaux de Menace, Expérience
|
||||||
* Utilise CONFIG.VERMINE.npcThreatLevels.
|
* et Rôle. Utilise CONFIG.VERMINE.npcThreatLevels, .npcExperienceLevels
|
||||||
|
* et .npcRoleLevels.
|
||||||
|
*/
|
||||||
|
_setNpcComputedValues() {
|
||||||
|
const threatLevel = this.threat?.value || 1
|
||||||
|
const expLevel = this.experience?.value || 1
|
||||||
|
const roleLevel = this.role?.value || 1
|
||||||
|
|
||||||
|
const threatConfig = CONFIG.VERMINE.npcThreatLevels[threatLevel] || {}
|
||||||
|
const expConfig = CONFIG.VERMINE.npcExperienceLevels[expLevel] || {}
|
||||||
|
const roleConfig = CONFIG.VERMINE.npcRoleLevels[roleLevel] || {}
|
||||||
|
|
||||||
|
// Menace
|
||||||
|
this.computed.attack = threatConfig.attack || 0
|
||||||
|
this.computed.vigor = threatConfig.vigor || 0
|
||||||
|
|
||||||
|
// Expérience
|
||||||
|
this.computed.action = expConfig.action || 3
|
||||||
|
this.computed.specialties = expConfig.specialties || 4
|
||||||
|
this.computed.rerolls = expConfig.rerolls || 0
|
||||||
|
this.computed.contact = expConfig.contact || "7"
|
||||||
|
|
||||||
|
// Rôle
|
||||||
|
this.computed.reaction = roleConfig.reaction || 0
|
||||||
|
this.computed.reactionBonus = roleConfig.reaction_bonus || 0
|
||||||
|
this.computed.pools = roleConfig.pools || 0
|
||||||
|
this.computed.gear = roleConfig.gear || 9
|
||||||
|
this.computed.gearHindrance = roleConfig.gear_hindrance || 0
|
||||||
|
this.computed.protection = roleConfig.protection || 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule les seuils et le nombre de boîtes de blessures à partir du
|
||||||
|
* niveau de menace (tableau "Niveau de Menace"). Utilise
|
||||||
|
* CONFIG.VERMINE.npcThreatLevels.
|
||||||
*/
|
*/
|
||||||
_setNpcWoundThresholds() {
|
_setNpcWoundThresholds() {
|
||||||
const health = this.abilities?.health?.value || 1
|
|
||||||
const threatLevel = this.threat?.value || 1
|
const threatLevel = this.threat?.value || 1
|
||||||
const threatConfig = CONFIG.VERMINE.npcThreatLevels[threatLevel] || {}
|
const threatConfig = CONFIG.VERMINE.npcThreatLevels[threatLevel] || {}
|
||||||
|
|
||||||
this.minorWound.threshold = health
|
this.minorWound.threshold = threatConfig.minorThreshold ?? 1
|
||||||
this.majorWound.threshold = health + 3
|
this.majorWound.threshold = threatConfig.majorThreshold ?? 4
|
||||||
this.deadlyWound.threshold = Math.min(health + 7, 10)
|
this.deadlyWound.threshold = threatConfig.deadlyThreshold ?? 6
|
||||||
|
|
||||||
this.minorWound.max = threatConfig.minorWound || 1
|
this.minorWound.max = threatConfig.minorWound || 1
|
||||||
this.majorWound.max = threatConfig.majorWound || 1
|
this.majorWound.max = threatConfig.majorWound || 1
|
||||||
this.deadlyWound.max = threatConfig.deadlyWound || 1
|
this.deadlyWound.max = threatConfig.deadlyWound || 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Définit les attributs dérivés (effort, sang-froid) selon le niveau de rôle.
|
|
||||||
* Utilise CONFIG.VERMINE.npcRoleLevels.
|
|
||||||
*/
|
|
||||||
_setNpcAttributes() {
|
|
||||||
const roleLevel = this.role?.value || 1
|
|
||||||
const roleConfig = CONFIG.VERMINE.npcRoleLevels[roleLevel] || {}
|
|
||||||
|
|
||||||
this.attributes.effort.max = roleConfig.pools || 0
|
|
||||||
this.attributes.self_control.max = roleConfig.reaction_bonus || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Définit les libellés localisés des caractéristiques.
|
|
||||||
*/
|
|
||||||
_setAbilityLabels() {
|
|
||||||
for (const [k, v] of Object.entries(this.abilities)) {
|
|
||||||
v.label = game.i18n.localize(CONFIG.VERMINE.abilities[k]) ?? k
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Met à jour le label du statut de combat en fonction de la difficulté.
|
* Met à jour le label du statut de combat en fonction de la difficulté.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,67 @@
|
|||||||
import { baseItemSchema } from "./_shared.mjs"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DataModel pour les items de type "vehicle" (véhicules).
|
* DataModel pour les acteurs de type "vehicle" (véhicules et montures).
|
||||||
* @augments {foundry.abstract.TypeDataModel}
|
* Un véhicule est désormais un acteur (et non un objet) : il possède sa propre
|
||||||
|
* fiche, peut être posé sur la scène comme un jeton et appartient à un groupe
|
||||||
|
* ou un personnage via `ownerId`.
|
||||||
|
*
|
||||||
|
* Caractéristiques (regles_vehicules.txt) : Rareté, Fiabilité (véhicules),
|
||||||
|
* Réserve d'Effort (montures), Distance, Vitesse (croisière/pointe) et Entretien.
|
||||||
|
* Pas de statistiques de combat (les règles ne définissent pas de points de vie).
|
||||||
*/
|
*/
|
||||||
|
import { raritySchema } from "./_shared.mjs"
|
||||||
|
|
||||||
export default class VermineVehicleData extends foundry.abstract.TypeDataModel {
|
export default class VermineVehicleData extends foundry.abstract.TypeDataModel {
|
||||||
|
|
||||||
/** @override */
|
/** @override */
|
||||||
static LOCALIZATION_PREFIXES = ["VERMINE.item.vehicle"]
|
static LOCALIZATION_PREFIXES = ["VERMINE.vehicle"]
|
||||||
|
|
||||||
/** @override */
|
/** @override */
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
const fields = foundry.data.fields
|
const fields = foundry.data.fields
|
||||||
const reqInt = { required: true, nullable: false, integer: true }
|
const reqInt = { required: true, nullable: false, integer: true }
|
||||||
return {
|
return {
|
||||||
...baseItemSchema(),
|
// Identité
|
||||||
mobility: new fields.NumberField({ ...reqInt, initial: 3, min: 0 })
|
identity: new fields.SchemaField({
|
||||||
|
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: "" }),
|
||||||
|
notes: new fields.HTMLField({ required: true, initial: "", textSearch: true })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Rareté (valeur 1-10 + handicap)
|
||||||
|
rarity: raritySchema(),
|
||||||
|
|
||||||
|
// 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é
|
||||||
|
mobility: new fields.NumberField({ ...reqInt, initial: 3, min: 0 }),
|
||||||
|
|
||||||
|
// Propriétaire (id d'acteur : groupe ou personnage)
|
||||||
|
ownerId: new fields.StringField({ required: true, nullable: false, initial: "" })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -99,10 +99,10 @@ VERMINE.totemDomains = {
|
|||||||
* NPC Threat Levels configuration
|
* NPC Threat Levels configuration
|
||||||
*/
|
*/
|
||||||
VERMINE.npcThreatLevels = {
|
VERMINE.npcThreatLevels = {
|
||||||
1: { "label": "THREAT_LEVELS.minor", "attack": 3, "vigor": 1, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
|
1: { "label": "THREAT_LEVELS.minor", "attack": 3, "vigor": 1, "minorThreshold": 1, "majorThreshold": 4, "deadlyThreshold": 6, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
|
||||||
2: { "label": "THREAT_LEVELS.serious", "attack": 4, "vigor": 2, "minorWound": 2, "majorWound": 1, "deadlyWound": 1 },
|
2: { "label": "THREAT_LEVELS.serious", "attack": 4, "vigor": 2, "minorThreshold": 2, "majorThreshold": 5, "deadlyThreshold": 8, "minorWound": 2, "majorWound": 1, "deadlyWound": 1 },
|
||||||
3: { "label": "THREAT_LEVELS.major", "attack": 5, "vigor": 3, "minorWound": 2, "majorWound": 1, "deadlyWound": 1 },
|
3: { "label": "THREAT_LEVELS.major", "attack": 5, "vigor": 3, "minorThreshold": 2, "majorThreshold": 5, "deadlyThreshold": 9, "minorWound": 2, "majorWound": 2, "deadlyWound": 1 },
|
||||||
4: { "label": "THREAT_LEVELS.deadly", "attack": 6, "vigor": 4, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }
|
4: { "label": "THREAT_LEVELS.deadly", "attack": 6, "vigor": 4, "minorThreshold": 3, "majorThreshold": 6, "deadlyThreshold": 9, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import { VermineUtils } from "../roll.mjs"
|
||||||
|
|
||||||
|
const { HandlebarsApplicationMixin } = foundry.applications.api
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialogue de jet simplifié pour les PNJ.
|
||||||
|
*
|
||||||
|
* Un PNJ n'a ni caractéristiques ni compétences : toutes ses actions sont
|
||||||
|
* résolues par une valeur unique dérivée de son Profil (Menace / Expérience /
|
||||||
|
* Rôle), exposée dans system.computed. Ce dialogue fixe le pool selon le type
|
||||||
|
* de jet (Action, Spécialité, Attaque ou Réaction) et n'offre que la difficulté,
|
||||||
|
* le handicap et les bonus simples.
|
||||||
|
*
|
||||||
|
* Utilisé par :
|
||||||
|
* - la fiche PNJ (boutons de jet rapide Action / Spécialité / Réaction) ;
|
||||||
|
* - la défense des PNJ dans les échanges de coups (mode verrouillé).
|
||||||
|
*/
|
||||||
|
export default class NpcRollDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
||||||
|
|
||||||
|
#actor
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
return game.i18n.localize("VERMINE.roll")
|
||||||
|
}
|
||||||
|
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
classes: ["vermine-roll"],
|
||||||
|
tag: "form",
|
||||||
|
window: {
|
||||||
|
icon: "fas fa-dice-d10",
|
||||||
|
resizable: false
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
width: 520,
|
||||||
|
height: 600
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
roll: NpcRollDialog.#onRoll,
|
||||||
|
cancel: NpcRollDialog.#onCancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
main: { template: "systems/vermine2047/templates/dialogs/npc-roll-dialog.hbs" }
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(data = {}) {
|
||||||
|
const actor = data.actor instanceof Actor ? data.actor : await game.actors.get(data.actorId)
|
||||||
|
if (!actor) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return new NpcRollDialog({
|
||||||
|
actor,
|
||||||
|
rolltype: data.rolltype ?? "action",
|
||||||
|
locks: data.locks ?? null,
|
||||||
|
defense: data.defense ?? null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options = {}) {
|
||||||
|
super(options)
|
||||||
|
this.#actor = options.actor
|
||||||
|
this.rolltype = options.rolltype ?? "action"
|
||||||
|
this.locks = options.locks ?? null
|
||||||
|
this.defense = options.defense ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Getters ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
get computed() { return this.#actor.system?.computed || {} }
|
||||||
|
|
||||||
|
/** Pool de dés selon le type de jet. */
|
||||||
|
get pool() {
|
||||||
|
const c = this.computed
|
||||||
|
switch (this.rolltype) {
|
||||||
|
case "specialty": return c.specialties || 0
|
||||||
|
case "attack": return c.attack || 0
|
||||||
|
case "reaction": return c.reaction || 0
|
||||||
|
default: return c.action || 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Réussites automatiques (bonus de Réaction du rôle). */
|
||||||
|
get freeSuccesses() {
|
||||||
|
return this.rolltype === "reaction" ? (this.computed.reactionBonus || 0) : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Relances : utilisables uniquement sur les attaques ou les Spécialités. */
|
||||||
|
get rerolls() {
|
||||||
|
if (this.rolltype !== "specialty" && this.rolltype !== "attack") return 0
|
||||||
|
return this.computed.rerolls || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
get label() {
|
||||||
|
const keys = {
|
||||||
|
action: "ADVERSITY.action",
|
||||||
|
specialty: "ADVERSITY.specialties",
|
||||||
|
attack: "ADVERSITY.attack",
|
||||||
|
reaction: "ADVERSITY.reaction"
|
||||||
|
}
|
||||||
|
return game.i18n.localize(keys[this.rolltype] || "VERMINE.roll")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Rendu ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async _prepareContext() {
|
||||||
|
const difficultyOptions = []
|
||||||
|
for (let d = 3; d <= 10; d++) {
|
||||||
|
difficultyOptions.push({ difficulty: d, label: String(d) })
|
||||||
|
}
|
||||||
|
let lockedDifficulty = null
|
||||||
|
let defaultDifficulty = 7
|
||||||
|
if (this.locks?.difficulty !== undefined && this.locks?.difficulty !== null) {
|
||||||
|
lockedDifficulty = parseInt(this.locks.difficulty, 10)
|
||||||
|
defaultDifficulty = lockedDifficulty
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
actor: this.#actor,
|
||||||
|
system: this.#actor.system,
|
||||||
|
config: CONFIG.VERMINE,
|
||||||
|
rollLabel: this.label,
|
||||||
|
rolltype: this.rolltype,
|
||||||
|
pool: this.pool,
|
||||||
|
freeSuccesses: this.freeSuccesses,
|
||||||
|
rerolls: this.rerolls,
|
||||||
|
isDefense: Boolean(this.defense),
|
||||||
|
lockedDifficulty,
|
||||||
|
difficultyOptions,
|
||||||
|
defaultDifficulty,
|
||||||
|
speakerId: this.#actor.id,
|
||||||
|
availableItems: this.#actor.items.filter(i => i.type === "item")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onRender(context, options) {
|
||||||
|
this.element.dataset.actorId = this.#actor.id
|
||||||
|
for (const inp of this.element.querySelectorAll("[data-roll]")) {
|
||||||
|
inp.addEventListener("change", this.#onInputChange.bind(this))
|
||||||
|
}
|
||||||
|
this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI())
|
||||||
|
this.#updateUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
get #el() { return this.element }
|
||||||
|
|
||||||
|
#getHandicap() {
|
||||||
|
const sel = this.#el.querySelector("#handicap")
|
||||||
|
return Math.max(0, (parseInt(sel?.value, 10) || 1) - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
#getDifficulty() {
|
||||||
|
const sel = this.#el.querySelector("#difficulty")
|
||||||
|
return parseInt(sel?.value, 10) || 7
|
||||||
|
}
|
||||||
|
|
||||||
|
#getHelped() { return this.#el.querySelector("#helped")?.checked ? 1 : 0 }
|
||||||
|
|
||||||
|
#getGroup() { return parseInt(this.#el.querySelector("#group")?.value, 10) || 0 }
|
||||||
|
|
||||||
|
getPoolBreakdown() {
|
||||||
|
const help = this.#getHelped()
|
||||||
|
const group = this.#getGroup()
|
||||||
|
return { pool: this.pool, help, group, total: this.pool + help + group }
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateUI() {
|
||||||
|
const b = this.getPoolBreakdown()
|
||||||
|
const totalEl = this.#el.querySelector("#dice-pool-total")
|
||||||
|
if (totalEl) totalEl.textContent = `${b.total}D`
|
||||||
|
const bonusEl = this.#el.querySelector("#total-bonus")
|
||||||
|
if (bonusEl) bonusEl.textContent = b.help + b.group
|
||||||
|
const freeEl = this.#el.querySelector("#free-successes")
|
||||||
|
if (freeEl) freeEl.textContent = String(this.freeSuccesses)
|
||||||
|
}
|
||||||
|
|
||||||
|
#onInputChange() {
|
||||||
|
this.#updateUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Actions ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
static async #onCancel(event, target) {
|
||||||
|
this.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onRoll(event, target) {
|
||||||
|
const label = this.defense
|
||||||
|
? (this.defense.type === "parade"
|
||||||
|
? game.i18n.localize("VERMINE.defense_parade")
|
||||||
|
: game.i18n.localize("VERMINE.defense_esquive"))
|
||||||
|
: this.label
|
||||||
|
|
||||||
|
await VermineUtils.roll({
|
||||||
|
actor: this.#actor,
|
||||||
|
NoD: this.getPoolBreakdown().total,
|
||||||
|
Reroll: this.rerolls,
|
||||||
|
difficulty: this.#getDifficulty(),
|
||||||
|
handicap: this.#getHandicap(),
|
||||||
|
rollLabel: label,
|
||||||
|
bonusSuccesses: this.freeSuccesses,
|
||||||
|
poolBreakdown: this.getPoolBreakdown(),
|
||||||
|
messageFlags: this.defense ? { "vermine-defense": this.defense } : null
|
||||||
|
})
|
||||||
|
|
||||||
|
this.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -0,0 +1,509 @@
|
|||||||
|
/**
|
||||||
|
* 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} · ${t('VERMINE.defender_successes')}: ${r.defenderSuccesses} → ${verdict}</div>`
|
||||||
|
]
|
||||||
|
if (r.hit) {
|
||||||
|
lines.push(`<div class="resolution-line">${t('VERMINE.raw_damage')}: <strong>${r.damage}</strong>${r.protection ? ` − ${t('ADVERSITY.protection')}: ${r.protection}` : ''} = ${t('VERMINE.net_damage')}: <strong>${r.netDamage}</strong></div>`)
|
||||||
|
if (r.woundCategory) {
|
||||||
|
lines.push(`<div class="resolution-line res-wound">${t('VERMINE.wound')}: <strong>${t(this.woundLabelKey(r.woundCategory))}</strong> (${t('VERMINE.wounds.threshold')} ${r.woundThreshold}D)</div>`)
|
||||||
|
} else {
|
||||||
|
lines.push(`<div class="resolution-line res-noshock">${t('VERMINE.no_wound')}</div>`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lines.push(`<div class="resolution-line res-nodamage">${t('VERMINE.no_damage')}</div>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const apply = (r.hit && r.woundCategory && r.canApply)
|
||||||
|
? `<button type="button" class="exchange-apply" data-action="exchange-apply" data-defender-id="${r.defenderId}" data-defender-uuid="${r.defenderUuid}" data-wound="${r.woundCategory}"><i class="fas fa-briefcase-medical"></i> ${t('VERMINE.apply_wound')}</button>`
|
||||||
|
: ''
|
||||||
|
|
||||||
|
return `<div class="exchange-resolution">
|
||||||
|
<div class="resolution-header"><i class="fas fa-exchange-alt"></i> ${t('VERMINE.resolve_result')}</div>
|
||||||
|
${lines.join('')}
|
||||||
|
${apply}
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Liaison chat ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lie les interactions d'échange de coups sur un message de chat.
|
||||||
|
* @param {HTMLElement} html élément du message
|
||||||
|
* @param {ChatMessage} message
|
||||||
|
*/
|
||||||
|
static bindChat(html, message) {
|
||||||
|
const exchange = message.getFlag('world', FLAG_EXCHANGE)
|
||||||
|
if (exchange) {
|
||||||
|
html.querySelectorAll('.exchange-defend').forEach(btn => {
|
||||||
|
btn.addEventListener('click', ev => this.#onDefense(ev, message))
|
||||||
|
})
|
||||||
|
html.querySelectorAll('.exchange-resolve').forEach(btn => {
|
||||||
|
btn.addEventListener('click', ev => this.#onResolve(ev, message))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
html.querySelectorAll('[data-action="exchange-apply"]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', ev => this.#onApply(ev, message))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onDefense(ev, attackMessage) {
|
||||||
|
ev.preventDefault()
|
||||||
|
ev.stopPropagation()
|
||||||
|
const btn = ev.currentTarget
|
||||||
|
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||||
|
if (!exchange) return
|
||||||
|
const type = btn.dataset.type
|
||||||
|
const actor = await this.#resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
|
||||||
|
if (!actor) {
|
||||||
|
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!game.user.isGM && !actor.isOwner) {
|
||||||
|
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (actor.type === 'creature') {
|
||||||
|
ui.notifications.warn(game.i18n.localize('VERMINE.error_creature_no_defense'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const defense = {
|
||||||
|
exchangeId: exchange.id,
|
||||||
|
type,
|
||||||
|
actorId: actor.id,
|
||||||
|
actorUuid: actor.uuid,
|
||||||
|
actorName: actor.name,
|
||||||
|
difficulty: exchange.difficulty,
|
||||||
|
parryLevel: type === 'parade' ? this.#parryLevel(actor) : null
|
||||||
|
}
|
||||||
|
// PNJ : pas de caractéristiques/compétences — la défense utilise la valeur
|
||||||
|
// d'Action (Expérience), difficulté verrouillée.
|
||||||
|
if (actor.type === 'npc') {
|
||||||
|
const { default: NpcRollDialog } = await import('./dialogs/npcRollDialog.mjs')
|
||||||
|
const dialog = await NpcRollDialog.create({
|
||||||
|
actor,
|
||||||
|
rolltype: 'action',
|
||||||
|
locks: { difficulty: exchange.difficulty },
|
||||||
|
defense
|
||||||
|
})
|
||||||
|
if (dialog) dialog.render(true)
|
||||||
|
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
|
||||||
|
})
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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') })
|
||||||
|
|||||||
+133
-29
@@ -29,7 +29,14 @@ export class VermineUtils {
|
|||||||
keepTotem = null,
|
keepTotem = null,
|
||||||
skillLevel = null,
|
skillLevel = null,
|
||||||
hasSpecialty = false,
|
hasSpecialty = false,
|
||||||
handicap = 0
|
handicap = 0,
|
||||||
|
bonusSuccesses = 0,
|
||||||
|
poolBreakdown = null,
|
||||||
|
specialtyName = null,
|
||||||
|
weapon = null,
|
||||||
|
targets = [],
|
||||||
|
attack = null,
|
||||||
|
messageFlags = null
|
||||||
}) {
|
}) {
|
||||||
// Validate inputs
|
// Validate inputs
|
||||||
if (!actor) {
|
if (!actor) {
|
||||||
@@ -105,6 +112,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());
|
||||||
|
|
||||||
@@ -120,9 +148,9 @@ export class VermineUtils {
|
|||||||
totemBonuses: { ...totemBonus },
|
totemBonuses: { ...totemBonus },
|
||||||
baseNoD: NoD,
|
baseNoD: NoD,
|
||||||
rerolls: Reroll,
|
rerolls: Reroll,
|
||||||
selfControl: self_control
|
selfControl: self_control,
|
||||||
|
bonusSuccesses: bonusSuccesses
|
||||||
};
|
};
|
||||||
|
|
||||||
// Evaluate the roll
|
// Evaluate the roll
|
||||||
await roll.evaluate();
|
await roll.evaluate();
|
||||||
|
|
||||||
@@ -143,7 +171,14 @@ export class VermineUtils {
|
|||||||
keepTotem,
|
keepTotem,
|
||||||
skillLevel,
|
skillLevel,
|
||||||
hasSpecialty,
|
hasSpecialty,
|
||||||
handicap
|
handicap,
|
||||||
|
bonusSuccesses,
|
||||||
|
poolBreakdown,
|
||||||
|
specialtyName,
|
||||||
|
weapon,
|
||||||
|
targets,
|
||||||
|
attack,
|
||||||
|
messageFlags
|
||||||
});
|
});
|
||||||
|
|
||||||
return roll;
|
return roll;
|
||||||
@@ -199,10 +234,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 +246,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 +256,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 +300,71 @@ 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;
|
||||||
|
});
|
||||||
|
// Réussites automatiques (ex. bonus de Réaction) : ajoutées au total.
|
||||||
|
const freeSuccesses = parseInt(rollMessage.dataset?.freeSuccesses ?? '0', 10) || 0;
|
||||||
|
total += freeSuccesses;
|
||||||
|
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 +390,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 +413,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 +421,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 +479,38 @@ 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 bonusSuccesses = param.bonusSuccesses ?? 0;
|
||||||
|
const total = (roll._total ?? 0) + bonusSuccesses;
|
||||||
|
const verdict = {
|
||||||
|
success: total >= required,
|
||||||
|
total,
|
||||||
|
required,
|
||||||
|
bonusSuccesses
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-9
@@ -37,11 +37,21 @@ 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;
|
||||||
|
CONFIG.Actor.dataModels.vehicle = models.VermineVehicleData;
|
||||||
|
|
||||||
|
// Libellés des types "community"/"vehicle" (TYPES.Actor.* du core ne les connaît pas)
|
||||||
|
CONFIG.Actor.typeLabels ??= {};
|
||||||
|
CONFIG.Actor.typeLabels.community = "VERMINE.type_community";
|
||||||
|
CONFIG.Actor.typeLabels.vehicle = "VERMINE.type_vehicle";
|
||||||
|
Hooks.once('i18nInit', () => {
|
||||||
|
CONFIG.Actor.typeLabels.community = "VERMINE.type_community";
|
||||||
|
CONFIG.Actor.typeLabels.vehicle = "VERMINE.type_vehicle";
|
||||||
|
});
|
||||||
|
|
||||||
CONFIG.Item.dataModels.item = models.VermineItemData;
|
CONFIG.Item.dataModels.item = models.VermineItemData;
|
||||||
CONFIG.Item.dataModels.weapon = models.VermineWeaponData;
|
CONFIG.Item.dataModels.weapon = models.VermineWeaponData;
|
||||||
CONFIG.Item.dataModels.defense = models.VermineDefenseData;
|
CONFIG.Item.dataModels.defense = models.VermineDefenseData;
|
||||||
CONFIG.Item.dataModels.vehicle = models.VermineVehicleData;
|
|
||||||
CONFIG.Item.dataModels.ability = models.VermineAbilityData;
|
CONFIG.Item.dataModels.ability = models.VermineAbilityData;
|
||||||
CONFIG.Item.dataModels.specialty = models.VermineSpecialtyData;
|
CONFIG.Item.dataModels.specialty = models.VermineSpecialtyData;
|
||||||
CONFIG.Item.dataModels.background = models.VermineBackgroundData;
|
CONFIG.Item.dataModels.background = models.VermineBackgroundData;
|
||||||
@@ -72,7 +82,7 @@ Hooks.once('init', async function () {
|
|||||||
// Register sheet application classes (ApplicationV2)
|
// Register sheet application classes (ApplicationV2)
|
||||||
// Unregister core sheets
|
// Unregister core sheets
|
||||||
foundry.documents.collections.Actors.unregisterSheet("core", foundry.applications.sheets.ActorSheetV2, {
|
foundry.documents.collections.Actors.unregisterSheet("core", foundry.applications.sheets.ActorSheetV2, {
|
||||||
types: ["character", "npc", "group", "creature"]
|
types: ["character", "npc", "group", "creature", "community", "vehicle"]
|
||||||
})
|
})
|
||||||
foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1?.sheets?.ItemSheet)
|
foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1?.sheets?.ItemSheet)
|
||||||
|
|
||||||
@@ -89,6 +99,12 @@ 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"
|
||||||
|
})
|
||||||
|
foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineVehicleSheetV2, {
|
||||||
|
types: ["vehicle"], makeDefault: true, label: "VERMINE.Sheet.vehicle"
|
||||||
|
})
|
||||||
|
|
||||||
// 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, {
|
||||||
@@ -100,9 +116,6 @@ Hooks.once('init', async function () {
|
|||||||
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineDefenseSheetV2, {
|
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineDefenseSheetV2, {
|
||||||
types: ["defense"], makeDefault: true
|
types: ["defense"], makeDefault: true
|
||||||
})
|
})
|
||||||
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineVehicleSheetV2, {
|
|
||||||
types: ["vehicle"], makeDefault: true
|
|
||||||
})
|
|
||||||
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineAbilitySheetV2, {
|
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineAbilitySheetV2, {
|
||||||
types: ["ability"], makeDefault: true
|
types: ["ability"], makeDefault: true
|
||||||
})
|
})
|
||||||
@@ -178,9 +191,8 @@ Hooks.once('init', async function () {
|
|||||||
"systems/vermine2047/templates/actor/appv2/character-stories.hbs",
|
"systems/vermine2047/templates/actor/appv2/character-stories.hbs",
|
||||||
"systems/vermine2047/templates/actor/appv2/character-combat.hbs",
|
"systems/vermine2047/templates/actor/appv2/character-combat.hbs",
|
||||||
"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-traits.hbs",
|
||||||
"systems/vermine2047/templates/actor/appv2/npc-skills.hbs",
|
"systems/vermine2047/templates/actor/appv2/npc-equipment.hbs",
|
||||||
"systems/vermine2047/templates/actor/appv2/npc-threat.hbs",
|
|
||||||
"systems/vermine2047/templates/actor/appv2/npc-combat.hbs",
|
"systems/vermine2047/templates/actor/appv2/npc-combat.hbs",
|
||||||
"systems/vermine2047/templates/actor/appv2/npc-notes.hbs",
|
"systems/vermine2047/templates/actor/appv2/npc-notes.hbs",
|
||||||
"systems/vermine2047/templates/actor/appv2/group-main.hbs",
|
"systems/vermine2047/templates/actor/appv2/group-main.hbs",
|
||||||
@@ -192,7 +204,13 @@ 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",
|
||||||
|
"systems/vermine2047/templates/actor/appv2/vehicle-main.hbs",
|
||||||
|
"systems/vermine2047/templates/actor/appv2/vehicle-specs.hbs"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Acuité auditive
|
||||||
|
type: evolution
|
||||||
|
_id: vPA7gekUx8kUNOWB
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage est doté d’une ouïe extraordinaire. Il perçoit des ondes
|
||||||
|
sonores très aigües, ce qui lui permet d’entendre les ultrasons. Son acuité
|
||||||
|
est aussi surdéveloppée. Il peut ainsi entendre un son à une distance cinq
|
||||||
|
fois supérieure aux autres personnages. Enfin, sa sensibilité lui permet de
|
||||||
|
distinguer les sons entre eux, et ainsi d’entendre distinctement un son qui
|
||||||
|
serait couvert par le vent, par exemple. Son ouïe extraordinaire lui offre
|
||||||
|
un Bonus de 3D à tous les jets de Perception basés sur l’audition.</p>
|
||||||
|
level:
|
||||||
|
value: 3
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198965
|
||||||
|
modifiedTime: 1785775198965
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!vPA7gekUx8kUNOWB'
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Adrénaline
|
||||||
|
type: evolution
|
||||||
|
_id: DItkLLGSyjAGzruO
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le corps du personnage s’est adapté au stress du combat. Quand le
|
||||||
|
personnage subit des Blessures lors d’un combat, il ne subit pas la perte de
|
||||||
|
Dés de Réserve associée. En revanche, à la fin du combat, il perd 2D
|
||||||
|
d’Effort et 1D de Sang-Froid pour chaque Blessure subie.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198966
|
||||||
|
modifiedTime: 1785775198966
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!DItkLLGSyjAGzruO'
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Analgésie
|
||||||
|
type: evolution
|
||||||
|
_id: Cs6ibCrcPxim7QHR
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Pour le personnage, la douleur n’est qu’une information dont il ne subit
|
||||||
|
pas les conséquences néfastes. Lorsqu’il subit une Blessure, il ne perd
|
||||||
|
aucun Dé de Sang- Froid. En outre, le Malus qu’il subit à la suite de
|
||||||
|
Blessures est diminué de 1D.</p>
|
||||||
|
level:
|
||||||
|
value: 3
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198966
|
||||||
|
modifiedTime: 1785775198966
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!Cs6ibCrcPxim7QHR'
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Anticipation
|
||||||
|
type: evolution
|
||||||
|
_id: pP9hfvk5JKsoFcnt
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>En décryptant le langage corporel – ou peut-être en percevant les
|
||||||
|
phéromones – de son adversaire, le personnage ressent ses intentions. Il
|
||||||
|
peut utiliser la caractéristique Empathie plutôt que Réflexes pour esquiver
|
||||||
|
les attaques d’un adversaire humain. S’il réussit une telle esquive, il
|
||||||
|
regagne 1D dans sa Réserve de Sang-Froid.</p>
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198967
|
||||||
|
modifiedTime: 1785775198967
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!pP9hfvk5JKsoFcnt'
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Au bout de l’effort
|
||||||
|
type: evolution
|
||||||
|
_id: h3Q6LwnnVuj6MH4Y
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage peut dépasser ses limites. Il peut dépenser, sur un jet, un
|
||||||
|
nombre de Dés de Réserve égal à la Caractéristique utilisée +1 (contre un
|
||||||
|
seul normalement).</p>
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198967
|
||||||
|
modifiedTime: 1785775198967
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!h3Q6LwnnVuj6MH4Y'
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Aura d’optimisme
|
||||||
|
type: evolution
|
||||||
|
_id: JrtHAUqFgA75e6pq
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage dégage une imperceptible aura bienfaisante autour de lui.
|
||||||
|
La présence réconfortante du personnage réduit de 2D les pertes de Dés de la
|
||||||
|
Réserve de Groupe à la suite d’un événement traumatisant. Le personnage doit
|
||||||
|
être témoin de l’événement pour que sa Capacité fonctionne.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198967
|
||||||
|
modifiedTime: 1785775198967
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!JrtHAUqFgA75e6pq'
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Camouflage
|
||||||
|
type: evolution
|
||||||
|
_id: t4p4n73kwwVoeSMe
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage ne dégage aucune odeur et émet des phéromones qui le
|
||||||
|
rendent insignifiant aux yeux des créatures qui l’entourent. Il peut ajouter
|
||||||
|
son score en Volonté aux jets destinés à camoufler sa présence, son odeur,
|
||||||
|
ses traces ou un objet lui appartenant. S’il camoufle sa présence et qu’il
|
||||||
|
reste immobile, il bénéficie en outre de trois Réussites supplémentaires.
|
||||||
|
Dès qu’il se déplace, il doit retenter le jet sans pouvoir bénéficier des
|
||||||
|
Réussites supplémentaires.</p>
|
||||||
|
level:
|
||||||
|
value: 3
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198967
|
||||||
|
modifiedTime: 1785775198967
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!t4p4n73kwwVoeSMe'
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Cicatrisation rapide
|
||||||
|
type: evolution
|
||||||
|
_id: 2h0xUozy1xqLdKcE
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>La Difficulté des Actions de soin sur le personnage est réduite de 2. Le
|
||||||
|
coût en Dés de Réserves pour guérir est diminué de 1D pour chaque type de
|
||||||
|
Blessure.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198968
|
||||||
|
modifiedTime: 1785775198968
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!2h0xUozy1xqLdKcE'
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Défenses immunitaires
|
||||||
|
type: evolution
|
||||||
|
_id: oNfjRGGRVFGtfbK0
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>La Virulence d’une infection affectant le personnage est réduite de 1 –
|
||||||
|
voir Les Règles, « Maladies et infections », page 105. Lorsque le personnage
|
||||||
|
reçoit des soins pour soigner une infection, le soigneur reçoit une Réussite
|
||||||
|
supplémentaire en cas de jet réussi.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198968
|
||||||
|
modifiedTime: 1785775198968
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!oNfjRGGRVFGtfbK0'
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Dur au mal
|
||||||
|
type: evolution
|
||||||
|
_id: vgYLoiEqEOxrGdy6
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: <p>Le Seuil de Blessure Légère du personnage augmente de 1.</p>
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198968
|
||||||
|
modifiedTime: 1785775198968
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!vgYLoiEqEOxrGdy6'
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Endurance
|
||||||
|
type: evolution
|
||||||
|
_id: uGG0FrBn1QtMcxbz
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage a toujours été particulièrement résistant. Toutes les
|
||||||
|
pertes de Réserve d’Effort qu’il subit sont réduites de 1D, jusqu’à un
|
||||||
|
minimum de 1D.</p>
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198968
|
||||||
|
modifiedTime: 1785775198968
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!uGG0FrBn1QtMcxbz'
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Exosquelette
|
||||||
|
type: evolution
|
||||||
|
_id: bV6V4i1v4ORWDJSQ
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage a développé un exosquelette fait de chitine. De nombreux
|
||||||
|
endroits de son corps et de son visage sont recouverts de plaques rigides
|
||||||
|
qui lui procurent une Protection de 3/5 (L) sans lui infliger de Handicap de
|
||||||
|
Mobilité – voir Les Règles, « Handicap de Mobilité », page 70. Il bénéficie
|
||||||
|
en outre d’un Bonus de 2D aux Actions dont le but est d’intimider ou
|
||||||
|
d’effrayer. En revanche, il subit un Malus identique pour toutes les autres
|
||||||
|
Actions sociales.</p>
|
||||||
|
level:
|
||||||
|
value: 4
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198968
|
||||||
|
modifiedTime: 1785775198968
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!bV6V4i1v4ORWDJSQ'
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Force mentale
|
||||||
|
type: evolution
|
||||||
|
_id: S8AwTKDxIdez7vHB
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage a des ressources mentales insoupçonnées. Il ne subit pas de
|
||||||
|
Malus d’Épuisement aux Actions mentales et sociales quand sa Réserve de
|
||||||
|
Sang-Froid est vide. Si ses deux Réserves sont vides, il subit néanmoins le
|
||||||
|
Malus normal de 2D à toutes ses Actions.</p>
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198969
|
||||||
|
modifiedTime: 1785775198969
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!S8AwTKDxIdez7vHB'
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Frappe lourde
|
||||||
|
type: evolution
|
||||||
|
_id: RjdZB4Qd2V8mp44x
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Lorsque le personnage attaque à mains nues, ses dommages sont égaux à
|
||||||
|
Vigueur (C).</p>
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198969
|
||||||
|
modifiedTime: 1785775198969
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!RjdZB4Qd2V8mp44x'
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Griffes
|
||||||
|
type: evolution
|
||||||
|
_id: kPoioCvlK7H0uJB1
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Les ongles du personnage peuvent pousser en un instant pour devenir des
|
||||||
|
armes dangereuses. Ces griffes infligent des Dommages de 2 (L) et
|
||||||
|
bénéficient du Trait Rapide (2). Quand le personnage réussit une attaque et
|
||||||
|
passe la protection de sa cible, il peut dépenser jusqu’à 3D de sa Réserve
|
||||||
|
d’Effort pour infliger 1 point de Dommages supplémentaire par dé dépensé.
|
||||||
|
Ces griffes se brisent à la fin du combat et ne peuvent repousser que
|
||||||
|
lorsque le personnage aura intégralement reconstitué sa Réserve
|
||||||
|
d’Effort.</p>
|
||||||
|
level:
|
||||||
|
value: 3
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198969
|
||||||
|
modifiedTime: 1785775198969
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!kPoioCvlK7H0uJB1'
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Habitué à la faim
|
||||||
|
type: evolution
|
||||||
|
_id: EqeONvUCK7OhJaNN
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le premier jour de rationnement ou de privation totale de nourriture n’a
|
||||||
|
pas d’effet sur le personnage. Ainsi, il ne subira aucune perte de Dés de
|
||||||
|
Réserves avant le deuxième jour de privation totale ou le troisième jour de
|
||||||
|
rationnement.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198969
|
||||||
|
modifiedTime: 1785775198969
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!EqeONvUCK7OhJaNN'
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Hyperosmie
|
||||||
|
type: evolution
|
||||||
|
_id: 8ShwJQCWKHsfXLko
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage est doté d’un odorat extrêmement développé, proche de celui
|
||||||
|
d’un chien. Cette acuité lui permet de distinguer des odeurs imperceptibles
|
||||||
|
pour ses semblables. Il peut ainsi reconnaître une personne ou un animal à
|
||||||
|
son odeur et savoir si cet individu est passé dans un endroit jusqu’à une
|
||||||
|
semaine après son passage. Cela lui offre un talent incroyable pour suivre
|
||||||
|
une piste et traquer une proie : il bénéficie d’un Bonus de 3D aux jets de
|
||||||
|
Perception quand l’odorat entre en jeu. Cette Mutation s’accompagne d’une
|
||||||
|
mémoire olfactive très développée. Une fois qu’il a senti une odeur, le
|
||||||
|
personnage ne l’oublie jamais et peut la distinguer et la reconnaître sans
|
||||||
|
faillir.</p>
|
||||||
|
level:
|
||||||
|
value: 3
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198969
|
||||||
|
modifiedTime: 1785775198969
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!8ShwJQCWKHsfXLko'
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Immunité absolue
|
||||||
|
type: evolution
|
||||||
|
_id: ivx8gS0XBEcoqtdl
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage est immunisé à toutes les Infections et toutes les Toxines.
|
||||||
|
Il ne peut par ailleurs pas développer d’addiction. En contrepartie, les
|
||||||
|
médicaments et autres stimulants sont totalement inefficaces sur lui et
|
||||||
|
n’octroient aucun bénéfice que ce soit.</p>
|
||||||
|
level:
|
||||||
|
value: 4
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198970
|
||||||
|
modifiedTime: 1785775198970
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!ivx8gS0XBEcoqtdl'
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Indestructible
|
||||||
|
type: evolution
|
||||||
|
_id: HfC2oOARrpKnpoQf
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le corps du personnage a gagné en densité, mais son poids augmente de 25
|
||||||
|
% et ses besoins nutritionnels explosent. Il gagne un cercle dans chaque
|
||||||
|
type de Blessure, subit (I) Handicap permanent de Mobilité, et a besoin de
|
||||||
|
deux rations de nourriture plutôt qu’une. En cas de rationnement de
|
||||||
|
nourriture, il subit des pertes de Réserves équivalentes à une absence
|
||||||
|
totale de nourriture.</p>
|
||||||
|
level:
|
||||||
|
value: 4
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198970
|
||||||
|
modifiedTime: 1785775198970
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!HfC2oOARrpKnpoQf'
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Insensible
|
||||||
|
type: evolution
|
||||||
|
_id: Re0aeZzupnz3EL6E
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le corps du personnage sécrète de grandes quantités d’hormones en cas de
|
||||||
|
stress intense, ce qui lui permet d’y résister plus facilement. L’Intensité
|
||||||
|
des événements stressants est diminuée d’un niveau pour le personnage. Il
|
||||||
|
n’a même pas à tenter de jet pour résister à un événement Mineur – voir Les
|
||||||
|
Règles, « Événements », page 92.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198970
|
||||||
|
modifiedTime: 1785775198970
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!Re0aeZzupnz3EL6E'
|
||||||
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Instinct
|
||||||
|
type: evolution
|
||||||
|
_id: UxNguZd8K62nt1GH
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Un jet d’Empathie + Vigilance contre une Difficulté de 9 permet au
|
||||||
|
personnage de pressentir un danger ou une menace imminente d’origine
|
||||||
|
naturelle ou animale. Chaque Réussite obtenue lui permet d’obtenir une
|
||||||
|
information sur le danger pressenti. Si celui-ci tourne au combat, le
|
||||||
|
personnage sera considéré comme Actif pour son jet de Réaction. S’il a le
|
||||||
|
temps d’informer ses compagnons du danger, ces derniers seront Actifs
|
||||||
|
également.</p>
|
||||||
|
|
||||||
|
<p>En Modes <strong>Cauchemar </strong>et <strong>Apocalypse</strong>,
|
||||||
|
chaque Réussite au jet de Vigilance octroie au personnage 1D de Bonus au jet
|
||||||
|
de Réaction ou au jet permettant d’éviter le danger perçu.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198970
|
||||||
|
modifiedTime: 1785775198970
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!UxNguZd8K62nt1GH'
|
||||||
|
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Musculature surdéveloppée
|
||||||
|
type: evolution
|
||||||
|
_id: AAw7U5tUxbUczorz
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage est exceptionnellement fort, ce que reflète sa silhouette
|
||||||
|
impressionnante. Il bénéficie d’un Bonus de 3D aux jets de Vigueur quand une
|
||||||
|
force musculaire élevée peut l’aider (soulever ou porter un objet lourd,
|
||||||
|
enfoncer une porte, abattre un mur, etc.) Lorsqu’il porte une attaque de
|
||||||
|
corps à corps ou de mêlée, sa Vigueur est considérée comme augmentée de 3D
|
||||||
|
pour calculer les Dommages de base de ses attaques. En revanche, sa force
|
||||||
|
surhumaine n’augmente pas sa précision, il ne gagne donc aucun Bonus à
|
||||||
|
l’attaque.</p>
|
||||||
|
level:
|
||||||
|
value: 3
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198971
|
||||||
|
modifiedTime: 1785775198971
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!AAw7U5tUxbUczorz'
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: OEil de lynx
|
||||||
|
type: evolution
|
||||||
|
_id: guC1pOfmcmitRGDo
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage bénéficie d’un Bonus de 1D sur les Actions impliquant
|
||||||
|
l’acuité visuelle.</p>
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198971
|
||||||
|
modifiedTime: 1785775198971
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!guC1pOfmcmitRGDo'
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Peau mimétique
|
||||||
|
type: evolution
|
||||||
|
_id: kFak0PMoNAuPnSfX
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>La peau du personnage peut prendre la teinte globale de son environnement
|
||||||
|
pour l’aider à se camoufler : elle deviendra plus sombre dans l’obscurité,
|
||||||
|
plus verte dans la nature… Pour activer cette Mutation, le personnage doit
|
||||||
|
dépenser 1D de sa Réserve de Sang-Froid. Dès cet instant, il bénéficie de 4D
|
||||||
|
Bonus sur tous les jets de Perception visant à rester discret. S’il attaque
|
||||||
|
sous ce camouflage sans avoir été préalablement repéré, il est considéré
|
||||||
|
comme Offensif et tous ses adversaires comme Passifs.</p>
|
||||||
|
level:
|
||||||
|
value: 4
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198971
|
||||||
|
modifiedTime: 1785775198971
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!kFak0PMoNAuPnSfX'
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Phéromones attractives
|
||||||
|
type: evolution
|
||||||
|
_id: 4jRJw5vuhZeqoKy9
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage est capable de dégager des phéromones qui attirent la
|
||||||
|
sympathie des créatures vivantes. Sans autre motivation, les humains et les
|
||||||
|
animaux sont instinctivement attirés par le personnage et ont un a priori
|
||||||
|
positif à son encontre. Le personnage peut en outre désigner un individu par
|
||||||
|
dé de Volonté et par scène. Il bénéficie d’un Bonus de 3D à ses Actions
|
||||||
|
sociales envers ces cibles. Les phéromones peuvent même annihiler toute
|
||||||
|
intention hostile. Une personne ou un animal qui souhaite agresser le
|
||||||
|
personnage, verbalement ou physiquement, doit d’abord réussir un Jet de
|
||||||
|
Volonté + Psychologie (ou de sa valeur d’Attaque s’il n’a pas de score de
|
||||||
|
Volonté) contre une Difficulté de 7. Chaque Réussite lui permet d’agir
|
||||||
|
normalement envers le personnage pendant une minute (ou un tour pendant un
|
||||||
|
combat). Le meneur est laissé seul juge d’éventuelles réactions indésirables
|
||||||
|
que ces phéromones pourraient produire chez des animaux ou des créatures
|
||||||
|
rencontrées…</p>
|
||||||
|
level:
|
||||||
|
value: 4
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198971
|
||||||
|
modifiedTime: 1785775198971
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!4jRJw5vuhZeqoKy9'
|
||||||
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Phéromones répulsives
|
||||||
|
type: evolution
|
||||||
|
_id: Y8UeTOYpGkM3nt9R
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage peut dégager des phéromones qui créent le dégoût et la
|
||||||
|
répulsion chez toutes les créatures vivantes. Lorsqu’il dégage ces
|
||||||
|
phéromones, tous les humains et les animaux dans un rayon d’une dizaine de
|
||||||
|
mètres se sentent mal à l’aise en présence du personnage et, sauf autre
|
||||||
|
motivation, s’éloignent de lui sans réellement comprendre pourquoi. Le
|
||||||
|
personnage peut en outre désigner un individu ou une créature par point de
|
||||||
|
Volonté et par scène. Ces cibles ne peuvent ni lui adresser la parole, ni
|
||||||
|
s’approcher, ni attaquer le personnage sauf si elles réussissent un Jet de
|
||||||
|
Volonté + Psychologie (ou de leur valeur d’Attaque si elles n’ont pas de
|
||||||
|
score de Volonté) contre une Difficulté de 7. Chaque Réussite leur permet
|
||||||
|
d’agir normalement envers le personnage pendant une minute (ou un tour
|
||||||
|
pendant un combat) mais elles subissent un Malus de 1D à toutes leurs
|
||||||
|
Actions envers lui. Le meneur est laissé seul juge d’éventuelles réactions
|
||||||
|
indésirables que ces phéromones pourraient produire chez des animaux ou des
|
||||||
|
créatures rencontrées…</p>
|
||||||
|
level:
|
||||||
|
value: 4
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198972
|
||||||
|
modifiedTime: 1785775198972
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!Y8UeTOYpGkM3nt9R'
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Réflexes éclairs
|
||||||
|
type: evolution
|
||||||
|
_id: BNZeEd0CpTgYkEou
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage réagit si rapidement au danger qu’il bénéficie d’un Bonus
|
||||||
|
de 2D à son jet de Réaction.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198972
|
||||||
|
modifiedTime: 1785775198972
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!BNZeEd0CpTgYkEou'
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Régénération
|
||||||
|
type: evolution
|
||||||
|
_id: wH5OPnv4cenzpgws
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Les plaies du personnage se referment très rapidement. Après une nuit de
|
||||||
|
repos complet, le personnage peut réduire le niveau d’une Blessure ou
|
||||||
|
effacer une Blessure Légère sans dépenser de dés, il peut ensuite dépenser
|
||||||
|
des dés pour soigner plusieurs autres Blessures s’il possède suffisamment de
|
||||||
|
Dés de Réserves. Le coût de guérison est réduit de 2D pour chaque type de
|
||||||
|
Blessure.</p>
|
||||||
|
level:
|
||||||
|
value: 3
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198972
|
||||||
|
modifiedTime: 1785775198972
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!wH5OPnv4cenzpgws'
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Résistance à la chaleur
|
||||||
|
type: evolution
|
||||||
|
_id: rCQOrAzaCgCF0Dri
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage est particulièrement résistant aux températures élevées. Il
|
||||||
|
ne subit jamais de perte de Dés de Réserves ni de Malus à la récupération à
|
||||||
|
cause de fortes chaleurs. Il ne subit pas non plus les Dommages cumulatifs
|
||||||
|
en cas d’exposition prolongée<em> – voir Les Règles, « Brûlures »</em>, page
|
||||||
|
99.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198973
|
||||||
|
modifiedTime: 1785775198973
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!rCQOrAzaCgCF0Dri'
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Résistance au froid
|
||||||
|
type: evolution
|
||||||
|
_id: DHv0PXsIgQ8rPPUY
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage est particulièrement résistant aux grands froids. Il ne
|
||||||
|
subit jamais de perte de Dés de Réserves ni de Malus à la récupération à
|
||||||
|
cause du froid extrême. Il ne subit pas non plus les Dommages cumulatifs en
|
||||||
|
cas d’exposition prolongée<em> – voir Les Règles, « Froid et chaleur »</em>,
|
||||||
|
page 99.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198972
|
||||||
|
modifiedTime: 1785775198972
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!DHv0PXsIgQ8rPPUY'
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Résistance aux toxines
|
||||||
|
type: evolution
|
||||||
|
_id: IMXQRepTNtiHZcaf
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>La Virulence des poisons et toxines qui affectent le personnage est
|
||||||
|
réduite de 1 – voir Les Règles, « Venins et poisons », page 107. Le
|
||||||
|
personnage peut en outre sélectionner une famille animale ou végétale
|
||||||
|
(guêpes, araignées, champignons, méduses, batraciens…) en accord avec le
|
||||||
|
meneur. Les toxines sécrétées par cette famille voient leur Virulence
|
||||||
|
réduite de 3.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198973
|
||||||
|
modifiedTime: 1785775198973
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!IMXQRepTNtiHZcaf'
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Résistance
|
||||||
|
type: evolution
|
||||||
|
_id: nxy53EF4ZRSSwCvr
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage ne subit pas de Malus d’Épuisement à ses Actions physiques
|
||||||
|
et manuelles quand sa Réserve d’Effort est vide. Si ses deux Réserves sont
|
||||||
|
vides, il subit néanmoins le Malus normal de 2D à toutes ses Actions.</p>
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198972
|
||||||
|
modifiedTime: 1785775198972
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!nxy53EF4ZRSSwCvr'
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Santé de fer
|
||||||
|
type: evolution
|
||||||
|
_id: uneyg1G0Gpbb7l5P
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Lorsque le personnage est blessé, les Malus de récupération à la Réserve
|
||||||
|
d’Effort ne se cumulent pas, seule la Blessure la plus grave est prise en
|
||||||
|
compte. Le coût en Dés de Réserves pour réduire une Blessure est en outre
|
||||||
|
réduit de 2D.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198973
|
||||||
|
modifiedTime: 1785775198973
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!uneyg1G0Gpbb7l5P'
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Silencieux
|
||||||
|
type: evolution
|
||||||
|
_id: mxzf2Od6SMlvhKon
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage anticipe chaque mouvement qui pourrait le faire repérer. Il
|
||||||
|
bénéficie d’un Bonus de 1D sur toutes ses Actions liées à la discrétion
|
||||||
|
lorsqu’il est seul.</p>
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198973
|
||||||
|
modifiedTime: 1785775198973
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!mxzf2Od6SMlvhKon'
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Sommeil réparateur
|
||||||
|
type: evolution
|
||||||
|
_id: 71goz10chR7bPQZY
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage n’a besoin de dormir que quatre heures par nuit. Il
|
||||||
|
récupère toujours la totalité de ses Réserves s’il a pu bénéficier de quatre
|
||||||
|
heures de repos ininterrompu, même dans des conditions difficiles.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198973
|
||||||
|
modifiedTime: 1785775198973
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!71goz10chR7bPQZY'
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Surhumain
|
||||||
|
type: evolution
|
||||||
|
_id: haijrxkf8tAxmWx5
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le joueur sélectionne une de ses Caractéristiques. Il augmente son score
|
||||||
|
de 1D et peut atteindre 4D. Quelle que soit la Caractéristique choisie, le
|
||||||
|
personnage dégage une aura étrange, difficilement identifiable, mais à
|
||||||
|
laquelle personne n’est indifférent.</p>
|
||||||
|
level:
|
||||||
|
value: 4
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198974
|
||||||
|
modifiedTime: 1785775198974
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!haijrxkf8tAxmWx5'
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Sympathie
|
||||||
|
type: evolution
|
||||||
|
_id: qdtjHLeC7pKh57tP
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage dégage des phéromones qui rendent les personnes qui le
|
||||||
|
côtoient au quotidien plus compatissantes envers lui. Lorsqu’il souhaite
|
||||||
|
dépenser des Dés de la Réserve de Groupe, il a le droit de voter avec les
|
||||||
|
autres membres du Groupe.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198974
|
||||||
|
modifiedTime: 1785775198974
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!qdtjHLeC7pKh57tP'
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Toucher empoisonné
|
||||||
|
type: evolution
|
||||||
|
_id: VIPHgGAqSklGFhFh
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage sécrète par la paume de ses mains un poison
|
||||||
|
particulièrement dangereux. S’il entre en contact avec la peau d’un humain
|
||||||
|
ou d’un animal (ce qui, en combat, nécessite une attaque au Corps à corps
|
||||||
|
qui passe la Protection de la cible), il peut dépenser un ou plusieurs Dés
|
||||||
|
de ses deux Réserves pour sécréter du poison. Ce dernier a une Virulence de
|
||||||
|
5 + le nombre de Dés d’Efforts dépensés – voir Les Règles, « Venins et
|
||||||
|
poisons », page 107. La victime subira alors une Blessure Légère
|
||||||
|
immédiatement, et une autre par minute pour chaque Dé de Sang-Froid dépensé.
|
||||||
|
Chaque Réussite au jet de Santé réalisé par la victime pour résister au
|
||||||
|
poison réduit la durée de l’empoisonnement d’une minute.</p>
|
||||||
|
level:
|
||||||
|
value: 4
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198974
|
||||||
|
modifiedTime: 1785775198974
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!VIPHgGAqSklGFhFh'
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Vigilance
|
||||||
|
type: evolution
|
||||||
|
_id: ZcvIaqqfjvy67qWx
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage est habitué à ne dormir que d’un oeil et à réagir au
|
||||||
|
moindre bruit ou mouvement suspect. Il n’est jamais considéré comme Passif
|
||||||
|
et gagne une Réussite supplémentaire sur tous ses jets réussis impliquant la
|
||||||
|
Compétence Vigilance.</p>
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198974
|
||||||
|
modifiedTime: 1785775198974
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!ZcvIaqqfjvy67qWx'
|
||||||
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Vision nocturne
|
||||||
|
type: evolution
|
||||||
|
_id: noIASNejaUkv0Qo3
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Les yeux du personnage se rapprochent de ceux d’un félin. Une faible
|
||||||
|
lumière, comme celle des étoiles, lui suffit à voir très clairement. En
|
||||||
|
revanche, quand la luminosité est si faible, il ne perçoit que très peu les
|
||||||
|
nuances de couleur. Tant qu’une infime source de lumière est disponible, le
|
||||||
|
manque de luminosité ne lui inflige aucun Malus aux jets de Perception basés
|
||||||
|
sur la vue. Dans le noir complet, cependant, il est aussi aveugle que
|
||||||
|
quiconque. Lorsque ses yeux percent les ténèbres, ils reflètent la lumière,
|
||||||
|
ce qui peut le rendre visible de loin et lui donner une apparence
|
||||||
|
troublante.</p>
|
||||||
|
level:
|
||||||
|
value: 3
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198974
|
||||||
|
modifiedTime: 1785775198974
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!noIASNejaUkv0Qo3'
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Volonté de fer
|
||||||
|
type: evolution
|
||||||
|
_id: flRHah4HO4JQ4DaM
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage bénéficie d’un Bonus de 1D sur tous ses jets liés au moral,
|
||||||
|
au stress ou à la détermination.</p>
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198975
|
||||||
|
modifiedTime: 1785775198975
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!flRHah4HO4JQ4DaM'
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
name: Adaptation
|
||||||
|
sorting: a
|
||||||
|
folder: null
|
||||||
|
type: Item
|
||||||
|
_id: WTyHT2h0fsSnUHFT
|
||||||
|
description: ''
|
||||||
|
sort: 100000
|
||||||
|
color: null
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: Folder.WTyHT2h0fsSnUHFT
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198946
|
||||||
|
modifiedTime: 1785775198993
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!folders!WTyHT2h0fsSnUHFT'
|
||||||
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
folder: 965pjqqbhRQqxXE4
|
||||||
|
name: Adaptabilité
|
||||||
|
type: ability
|
||||||
|
_id: zfeNmyLcOyfD4AxI
|
||||||
|
img: systems/vermine2047/assets/icons/items/ability.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Lorsque le personnage utilise une Compétence qu’il ne possède pas, il
|
||||||
|
ignore son Handicap de Rareté, comme s’il s’agissait d’une Compétence
|
||||||
|
basique.</p>
|
||||||
|
type: character
|
||||||
|
totem: loner
|
||||||
|
learn:
|
||||||
|
threshold: 5
|
||||||
|
hindrance: 0
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 3
|
||||||
|
effects: []
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775498760
|
||||||
|
modifiedTime: 1785775498760
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!zfeNmyLcOyfD4AxI'
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
folder: BJAxv25PiSk29dK0
|
||||||
|
name: Adaptation
|
||||||
|
type: ability
|
||||||
|
_id: CQeFYqMRYMFa6pAa
|
||||||
|
img: systems/vermine2047/assets/icons/items/ability.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage peut se lier avec une espèce animale ou végétale. Il
|
||||||
|
devient immunisé aux poisons ou venins de cette espèce. S’il s’agit
|
||||||
|
d’animaux, il peut apaiser des individus de cette espèce comme s’il
|
||||||
|
possédait la Capacité « Apaisement » – voir page 99. S’il l’a acquise, un
|
||||||
|
jet réussi permet de les rendre amicaux et il peut être accompagné d’une
|
||||||
|
nuée d’animaux de Taille 0 ou d’un petit groupe de Taille 1 qui le
|
||||||
|
défendront de leur mieux. S’il s’agit de végétaux, il bénéficie des effets
|
||||||
|
de la Capacité « Berceau » comme s’il la possédait – voir page 99. S’il l’a
|
||||||
|
acquise, les effets sont doublés (Bonus de 2D et coûts de guérison diminués
|
||||||
|
de 2D) et le personnage est capable de les faire pousser en quelques
|
||||||
|
heures.</p>
|
||||||
|
type: character
|
||||||
|
totem: symbiote
|
||||||
|
learn:
|
||||||
|
threshold: 5
|
||||||
|
hindrance: 0
|
||||||
|
level:
|
||||||
|
value: 3
|
||||||
|
min: 1
|
||||||
|
max: 3
|
||||||
|
effects: []
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775498762
|
||||||
|
modifiedTime: 1785775498762
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!CQeFYqMRYMFa6pAa'
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
folder: K27yLKAXwiIQTzfW
|
||||||
|
name: Alpha
|
||||||
|
type: ability
|
||||||
|
_id: NOBtH3xQJPptBU6I
|
||||||
|
img: systems/vermine2047/assets/icons/items/ability.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le Groupe laisse l’un des siens combattre l’un des membres d’un groupe
|
||||||
|
adverse. Il désigne un personnage comme son champion (en recourant au vote
|
||||||
|
au besoin). Ce dernier affronte l’adversaire de son choix, pendant que tous
|
||||||
|
les autres protagonistes doivent assister à ce duel singulier sans
|
||||||
|
intervenir. Les créatures sans aucun intellect ne sont pas affectées. Cette
|
||||||
|
Capacité ne fonctionne qu’en présence d’au moins la moitié du Groupe.</p>
|
||||||
|
type: group
|
||||||
|
totem: predator
|
||||||
|
learn:
|
||||||
|
threshold: 5
|
||||||
|
hindrance: 0
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 3
|
||||||
|
effects: []
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775496862
|
||||||
|
modifiedTime: 1785775496862
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!NOBtH3xQJPptBU6I'
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
folder: 84EDVIIqk9YkrBgF
|
||||||
|
name: Analyse
|
||||||
|
type: ability
|
||||||
|
_id: Vggeykg6wQGTaR2o
|
||||||
|
img: systems/vermine2047/assets/icons/items/ability.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage étudie une zone ou un objet pour savoir ce qui s’y est
|
||||||
|
passé ou ce à quoi il a servi. Il peut poser une question au meneur, avant
|
||||||
|
d’effectuer un jet de Perception + Armurerie / Artisanat / Mécanique /
|
||||||
|
Technologie (en fonction de l’objet). Le tableau ci-dessous indique la
|
||||||
|
Difficulté en fonction de la période à laquelle remontent les événements.
|
||||||
|
Pour chaque Réussite, le personnage obtient une information.</p>
|
||||||
|
type: character
|
||||||
|
totem: builder
|
||||||
|
learn:
|
||||||
|
threshold: 5
|
||||||
|
hindrance: 0
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 3
|
||||||
|
effects: []
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775498746
|
||||||
|
modifiedTime: 1785775498746
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!Vggeykg6wQGTaR2o'
|
||||||
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
folder: BJAxv25PiSk29dK0
|
||||||
|
name: Apaisement
|
||||||
|
type: ability
|
||||||
|
_id: X1RckrwGCbG7IQVR
|
||||||
|
img: systems/vermine2047/assets/icons/items/ability.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage peut apaiser un animal ou une créature solitaire. Il lui
|
||||||
|
faudra réussir un jet d’Empathie + Animalisme dont la Difficulté dépend du
|
||||||
|
degré d’agressivité de la créature, déterminé par le meneur. La taille est
|
||||||
|
appliquée en Handicap. S’il s’agit d’une espèce incapable de communiquer
|
||||||
|
avec l’homme, le jet aura deux degrés de Handicap supplémentaires. Si le jet
|
||||||
|
est réussi, l’état d’esprit de l’animal ou de la créature s’améliore d’un
|
||||||
|
cran. Il ne deviendra pas amical d’un coup.</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
|
||||||
|
<li>En Mode <strong>Cauchemar</strong>, le Handicap ne sera que de (I), peu
|
||||||
|
importe la taille de la créature.</li>
|
||||||
|
|
||||||
|
<li>En Mode <strong>Apocalypse</strong>, la Capacité peut également affecter
|
||||||
|
une meute ou une nuée.</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
type: character
|
||||||
|
totem: symbiote
|
||||||
|
learn:
|
||||||
|
threshold: 5
|
||||||
|
hindrance: 0
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 3
|
||||||
|
effects: []
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775498763
|
||||||
|
modifiedTime: 1785775498763
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!X1RckrwGCbG7IQVR'
|
||||||
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
folder: X6lFKdHexGbMpMHh
|
||||||
|
name: Appât du gain
|
||||||
|
type: ability
|
||||||
|
_id: 1UA2GqAD1OXwuAZx
|
||||||
|
img: systems/vermine2047/assets/icons/items/ability.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Les personnages tirent leur force de leurs trouvailles. Chaque découverte
|
||||||
|
est pour eux l’occasion de se réjouir. Après chaque scène où le Groupe a mis
|
||||||
|
la main sur des ressources ou du matériel de Rareté supérieur ou égale à 7
|
||||||
|
sans avoir recours à une Capacité, ses membres regagnent 1D dans chacune de
|
||||||
|
leurs réserves, y compris celle de Groupe. S’ils trouvent un objet de Rareté
|
||||||
|
supérieure ou égale à 9, le gain est de 2D par Réserve, et le Groupe gagne
|
||||||
|
1D d’expérience de Découverte.</p>
|
||||||
|
type: group
|
||||||
|
totem: scavenger
|
||||||
|
learn:
|
||||||
|
threshold: 5
|
||||||
|
hindrance: 0
|
||||||
|
level:
|
||||||
|
value: 3
|
||||||
|
min: 1
|
||||||
|
max: 3
|
||||||
|
effects: []
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775496847
|
||||||
|
modifiedTime: 1785775496847
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!1UA2GqAD1OXwuAZx'
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
folder: xtfO1fRGXZx3bm2j
|
||||||
|
name: Apprentissage commun
|
||||||
|
type: ability
|
||||||
|
_id: dDIHKXaimA3aaOdO
|
||||||
|
img: systems/vermine2047/assets/icons/items/ability.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Les personnages apprennent au contact les uns des autres : les Dés
|
||||||
|
d’Expérience de Groupe peuvent bénéficier à tous les personnages pour leurs
|
||||||
|
jets individuels sur un apprentissage commun, moyennant la dépense de 1D de
|
||||||
|
Réserve de Groupe par personnage impliqué dans l’apprentissage.</p>
|
||||||
|
type: group
|
||||||
|
totem: symbiote
|
||||||
|
learn:
|
||||||
|
threshold: 5
|
||||||
|
hindrance: 0
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 3
|
||||||
|
effects: []
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775496872
|
||||||
|
modifiedTime: 1785775496872
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!dDIHKXaimA3aaOdO'
|
||||||
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
folder: YdwBSZ49OdVFRGIW
|
||||||
|
name: Architecture d'antan
|
||||||
|
type: ability
|
||||||
|
_id: vXAgLueVVeORjEzO
|
||||||
|
img: systems/vermine2047/assets/icons/items/ability.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le Groupe érige une structure de bric et de broc aussi solide que si elle
|
||||||
|
était construite en matériaux de l’ancien temps. Tant que les personnages
|
||||||
|
disposent de quoi monter un abri – moyennant quelques Actions de fouille –,
|
||||||
|
le meneur considérera qu’il s’agit d’une construction en dur, pour tout ce
|
||||||
|
qui est repos, protection des intempéries, couvert face aux tirs…
|
||||||
|
L’intérieur de la construction reste cependant vide et si les personnages
|
||||||
|
doivent dormir, ils le feront à même le sol – ou dans les sacs de couchage
|
||||||
|
dont ils disposent dans leur équipement ou sur des couches qu’ils auront
|
||||||
|
fabriquées par ailleurs.</p>
|
||||||
|
type: group
|
||||||
|
totem: builder
|
||||||
|
learn:
|
||||||
|
threshold: 5
|
||||||
|
hindrance: 0
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 3
|
||||||
|
effects: []
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775496840
|
||||||
|
modifiedTime: 1785775496840
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!vXAgLueVVeORjEzO'
|
||||||
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
folder: QIrB0DXlWitQf77h
|
||||||
|
name: Attitude positive
|
||||||
|
type: ability
|
||||||
|
_id: 37V8cS5XqgodzRYB
|
||||||
|
img: systems/vermine2047/assets/icons/items/ability.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>La coopération entre membres du Groupe est inébranlable et se manifeste
|
||||||
|
dans toutes les situations possibles. Les événements négatifs font baisser
|
||||||
|
la Réserve de Groupe de 1D de moins que la normale – voir Les Règles, « La
|
||||||
|
Réserve de Groupe » page 21.</p>
|
||||||
|
|
||||||
|
<p>En Mode <strong>Apocalypse</strong>, les événements négatifs font baisser
|
||||||
|
la Réserve de Groupe de 2D de moins que la normale.</p>
|
||||||
|
type: group
|
||||||
|
totem: hive
|
||||||
|
learn:
|
||||||
|
threshold: 5
|
||||||
|
hindrance: 0
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 3
|
||||||
|
effects: []
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775496825
|
||||||
|
modifiedTime: 1785775496825
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!37V8cS5XqgodzRYB'
|
||||||
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
folder: dgcQogTYNhmMOwbG
|
||||||
|
name: Autorité
|
||||||
|
type: ability
|
||||||
|
_id: aHRsfu7k2kEQusni
|
||||||
|
img: systems/vermine2047/assets/icons/items/ability.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Une fois par jour, le personnage peut effectuer un jet de Volonté +
|
||||||
|
Psychologie contre une Difficulté de 7 pour exalter, fédérer ou recadrer ses
|
||||||
|
compagnons. Tous les personnages présents regagnent 1D de Réserve de
|
||||||
|
Sang-Froid par Réussite obtenue. Si au moins la moitié du Groupe est réunie,
|
||||||
|
le Groupe regagne également le même nombre de Dés de Réserve de Groupe.</p>
|
||||||
|
type: character
|
||||||
|
totem: hive
|
||||||
|
learn:
|
||||||
|
threshold: 5
|
||||||
|
hindrance: 0
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 3
|
||||||
|
effects: []
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775498741
|
||||||
|
modifiedTime: 1785775498741
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!aHRsfu7k2kEQusni'
|
||||||
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
folder: X6lFKdHexGbMpMHh
|
||||||
|
name: Bagout du roublard
|
||||||
|
type: ability
|
||||||
|
_id: svA8aIY2ACliNr4f
|
||||||
|
img: systems/vermine2047/assets/icons/items/ability.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Deux membres du Groupe travaillant de concert pour baratiner autrui ont
|
||||||
|
bien plus de chances de réussir : ils additionnent leurs Niveaux de
|
||||||
|
Compétence pour constituer leurs Groupements de dés. Pour cela, considérer
|
||||||
|
le Niveau Débutant équivalent à un Niveau 1, Confirmé 2, Expert 3, Maître 4
|
||||||
|
et Légende 5. Cette Capacité est limitée à un Niveau final de Maître.</p>
|
||||||
|
|
||||||
|
<p>En Mode <strong>Apocalypse</strong>, il est possible de dépasser le
|
||||||
|
Niveau Maître, avec un total de 5 correspondant à un Bonus de 3D et 2
|
||||||
|
Relances, et un total de 6 à un Bonus de 3D et 3 Relances.</p>
|
||||||
|
type: group
|
||||||
|
totem: scavenger
|
||||||
|
learn:
|
||||||
|
threshold: 5
|
||||||
|
hindrance: 0
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 3
|
||||||
|
effects: []
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775496848
|
||||||
|
modifiedTime: 1785775496848
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!svA8aIY2ACliNr4f'
|
||||||
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
folder: BJAxv25PiSk29dK0
|
||||||
|
name: Berceau
|
||||||
|
type: ability
|
||||||
|
_id: uzUrffNWChsgFiEm
|
||||||
|
img: systems/vermine2047/assets/icons/items/ability.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage sait se faire accepter par l’environnement naturel. Il
|
||||||
|
bénéficie d’un Bonus de 1D pour se cacher dans la nature ou y trouver de la
|
||||||
|
nourriture ou de l’eau. Il peut faire bénéficier ses compagnons de ce Bonus
|
||||||
|
du moment qu’ils ne dégradent rien. Le coût en Dés de Réserves pour la
|
||||||
|
guérison est diminué de 1D (seulement pour lui).</p>
|
||||||
|
type: character
|
||||||
|
totem: symbiote
|
||||||
|
learn:
|
||||||
|
threshold: 5
|
||||||
|
hindrance: 0
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 3
|
||||||
|
effects: []
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775498763
|
||||||
|
modifiedTime: 1785775498763
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!uzUrffNWChsgFiEm'
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user