23 Commits
Author SHA1 Message Date
uberwald cf905cdcfb feat: fiche PNJ (profil + niveaux) et passage du véhicule en acteur
Release Creation / build (release) Failing after 1m17s
2026-08-04 20:34:20 +02:00
uberwald ea3322e843 feat: fiche véhicule (caractéristiques) et capacités de totem de groupe
Release Creation / build (release) Failing after 1m24s
- véhicule : fiabilité 1-10, type véhicule/monture, distance, vitesse (croisière/pointe), entretien, réserve d'effort
- nettoyage du tableau d'entretien à la soumission
- fiche de groupe : section capacités de totem (création type totem + liste)
2026-08-04 17:20:37 +02:00
uberwald 6247f65590 feat: fiche de Communauté (socle)
- acteur community (identité, géographie, histoire, objectifs, taille/effectif)
- 4 Domaines x 2 jauges avec états (Crise/Bas/Stable/Opulent), moral, réserves
- onglets Identité / Domaines / Ressources, édition ProseMirror (formInput)
- header avec fond, totem, niveau, effectif ; icône par défaut
- libellé du type via typeLabels (sans écraser TYPES.* du core)
2026-08-04 17:20:29 +02:00
uberwald 8d8428cad3 feat: gérer les armes des créatures (morsure, griffes, piqûre…)
- gestion des armes dans l'onglet Combat des créatures (équipement + liste)
- dégâts de l'arme équipée utilisés à la place de computed.damage (+ Vigueur+)
- correction du libellé Vigueur+ (ADVERSITY.vigor au lieu de VERMINE.vigor)
2026-08-04 14:25:27 +02:00
uberwald 9cf7d9ac68 feat: système d'expérience et transferts de dés d'expérience
Release Creation / build (release) Failing after 1m20s
- phase d'expérience (récompenses collectives et individuelles, objectifs, traumatisme)
- jets d'apprentissage (compétences, spécialités, caractéristiques, réserves, évolution)
- bonus de réserve acquis par apprentissage intégré au max dérivé
- transferts de dés individuels <-> groupe dans le dialogue de jet
- onglet expérience sur la fiche personnage
- guide utilisateur combat (docs/user/COMBAT.md)
2026-08-04 14:14:09 +02:00
uberwald f2c8cfe246 feat: équipement, échanges de coups et améliorations des fiches
- équipement des armes et protections (champ equipped, liste des équipés, toggle)
- échanges de coups automatisés (attaque/défense/résolution/blessures dans le chat)
- dialogue d'attaque par type (personnage/PNJ/créature), portées et difficultés
- conservation du scroll des fiches lors des modifs (option scrollable + classe active)
- verrouillage des compétences PNJ en mode jeu
- corrections review combat (défenseur, difficulté >10, multi-cibles, permissions)
2026-08-04 14:14:03 +02:00
uberwald 31f2f4530c feat: améliorer les fiches (tests PNJ/créature, message de jet, compétences, créatures)
Release Creation / build (release) Failing after 1m23s
2026-08-04 00:49:37 +02:00
uberwald 9c7ebc6df9 Sync compendiums
Release Creation / build (release) Failing after 1m24s
2026-08-03 21:26:03 +02:00
uberwald 05f668acf0 ci: compiler les compendiums lors de la release
Le workflow Gitea zippait packs/ sans recompiler les LevelDB depuis les
sources YAML (gitignorées en CI). Ajoute npm run pullYAMLtoLDB entre le
build CSS et le zip pour embarquer des compendiums à jour dans la release.
2026-08-03 20:27:27 +02:00
uberwald 10b01614e6 chore: regénérer les compendiums depuis le monde
Extraction complète des packs vers src/packs/*.yml après mise à jour des
compendiums dans le monde. Ajoute les nouveaux items et rites, met à
jour le contenu existant, et renomme le pack Historiques en 'Historiques
& Création'.
2026-08-03 18:49:06 +02:00
uberwald 89d9d71395 style: ajuster la mise en page des fiches
Release Creation / build (release) Failing after 1m51s
Réduit la taille des polices d'en-tête et du logo, retravaille la
disposition des totems, et double la hauteur des zones Notes et
Relations dans l'onglet Histoire.
2026-08-03 15:59:57 +02:00
uberwald 1e298ed809 fix: éviter le double affichage des dés 3D
Le message de jet n'attache plus la liste des dés : Dice So Nice
réaffichait le jet via son hook updateChatMessage en plus de l'appel
explicite à showDiceSoNice, provoquant une double animation.
2026-08-03 15:59:51 +02:00
uberwald 4ffaebf52b feat: lancer les armes via la boîte de dialogue avec compétence
Ajoute un champ compétence au modèle arme, un sélecteur dans la fiche
d'arme et ouvre la boîte de dialogue de jet pré-remplie au clic sur
l'icône dé (réservé aux acteurs disposant d'aptitudes/compétences).
2026-08-03 15:59:47 +02:00
uberwald de7691c4dc fix: transmettre le handicap au message de jet (succès requis) 2026-08-01 21:35:48 +02:00
uberwald c87b25fbaf fix: aligner les jets et la boîte de dialogue avec les règles 2026-08-01 21:33:38 +02:00
uberwald 6ce51ed8cf fix: réparer les contrôles du combat tracker et la pause (v14) 2026-08-01 21:33:38 +02:00
uberwald df852f17d1 fix: aligner le JS avec l'API Foundry v14
- effects.mjs: remove e._getSourceName() (removed in v14, now a getter)
- fight.mjs: convert VermineCombatTracker from AppV1 (get template/getData)
  to AppV2 PARTS + _prepareContext/_prepareTurnContext
- fight.mjs: VermineCombat.rollInitiative v14 options-object signature
- hooks.mjs: renderPause -> renderGamePause; getSceneControlButtons now
  uses controls.tokens and tools record (v14 names/structure)
- roll.mjs: ChatMessage.create uses rolls array (v14 removed 'roll' key)
- item.mjs: getRollData returns system when no actor (v14 expects object)
- vermine2047.mjs: correct unregisterSheet API for ActorSheetV2
- base-item-sheet.mjs: call super._onRender
2026-08-01 21:33:38 +02:00
uberwald 8916600bf6 chore: ignorer le répertoire local .swival/ 2026-08-01 21:33:38 +02:00
uberwald 0189f62734 fix: importer foundry.applications.api et utiliser _onRender dans les pickers AppV2
- applications.mjs: import HandlebarsApplicationMixin/ApplicationV2 from
  foundry.applications.api (they are not globals in Foundry v14), fixing
  'ReferenceError: HandlebarsApplicationMixin is not defined'
- Replace activateListeners(html) with _onRender(context, options) and
  this.element — AppV2 does not call activateListeners
- TraitSelector: remove recursive render(true) in _validateTraits
- fight.mjs: VermineCombatTracker same _onRender conversion
2026-08-01 21:33:38 +02:00
uberwald 29b6a98c89 Fix dummy bug
Release Creation / build (release) Failing after 1m46s
2026-07-17 22:09:53 +02:00
uberwald b1d73e6b08 chore: remove CHANGELOG.md, add LICENSE.md (CC BY-NC-SA 4.0) 2026-07-12 22:32:48 +02:00
uberwald 509a5efd9d Actualiser README.md 2026-07-12 22:31:15 +02:00
uberwald fea7c1c271 docs: translate README to French with English summary 2026-07-12 22:30:16 +02:00
579 changed files with 27115 additions and 5409 deletions
+4
View File
@@ -36,6 +36,10 @@ jobs:
- name: Build CSS
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
- run: |
apt update -y
+3
View File
@@ -19,6 +19,9 @@ foundry
# Ancien dossier SCSS (archivé)
scss_old/
# Outils d'audit local
.swival/
# Fichiers CSS générés
css/vermine2047.*.css
!css/vermine2047.css
-141
View File
@@ -1,141 +0,0 @@
# CHANGELOG - Vermine2047 System
## 0.1.14 - 2026-06-04
### 🚀 Nouveautés
#### Système
- **Mise à jour de la compatibilité**: Support officiel de FoundryVTT v14 (tout en maintenant la compatibilité v11-v12)
- **Nouvelle classe GroupLink**: Gestion complète des liens bidirectionnels entre acteurs et groupes
- Synchronisation automatique des membres et rencontres
- Hooks pour la création, mise à jour et suppression d'acteurs
- Méthodes utilitaires pour gérer les relations
#### Configuration
- **Domaines des totems**: Ajout de `CONFIG.VERMINE.totemDomains` avec les domaines d'influence pour chaque totem
- **Configurations étendues**: Ajout des configurations pour PNJ et créatures
- `npcThreatLevels`, `npcExperienceLevels`, `npcRoleLevels`
- `creaturePatternLevels`, `creatureSizeLevels`, `creatureRoleLevels`, `creaturePackLevels`
#### Fiches
- **Fiche PNJ**: Remplacement des inputs numériques par des sélecteurs pour menace, expérience et rôle
- **Fiche Créature**: Remplacement des inputs numériques par des sélecteurs pour gabarit, taille, rôle et meute
- **Ajout du champ encounters**: Les personnages peuvent maintenant appartenir à des groupes
#### Jets de dés
- **Redesign complet du RollDialog**: Interface plus compacte et organisée
- Utilisation de `<details>`/`<summary>` pour une meilleure organisation
- Affichage du total du pool de dés en temps réel
- Sélecteur pour choisir quel totem garder (humain ou adapté)
- Affichage des bonus/malus par domaine de totem
- **Bonus/malus par domaine**: Implémentation des bonus de totem basés sur le domaine de prédilection
- Bonus: +1 dé si le domaine de prédilection est dans les domaines du totem
- Malus: -1 dé si le domaine de prédilection est dans les domaines du totem opposé
- **Réussites automatiques**: Implémentation des réussites automatiques basées sur le niveau de maîtrise
- Niveau 2 + spécialité: +1 réussite automatique
- Niveau 3: +1 réussite automatique
- Niveau 4 + spécialité: +2 réussites automatiques
- Niveau 5: +2 réussites automatiques
- **Seuils automatiques**: Implémentation des seuils automatiques pour les compétences non maîtrisées
- Niveau 0 (Incompétent): seuil = 9
- Niveau 1 (Débutant): seuil = 7
- Niveau >= 2: utilise la difficulté spécifiée
#### Items
- **Correction des templates de chat cards**: Tous les templates sont maintenant en `.hbs`
- **Chat cards améliorées**: Affichage plus complet des informations pour chaque type d'item
- Armes: dégâts, type, portée, munitions
- Protections: niveau, mobilité, bouclier
- Capacités: type, totem, niveau, effets
- etc.
#### Traductions
- Ajout de nombreuses nouvelles traductions pour les nouvelles fonctionnalités
- Correction des traductions existantes
#### Documentation
- **Documentation technique complète**: `docs/technical/ARCHITECTURE.md`
- Structure du projet
- Configuration du système
- Architecture des documents
- Système de dés
- Système de combat
- Gestion des groupes
- Bonnes pratiques de développement
### 🐛 Corrections
- Correction des références de templates (`.html``.hbs`)
- Correction des erreurs dans les templates de chat cards
- Amélioration de la gestion des totems dans les rolls
- Nettoyage du code et suppression des logs de débogage
### 📝 Modifications mineures
- Mise à jour des métadonnées du système dans `system.json`
- Ajout du champ `encounters` au template des personnages
- Amélioration des helpers Handlebars avec de nouveaux helpers pour les configurations PNJ/Créature
---
## 0.1.13
- ajout des historiques
- ajout des rites
## 0.1.12
- passage des packs en LevelDB
## 0.1.11
- passage à une compatibilité v11
## 0.1.10
- révision du template évolution
- révision du template traumatisme
- ajout du type rite
- test import de pack
## 0.1.9
- versions condensées des méthodes (@kristov)
- renommage des templates
- image actors par défaut
- image des créatures
- ajout du totem et de l'apprentissage dans les capacités
## 0.1.8
- refactoring sheet vers actor
## 0.1.7
- sang-froid, effort, et santé
## 0.1.6
- finition de l'onglet totem
- ajout du logo de totem dans la sidebar
- ajout de traductions manquantes
## 0.1.5
- début de mise en forme des feuilles créature et pnj
- possibilité de changer le type de capacité (pour ajouter des capacités de totem)
---
## Notes de migration
### Pour les utilisateurs
1. **Compatibilité**: Le système est maintenant compatible avec FoundryVTT v14
2. **Nouveaux champs**: Les personnages ont maintenant un champ `encounters` pour gérer leurs groupes
3. **RollDialog**: L'interface du dialogue de jet a été complètement redessinée pour être plus intuitive
4. **Bonus de totem**: Les bonus de domaine sont maintenant automatiquement appliqués
### Pour les développeurs
1. **GroupLink**: Utilisez la classe GroupLink pour gérer les relations entre acteurs et groupes
2. **Nouveaux helpers**: De nombreux nouveaux helpers Handlebars ont été ajoutés pour les configurations PNJ/Créature
3. **CONFIG.VERMINE**: De nombreuses nouvelles configurations ont été ajoutées
---
*Généré le 2026-06-04*
+7
View File
@@ -0,0 +1,7 @@
Code Author :
- Developed by LeRatierBretonnien / www.uberwald.me
Code LICENCE :
C BY-NC-SA 4.0
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
This license requires that reusers give credit to the creator. It allows reusers to distribute, remix, adapt, and build upon the material in any medium or format, for noncommercial purposes only. If others modify or adapt the material, they must license the modified material under identical terms.
+87 -71
View File
@@ -1,51 +1,53 @@
# Vermine 2047 — Foundry VTT System
# Vermine 2047 — Système Foundry VTT
![Foundry v14](https://img.shields.io/badge/Foundry-v14-blue)
![Version](https://img.shields.io/badge/version-0.1.13-green)
Unofficial game system for **Vermine 2047**, a French post-apocalyptic tabletop RPG by **Éditions John Doe** (https://www.kickandgo.net/boutique-agate/category.php?cl=UyQcRrFtYcYrAnLbOfDj&idp=20).
Système non-officiel pour le jeu de rôle **Vermine 2047**, univers post-apo français par **Studio Agate**.
Bilingual **FR/EN** interface.
► Achetez le jeu : https://www.kickandgo.net/boutique-agate/category.php?cl=UyQcRrFtYcYrAnLbOfDj&idp=20
Interface bilingue **FR/EN**.
---
## Actor Types
## Types d'Acteurs
| Type | Sheet | Description |
| Type | Fiche | Description |
|------|-------|-------------|
| **character** | AppV2 | Player character with skills, abilities, totem, equipment |
| **npc** | AppV2 | Non-player character with role/threat, wound thresholds |
| **group** | AppV2 | Group with members, encounters, link sync |
| **creature** | AppV2 | Creature with template/size/role, custom attack stats |
| **character** | AppV2 | Personnage joueur — compétences, capacités, totem, équipement |
| **npc** | AppV2 | PNJ avec rôle/menace, seuils de blessure |
| **group** | AppV2 | Groupe avec membres, rencontres, synchro liée |
| **creature** | AppV2 | Créature avec gabarit/taille/rôle, attaques custom |
## Item Types
## Types d'Objets
| Type | Use |
|------|-----|
| **item** | Generic items (gear, resources) |
| **weapon** | Weapons with damage, range, reliability, rarity |
| **defense** | Armor/shields with protection, coverage |
| **vehicle** | Vehicles with speed, structure, weapons |
| **ability** | Special abilities (apprenticeship, domain) |
| **specialty** | Skill specializations (attached to skills) |
| **background** | Character backgrounds |
| **trauma** | Mental/physical trauma |
| **evolution** | Character evolutions |
| **rumor** | Rumors (group) |
| **target** | Targets (group) |
| **rite** | Rites (magic/rituals) |
| Type | Utilisation |
|------|-------------|
| **item** | Objets génériques (matériel, ressources) |
| **weapon** | Armes — dégâts, portée, fiabilité, rareté |
| **defense** | Armures/boucliers — protection, couverture |
| **vehicle** | Véhicules — vitesse, structure, armement |
| **ability** | Capacités spéciales (apprentissage, domaine) |
| **specialty** | Spécialités de compétence |
| **background** | Historiques de personnage |
| **trauma** | Traumatismes physiques/mentaux |
| **evolution** | Évolutions du personnage |
| **rumor** | Rumeurs (groupe) |
| **target** | Cibles (groupe) |
| **rite** | Rites (magie/rituels) |
## Dice System
## Système de Dés
- **d10-based success counting**: `Xd10cs>=N`
- Difficulty = number of successes required
- Totems give domain bonuses (human/adapted dice)
- Rerolls via Effort and Sang-froid (Composure)
- Automatic successes and failure thresholds
- **Dice So Nice!** 3D dice integration with custom d10 face images
- **Comptage de succès sur d10** : `Xd10cs>=N`
- Difficulté = nombre de succès requis
- Les totems donnent des bonus de domaine (dés humains/adaptés)
- Relances via l'Effort et le Sang-froid
- Succès automatiques et seuils d'échec
- Intégration **Dice So Nice!** avec faces de d10 personnalisées
### Initiative
Formula: `(@abilities.reflexes.value + @skills.alertness.value)d10cs>=@combatStatus.difficulty`
Formule : `(@abilities.reflexes.value + @skills.alertness.value)d10cs>=@combatStatus.difficulty`
---
@@ -53,68 +55,82 @@ Formula: `(@abilities.reflexes.value + @skills.alertness.value)d10cs>=@combatSta
```
module/
├── vermine2047.mjs # Entry point (Hooks.once init)
├── vermine2047.mjs # Point d'entrée (Hooks.once init)
├── models/ # DataModels (Foundry v2 TypeDataModel)
│ ├── actor/ # character.mjs, npc.mjs, group.mjs, creature.mjs
│ └── item/ # All 12 item types
│ └── item/ # Les 12 types d'objets
├── documents/ # VermineActor, VermineItem
├── applications/
│ └── sheets/ # AppV2 sheets per type
│ └── sheets/ # Fiches AppV2 par type
├── system/
│ ├── config.mjs # CONFIG.VERMINE (totems, traits, skills…)
│ ├── config.mjs # CONFIG.VERMINE (totems, traits, compétences…)
│ ├── roll.mjs # VermineUtils.roll()
│ ├── fight.mjs # Combat system, VermineCombat, VermineCombatant
│ ├── functions.mjs # Shared helpers (skill scores, etc.)
│ ├── hooks.mjs # Hook registrations (DiceSoNice, chat, pause…)
│ ├── dice3d.mjs # Dice So Nice colors, initUserDice
│ ├── fight.mjs # Système de combat, VermineCombat, VermineCombatant
│ ├── functions.mjs # Fonctions partagées
│ ├── hooks.mjs # Hooks (DiceSoNice, chat, pause…)
│ ├── dice3d.mjs # Couleurs Dice So Nice, initUserDice
│ ├── dialogs/rollDialog.mjs
│ ├── settings.mjs # System settings (game-mode, etc.)
│ ├── tour.mjs # Tours (stub)
│ └── group-link.mjs # GroupLink: auto-sync actors↔groups
│ ├── settings.mjs # Paramètres du système (mode de jeu…)
│ ├── tour.mjs # Visites guidées (stub)
│ └── group-link.mjs # GroupLink : synchro acteurs↔groupes
templates/
├── actor/appv2/ # AppV2 actor sheet templates
├── actor/ # Legacy templates (coexisting)
└── item/ # Item sheet templates
├── actor/appv2/ # Templates AppV2 des fiches acteurs
├── actor/ # Templates legacy (coexistence)
└── item/ # Templates des fiches objets
css/
└── vermine2047.min.css # Compiled from LESS
└── vermine2047.min.css # Compilé depuis LESS
less/
└── base.less # Main stylesheet (LESS source)
└── base.less # Source LESS principale
```
## Key Objects
## Objets Clés
- `game.vermine2047``{ VermineUtils, VermineCombat, GroupLink }`
- `CONFIG.VERMINE` → totems, traits, skills, domains, combat statuses
- `CONFIG.ui.combat` custom `VermineCombatTracker`
- `CONFIG.VERMINE` → totems, traits, compétences, domaines, états de combat
- `CONFIG.ui.combat``VermineCombatTracker` personnalisé
## Commands
## Commandes
```sh
npm run build:less # LESS → css/vermine2047.min.css
npm run build:less:dev # LESS → css/vermine2047.dev.css (unminified)
npm run build:all-css # Both
npm run build:less:dev # LESS → css/vermine2047.dev.css (non minifié)
npm run build:all-css # Les deux
npm run watch # Gulp watch + browser-sync → localhost:30000
npm run lint:less # LESS lint
npm run pushLDBtoYAML # Extract packs → src/packs/*.yml
npm run pullYAMLtoLDB # Compile packs → packs/* (LevelDB)
make build|watch|lint # Makefile wrappers
npm run lint:less # Vérification LESS
npm run pushLDBtoYAML # Extraire les packs → src/packs/*.yml
npm run pullYAMLtoLDB # Compiler les packs → packs/* (LevelDB)
make build|watch|lint # Raccourcis Makefile
```
### Requirements
- Node.js `v16.15.1` (see `.nvmrc`)
### Prérequis
- Node.js `v16.15.1` (cf `.nvmrc`)
- Foundry VTT **v14**
## Development Notes
## Notes de Développement
- **DataModels before Document classes**: In `init` hook, register `CONFIG.Actor.dataModels.*` and `CONFIG.Item.dataModels.*` **before** setting `CONFIG.Actor.documentClass` / `CONFIG.Item.documentClass`.
- **Two template systems**: AppV2 (`templates/actor/appv2/`) and legacy (`templates/actor/*.hbs`) coexist — don't confuse them.
- **Packs**: Edit YAML source in `src/packs/*.yml`, then run `pullYAMLtoLDB` to compile to LevelDB.
- **CSS**: Declared in `system.json` styles — do **not** inject `<link>` manually.
- **ESLint**: ~350 rules. Run `npx eslint module/` before committing. No `console.log` (use `console.error` sparingly).
- **editMode**: Not auto-set on actor creation (causes ActiveEffect errors). Enable manually via sheet checkbox.
- **GroupLink**: Auto-syncs on actor update. Changes propagate bidirectionally.
- **HotReload**: Enabled for css, scss, hbs, json in `system.json`.
- **DataModels avant Document classes** : Dans le hook `init`, enregistrer `CONFIG.Actor.dataModels.*` et `CONFIG.Item.dataModels.*` **avant** de définir `CONFIG.Actor.documentClass` / `CONFIG.Item.documentClass`.
- **Deux systèmes de templates** : AppV2 (`templates/actor/appv2/`) et legacy (`templates/actor/*.hbs`) coexistent.
- **Packs** : Éditer les sources YAML dans `src/packs/*.yml`, puis lancer `pullYAMLtoLDB`.
- **CSS** : Déclaré dans `system.json` — ne **pas** injecter de `<link>` manuellement.
- **ESLint** : ~350 règles. Lancer `npx eslint module/` avant chaque commit. Pas de `console.log`.
- **editMode** : Ne se met plus automatiquement à la création d'acteur (cause des erreurs ActiveEffect). Activer manuellement via la case à cocher.
- **GroupLink** : Synchronisation automatique bidirectionnelle acteurs↔groupes.
- **HotReload** : Activé pour css, scss, hbs, json dans `system.json`.
## License
---
This system is an unofficial fan project. The Vermine 2047 setting and trademarks belong to Éditions John Doe.
## English Summary
**Vermine 2047** is a French post-apocalyptic TTRPG. This is an unofficial Foundry VTT system (v14).
- **4 actor types**: character, npc, group, creature
- **12 item types**: item, weapon, defense, vehicle, ability, specialty, background, trauma, evolution, rumor, target, rite
- **Dice**: d10 success-counting with totem domains, Effort rerolls, Dice So Nice! 3D integration
- **Sheets**: AppV2 with tab navigation, edit/play mode toggle
- **Bilingual**: FR/EN interface
---
## Licence
Projet fan non-officiel. L'univers et les marques Vermine 2047 appartiennent aux Éditions John Doe.
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

+34
View File
@@ -0,0 +1,34 @@
# vermine2047 Audit Findings
Security audit of `vermine2047` at commit `b1d73e6b08bf`. Each finding links to a detailed write-up (`NNN-*.md`) and a proposed patch (`NNN-*.patch`).
## Summary
**Total findings: 0**
## Findings
## Dropped in adjudication
These findings reproduced in Phase 4 but the Phase 4.5 gate judged them false positives or out of a realistic threat model.
- **Unpinned mutable CI action enabling supply chain hijacking** (high, .gitea/workflows/release.yaml): Malformed URL in `uses` breaks workflow parsing; step is unreachable and prevents execution entirely.; Relies on improbable upstream compromise; standard configuration gap rather than immediate vulnerability.
## Run details
- run id: `aa4595a1`
- commit: `b1d73e6b08bf7ed0157a03137cc3bb50c8d3a26c`
- branch: `main`
- scope: 214 file(s)
- files triaged: 214
- files escalated: 14
- files deep-reviewed: 14
- findings proposed: 1
- findings verified: 0
- findings discarded: 0
- findings dropped in adjudication: 1
- artifacts: 0 written, 0 failed, 0 pending
## How to read this directory
Each finding has a paired `NNN-<slug>.md` (the human report) and `NNN-<slug>.patch` (the proposed fix as a unified diff). The report carries the narrative; the patch is the suggested change. Regenerate a single finding's artifacts with `/audit --regen --finding N`.
+126
View File
@@ -0,0 +1,126 @@
# Combat — Échanges de coups (assisté)
## Principe
Chaque échange de coups se déroule dans le chat : un jet d'attaque, un éventuel
jet de défense, puis une résolution assistée. Tous les calculs (réussites,
dégâts, protection, blessure) sont effectués automatiquement ; la blessure
s'applique ensuite d'un simple clic.
```
Attaque → (Défense) → Résoudre → Appliquer
```
## 1. L'attaque
Ouvrir le dialogue d'attaque :
- **Personnage / PNJ** : cliquer l'icône cible (`fa-crosshairs`) sur une arme
dans l'onglet **Combat** (liste des armes équipées) ou dans l'onglet
**Matériel**.
- **Créature** : cliquer la valeur « Attaque » de l'onglet **Stats**.
Le dialogue est prérempli selon le type d'acteur :
| Type | Pool | Dégâts de base | Difficulté |
|------|------|----------------|------------|
| **Personnage** | Caractéristique + compétence (verrouillées) | Arme (+ Vigueur si « Vigueur+ ») | Selon maîtrise / portée |
| **PNJ** | Pool de menace + réserves de rôle | Arme équipée, sinon `Vigueur` de menace | Selon niveau d'expérience |
| **Créature** | `Attaque` calculée | `Dégâts` calculés | Son statut de combat (verrouillée) |
### Difficulté
- **Contact** : dépend du niveau de compétence de l'attaquant.
- Incompétent / Débutant : `7`
- Confirmé : `5 ou 7`
- Expert : `5, 7 ou 9`
- Maître / Légende : `3, 5, 7 ou 9`
- **Distance** : par portée.
- Courte : `5` — Moyenne : `7` — Longue : `9`
- **Créature ciblée** : la difficulté est forcée au statut de combat de la
créature (toute réussite ≥ 1 touche).
### Options du dialogue
- **Deuxième action** : +2 à la difficulté.
- **Handicap** : augmente les succès requis.
- **Sang-Froid** : dés bonus (débités du personnage).
- **Totems** : dés de totem (+ garder un totem si les deux sont actifs).
- **Consommation de munitions** : pour les armes à distance (coche cochée par
défaut, la munition est décomptée au jet).
- Les **cibles** sélectionnées sur la carte (`Ctrl` + clic / outil de ciblage)
sont affichées et reprises dans l'échange.
### La carte de chat
Le message d'attaque affiche l'arme (dégâts, portées, munitions) puis le bloc
**Échange de coups** avec, **par cible**, les boutons **Esquive** et **Parade**
(contact) ou **Esquive** seule (distance), ainsi que le bouton **Résoudre**.
## 2. La défense
Seuls les acteurs **capables de se défendre** (personnages et PNJ) ont des
boutons de défense — pas les créatures (défense passive).
- Cliquer **Esquive** ou **Parade** sur la carte d'attaque.
- Le dialogue de défense est **verrouillé** :
- **Esquive** : `Réflexes` + `Corps-à-corps` (attaque au contact) ou
`Vigilance` (attaque à distance).
- **Parade** : `Vigueur` + `Mêlée` (ou `Corps-à-corps`).
- **Difficulté = celle de l'attaque** (non modifiable).
- Le jet de défense est lié au message d'attaque (même échange).
> Permission : Esquive / Parade ne sont utilisables que par le **propriétaire
> de la cible** ou le **MJ**.
## 3. La résolution
Cliquer **Résoudre** (auteur du message ou MJ). Le système compare les
**réussites** des jets d'attaque et de défense :
- **Attaquant > Défenseur** → **Touché** (parade insuffisante si parade).
- **Attaquant ≤ Défenseur** → **Esquivé / Paré** : aucun dégât.
- **Sans défense** (abstention ou créature) : toute réussite ≥ 1 touche.
Les totaux sont lus en direct : une **relance** de dé effectuée avant la
résolution est prise en compte.
### Dégâts
```
Dégâts = dégâts de base de l'arme (+ Vigueur si « Vigueur+ ») + réussites d'attaque
```
### Protection
- **Parade** : seule la protection utilisée pour la parade réduit.
- **Sinon** : somme des **protections équipées** de la cible
(créature : protection de rôle).
### Blessure
La blessure correspond au **seuil le plus élevé** inférieur ou égal aux dégâts
nets : **Légère**, **Grave** ou **Mortelle**. Un bloc de résultat est affiché
**par cible** (attaque multi-cibles = un bloc par cible).
## 4. L'application de la blessure
Cliquer **« Appliquer la blessure »** sur le bloc de résultat (propriétaire de
la cible ou MJ) : le cercle de blessure correspondant est coché sur l'acteur.
## Rappels et permissions
| Action | Qui peut la faire |
|--------|-------------------|
| Esquive / Parade | Propriétaire de la cible ou MJ |
| Résoudre | Auteur du message d'attaque ou MJ |
| Appliquer la blessure | Propriétaire de la cible ou MJ |
- Les **créatures** ne lancent pas de défense : leur difficulté d'attaque et de
défense est leur **statut de combat**.
- Les **PNJ** lancent leur défense comme un personnage (capacités + compétences).
- Les **réserves de rôle** des PNJ sont à dépenser à la main (gérées par le MJ).
---
*Guide utilisateur — Vermine2047 pour FoundryVTT.*
+229 -4
View File
@@ -16,7 +16,7 @@
"TYPES.Item.item": "Item",
"TYPES.Item.weapon": "Weapon",
"TYPES.Item.defense": "Defense",
"TYPES.Item.vehicle": "Vehicle",
"TYPES.Actor.vehicle": "Vehicle",
"TYPES.Item.ability": "Ability",
"TYPES.Item.specialty": "Specialty",
"TYPES.Item.background": "Background",
@@ -145,8 +145,12 @@
},
"pool": "Reserve",
"pools": "Reserves",
"dice_pool": "Dice pool",
"auto_successes": "automatic successes",
"self_control": "Self-control",
"effort": "Effort",
"equip": "Equip",
"unequip": "Unequip",
"group": "Group",
"abilities": "Characteristics",
"ability": "Characteristic",
@@ -172,6 +176,18 @@
"range": "Range",
"ranges": "Ranges",
"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_sm": "Rar.",
"reliability": "Reliability",
@@ -202,6 +218,8 @@
"type_character": "Character",
"type_npc": "NPC",
"type_creature": "Creature",
"type_community": "Community",
"type_vehicle": "Vehicle",
"type": "Type",
"group_members": "Members",
"encounters": "Encounters",
@@ -223,6 +241,7 @@
"world": "World"
},
"CharacterNamePlaceholder": "Character Name",
"ProfilePlaceholder": "Profile",
"none": "None",
"bonuses": "Bonuses",
"choose_ability": "Choose Ability",
@@ -240,6 +259,29 @@
"success_count": "Success Count",
"success_required": "Success Required",
"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",
"totem_dice": "Totem Dice",
"totem_hint": "Totem Hint",
@@ -257,12 +299,109 @@
"error_no_actor_selected": "No actor selected",
"error_not_enough_self_control": "Not enough self control",
"error_select_ability": "Select an ability",
"attack": "Attack",
"attack_contact": "Melee attack",
"attack_ranged": "Ranged attack",
"defense_esquive": "Dodge",
"defense_parade": "Parry",
"passive_defense": "Passive defense",
"range_short": "Short",
"range_mid": "Medium",
"range_long": "Long",
"second_action": "Second action (+2 difficulty)",
"consume_ammo": "Consume ammo",
"npc_pool": "Pools used",
"exchange": "Combat exchange",
"resolve": "Resolve",
"resolve_result": "Exchange resolution",
"attacker_successes": "Attack successes",
"defender_successes": "Defense successes",
"raw_damage": "Raw damage",
"net_damage": "Net damage",
"hit": "Hit",
"hit_parade": "Hit (insufficient parry)",
"missed_esquive": "Dodged",
"missed_parade": "Parried",
"no_damage": "No damage",
"wound": "Wound",
"no_wound": "No wound",
"apply_wound": "Apply wound",
"wound_applied": "Wound applied",
"error_creature_no_defense": "Creatures cannot defend (passive defense)",
"error_no_permission": "Insufficient permissions",
"error_no_target": "No target selected for this exchange",
"experience_phase": "Experience phase",
"experience_dice": "Experience dice",
"experience_rewards": "Experience rewards",
"collective": "collective",
"collective_rewards": "Collective rewards",
"individual_rewards": "Individual rewards",
"collective_group": "Group (collective pool)",
"danger": "Danger",
"discoveries": "Discoveries",
"duration": "Duration",
"implication": "Implication",
"influence": "Influence",
"interpretation": "Interpretation",
"reward_anecdotic": "Anecdotal",
"reward_minor": "Minor",
"reward_major": "Major",
"reward_exceptionnal": "Exceptional",
"objective_minor": "Minor objective",
"objective_major": "Major objective",
"trauma_reward": "Chosen trauma",
"trauma_reward_5": "Trauma reward (+5D)",
"evolution_dice": "Evolution dice",
"open_experience_phase": "Experience phase",
"open_learning": "Learning",
"learning_limits": "Learning limits (per phase)",
"learned_skill": "Skill",
"learned_specialty": "Specialty",
"learned_reserve": "Reserve",
"learned_characteristic": "Characteristic",
"learned_evolution": "Evolution",
"no_character": "No character in this world.",
"apply": "Apply",
"characteristic": "Characteristic",
"learn_skill": "Skill",
"learn_specialty": "Specialty",
"learn_characteristic": "Characteristic",
"learn_reserve": "Reserve",
"learn_evolution": "Evolution die",
"learn_specialty_name": "Specialty name",
"specialty_hint": "Prerequisite: skill at least Proficient (≥2).",
"characteristic_hint": "Difficulty 9 + Handicap = current value. Max 3 (except Superhuman).",
"reserve_hint": "Difficulty = current value + age Handicap. Max 10.",
"evolution_hint": "Difficulty by age + Handicap = accumulated Evolution dice. Adapted totem +1D, Human -1D.",
"dice_spent": "Dice spent",
"learning_success": "Learning successful!",
"error_no_experience_dice": "No Experience dice available.",
"error_learning_limit": "Limit reached for this type of learning in this phase.",
"error_skill_max": "This skill is already at maximum level.",
"error_characteristic_max": "Characteristic already at maximum (3).",
"error_specialty_requirement": "Specialty requires: skill at least Proficient (≥2).",
"error_reserve_max": "Reserve already at maximum (10).",
"experience_transfer": "Experience dice",
"experience_pull": "Pull from group",
"experience_give": "Give 1D to group",
"pool_experience": "Experience",
"error_no_group": "No group associated with this character.",
"error_no_group_dice": "Not enough group pool dice.",
"tabs": {
"abilities": "Abilities",
"combat": "Combat",
"equipment": "Equipment",
"stories": "Stories",
"totem": "Totem"
"totem": "Totem",
"experience": "Experience"
},
"Sheet": {
"character": "Character",
"npc": "NPC",
"group": "Group",
"creature": "Creature",
"community": "Community",
"vehicle": "Vehicle"
},
"base_values": "Base Values",
"computed_values": "Computed Values",
@@ -326,6 +465,91 @@
"effects.passive": "Passive",
"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": {
"item": "Item",
"items": "Items",
@@ -573,7 +797,7 @@
"name": "Repulsion"
},
"tracks": {
"name": "Tracks"
"name": "Tracking"
},
"crafting": {
"name": "Crafting"
@@ -603,7 +827,7 @@
"name": "Melee"
},
"archery": {
"name": "Ranged"
"name": "Ranged Weapons"
},
"throwing": {
"name": "Throwing"
@@ -655,6 +879,7 @@
"Round": "Round"
},
"SIZE_LEVELS": {
"tiny": "Insect",
"small": "Small",
"medium": "Medium",
"large": "Large"
+225 -6
View File
@@ -8,7 +8,7 @@
"TYPES.Item.item": "Objet",
"TYPES.Item.weapon": "Arme",
"TYPES.Item.defense": "Protection",
"TYPES.Item.vehicle": "Véhicule",
"TYPES.Actor.vehicle": "Véhicule",
"TYPES.Item.ability": "Capacité",
"TYPES.Item.specialty": "Spécialité",
"TYPES.Item.background": "Historique",
@@ -93,7 +93,7 @@
"AGE_TYPES": {
"young": "Jeune",
"adult": "Adulte",
"old": "Vieux"
"old": "Ancien"
},
"IDENTITY": {
"biography": "Biographie",
@@ -152,8 +152,12 @@
"pool": "Réserve",
"pools": "Réserves",
"reserves": "Réserves",
"dice_pool": "Pool de dés",
"auto_successes": "réussites automatiques",
"self_control": "Sang-Froid",
"effort": "Effort",
"equip": "Équiper",
"unequip": "Déséquiper",
"preferred_category": "Catégorie préférée",
"wounds": {
"name": "Blessures",
@@ -188,6 +192,29 @@
"grant_reroll": "accorder des relances",
"success_count": "nombre de succès",
"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_cannot_reroll": "Vous ne pouvez pas relancer un dés sur ce jet",
"error_no_rerolls_left": "Plus de relances possibles",
@@ -230,6 +257,18 @@
"range": "Portée",
"ranges": "Portées",
"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_sm": "Rar.",
"reliability": "Fiabilité",
@@ -244,6 +283,94 @@
"totem_dice": "Dés de totem",
"keep_totem": "Garder le totem",
"totem_hint": "Cochez pour utiliser les dés de totem (double réussite possible)",
"attack": "Attaque",
"attack_contact": "Attaque au contact",
"attack_ranged": "Attaque à distance",
"defense_esquive": "Esquive",
"defense_parade": "Parade",
"passive_defense": "Défense passive",
"range_short": "Courte",
"range_mid": "Moyenne",
"range_long": "Longue",
"second_action": "Deuxième action (+2 difficulté)",
"consume_ammo": "Consommer des munitions",
"npc_pool": "Réserves utilisées",
"exchange": "Échange de coups",
"resolve": "Résoudre",
"resolve_result": "Résolution de l'échange",
"attacker_successes": "Réussites d'attaque",
"defender_successes": "Réussites de défense",
"raw_damage": "Dégâts bruts",
"net_damage": "Dégâts nets",
"hit": "Touché",
"hit_parade": "Touché (parade insuffisante)",
"missed_esquive": "Esquivé",
"missed_parade": "Paré",
"no_damage": "Aucun dégât",
"wound": "Blessure",
"no_wound": "Aucune blessure",
"apply_wound": "Appliquer la blessure",
"wound_applied": "Blessure appliquée",
"error_creature_no_defense": "Les créatures ne peuvent pas se défendre (défense passive)",
"error_no_permission": "Permissions insuffisantes",
"error_no_target": "Aucune cible sélectionnée pour cet échange",
"experience_phase": "Phase d'Expérience",
"experience_dice": "Dés d'Expérience",
"experience_rewards": "Récompenses d'Expérience",
"collective": "collectifs",
"collective_rewards": "Récompenses collectives",
"individual_rewards": "Récompenses individuelles",
"collective_group": "Groupe (réserve collective)",
"danger": "Danger",
"discoveries": "Découvertes",
"duration": "Durée",
"implication": "Implication",
"influence": "Influence",
"interpretation": "Interprétation",
"reward_anecdotic": "Anecdotique",
"reward_minor": "Mineure",
"reward_major": "Majeure",
"reward_exceptionnal": "Exceptionnelle",
"objective_minor": "Objectif mineur",
"objective_major": "Objectif majeur",
"trauma_reward": "Traumatisme choisi",
"trauma_reward_5": "Récompense de Traumatisme (+5D)",
"evolution_dice": "Dés d'Évolution",
"open_experience_phase": "Phase d'Expérience",
"open_learning": "Apprentissage",
"learning_limits": "Limites d'apprentissage (par phase)",
"learned_skill": "Compétence",
"learned_specialty": "Spécialité",
"learned_reserve": "Réserve",
"learned_characteristic": "Caractéristique",
"learned_evolution": "Évolution",
"no_character": "Aucun personnage dans ce monde.",
"apply": "Appliquer",
"characteristic": "Caractéristique",
"learn_skill": "Compétence",
"learn_specialty": "Spécialité",
"learn_characteristic": "Caractéristique",
"learn_reserve": "Réserve",
"learn_evolution": "Dé d'Évolution",
"learn_specialty_name": "Nom de la spécialité",
"specialty_hint": "Prérequis : compétence au moins Confirmée (≥2).",
"characteristic_hint": "Difficulté 9 + Handicap = valeur actuelle. Max 3 (Surhumain excepté).",
"reserve_hint": "Difficulté = valeur actuelle + Handicap d'âge. Max 10.",
"evolution_hint": "Difficulté selon l'âge + Handicap = Dés d'Évolution accumulés. Totem Adapté +1D, Humain -1D.",
"dice_spent": "Dés dépensés",
"learning_success": "Apprentissage réussi !",
"error_no_experience_dice": "Pas de Dés d'Expérience disponibles.",
"error_learning_limit": "Limite atteinte pour ce type d'apprentissage dans cette phase.",
"error_skill_max": "Cette compétence est déjà au niveau maximal.",
"error_characteristic_max": "Caractéristique déjà au maximum (3).",
"error_specialty_requirement": "Spécialité requise : compétence au moins Confirmée (≥2).",
"error_reserve_max": "Réserve déjà au maximum (10).",
"experience_transfer": "Dés d'Expérience",
"experience_pull": "Puiser du Groupe",
"experience_give": "Donner 1D au Groupe",
"pool_experience": "Expérience",
"error_no_group": "Aucun Groupe associé à ce personnage.",
"error_no_group_dice": "Réserve de Groupe insuffisante.",
"error_select_skill": "Veuillez sélectionner une compétence",
"needed": "nécessaire",
"cost": "Coût",
@@ -259,7 +386,8 @@
"totem": "Totem et ajustements",
"combat": "Combat et reserves",
"equipment": "Matériel",
"stories": "Histoire"
"stories": "Histoire",
"experience": "Expérience"
},
"ammo": "Munitions",
"ammo_sm": "Mun",
@@ -281,6 +409,8 @@
"type_character": "Personnage",
"type_npc": "PNJ",
"type_creature": "Créature",
"type_community": "Communauté",
"type_vehicle": "Véhicule",
"type": "Type",
"group_members": "Membres",
"encounters": "Rencontres",
@@ -314,6 +444,7 @@
"total_damage": "Dégâts totaux",
"wound_thresholds": "Seuils de blessures",
"CharacterNamePlaceholder": "Nom du personnage",
"ProfilePlaceholder": "Profil",
"sexes": {
"female": "Féminin",
"male": "Masculin"
@@ -325,7 +456,9 @@
"character": "Personnage",
"npc": "PNJ",
"group": "Groupe",
"creature": "Créature"
"creature": "Créature",
"community": "Communauté",
"vehicle": "Véhicule"
}
},
"UI": {
@@ -343,6 +476,91 @@
"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": {
"item": "Objet",
"items": "Objets",
@@ -432,6 +650,7 @@
"major": "Majeur"
},
"SIZE_LEVELS": {
"tiny": "Insecte",
"small": "Petit",
"medium": "Moyen",
"large": "Grand"
@@ -595,7 +814,7 @@
"name": "Répulsion"
},
"tracks": {
"name": "Pistage"
"name": "Traces"
},
"crafting": {
"name": "Artisanat"
@@ -616,7 +835,7 @@
"name": "Armes à feu"
},
"archery": {
"name": "Tir à l'arc"
"name": "Armes de Tir"
},
"armory": {
"name": "Armurerie"
+41 -12
View File
@@ -31,7 +31,7 @@
font-size: 0.9rem;
}
.char-name-value {
font-size: 1.5rem;
font-size: 1.1rem;
}
input:not([type="radio"]):not([type="checkbox"]) {
font-size: 1rem;
@@ -106,6 +106,10 @@
overflow-y: auto;
padding: 8px;
border-right: 1px solid rgba(255, 255, 255, 0.1);
> * {
flex: 0 0 auto;
}
}
> nav.tabs {
@@ -291,7 +295,7 @@
.chooseTotem {
font-family: "DistressBlack", sans-serif;
font-size: 0.9rem;
font-size: 0.8rem;
cursor: pointer;
&:hover {
@@ -331,21 +335,39 @@
}
.padding-with-frieze {
margin-left: 18% !important;
margin-right: 10% !important;
padding: 0.25rem;
li {
max-width: 100%;
}
.major-totem {
position: relative;
.resource-label {
h4 {
position: absolute;
transform: rotate(-8deg);
opacity: 1;
transition: 0.2s;
font-size: 1em;
}
}
.major-totem {
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 {
margin: 0;
font-size: 1.1em;
}
.img-totem {
width: 64px;
height: 64px;
filter: drop-shadow(0px 0px 10px rgb(0, 0, 0));
}
}
}
@@ -377,7 +399,7 @@
}
.char-vermine2047 {
font-size: 1.5rem;
font-size: 1.1rem;
}
}
@@ -409,6 +431,13 @@
}
}
// ── Onglet Histoire : doubler la hauteur des zones Notes et Relations ──
.tab.stories {
prose-mirror.editor {
min-height: 150px;
}
}
nav.tabs[data-group="sheet"] {
display: inline-flex;
justify-content: space-around;
+152
View File
@@ -0,0 +1,152 @@
@import "../variables";
@import "../utilities";
.system-vermine2047 .vermine2047.actor.community {
.char-header {
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
max-height: 130px;
.community-portrait {
display: flex;
align-items: center;
justify-content: center;
}
.char-name,
.char-vermine2047 {
border-bottom: none;
line-height: 2rem;
}
.char-vermine2047 {
font-size: 1.1rem;
}
.profile,
.origin,
.level,
.population {
width: 100%;
}
input[type="text"] {
flex: 1;
min-width: 0;
}
.level input,
.population input {
width: 60px;
text-align: center;
}
}
.community-section {
padding: 8px;
border: 1px solid @color-border-dark-4;
background: fade(@color-chitin-dark, 20%);
h3 {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-13;
color: @theme-color-dark;
margin: 0 0 8px;
text-align: left;
border-bottom: none;
}
}
.community-linked-group {
display: flex;
gap: 6px;
margin-bottom: 8px;
padding: 6px 8px;
border: 1px dashed @color-hemolymph;
background: fade(@color-hemolymph, 8%);
.label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
color: @color-amber;
}
}
.community-size-info {
margin: 6px 0 0;
font-size: @font-size-11;
color: @color-text-light-0;
opacity: 0.85;
}
.community-warning {
margin: 6px 0 0;
padding: 4px 8px;
font-size: @font-size-11;
font-weight: 700;
color: @color-hemolymph;
background: fade(@color-hemolymph, 12%);
border: 1px solid @color-hemolymph;
}
.community-hint {
margin: 0;
font-size: @font-size-11;
font-style: italic;
color: @color-text-light-0;
opacity: 0.7;
}
// Jauges de Domaines
.gauge {
display: flex;
flex-direction: column;
gap: 2px;
padding: 6px;
border: 1px solid @color-border-dark-4;
border-left: 4px solid @color-border-dark-4;
label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
color: @color-amber;
}
input[type="number"] {
width: 48px;
text-align: center;
padding: 2px;
}
.gauge-value {
font-family: "DistressBlack", sans-serif;
font-size: @font-size-16;
color: @color-text-light-0;
}
.gauge-state {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
font-weight: 700;
text-transform: uppercase;
}
&.crisis { border-left-color: @color-hemolymph; .gauge-state { color: @color-hemolymph; } }
&.low { border-left-color: @color-amber; .gauge-state { color: @color-amber; } }
&.stable { border-left-color: @color-acid-green; .gauge-state { color: @color-acid-green; } }
&.opulent { border-left-color: @theme-color-accent; .gauge-state { color: @theme-color-accent; } }
}
.morale-level {
font-family: "Roboto", sans-serif;
font-size: @font-size-12;
font-weight: 700;
&.high { color: @color-acid-green; }
&.normal { color: @color-text-light-0; }
&.low { color: @color-amber; }
&.crisis { color: @color-hemolymph; }
}
}
+65 -94
View File
@@ -30,77 +30,70 @@
}
}
.skills-container {
display: flex;
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;
align-items: center;
gap: 8px;
padding: 5px;
border-bottom: 1px solid @color-border-dark-3;
&:last-child { border-bottom: none; }
label {
flex: 1;
.npc-trait-list {
li {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 0;
border-bottom: 1px solid @color-border-dark-3;
font-size: @font-size-12;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:last-child { border-bottom: none; }
strong { flex: 1; }
}
.trait-value {
min-width: 30px;
text-align: center;
font-weight: bold;
white-space: nowrap;
}
}
.npc-roll-btn {
color: @color-text-light-primary;
opacity: 0.7;
transition: opacity 0.15s ease;
&:hover {
box-shadow: 0 0 10px @theme-color-highlight inset;
opacity: 1;
color: @color-text-light-1;
text-shadow: 0 0 6px @theme-color-highlight;
}
}
.skill-control {
display: flex;
align-items: center;
gap: 5px;
.npc-wounds {
flex-wrap: wrap;
.wound-cat {
display: inline-flex;
align-items: center;
gap: 4px;
font-weight: bold;
}
.wound-boxes {
display: inline-flex;
gap: 2px;
i {
font-size: 8px;
color: @color-border-dark-2;
}
}
}
.skill-value {
min-width: 25px;
text-align: center;
font-weight: bold;
}
.npc-free-skills {
margin-top: 15px;
.form-group-style();
.rarity-badge {
display: inline-block;
padding: 1px 4px;
border-radius: 3px;
font-size: 10px;
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 {
margin-bottom: 15px;
.card-style();
label {
font-size: @font-size-12;
font-weight: bold;
white-space: nowrap;
}
}
.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 {
padding: 10px;
overflow-y: auto;
@@ -213,6 +173,17 @@
padding: 3px 5px;
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 {
+38 -29
View File
@@ -19,17 +19,39 @@
}
div.minor-totems {
position: relative;
display: flex;
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);
h3 {
text-align: center;
margin: 0;
font-size: 1.2rem;
line-height: 1.1;
}
.totem-row {
display: flex;
justify-content: space-between;
align-items: center;
}
h5 {
position: absolute;
top: 0;
margin: 0;
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
font-size: 0.8rem;
img {
max-width: 2rem;
position: absolute;
bottom: -2rem;
max-width: 1.5rem;
height: auto;
}
&.human, &.adapted {
@@ -53,44 +75,31 @@
}
.totem-dice {
display: flex;
justify-content: space-between;
align-items: center;
.human-dice,
.adapted-dice {
display: flex;
flex-direction: row;
margin-left: 2rem;
align-items: center;
}
.human-dice i,
.adapted-dice i {
padding-top: 0.5rem;
color: @totem-human-color;
position: relative;
z-index: 0;
pointer-events: none;
}
.adapted-dice {
justify-content: flex-end;
margin-left: 0;
margin-right: 2rem;
transform: rotate(180deg);
i {
transform: rotate(180deg);
color: @totem-adapted-color;
}
}
.adapted-dice i {
transform: rotate(180deg);
padding-top: 0.5rem;
color: @totem-adapted-color;
}
}
.human {
left: 0;
img { left: 0; }
}
.adapted {
right: 0;
img { right: 0; }
}
}
}
+172
View File
@@ -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
View File
@@ -44,6 +44,24 @@ body.system-vermine2047.theme-dark {
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 {
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 {
&.smb { margin-bottom: 0.25rem; }
&.mdb { margin-bottom: 0.5rem; }
@@ -113,6 +147,29 @@ ul.unstyled {
.rollable {
.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 {
@@ -420,7 +477,7 @@ body.system-vermine2047 img#logo {
}
.item-name {
padding-left: 5px;
padding: 0 2px 0 5px;
text-align: left;
}
}
@@ -674,6 +731,296 @@ ol.chat-log {
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 {
@@ -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 {
.group-content,
.npc-content,
.creature-content {
.creature-content,
.community-content {
padding: 15px;
}
}
// ── Onglet Expérience ─────────────────────────────────────────────────
.tab.experience {
.experience-summary {
display: flex;
gap: 10px;
margin-bottom: 10px;
.exp-stat {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding: 8px;
border: 1px solid @color-border-dark-4;
background: fade(@color-chitin-dark, 30%);
label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
color: @color-amber;
}
.exp-value {
font-family: "DistressBlack", sans-serif;
font-size: @font-size-24;
color: @color-acid-green;
}
}
}
.exp-actions {
gap: 8px;
margin-bottom: 10px;
button {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-12;
padding: 5px 14px;
cursor: pointer;
color: @color-text-light-0;
background: fade(@color-hemolymph, 20%);
border: 1px solid @color-hemolymph;
border-radius: 3px;
.transition();
&:hover {
color: @theme-color-dark;
background: @color-hemolymph;
}
}
}
.exp-rewards {
margin-bottom: 10px;
padding: 8px;
border: 1px solid @color-border-dark-4;
background: fade(@color-chitin-dark, 20%);
h4 {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-12;
color: @theme-color-dark;
margin: 0 0 6px;
.exp-total {
color: @color-amber;
}
}
.exp-reward-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
font-size: @font-size-12;
label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
color: @color-amber;
}
}
.exp-learned {
display: flex;
flex-wrap: wrap;
gap: 6px;
span {
padding: 2px 8px;
font-size: @font-size-11;
border: 1px solid @color-border-dark-4;
border-radius: 3px;
opacity: 0.6;
&.learned {
opacity: 1;
color: @color-acid-green;
border-color: @color-acid-green;
}
}
}
}
}
+248
View File
@@ -399,6 +399,254 @@ iframe {
}
}
// ── Combat dialog (attaque) ──────────────────────────────────────
.combat-mode-label {
margin: 0;
text-align: center;
font-size: @font-size-14;
color: @color-hemolymph;
text-transform: uppercase;
}
.locked-value {
flex: 1;
display: flex;
align-items: center;
min-height: 28px;
padding: 0 8px;
font-size: @font-size-12;
color: @color-chitin-dark;
background: @color-text-light-0;
border-radius: 3px;
.custom-select-style();
}
.combat-dialog-weapon {
border: 1px solid @color-hemolymph;
padding: 6px 8px;
background: fade(@color-chitin-dark, 40%);
.weapon-block-header {
align-items: center;
gap: 6px;
.weapon-icon { color: @color-hemolymph; }
.weapon-name {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-12;
font-weight: 900;
color: @theme-color-dark;
}
}
.weapon-details {
gap: 4px 12px;
flex-wrap: wrap;
.weapon-detail {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
color: @color-text-light-0;
strong { text-transform: uppercase; }
}
}
}
.combat-difficulty-options {
flex-wrap: wrap;
gap: 8px;
.difficulty-option {
display: flex;
align-items: center;
gap: 4px;
font-family: "Roboto", sans-serif;
text-transform: none;
font-size: @font-size-12;
color: @color-text-light-0;
cursor: pointer;
input[type="radio"] { flex: none; }
&.locked {
opacity: 0.8;
font-style: italic;
}
}
}
.combat-targets {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
padding: 4px 2px;
.label {
font-size: @font-size-11;
color: @color-amber;
}
.combat-target-tag {
padding: 2px 8px;
font-size: @font-size-11;
font-family: "Roboto", sans-serif;
color: @color-text-light-0;
background: fade(@color-hemolymph, 20%);
border: 1px solid fade(@color-hemolymph, 50%);
border-radius: 3px;
}
}
// ── Phase d'Expérience ───────────────────────────────────────────
.exp-phase-characters {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
min-height: 0;
details > div:not(summary) {
padding: 8px;
background: fade(@color-chitin-dark, 30%);
}
}
// ── Dialogue d'apprentissage ─────────────────────────────────────
.learning-summary {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border: 1px solid @color-hemolymph;
background: fade(@color-hemolymph, 10%);
.label {
font-size: @font-size-12;
color: @color-amber;
}
.exp-value {
font-family: "DistressBlack", sans-serif;
font-size: @font-size-20;
color: @color-acid-green;
}
}
.learning-type {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 6px 2px;
label {
display: flex;
align-items: center;
gap: 4px;
font-family: "Roboto", sans-serif;
text-transform: none;
font-size: @font-size-12;
color: @color-text-light-0;
cursor: pointer;
input[type="radio"] { flex: none; }
&:hover { color: @color-amber; }
}
}
.learning-targets {
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px;
border: 1px solid @color-border-dark-4;
background: fade(@color-chitin-dark, 30%);
.learning-hint {
margin: 0;
font-size: @font-size-11;
font-style: italic;
color: @color-text-light-0;
opacity: 0.75;
}
}
.learning-result {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
padding: 8px;
border: 1px solid @color-amber;
background: fade(@color-amber, 8%);
.context-item {
display: flex;
align-items: center;
gap: 6px;
.context-label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
color: @color-amber;
}
.context-value {
font-family: "Roboto", sans-serif;
font-size: @font-size-13;
font-weight: 700;
color: @color-text-light-0;
}
}
}
// ── Transfert de Dés d'Expérience ─────────────────────────────────
.experience-section {
.experience-controls {
gap: 8px;
padding: 8px;
.experience-group {
font-size: @font-size-11;
color: @color-text-light-0;
opacity: 0.8;
#exp-group {
font-weight: 700;
color: @color-acid-green;
}
}
button[data-action="giveToGroup"] {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
padding: 3px 12px;
cursor: pointer;
color: @color-text-light-0;
background: fade(@color-acid-green, 15%);
border: 1px solid @color-acid-green;
border-radius: 3px;
.transition();
&:hover:not(:disabled) {
color: @theme-color-dark;
background: @color-acid-green;
}
&:disabled {
opacity: 0.6;
cursor: default;
}
}
}
}
// ── Total dice pool section ─────────────────────────────────────
.total-section {
align-items: center;
+2
View File
@@ -22,6 +22,8 @@
@import "actor/npc";
@import "actor/group";
@import "actor/creature";
@import "actor/community";
@import "actor/vehicle";
// 6. Styles des items
@import "items";
+2 -1
View File
@@ -4,11 +4,12 @@ export { default as VermineCharacterSheetV2 } from "./character-sheet.mjs"
export { default as VermineNpcSheetV2 } from "./npc-sheet.mjs"
export { default as VermineGroupSheetV2 } from "./group-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 {
VermineItemSheetV2,
VermineWeaponSheetV2,
VermineDefenseSheetV2,
VermineVehicleSheetV2,
VermineAbilitySheetV2,
VermineSpecialtySheetV2,
VermineBackgroundSheetV2,
@@ -30,8 +30,13 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
toggleSheet: VermineBaseActorSheet.#onToggleSheet,
edit: VermineBaseActorSheet.#onItemEdit,
delete: VermineBaseActorSheet.#onItemDelete,
toggleEquip: VermineBaseActorSheet.#onToggleEquip,
create: VermineBaseActorSheet.#onItemCreate,
createVehicle: VermineBaseActorSheet.#onCreateVehicle,
editVehicle: VermineBaseActorSheet.#onEditVehicle,
deleteVehicle: VermineBaseActorSheet.#onDeleteVehicle,
roll: VermineBaseActorSheet.#onRollItem,
attack: VermineBaseActorSheet.#onAttack,
clickRadio: VermineBaseActorSheet.#onClickRadioHexa,
effectControl: VermineBaseActorSheet.#onEffectControl,
chooseTotem: VermineBaseActorSheet.#onChooseTotem
@@ -114,7 +119,8 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
isEditable: this.isEditable,
enrichedNotes: await enrich("identity.notes"),
enrichedBiography: await enrich("identity.biography"),
enrichedRelations: await enrich("identity.relations")
enrichedRelations: await enrich("identity.relations"),
enrichedEquipmentNotes: await enrich("equipment.description")
}
}
@@ -226,13 +232,78 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
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) {
const id = target.closest("[data-item-id]")?.dataset?.itemId
if (!id) return
const item = this.document.items.get(id)
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) {
const { default: RollDialog } = await import("../../system/dialogs/rollDialog.mjs")
const dialog = await RollDialog.create({
actorId: this.document.id,
rolltype: "skill",
label: item.system.skill,
weapon: item
})
if (dialog) dialog.render(true)
return
}
item?.roll()
}
static async #onAttack(event, target) {
const id = target.closest("[data-item-id]")?.dataset?.itemId
if (!id) return
const item = this.document.items.get(id)
if (!item) return
const { default: CombatDialog } = await import("../../system/dialogs/combatDialog.mjs")
const dialog = await CombatDialog.create({ actorId: this.document.id, weapon: item })
if (dialog) dialog.render(true)
}
static async #onToggleEquip(event, target) {
const id = target.closest("[data-item-id]")?.dataset?.itemId
if (!id) return
const item = this.document.items.get(id)
if (!item) return
await item.update({ "system.equipped": !item.system.equipped })
}
static #onClickRadioHexa(event, target) {
event.preventDefault()
event.stopPropagation()
@@ -76,6 +76,7 @@ export class VermineBaseItemSheet extends HandlebarsApplicationMixin(foundry.app
// ── Rendu ───────────────────────────────────────────────────────────
_onRender(context, options) {
super._onRender(context, options);
this.#dragDrop.forEach(d => d.bind(this.element))
}
+38 -9
View File
@@ -7,18 +7,21 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
position: { width: 860, height: 720 },
window: { contentClasses: ["character-content"] },
actions: {
addSpecialty: VermineCharacterSheetV2.#onAddSpecialty
addSpecialty: VermineCharacterSheetV2.#onAddSpecialty,
openExperiencePhase: VermineCharacterSheetV2.#onOpenExperiencePhase,
openLearning: VermineCharacterSheetV2.#onOpenLearning
}
}
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" },
abilities: { template: "systems/vermine2047/templates/actor/appv2/character-abilities.hbs" },
totem: { template: "systems/vermine2047/templates/actor/appv2/character-totem.hbs" },
equipment: { template: "systems/vermine2047/templates/actor/appv2/character-equipment.hbs" },
stories: { template: "systems/vermine2047/templates/actor/appv2/character-stories.hbs" },
combat: { template: "systems/vermine2047/templates/actor/appv2/character-combat.hbs" }
abilities: { template: "systems/vermine2047/templates/actor/appv2/character-abilities.hbs", scrollable: [""] },
totem: { template: "systems/vermine2047/templates/actor/appv2/character-totem.hbs", scrollable: [""] },
equipment: { template: "systems/vermine2047/templates/actor/appv2/character-equipment.hbs", scrollable: [""] },
stories: { template: "systems/vermine2047/templates/actor/appv2/character-stories.hbs", scrollable: [""] },
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" }
@@ -29,7 +32,8 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
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" },
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)) {
v.active = this.tabGroups[v.group] === v.id
@@ -51,6 +55,7 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
case "main": break
case "abilities":
context.tab = context.tabs.abilities
context.specialties = doc.itemTypes.specialty
break
case "totem":
context.tab = context.tabs.totem
@@ -66,16 +71,28 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
context.gear = doc.itemTypes.item
context.weapons = doc.itemTypes.weapon
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
case "stories":
context.tab = context.tabs.stories
break
case "combat":
context.tab = context.tabs.combat
context.equippedWeapons = doc.itemTypes.weapon.filter(i => i.system.equipped)
context.equippedDefenses = doc.itemTypes.defense.filter(i => i.system.equipped)
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
context.effects = prepareActiveEffectCategories(doc.effects)
break
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
}
@@ -95,4 +112,16 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
if (skillKey) itemData.system = { skill: skillKey }
await this.document.createEmbeddedDocuments("Item", [itemData])
}
static async #onOpenExperiencePhase(event, target) {
const { default: ExperiencePhaseDialog } = await import("../../system/dialogs/experiencePhaseDialog.mjs")
const dialog = await ExperiencePhaseDialog.create()
if (dialog) dialog.render(true)
}
static async #onOpenLearning(event, target) {
const { default: LearningDialog } = await import("../../system/dialogs/learningDialog.mjs")
const dialog = await LearningDialog.create({ actorId: this.document.id })
if (dialog) dialog.render(true)
}
}
@@ -0,0 +1,123 @@
import VermineBaseActorSheet from "./base-actor-sheet.mjs"
export default class VermineCommunitySheetV2 extends VermineBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ["community"],
position: { width: 760, height: 640 },
window: { contentClasses: ["community-content"] },
actions: {
addObjective: VermineCommunitySheetV2.#onAddObjective,
deleteObjective: VermineCommunitySheetV2.#onDeleteObjective
}
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/community-main.hbs", scrollable: [""] },
tabs: { template: "templates/generic/tab-navigation.hbs" },
identity: { template: "systems/vermine2047/templates/actor/appv2/community-identity.hbs", scrollable: [""] },
domains: { template: "systems/vermine2047/templates/actor/appv2/community-domains.hbs", scrollable: [""] },
resources: { template: "systems/vermine2047/templates/actor/appv2/community-resources.hbs", scrollable: [""] }
}
tabGroups = { sheet: "identity" }
#getTabs() {
const tabs = {
identity: { id: "identity", group: "sheet", icon: "fas fa-info-circle", label: "COMMUNITY.tabs.identity" },
domains: { id: "domains", group: "sheet", icon: "fas fa-chart-simple", label: "COMMUNITY.tabs.domains" },
resources: { id: "resources", group: "sheet", icon: "fas fa-boxes-stacked", label: "COMMUNITY.tabs.resources" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
// Groupe de personnages rattaché
context.resolvedGroup = null
if (this.document.system.groupId) {
const g = game.actors.get(this.document.system.groupId)
if (g) context.resolvedGroup = { id: g.id, name: g.name }
}
// Enrichissement HTML des champs longs (même logique que la fiche PNJ)
const enrich = async (path) => {
const val = foundry.utils.getProperty(this.document.system, path)
return val ? await foundry.applications.ux.TextEditor.implementation.enrichHTML(val, { async: true }) : ""
}
context.enrichedNotes = await enrich("identity.notes")
context.enrichedLandmarks = await enrich("location.landmarks")
context.enrichedTerritory = await enrich("location.territory")
context.enrichedEvents = await enrich("history.events")
context.enrichedRelations = await enrich("history.relations")
return context
}
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main": break
case "identity":
context.tab = context.tabs.identity
context.ageOptions = Object.entries(CONFIG.VERMINE.communityAges).map(([key, label]) => ({ key, label: game.i18n.localize(label) }))
break
case "domains":
context.tab = context.tabs.domains
context.sizeOptions = CONFIG.VERMINE.communitySizeLevels
context.sizeLabel = doc.system.size?.value ? game.i18n.localize(CONFIG.VERMINE.communitySizeLevels[doc.system.size.value]?.label ?? "") : ""
context.sizeInfo = doc.system.size?.value ? CONFIG.VERMINE.communitySizeLevels[doc.system.size.value] : null
// Préparer les 4 Domaines avec leurs 2 sections (jauges) et leur état
context.domains = []
for (const [dkey, dcfg] of Object.entries(CONFIG.VERMINE.communityDomains)) {
const domain = doc.system.domains?.[dkey]
context.domains.push({
key: dkey,
label: game.i18n.localize(dcfg.label),
sections: Object.entries(dcfg.sections).map(([skey, label]) => {
const gauge = domain?.[skey] ?? { value: 0 }
const state = gauge.state || "crisis"
return {
domainKey: dkey,
key: skey,
label: game.i18n.localize(label),
gauge,
state,
stateLabel: game.i18n.localize(CONFIG.VERMINE.communityGaugeStates[state]?.label ?? "COMMUNITY.states.crisis")
}
})
})
}
break
case "resources":
context.tab = context.tabs.resources
break
}
return context
}
changeTab(tab, group, options = {}) {
super.changeTab(tab, group, options)
if (group === "sheet") {
const main = this.element?.querySelector('[data-group="sheet"][data-tab="main"]')
if (main) main.classList.add("active")
}
}
static #onDeleteObjective(event, target) {
const index = parseInt(target.dataset.index)
if (isNaN(index)) return
const objectives = foundry.utils.duplicate(this.document.system.objectives || { major: "", minor: [] })
objectives.minor.splice(index, 1)
this.document.update({ "system.objectives": objectives })
}
static #onAddObjective(event, target) {
const objectives = foundry.utils.duplicate(this.document.system.objectives || { major: "", minor: [] })
objectives.minor.push("")
this.document.update({ "system.objectives": objectives })
}
}
+38 -8
View File
@@ -2,6 +2,10 @@ import VermineBaseActorSheet from "./base-actor-sheet.mjs"
export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
// 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 = {
classes: ["creature"],
position: { width: 700, height: 650 },
@@ -9,12 +13,12 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/creature-main.hbs" },
main: { template: "systems/vermine2047/templates/actor/appv2/creature-main.hbs", scrollable: [""] },
tabs: { template: "templates/generic/tab-navigation.hbs" },
info: { template: "systems/vermine2047/templates/actor/appv2/creature-info.hbs" },
stats: { template: "systems/vermine2047/templates/actor/appv2/creature-stats.hbs" },
combat: { template: "systems/vermine2047/templates/actor/appv2/creature-combat.hbs" },
effects: { template: "systems/vermine2047/templates/actor/appv2/creature-effects.hbs" }
info: { template: "systems/vermine2047/templates/actor/appv2/creature-info.hbs", scrollable: [""] },
stats: { template: "systems/vermine2047/templates/actor/appv2/creature-stats.hbs", scrollable: [""] },
combat: { template: "systems/vermine2047/templates/actor/appv2/creature-combat.hbs", scrollable: [""] },
effects: { template: "systems/vermine2047/templates/actor/appv2/creature-effects.hbs", scrollable: [""] }
}
tabGroups = { sheet: "info" }
@@ -61,13 +65,28 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
break
case "stats":
context.tab = context.tabs.stats
context.patternLabel = doc.system.pattern?.value ? CONFIG.VERMINE.creaturePatternLevels[doc.system.pattern.value]?.label : ""
context.sizeLabel = doc.system.size?.value || ""
context.roleLabel = doc.system.role?.value ? CONFIG.VERMINE.creatureRoleLevels[doc.system.role.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 !== undefined ? game.i18n.localize(CONFIG.VERMINE.creatureSizeLevels[doc.system.size.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")
// 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
case "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
case "effects":
context.tab = context.tabs.effects
@@ -77,4 +96,15 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
}
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)
}
}
+21 -8
View File
@@ -11,17 +11,18 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
deleteMember: VermineGroupSheetV2.#onDeleteMember,
deleteEncounter: VermineGroupSheetV2.#onDeleteEncounter,
deleteObjective: VermineGroupSheetV2.#onDeleteObjective,
addObjective: VermineGroupSheetV2.#onAddObjective
addObjective: VermineGroupSheetV2.#onAddObjective,
createTotemAbility: VermineGroupSheetV2.#onCreateTotemAbility
}
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/group-main.hbs" },
main: { template: "systems/vermine2047/templates/actor/appv2/group-main.hbs", scrollable: [""] },
tabs: { template: "templates/generic/tab-navigation.hbs" },
info: { template: "systems/vermine2047/templates/actor/appv2/group-info.hbs" },
gear: { template: "systems/vermine2047/templates/actor/appv2/group-gear.hbs" },
road: { template: "systems/vermine2047/templates/actor/appv2/group-road.hbs" },
reserve: { template: "systems/vermine2047/templates/actor/appv2/group-reserve.hbs" }
info: { template: "systems/vermine2047/templates/actor/appv2/group-info.hbs", scrollable: [""] },
gear: { template: "systems/vermine2047/templates/actor/appv2/group-gear.hbs", scrollable: [""] },
road: { template: "systems/vermine2047/templates/actor/appv2/group-road.hbs", scrollable: [""] },
reserve: { template: "systems/vermine2047/templates/actor/appv2/group-reserve.hbs", scrollable: [""] }
}
tabGroups = { sheet: "info" }
@@ -67,7 +68,8 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
case "main": break
case "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.backgrounds = doc.itemTypes.background
context.traumas = doc.itemTypes.trauma
@@ -78,10 +80,11 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
context.gear = doc.itemTypes.item
context.weapons = doc.itemTypes.weapon
context.defenses = doc.itemTypes.defense
context.vehicles = this.#getOwnedVehicles()
break
case "road":
context.tab = context.tabs.road
context.vehicles = doc.itemTypes.vehicle
context.vehicles = this.#getOwnedVehicles()
break
case "reserve":
context.tab = context.tabs.reserve
@@ -90,6 +93,11 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
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
static async #onChooseTotem(event, target) {
const { TotemPicker } = await import("../../system/applications.mjs")
@@ -135,4 +143,9 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
objectives[type].push("")
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" } }])
}
}
+11 -17
View File
@@ -3,71 +3,65 @@ import { VermineBaseItemSheet } from "./base-item-sheet.mjs"
// ── Item générique ────────────────────────────────────────────────────
export class VermineItemSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["item-gear"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-item-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-item-sheet.hbs", scrollable: [""] } }
}
// ── Arme ──────────────────────────────────────────────────────────────
export class VermineWeaponSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["weapon"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-weapon-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-weapon-sheet.hbs", scrollable: [""] } }
}
// ── Défense ───────────────────────────────────────────────────────────
export class VermineDefenseSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["defense"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-defense-sheet.hbs" } }
}
// ── Véhicule ──────────────────────────────────────────────────────────
export class VermineVehicleSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["vehicle"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-vehicle-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-defense-sheet.hbs", scrollable: [""] } }
}
// ── Capacité ──────────────────────────────────────────────────────────
export class VermineAbilitySheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["ability"], position: { width: 560 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-ability-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-ability-sheet.hbs", scrollable: [""] } }
}
// ── Spécialité ────────────────────────────────────────────────────────
export class VermineSpecialtySheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["specialty"], position: { width: 400 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-specialty-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-specialty-sheet.hbs", scrollable: [""] } }
}
// ── Historique ────────────────────────────────────────────────────────
export class VermineBackgroundSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["background"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-background-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-background-sheet.hbs", scrollable: [""] } }
}
// ── Traumatisme ───────────────────────────────────────────────────────
export class VermineTraumaSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["trauma"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-trauma-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-trauma-sheet.hbs", scrollable: [""] } }
}
// ── Évolution ─────────────────────────────────────────────────────────
export class VermineEvolutionSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["evolution"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-evolution-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-evolution-sheet.hbs", scrollable: [""] } }
}
// ── Rumeur ────────────────────────────────────────────────────────────
export class VermineRumorSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["rumor"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rumor-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rumor-sheet.hbs", scrollable: [""] } }
}
// ── Cible ─────────────────────────────────────────────────────────────
export class VermineTargetSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["target"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-target-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-target-sheet.hbs", scrollable: [""] } }
}
// ── Rite ──────────────────────────────────────────────────────────────
export class VermineRiteSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["rite"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rite-sheet.hbs" } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rite-sheet.hbs", scrollable: [""] } }
}
+44 -17
View File
@@ -2,6 +2,11 @@ import VermineBaseActorSheet from "./base-actor-sheet.mjs"
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 = {
classes: ["npc"],
position: { width: 750, height: 680 },
@@ -9,22 +14,20 @@ export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/npc-main.hbs" },
main: { template: "systems/vermine2047/templates/actor/appv2/npc-main.hbs", scrollable: [""] },
tabs: { template: "templates/generic/tab-navigation.hbs" },
characteristics: { template: "systems/vermine2047/templates/actor/appv2/npc-characteristics.hbs" },
skills: { template: "systems/vermine2047/templates/actor/appv2/npc-skills.hbs" },
threat: { template: "systems/vermine2047/templates/actor/appv2/npc-threat.hbs" },
combat: { template: "systems/vermine2047/templates/actor/appv2/npc-combat.hbs" },
notes: { template: "systems/vermine2047/templates/actor/appv2/npc-notes.hbs" }
traits: { template: "systems/vermine2047/templates/actor/appv2/npc-traits.hbs", scrollable: [""] },
equipment: { template: "systems/vermine2047/templates/actor/appv2/npc-equipment.hbs", scrollable: [""] },
combat: { template: "systems/vermine2047/templates/actor/appv2/npc-combat.hbs", scrollable: [""] },
notes: { template: "systems/vermine2047/templates/actor/appv2/npc-notes.hbs", scrollable: [""] }
}
tabGroups = { sheet: "characteristics" }
tabGroups = { sheet: "traits" }
#getTabs() {
const tabs = {
characteristics: { id: "characteristics", group: "sheet", icon: "fas fa-dice", label: "VERMINE.abilities" },
skills: { id: "skills", group: "sheet", icon: "fas fa-brain", label: "VERMINE.skills" },
threat: { id: "threat", group: "sheet", icon: "fas fa-exclamation-triangle", label: "ADVERSITY.threat" },
traits: { id: "traits", group: "sheet", icon: "fas fa-dice", label: "VERMINE.traits" },
equipment: { id: "equipment", group: "sheet", icon: "fas fa-hammer", label: "VERMINE.tabs.equipment" },
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" }
}
@@ -51,17 +54,18 @@ export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
context.totemOptions = CONFIG.VERMINE.totems
context.originOptions = CONFIG.VERMINE.origins
break
case "characteristics":
context.tab = context.tabs.characteristics
case "traits":
context.tab = context.tabs.traits
break
case "skills":
context.tab = context.tabs.skills
break
case "threat":
context.tab = context.tabs.threat
case "equipment":
context.tab = context.tabs.equipment
context.weapons = doc.itemTypes.weapon
context.defenses = doc.itemTypes.defense
break
case "combat":
context.tab = context.tabs.combat
context.equippedWeapons = doc.itemTypes.weapon.filter(i => i.system.equipped)
context.equippedDefenses = doc.itemTypes.defense.filter(i => i.system.equipped)
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
context.effects = prepareActiveEffectCategories(doc.effects)
break
@@ -71,4 +75,27 @@ export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
}
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
}
}
+1 -1
View File
@@ -54,7 +54,7 @@ export default class VermineItem extends Item {
*/
getRollData() {
// If present, return the actor's roll data.
if (!this.actor) return null;
if (!this.actor) return this.system;
const rollData = this.actor.getRollData();
// Grab the item's system data as well.
rollData.item = foundry.utils.deepClone(this.system);
+1
View File
@@ -2,6 +2,7 @@ export { default as VermineCharacterData } from "./character.mjs"
export { default as VermineNpcData } from "./npc.mjs"
export { default as VermineGroupData } from "./group.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 VermineWeaponData } from "./weapon.mjs"
export { default as VermineDefenseData } from "./defense.mjs"
+1 -1
View File
@@ -219,7 +219,7 @@ const reqInt = { required: true, nullable: false, integer: true }
export function raritySchema() {
const fields = foundry.data.fields
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 })
})
}
+38 -4
View File
@@ -72,8 +72,40 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
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)
self_control: attributeSchema(0, 0, 10),
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
@@ -127,7 +159,8 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
const sum = abilities
.filter(a => a.category === "mental" || a.category === "social")
.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
.filter(a => a.category === "physical" || a.category === "manual")
.reduce((acc, a) => acc + a.value, 0)
this.attributes.effort.max = sum + modFromAge
const bonus = this.experience?.reserveBonus?.effort || 0
this.attributes.effort.max = Math.min(10, sum + modFromAge + bonus)
}
/**
+195
View File
@@ -0,0 +1,195 @@
/**
* DataModel pour les acteurs de type "community" (Communauté).
* La Communauté est l'extension sédentarisée d'un Groupe : elle englobe le
* Groupe de personnages, des Figures et des Habitants répartis en Compagnies.
* Le socle couvre l'identité, la géographie, l'histoire, les objectifs, la
* taille/l'effectif, les quatre Domaines (8 jauges), le Moral et les réserves.
*/
import {
attributeSchema,
levelSchema,
combatStatusSchema
} from "./_shared.mjs"
export default class VermineCommunityData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.community"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
const gauge = () => attributeSchema(0, 0, 10)
return {
// Statut de combat (base, peu utilisé pour une Communauté)
combatStatus: combatStatusSchema("9"),
// Identité
identity: new fields.SchemaField({
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
profile: new fields.StringField({ required: true, nullable: false, initial: "" }),
origin: new fields.StringField({ required: true, nullable: false, initial: "" }),
theme: new fields.StringField({ required: true, nullable: false, initial: "" }),
instincts: new fields.StringField({ required: true, nullable: false, initial: "" }),
prohibits: new fields.StringField({ required: true, nullable: false, initial: "" }),
// Âge de la Communauté : recent | established | old
age: new fields.StringField({ required: true, nullable: false, initial: "recent" }),
notes: new fields.HTMLField({ required: true, initial: "", textSearch: true })
}),
// Géographie / localisation
location: new fields.SchemaField({
sector: new fields.StringField({ required: true, nullable: false, initial: "" }),
placeType: new fields.StringField({ required: true, nullable: false, initial: "" }),
landmarks: new fields.HTMLField({ required: true, initial: "", textSearch: true }),
territory: new fields.HTMLField({ required: true, initial: "", textSearch: true })
}),
// Histoire
history: new fields.SchemaField({
founding: new fields.StringField({ required: true, nullable: false, initial: "" }),
events: new fields.HTMLField({ required: true, initial: "", textSearch: true }),
relations: new fields.HTMLField({ required: true, initial: "", textSearch: true })
}),
// Objectifs : un majeur + des mineurs
objectives: new fields.SchemaField({
major: new fields.StringField({ required: true, nullable: false, initial: "" }),
minor: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" }))
}),
// Taille (1-3) et effectif
size: attributeSchema(1, 1, 3),
population: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
// Domaines : 4 Domaines × 2 sections (jauges en Dés)
domains: new fields.SchemaField({
resources: new fields.SchemaField({
vivre: gauge(),
supplies: gauge()
}),
security: new fields.SchemaField({
defense: gauge(),
care: gauge()
}),
dynamism: new fields.SchemaField({
cohesion: gauge(),
productivity: gauge()
}),
relations: new fields.SchemaField({
human: gauge(),
ecology: gauge()
})
}),
// Moral (comme le Groupe) : value 0-7, level dérivé
morale: new fields.SchemaField({
level: new fields.StringField({ required: true, nullable: false, initial: "normal" }),
value: new fields.NumberField({ ...reqInt, initial: 7, min: 0, max: 7 }),
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: 7, min: 0 })
}),
// Réserve de Groupe
reserve: attributeSchema(0, 0, 10),
// Dés collectifs de la Communauté
material: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
experience: new fields.SchemaField({
dice: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
}),
// Niveau de la Communauté (pour l'expérience des habitants)
level: levelSchema(1, 1, 10),
// Groupe de personnages rattaché (id d'acteur)
groupId: new fields.StringField({ required: true, nullable: false, initial: "" }),
// Habitants (phase 2) : Figures et Compagnies
figures: new fields.ArrayField(new fields.SchemaField({
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
role: new fields.StringField({ required: true, nullable: false, initial: "" }),
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
specialty: new fields.SchemaField({
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
score: new fields.NumberField({ ...reqInt, initial: 3, min: 0, max: 10 })
})
})),
companies: new fields.ArrayField(new fields.SchemaField({
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
specialty: new fields.SchemaField({
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
score: new fields.NumberField({ ...reqInt, initial: 3, min: 0, max: 10 })
}),
level: levelSchema(1, 1, 4),
members: new fields.NumberField({ ...reqInt, initial: 1, min: 0 }),
figureName: new fields.StringField({ required: true, nullable: false, initial: "" })
}))
}
}
/** @override */
prepareDerivedData() {
super.prepareDerivedData()
// 1. État (Crise/Bas/Stable/Opulent) de chaque jauge de Domaine
this._computeGaugeStates()
// 2. Les quatre Domaines sont-ils Bas ? (Moral inactif)
this._computeAllDomainsLow()
// 3. Mettre à jour le niveau de Moral selon la valeur
this._updateMorale()
}
/**
* État d'une jauge selon sa valeur en Dés.
* Crise 0D / Bas 1-2D / Stable 3-5D / Opulent 6D+.
* @param {number} value
* @returns {string}
*/
static gaugeState(value) {
const v = parseInt(value, 10) || 0
if (v >= 6) return "opulent"
if (v >= 3) return "stable"
if (v >= 1) return "low"
return "crisis"
}
_computeGaugeStates() {
for (const domain of Object.values(this.domains)) {
for (const gauge of Object.values(domain)) {
gauge.state = VermineCommunityData.gaugeState(gauge.value)
}
}
}
/**
* Un Domaine est "Bas" lorsque ses deux sections sont à 2D ou moins.
* Si les quatre Domaines sont Bas, le Bonus de Moral est inactif.
*/
_computeAllDomainsLow() {
const domainLow = (domain) => Object.values(domain).every(g => (parseInt(g.value, 10) || 0) <= 2)
this.allDomainsLow = Object.values(this.domains).every(domainLow)
}
/**
* Met à jour le niveau de Moral en fonction de la valeur de dés.
* Règles : 7D = Haut, 6-3D = Normal, 2-1D = Bas, 0D = Crise.
*/
_updateMorale() {
const moraleValue = parseInt(this.morale?.value, 10) || 0
if (this.morale.level && this.morale.level !== "normal") return
if (moraleValue >= 7) {
this.morale.level = "high"
} else if (moraleValue >= 3) {
this.morale.level = "normal"
} else if (moraleValue >= 1) {
this.morale.level = "low"
} else {
this.morale.level = "crisis"
}
}
}
+32 -11
View File
@@ -48,7 +48,7 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
// Niveaux de créature (patron, taille, rôle, meute)
pattern: attributeSchema(1, 1, 4),
size: attributeSchema(1, 1, 3),
size: attributeSchema(0, 0, 3),
role: attributeSchema(1, 1, 4),
pack: attributeSchema(0, 0, 3),
@@ -97,28 +97,38 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
*/
_calculateCreatureComputedValues() {
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 packLevel = this.pack?.value || 0
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {}
const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {}
const roleConfig = CONFIG.VERMINE.creatureRoleLevels[roleLevel] || {}
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
this.computed.attack = (patternConfig.attack || 0)
+ (sizeConfig.attack || 0)
+ sizeAttack
+ (packConfig.attack || 0)
+ (roleConfig.reaction || 0)
// Dégâts : patron + vigueur de taille + meute
this.computed.damage = (patternConfig.damage || 0)
+ (sizeConfig.vigor || 0)
+ sizeVigor
+ (packConfig.damage || 0)
// Vigueur : taille + meute
this.computed.vigor = (sizeConfig.vigor || 0) + (packConfig.damage || 0)
this.computed.vigor = sizeVigor + (packConfig.damage || 0)
// Réaction : rôle
this.computed.reaction = (roleConfig.reaction || 0) + (roleConfig.reaction_bonus || 0)
@@ -141,21 +151,32 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
*/
_calculateCreatureWoundThresholds() {
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 patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {}
const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {}
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)
+ (sizeConfig.minorWound || 0)
+ sizeMinor
+ (packConfig.minorWound || 0)
this.majorWound.threshold = (patternConfig.majorWound || 0)
+ (sizeConfig.majorWound || 0)
+ sizeMajor
+ (packConfig.majorWound || 0)
this.deadlyWound.threshold = (patternConfig.deadlyWound || 0)
+ (sizeConfig.deadlyWound || 0)
+ sizeDeadly
+ (packConfig.deadlyWound || 0)
// Max de blessures
+1
View File
@@ -15,6 +15,7 @@ export default class VermineDefenseData extends foundry.abstract.TypeDataModel {
return {
...baseItemSchema(),
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
equipped: new fields.BooleanField({ required: true, initial: false }),
specificLevel: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "" }),
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
+5
View File
@@ -75,6 +75,11 @@ export default class VermineGroupData extends foundry.abstract.TypeDataModel {
// Membres (IDs d'acteurs)
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
encounters: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" }))
}
+75 -62
View File
@@ -2,17 +2,22 @@
* DataModel pour les acteurs de type "npc" (PNJ).
* Étend foundry.abstract.TypeDataModel.
*
* Note : le champ libre de compétences (texte descriptif) est nommé "freeSkills"
* pour éviter le conflit avec le SchemaField "skills" qui contient les 30 compétences.
* Conforme aux règles des PNJ (regles/regles_pnj.txt) : un PNJ n'a ni
* 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 {
woundSchema,
combatStatusSchema,
equipmentSchema,
attributeSchema,
abilitiesSchema,
skillCategoriesSchema,
skillsSchema
attributeSchema
} from "./_shared.mjs"
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.
* 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
* (SchemaField) et utilise "freeSkills" pour le texte libre.
* pour les PNJ. Le DataModel utilise "freeSkills" pour cette description.
* @param {Object} source Données brutes avant validation du schéma
* @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)
combatStatus: combatStatusSchema("9"),
// Identité
// Identité (Profil du PNJ)
identity: new fields.SchemaField({
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: "" }),
origin: 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: "" })
}),
// Attributs (XP, réputation, sang-froid, effort)
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)
// Critères de définition du PNJ (Menace, Expérience, Rôle)
threat: attributeSchema(1, 1, 4),
experience: attributeSchema(1, 1, 4),
role: attributeSchema(1, 1, 4),
// Compétences (les 30 compétences individuelles)
skills: skillsSchema(),
// Description libre des compétences (champ texte PNJ)
// Description libre des Spécialités (champ texte PNJ)
freeSkills: new fields.StringField({ required: true, nullable: false, initial: "" }),
// Catégories de compétences
skill_categories: skillCategoriesSchema(),
// Caractéristiques (8)
abilities: abilitiesSchema(),
// Valeurs calculées (dérivées des niveaux de menace / expérience / rôle)
computed: new fields.SchemaField({
attack: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
vigor: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
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
equipment: equipmentSchema()
@@ -92,58 +96,67 @@ export default class VermineNpcData extends foundry.abstract.TypeDataModel {
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()
// 2. Calculer les réserves selon le niveau de rôle
this._setNpcAttributes()
// 3. Définir les libellés des caractéristiques
this._setAbilityLabels()
// 4. Mettre à jour le statut de combat
// 3. Mettre à jour le statut de combat
this._updateCombatStatus()
}
/**
* Calcule les seuils de blessures à partir du niveau de menace.
* Utilise CONFIG.VERMINE.npcThreatLevels.
* Calcule les valeurs dérivées à partir des niveaux de Menace, Expérience
* 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() {
const health = this.abilities?.health?.value || 1
const threatLevel = this.threat?.value || 1
const threatConfig = CONFIG.VERMINE.npcThreatLevels[threatLevel] || {}
this.minorWound.threshold = health
this.majorWound.threshold = health + 3
this.deadlyWound.threshold = Math.min(health + 7, 10)
this.minorWound.threshold = threatConfig.minorThreshold ?? 1
this.majorWound.threshold = threatConfig.majorThreshold ?? 4
this.deadlyWound.threshold = threatConfig.deadlyThreshold ?? 6
this.minorWound.max = threatConfig.minorWound || 1
this.majorWound.max = threatConfig.majorWound || 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é.
*/
+3 -1
View File
@@ -10,9 +10,11 @@ export default class VermineSpecialtyData extends foundry.abstract.TypeDataModel
/** @override */
static defineSchema() {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
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 })
}
}
}
+54 -7
View File
@@ -1,20 +1,67 @@
import { baseItemSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "vehicle" (véhicules).
* @augments {foundry.abstract.TypeDataModel}
* DataModel pour les acteurs de type "vehicle" (véhicules et montures).
* 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 {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.vehicle"]
static LOCALIZATION_PREFIXES = ["VERMINE.vehicle"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
...baseItemSchema(),
mobility: new fields.NumberField({ ...reqInt, initial: 3, min: 0 })
// Identité
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: "" })
}
}
}
+2
View File
@@ -14,6 +14,8 @@ export default class VermineWeaponData extends foundry.abstract.TypeDataModel {
const reqInt = { required: true, nullable: false, integer: true }
return {
...baseItemSchema(),
skill: new fields.StringField({ required: true, nullable: false, initial: "" }),
equipped: new fields.BooleanField({ required: true, initial: false }),
min_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
damage: new fields.SchemaField({
+13 -12
View File
@@ -1,3 +1,5 @@
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api
export class TotemPicker extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(linkEl, actor) {
@@ -24,9 +26,9 @@ export class TotemPicker extends HandlebarsApplicationMixin(ApplicationV2) {
return { config: CONFIG.VERMINE };
}
activateListeners(html) {
super.activateListeners(html);
html.querySelectorAll('.totem').forEach(el => {
_onRender(context, options) {
super._onRender(context, options);
this.element.querySelectorAll('.totem').forEach(el => {
el.addEventListener('click', event => {
const link = event.target.closest('a');
if (!link?.dataset.totem) return;
@@ -80,9 +82,9 @@ export class ActorPicker extends HandlebarsApplicationMixin(ApplicationV2) {
};
}
activateListeners(html) {
super.activateListeners(html);
html.querySelectorAll('.actor').forEach(el => {
_onRender(context, options) {
super._onRender(context, options);
this.element.querySelectorAll('.actor').forEach(el => {
el.addEventListener('click', async event => {
const div = event.target.closest('div');
const actorId = div?.dataset.actorId;
@@ -148,15 +150,15 @@ export class TraitSelector extends HandlebarsApplicationMixin(ApplicationV2) {
};
}
activateListeners(html) {
super.activateListeners(html);
this._validateTraits(html);
html.querySelectorAll('input').forEach(el => {
_onRender(context, options) {
super._onRender(context, options);
this._validateTraits(this.element);
this.element.querySelectorAll('input').forEach(el => {
el.addEventListener('change', ev => this._onChangeInput(ev));
});
}
async _validateTraits(html) {
_validateTraits(html) {
const checks = html.querySelectorAll("input.trait-selector");
for (const inp of checks) {
if (this.targetItem.system.traits[inp.dataset.trait]) {
@@ -165,7 +167,6 @@ export class TraitSelector extends HandlebarsApplicationMixin(ApplicationV2) {
}
}
}
await this.render(true);
}
async _onChangeInput(ev) {
+59 -7
View File
@@ -99,10 +99,10 @@ VERMINE.totemDomains = {
* NPC Threat Levels configuration
*/
VERMINE.npcThreatLevels = {
1: { "label": "THREAT_LEVELS.minor", "attack": 3, "vigor": 1, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
2: { "label": "THREAT_LEVELS.serious", "attack": 4, "vigor": 2, "minorWound": 2, "majorWound": 1, "deadlyWound": 1 },
3: { "label": "THREAT_LEVELS.major", "attack": 5, "vigor": 3, "minorWound": 2, "majorWound": 1, "deadlyWound": 1 },
4: { "label": "THREAT_LEVELS.deadly", "attack": 6, "vigor": 4, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }
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, "minorThreshold": 2, "majorThreshold": 5, "deadlyThreshold": 8, "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, "minorThreshold": 3, "majorThreshold": 6, "deadlyThreshold": 9, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }
}
/**
@@ -139,9 +139,10 @@ VERMINE.creaturePatternLevels = {
* Creature Size Levels configuration
*/
VERMINE.creatureSizeLevels = {
1: { "label": "SIZE_LEVELS.small", "attack": 2, "vigor": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
2: { "label": "SIZE_LEVELS.medium", "attack": 3, "vigor": 2, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
3: { "label": "SIZE_LEVELS.large", "attack": 4, "vigor": 3, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 }
0: { "label": "SIZE_LEVELS.tiny", "attack": 0, "vigor": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 },
1: { "label": "SIZE_LEVELS.small", "attack": 1, "vigor": 1, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
2: { "label": "SIZE_LEVELS.medium", "attack": 2, "vigor": 2, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 },
3: { "label": "SIZE_LEVELS.large", "attack": 3, "vigor": 3, "minorWound": 3, "majorWound": 3, "deadlyWound": 3 }
}
/**
@@ -468,3 +469,54 @@ VERMINE.totemEffects = {
}
}
/**
* Communauté — Domaines et sections (jauges).
*/
VERMINE.communityDomains = {
resources: {
label: "COMMUNITY.domains.resources",
sections: { vivre: "COMMUNITY.sections.vivre", supplies: "COMMUNITY.sections.supplies" }
},
security: {
label: "COMMUNITY.domains.security",
sections: { defense: "COMMUNITY.sections.defense", care: "COMMUNITY.sections.care" }
},
dynamism: {
label: "COMMUNITY.domains.dynamism",
sections: { cohesion: "COMMUNITY.sections.cohesion", productivity: "COMMUNITY.sections.productivity" }
},
relations: {
label: "COMMUNITY.domains.relations",
sections: { human: "COMMUNITY.sections.human", ecology: "COMMUNITY.sections.ecology" }
}
}
/**
* Communauté — Tailles (effectif, Figures, Compagnies recommandés).
*/
VERMINE.communitySizeLevels = {
1: { label: "COMMUNITY.sizeLevels.1", population: "7-50", figures: 3, companies: 5 },
2: { label: "COMMUNITY.sizeLevels.2", population: "51-75", figures: 5, companies: 7 },
3: { label: "COMMUNITY.sizeLevels.3", population: "100+", figures: 7, companies: 9 }
}
/**
* Communauté — États d'une jauge de Domaine.
* Crise 0D / Bas 1-2D / Stable 3-5D / Opulent 6D+.
*/
VERMINE.communityGaugeStates = {
crisis: { label: "COMMUNITY.states.crisis" },
low: { label: "COMMUNITY.states.low" },
stable: { label: "COMMUNITY.states.stable" },
opulent: { label: "COMMUNITY.states.opulent" }
}
/**
* Communauté — Âge (notion de temps).
*/
VERMINE.communityAges = {
recent: "COMMUNITY.age.recent",
established: "COMMUNITY.age.established",
old: "COMMUNITY.age.old"
}
+399
View File
@@ -0,0 +1,399 @@
import { VermineUtils } from "../roll.mjs";
import { VermineExchange } from "../exchange.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
/**
* Dialogue d'attaque pour les échanges de coups.
* Prépare le pool, la difficulté (contact / distance) et le contexte
* d'échange (cibles, dégâts de base) selon le type d'acteur :
* - Personnage : capacité + compétence de l'arme.
* - PNJ : pool d'attaque du niveau de menace + réserves de rôle.
* - Créature : pool d'attaque calculé, difficulté du statut de combat.
*/
export default class CombatDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
static DEFAULT_OPTIONS = {
classes: ["vermine-roll"],
tag: "form",
window: {
icon: "fas fa-crosshairs",
resizable: false
},
position: {
width: 520,
height: 600
},
actions: {
roll: CombatDialog.#onRoll,
cancel: CombatDialog.#onCancel
}
};
static PARTS = {
main: { template: "systems/vermine2047/templates/dialogs/combat-dialog.hbs" }
};
static async create({ actorId, weapon = null }) {
const actor = await game.actors.get(actorId);
if (!actor) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
return null;
}
const mode = actor.type === "creature" ? "contact" : VermineExchange.modeForWeapon(weapon);
return new CombatDialog({ actor, weapon, mode });
}
constructor(options = {}) {
super(options);
this.actor = options.actor;
this.weapon = options.weapon ?? null;
this.mode = options.mode ?? "contact";
this.abilityKey = VermineExchange.attackAbility(this.mode);
this.skillKey = this.weapon?.system?.skill || null;
this.skillValue = this.skillKey ? (this.actor.system?.skills?.[this.skillKey]?.value || 0) : 0;
this.skillPool = this.skillValue ? (CONFIG.VERMINE.SkillLevels?.[this.skillValue]?.dicePool || 0) : 0;
this.skillReroll = this.skillValue ? (CONFIG.VERMINE.SkillLevels?.[this.skillValue]?.reroll || 0) : 0;
this.basePool = VermineExchange.baseAttackPool(this.actor, this.weapon, this.mode);
this.baseDamage = VermineExchange.baseDamage(this.actor, this.weapon);
}
get title() {
return game.i18n.localize("VERMINE.attack");
}
async _prepareContext() {
const actor = this.actor;
const isCharacter = actor.type === "character";
const isNpc = actor.type === "npc";
const isCreature = actor.type === "creature";
const targets = [...game.user.targets].map(t => ({
id: t.actor?.id ?? null,
uuid: t.actor?.uuid ?? null,
name: t.name,
canDefend: t.actor?.type !== "creature"
}));
let difficultyOptions = [];
let lockedDifficulty = null;
let defaultDifficulty = null;
if (isCreature) {
const d = parseInt(actor.system?.combatStatus?.difficulty, 10) || 9;
lockedDifficulty = d;
difficultyOptions = [{ difficulty: d, label: "", locked: true }];
defaultDifficulty = d;
} else if (this.mode === "ranged") {
difficultyOptions = VermineExchange.rangeDifficultyOptions().map(o => ({
difficulty: o.difficulty,
label: game.i18n.localize(o.label),
rangeKey: o.key
}));
defaultDifficulty = difficultyOptions[0]?.difficulty ?? 7;
} else if (isNpc) {
difficultyOptions = VermineExchange.npcDifficultyOptions(actor).map(d => ({
difficulty: d,
label: String(d)
}));
defaultDifficulty = difficultyOptions[0]?.difficulty ?? 7;
} else {
difficultyOptions = VermineExchange.contactDifficultyOptions(this.skillValue).map(d => ({
difficulty: d,
label: String(d)
}));
defaultDifficulty = difficultyOptions[0]?.difficulty ?? 7;
}
// Cible de type créature : défense passive, la difficulté d'attaque est
// forcée au statut de combat de la créature (toute réussite ≥1 touche).
const creatureTargets = targets.filter(t => t.id && !t.canDefend);
const creatureLocked = !isCreature && creatureTargets.length && creatureTargets.length === targets.length;
if (creatureLocked) {
const ref = creatureTargets[0];
const creature = ref.id ? game.actors.get(ref.id) : null;
const actor = creature ?? (ref.uuid ? await fromUuid(ref.uuid) : null);
const d = parseInt(actor?.system?.combatStatus?.difficulty, 10) || 7;
lockedDifficulty = d;
difficultyOptions = [{ difficulty: d, label: "", locked: true }];
defaultDifficulty = d;
}
const threat = actor.system?.threat?.value || 1;
const role = actor.system?.role?.value || 1;
return {
actor,
system: actor.system,
config: CONFIG.VERMINE,
actorType: actor.type,
mode: this.mode,
weapon: this.weapon,
abilityKey: this.abilityKey,
abilityValue: actor.system?.abilities?.[this.abilityKey]?.value || 0,
skillKey: this.skillKey,
skillValue: this.skillValue,
basePool: this.basePool,
baseDamage: this.baseDamage,
isCharacter,
isNpc,
isCreature,
creatureLocked,
npcThreatPool: isNpc ? (CONFIG.VERMINE.npcThreatLevels?.[threat]?.attack || 0) : 0,
npcPoolMax: isNpc ? (CONFIG.VERMINE.npcRoleLevels?.[role]?.pools || 0) : 0,
difficultyOptions,
lockedDifficulty,
defaultDifficulty,
hasAmmo: this.mode === "ranged" && (this.weapon?.system?.ammo ?? 0) > 0,
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
availableItems: actor.items.filter(i => i.type === "item"),
targets
};
}
async _onRender(context, options) {
this.element.dataset.actorId = this.actor.id;
this.element.dataset.weaponId = this.weapon?.id ?? "";
this.element.dataset.mode = this.mode;
for (const inp of this.element.querySelectorAll("[data-roll]")) {
inp.addEventListener("change", this.#onInputChange.bind(this));
}
const selfControl = this.element.querySelector("#self_control");
if (selfControl) {
selfControl.addEventListener("change", this.#onChangeSelfControl.bind(this));
}
this.element.querySelector("#second-action")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#human-totem")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#adapted-totem")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#pools-spent")?.addEventListener("change", () => this.#updateUI());
const ability = this.element.querySelector("#ability");
if (ability && this.actor.type === "character") {
const sc = this.element.querySelector("#self_control");
if (sc) sc.max = ability.value;
}
this.#updateUI();
}
// ── Getters ──────────────────────────────────────────────────────────
get #el() { return this.element; }
#getDifficultyBase() {
const el = this.#el.querySelector('input[name="combat-difficulty"]:checked');
return parseInt(el?.value, 10) || 7;
}
#getSecondAction() {
return this.#el.querySelector("#second-action")?.checked ?? false;
}
#getHandicap() {
const sel = this.#el.querySelector("#handicap");
return Math.max(0, (parseInt(sel?.value, 10) || 1) - 1);
}
#getSelfCtrl() {
return parseInt(this.#el.querySelector("#self_control")?.value, 10) || 0;
}
#getPoolsSpent() {
return parseInt(this.#el.querySelector("#pools-spent")?.value, 10) || 0;
}
#getUseAmmo() {
return this.#el.querySelector("#use-ammo")?.checked ?? false;
}
#getTotems() {
return {
human: this.#el.querySelector("#human-totem")?.checked ?? false,
adapted: this.#el.querySelector("#adapted-totem")?.checked ?? false
};
}
#getKeepTotem() {
return this.#el.querySelector("#keep-totem-select")?.value ?? null;
}
getDifficultySelect() {
return this.#getDifficultyBase() + (this.#getSecondAction() ? 2 : 0);
}
getSkillCategory() {
if (this.actor.type !== "character" || !this.skillKey) return null;
return this.actor.system.skills[this.skillKey]?.category ?? null;
}
getSkillLevel() {
return this.actor.type === "character" && this.skillValue ? this.skillValue : null;
}
getReroll() {
if (this.actor.type === "npc") {
const exp = this.actor.system?.experience?.value || 1;
return CONFIG.VERMINE.npcExperienceLevels?.[exp]?.rerolls || 0;
}
if (this.actor.type === "creature") return 0;
return this.skillReroll;
}
getMaxEffort() {
if (this.actor.type === "character") return this.actor.system?.abilities?.[this.abilityKey]?.value || 0;
return 0;
}
getPoolBreakdown() {
const ability = this.actor.type === "character"
? (this.actor.system?.abilities?.[this.abilityKey]?.value || 0)
: 0;
const skill = this.actor.type === "character" ? this.skillPool : 0;
const selfControl = this.#getSelfCtrl();
const pools = this.actor.type === "npc" ? this.#getPoolsSpent() : 0;
const specialty = this.#hasSpecialtySelected() ? 1 : 0;
const help = this.#el.querySelector("#helped")?.checked ? 1 : 0;
const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked");
const tooling = toolsChecked && toolsChecked.value !== "0" ? 1 : 0;
const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
const total = this.basePool + selfControl + pools + specialty + help + tooling + group;
return { ability, skill, selfControl, pools, specialty, help, tooling, group, total };
}
getDicePool() {
return this.getPoolBreakdown().total;
}
#hasSpecialtySelected() {
const checked = this.#el.querySelector("input[name='usingSpecialization']:checked");
return Boolean(checked && checked.value !== "aucune");
}
getSpecialtyName() {
const checked = this.#el.querySelector("input[name='usingSpecialization']:checked");
return checked && checked.value !== "aucune" ? checked.value : null;
}
getLabel() {
const base = this.mode === "ranged"
? game.i18n.localize("VERMINE.attack_ranged")
: game.i18n.localize("VERMINE.attack_contact");
return this.weapon ? `${base} (${this.weapon.name})` : base;
}
// ── UI ───────────────────────────────────────────────────────────────
#updateUI() {
const total = this.getDicePool();
const totalEl = this.#el.querySelector("#dice-pool-total");
if (totalEl) totalEl.textContent = `${total}D`;
const finalEls = this.#el.querySelectorAll("#final-difficulty");
for (const el of finalEls) el.textContent = this.getDifficultySelect();
const b = this.getPoolBreakdown();
const bonusEl = this.#el.querySelector("#total-bonus");
if (bonusEl) bonusEl.textContent = b.selfControl + b.pools + b.specialty + b.help + b.tooling + b.group;
const handSel = this.#el.querySelector("#handicap");
const handEl = this.#el.querySelector("#current-handicap");
if (handSel && handEl) {
handEl.textContent = handSel.options[handSel.selectedIndex]?.text ?? "";
}
}
#onInputChange() {
this.#updateUI();
}
#onChangeSelfControl(ev) {
const valEl = this.#el.querySelector("#self_control_value");
if (valEl) valEl.textContent = ev.currentTarget.value;
this.#updateUI();
}
static async #onCancel(event, target) {
this.close();
}
static async #onRoll(event, target) {
const selfCtrl = this.getPoolBreakdown().selfControl;
if (selfCtrl > 0) {
const current = this.actor?.system?.attributes?.self_control?.value ?? 0;
if (current < selfCtrl) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_not_enough_self_control"));
return;
}
}
const finalDifficulty = this.getDifficultySelect();
// Consommation de munitions (armes à distance)
if (this.mode === "ranged" && this.#getUseAmmo() && this.weapon) {
const ammo = this.weapon.system.ammo || 0;
if (ammo > 0) {
await this.weapon.update({ "system.ammo": ammo - 1 });
}
}
if (selfCtrl > 0) {
const newVal = this.actor.system.attributes.self_control.value - selfCtrl;
await this.actor.update({ "system.attributes.self_control.value": newVal });
}
const targets = [...game.user.targets].map(t => ({
id: t.actor?.id ?? null,
uuid: t.actor?.uuid ?? null,
name: t.name,
canDefend: t.actor?.type !== "creature"
}));
const attack = {
id: foundry.utils.randomID(),
attackerId: this.actor.id,
attackerName: this.actor.name,
mode: this.mode,
difficulty: finalDifficulty,
range: this.mode === "ranged" ? this.#getRangeKey() : null,
secondAction: this.#getSecondAction(),
baseDamage: this.baseDamage,
targets,
weapon: this.weapon
? { name: this.weapon.name, img: this.weapon.img, damage: foundry.utils.duplicate(this.weapon.system.damage) }
: null
};
await VermineUtils.roll({
actor: this.actor,
NoD: this.getDicePool(),
Reroll: this.getReroll(),
difficulty: finalDifficulty,
handicap: this.#getHandicap(),
rollLabel: this.getLabel(),
totems: this.#getTotems(),
self_control: selfCtrl,
max_effort: this.getMaxEffort(),
keepTotem: this.#getKeepTotem(),
skillCategory: this.getSkillCategory(),
skillLevel: this.getSkillLevel(),
hasSpecialty: this.#hasSpecialtySelected(),
poolBreakdown: this.getPoolBreakdown(),
specialtyName: this.getSpecialtyName(),
weapon: this.weapon?.toObject() ?? null,
targets: targets.map(t => t.name),
attack,
messageFlags: { "vermine-exchange": attack }
});
this.close();
}
#getRangeKey() {
const el = this.#el.querySelector('input[name="combat-difficulty"]:checked');
return el?.dataset?.rangeKey ?? null;
}
}
@@ -0,0 +1,150 @@
const { HandlebarsApplicationMixin } = foundry.applications.api;
/**
* Dialogue du meneur pour ouvrir une phase d'Expérience.
* Permet d'attribuer les Dés d'Expérience collectifs (Danger, Découvertes,
* Durée) au Groupe et aux personnages, ainsi que les Dés individuels
* (Implication, Influence, Interprétation), les récompenses d'Objectifs et
* la récompense de Traumatisme choisi.
*/
export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
static DEFAULT_OPTIONS = {
classes: ["vermine-roll"],
tag: "form",
window: {
icon: "fas fa-star",
resizable: true
},
position: {
width: 620,
height: 700
},
actions: {
apply: ExperiencePhaseDialog.#onApply,
cancel: ExperiencePhaseDialog.#onCancel
}
};
static PARTS = {
main: { template: "systems/vermine2047/templates/dialogs/experience-phase-dialog.hbs", scrollable: [""] }
};
static async create() {
return new ExperiencePhaseDialog();
}
get title() {
return game.i18n.localize("VERMINE.experience_phase");
}
async _prepareContext() {
const characters = game.actors.filter(a => a.type === "character");
const groups = game.actors.filter(a => a.type === "group");
return {
characters: characters.map(a => ({
id: a.id,
name: a.name,
dice: a.system?.experience?.dice || 0
})),
groups: groups.map(a => ({
id: a.id,
name: a.name,
dice: a.system?.experience?.dice || 0
})),
rewards: [
{ value: 0, label: game.i18n.localize("VERMINE.reward_anecdotic") },
{ value: 1, label: game.i18n.localize("VERMINE.reward_minor") },
{ value: 2, label: game.i18n.localize("VERMINE.reward_major") },
{ value: 3, label: game.i18n.localize("VERMINE.reward_exceptionnal") }
]
};
}
async _onRender(context, options) {
this.element.querySelectorAll("[data-roll]").forEach(inp => inp.addEventListener("change", () => this.#updateUI()));
this.#updateUI();
}
#updateUI() {
const danger = parseInt(this.element.querySelector("#collective-danger")?.value, 10) || 0;
const discoveries = parseInt(this.element.querySelector("#collective-discoveries")?.value, 10) || 0;
const duration = parseInt(this.element.querySelector("#collective-duration")?.value, 10) || 0;
const el = this.element.querySelector("#collective-total");
if (el) el.textContent = `${danger + discoveries + duration}D`;
}
static async #onCancel(event, target) {
this.close();
}
static async #onApply(event, target) {
const collective = (parseInt(this.element.querySelector("#collective-danger")?.value, 10) || 0)
+ (parseInt(this.element.querySelector("#collective-discoveries")?.value, 10) || 0)
+ (parseInt(this.element.querySelector("#collective-duration")?.value, 10) || 0);
const groupId = this.element.querySelector("#group-select")?.value || null;
if (groupId) {
const group = game.actors.get(groupId);
if (group && collective > 0) {
const current = group.system?.experience?.dice || 0;
await group.update({ "system.experience.dice": current + collective });
}
}
const results = [];
for (const row of this.element.querySelectorAll("[data-character-id]")) {
const id = row.dataset.characterId;
const actor = game.actors.get(id);
if (!actor) continue;
const implication = parseInt(row.querySelector(`[name="implication-${id}"]`)?.value, 10) || 0;
const influence = parseInt(row.querySelector(`[name="influence-${id}"]`)?.value, 10) || 0;
const interpretation = parseInt(row.querySelector(`[name="interpretation-${id}"]`)?.value, 10) || 0;
const objectiveMinor = row.querySelector(`[name="objective-minor-${id}"]`)?.checked ? 3 : 0;
const objectiveMajor = row.querySelector(`[name="objective-major-${id}"]`)?.checked ? 5 : 0;
const trauma = row.querySelector(`[name="trauma-${id}"]`)?.checked ? 5 : 0;
const individual = implication + influence + interpretation;
const bonus = objectiveMinor + objectiveMajor + trauma;
const gained = collective + individual + bonus;
const ex = actor.system?.experience;
const nextPhase = (ex?.phase || 0) + 1;
const update = {
"system.experience.phase": nextPhase,
"system.experience.dice": (ex?.dice || 0) + gained,
"system.experience.collective.danger": parseInt(this.element.querySelector("#collective-danger")?.value, 10) || 0,
"system.experience.collective.discoveries": parseInt(this.element.querySelector("#collective-discoveries")?.value, 10) || 0,
"system.experience.collective.duration": parseInt(this.element.querySelector("#collective-duration")?.value, 10) || 0,
"system.experience.individual.implication": implication,
"system.experience.individual.influence": influence,
"system.experience.individual.interpretation": interpretation,
"system.experience.traumaReward": trauma > 0,
"system.experience.learned.phase": nextPhase,
"system.experience.learned.skill": false,
"system.experience.learned.specialty": false,
"system.experience.learned.reserve": false,
"system.experience.learned.characteristic": false,
"system.experience.learned.evolution": false
};
await actor.update(update);
results.push({ name: actor.name, gained });
}
const lines = [`${game.i18n.localize("VERMINE.experience_phase")} +1`];
if (groupId) {
const group = game.actors.get(groupId);
lines.push(`${group?.name ?? ""} (${game.i18n.localize("VERMINE.collective")}): +${collective}D`);
}
for (const r of results) {
lines.push(`${r.name}: +${r.gained}D`);
}
await ChatMessage.create({
user: game.user?._id,
speaker: ChatMessage.getSpeaker(),
content: `<div class="vermine-roll-message"><h3>${game.i18n.localize("VERMINE.experience_rewards")}</h3><div class="roll-context flexrow flex-wrap">${lines.map(l => `<span class="context-item">${l}</span>`).join("")}</div></div>`
});
this.close();
}
}
+327
View File
@@ -0,0 +1,327 @@
import { VermineUtils } from "../roll.mjs";
import { VermineExperience } from "../experience.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
/**
* Dialogue d'apprentissage (phase d'Expérience).
* Permet de dépenser des Dés d'Expérience pour tenter de développer une
* Compétence, une Spécialité, une Caractéristique, une Réserve ou un
* Dé d'Évolution. La progression est appliquée automatiquement en cas de
* réussite.
*/
export default class LearningDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
#actor;
static DEFAULT_OPTIONS = {
classes: ["vermine-roll"],
tag: "form",
window: {
icon: "fas fa-graduation-cap",
resizable: false
},
position: {
width: 560,
height: 640
},
actions: {
roll: LearningDialog.#onRoll,
cancel: LearningDialog.#onCancel
}
};
static PARTS = {
main: { template: "systems/vermine2047/templates/dialogs/learning-dialog.hbs", scrollable: [""] }
};
static async create({ actorId }) {
const actor = await game.actors.get(actorId);
if (!actor) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
return null;
}
return new LearningDialog({ actor });
}
constructor(options = {}) {
super(options);
this.#actor = options.actor;
}
get title() {
return game.i18n.localize("VERMINE.open_learning");
}
async _prepareContext() {
const actor = this.#actor;
const skills = Object.entries(CONFIG.VERMINE.skills).map(([key, cfg]) => ({
key,
name: game.i18n.localize("SKILLS." + key + ".name"),
category: cfg.category,
value: actor.system?.skills?.[key]?.value || 0,
target: Math.min(5, (actor.system?.skills?.[key]?.value || 0) + 1),
rarity: actor.system?.skills?.[key]?.rarity || 0,
isDomain: VermineExperience.isPreferredDomain(actor, key)
}));
return {
actor,
system: actor.system,
config: CONFIG.VERMINE,
dice: actor.system?.experience?.dice || 0,
ageType: actor.system?.identity?.ageType || 2,
abilities: Object.entries(CONFIG.VERMINE.abilities).map(([key, label]) => ({
key,
label: game.i18n.localize(label),
value: actor.system?.abilities?.[key]?.value || 0
})),
reserves: [
{ key: "self_control", label: game.i18n.localize("VERMINE.self_control"), value: actor.system?.attributes?.self_control?.value || 0, max: actor.system?.attributes?.self_control?.max || 0 },
{ key: "effort", label: game.i18n.localize("VERMINE.effort"), value: actor.system?.attributes?.effort?.value || 0, max: actor.system?.attributes?.effort?.max || 0 }
],
specialties: actor.itemTypes?.specialty || [],
skills,
specialtyCount: (actor.itemTypes?.specialty || []).length
};
}
async _onRender(context, options) {
this.element.dataset.actorId = this.#actor.id;
for (const inp of this.element.querySelectorAll("[data-roll]")) {
inp.addEventListener("change", () => this.#updateUI());
}
this.element.querySelector("#learning-dice")?.addEventListener("input", () => this.#updateUI());
this.#updateUI();
}
get #el() { return this.element; }
#getType() {
return this.#el.querySelector('input[name="learning-type"]:checked')?.value ?? "skill";
}
#getTargetKey() {
const type = this.#getType();
if (type === "characteristic") return this.#el.querySelector("#learning-ability")?.value ?? "";
if (type === "reserve") return this.#el.querySelector("#learning-reserve")?.value ?? "";
return this.#el.querySelector("#learning-target")?.value ?? "";
}
#getDiceSpent() {
return Math.max(1, parseInt(this.#el.querySelector("#learning-dice")?.value, 10) || 1);
}
#getSpecialtyName() {
return this.#el.querySelector("#specialty-name")?.value?.trim() || game.i18n.localize("ITEMS.new_specialty");
}
#compute() {
const actor = this.#actor;
const type = this.#getType();
const key = this.#getTargetKey();
const difficulty = 7;
let handicap = 0;
let poolModifier = 0;
let label = "";
let targetLevel = null;
if (type === "skill") {
const current = key ? actor.system?.skills?.[key]?.value || 0 : 0;
targetLevel = Math.min(5, current + 1);
const rarity = key ? actor.system?.skills?.[key]?.rarity || 0 : 0;
label = game.i18n.localize(VermineExperience.skillLevelLabelKey(targetLevel));
const diff = VermineExperience.skillLearningDifficulty(targetLevel);
const domain = key ? VermineExperience.isPreferredDomain(actor, key) : false;
return {
type,
key,
targetLevel,
difficulty: domain ? Math.max(3, diff - 2) : diff,
handicap: VermineExperience.skillLearningHandicap(targetLevel, rarity),
poolModifier: 0,
label,
targetLabel: key ? game.i18n.localize("SKILLS." + key + ".name") : ""
};
}
if (type === "specialty") {
const current = key ? actor.system?.skills?.[key]?.value || 0 : 0;
const domain = key ? VermineExperience.isPreferredDomain(actor, key) : false;
label = game.i18n.localize("VERMINE.specialty");
return {
type,
key,
targetLevel: current,
difficulty: VermineExperience.specialtyLearningDifficulty(current, domain),
handicap: VermineExperience.specialtyLearningHandicap(actor.itemTypes?.specialty?.length || 0),
poolModifier: 0,
label,
targetLabel: key ? game.i18n.localize("SKILLS." + key + ".name") : ""
};
}
if (type === "characteristic") {
const current = key ? actor.system?.abilities?.[key]?.value || 0 : 0;
label = game.i18n.localize("VERMINE.characteristic");
return {
type,
key,
targetLevel: current,
difficulty: VermineExperience.characteristicLearningDifficulty(),
handicap: current,
poolModifier: 0,
label,
targetLabel: key ? game.i18n.localize(CONFIG.VERMINE.abilities[key]) : ""
};
}
if (type === "reserve") {
const current = key ? actor.system?.attributes?.[key]?.value || 0 : 0;
label = game.i18n.localize("VERMINE.reserve");
return {
type,
key,
targetLevel: current,
difficulty: VermineExperience.reserveLearningDifficulty(current),
handicap: VermineExperience.ageHandicap(actor.system?.identity?.ageType || 2),
poolModifier: 0,
label,
targetLabel: key ? game.i18n.localize(key === "self_control" ? "VERMINE.self_control" : "VERMINE.effort") : ""
};
}
// evolution
const human = actor.system?.adaptation?.totems?.human?.value || 0;
const adapted = actor.system?.adaptation?.totems?.adapted?.value || 0;
label = game.i18n.localize("VERMINE.evolution_dice");
return {
type,
key,
targetLevel: actor.system?.experience?.evolution || 0,
difficulty: VermineExperience.evolutionDifficulty(actor.system?.identity?.ageType || 2),
handicap: actor.system?.experience?.evolution || 0,
poolModifier: VermineExperience.evolutionTotemModifier(human, adapted),
label,
targetLabel: ""
};
}
#updateUI() {
const type = this.#getType();
const show = (id, visible) => {
const el = this.#el.querySelector(id);
if (el) el.style.display = visible ? "" : "none";
};
show("#learning-skill-section", type === "skill" || type === "specialty");
show("#learning-specialty-section", type === "specialty");
show("#learning-ability-section", type === "characteristic");
show("#learning-reserve-section", type === "reserve");
show("#learning-evolution-section", type === "evolution");
// Pour une Spécialité, seule une compétence au moins Confirmée (≥2) est valable.
const skillSelect = this.#el.querySelector("#learning-target");
if (skillSelect) {
for (const opt of skillSelect.options) {
const value = parseInt(opt.dataset.value, 10) || 0;
opt.hidden = (type === "specialty") ? value < 2 : false;
}
}
const c = this.#compute();
const set = (sel, txt) => { const el = this.#el.querySelector(sel); if (el) el.textContent = txt; };
set("#diff-value", c.difficulty);
set("#handicap-value", c.handicap);
set("#required-value", 1 + c.handicap);
const pool = Math.max(0, this.#getDiceSpent() + c.poolModifier);
set("#pool-value", `${pool}D${c.poolModifier ? ` (${c.poolModifier >= 0 ? "+" : ""}${c.poolModifier}D)` : ""}`);
const poolInput = this.#el.querySelector("#learning-dice");
if (poolInput) poolInput.max = this.#actor.system?.experience?.dice || 0;
}
static async #onCancel(event, target) {
this.close();
}
static async #onRoll(event, target) {
const actor = this.#actor;
const ex = actor.system?.experience;
const dice = ex?.dice || 0;
const spent = Math.min(this.#getDiceSpent(), dice);
if (spent < 1) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_experience_dice"));
return;
}
const c = this.#compute();
// Limite : une seule compétence, spécialité, réserve, caractéristique et
// un seul Dé d'Évolution par phase.
if (ex?.learned?.phase === ex?.phase) {
const flagMap = { skill: "skill", specialty: "specialty", reserve: "reserve", characteristic: "characteristic", evolution: "evolution" };
if (ex.learned[flagMap[c.type]]) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_learning_limit"));
return;
}
}
if (c.type === "skill" && c.targetLevel !== null && (actor.system?.skills?.[c.key]?.value || 0) >= 5) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_skill_max"));
return;
}
if (c.type === "characteristic" && c.targetLevel !== null && (actor.system?.abilities?.[c.key]?.value || 0) >= 3) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_characteristic_max"));
return;
}
if (c.type === "specialty" && c.targetLevel !== null && c.targetLevel < 2) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_specialty_requirement"));
return;
}
if (c.type === "reserve" && c.targetLevel !== null && (actor.system?.attributes?.[c.key]?.max || 0) >= 10) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_reserve_max"));
return;
}
const pool = Math.max(0, spent + c.poolModifier);
const rollLabel = `${game.i18n.localize("VERMINE.open_learning")}${c.label}${c.targetLabel ? " (" + c.targetLabel + ")" : ""}`;
const roll = await VermineUtils.roll({
actor,
NoD: pool,
Reroll: 0,
difficulty: c.difficulty,
handicap: c.handicap,
rollLabel,
poolBreakdown: { ability: 0, skill: 0, selfControl: 0, specialty: 0, help: 0, tooling: 0, group: 0, total: pool },
messageFlags: { "vermine-learning": { type: c.type, key: c.key, spent, success: false } }
});
const required = 1 + c.handicap;
const specialtyName = this.#getSpecialtyName();
if ((roll._total ?? 0) >= required) {
await this.#applySuccess(c, spent, specialtyName);
}
this.close();
}
async #applySuccess(c, spent, specialtyName) {
const actor = this.#actor;
const updates = { "system.experience.dice": (actor.system.experience.dice || 0) - spent };
if (c.type === "skill") {
const current = actor.system.skills[c.key].value || 0;
updates["system.skills." + c.key + ".value"] = Math.min(5, current + 1);
updates["system.experience.learned.skill"] = true;
} else if (c.type === "specialty") {
await actor.createEmbeddedDocuments("Item", [{ name: specialtyName, type: "specialty", system: { skill: c.key } }]);
updates["system.experience.learned.specialty"] = true;
} else if (c.type === "characteristic") {
const current = actor.system.abilities[c.key].value || 0;
updates["system.abilities." + c.key + ".value"] = Math.min(3, current + 1);
updates["system.experience.learned.characteristic"] = true;
} else if (c.type === "reserve") {
const bonus = actor.system.experience.reserveBonus?.[c.key] || 0;
updates["system.experience.reserveBonus." + c.key] = bonus + 1;
updates["system.experience.learned.reserve"] = true;
} else {
updates["system.experience.evolution"] = (actor.system.experience.evolution || 0) + 1;
updates["system.experience.learned.evolution"] = true;
}
updates["system.experience.learned.phase"] = actor.system.experience.phase;
await actor.update(updates);
ui.notifications.info(game.i18n.localize("VERMINE.learning_success"));
}
}
+208
View File
@@ -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()
}
}
+171 -23
View File
@@ -23,7 +23,8 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
},
actions: {
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 = {}) {
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;
if (!actorId || typeof actorId !== "string") {
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"));
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 = {}) {
@@ -50,10 +54,16 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
this.#actor = options.actor;
this.label = options.label ?? null;
this.rolltype = options.rolltype ?? null;
this.weapon = options.weapon ?? null;
this.locks = options.locks ?? null;
this.defense = options.defense ?? null;
}
async _prepareContext() {
const actor = this.#actor;
const isCharacter = actor.type === "character";
const group = isCharacter ? RollDialog.#findGroup(actor) : null;
const groupPool = group?.system?.experience?.dice || 0;
return {
actor,
system: actor.system,
@@ -66,13 +76,28 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
help: false,
specialty: false,
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) {
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]")) {
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.#displaySpecialties();
this.#refreshExperienceUI();
this.#updateUI();
if (ability?.value !== "0") {
@@ -111,18 +137,44 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
#getDifficulty() { return this.#el.querySelector("#difficulty"); }
#getHandicap() { return this.#el.querySelector("#handicap"); }
#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 abilVal = parseInt(abil?.options[abil?.selectedIndex]?.value, 10) || 0;
const ability = parseInt(abil?.options[abil?.selectedIndex]?.value, 10) || 0;
const skill = this.#getSkill();
const skillPool = parseInt(skill?.options[skill?.selectedIndex]?.dataset?.pool, 10) || 0;
const sc = parseInt(this.#getSelfCtrl()?.value, 10) || 0;
const specChecked = this.#el.querySelector("#usingSpecialization")?.checked;
const selfControl = parseInt(this.#getSelfCtrl()?.value, 10) || 0;
const specChecked = this.hasSpecialtySelected();
const helped = this.#el.querySelector("#helped")?.checked;
const tools = this.#el.querySelector("input[name='usingTools']:checked")?.value !== "0";
const bonuses = (specChecked ? 1 : 0) + (helped ? 1 : 0) + (tools ? 1 : 0);
return (abilVal + sc + skillPool + bonuses) || 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 experience = this.#getExperiencePull();
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() {
@@ -199,6 +251,40 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
// ── 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() {
for (const el of this.#el.querySelectorAll("[data-spec-skill]")) {
el.style.display = "inline";
@@ -206,18 +292,32 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
}
#calculateBonusCount() {
let b = 0;
if (this.#el.querySelector("#helped")?.checked) b += 1;
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");
if (tools && tools.value !== "0") b += 1;
const human = this.#el.querySelector("#human-totem");
if (human?.checked) b += parseInt(this.#actor?.system?.adaptation?.totems?.human?.value, 10) || 0;
const adapted = this.#el.querySelector("#adapted-totem");
if (adapted?.checked) b += parseInt(this.#actor?.system?.adaptation?.totems?.adapted?.value, 10) || 0;
if (this.hasSpecialtySelected()) b += 1;
return b;
const b = this.getPoolBreakdown();
return b.specialty + b.help + b.tooling + b.group + b.selfControl + b.experience;
}
#refreshExperienceUI() {
const actor = this.#actor;
const group = RollDialog.#findGroup(actor);
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() {
@@ -284,6 +384,29 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
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) {
const selfCtrl = this.getSelfControl();
if (selfCtrl > 0) {
@@ -305,6 +428,26 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
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({
actor: this.#actor,
NoD: this.getDicePool(),
@@ -318,7 +461,12 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
keepTotem: this.getKeepTotem(),
skillCategory: this.getSkillCategory(),
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();
-1
View File
@@ -54,7 +54,6 @@ export function prepareActiveEffectCategories(effects) {
// Iterate over active effects, classifying them into categories
for ( let e of effects ) {
e._getSourceName(); // Trigger a lookup for the source name
if ( e.disabled ) categories.inactive.effects.push(e);
else if ( e.isTemporary ) categories.temporary.effects.push(e);
else categories.passive.effects.push(e);
+509
View File
@@ -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} &middot; ${t('VERMINE.defender_successes')}: ${r.defenderSuccesses} &rarr; ${verdict}</div>`
]
if (r.hit) {
lines.push(`<div class="resolution-line">${t('VERMINE.raw_damage')}: <strong>${r.damage}</strong>${r.protection ? ` &minus; ${t('ADVERSITY.protection')}: ${r.protection}` : ''} = ${t('VERMINE.net_damage')}: <strong>${r.netDamage}</strong></div>`)
if (r.woundCategory) {
lines.push(`<div class="resolution-line res-wound">${t('VERMINE.wound')}: <strong>${t(this.woundLabelKey(r.woundCategory))}</strong> (${t('VERMINE.wounds.threshold')} ${r.woundThreshold}D)</div>`)
} else {
lines.push(`<div class="resolution-line res-noshock">${t('VERMINE.no_wound')}</div>`)
}
} else {
lines.push(`<div class="resolution-line res-nodamage">${t('VERMINE.no_damage')}</div>`)
}
const apply = (r.hit && r.woundCategory && r.canApply)
? `<button type="button" class="exchange-apply" data-action="exchange-apply" data-defender-id="${r.defenderId}" data-defender-uuid="${r.defenderUuid}" data-wound="${r.woundCategory}"><i class="fas fa-briefcase-medical"></i> ${t('VERMINE.apply_wound')}</button>`
: ''
return `<div class="exchange-resolution">
<div class="resolution-header"><i class="fas fa-exchange-alt"></i> ${t('VERMINE.resolve_result')}</div>
${lines.join('')}
${apply}
</div>`
}
// ── Liaison chat ────────────────────────────────────────────────────
/**
* Lie les interactions d'échange de coups sur un message de chat.
* @param {HTMLElement} html élément du message
* @param {ChatMessage} message
*/
static bindChat(html, message) {
const exchange = message.getFlag('world', FLAG_EXCHANGE)
if (exchange) {
html.querySelectorAll('.exchange-defend').forEach(btn => {
btn.addEventListener('click', ev => this.#onDefense(ev, message))
})
html.querySelectorAll('.exchange-resolve').forEach(btn => {
btn.addEventListener('click', ev => this.#onResolve(ev, message))
})
}
html.querySelectorAll('[data-action="exchange-apply"]').forEach(btn => {
btn.addEventListener('click', ev => this.#onApply(ev, message))
})
}
static async #onDefense(ev, attackMessage) {
ev.preventDefault()
ev.stopPropagation()
const btn = ev.currentTarget
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
if (!exchange) return
const type = btn.dataset.type
const actor = await this.#resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
if (!actor) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
return
}
if (!game.user.isGM && !actor.isOwner) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
return
}
if (actor.type === 'creature') {
ui.notifications.warn(game.i18n.localize('VERMINE.error_creature_no_defense'))
return
}
const 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
}
}
+146
View File
@@ -0,0 +1,146 @@
/**
* Règles d'apprentissage (phase d'Expérience).
* Helpers purs de difficultés, handicaps et calculs de jets d'Expérience.
*/
export class VermineExperience {
/**
* Difficulté d'un jet d'apprentissage de Compétence selon le niveau visé.
* @param {number} targetLevel niveau visé (1-5)
* @returns {number}
*/
static skillLearningDifficulty(targetLevel) {
switch (parseInt(targetLevel, 10)) {
case 1: return 5 // Débutant
case 2: return 7 // Confirmé
case 3: return 9 // Expert
case 4: return 10 // Maître
case 5: return 10 // Légende
default: return 7
}
}
/**
* Handicap d'un jet d'apprentissage de Compétence.
* Rareté (1-3) + handicap (I) supplémentaire pour le niveau Légende.
* @param {number} targetLevel niveau visé (1-5)
* @param {number} rarity rareté de la compétence (0-3)
* @returns {number}
*/
static skillLearningHandicap(targetLevel, rarity) {
let handicap = parseInt(rarity, 10) || 0
if (parseInt(targetLevel, 10) === 5) handicap += 1 // Légende : (I)
return handicap
}
/**
* Difficulté d'un jet d'apprentissage de Spécialité selon le niveau actuel
* de la compétence associée. Domaine de prédilection : -2.
* @param {number} skillLevel niveau actuel de la compétence (2-5)
* @param {boolean} isDomain la compétence est dans le domaine de prédilection
* @returns {number}
*/
static specialtyLearningDifficulty(skillLevel, isDomain) {
let difficulty
switch (parseInt(skillLevel, 10)) {
case 2: difficulty = 9 // Confirmé
break
case 3: difficulty = 7 // Expert
break
case 4: difficulty = 5 // Maître
break
case 5: difficulty = 5 // Légende (relance automatique)
break
default: difficulty = 9
}
return isDomain ? difficulty - 2 : difficulty
}
/**
* Handicap d'un jet d'apprentissage de Spécialité : nombre de spécialités
* déjà possédées (requiert au moins Confirmé dans la compétence associée).
* @param {number} specialtyCount
* @returns {number}
*/
static specialtyLearningHandicap(specialtyCount) {
return Math.max(0, parseInt(specialtyCount, 10) || 0)
}
/**
* Difficulté d'un jet d'apprentissage de Caractéristique.
* @returns {number}
*/
static characteristicLearningDifficulty() {
return 9
}
/**
* Difficulté d'un jet d'apprentissage de Réserve : valeur actuelle.
* @param {number} currentValue
* @returns {number}
*/
static reserveLearningDifficulty(currentValue) {
return Math.max(0, parseInt(currentValue, 10) || 0)
}
/**
* Handicap d'âge pour l'apprentissage des Réserves et autres jets liés à l'âge.
* @param {number} ageType 1=Jeune, 2=Adulte, 3=Ancien
* @returns {number}
*/
static ageHandicap(ageType) {
switch (parseInt(ageType, 10)) {
case 1: return 1 // Jeune (I)
case 3: return 3 // Ancien (III)
default: return 2 // Adulte (II)
}
}
/**
* Difficulté d'un jet d'acquisition d'un Dé d'Évolution selon l'âge.
* @param {number} ageType 1=Jeune, 2=Adulte, 3=Ancien
* @returns {number}
*/
static evolutionDifficulty(ageType) {
switch (parseInt(ageType, 10)) {
case 1: return 5
case 3: return 9
default: return 7
}
}
/**
* Bonus/Malus de Totem sur le jet de Dé d'Évolution.
* Plus de Dés Adaptés que Humains : +1D. Inversement : -1D.
* @param {number} human human totem dice
* @param {number} adapted adapted totem dice
* @returns {number}
*/
static evolutionTotemModifier(human, adapted) {
if (adapted > human) return 1
if (human > adapted) return -1
return 0
}
/**
* Clé i18n du libellé du niveau de compétence visé.
* @param {number} level
* @returns {string}
*/
static skillLevelLabelKey(level) {
return CONFIG.VERMINE.SkillLevels?.[level]?.label ?? "SKILL_LEVELS.incompetent"
}
/**
* Détermine si une compétence appartient au domaine de prédilection.
* @param {Actor} actor
* @param {string} skillKey
* @returns {boolean}
*/
static isPreferredDomain(actor, skillKey) {
const category = actor.system?.skills?.[skillKey]?.category
const preferred = actor.system?.skill_categories?.preferred
return Boolean(category && preferred && category === preferred)
}
}
+64 -15
View File
@@ -184,10 +184,9 @@ export class VermineCombat extends Combat {
// encounter check
}
async rollInitiative(ids, formula = undefined, messageOptions = {}) {
async rollInitiative(ids, {formula=null, updateTurn=true, messageMode, messageOptions={}}={}) {
// roll initiative
return super.rollInitiative(ids, formula, messageOptions)
return super.rollInitiative(ids, {formula, updateTurn, messageMode, messageOptions});
}
nextRound() {
@@ -224,24 +223,74 @@ export class VermineCombat extends Combat {
export class VermineCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
get template() {
return "systems/vermine2047/templates/combat-tracker.hbs";
}
async getData(options) {
const context = await super.getData(options);
if (!context.hasCombat) {
return context;
/** @override */
static PARTS = {
main: {
template: "systems/vermine2047/templates/combat-tracker.hbs",
root: true
}
};
/** @override */
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.user = game.user;
const combat = this.viewed;
const hasCombat = combat !== null;
const combats = this.combats;
const currentIdx = combats.indexOf(combat);
const isPlayerTurn = combat?.combatant?.players?.includes(game.user);
const canControl = combat?.turn && combat.turn.between(1, combat.turns.length - 2)
? combat.canUserModify(game.user, "update", { turn: 0 })
: combat?.canUserModify(game.user, "update", { round: 0 });
Object.assign(context, {
combat,
hasCombat,
combatCount: combats.length,
currentIndex: currentIdx + 1,
previousId: combats[currentIdx - 1]?.id,
nextId: combats[currentIdx + 1]?.id,
round: combat?.round,
control: isPlayerTurn && canControl,
linked: combat?.scene !== null,
cssClass: combats.length > 7 ? "cycle" : combats.length ? "tabbed" : "",
cssId: "combat",
tabName: "combat",
labels: {
scope: game.i18n.localize(`COMBAT.${combat?.scene ? "Linked" : "Unlinked"}`)
},
turns: []
});
if ( combat ) {
for ( const [i, combatant] of combat.turns.entries() ) {
if ( !combatant.visible ) continue;
context.turns.push(await this._prepareTurnContext(combat, combatant, i));
}
}
return context;
}
activateListeners(html) {
super.activateListeners(html);
html.querySelectorAll("[data-attitude]").forEach(el => {
/** @override */
async _prepareTurnContext(combat, combatant, index) {
const turn = await super._prepareTurnContext(combat, combatant, index);
const actor = combatant.actor;
turn.attitude = combatant.getFlag("world", "attitude") || "active";
turn.isPlayer = actor?.hasPlayerOwner ?? false;
turn.isNpc = !!actor && !actor.hasPlayerOwner;
turn.owner = combatant.isOwner;
turn.defeated = combatant.isDefeated;
turn.hasRolled = combatant.initiative !== null;
turn.hasResource = combatant.resource !== null && combatant.resource !== undefined;
turn.effects = (turn.effects?.icons ?? []).map(e => e.img);
return turn;
}
_onRender(context, options) {
super._onRender(context, options);
this.element.querySelectorAll("[data-attitude]").forEach(el => {
el.addEventListener("click", this._setStatut.bind(this));
});
}
+31
View File
@@ -12,6 +12,7 @@ export const preloadHandlebarsTemplates = async function () {
"systems/vermine2047/templates/actor/parts/actor-items.hbs",
"systems/vermine2047/templates/actor/parts/actor-weapons.hbs",
"systems/vermine2047/templates/actor/parts/actor-defenses.hbs",
"systems/vermine2047/templates/actor/parts/actor-equipped-gear.hbs",
// Character partials.
"systems/vermine2047/templates/actor/character/character-id.hbs",
@@ -32,6 +33,7 @@ export const preloadHandlebarsTemplates = async function () {
// npc partials
"systems/vermine2047/templates/actor/npc/npc-combat.hbs",
"systems/vermine2047/templates/actor/parts/npc-skill-category.hbs",
"systems/vermine2047/templates/actor/parts/npc-skill-item.hbs",
// creature partials
"systems/vermine2047/templates/actor/creature/creature-combat.hbs",
@@ -100,6 +102,21 @@ export const registerHandlebarsHelpers = function () {
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) {
return game.i18n.localize(arrayLabel + "." + objectLabel + "." + key);
});
@@ -347,6 +364,9 @@ export const registerHandlebarsHelpers = function () {
Handlebars.registerHelper('ifincludes', function (arg1, arg2, options) {
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
@@ -388,4 +408,15 @@ export const registerHandlebarsHelpers = function () {
Handlebars.registerHelper("setVar", function (varName, varValue, options) {
options.data.root[varName] = varValue;
});
// assign local variables in the current context (used by roll-message.hbs)
Handlebars.registerHelper("set", function (options) {
const ctx = (this && typeof this === "object") ? this : options.contexts?.[0];
if ( ctx && typeof ctx === "object" ) {
for ( const [key, value] of Object.entries(options.hash) ) {
ctx[key] = value;
}
}
return "";
});
}
+10 -6
View File
@@ -41,6 +41,10 @@ export const registerHooks = function () {
});
Hooks.on('renderChatMessageHTML', async (message, html, data) => {
// Échanges de coups : liaisons ouvertes à tous (propriétaires de cibles inclus)
const { VermineExchange } = await import('./exchange.mjs');
VermineExchange.bindChat(html, message);
let rerollTitle = html.querySelector(".reroll-fromroll h4");
if (rerollTitle) {
rerollTitle.addEventListener("click", () => { html.querySelector(".reroll").classList.toggle('visible') })
@@ -69,8 +73,8 @@ export const registerHooks = function () {
});
// changement de la pause
Hooks.on("renderPause", async function () {
const pauseEl = document.getElementById("pause");
Hooks.on("renderGamePause", async function (app, element) {
const pauseEl = element ?? document.getElementById("pause");
if (!pauseEl || pauseEl.className !== "paused") return;
pauseEl.querySelectorAll("img").forEach(img => {
img.src = 'systems/vermine2047/assets/images/ui/vermine_pause.webp';
@@ -94,17 +98,17 @@ export const registerHooks = function () {
});
Hooks.on('getSceneControlButtons', async (controls) => {
const tokenControls = controls.token;
const tokenControls = controls.tokens;
if (tokenControls && tokenControls.tools) {
tokenControls.tools.push({
tokenControls.tools['dice-roller'] = {
name: 'Dice Roller',
title: game.i18n.localize("VERMINE.RollTool"),
icon: 'fas fa-dice-d10',
button: true,
onClick() {
onChange() {
RollDialog.create().then(d => d.render(true));
}
});
};
}
});
+141 -105
View File
@@ -29,7 +29,14 @@ export class VermineUtils {
keepTotem = null,
skillLevel = null,
hasSpecialty = false,
handicap = 0
handicap = 0,
bonusSuccesses = 0,
poolBreakdown = null,
specialtyName = null,
weapon = null,
targets = [],
attack = null,
messageFlags = null
}) {
// Validate inputs
if (!actor) {
@@ -49,22 +56,13 @@ export class VermineUtils {
totemBonus = this._calculateTotemDomainBonuses(skillCategory, actor);
}
// Apply auto-successes and auto-thresholds
let autoSuccesses = 0;
// Apply automatic successes from skill mastery
let adjustedDifficulty = difficulty;
if (skillLevel !== null && skillLevel !== undefined) {
autoSuccesses = this._calculateAutoSuccesses(skillLevel, hasSpecialty);
const autoThreshold = this._getAutoThreshold(skillLevel);
if (autoThreshold !== null) {
adjustedDifficulty = autoThreshold;
}
}
// Handle human totem
if (totems.human) {
NoD--;
const humanDifficulty = skillLevel !== null ? Math.max(adjustedDifficulty, difficulty) : adjustedDifficulty;
const humanDifficulty = adjustedDifficulty;
const humanFormula = `(1D10cs>=${humanDifficulty}[human_${safeUserName}]*2)`;
// Apply domain bonus/malus
@@ -78,7 +76,7 @@ export class VermineUtils {
// Handle adapted totem
if (totems.adapted) {
NoD--;
const adaptedDifficulty = skillLevel !== null ? Math.max(adjustedDifficulty, difficulty) : adjustedDifficulty;
const adaptedDifficulty = adjustedDifficulty;
const adaptedFormula = `(1D10cs>=${adaptedDifficulty}[adapted_${safeUserName}]*2)`;
// Apply domain bonus/malus
@@ -105,8 +103,8 @@ export class VermineUtils {
}
}
// Apply handicap (wounds/maluses reduce dice pool)
NoD = Math.max(1, NoD - handicap)
// Ensure the dice pool cannot go below zero
NoD = Math.max(0, NoD)
// Build base formula
const baseFormula = `${NoD}d10cs>=${adjustedDifficulty}[regular_${safeUserName}]`;
@@ -114,6 +112,27 @@ export class VermineUtils {
// Build final formula
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
const roll = new Roll(formula, actor.getRollData());
@@ -126,13 +145,12 @@ export class VermineUtils {
skillCategory: skillCategory,
skillLevel: skillLevel,
hasSpecialty: hasSpecialty,
autoSuccesses: autoSuccesses,
totemBonuses: { ...totemBonus },
baseNoD: NoD,
rerolls: Reroll,
selfControl: self_control
selfControl: self_control,
bonusSuccesses: bonusSuccesses
};
// Evaluate the roll
await roll.evaluate();
@@ -152,7 +170,15 @@ export class VermineUtils {
skillCategory,
keepTotem,
skillLevel,
hasSpecialty
hasSpecialty,
handicap,
bonusSuccesses,
poolBreakdown,
specialtyName,
weapon,
targets,
attack,
messageFlags
});
return roll;
@@ -205,70 +231,13 @@ export class VermineUtils {
return bonuses;
}
/**
* Calculates automatic successes based on skill mastery level.
* @param {number} skillLevel - Skill level (0-5)
* @param {boolean} [hasSpecialty=false] - Whether a specialty is used
* @returns {number} Number of automatic successes
*/
static _calculateAutoSuccesses(skillLevel, hasSpecialty = false) {
// According to Vermine2047 rules, automatic successes are based on mastery level:
// Level 0 (Incompetent): 0 automatic successes
// Level 1 (Beginner): 0 automatic successes
// Level 2 (Proficient): 1 automatic success if specialty is used
// Level 3 (Expert): 1 automatic success
// Level 4 (Master): 1 automatic success + 1 if specialty is used
// Level 5 (Legend): 2 automatic successes
if (!skillLevel) return 0;
let autoSuccesses = 0;
switch (skillLevel) {
case 2: // Compétent
if (hasSpecialty) autoSuccesses = 1;
break;
case 3: // Expert
autoSuccesses = 1;
break;
case 4: // Maître
autoSuccesses = 1;
if (hasSpecialty) autoSuccesses += 1;
break;
case 5: // Légende
autoSuccesses = 2;
break;
default:
autoSuccesses = 0;
}
return autoSuccesses;
}
/**
* Determines the automatic threshold if the skill is not mastered.
* @param {number} skillLevel - Skill level
* @returns {number|null} Automatic threshold or null if skill is mastered
*/
static _getAutoThreshold(skillLevel) {
// If the skill is not mastered (level 0 or 1), use a default threshold
// Level 0 (Incompetent): threshold = 9 (very hard)
// Level 1 (Beginner): threshold = 7 (hard)
// Level >= 2: null (use normal threshold)
if (skillLevel === 0) return 9; // Very hard
if (skillLevel === 1) return 7; // Hard
return null; // Utiliser le seuil normal
}
/**
* Handles reroll events on dice in chat messages.
* @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
*/
static async onReroll(message, ev) {
static async onReroll(message, target) {
// Verify user permissions
const msgUserId = message.user?.id ?? message.user;
if (msgUserId !== game.user?.id && !game.user?.isGM) {
@@ -277,7 +246,7 @@ export class VermineUtils {
}
// Get reroll count
const rollMessage = ev.currentTarget.closest('div.vermine-roll-message');
const rollMessage = target.closest('div.vermine-roll-message');
if (!rollMessage) {
return false;
}
@@ -287,22 +256,22 @@ export class VermineUtils {
// Check if rerolls are available
if (!rerollCount || parseInt(rerollCount, 10) < 1) {
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) {
rerollables.forEach(el => el.classList.remove('rerollable'));
}
return false;
}
ev.currentTarget.classList.add('rerolled');
target.classList.add('rerolled');
// Set reroll flag
await message.setFlag("world", "reroll", true);
// Get difficulty and dice type
const ulElement = ev.currentTarget.closest('ul');
const ulElement = target.closest('ul');
const difficulty = ulElement?.dataset.difficulty ?? 7;
let diceType = ev.currentTarget.dataset.diceType;
let diceType = target.dataset.diceType;
// Sanitize user name
const safeUserName = (game.user?.name ?? "user").replace(/[^a-zA-Z0-9_]/g, '_');
@@ -331,30 +300,71 @@ export class VermineUtils {
// Update die display
const result = reroll.dice[0]?.results[0]?.result ?? 0;
const dieSpan = ev.currentTarget.querySelector('span');
const dieSpan = target.querySelector('span');
if (dieSpan) {
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;
if (success) {
ev.currentTarget.classList.add('success');
const totalElement = rollMessage.querySelector('#total');
if (totalElement) {
const currentTotal = parseInt(totalElement.innerText, 10) || 0;
totalElement.innerText = currentTotal + reroll.total;
}
target.classList.toggle('success', !!success);
target.classList.remove("rerollable");
// Recompute the total from the live dice in the DOM. This avoids the
// cumulative-increment drift of the old logic (which never removed a
// 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
ev.currentTarget.classList.remove("rerollable");
const messageContent = ev.currentTarget.closest('div.message-content');
const messageContent = target.closest('div.message-content');
if (messageContent) {
const newRerollCount = parseInt(rerollCount, 10) - 1;
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({
content: messageContent.outerHTML
});
@@ -380,10 +390,11 @@ export class VermineUtils {
html.querySelectorAll('.rerollable').forEach(el => {
el.addEventListener('click', async (ev) => {
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) {
const message = await game.messages.get(msgId);
await VermineUtils.onReroll(message, ev);
await VermineUtils.onReroll(message, dieEl);
}
});
});
@@ -402,6 +413,7 @@ export class VermineUtils {
// Add click event for granting rerolls
html.querySelectorAll("button.grant-reroll").forEach(el => {
el.addEventListener('click', async (ev) => {
const btn = ev.currentTarget;
const grantedRerollElement = html.querySelector('#granted-reroll');
const allowedRerollElement = html.querySelector("#allowed_reroll");
@@ -409,13 +421,13 @@ export class VermineUtils {
allowedRerollElement.innerText = grantedRerollElement.innerText;
}
const mesEl = ev.currentTarget.closest('[data-message-id]');
const mesEl = btn.closest('[data-message-id]');
const messageId = mesEl?.dataset?.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) {
const content = rollMessage.outerHTML;
const message = await game.messages.get(messageId);
@@ -467,15 +479,39 @@ export class VermineUtils {
* @returns {Promise<ChatMessage>} The created chat message
*/
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 = {
user: game.user?._id,
speaker: ChatMessage.getSpeaker(),
content: content,
roll: roll
content: content
};
if (param.messageFlags) {
chatData.flags = { world: param.messageFlags };
}
const msg = await ChatMessage.create(chatData);
await msg.setFlag('world', 'roll', roll);
return msg;
}
+28 -10
View File
@@ -37,11 +37,21 @@ Hooks.once('init', async function () {
CONFIG.Actor.dataModels.npc = models.VermineNpcData;
CONFIG.Actor.dataModels.group = models.VermineGroupData;
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.weapon = models.VermineWeaponData;
CONFIG.Item.dataModels.defense = models.VermineDefenseData;
CONFIG.Item.dataModels.vehicle = models.VermineVehicleData;
CONFIG.Item.dataModels.ability = models.VermineAbilityData;
CONFIG.Item.dataModels.specialty = models.VermineSpecialtyData;
CONFIG.Item.dataModels.background = models.VermineBackgroundData;
@@ -71,8 +81,8 @@ Hooks.once('init', async function () {
// Register sheet application classes (ApplicationV2)
// Unregister core sheets
foundry.applications.sheets.ActorSheetV2?.unregisterSheet?.("core", "Actor", {
types: ["character", "npc", "group", "creature"]
foundry.documents.collections.Actors.unregisterSheet("core", foundry.applications.sheets.ActorSheetV2, {
types: ["character", "npc", "group", "creature", "community", "vehicle"]
})
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, {
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
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, {
types: ["defense"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineVehicleSheetV2, {
types: ["vehicle"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineAbilitySheetV2, {
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-combat.hbs",
"systems/vermine2047/templates/actor/appv2/npc-main.hbs",
"systems/vermine2047/templates/actor/appv2/npc-characteristics.hbs",
"systems/vermine2047/templates/actor/appv2/npc-skills.hbs",
"systems/vermine2047/templates/actor/appv2/npc-threat.hbs",
"systems/vermine2047/templates/actor/appv2/npc-traits.hbs",
"systems/vermine2047/templates/actor/appv2/npc-equipment.hbs",
"systems/vermine2047/templates/actor/appv2/npc-combat.hbs",
"systems/vermine2047/templates/actor/appv2/npc-notes.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-stats.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é dune ouïe extraordinaire. Il perçoit des ondes
sonores très aigües, ce qui lui permet dentendre 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 dentendre 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 laudition.</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 sest adapté au stress du combat. Quand le
personnage subit des Blessures lors dun combat, il ne subit pas la perte de
Dés de Réserve associée. En revanche, à la fin du combat, il perd 2D
dEffort 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 nest quune information dont il ne subit
pas les conséquences néfastes. Lorsquil subit une Blessure, il ne perd
aucun Dé de Sang- Froid. En outre, le Malus quil 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 dun adversaire humain. Sil 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 leffort
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 doptimisme
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 dun é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 lentourent. Il peut ajouter
son score en Volonté aux jets destinés à camoufler sa présence, son odeur,
ses traces ou un objet lui appartenant. Sil camoufle sa présence et quil
reste immobile, il bénéficie en outre de trois Réussites supplémentaires.
Dès quil 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 dune 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 dEffort quil 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 dun Bonus de 2D aux Actions dont le but est dintimider ou
deffrayer. 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
dEffort 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
dEffort.</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 na
pas deffet 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é dun odorat extrêmement développé, proche de celui
dun 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 dun Bonus de 3D aux jets de
Perception quand lodorat entre en jeu. Cette Mutation saccompagne dune
mémoire olfactive très développée. Une fois quil a senti une odeur, le
personnage ne loublie 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 daddiction. En contrepartie, les
médicaments et autres stimulants sont totalement inefficaces sur lui et
noctroient 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 quune. 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 dhormones en cas de
stress intense, ce qui lui permet dy résister plus facilement. LIntensité
des événements stressants est diminuée dun niveau pour le personnage. Il
na 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 dEmpathie + Vigilance contre une Difficulté de 9 permet au
personnage de pressentir un danger ou une menace imminente dorigine
naturelle ou animale. Chaque Réussite obtenue lui permet dobtenir 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. Sil a le
temps dinformer 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'
@@ -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 dun Bonus de 3D aux jets de Vigueur quand une
force musculaire élevée peut laider (soulever ou porter un objet lourd,
enfoncer une porte, abattre un mur, etc.) Lorsquil 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 naugmente pas sa précision, il ne gagne donc aucun Bonus à
lattaque.</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 dun Bonus de 1D sur les Actions impliquant
lacuité 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 laider à se camoufler : elle deviendra plus sombre dans lobscurité,
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. Sil 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 dun 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 dabord réussir un Jet de
Volonté + Psychologie (ou de sa valeur dAttaque sil na pas de score de
Volonté) contre une Difficulté de 7. Chaque Réussite lui permet dagir
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. Lorsquil dégage ces
phéromones, tous les humains et les animaux dans un rayon dune dizaine de
mètres se sentent mal à laise 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
sapprocher, ni attaquer le personnage sauf si elles réussissent un Jet de
Volonté + Psychologie (ou de leur valeur dAttaque si elles nont pas de
score de Volonté) contre une Difficulté de 7. Chaque Réussite leur permet
dagir 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 quil bénéficie dun 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 dune 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 sil 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 dexposition 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 dexposition 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 dEffort 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
dEffort 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 dun Bonus de 1D sur toutes ses Actions liées à la discrétion
lorsquil 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 na besoin de dormir que quatre heures par nuit. Il
récupère toujours la totalité de ses Réserves sil 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 nest 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. Lorsquil 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. Sil entre en contact avec la peau dun humain
ou dun 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 dEfforts 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 lempoisonnement dune 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 dun oeil et à réagir au
moindre bruit ou mouvement suspect. Il nest 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 dun 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 quune 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 dun 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 quil ne possède pas, il
ignore son Handicap de Rareté, comme sil sagissait dune 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. Sil sagit
danimaux, il peut apaiser des individus de cette espèce comme sil
possédait la Capacité « Apaisement » voir page 99. Sil la acquise, un
jet réussi permet de les rendre amicaux et il peut être accompagné dune
nuée danimaux de Taille 0 ou dun petit groupe de Taille 1 qui le
défendront de leur mieux. Sil sagit de végétaux, il bénéficie des effets
de la Capacité « Berceau » comme sil la possédait voir page 99. Sil la
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'

Some files were not shown because too many files have changed in this diff Show More