Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0a3aff648 | ||
|
|
3cb96410b9 | ||
|
|
ad4daf0b00 | ||
|
|
c96e8d69d7 | ||
|
|
282fb2794a | ||
|
|
46b9512256 | ||
|
|
8316112741 | ||
|
|
75f39551f8 | ||
|
|
ef76ae1116 | ||
|
|
18180a8445 | ||
|
|
3235017a4c | ||
|
|
b2a044640c | ||
|
|
a13b0fc2d6 | ||
|
|
0ad240bd03 | ||
|
|
9d36b92fa9 | ||
|
|
d0373fab5a | ||
|
|
cf905cdcfb | ||
|
|
ea3322e843 | ||
|
|
6247f65590 | ||
|
|
8d8428cad3 | ||
|
|
9cf7d9ac68 | ||
|
|
f2c8cfe246 | ||
|
|
31f2f4530c | ||
|
|
9c7ebc6df9 | ||
|
|
05f668acf0 | ||
|
|
10b01614e6 | ||
|
|
89d9d71395 | ||
|
|
1e298ed809 | ||
|
|
4ffaebf52b | ||
|
|
de7691c4dc | ||
|
|
c87b25fbaf | ||
|
|
6ce51ed8cf | ||
|
|
df852f17d1 | ||
|
|
8916600bf6 | ||
|
|
0189f62734 | ||
|
|
29b6a98c89 | ||
|
|
b1d73e6b08 | ||
|
|
509a5efd9d | ||
|
|
fea7c1c271 | ||
|
|
412573fc4d | ||
|
|
47e2ebe45b |
@@ -34,7 +34,8 @@ module.exports = {
|
||||
Dialog: 'readonly',
|
||||
foundry: 'readonly',
|
||||
Handlebars: 'readonly',
|
||||
renderTemplate: 'readonly'
|
||||
renderTemplate: 'readonly',
|
||||
fromUuid: 'readonly'
|
||||
},
|
||||
rules: {
|
||||
// Possible Problems
|
||||
@@ -86,7 +87,6 @@ module.exports = {
|
||||
'default-case': 'warn',
|
||||
'default-case-last': 'warn',
|
||||
'default-param-last': 'warn',
|
||||
'dot-locale-compare': 'warn',
|
||||
'dot-notation': ['warn', { allowKeywords: true }],
|
||||
'eqeqeq': ['error', 'always', { null: 'ignore' }],
|
||||
'func-name-matching': 'warn',
|
||||
@@ -123,7 +123,6 @@ module.exports = {
|
||||
'no-delete-var': 'error',
|
||||
'no-div-regex': 'warn',
|
||||
'no-else-return': ['warn', { allowElseIf: true }],
|
||||
'no-empty-destructuring': 'warn',
|
||||
'no-empty-function': ['warn', { allow: ['constructors'] }],
|
||||
'no-empty-pattern': 'warn',
|
||||
'no-eq-null': 'off',
|
||||
@@ -153,7 +152,6 @@ module.exports = {
|
||||
'no-new-func': 'warn',
|
||||
'no-new-wrappers': 'error',
|
||||
'no-nonoctal-decimal-escape': 'error',
|
||||
'no-object-multi-space': 'warn',
|
||||
'no-octal-escape': 'error',
|
||||
'no-param-reassign': ['warn', { props: false }],
|
||||
'no-plusplus': 'off',
|
||||
@@ -267,10 +265,9 @@ module.exports = {
|
||||
VariableDeclarator: { var: 2, let: 2, const: 3 },
|
||||
outerIIFEBody: 1,
|
||||
MemberExpression: 'off',
|
||||
FunctionDeclaration: { body: 1, parameters: 1, parameters: { var: 2, let: 2, const: 3 } },
|
||||
FunctionExpression: { body: 1, parameters: 1, parameters: { var: 2, let: 2, const: 3 } },
|
||||
StaticBlock: { body: 1 },
|
||||
ClassBody: 1
|
||||
FunctionDeclaration: { body: 1, parameters: 1 },
|
||||
FunctionExpression: { body: 1, parameters: 1 },
|
||||
StaticBlock: { body: 1 }
|
||||
}],
|
||||
'jsx-quotes': 'off',
|
||||
'key-spacing': ['warn', { beforeColon: false, afterColon: true, mode: 'strict' }],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,6 +19,10 @@ foundry
|
||||
# Ancien dossier SCSS (archivé)
|
||||
scss_old/
|
||||
|
||||
# Outils d'audit local
|
||||
.swival/
|
||||
|
||||
# Fichiers CSS générés
|
||||
css/vermine2047.*.css
|
||||
!css/vermine2047.css
|
||||
regles/
|
||||
|
||||
@@ -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*
|
||||
@@ -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.
|
||||
@@ -1,61 +1,142 @@
|
||||
# Vermine 2047 System
|
||||
# Vermine 2047 — Système Foundry VTT
|
||||
|
||||

|
||||

|
||||
|
||||
##TODO
|
||||
Système non-officiel pour le jeu de rôle **Vermine 2047**, univers post-apo français par **Studio Agate**.
|
||||
|
||||
### fiche de perso
|
||||
► Achetez le jeu : https://www.kickandgo.net/boutique-agate/category.php?cl=UyQcRrFtYcYrAnLbOfDj&idp=20
|
||||
|
||||
- [X] création de spécialitées depuis le (+) des compétences
|
||||
- [X] fixer les cases hexas , comprtement chelou , piste = un click à l'air d'envoyer 2 update, verifier le onChange
|
||||
- [X] dialog d'edition des min-max
|
||||
Interface bilingue **FR/EN**.
|
||||
|
||||
### fiche de groupe
|
||||
- [X] pas encore penché dessus
|
||||
---
|
||||
|
||||
#### Members et encounters
|
||||
## Types d'Acteurs
|
||||
|
||||
- [X] faire une classe GroupLink pour avoir les actors en objets dans les array group.members[], group.encounters[], et character.encounters[],
|
||||
- [X] faire une fonction sur le Hook.onUpdateActor => update des groupes dans characters, update des encounters et members dans groups
|
||||
| Type | Fiche | Description |
|
||||
|------|-------|-------------|
|
||||
| **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 |
|
||||
|
||||
### fiche de pnj créature
|
||||
- [X] à faire,
|
||||
- [X] lister les gabarit/taille/roles(creatures) et menace/experience/role(pnj)... stocker les modifs dans CONFIG.VERMINE,
|
||||
## Types d'Objets
|
||||
|
||||
| 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) |
|
||||
|
||||
### les jets de dés
|
||||
- [X] redesign de rollDialog => `<details>+<sumary>`=> rendre moins dense
|
||||
- [X] envoyer les spécialités utilisables au rollDialog
|
||||
- [X] envoyer les items utilisables au rollDialog
|
||||
- [X] gérer le fait de choisir quel totem garder : recalcul des réussites
|
||||
- [X] refacto des template chat de roll
|
||||
- [X] gérer les dés de totems humains et adapté : couleur différente/double succès +update actor
|
||||
- [X] gérer les rerolls depuis chat(cf noc)
|
||||
- [X] gérer les rerolls après le jet en fonction du score d'effort et de la carac
|
||||
- [X] faire l'update l' l'actor juste après s'etre accorder des rerolls, et avoir utiliser le sang-froid
|
||||
- [X] update des reserves de sang-froids lors de jets
|
||||
- [X] ajout des domaines de prédilections
|
||||
- [X] gérer le dés en +/- selon l'influence du totem adapté ou humain selon les domaines
|
||||
- [X] gérer les réussites auto
|
||||
- [X] gérer les seuils auto si compétence non maitrisée
|
||||
## Système de Dés
|
||||
|
||||
### le combat
|
||||
- [X] modifier la difficulté en fonction de l'état du combatant /offensif/actif/passif/
|
||||
- **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
|
||||
Formule : `(@abilities.reflexes.value + @skills.alertness.value)d10cs>=@combatStatus.difficulty`
|
||||
|
||||
### les items
|
||||
- [X] ajouter apprentissage aux abilities
|
||||
- [X] passer le type d'arme en select/options
|
||||
- [X] ajouter handicap de rareté
|
||||
- [X] ajouter pour items Item "competence nécessaire"
|
||||
- [X] gérer les rolls d'items dans le chat
|
||||
- [X] repasser sur les différents itemTypes et sheets
|
||||
- [X] verifier le selector de traits (trait pratique cf : msg pretre)
|
||||
- [X] construire une selecteur de traits, traits= CONFIG.VERMINE.traits
|
||||
traits:[
|
||||
key:{
|
||||
name:string,
|
||||
description:string,
|
||||
value:number
|
||||
}
|
||||
]
|
||||
- [X] ajouter des dégats sur chaques fiches item
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
module/
|
||||
├── vermine2047.mjs # Point d'entrée (Hooks.once init)
|
||||
├── models/ # DataModels (Foundry v2 TypeDataModel)
|
||||
│ ├── actor/ # character.mjs, npc.mjs, group.mjs, creature.mjs
|
||||
│ └── item/ # Les 12 types d'objets
|
||||
├── documents/ # VermineActor, VermineItem
|
||||
├── applications/
|
||||
│ └── sheets/ # Fiches AppV2 par type
|
||||
├── system/
|
||||
│ ├── config.mjs # CONFIG.VERMINE (totems, traits, compétences…)
|
||||
│ ├── roll.mjs # VermineUtils.roll()
|
||||
│ ├── 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 # Paramètres du système (mode de jeu…)
|
||||
│ ├── tour.mjs # Visites guidées (stub)
|
||||
│ └── group-link.mjs # GroupLink : synchro acteurs↔groupes
|
||||
templates/
|
||||
├── actor/appv2/ # Templates AppV2 des fiches acteurs
|
||||
├── actor/ # Templates legacy (coexistence)
|
||||
└── item/ # Templates des fiches objets
|
||||
css/
|
||||
└── vermine2047.min.css # Compilé depuis LESS
|
||||
less/
|
||||
└── base.less # Source LESS principale
|
||||
```
|
||||
|
||||
## Objets Clés
|
||||
|
||||
- `game.vermine2047` → `{ VermineUtils, VermineCombat, GroupLink }`
|
||||
- `CONFIG.VERMINE` → totems, traits, compétences, domaines, états de combat…
|
||||
- `CONFIG.ui.combat` → `VermineCombatTracker` personnalisé
|
||||
|
||||
## Commandes
|
||||
|
||||
```sh
|
||||
npm run build:less # LESS → css/vermine2047.min.css
|
||||
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 # 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
|
||||
```
|
||||
|
||||
### Prérequis
|
||||
- Node.js `v16.15.1` (cf `.nvmrc`)
|
||||
- Foundry VTT **v14**
|
||||
|
||||
## Notes de Développement
|
||||
|
||||
- **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`.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
VERMINE 2047, Un jeu de Julien Blondel
|
||||
|
||||
© 2023 Studio Agate
|
||||
TOTEM System
|
||||
|
||||
© 2023 Julien Blondel
|
||||
Tous droits réservés
|
||||
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 44 KiB |
@@ -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`.
|
||||
|
Before Width: | Height: | Size: 855 KiB |
|
Before Width: | Height: | Size: 776 KiB |
@@ -1,680 +0,0 @@
|
||||
uid=78_0 RootWebArea "Foundry Virtual Tabletop" url="https://localhost:31000/game"
|
||||
uid=78_1 ignored
|
||||
uid=78_2 generic
|
||||
uid=78_3 list
|
||||
uid=78_4 generic
|
||||
uid=78_5 generic
|
||||
uid=78_6 generic
|
||||
uid=78_7 generic
|
||||
uid=78_8 generic
|
||||
uid=78_9 generic
|
||||
uid=78_10 list
|
||||
uid=78_11 listitem level="1"
|
||||
uid=78_12 tab "Outils de token" selectable
|
||||
uid=78_13 listitem level="1"
|
||||
uid=78_14 tab "Outils de tuile" selectable
|
||||
uid=78_15 listitem level="1"
|
||||
uid=78_16 tab "Outils de dessin" selectable
|
||||
uid=78_17 listitem level="1"
|
||||
uid=78_18 tab "Outils de mur" selectable
|
||||
uid=78_19 listitem level="1"
|
||||
uid=78_20 tab "Outils de lumière" selectable
|
||||
uid=78_21 listitem level="1"
|
||||
uid=78_22 tab "Outils de son d’ambiance" selectable
|
||||
uid=78_23 listitem level="1"
|
||||
uid=78_24 tab "Outils de région" selectable
|
||||
uid=78_25 listitem level="1"
|
||||
uid=78_26 tab "Notes" selectable
|
||||
uid=78_27 list
|
||||
uid=78_28 listitem level="1"
|
||||
uid=78_29 button "Sélection de tokens" pressed
|
||||
uid=78_30 listitem level="1"
|
||||
uid=78_31 button "Sélection de cibles"
|
||||
uid=78_32 listitem level="1"
|
||||
uid=78_33 button "Règle"
|
||||
uid=78_34 listitem level="1"
|
||||
uid=78_35 button "Déplacement sans contrainte"
|
||||
uid=78_36 generic
|
||||
uid=78_37 generic
|
||||
uid=78_38 list
|
||||
uid=78_39 listitem level="1"
|
||||
uid=78_40 ignored
|
||||
uid=78_41 StaticText "Gamemaster [MJ]"
|
||||
uid=78_42 InlineTextBox "Gamemaster [MJ]"
|
||||
uid=78_43 generic
|
||||
uid=78_44 generic
|
||||
uid=78_45 LabelText
|
||||
uid=78_46 StaticText "Latence"
|
||||
uid=78_42 InlineTextBox "Latence"
|
||||
uid=78_47 StaticText " "
|
||||
uid=78_42 InlineTextBox " "
|
||||
uid=78_48 StaticText "1ms"
|
||||
uid=78_42 InlineTextBox "1ms"
|
||||
uid=78_49 generic
|
||||
uid=78_50 LabelText
|
||||
uid=78_51 StaticText "IPS"
|
||||
uid=78_42 InlineTextBox "IPS"
|
||||
uid=78_52 StaticText " "
|
||||
uid=78_42 InlineTextBox " "
|
||||
uid=78_53 StaticText "--"
|
||||
uid=78_42 InlineTextBox "--"
|
||||
uid=78_54 button
|
||||
uid=78_55 generic
|
||||
uid=78_56 navigation
|
||||
uid=78_57 list
|
||||
uid=78_58 generic
|
||||
uid=78_59 sectionheader
|
||||
uid=78_60 generic
|
||||
uid=78_61 generic
|
||||
uid=78_62 LabelText
|
||||
uid=78_63 LabelText
|
||||
uid=78_64 sectionfooter
|
||||
uid=78_65 generic roledescription="Barre de raccourcis"
|
||||
uid=78_66 generic
|
||||
uid=78_67 button "Couper le son"
|
||||
uid=78_68 button "Menu principal"
|
||||
uid=78_69 list
|
||||
uid=78_70 button "Emplacement vide"
|
||||
uid=78_71 ignored
|
||||
uid=78_72 StaticText "1"
|
||||
uid=78_42 InlineTextBox "1"
|
||||
uid=78_73 button "Emplacement vide"
|
||||
uid=78_74 ignored
|
||||
uid=78_75 StaticText "2"
|
||||
uid=78_42 InlineTextBox "2"
|
||||
uid=78_76 button "Emplacement vide"
|
||||
uid=78_77 ignored
|
||||
uid=78_78 StaticText "3"
|
||||
uid=78_42 InlineTextBox "3"
|
||||
uid=78_79 button "Emplacement vide"
|
||||
uid=78_80 ignored
|
||||
uid=78_81 StaticText "4"
|
||||
uid=78_42 InlineTextBox "4"
|
||||
uid=78_82 button "Emplacement vide"
|
||||
uid=78_83 ignored
|
||||
uid=78_84 StaticText "5"
|
||||
uid=78_42 InlineTextBox "5"
|
||||
uid=78_85 button "Emplacement vide"
|
||||
uid=78_86 ignored
|
||||
uid=78_87 StaticText "6"
|
||||
uid=78_42 InlineTextBox "6"
|
||||
uid=78_88 button "Emplacement vide"
|
||||
uid=78_89 ignored
|
||||
uid=78_90 StaticText "7"
|
||||
uid=78_42 InlineTextBox "7"
|
||||
uid=78_91 button "Emplacement vide"
|
||||
uid=78_92 ignored
|
||||
uid=78_93 StaticText "8"
|
||||
uid=78_42 InlineTextBox "8"
|
||||
uid=78_94 button "Emplacement vide"
|
||||
uid=78_95 ignored
|
||||
uid=78_96 StaticText "9"
|
||||
uid=78_42 InlineTextBox "9"
|
||||
uid=78_97 button "Emplacement vide"
|
||||
uid=78_98 ignored
|
||||
uid=78_99 StaticText "0"
|
||||
uid=78_42 InlineTextBox "0"
|
||||
uid=78_100 generic
|
||||
uid=78_101 navigation
|
||||
uid=78_102 button "Page suivante"
|
||||
uid=78_103 ignored
|
||||
uid=78_104 StaticText "1"
|
||||
uid=78_42 InlineTextBox "1"
|
||||
uid=78_105 button "Page précédente"
|
||||
uid=78_106 ignored
|
||||
uid=78_107 button "Verrouiller la barre de raccourcis"
|
||||
uid=78_108 button "Effacer la barre de raccourcis"
|
||||
uid=78_109 generic
|
||||
uid=78_110 generic
|
||||
uid=78_111 generic
|
||||
uid=78_112 ignored
|
||||
uid=78_113 list
|
||||
uid=78_114 generic "Tchat"
|
||||
uid=78_115 generic
|
||||
uid=78_116 generic value="
|
||||
"
|
||||
uid=78_117 paragraph
|
||||
uid=78_118 LineBreak "
|
||||
"
|
||||
uid=78_42 InlineTextBox "
|
||||
"
|
||||
uid=78_119 ignored
|
||||
uid=78_42 generic
|
||||
uid=78_42 StaticText "Entrer un message"
|
||||
uid=78_42 InlineTextBox "Entrer un message"
|
||||
uid=78_120 generic
|
||||
uid=78_121 generic
|
||||
uid=78_122 button "Public en tant qu'utilisateur" pressed
|
||||
uid=78_123 button "Privé pour les maîtres de jeu"
|
||||
uid=78_124 button "Aveugle pour les maîtres de jeu"
|
||||
uid=78_125 button "Seulement pour soi-même"
|
||||
uid=78_126 button "Public en tant que personnage"
|
||||
uid=78_127 generic
|
||||
uid=78_128 tablist orientation="horizontal"
|
||||
uid=78_129 list
|
||||
uid=78_130 listitem level="1"
|
||||
uid=78_131 tab "Messages du tchat" selectable
|
||||
uid=78_132 listitem level="1"
|
||||
uid=78_133 tab "Rencontres de combat" selectable
|
||||
uid=78_134 listitem level="1"
|
||||
uid=78_135 tab "Scènes" selectable
|
||||
uid=78_136 listitem level="1"
|
||||
uid=78_137 tab "Objets plaçables" selectable
|
||||
uid=78_138 listitem level="1"
|
||||
uid=78_139 tab "Acteurs" selectable
|
||||
uid=78_140 listitem level="1"
|
||||
uid=78_141 tab "Objets" selectable
|
||||
uid=78_142 listitem level="1"
|
||||
uid=78_143 tab "Journaux" selectable
|
||||
uid=78_144 listitem level="1"
|
||||
uid=78_145 tab "Tables aléatoires" selectable
|
||||
uid=78_146 listitem level="1"
|
||||
uid=78_147 tab "Jeux de cartes" selectable
|
||||
uid=78_148 listitem level="1"
|
||||
uid=78_149 tab "Macros" selectable
|
||||
uid=78_150 listitem level="1"
|
||||
uid=78_151 tab "Playlists" selectable
|
||||
uid=78_152 listitem level="1"
|
||||
uid=78_153 tab "Compendiums" selectable
|
||||
uid=78_154 listitem level="1"
|
||||
uid=78_155 tab "Paramètres" selectable
|
||||
uid=78_156 listitem level="1"
|
||||
uid=78_157 button "Réduire"
|
||||
uid=78_158 generic
|
||||
uid=78_159 ignored
|
||||
uid=78_160 ignored
|
||||
uid=78_161 ignored
|
||||
uid=78_162 ignored
|
||||
uid=78_163 generic
|
||||
uid=78_164 sectionheader
|
||||
uid=78_165 ignored
|
||||
uid=78_166 button "Créer un acteur"
|
||||
uid=78_167 ignored
|
||||
uid=78_168 ignored
|
||||
uid=78_169 StaticText "Créer un acteur"
|
||||
uid=78_42 InlineTextBox "Créer un acteur"
|
||||
uid=78_170 button "Créer un dossier"
|
||||
uid=78_171 ignored
|
||||
uid=78_172 ignored
|
||||
uid=78_173 StaticText "Créer un dossier"
|
||||
uid=78_42 InlineTextBox "Créer un dossier"
|
||||
uid=78_174 search
|
||||
uid=78_175 button "Recherche par nom uniquement"
|
||||
uid=78_176 searchbox "Chercher dans les Acteurs"
|
||||
uid=78_177 ignored
|
||||
uid=78_178 ignored
|
||||
uid=78_179 generic
|
||||
uid=78_180 button "Trier par ordre alphabétique"
|
||||
uid=78_181 button "Réduire tous les dossiers"
|
||||
uid=78_182 list
|
||||
uid=78_183 listitem level="1"
|
||||
uid=78_184 image "Acteur" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/creature.webp"
|
||||
uid=78_185 generic
|
||||
uid=78_186 StaticText "Acteur"
|
||||
uid=78_42 InlineTextBox "Acteur"
|
||||
uid=78_187 listitem level="1"
|
||||
uid=78_188 image "Acteur (2)" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/group.webp"
|
||||
uid=78_189 generic
|
||||
uid=78_190 StaticText "Acteur (2)"
|
||||
uid=78_42 InlineTextBox "Acteur (2)"
|
||||
uid=78_191 listitem level="1"
|
||||
uid=78_192 image "Acteur (3)" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/character.webp"
|
||||
uid=78_193 generic
|
||||
uid=78_194 StaticText "Acteur (3)"
|
||||
uid=78_42 InlineTextBox "Acteur (3)"
|
||||
uid=78_195 sectionfooter
|
||||
uid=78_196 ignored
|
||||
uid=78_197 ignored
|
||||
uid=78_198 ignored
|
||||
uid=78_199 ignored
|
||||
uid=78_200 ignored
|
||||
uid=78_201 ignored
|
||||
uid=78_202 ignored
|
||||
uid=78_203 ignored
|
||||
uid=78_204 ignored
|
||||
uid=78_205 ignored
|
||||
uid=78_206 ignored
|
||||
uid=78_207 ignored
|
||||
uid=78_208 ignored
|
||||
uid=78_209 ignored
|
||||
uid=78_210 ignored
|
||||
uid=78_211 ignored
|
||||
uid=78_212 ignored
|
||||
uid=78_213 ignored
|
||||
uid=78_214 ignored
|
||||
uid=78_215 ignored
|
||||
uid=78_216 ignored
|
||||
uid=78_217 ignored
|
||||
uid=78_218 ignored
|
||||
uid=78_219 figure
|
||||
uid=78_220 image url="https://localhost:31000/ui/pause.svg"
|
||||
uid=78_221 Figcaption
|
||||
uid=78_222 StaticText "JEU EN PAUSE"
|
||||
uid=78_42 InlineTextBox "JEU EN PAUSE"
|
||||
uid=78_223 form
|
||||
uid=78_224 banner
|
||||
uid=78_225 heading "Personnage: Acteur (3)" level="1"
|
||||
uid=78_226 StaticText "Personnage: Acteur (3)"
|
||||
uid=78_42 InlineTextBox "Personnage: Acteur (3)"
|
||||
uid=78_227 button "Basculer les contrôles"
|
||||
uid=78_228 button "Copier l'UUID du document"
|
||||
uid=78_229 button "Fermer la fenêtre"
|
||||
uid=78_230 generic
|
||||
uid=78_231 ignored
|
||||
uid=78_232 ignored
|
||||
uid=78_233 button "MODE ÉDITION"
|
||||
uid=78_234 StaticText "MODE ÉDITION"
|
||||
uid=78_42 InlineTextBox "MODE ÉDITION"
|
||||
uid=78_235 image "logo Vermine" url="https://localhost:31000/systems/vermine2047/assets/images/ui/logo.webp"
|
||||
uid=78_236 ignored
|
||||
uid=78_237 image "Acteur (3)" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/character.webp"
|
||||
uid=78_238 generic
|
||||
uid=78_239 heading "ADAPTATION" level="3"
|
||||
uid=78_240 StaticText "ADAPTATION"
|
||||
uid=78_42 InlineTextBox "ADAPTATION"
|
||||
uid=78_241 heading "L'Humain" level="5"
|
||||
uid=78_242 StaticText "L'Humain"
|
||||
uid=78_42 InlineTextBox "L'Humain"
|
||||
uid=78_243 image url="https://localhost:31000/systems/vermine2047/assets/images/ui/totems/human.webp"
|
||||
uid=78_244 heading "L'adapté" level="5"
|
||||
uid=78_245 StaticText "L'adapté"
|
||||
uid=78_42 InlineTextBox "L'adapté"
|
||||
uid=78_246 image url="https://localhost:31000/systems/vermine2047/assets/images/ui/totems/adapted.webp"
|
||||
uid=78_247 ignored
|
||||
uid=78_248 ignored
|
||||
uid=78_249 ignored
|
||||
uid=78_250 generic
|
||||
uid=78_251 generic
|
||||
uid=78_252 ignored
|
||||
uid=78_253 ignored
|
||||
uid=78_254 ignored
|
||||
uid=78_255 ignored
|
||||
uid=78_256 generic
|
||||
uid=78_257 generic
|
||||
uid=78_258 ignored
|
||||
uid=78_259 ignored
|
||||
uid=78_260 list
|
||||
uid=78_261 listitem level="1"
|
||||
uid=78_262 ignored
|
||||
uid=78_263 heading "LA HORDE" level="4"
|
||||
uid=78_264 StaticText "LA HORDE"
|
||||
uid=78_42 InlineTextBox "LA HORDE"
|
||||
uid=78_265 image "La Horde" url="https://localhost:31000/systems/vermine2047/assets/images/ui/totems/horde.webp"
|
||||
uid=78_266 navigation roledescription="Navigation dans lâonglet des feuilles de personnages"
|
||||
uid=78_267 generic
|
||||
uid=78_268 ignored
|
||||
uid=78_269 StaticText "Caractéristiques et compétences"
|
||||
uid=78_42 InlineTextBox "Caractéristiques "
|
||||
uid=78_42 InlineTextBox "et compétences"
|
||||
uid=78_270 generic
|
||||
uid=78_271 ignored
|
||||
uid=78_272 StaticText "Totem et ajustements"
|
||||
uid=78_42 InlineTextBox "Totem et "
|
||||
uid=78_42 InlineTextBox "ajustements"
|
||||
uid=78_273 generic
|
||||
uid=78_274 ignored
|
||||
uid=78_275 StaticText "Matériel"
|
||||
uid=78_42 InlineTextBox "Matériel"
|
||||
uid=78_276 generic
|
||||
uid=78_277 ignored
|
||||
uid=78_278 StaticText "Histoire"
|
||||
uid=78_42 InlineTextBox "Histoire"
|
||||
uid=78_279 generic
|
||||
uid=78_280 ignored
|
||||
uid=78_281 StaticText "Combat et reserves"
|
||||
uid=78_42 InlineTextBox "Combat "
|
||||
uid=78_42 InlineTextBox "et "
|
||||
uid=78_42 InlineTextBox "reserves"
|
||||
uid=78_282 generic
|
||||
uid=78_283 sectionheader
|
||||
uid=78_284 generic
|
||||
uid=78_285 heading "Nom Acteur (3)" level="1"
|
||||
uid=78_286 LabelText
|
||||
uid=78_287 StaticText "Nom"
|
||||
uid=78_42 InlineTextBox "Nom"
|
||||
uid=78_288 ignored
|
||||
uid=78_289 StaticText "Acteur (3)"
|
||||
uid=78_42 InlineTextBox "Acteur (3)"
|
||||
uid=78_290 ignored
|
||||
uid=78_291 LabelText
|
||||
uid=78_292 StaticText "Profil"
|
||||
uid=78_42 InlineTextBox "Profil"
|
||||
uid=78_293 ignored
|
||||
uid=78_294 ignored
|
||||
uid=78_295 LabelText
|
||||
uid=78_296 StaticText "Age"
|
||||
uid=78_42 InlineTextBox "Age"
|
||||
uid=78_297 ignored
|
||||
uid=78_298 ignored
|
||||
uid=78_299 StaticText "15"
|
||||
uid=78_42 InlineTextBox "15"
|
||||
uid=78_300 generic
|
||||
uid=78_301 StaticText "(Jeune)"
|
||||
uid=78_42 InlineTextBox "(Jeune)"
|
||||
uid=78_302 generic
|
||||
uid=78_303 heading "Totem La Horde" level="1"
|
||||
uid=78_304 LabelText
|
||||
uid=78_305 StaticText "Totem"
|
||||
uid=78_42 InlineTextBox "Totem"
|
||||
uid=78_306 generic
|
||||
uid=78_307 StaticText "La Horde"
|
||||
uid=78_42 InlineTextBox "La Horde"
|
||||
uid=78_308 ignored
|
||||
uid=78_309 LabelText
|
||||
uid=78_310 StaticText "Réputation"
|
||||
uid=78_42 InlineTextBox "Réputation"
|
||||
uid=78_311 ignored
|
||||
uid=78_312 StaticText "0"
|
||||
uid=78_42 InlineTextBox "0"
|
||||
uid=78_313 LabelText
|
||||
uid=78_314 StaticText "Expérience"
|
||||
uid=78_42 InlineTextBox "Expérience"
|
||||
uid=78_315 ignored
|
||||
uid=78_316 StaticText "0"
|
||||
uid=78_42 InlineTextBox "0"
|
||||
uid=78_317 heading "CARACTÉRISTIQUES" level="3"
|
||||
uid=78_318 StaticText "CARACTÉRISTIQUES"
|
||||
uid=78_42 InlineTextBox "CARACTÉRISTIQUES"
|
||||
uid=78_319 ignored
|
||||
uid=78_320 ignored
|
||||
uid=78_321 heading "PHYSIQUE" level="4"
|
||||
uid=78_322 StaticText "PHYSIQUE"
|
||||
uid=78_42 InlineTextBox "PHYSIQUE"
|
||||
uid=78_323 generic
|
||||
uid=78_324 LabelText
|
||||
uid=78_325 StaticText "Vigueur"
|
||||
uid=78_42 InlineTextBox "Vigueur"
|
||||
uid=78_326 generic
|
||||
uid=78_327 LabelText
|
||||
uid=78_328 StaticText "Santé"
|
||||
uid=78_42 InlineTextBox "Santé"
|
||||
uid=78_329 ignored
|
||||
uid=78_330 heading "MANUEL" level="4"
|
||||
uid=78_331 StaticText "MANUEL"
|
||||
uid=78_42 InlineTextBox "MANUEL"
|
||||
uid=78_332 generic
|
||||
uid=78_333 LabelText
|
||||
uid=78_334 StaticText "Précision"
|
||||
uid=78_42 InlineTextBox "Précision"
|
||||
uid=78_335 generic
|
||||
uid=78_336 LabelText
|
||||
uid=78_337 StaticText "Réflexes"
|
||||
uid=78_42 InlineTextBox "Réflexes"
|
||||
uid=78_338 ignored
|
||||
uid=78_339 heading "MENTAL" level="4"
|
||||
uid=78_340 StaticText "MENTAL"
|
||||
uid=78_42 InlineTextBox "MENTAL"
|
||||
uid=78_341 generic
|
||||
uid=78_342 LabelText
|
||||
uid=78_343 StaticText "Savoir"
|
||||
uid=78_42 InlineTextBox "Savoir"
|
||||
uid=78_344 generic
|
||||
uid=78_345 LabelText
|
||||
uid=78_346 StaticText "Perception"
|
||||
uid=78_42 InlineTextBox "Perception"
|
||||
uid=78_347 ignored
|
||||
uid=78_348 heading "SOCIAL" level="4"
|
||||
uid=78_349 StaticText "SOCIAL"
|
||||
uid=78_42 InlineTextBox "SOCIAL"
|
||||
uid=78_350 generic
|
||||
uid=78_351 LabelText
|
||||
uid=78_352 StaticText "Volonté"
|
||||
uid=78_42 InlineTextBox "Volonté"
|
||||
uid=78_353 generic
|
||||
uid=78_354 LabelText
|
||||
uid=78_355 StaticText "Empathie"
|
||||
uid=78_42 InlineTextBox "Empathie"
|
||||
uid=78_356 heading "COMPÉTENCES" level="3"
|
||||
uid=78_357 StaticText "COMPÉTENCES"
|
||||
uid=78_42 InlineTextBox "COMPÉTENCES"
|
||||
uid=78_358 ignored
|
||||
uid=78_359 ignored
|
||||
uid=78_360 heading "L'HOMME" level="4"
|
||||
uid=78_361 StaticText "L'HOMME"
|
||||
uid=78_42 InlineTextBox "L'HOMME"
|
||||
uid=78_362 generic
|
||||
uid=78_363 LabelText
|
||||
uid=78_364 StaticText "Arts "
|
||||
uid=78_42 InlineTextBox "Arts "
|
||||
uid=78_365 superscript
|
||||
uid=78_366 StaticText "(I)"
|
||||
uid=78_42 InlineTextBox "(I)"
|
||||
uid=78_367 generic
|
||||
uid=78_368 LabelText
|
||||
uid=78_369 StaticText "Civilisation "
|
||||
uid=78_42 InlineTextBox "Civilisation "
|
||||
uid=78_370 superscript
|
||||
uid=78_371 StaticText "(II)"
|
||||
uid=78_42 InlineTextBox "(II)"
|
||||
uid=78_372 generic
|
||||
uid=78_373 LabelText
|
||||
uid=78_374 StaticText "Psychologie "
|
||||
uid=78_42 InlineTextBox "Psychologie "
|
||||
uid=78_375 superscript
|
||||
uid=78_376 StaticText "(I)"
|
||||
uid=78_42 InlineTextBox "(I)"
|
||||
uid=78_377 generic
|
||||
uid=78_378 LabelText
|
||||
uid=78_379 StaticText "Rumeurs"
|
||||
uid=78_42 InlineTextBox "Rumeurs"
|
||||
uid=78_380 generic
|
||||
uid=78_381 LabelText
|
||||
uid=78_382 StaticText "Soins "
|
||||
uid=78_42 InlineTextBox "Soins "
|
||||
uid=78_383 superscript
|
||||
uid=78_384 StaticText "(I)"
|
||||
uid=78_42 InlineTextBox "(I)"
|
||||
uid=78_385 ignored
|
||||
uid=78_386 heading "L'ANIMAL" level="4"
|
||||
uid=78_387 StaticText "L'ANIMAL"
|
||||
uid=78_42 InlineTextBox "L'ANIMAL"
|
||||
uid=78_388 generic
|
||||
uid=78_389 LabelText
|
||||
uid=78_390 StaticText "Animalisme "
|
||||
uid=78_42 InlineTextBox "Animalisme "
|
||||
uid=78_391 superscript
|
||||
uid=78_392 StaticText "(I)"
|
||||
uid=78_42 InlineTextBox "(I)"
|
||||
uid=78_393 generic
|
||||
uid=78_394 LabelText
|
||||
uid=78_395 StaticText "Dissection "
|
||||
uid=78_42 InlineTextBox "Dissection "
|
||||
uid=78_396 superscript
|
||||
uid=78_397 StaticText "(II)"
|
||||
uid=78_42 InlineTextBox "(II)"
|
||||
uid=78_398 generic
|
||||
uid=78_399 LabelText
|
||||
uid=78_400 StaticText "Faune "
|
||||
uid=78_42 InlineTextBox "Faune "
|
||||
uid=78_401 superscript
|
||||
uid=78_402 StaticText "(I)"
|
||||
uid=78_42 InlineTextBox "(I)"
|
||||
uid=78_403 generic
|
||||
uid=78_404 LabelText
|
||||
uid=78_405 StaticText "Répulsion"
|
||||
uid=78_42 InlineTextBox "Répulsion"
|
||||
uid=78_406 generic
|
||||
uid=78_407 LabelText
|
||||
uid=78_408 StaticText "Pistage"
|
||||
uid=78_42 InlineTextBox "Pistage"
|
||||
uid=78_409 ignored
|
||||
uid=78_410 heading "LA MACHINE" level="4"
|
||||
uid=78_411 StaticText "LA MACHINE"
|
||||
uid=78_42 InlineTextBox "LA MACHINE"
|
||||
uid=78_412 generic
|
||||
uid=78_413 LabelText
|
||||
uid=78_414 StaticText "Artisanat "
|
||||
uid=78_42 InlineTextBox "Artisanat "
|
||||
uid=78_415 superscript
|
||||
uid=78_416 StaticText "(II)"
|
||||
uid=78_42 InlineTextBox "(II)"
|
||||
uid=78_417 generic
|
||||
uid=78_418 LabelText
|
||||
uid=78_419 StaticText "Bricolage"
|
||||
uid=78_42 InlineTextBox "Bricolage"
|
||||
uid=78_420 generic
|
||||
uid=78_421 LabelText
|
||||
uid=78_422 StaticText "Mécanique "
|
||||
uid=78_42 InlineTextBox "Mécanique "
|
||||
uid=78_423 superscript
|
||||
uid=78_424 StaticText "(II)"
|
||||
uid=78_42 InlineTextBox "(II)"
|
||||
uid=78_425 generic
|
||||
uid=78_426 LabelText
|
||||
uid=78_427 StaticText "Pilotage "
|
||||
uid=78_42 InlineTextBox "Pilotage "
|
||||
uid=78_428 superscript
|
||||
uid=78_429 StaticText "(I)"
|
||||
uid=78_42 InlineTextBox "(I)"
|
||||
uid=78_430 generic
|
||||
uid=78_431 LabelText
|
||||
uid=78_432 StaticText "Technologie "
|
||||
uid=78_42 InlineTextBox "Technologie "
|
||||
uid=78_433 superscript
|
||||
uid=78_434 StaticText "(II)"
|
||||
uid=78_42 InlineTextBox "(II)"
|
||||
uid=78_435 ignored
|
||||
uid=78_436 heading "L'ARME" level="4"
|
||||
uid=78_437 StaticText "L'ARME"
|
||||
uid=78_42 InlineTextBox "L'ARME"
|
||||
uid=78_438 generic
|
||||
uid=78_439 LabelText
|
||||
uid=78_440 StaticText "Armes à feu "
|
||||
uid=78_42 InlineTextBox "Armes à feu "
|
||||
uid=78_441 superscript
|
||||
uid=78_442 StaticText "(II)"
|
||||
uid=78_42 InlineTextBox "(II)"
|
||||
uid=78_443 generic
|
||||
uid=78_444 LabelText
|
||||
uid=78_445 StaticText "Tir à l'arc"
|
||||
uid=78_42 InlineTextBox "Tir à l'arc"
|
||||
uid=78_446 generic
|
||||
uid=78_447 LabelText
|
||||
uid=78_448 StaticText "Armurerie "
|
||||
uid=78_42 InlineTextBox "Armurerie "
|
||||
uid=78_449 superscript
|
||||
uid=78_450 StaticText "(II)"
|
||||
uid=78_42 InlineTextBox "(II)"
|
||||
uid=78_451 generic
|
||||
uid=78_452 LabelText
|
||||
uid=78_453 StaticText "Lancer"
|
||||
uid=78_42 InlineTextBox "Lancer"
|
||||
uid=78_454 generic
|
||||
uid=78_455 LabelText
|
||||
uid=78_456 StaticText "Mêlée"
|
||||
uid=78_42 InlineTextBox "Mêlée"
|
||||
uid=78_457 ignored
|
||||
uid=78_458 heading "LA SURVIE" level="4"
|
||||
uid=78_459 StaticText "LA SURVIE"
|
||||
uid=78_42 InlineTextBox "LA SURVIE"
|
||||
uid=78_460 generic
|
||||
uid=78_461 LabelText
|
||||
uid=78_462 StaticText "Vigilance"
|
||||
uid=78_42 InlineTextBox "Vigilance"
|
||||
uid=78_463 generic
|
||||
uid=78_464 LabelText
|
||||
uid=78_465 StaticText "Athlétisme"
|
||||
uid=78_42 InlineTextBox "Athlétisme"
|
||||
uid=78_466 generic
|
||||
uid=78_467 LabelText
|
||||
uid=78_468 StaticText "Alimentation"
|
||||
uid=78_42 InlineTextBox "Alimentation"
|
||||
uid=78_469 generic
|
||||
uid=78_470 LabelText
|
||||
uid=78_471 StaticText "Discrétion"
|
||||
uid=78_42 InlineTextBox "Discrétion"
|
||||
uid=78_472 generic
|
||||
uid=78_473 LabelText
|
||||
uid=78_474 StaticText "Corps-à-corps"
|
||||
uid=78_42 InlineTextBox "Corps-à-corps"
|
||||
uid=78_475 ignored
|
||||
uid=78_476 heading "LA TERRE" level="4"
|
||||
uid=78_477 StaticText "LA TERRE"
|
||||
uid=78_42 InlineTextBox "LA TERRE"
|
||||
uid=78_478 generic
|
||||
uid=78_479 LabelText
|
||||
uid=78_480 StaticText "Environnement "
|
||||
uid=78_42 InlineTextBox "Environnement "
|
||||
uid=78_481 superscript
|
||||
uid=78_482 StaticText "(I)"
|
||||
uid=78_42 InlineTextBox "(I)"
|
||||
uid=78_483 generic
|
||||
uid=78_484 LabelText
|
||||
uid=78_485 StaticText "Flore "
|
||||
uid=78_42 InlineTextBox "Flore "
|
||||
uid=78_486 superscript
|
||||
uid=78_487 StaticText "(I)"
|
||||
uid=78_42 InlineTextBox "(I)"
|
||||
uid=78_488 generic
|
||||
uid=78_489 LabelText
|
||||
uid=78_490 StaticText "Route"
|
||||
uid=78_42 InlineTextBox "Route"
|
||||
uid=78_491 generic
|
||||
uid=78_492 LabelText
|
||||
uid=78_493 StaticText "Toxiques "
|
||||
uid=78_42 InlineTextBox "Toxiques "
|
||||
uid=78_494 superscript
|
||||
uid=78_495 StaticText "(II)"
|
||||
uid=78_42 InlineTextBox "(II)"
|
||||
uid=78_496 generic
|
||||
uid=78_497 LabelText
|
||||
uid=78_498 StaticText "Vestiges "
|
||||
uid=78_42 InlineTextBox "Vestiges "
|
||||
uid=78_499 superscript
|
||||
uid=78_500 StaticText "(I)"
|
||||
uid=78_42 InlineTextBox "(I)"
|
||||
uid=78_501 ignored
|
||||
uid=78_502 ignored
|
||||
uid=78_503 ignored
|
||||
uid=78_504 ignored
|
||||
uid=78_505 ignored
|
||||
uid=78_506 ignored
|
||||
uid=78_507 ignored
|
||||
uid=78_508 ignored
|
||||
uid=78_509 ignored
|
||||
uid=78_510 ignored
|
||||
uid=78_511 ignored
|
||||
uid=78_512 ignored
|
||||
uid=78_513 ignored
|
||||
uid=78_514 ignored
|
||||
uid=78_515 ignored
|
||||
uid=78_516 ignored
|
||||
uid=78_517 ignored
|
||||
uid=78_518 ignored
|
||||
uid=78_519 ignored
|
||||
uid=78_520 ignored
|
||||
uid=78_521 ignored
|
||||
uid=78_522 ignored
|
||||
uid=78_523 ignored
|
||||
uid=78_524 ignored
|
||||
uid=78_525 ignored
|
||||
uid=78_526 ignored
|
||||
uid=78_527 ignored
|
||||
uid=78_528 ignored
|
||||
uid=78_529 ignored
|
||||
uid=78_530 ignored
|
||||
uid=78_531 ignored
|
||||
uid=78_532 ignored
|
||||
uid=78_533 ignored
|
||||
uid=78_534 ignored
|
||||
uid=78_535 ignored
|
||||
uid=78_536 ignored
|
||||
uid=78_537 ignored
|
||||
uid=78_538 ignored
|
||||
uid=78_539 ignored
|
||||
uid=78_540 ignored
|
||||
uid=78_541 ignored
|
||||
uid=78_542 ignored
|
||||
uid=78_543 ignored
|
||||
uid=78_544 ignored
|
||||
uid=78_545 ignored
|
||||
uid=78_546 ignored
|
||||
uid=78_547 ignored
|
||||
uid=78_548 ignored
|
||||
uid=78_549 ignored
|
||||
uid=78_550 ignored
|
||||
uid=78_551 ignored
|
||||
uid=78_552 ignored
|
||||
uid=78_553 ignored
|
||||
uid=78_554 ignored
|
||||
uid=78_555 generic
|
||||
@@ -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.*
|
||||
|
Before Width: | Height: | Size: 834 KiB |
@@ -16,8 +16,9 @@
|
||||
"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.creaturecapacity": "Creature capacity",
|
||||
"TYPES.Item.specialty": "Specialty",
|
||||
"TYPES.Item.background": "Background",
|
||||
"TYPES.Item.trauma": "Trauma",
|
||||
@@ -43,6 +44,20 @@
|
||||
"nightmare": "Nightmare",
|
||||
"apocalypse": "Apocalypse"
|
||||
},
|
||||
"CREATURE_CAPACITY_TYPES": {
|
||||
"base": {
|
||||
"name": "Base"
|
||||
},
|
||||
"survival": {
|
||||
"name": "Survival"
|
||||
},
|
||||
"nightmare": {
|
||||
"name": "Nightmare"
|
||||
},
|
||||
"apocalypse": {
|
||||
"name": "Apocalypse"
|
||||
}
|
||||
},
|
||||
"ROLLS": {
|
||||
"tool": "Dice Roller",
|
||||
"roll_dice": "Roll the dice"
|
||||
@@ -140,13 +155,19 @@
|
||||
"level": "Level",
|
||||
"experience": "Experience",
|
||||
"reputation": "Reputation",
|
||||
"chaman": "Shaman",
|
||||
"sheet": {
|
||||
"description": "Description"
|
||||
},
|
||||
"pool": "Reserve",
|
||||
"pools": "Reserves",
|
||||
"dice_pool": "Dice pool",
|
||||
"pool_malus": "Wound penalty",
|
||||
"auto_successes": "automatic successes",
|
||||
"self_control": "Self-control",
|
||||
"effort": "Effort",
|
||||
"equip": "Equip",
|
||||
"unequip": "Unequip",
|
||||
"group": "Group",
|
||||
"abilities": "Characteristics",
|
||||
"ability": "Characteristic",
|
||||
@@ -172,6 +193,20 @@
|
||||
"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",
|
||||
"no_encounters": "No encounters",
|
||||
"travel_notes": "Travel notes",
|
||||
"rarity": "Rarity",
|
||||
"rarity_sm": "Rar.",
|
||||
"reliability": "Reliability",
|
||||
@@ -198,10 +233,13 @@
|
||||
"boost": "Boost",
|
||||
"group_abilities": "Group abilities",
|
||||
"totem_abilities": "Totem abilities",
|
||||
"creature_capacities": "Creature capacities",
|
||||
"type_group": "Group",
|
||||
"type_character": "Character",
|
||||
"type_npc": "NPC",
|
||||
"type_creature": "Creature",
|
||||
"type_community": "Community",
|
||||
"type_vehicle": "Vehicle",
|
||||
"type": "Type",
|
||||
"group_members": "Members",
|
||||
"encounters": "Encounters",
|
||||
@@ -223,6 +261,7 @@
|
||||
"world": "World"
|
||||
},
|
||||
"CharacterNamePlaceholder": "Character Name",
|
||||
"ProfilePlaceholder": "Profile",
|
||||
"none": "None",
|
||||
"bonuses": "Bonuses",
|
||||
"choose_ability": "Choose Ability",
|
||||
@@ -240,9 +279,45 @@
|
||||
"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",
|
||||
"totem_hint": "Check to use totem dice (double success possible)",
|
||||
"totem_neutral": "Neutral",
|
||||
"totem_manage": "Manage Totem Dice (Instincts / Interdits)",
|
||||
"totem_serious": "Serious act (2 dice)",
|
||||
"totem_manage_hint": "Instincts: obeying the Human's Instincts gains 1 Human die; another Totem's Instincts gain 1 Adapted die. Interdit: breaking a Human Interdit loses 1 Human die (or gains 1 Adapted die if none). Max 3 dice of the same Totem and 5 dice total.",
|
||||
"totem_instinct_human": "Human Instinct (+1 Human die)",
|
||||
"totem_instinct_adapted": "Another Totem's Instinct (+1 Adapted die)",
|
||||
"totem_interdit_human": "Human Interdit (-1 Human die)",
|
||||
"totem_gained": "{totem} +1D",
|
||||
"totem_gained_adapted": "+1D Adapted",
|
||||
"totem_exchanged": "{totem} gained in exchange (1 opposite die removed)",
|
||||
"totem_replaced": "Opposite die removed instead (max 3D reached)",
|
||||
"totem_lost": "-1 Human die",
|
||||
"totem_limit_reached": "Totem dice limit reached.",
|
||||
"FightTool": "Fight Tool",
|
||||
"RollTool": "Roll Tool",
|
||||
"Roll4Fight": "Roll for Fight",
|
||||
@@ -257,12 +332,172 @@
|
||||
"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",
|
||||
"wound_escalated": "{to} wound (escalated from {from})",
|
||||
"wound_death": "All wounds checked — death",
|
||||
"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",
|
||||
"buy_evolution": "Buy an Adaptation/Mutation",
|
||||
"evolution_name": "Name",
|
||||
"evolution_level": "Level (cost in Evolution dice)",
|
||||
"evolution_mutation": "Mutation",
|
||||
"evolution_adaptation": "Adaptation",
|
||||
"evolution_available_dice": "Accumulated Evolution dice",
|
||||
"evolution_total": "Total levels (after purchase)",
|
||||
"evolution_mode_max": "Game mode maximum",
|
||||
"game_mode": "Game mode",
|
||||
"mode_survie": "Survival",
|
||||
"mode_cauchemar": "Nightmare",
|
||||
"mode_apocalypse": "Apocalypse",
|
||||
"evolution_buy_hint": "The purchase consumes and erases all accumulated Evolution dice. Limits: Survival 5D, Nightmare 6D, Apocalypse 7D total. Level 3/4 Mutations are forbidden in Survival; only one level 4 Mutation.",
|
||||
"buy_evolution_done": "Adaptation/Mutation acquired. Evolution dice erased.",
|
||||
"error_evolution_no_name": "Provide a name for the Adaptation/Mutation.",
|
||||
"error_evolution_invalid_level": "Invalid level (1 to 4).",
|
||||
"error_evolution_not_enough_dice": "Not enough Evolution dice for this level.",
|
||||
"error_evolution_mode_limit": "Game mode maximum Evolution dice reached.",
|
||||
"error_evolution_mutation_mode": "Level 3/4 Mutations require Nightmare or Apocalypse modes.",
|
||||
"error_evolution_mutation_4d": "The character already has a level 4 Mutation.",
|
||||
"error_evolution_create": "Could not create the Adaptation/Mutation.",
|
||||
"open_experience_phase": "Experience phase",
|
||||
"open_learning": "Learning",
|
||||
"validate_phase": "Validate phase",
|
||||
"validate_phase_done": "Experience phase validated: learning limits reset.",
|
||||
"error_not_editable": "You do not have permission to modify this element.",
|
||||
"vehicle_assign": "Attach vehicle",
|
||||
"vehicle_assign_confirm": "This vehicle already belongs to \"{group}\". Attach it to this group?",
|
||||
"vehicle_assign_failed": "Could not attach this vehicle to the group.",
|
||||
"vehicle_import_failed": "Could not import this vehicle from the compendium.",
|
||||
"learning_limits": "Learning limits (per phase)",
|
||||
"learned_skill": "Skill",
|
||||
"learned_specialty": "Specialty",
|
||||
"learned_reserve": "Reserve",
|
||||
"learned_characteristic": "Characteristic",
|
||||
"learned_evolution": "Evolution",
|
||||
"learned_ability": "Totem capacity",
|
||||
"learned_background": "Background",
|
||||
"learned_reputation": "Reputation",
|
||||
"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_totem_ability": "Totem capacity",
|
||||
"learn_background": "Background element",
|
||||
"learn_reputation": "Reputation",
|
||||
"learn_adapted_die": "Replace 1 Experience die with an Adapted totem die (×2)",
|
||||
"adapted_die_x2": "1 Adapted totem die ×2",
|
||||
"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.",
|
||||
"totem_ability_hint": "Prerequisites: same Totem as the character, Group level ≥ Capacity level, game mode compatible (Survival N1, Nightmare N2, Apocalypse N3).",
|
||||
"background_hint": "Difficulty 9. Handicap (I) if the Background is marked with an asterisk (*). GM authorization required.",
|
||||
"reputation_hint": "Reduce = more famous (Difficulty 10 − target score, 1 pt max per phase). Increase = fade into obscurity (requires a major Objective, Difficulty = current Reputation, +1 per Success).",
|
||||
"reputation_reduce": "Become more famous (reduce)",
|
||||
"reputation_increase": "Fade into obscurity (increase)",
|
||||
"reputation_current": "Current Reputation",
|
||||
"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).",
|
||||
"no_totem_ability": "No Totem capacity available to learn.",
|
||||
"no_background": "No background element to acquire.",
|
||||
"error_totem_mode": "Capacity level incompatible with the game mode.",
|
||||
"error_totem_totem": "The character must share the Totem of the Capacity.",
|
||||
"error_totem_group_level": "Group level insufficient for this Capacity.",
|
||||
"error_totem_limit": "Totem capacity limit reached (3× Level 1, 1× Levels 2 and 3, max 5).",
|
||||
"error_reputation_min": "Reputation already at minimum (0).",
|
||||
"error_reputation_max": "Reputation already at maximum (10).",
|
||||
"error_evolution_no_adapted": "The character must have strictly more Adapted than Human totem dice.",
|
||||
"learn_level": "Group level",
|
||||
"learn_group_reserve": "Group reserve",
|
||||
"learn_ability": "Group capacity",
|
||||
"group_level_hint": "Difficulty = number of members. Handicap = current Group level. Max 10.",
|
||||
"group_reserve_hint": "Difficulty = current Reserve value. Handicap II. Max 10.",
|
||||
"group_ability_hint": "Difficulty = Learning. Handicap = Capacity level. Requires Group level ≥ Capacity level.",
|
||||
"no_group_ability": "No Group capacity available to develop.",
|
||||
"error_group_level_max": "Group level already at maximum (10).",
|
||||
"error_group_reserve_max": "Group reserve already at maximum.",
|
||||
"error_group_level_requirement": "Group level insufficient for this Capacity.",
|
||||
"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",
|
||||
@@ -279,6 +514,8 @@
|
||||
"morale": "Morale",
|
||||
"needed": "Needed",
|
||||
"objective_placeholder": "eg: Survive, Explore",
|
||||
"major_objective_placeholder": "eg: Reach a distant goal",
|
||||
"minor_objective_placeholder": "eg: Find a medic, scavenge",
|
||||
"objectives": "Objectives",
|
||||
"pack": "Pack",
|
||||
"pattern": "Pattern",
|
||||
@@ -326,6 +563,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",
|
||||
@@ -346,6 +668,8 @@
|
||||
"ability": "Ability",
|
||||
"new_ability": "New ability",
|
||||
"abilities": "Abilities",
|
||||
"creaturecapacity": "Creature capacity",
|
||||
"new_creaturecapacity": "New creature capacity",
|
||||
"specialties": "Specialties",
|
||||
"new_specialty": "New specialty",
|
||||
"target": "Target",
|
||||
@@ -420,6 +744,11 @@
|
||||
"dog": "Dog",
|
||||
"bear": "Bear"
|
||||
},
|
||||
"PACK_LEVELS": {
|
||||
"small": "Small",
|
||||
"large": "Large",
|
||||
"giant": "Giant"
|
||||
},
|
||||
"TOTEMS": {
|
||||
"human": {
|
||||
"name": "Human",
|
||||
@@ -573,7 +902,7 @@
|
||||
"name": "Repulsion"
|
||||
},
|
||||
"tracks": {
|
||||
"name": "Tracks"
|
||||
"name": "Tracking"
|
||||
},
|
||||
"crafting": {
|
||||
"name": "Crafting"
|
||||
@@ -603,7 +932,7 @@
|
||||
"name": "Melee"
|
||||
},
|
||||
"archery": {
|
||||
"name": "Ranged"
|
||||
"name": "Ranged Weapons"
|
||||
},
|
||||
"throwing": {
|
||||
"name": "Throwing"
|
||||
@@ -655,6 +984,7 @@
|
||||
"Round": "Round"
|
||||
},
|
||||
"SIZE_LEVELS": {
|
||||
"tiny": "Insect",
|
||||
"small": "Small",
|
||||
"medium": "Medium",
|
||||
"large": "Large"
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
"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.creaturecapacity": "Capacité de créature",
|
||||
"TYPES.Item.specialty": "Spécialité",
|
||||
"TYPES.Item.background": "Historique",
|
||||
"TYPES.Item.trauma": "Traumatisme",
|
||||
@@ -49,6 +50,20 @@
|
||||
"nightmare": "Cauchemar",
|
||||
"apocalypse": "Apocalypse"
|
||||
},
|
||||
"CREATURE_CAPACITY_TYPES": {
|
||||
"base": {
|
||||
"name": "Base"
|
||||
},
|
||||
"survival": {
|
||||
"name": "Survie"
|
||||
},
|
||||
"nightmare": {
|
||||
"name": "Cauchemar"
|
||||
},
|
||||
"apocalypse": {
|
||||
"name": "Apocalypse"
|
||||
}
|
||||
},
|
||||
"ROLLS": {
|
||||
"tool": "Lanceur de dés",
|
||||
"roll_dice": "Jeter les dés"
|
||||
@@ -93,7 +108,7 @@
|
||||
"AGE_TYPES": {
|
||||
"young": "Jeune",
|
||||
"adult": "Adulte",
|
||||
"old": "Vieux"
|
||||
"old": "Ancien"
|
||||
},
|
||||
"IDENTITY": {
|
||||
"biography": "Biographie",
|
||||
@@ -146,14 +161,20 @@
|
||||
"level": "Niveau",
|
||||
"experience": "Expérience",
|
||||
"reputation": "Réputation",
|
||||
"chaman": "Chaman",
|
||||
"sheet": {
|
||||
"description": "Description"
|
||||
},
|
||||
"pool": "Réserve",
|
||||
"pools": "Réserves",
|
||||
"reserves": "Réserves",
|
||||
"dice_pool": "Pool de dés",
|
||||
"pool_malus": "Malus Blessure",
|
||||
"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 +209,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 +274,20 @@
|
||||
"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",
|
||||
"no_encounters": "Aucune rencontre",
|
||||
"travel_notes": "Notes de voyage",
|
||||
"rarity": "Rareté",
|
||||
"rarity_sm": "Rar.",
|
||||
"reliability": "Fiabilité",
|
||||
@@ -244,6 +302,170 @@
|
||||
"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)",
|
||||
"totem_neutral": "Neutre",
|
||||
"totem_manage": "Gérer les Dés de Totems (Instincts / Interdits)",
|
||||
"totem_serious": "Acte grave (2 dés)",
|
||||
"totem_manage_hint": "Instincts : obéir aux Instincts de l'Humain gagne 1D Humain ; ceux d'un autre Totem gagnent 1D Adapté. Interdit : briser un Interdit de l'Humain fait perdre 1D Humain (ou gagner 1D Adapté si aucun). Max 3 Dés du même Totem et 5 Dés au total.",
|
||||
"totem_instinct_human": "Instinct de l'Humain (+1D Humain)",
|
||||
"totem_instinct_adapted": "Instinct d'un autre Totem (+1D Adapté)",
|
||||
"totem_interdit_human": "Interdit de l'Humain (-1D Humain)",
|
||||
"totem_gained": "{totem} +1D",
|
||||
"totem_gained_adapted": "+1D Adapté",
|
||||
"totem_exchanged": "{totem} gagné en échange (1D opposé retiré)",
|
||||
"totem_replaced": "Dé opposé retiré à la place (max 3D atteint)",
|
||||
"totem_lost": "-1D Humain",
|
||||
"totem_limit_reached": "Limite de Dés de Totems atteinte.",
|
||||
"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",
|
||||
"wound_escalated": "Blessure {to} (montée depuis {from})",
|
||||
"wound_death": "Toutes les blessures sont cochées — mort",
|
||||
"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",
|
||||
"buy_evolution": "Acheter une Adaptation/Mutation",
|
||||
"evolution_name": "Nom",
|
||||
"evolution_level": "Niveau (coût en Dés d'Évolution)",
|
||||
"evolution_mutation": "Mutation",
|
||||
"evolution_adaptation": "Adaptation",
|
||||
"evolution_available_dice": "Dés d'Évolution accumulés",
|
||||
"evolution_total": "Total des niveaux (après achat)",
|
||||
"evolution_mode_max": "Maximum du Mode de jeu",
|
||||
"game_mode": "Mode de jeu",
|
||||
"mode_survie": "Survie",
|
||||
"mode_cauchemar": "Cauchemar",
|
||||
"mode_apocalypse": "Apocalypse",
|
||||
"evolution_buy_hint": "L'achat consomme et efface tous les Dés d'Évolution accumulés. Limites : Survie 5D, Cauchemar 6D, Apocalypse 7D au total. Mutations 3D/4D interdites en Mode Survie ; une seule Mutation à 4D.",
|
||||
"buy_evolution_done": "Adaptation/Mutation acquise. Dés d'Évolution effacés.",
|
||||
"error_evolution_no_name": "Indiquez un nom pour l'Adaptation/Mutation.",
|
||||
"error_evolution_invalid_level": "Niveau invalide (1 à 4).",
|
||||
"error_evolution_not_enough_dice": "Dés d'Évolution insuffisants pour ce niveau.",
|
||||
"error_evolution_mode_limit": "Maximum de Dés d'Évolution du Mode de jeu atteint.",
|
||||
"error_evolution_mutation_mode": "Les Mutations de Niveau 3/4 nécessitent les Modes Cauchemar ou Apocalypse.",
|
||||
"error_evolution_mutation_4d": "Le personnage possède déjà une Mutation à 4D.",
|
||||
"error_evolution_create": "Impossible de créer l'Adaptation/Mutation.",
|
||||
"open_experience_phase": "Phase d'Expérience",
|
||||
"open_learning": "Apprentissage",
|
||||
"validate_phase": "Valider la phase",
|
||||
"validate_phase_done": "Phase d'Expérience validée : limites d'apprentissage réinitialisées.",
|
||||
"error_not_editable": "Vous n'avez pas la permission de modifier cet élément.",
|
||||
"vehicle_assign": "Attacher le véhicule",
|
||||
"vehicle_assign_confirm": "Ce véhicule appartient déjà à « {group} ». L'attacher à ce Groupe ?",
|
||||
"vehicle_assign_failed": "Impossible d'attacher ce véhicule au Groupe.",
|
||||
"vehicle_import_failed": "Impossible d'importer ce véhicule depuis le compendium.",
|
||||
"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",
|
||||
"learned_ability": "Capacité de Totem",
|
||||
"learned_background": "Historique",
|
||||
"learned_reputation": "Réputation",
|
||||
"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_totem_ability": "Capacité de Totem",
|
||||
"learn_background": "Élément d'Historique",
|
||||
"learn_reputation": "Réputation",
|
||||
"learn_adapted_die": "Remplacer 1D d'Expérience par un Dé de Totem Adapté (×2)",
|
||||
"adapted_die_x2": "1 Dé de Totem Adapté ×2",
|
||||
"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.",
|
||||
"totem_ability_hint": "Prérequis : même Totem que le personnage, Niveau de Groupe ≥ Niveau de la Capacité, Mode de jeu compatible (Survie N1, Cauchemar N2, Apocalypse N3).",
|
||||
"background_hint": "Difficulté 9. Handicap (I) si l'Historique est marqué d'un astérisque (*). Autorisation du MJ requise.",
|
||||
"reputation_hint": "Réduire = plus célèbre (Difficulté 10 − score visé, 1 pt max/phase). Augmenter = se faire oublier (exige un Objectif majeur, Difficulté = Réputation actuelle, +1 par Réussite).",
|
||||
"reputation_reduce": "Devenir plus célèbre (réduire)",
|
||||
"reputation_increase": "Se faire oublier (augmenter)",
|
||||
"reputation_current": "Réputation actuelle",
|
||||
"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).",
|
||||
"no_totem_ability": "Aucune Capacité de Totem disponible à apprendre.",
|
||||
"no_background": "Aucun élément d'Historique à acquérir.",
|
||||
"error_totem_mode": "Niveau de Capacité incompatible avec le Mode de jeu.",
|
||||
"error_totem_totem": "Le personnage doit être du même Totem que la Capacité.",
|
||||
"error_totem_group_level": "Niveau de Groupe insuffisant pour cette Capacité.",
|
||||
"error_totem_limit": "Limite de Capacités de Totem atteinte (3× Niveau 1, 1× Niveaux 2 et 3, max 5).",
|
||||
"error_reputation_min": "Réputation déjà au minimum (0).",
|
||||
"error_reputation_max": "Réputation déjà au maximum (10).",
|
||||
"error_evolution_no_adapted": "Il faut strictement plus de Dés du Totem Adapté que Humains.",
|
||||
"learn_level": "Niveau de Groupe",
|
||||
"learn_group_reserve": "Réserve de Groupe",
|
||||
"learn_ability": "Capacité de Groupe",
|
||||
"group_level_hint": "Difficulté = nombre de membres. Handicap = Niveau actuel du Groupe. Max 10.",
|
||||
"group_reserve_hint": "Difficulté = valeur actuelle de la Réserve. Handicap II. Max 10.",
|
||||
"group_ability_hint": "Difficulté = Apprentissage. Handicap = Niveau de la Capacité. Nécessite un Niveau de Groupe ≥ Niveau de la Capacité.",
|
||||
"no_group_ability": "Aucune Capacité de Groupe disponible à développer.",
|
||||
"error_group_level_max": "Niveau de Groupe déjà au maximum (10).",
|
||||
"error_group_reserve_max": "Réserve de Groupe déjà au maximum.",
|
||||
"error_group_level_requirement": "Niveau de Groupe insuffisant pour cette Capacité.",
|
||||
"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 +481,8 @@
|
||||
"totem": "Totem et ajustements",
|
||||
"combat": "Combat et reserves",
|
||||
"equipment": "Matériel",
|
||||
"stories": "Histoire"
|
||||
"stories": "Histoire",
|
||||
"experience": "Expérience"
|
||||
},
|
||||
"ammo": "Munitions",
|
||||
"ammo_sm": "Mun",
|
||||
@@ -277,10 +500,13 @@
|
||||
"boost": "boost",
|
||||
"group_abilities": "Capacités de groupe",
|
||||
"totem_abilities": "Capacités de totem",
|
||||
"creature_capacities": "Capacités de créature",
|
||||
"type_group": "Groupe",
|
||||
"type_character": "Personnage",
|
||||
"type_npc": "PNJ",
|
||||
"type_creature": "Créature",
|
||||
"type_community": "Communauté",
|
||||
"type_vehicle": "Véhicule",
|
||||
"type": "Type",
|
||||
"group_members": "Membres",
|
||||
"encounters": "Rencontres",
|
||||
@@ -301,6 +527,8 @@
|
||||
"morale_low": "Bas",
|
||||
"morale_crisis": "Crise",
|
||||
"objective_placeholder": "Objectif...",
|
||||
"major_objective_placeholder": "Objectif majeur...",
|
||||
"minor_objective_placeholder": "Objectif mineur...",
|
||||
"instincts_placeholder": "Ex: Triompher, relever un défi...",
|
||||
"prohibits_placeholder": "Ex: Fuir, abandonner...",
|
||||
"error_unknown_actor": "Acteur inconnu",
|
||||
@@ -314,6 +542,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 +554,9 @@
|
||||
"character": "Personnage",
|
||||
"npc": "PNJ",
|
||||
"group": "Groupe",
|
||||
"creature": "Créature"
|
||||
"creature": "Créature",
|
||||
"community": "Communauté",
|
||||
"vehicle": "Véhicule"
|
||||
}
|
||||
},
|
||||
"UI": {
|
||||
@@ -343,6 +574,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",
|
||||
@@ -363,6 +679,8 @@
|
||||
"ability": "Capacité",
|
||||
"new_ability": "Nouvelle capacité",
|
||||
"abilities": "Capacités",
|
||||
"creaturecapacity": "Capacité de créature",
|
||||
"new_creaturecapacity": "Nouvelle capacité de créature",
|
||||
"specialties": "Spécialités",
|
||||
"new_specialty": "Nouvelle spécialité",
|
||||
"target": "Cible",
|
||||
@@ -432,6 +750,7 @@
|
||||
"major": "Majeur"
|
||||
},
|
||||
"SIZE_LEVELS": {
|
||||
"tiny": "Insecte",
|
||||
"small": "Petit",
|
||||
"medium": "Moyen",
|
||||
"large": "Grand"
|
||||
@@ -442,6 +761,11 @@
|
||||
"dog": "Chien",
|
||||
"bear": "Ours"
|
||||
},
|
||||
"PACK_LEVELS": {
|
||||
"small": "Petit",
|
||||
"large": "Grand",
|
||||
"giant": "Géant"
|
||||
},
|
||||
"TOTEMS": {
|
||||
"human": {
|
||||
"name": "L'Humain",
|
||||
@@ -595,7 +919,7 @@
|
||||
"name": "Répulsion"
|
||||
},
|
||||
"tracks": {
|
||||
"name": "Pistage"
|
||||
"name": "Traces"
|
||||
},
|
||||
"crafting": {
|
||||
"name": "Artisanat"
|
||||
@@ -616,7 +940,7 @@
|
||||
"name": "Armes à feu"
|
||||
},
|
||||
"archery": {
|
||||
"name": "Tir à l'arc"
|
||||
"name": "Armes de Tir"
|
||||
},
|
||||
"armory": {
|
||||
"name": "Armurerie"
|
||||
|
||||
@@ -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 {
|
||||
@@ -184,6 +188,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stacked layout for creature sheets (header + tabs + content) ───
|
||||
&.creature .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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -291,7 +331,7 @@
|
||||
|
||||
.chooseTotem {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
@@ -300,6 +340,61 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Fiche personnage (ApplicationV2 : l'élément fiche EST le <form>, le
|
||||
// sélecteur "form div.hexa" ci-dessus ne s'y applique donc pas).
|
||||
&.character div.hexa {
|
||||
.hexa-style();
|
||||
.transition();
|
||||
margin: 0.2rem;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.hexa-style(rgba(255, 255, 255, 0.425), rgba(0, 0, 0, 0.288));
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
.hexa-style(rgb(0, 0, 0), rgba(0, 0, 0, 0.288));
|
||||
&:hover { .hexa-style(rgb(43, 43, 43), rgba(0, 0, 0, 0.288)); }
|
||||
}
|
||||
}
|
||||
|
||||
// Fiche de Groupe : mêmes dés de Réserve cliquables (AppV2).
|
||||
&.group div.hexa {
|
||||
.hexa-style();
|
||||
.transition();
|
||||
margin: 0.2rem;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.hexa-style(rgba(255, 255, 255, 0.425), rgba(0, 0, 0, 0.288));
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
.hexa-style(rgb(0, 0, 0), rgba(0, 0, 0, 0.288));
|
||||
&:hover { .hexa-style(rgb(43, 43, 43), rgba(0, 0, 0, 0.288)); }
|
||||
}
|
||||
}
|
||||
|
||||
.sheet-header-toggle {
|
||||
text-align: right;
|
||||
margin-bottom: 0.5rem;
|
||||
@@ -331,21 +426,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 +490,7 @@
|
||||
}
|
||||
|
||||
.char-vermine2047 {
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,6 +522,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;
|
||||
@@ -576,6 +696,27 @@
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
}
|
||||
|
||||
.evolution-badge {
|
||||
margin-left: 6px;
|
||||
padding: 1px 6px;
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: @font-size-11;
|
||||
text-transform: uppercase;
|
||||
border-radius: 3px;
|
||||
|
||||
&.adaptation {
|
||||
color: @color-text-light-0;
|
||||
background: fade(@color-acid-green, 25%);
|
||||
border: 1px solid @color-acid-green;
|
||||
}
|
||||
|
||||
&.mutation {
|
||||
color: @color-text-light-0;
|
||||
background: fade(@color-hemolymph, 30%);
|
||||
border: 1px solid @color-hemolymph;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── V2 tab navigation — chitin-plate style ─────────────────────────
|
||||
@@ -586,7 +727,7 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: @color-chitin-dark;
|
||||
border-bottom: 2px solid @color-amber;
|
||||
border-bottom: 2px solid @color-hemolymph;
|
||||
min-height: 36px;
|
||||
|
||||
> a {
|
||||
@@ -610,7 +751,7 @@
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(180deg, @color-amber 0%, @color-honeycomb 100%);
|
||||
background: @color-honeycomb;
|
||||
color: @color-chitin-dark;
|
||||
|
||||
&::after {
|
||||
|
||||
@@ -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-text-light-2;
|
||||
}
|
||||
}
|
||||
|
||||
.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-text-light-2;
|
||||
}
|
||||
|
||||
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-honeycomb; .gauge-state { color: @color-honeycomb; } }
|
||||
&.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-honeycomb; }
|
||||
&.crisis { color: @color-hemolymph; }
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
.sheet-header {
|
||||
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
|
||||
padding: 10px;
|
||||
max-height: 110px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.header-fields { flex: 1; }
|
||||
@@ -13,6 +13,10 @@
|
||||
|
||||
.resource {
|
||||
.card-style();
|
||||
|
||||
// Le mixin card-style ajoute un margin-bottom (15px) : inutile dans le
|
||||
// header, on le neutralise pour garder une hauteur compacte.
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.resource-label {
|
||||
@@ -34,13 +38,20 @@
|
||||
|
||||
.charname {
|
||||
margin: 0;
|
||||
height: auto;
|
||||
font-size: @font-size-18;
|
||||
line-height: 1.2;
|
||||
|
||||
span,
|
||||
input {
|
||||
width: 100%;
|
||||
font-size: @font-size-18;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
@@ -116,4 +127,85 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
// ── Cases de blessures (hexagones) ──────────────────────────────────
|
||||
// Le radio est masqué et recouvre l'hexagone : cliquer sur la case la coche.
|
||||
div.hexa {
|
||||
.hexa-style();
|
||||
.transition();
|
||||
margin: 0.2rem;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.hexa-style(rgba(255, 255, 255, 0.425), rgba(0, 0, 0, 0.288));
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
.hexa-style(rgb(0, 0, 0), rgba(0, 0, 0, 0.288));
|
||||
&:hover { .hexa-style(rgb(43, 43, 43), rgba(0, 0, 0, 0.288)); }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Capacités de créature ───────────────────────────────────────────
|
||||
.creature-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
> li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 6px;
|
||||
border-bottom: 1px solid @color-border-dark-3;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
&:hover { box-shadow: 0 0 10px @theme-color-highlight inset; }
|
||||
}
|
||||
|
||||
.item-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.item-roll { cursor: pointer; }
|
||||
}
|
||||
|
||||
.capacity-type {
|
||||
flex: 0 0 auto;
|
||||
font-size: @font-size-12;
|
||||
color: @color-text-light-2;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.item-controls {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
a.item-control {
|
||||
color: inherit;
|
||||
opacity: 0.85;
|
||||
|
||||
&:hover { opacity: 1; }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,13 +15,113 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── Titres de section : uniforme (centré, scotch, majuscules) ───────
|
||||
h4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-14;
|
||||
text-align: center;
|
||||
padding: 2px 8px;
|
||||
margin: 0 0 8px;
|
||||
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
opacity: 0.85;
|
||||
|
||||
&:hover { opacity: 1; }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sous-titres (ex. Objectifs majeurs / mineurs) ───────────────────
|
||||
h5 {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-12;
|
||||
// Brun foncé lisible sur le fond beige de la fiche (le jaune était illisible).
|
||||
color: @color-chitin-dark;
|
||||
margin: 8px 0 4px;
|
||||
text-align: left;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
// ── Liste uniforme (objectifs, capacités, membres, rencontres) ──────
|
||||
.group-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
> li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 6px;
|
||||
border-bottom: 1px solid @color-border-dark-3;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
&:hover { box-shadow: 0 0 10px @theme-color-highlight inset; }
|
||||
}
|
||||
|
||||
> li.group-list-header {
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-12;
|
||||
border-bottom: 2px solid @color-border-dark-3;
|
||||
box-shadow: none;
|
||||
|
||||
> div { text-align: center; }
|
||||
}
|
||||
|
||||
.item-name, .actor-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-controls {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Champs (Origine, Concept, Instincts, Interdits…) ────────────────
|
||||
.group-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
label {
|
||||
flex: 0 0 auto;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
> :last-child {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
input[type="text"] { width: 100%; }
|
||||
}
|
||||
|
||||
// ── Colonnes des tableaux (Rencontres) ──────────────────────────────
|
||||
.gcol-name { flex: 2; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.gcol-profile { flex: 3; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.gcol-totem { flex: 2; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
.reserve-control,
|
||||
.morale-control {
|
||||
.card-style();
|
||||
@@ -39,58 +139,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.actor-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
li.actor {
|
||||
padding: 5px;
|
||||
border-bottom: 1px solid @color-border-dark-3;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 10px @theme-color-highlight inset;
|
||||
}
|
||||
}
|
||||
|
||||
.actor-name, .item-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.objective-item {
|
||||
padding: 5px;
|
||||
border-bottom: 1px solid @color-border-dark-4;
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.totem-display {
|
||||
.card-style();
|
||||
flex: 1;
|
||||
|
||||
span { font-weight: bold; }
|
||||
}
|
||||
|
||||
.identity-field {
|
||||
margin-bottom: 8px;
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
input { width: 100%; }
|
||||
}
|
||||
|
||||
.level input,
|
||||
.reputation input {
|
||||
width: 60px;
|
||||
|
||||
@@ -5,7 +5,25 @@
|
||||
.sheet-header {
|
||||
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
|
||||
padding: 0.5rem;
|
||||
max-height: 150px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.charname {
|
||||
margin: 0;
|
||||
height: auto;
|
||||
font-size: @font-size-18;
|
||||
line-height: 1.2;
|
||||
|
||||
span,
|
||||
input {
|
||||
font-size: @font-size-18;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ability-value,
|
||||
@@ -30,77 +48,81 @@
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
gap: 8px 16px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.wound-cat {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-weight: bold;
|
||||
|
||||
.wound-label {
|
||||
font-size: @font-size-12;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.wound-boxes {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
|
||||
i {
|
||||
font-size: 8px;
|
||||
color: @color-border-dark-2;
|
||||
}
|
||||
|
||||
.hexa {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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 +155,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 +202,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 {
|
||||
@@ -228,9 +228,25 @@
|
||||
.hexa {
|
||||
.hexa-style();
|
||||
.transition();
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.hexa-style(rgba(255, 255, 255, 0.425), rgba(0, 0, 0, 0.288));
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
.hexa-style(rgb(0, 0, 0), rgba(0, 0, 0, 0.288));
|
||||
&:hover { .hexa-style(rgb(43, 43, 43), rgba(0, 0, 0, 0.288)); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,18 +18,63 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle « Chaman » de l'onglet Totem : ne pas étirer sur toute la largeur.
|
||||
.chaman-toggle {
|
||||
width: fit-content;
|
||||
margin: 0.5rem auto;
|
||||
gap: 0.5rem;
|
||||
|
||||
label {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-12;
|
||||
color: @theme-color-dark;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
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 +98,63 @@
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.adapted-dice i {
|
||||
transform: rotate(180deg);
|
||||
padding-top: 0.5rem;
|
||||
color: @totem-adapted-color;
|
||||
i {
|
||||
transform: rotate(180deg);
|
||||
color: @totem-adapted-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.human {
|
||||
left: 0;
|
||||
img { left: 0; }
|
||||
}
|
||||
.totem-influence-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
margin-top: 2px;
|
||||
font-size: 0.75rem;
|
||||
|
||||
.adapted {
|
||||
right: 0;
|
||||
img { right: 0; }
|
||||
.totem-influence-label {
|
||||
color: @color-text-light-0;
|
||||
|
||||
strong.totem-human { color: @totem-human-color; }
|
||||
strong.totem-adapted { color: @totem-adapted-color; }
|
||||
}
|
||||
|
||||
.totem-manage-btn {
|
||||
flex: none;
|
||||
padding: 2px 8px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
@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;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.header-fields { flex: 1; }
|
||||
|
||||
.resource {
|
||||
.card-style();
|
||||
|
||||
// Le mixin card-style ajoute un margin-bottom (15px) : inutile dans le
|
||||
// header, on le neutralise pour garder une hauteur compacte.
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.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;
|
||||
height: auto;
|
||||
font-size: @font-size-18;
|
||||
line-height: 1.2;
|
||||
|
||||
span,
|
||||
input {
|
||||
font-size: @font-size-18;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.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-text-light-2;
|
||||
}
|
||||
}
|
||||
|
||||
.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-text-light-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -608,7 +665,11 @@ ol.chat-log {
|
||||
cursor: pointer;
|
||||
&:hover { transform: translateY(0.5rem); }
|
||||
}
|
||||
&.success { border-bottom-color: @color-acid-green; }
|
||||
&.success {
|
||||
border-bottom-color: @color-acid-green;
|
||||
border-bottom-width: 6px;
|
||||
box-shadow: 0 0 8px fade(@color-acid-green, 55%);
|
||||
}
|
||||
&.adapted::after { content: "adapté"; }
|
||||
&.human::after { content: "humain"; }
|
||||
&.rerolled { transform: translateY(0rem); }
|
||||
@@ -671,7 +732,297 @@ ol.chat-log {
|
||||
}
|
||||
|
||||
#required {
|
||||
color: @color-amber;
|
||||
color: @color-text-light-2;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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-text-light-2;
|
||||
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-hemolymph;
|
||||
background: fade(@color-hemolymph, 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-hemolymph; }
|
||||
}
|
||||
|
||||
.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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -681,11 +1032,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-text-light-2;
|
||||
}
|
||||
|
||||
.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-text-light-2;
|
||||
}
|
||||
}
|
||||
|
||||
.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-text-light-2;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ iframe {
|
||||
|
||||
> label {
|
||||
font-size: @font-size-13;
|
||||
color: @color-amber;
|
||||
color: @color-text-light-2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ iframe {
|
||||
|
||||
label {
|
||||
font-size: @font-size-11;
|
||||
color: @color-amber;
|
||||
color: @color-chitin-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,7 +294,7 @@ iframe {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: @color-amber;
|
||||
color: @color-text-light-highlight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ iframe {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: @color-amber;
|
||||
color: @color-text-light-highlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,7 +381,7 @@ iframe {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: @color-amber;
|
||||
color: @color-text-light-highlight;
|
||||
}
|
||||
|
||||
small {
|
||||
@@ -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-text-light-2;
|
||||
}
|
||||
|
||||
.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-text-light-2;
|
||||
}
|
||||
|
||||
.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-text-light-highlight; }
|
||||
}
|
||||
}
|
||||
|
||||
.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-hemolymph;
|
||||
background: fade(@color-hemolymph, 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-text-light-2;
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -473,3 +721,92 @@ iframe {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Achat d'Adaptation/Mutation ───────────────────────────────────────
|
||||
.application.buy-evolution .bonus-item {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
> label.label {
|
||||
flex: none;
|
||||
font-size: @font-size-12;
|
||||
color: @color-text-light-2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
select {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
flex: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Gestion des Dés de Totems ─────────────────────────────────────────
|
||||
.application.totem-dice .totem-dice-state {
|
||||
gap: 12px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid @color-hemolymph;
|
||||
background: fade(@color-hemolymph, 8%);
|
||||
|
||||
.totem-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.label {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-12;
|
||||
color: @color-text-light-2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.totem-human .label { color: @color-text-light-2; }
|
||||
&.totem-adapted .label { color: @color-text-light-2; }
|
||||
|
||||
.value {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: @font-size-14;
|
||||
font-weight: 700;
|
||||
color: @color-text-light-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.application.totem-dice .totem-influence {
|
||||
font-size: @font-size-16;
|
||||
}
|
||||
|
||||
.application.totem-dice .totem-actions {
|
||||
gap: 8px;
|
||||
|
||||
button {
|
||||
flex: 1 1 100%;
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-12;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
color: @color-text-light-0;
|
||||
background: fade(@color-hemolymph, 15%);
|
||||
border: 1px solid @color-hemolymph;
|
||||
border-radius: 3px;
|
||||
.transition();
|
||||
|
||||
&:hover {
|
||||
color: @theme-color-dark;
|
||||
background: @color-hemolymph;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,3 +98,14 @@ ol#chat-log div.item-card header img,
|
||||
ol.chat-log div.item-card header img {
|
||||
max-width: 30%;
|
||||
}
|
||||
|
||||
// Icônes des cartes d'item envoyées au chat (ApplicationV2).
|
||||
// Le template des cartes (templates/item/chatCards/parts/base.hbs) rend
|
||||
// une <img> pleine taille : on la plafonne à 64×64.
|
||||
ol#chat-log .chat-card-img,
|
||||
ol.chat-log .chat-card-img {
|
||||
max-width: 64px;
|
||||
max-height: 64px;
|
||||
object-fit: contain;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
// Niveaux de gris pour le texte clair
|
||||
@color-text-light-0: #fff;
|
||||
@color-text-light-1: #e0f0f0;
|
||||
@color-text-light-2: #c9e0c0;
|
||||
@color-text-light-2: #3f9b55;
|
||||
@color-text-light-3: #90c4a4;
|
||||
@color-text-light-4: #80c08b;
|
||||
@color-text-light-5: #60b06b;
|
||||
@@ -138,8 +138,7 @@
|
||||
@theme-color-highlight: #005a3c;
|
||||
|
||||
// Couleurs insecte / organique
|
||||
@color-acid-green: #6ab04c;
|
||||
@color-amber: #e8b84b;
|
||||
@color-acid-green: #1e7d32;
|
||||
@color-chitin-dark: #2a2520;
|
||||
@color-hemolymph: #8b4513;
|
||||
@color-membrane: rgba(106, 176, 76, 0.1);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -4,12 +4,14 @@ 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,
|
||||
VermineCreatureCapacitySheetV2,
|
||||
VermineSpecialtySheetV2,
|
||||
VermineBackgroundSheetV2,
|
||||
VermineTraumaSheetV2,
|
||||
|
||||
@@ -30,9 +30,15 @@ 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,
|
||||
clickWound: VermineBaseActorSheet.#onClickWound,
|
||||
effectControl: VermineBaseActorSheet.#onEffectControl,
|
||||
chooseTotem: VermineBaseActorSheet.#onChooseTotem
|
||||
}
|
||||
@@ -114,7 +120,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,16 +233,89 @@ 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 et créatures → dialogue de combat (valeur d'Attaque) ;
|
||||
// personnage → dialogue de compétence. PNJ/créatures n'ont ni
|
||||
// caractéristiques ni compétences.
|
||||
if (item.type === "weapon" && (this.document.type === 'npc' || this.document.type === 'creature')) {
|
||||
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) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
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()
|
||||
// Évite une erreur de permission pour un utilisateur non éditeur.
|
||||
if (!this.isEditable) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_not_editable"))
|
||||
return
|
||||
}
|
||||
const input = target
|
||||
const update = {}
|
||||
let current = this.document
|
||||
@@ -251,6 +331,36 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
|
||||
this.document.update(update)
|
||||
}
|
||||
|
||||
/**
|
||||
* Coche/décoche les cases de Blessure en respectant l'ordre de gravité
|
||||
* (règles p.39) : on ne peut cocher que la case suivante dans l'ordre
|
||||
* (une blessure s'applique toujours au Seuil égal ou inférieur le plus
|
||||
* élevé), et on ne peut pas en sauter. Décocher une case retire toutes
|
||||
* celles situées après elle.
|
||||
*/
|
||||
static #onClickWound(event, target) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const input = target
|
||||
const box = parseInt(input.value, 10) || 0 // numéro de case (1-based)
|
||||
let current = this.document
|
||||
const propTree = input.name.split(".")
|
||||
for (const prop of propTree) {
|
||||
current = current[prop]
|
||||
}
|
||||
const currentValue = parseInt(current, 10) || 0
|
||||
let next = currentValue
|
||||
if (box === currentValue + 1) {
|
||||
// Cocher la case suivante dans l'ordre.
|
||||
next = box
|
||||
} else if (box <= currentValue) {
|
||||
// Décocher : retire la case cliquée et toutes celles après.
|
||||
next = box - 1
|
||||
}
|
||||
// Si box > currentValue + 1 : on ne permet pas de sauter des cases.
|
||||
this.document.update({ [input.name]: next })
|
||||
}
|
||||
|
||||
static #onEffectControl(event, target) {
|
||||
onManageActiveEffect(event, this.document)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -4,21 +4,27 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["character"],
|
||||
position: { width: 860, height: 720 },
|
||||
position: { width: 920, height: 720 },
|
||||
window: { contentClasses: ["character-content"] },
|
||||
actions: {
|
||||
addSpecialty: VermineCharacterSheetV2.#onAddSpecialty
|
||||
addSpecialty: VermineCharacterSheetV2.#onAddSpecialty,
|
||||
openExperiencePhase: VermineCharacterSheetV2.#onOpenExperiencePhase,
|
||||
openLearning: VermineCharacterSheetV2.#onOpenLearning,
|
||||
validatePhase: VermineCharacterSheetV2.#onValidatePhase,
|
||||
buyEvolution: VermineCharacterSheetV2.#onBuyEvolution,
|
||||
openTotemDice: VermineCharacterSheetV2.#onOpenTotemDice
|
||||
}
|
||||
}
|
||||
|
||||
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 +35,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 +58,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
|
||||
@@ -60,22 +68,35 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
||||
context.backgrounds = doc.itemTypes.background
|
||||
context.traumas = doc.itemTypes.trauma
|
||||
context.evolutions = doc.itemTypes.evolution
|
||||
context.rites = doc.itemTypes.rite
|
||||
break
|
||||
case "equipment":
|
||||
context.tab = context.tabs.equipment
|
||||
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 +116,37 @@ 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)
|
||||
}
|
||||
|
||||
/** Valide la phase d'Expérience courante et réinitialise les limites. */
|
||||
static async #onValidatePhase() {
|
||||
const { VermineExperience } = await import("../../system/experience.mjs")
|
||||
await VermineExperience.resetPhaseLimits(this.document)
|
||||
ui.notifications.info(game.i18n.localize("VERMINE.validate_phase_done"))
|
||||
}
|
||||
|
||||
/** Ouvre le dialogue d'achat d'une Adaptation/Mutation (Dés d'Évolution). */
|
||||
static async #onBuyEvolution(event, target) {
|
||||
const { default: BuyEvolutionDialog } = await import("../../system/dialogs/buyEvolutionDialog.mjs")
|
||||
const dialog = await BuyEvolutionDialog.create({ actorId: this.document.id })
|
||||
if (dialog) dialog.render(true)
|
||||
}
|
||||
|
||||
/** Ouvre le dialogue de gestion des Dés de Totems. */
|
||||
static async #onOpenTotemDice(event, target) {
|
||||
const { default: TotemDiceDialog } = await import("../../system/dialogs/totemDiceDialog.mjs")
|
||||
const dialog = await TotemDiceDialog.create({ actorId: this.document.id })
|
||||
if (dialog) dialog.render(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import VermineBaseActorSheet from "./base-actor-sheet.mjs"
|
||||
|
||||
export default class VermineCommunitySheetV2 extends VermineBaseActorSheet {
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["community"],
|
||||
position: { width: 760, height: 640 },
|
||||
window: { contentClasses: ["community-content"] },
|
||||
actions: {
|
||||
addObjective: VermineCommunitySheetV2.#onAddObjective,
|
||||
deleteObjective: VermineCommunitySheetV2.#onDeleteObjective
|
||||
}
|
||||
}
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/vermine2047/templates/actor/appv2/community-main.hbs", scrollable: [""] },
|
||||
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||
identity: { template: "systems/vermine2047/templates/actor/appv2/community-identity.hbs", scrollable: [""] },
|
||||
domains: { template: "systems/vermine2047/templates/actor/appv2/community-domains.hbs", scrollable: [""] },
|
||||
resources: { template: "systems/vermine2047/templates/actor/appv2/community-resources.hbs", scrollable: [""] }
|
||||
}
|
||||
|
||||
tabGroups = { sheet: "identity" }
|
||||
|
||||
#getTabs() {
|
||||
const tabs = {
|
||||
identity: { id: "identity", group: "sheet", icon: "fas fa-info-circle", label: "COMMUNITY.tabs.identity" },
|
||||
domains: { id: "domains", group: "sheet", icon: "fas fa-chart-simple", label: "COMMUNITY.tabs.domains" },
|
||||
resources: { id: "resources", group: "sheet", icon: "fas fa-boxes-stacked", label: "COMMUNITY.tabs.resources" }
|
||||
}
|
||||
for (const v of Object.values(tabs)) {
|
||||
v.active = this.tabGroups[v.group] === v.id
|
||||
v.cssClass = v.active ? "active" : ""
|
||||
}
|
||||
return tabs
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
// Groupe de personnages rattaché
|
||||
context.resolvedGroup = null
|
||||
if (this.document.system.groupId) {
|
||||
const g = game.actors.get(this.document.system.groupId)
|
||||
if (g) context.resolvedGroup = { id: g.id, name: g.name }
|
||||
}
|
||||
// Enrichissement HTML des champs longs (même logique que la fiche PNJ)
|
||||
const enrich = async (path) => {
|
||||
const val = foundry.utils.getProperty(this.document.system, path)
|
||||
return val ? await foundry.applications.ux.TextEditor.implementation.enrichHTML(val, { async: true }) : ""
|
||||
}
|
||||
context.enrichedNotes = await enrich("identity.notes")
|
||||
context.enrichedLandmarks = await enrich("location.landmarks")
|
||||
context.enrichedTerritory = await enrich("location.territory")
|
||||
context.enrichedEvents = await enrich("history.events")
|
||||
context.enrichedRelations = await enrich("history.relations")
|
||||
return context
|
||||
}
|
||||
|
||||
async _preparePartContext(partId, context) {
|
||||
const doc = this.document
|
||||
switch (partId) {
|
||||
case "main": break
|
||||
case "identity":
|
||||
context.tab = context.tabs.identity
|
||||
context.ageOptions = Object.entries(CONFIG.VERMINE.communityAges).map(([key, label]) => ({ key, label: game.i18n.localize(label) }))
|
||||
break
|
||||
case "domains":
|
||||
context.tab = context.tabs.domains
|
||||
context.sizeOptions = CONFIG.VERMINE.communitySizeLevels
|
||||
context.sizeLabel = doc.system.size?.value ? game.i18n.localize(CONFIG.VERMINE.communitySizeLevels[doc.system.size.value]?.label ?? "") : ""
|
||||
context.sizeInfo = doc.system.size?.value ? CONFIG.VERMINE.communitySizeLevels[doc.system.size.value] : null
|
||||
// Préparer les 4 Domaines avec leurs 2 sections (jauges) et leur état
|
||||
context.domains = []
|
||||
for (const [dkey, dcfg] of Object.entries(CONFIG.VERMINE.communityDomains)) {
|
||||
const domain = doc.system.domains?.[dkey]
|
||||
context.domains.push({
|
||||
key: dkey,
|
||||
label: game.i18n.localize(dcfg.label),
|
||||
sections: Object.entries(dcfg.sections).map(([skey, label]) => {
|
||||
const gauge = domain?.[skey] ?? { value: 0 }
|
||||
const state = gauge.state || "crisis"
|
||||
return {
|
||||
domainKey: dkey,
|
||||
key: skey,
|
||||
label: game.i18n.localize(label),
|
||||
gauge,
|
||||
state,
|
||||
stateLabel: game.i18n.localize(CONFIG.VERMINE.communityGaugeStates[state]?.label ?? "COMMUNITY.states.crisis")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
break
|
||||
case "resources":
|
||||
context.tab = context.tabs.resources
|
||||
break
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
changeTab(tab, group, options = {}) {
|
||||
super.changeTab(tab, group, options)
|
||||
if (group === "sheet") {
|
||||
const main = this.element?.querySelector('[data-group="sheet"][data-tab="main"]')
|
||||
if (main) main.classList.add("active")
|
||||
}
|
||||
}
|
||||
|
||||
static #onDeleteObjective(event, target) {
|
||||
const index = parseInt(target.dataset.index)
|
||||
if (isNaN(index)) return
|
||||
const objectives = foundry.utils.duplicate(this.document.system.objectives || { major: "", minor: [] })
|
||||
objectives.minor.splice(index, 1)
|
||||
this.document.update({ "system.objectives": objectives })
|
||||
}
|
||||
|
||||
static #onAddObjective(event, target) {
|
||||
const objectives = foundry.utils.duplicate(this.document.system.objectives || { major: "", minor: [] })
|
||||
objectives.minor.push("")
|
||||
this.document.update({ "system.objectives": objectives })
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,10 @@ import VermineBaseActorSheet from "./base-actor-sheet.mjs"
|
||||
|
||||
export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
|
||||
|
||||
// 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" }
|
||||
@@ -55,19 +59,43 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
|
||||
context.roleOptions = CONFIG.VERMINE.creatureRoleLevels
|
||||
context.sizeOptions = CONFIG.VERMINE.creatureSizeLevels
|
||||
context.packOptions = CONFIG.VERMINE.creaturePackLevels
|
||||
context.sizeLabel = doc.system.size?.value !== undefined
|
||||
? game.i18n.localize(CONFIG.VERMINE.creatureSizeLevels[doc.system.size.value]?.label ?? "")
|
||||
: ""
|
||||
context.packLabel = doc.system.pack?.value
|
||||
? game.i18n.localize(CONFIG.VERMINE.creaturePackLevels[doc.system.pack.value]?.label ?? "")
|
||||
: ""
|
||||
break
|
||||
case "info":
|
||||
context.tab = context.tabs.info
|
||||
context.capacities = doc.itemTypes.creaturecapacity
|
||||
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.packLabel = doc.system.pack?.value || game.i18n.localize("VERMINE.none")
|
||||
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
|
||||
? `${doc.system.pack.value} (${game.i18n.localize(CONFIG.VERMINE.creaturePackLevels[doc.system.pack.value]?.label ?? "")})`
|
||||
: 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 +105,21 @@ 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é) ou de Réaction. */
|
||||
async _onRoll(event) {
|
||||
event.preventDefault()
|
||||
const el = event.currentTarget
|
||||
const type = el.dataset.type
|
||||
if (type === "creature-reaction") {
|
||||
const { default: NpcRollDialog } = await import("../../system/dialogs/npcRollDialog.mjs")
|
||||
const dialog = await NpcRollDialog.create({ actorId: this.document.id, rolltype: "reaction" })
|
||||
if (dialog) dialog.render(true)
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,21 +7,26 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
||||
position: { width: 700, height: 600 },
|
||||
window: { contentClasses: ["group-content"] },
|
||||
actions: {
|
||||
chooseTotem: VermineGroupSheetV2.#onChooseTotem,
|
||||
chooseActor: VermineGroupSheetV2.#onChooseActor,
|
||||
deleteMember: VermineGroupSheetV2.#onDeleteMember,
|
||||
deleteEncounter: VermineGroupSheetV2.#onDeleteEncounter,
|
||||
deleteObjective: VermineGroupSheetV2.#onDeleteObjective,
|
||||
addObjective: VermineGroupSheetV2.#onAddObjective
|
||||
addObjective: VermineGroupSheetV2.#onAddObjective,
|
||||
createTotemAbility: VermineGroupSheetV2.#onCreateTotemAbility,
|
||||
openLearning: VermineGroupSheetV2.#onOpenLearning,
|
||||
openExperiencePhase: VermineGroupSheetV2.#onOpenExperiencePhase,
|
||||
validatePhase: VermineGroupSheetV2.#onValidatePhase
|
||||
}
|
||||
}
|
||||
|
||||
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" }
|
||||
@@ -55,9 +60,22 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
||||
if (this.document.system.encounters?.length > 0) {
|
||||
for (const encId of this.document.system.encounters) {
|
||||
const a = game.actors.get(encId)
|
||||
if (a) context.resolvedEncounters[encId] = { name: a.name, id: a.id }
|
||||
if (a) {
|
||||
context.resolvedEncounters[encId] = {
|
||||
name: a.name,
|
||||
id: a.id,
|
||||
type: a.type,
|
||||
profile: a.system?.identity?.profile || "",
|
||||
totem: a.system?.identity?.totem || ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Notes de voyage (riche texte)
|
||||
const roadNotes = this.document.system?.roadNotes
|
||||
context.enrichedRoadNotes = roadNotes
|
||||
? await foundry.applications.ux.TextEditor.implementation.enrichHTML(roadNotes, { async: true })
|
||||
: ""
|
||||
return context
|
||||
}
|
||||
|
||||
@@ -67,21 +85,18 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
||||
case "main": break
|
||||
case "info":
|
||||
context.tab = context.tabs.info
|
||||
context.abilities = doc.itemTypes.ability
|
||||
context.specialties = doc.itemTypes.specialty
|
||||
context.backgrounds = doc.itemTypes.background
|
||||
context.traumas = doc.itemTypes.trauma
|
||||
context.evolutions = doc.itemTypes.evolution
|
||||
context.abilities = doc.itemTypes.ability.filter(i => i.system.type !== "totem")
|
||||
context.totem_abilities = doc.itemTypes.ability.filter(i => i.system.type === "totem")
|
||||
break
|
||||
case "gear":
|
||||
context.tab = context.tabs.gear
|
||||
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
|
||||
break
|
||||
case "reserve":
|
||||
context.tab = context.tabs.reserve
|
||||
@@ -90,10 +105,60 @@ 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dépôt d'un acteur Véhicule sur la fiche : lie le véhicule au Groupe
|
||||
* (system.ownerId) au lieu de le dupliquer, puis affiche la liste.
|
||||
* Gère les dépôts depuis un compendium (copie importée dans le monde)
|
||||
* et demande confirmation si le véhicule appartient déjà à un autre Groupe.
|
||||
* @override
|
||||
*/
|
||||
async _onDropActor(event, data) {
|
||||
let actor = data instanceof foundry.abstract.Document ? data : await fromUuid(data.uuid)
|
||||
if (actor?.type !== "vehicle") {
|
||||
return super._onDropActor?.(event, data)
|
||||
}
|
||||
// Véhicule issu d'un compendium : importer une copie dans le monde.
|
||||
if (actor.compendium) {
|
||||
try {
|
||||
const copy = await game.actors.importFromCompendium(actor.compendium, actor._id, { renderSheet: false })
|
||||
if (!copy) return
|
||||
actor = copy
|
||||
} catch {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.vehicle_import_failed"))
|
||||
return
|
||||
}
|
||||
}
|
||||
// Véhicule déjà rattaché à un autre Groupe : confirmation avant transfert.
|
||||
const currentOwner = actor.system?.ownerId
|
||||
if (currentOwner && currentOwner !== this.document.id) {
|
||||
const other = game.actors.get(currentOwner)
|
||||
const content = game.i18n.localize("VERMINE.vehicle_assign_confirm").replace("{group}", other?.name ?? currentOwner)
|
||||
const confirmed = await Dialog.confirm({
|
||||
title: game.i18n.localize("VERMINE.vehicle_assign"),
|
||||
content,
|
||||
defaultYes: false
|
||||
})
|
||||
if (!confirmed) return
|
||||
}
|
||||
try {
|
||||
await actor.update({ "system.ownerId": this.document.id })
|
||||
} catch {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.vehicle_assign_failed"))
|
||||
return
|
||||
}
|
||||
this.render()
|
||||
}
|
||||
|
||||
// Actions : délégation aux applications AppV1 existantes pour TotemPicker/ActorPicker
|
||||
static async #onChooseTotem(event, target) {
|
||||
const { TotemPicker } = await import("../../system/applications.mjs")
|
||||
new TotemPicker(target, this.document).render(true)
|
||||
// Le Totem du Groupe remplit automatiquement Instincts/Interdits.
|
||||
new TotemPicker(target, this.document, { fillInstincts: true }).render(true)
|
||||
}
|
||||
static async #onChooseActor(event, target) {
|
||||
const { ActorPicker } = await import("../../system/applications.mjs")
|
||||
@@ -135,4 +200,28 @@ 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" } }])
|
||||
}
|
||||
|
||||
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: GroupLearningDialog } = await import("../../system/dialogs/groupLearningDialog.mjs")
|
||||
const dialog = await GroupLearningDialog.create({ actorId: this.document.id })
|
||||
if (dialog) dialog.render(true)
|
||||
}
|
||||
|
||||
/** Valide la phase d'Expérience courante du Groupe et réinitialise les limites. */
|
||||
static async #onValidatePhase() {
|
||||
const { VermineExperience } = await import("../../system/experience.mjs")
|
||||
await VermineExperience.resetPhaseLimits(this.document)
|
||||
ui.notifications.info(game.i18n.localize("VERMINE.validate_phase_done"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,71 +3,71 @@ import { VermineBaseItemSheet } from "./base-item-sheet.mjs"
|
||||
// ── Item générique ────────────────────────────────────────────────────
|
||||
export class VermineItemSheetV2 extends VermineBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = { classes: ["item-gear"], position: { width: 520 } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-item-sheet.hbs" } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-item-sheet.hbs", scrollable: [""] } }
|
||||
}
|
||||
|
||||
// ── Arme ──────────────────────────────────────────────────────────────
|
||||
export class VermineWeaponSheetV2 extends VermineBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = { classes: ["weapon"], position: { width: 520 } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-weapon-sheet.hbs" } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-weapon-sheet.hbs", scrollable: [""] } }
|
||||
}
|
||||
|
||||
// ── Défense ───────────────────────────────────────────────────────────
|
||||
export class VermineDefenseSheetV2 extends VermineBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = { classes: ["defense"], position: { width: 520 } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-defense-sheet.hbs" } }
|
||||
}
|
||||
|
||||
// ── 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: [""] } }
|
||||
}
|
||||
|
||||
// ── Capacité de créature ────────────────────────────────────────────────
|
||||
export class VermineCreatureCapacitySheetV2 extends VermineBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = { classes: ["creaturecapacity"], position: { width: 560 } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-creaturecapacity-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: [""] } }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -2,11 +2,13 @@ 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"
|
||||
export { default as VermineVehicleData } from "./vehicle.mjs"
|
||||
export { default as VermineAbilityData } from "./ability.mjs"
|
||||
export { default as VermineCreatureCapacityData } from "./creaturecapacity.mjs"
|
||||
export { default as VermineSpecialtyData } from "./specialty.mjs"
|
||||
export { default as VermineBackgroundData } from "./background.mjs"
|
||||
export { default as VermineTraumaData } from "./trauma.mjs"
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export default class VermineAbilityData extends foundry.abstract.TypeDataModel {
|
||||
totem: new fields.StringField({ required: true, initial: "" }),
|
||||
learn: learnSchema(),
|
||||
level: levelSchema(1, 1, 3),
|
||||
learned: new fields.BooleanField({ required: true, initial: false }),
|
||||
effects: new fields.ArrayField(new fields.StringField({ required: true, initial: "" }), {
|
||||
initial: []
|
||||
})
|
||||
|
||||
@@ -14,7 +14,9 @@ export default class VermineBackgroundData extends foundry.abstract.TypeDataMode
|
||||
const reqInt = { required: true, nullable: false, integer: true }
|
||||
return {
|
||||
...listItemSchema(),
|
||||
cost: new fields.NumberField({ ...reqInt, initial: 1, min: 0 })
|
||||
cost: new fields.NumberField({ ...reqInt, initial: 1, min: 0 }),
|
||||
asterisk: new fields.BooleanField({ required: true, initial: false }),
|
||||
learned: new fields.BooleanField({ required: true, initial: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
|
||||
height: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
|
||||
weight: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
|
||||
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
// Statut de Chaman : débloque l'affichage des Rites chamaniques maîtrisés.
|
||||
chaman: new fields.BooleanField({ required: true, initial: false }),
|
||||
age: new fields.StringField({ required: true, nullable: false, initial: "15" }),
|
||||
ageType: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 2 }),
|
||||
profile: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
@@ -72,8 +74,43 @@ 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 }),
|
||||
ability: new fields.BooleanField({ required: true, initial: false }),
|
||||
background: new fields.BooleanField({ required: true, initial: false }),
|
||||
reputation: new fields.BooleanField({ required: true, initial: false })
|
||||
})
|
||||
}),
|
||||
|
||||
// Rencontres
|
||||
@@ -127,7 +164,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 +178,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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -90,38 +90,50 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
|
||||
* .creatureRoleLevels, .creaturePackLevels.
|
||||
*
|
||||
* Règles :
|
||||
* - Attaque = pattern.attack + size.attack + pack.attack + role.reaction
|
||||
* - Attaque = pattern.attack + size.attack + pack.attack
|
||||
* - Dégâts = pattern.damage + size.vigor + pack.damage
|
||||
* - Vigueur = size.vigor + pack.damage
|
||||
* - Réaction = role.reaction + role.reaction_bonus
|
||||
*/
|
||||
_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] || {}
|
||||
|
||||
// Attaque : patron + taille + meute + réaction du rôle
|
||||
// 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.
|
||||
// NB : le Rôle ne donne que la Réaction (dés + réussites automatiques),
|
||||
// il n'ajoute pas de dés d'attaque (regles_creatures.txt).
|
||||
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)
|
||||
// Réaction : rôle (le bonus de rôle est compté séparément et ajouté
|
||||
// comme réussites automatiques par le NpcRollDialog — cf. modèle PNJ).
|
||||
this.computed.reaction = roleConfig.reaction || 0
|
||||
this.computed.reactionBonus = roleConfig.reaction_bonus || 0
|
||||
|
||||
// Réserves
|
||||
@@ -136,32 +148,45 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les seuils de blessures à partir du patron, de la taille et de la meute.
|
||||
* Les seuils sont la somme des valeurs correspondantes des trois sources.
|
||||
* Calcule les seuils et les cercles de blessures à partir du patron, de la
|
||||
* taille et de la meute.
|
||||
*
|
||||
* Règles (regles_creatures.txt) :
|
||||
* - Seuil = contribution du Gabarit + contributions de Taille (cumulées).
|
||||
* Le Groupe n'ajoute PAS de seuil (tableau « Groupe » : cercles seuls).
|
||||
* - Cercles = cercles du Gabarit + cercles de Taille (cumulés) + cercles du Groupe.
|
||||
*/
|
||||
_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] || {}
|
||||
|
||||
this.minorWound.threshold = (patternConfig.minorWound || 0)
|
||||
+ (sizeConfig.minorWound || 0)
|
||||
+ (packConfig.minorWound || 0)
|
||||
this.majorWound.threshold = (patternConfig.majorWound || 0)
|
||||
+ (sizeConfig.majorWound || 0)
|
||||
+ (packConfig.majorWound || 0)
|
||||
this.deadlyWound.threshold = (patternConfig.deadlyWound || 0)
|
||||
+ (sizeConfig.deadlyWound || 0)
|
||||
+ (packConfig.deadlyWound || 0)
|
||||
// Les modificateurs de Taille se cumulent (Taille N = somme des tailles 1..N).
|
||||
const sizeLevels = CONFIG.VERMINE.creatureSizeLevels || {}
|
||||
const sizeThresholds = { minor: 0, major: 0, deadly: 0 }
|
||||
const sizeWounds = { minor: 0, major: 0, deadly: 0 }
|
||||
for (let i = 1; i <= sizeLevel; i++) {
|
||||
const cfg = sizeLevels[i] || {}
|
||||
sizeThresholds.minor += cfg.minorThreshold || 0
|
||||
sizeThresholds.major += cfg.majorThreshold || 0
|
||||
sizeThresholds.deadly += cfg.deadlyThreshold || 0
|
||||
sizeWounds.minor += cfg.minorWound || 0
|
||||
sizeWounds.major += cfg.majorWound || 0
|
||||
sizeWounds.deadly += cfg.deadlyWound || 0
|
||||
}
|
||||
|
||||
// Max de blessures
|
||||
this.minorWound.max = Math.min(5, this.minorWound.threshold + 2)
|
||||
this.majorWound.max = Math.min(4, this.majorWound.threshold + 1)
|
||||
this.deadlyWound.max = Math.min(2, this.deadlyWound.threshold)
|
||||
// Seuils : gabarit + taille cumulée (pas de contribution du groupe).
|
||||
this.minorWound.threshold = (patternConfig.minorThreshold ?? 0) + sizeThresholds.minor
|
||||
this.majorWound.threshold = (patternConfig.majorThreshold ?? 0) + sizeThresholds.major
|
||||
this.deadlyWound.threshold = (patternConfig.deadlyThreshold ?? 0) + sizeThresholds.deadly
|
||||
|
||||
// Cercles : gabarit + taille cumulée + groupe.
|
||||
this.minorWound.max = (patternConfig.minorWound ?? 0) + sizeWounds.minor + (packConfig.minorWound ?? 0)
|
||||
this.majorWound.max = (patternConfig.majorWound ?? 0) + sizeWounds.major + (packConfig.majorWound ?? 0)
|
||||
this.deadlyWound.max = (patternConfig.deadlyWound ?? 0) + sizeWounds.deadly + (packConfig.deadlyWound ?? 0)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { listItemSchema } from "./_shared.mjs"
|
||||
|
||||
/**
|
||||
* DataModel pour les items de type "creaturecapacity" (capacités de créature).
|
||||
* @augments {foundry.abstract.TypeDataModel}
|
||||
*/
|
||||
export default class VermineCreatureCapacityData extends foundry.abstract.TypeDataModel {
|
||||
/** @override */
|
||||
static LOCALIZATION_PREFIXES = ["VERMINE.item.creaturecapacity"]
|
||||
|
||||
/** @override */
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields
|
||||
return {
|
||||
...listItemSchema(),
|
||||
type: new fields.StringField({ required: true, initial: "base" })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
|
||||
@@ -13,7 +13,8 @@ export default class VermineEvolutionData extends foundry.abstract.TypeDataModel
|
||||
const fields = foundry.data.fields
|
||||
return {
|
||||
...listItemSchema(),
|
||||
level: levelSchema(1, 1, 4)
|
||||
level: levelSchema(1, 1, 4),
|
||||
mutation: new fields.BooleanField({ required: true, initial: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ export default class VermineGroupData extends foundry.abstract.TypeDataModel {
|
||||
// Équipement
|
||||
equipment: equipmentSchema(),
|
||||
|
||||
// Notes de voyage (lieux, dangers, routes…)
|
||||
roadNotes: new fields.HTMLField({ required: true, initial: "" }),
|
||||
|
||||
// Niveau du groupe (1-10)
|
||||
level: levelSchema(1, 1, 10),
|
||||
|
||||
@@ -75,6 +78,19 @@ 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 }),
|
||||
phase: 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 }),
|
||||
level: new fields.BooleanField({ required: true, initial: false }),
|
||||
reserve: new fields.BooleanField({ required: true, initial: false }),
|
||||
ability: new fields.BooleanField({ required: true, initial: false })
|
||||
})
|
||||
}),
|
||||
|
||||
// Rencontres
|
||||
encounters: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" }))
|
||||
}
|
||||
@@ -113,12 +129,14 @@ export default class VermineGroupData extends foundry.abstract.TypeDataModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule la réserve max en fonction du niveau du groupe.
|
||||
* Règle simplifiée : niveau × 2, plafonné à 10.
|
||||
* Calcule la réserve max selon le nombre de membres (personnages).
|
||||
* Règles p. 68-69 : la Réserve débute à 3D et gagne 1D par personnage,
|
||||
* avec un maximum de 10D.
|
||||
*/
|
||||
_calculateGroupReserve() {
|
||||
const level = this.level?.value || 1
|
||||
this.reserve.max = Math.min(10, level * 2)
|
||||
const members = Array.isArray(this.members) ? this.members : []
|
||||
const characterCount = members.filter((id) => game.actors?.get(id)?.type === "character").length
|
||||
this.reserve.max = Math.min(10, 3 + characterCount)
|
||||
|
||||
if (this.reserve.value > this.reserve.max) {
|
||||
this.reserve.value = this.reserve.max
|
||||
|
||||
@@ -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é.
|
||||
*/
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: "" })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api
|
||||
|
||||
export class TotemPicker extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
|
||||
constructor(linkEl, actor) {
|
||||
constructor(linkEl, actor, options = {}) {
|
||||
super();
|
||||
this.linkEl = linkEl;
|
||||
this.actor = actor;
|
||||
this.fillInstincts = options.fillInstincts === true;
|
||||
}
|
||||
|
||||
static get DEFAULT_OPTIONS() {
|
||||
@@ -24,13 +27,23 @@ 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;
|
||||
this.actor.update({ 'system.identity.totem': link.dataset.totem });
|
||||
const update = { 'system.identity.totem': link.dataset.totem };
|
||||
// Remplit automatiquement Instincts/Interdits depuis la config du totem
|
||||
// (utilisé pour le Totem du Groupe).
|
||||
if (this.fillInstincts && this.actor.system?.identity) {
|
||||
const totemKey = link.dataset.totem;
|
||||
const instincts = game.i18n.localize(`TOTEMS.${totemKey}.instincts`);
|
||||
const bans = game.i18n.localize(`TOTEMS.${totemKey}.bans`);
|
||||
if (!instincts.startsWith("TOTEMS.")) update['system.identity.instincts'] = instincts;
|
||||
if (!bans.startsWith("TOTEMS.")) update['system.identity.prohibits'] = bans;
|
||||
}
|
||||
this.actor.update(update);
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
@@ -80,9 +93,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 +161,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 +178,6 @@ export class TraitSelector extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
}
|
||||
}
|
||||
}
|
||||
await this.render(true);
|
||||
}
|
||||
|
||||
async _onChangeInput(ev) {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,29 +129,30 @@ VERMINE.npcRoleLevels = {
|
||||
* Creature Pattern Levels configuration
|
||||
*/
|
||||
VERMINE.creaturePatternLevels = {
|
||||
1: { "label": "PATTERN_LEVELS.insect", "attack": 2, "damage": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
|
||||
2: { "label": "PATTERN_LEVELS.rat", "attack": 3, "damage": 1, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
|
||||
3: { "label": "PATTERN_LEVELS.dog", "attack": 4, "damage": 3, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
|
||||
4: { "label": "PATTERN_LEVELS.bear", "attack": 6, "damage": 6, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }
|
||||
1: { "label": "PATTERN_LEVELS.insect", "attack": 2, "damage": 0, "minorThreshold": 0, "majorThreshold": 0, "deadlyThreshold": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
|
||||
2: { "label": "PATTERN_LEVELS.rat", "attack": 3, "damage": 1, "minorThreshold": 0, "majorThreshold": 1, "deadlyThreshold": 3, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
|
||||
3: { "label": "PATTERN_LEVELS.dog", "attack": 4, "damage": 3, "minorThreshold": 1, "majorThreshold": 3, "deadlyThreshold": 5, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
|
||||
4: { "label": "PATTERN_LEVELS.bear", "attack": 6, "damage": 6, "minorThreshold": 3, "majorThreshold": 5, "deadlyThreshold": 7, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, "minorThreshold": 0, "majorThreshold": 0, "deadlyThreshold": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 },
|
||||
1: { "label": "SIZE_LEVELS.small", "attack": 1, "vigor": 1, "minorThreshold": 0, "majorThreshold": 1, "deadlyThreshold": 1, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
|
||||
2: { "label": "SIZE_LEVELS.medium", "attack": 2, "vigor": 2, "minorThreshold": 2, "majorThreshold": 2, "deadlyThreshold": 2, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
|
||||
3: { "label": "SIZE_LEVELS.large", "attack": 3, "vigor": 3, "minorThreshold": 3, "majorThreshold": 3, "deadlyThreshold": 3, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creature Pack Levels configuration
|
||||
*/
|
||||
VERMINE.creaturePackLevels = {
|
||||
0: { "attack": 0, "damage": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 },
|
||||
1: { "attack": 1, "damage": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
|
||||
2: { "attack": 2, "damage": 2, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 },
|
||||
3: { "attack": 5, "damage": 5, "minorWound": 3, "majorWound": 3, "deadlyWound": 3 }
|
||||
0: { "label": "VERMINE.none", "attack": 0, "damage": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 },
|
||||
1: { "label": "PACK_LEVELS.small", "attack": 1, "damage": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
|
||||
2: { "label": "PACK_LEVELS.large", "attack": 2, "damage": 2, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 },
|
||||
3: { "label": "PACK_LEVELS.giant", "attack": 5, "damage": 5, "minorWound": 3, "majorWound": 3, "deadlyWound": 3 }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,6 +180,25 @@ VERMINE.abilityCategories = {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Types de Capacités de créature (mode d'activation/disponibilité).
|
||||
* Correspond aux Modes de jeu + une catégorie "base".
|
||||
*/
|
||||
VERMINE.creatureCapacityTypes = {
|
||||
"base": {
|
||||
"label": "CREATURE_CAPACITY_TYPES.base"
|
||||
},
|
||||
"survival": {
|
||||
"label": "CREATURE_CAPACITY_TYPES.survival"
|
||||
},
|
||||
"nightmare": {
|
||||
"label": "CREATURE_CAPACITY_TYPES.nightmare"
|
||||
},
|
||||
"apocalypse": {
|
||||
"label": "CREATURE_CAPACITY_TYPES.apocalypse"
|
||||
}
|
||||
}
|
||||
|
||||
VERMINE.abilities = {
|
||||
"vigor": "ABILITIES.vigor.name",
|
||||
"health": "ABILITIES.health.name",
|
||||
@@ -268,6 +288,23 @@ VERMINE.totems = {
|
||||
"loner": "TOTEMS.loner.name",
|
||||
"adapted": "TOTEMS.adapted.name"
|
||||
}
|
||||
/**
|
||||
* Influence des Dés de Totems (règles p. 121).
|
||||
* Selon le Totem dominant (strictement plus de Dés Humains ou Adaptés),
|
||||
* le personnage subit un Bonus/Malus de 1D sur les Domaines de Compétences.
|
||||
* Neutre : aucun modificateur.
|
||||
*/
|
||||
VERMINE.totemInfluence = {
|
||||
"human": {
|
||||
"bonus": ["man", "tool", "world"],
|
||||
"malus": ["animal", "survival", "weapon"]
|
||||
},
|
||||
"adapted": {
|
||||
"bonus": ["animal", "survival", "weapon"],
|
||||
"malus": ["man", "tool", "world"]
|
||||
}
|
||||
}
|
||||
|
||||
VERMINE.totem_opposites = {
|
||||
"human": "TOTEMS.adapted.name",
|
||||
"predator": "TOTEMS.scavenger.name",
|
||||
@@ -468,3 +505,54 @@ VERMINE.totemEffects = {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Communauté — Domaines et sections (jauges).
|
||||
*/
|
||||
VERMINE.communityDomains = {
|
||||
resources: {
|
||||
label: "COMMUNITY.domains.resources",
|
||||
sections: { vivre: "COMMUNITY.sections.vivre", supplies: "COMMUNITY.sections.supplies" }
|
||||
},
|
||||
security: {
|
||||
label: "COMMUNITY.domains.security",
|
||||
sections: { defense: "COMMUNITY.sections.defense", care: "COMMUNITY.sections.care" }
|
||||
},
|
||||
dynamism: {
|
||||
label: "COMMUNITY.domains.dynamism",
|
||||
sections: { cohesion: "COMMUNITY.sections.cohesion", productivity: "COMMUNITY.sections.productivity" }
|
||||
},
|
||||
relations: {
|
||||
label: "COMMUNITY.domains.relations",
|
||||
sections: { human: "COMMUNITY.sections.human", ecology: "COMMUNITY.sections.ecology" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Communauté — Tailles (effectif, Figures, Compagnies recommandés).
|
||||
*/
|
||||
VERMINE.communitySizeLevels = {
|
||||
1: { label: "COMMUNITY.sizeLevels.1", population: "7-50", figures: 3, companies: 5 },
|
||||
2: { label: "COMMUNITY.sizeLevels.2", population: "51-75", figures: 5, companies: 7 },
|
||||
3: { label: "COMMUNITY.sizeLevels.3", population: "100+", figures: 7, companies: 9 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Communauté — États d'une jauge de Domaine.
|
||||
* Crise 0D / Bas 1-2D / Stable 3-5D / Opulent 6D+.
|
||||
*/
|
||||
VERMINE.communityGaugeStates = {
|
||||
crisis: { label: "COMMUNITY.states.crisis" },
|
||||
low: { label: "COMMUNITY.states.low" },
|
||||
stable: { label: "COMMUNITY.states.stable" },
|
||||
opulent: { label: "COMMUNITY.states.opulent" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Communauté — Âge (notion de temps).
|
||||
*/
|
||||
VERMINE.communityAges = {
|
||||
recent: "COMMUNITY.age.recent",
|
||||
established: "COMMUNITY.age.established",
|
||||
old: "COMMUNITY.age.old"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { VermineExperience } from "../experience.mjs";
|
||||
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
/**
|
||||
* Dialogue d'achat d'une Adaptation/Mutation avec les Dés d'Évolution
|
||||
* accumulés. Vérifie le coût (niveau 1-4), la limite du Mode de jeu et les
|
||||
* restrictions sur les Mutations ; en cas de succès, crée l'item et efface
|
||||
* les Dés d'Évolution accumulés.
|
||||
*/
|
||||
export default class BuyEvolutionDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
||||
|
||||
#actor;
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["vermine-roll", "buy-evolution"],
|
||||
tag: "form",
|
||||
window: {
|
||||
icon: "fas fa-cart-shopping",
|
||||
resizable: false
|
||||
},
|
||||
position: {
|
||||
width: 480,
|
||||
height: 460
|
||||
},
|
||||
actions: {
|
||||
buy: BuyEvolutionDialog.#onBuy,
|
||||
cancel: BuyEvolutionDialog.#onCancel
|
||||
}
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/vermine2047/templates/dialogs/buy-evolution-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 BuyEvolutionDialog({ actor });
|
||||
}
|
||||
|
||||
constructor(options = {}) {
|
||||
super(options);
|
||||
this.#actor = options.actor;
|
||||
}
|
||||
|
||||
get title() {
|
||||
return game.i18n.localize("VERMINE.buy_evolution");
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const actor = this.#actor;
|
||||
const mode = VermineExperience.gameMode();
|
||||
const modeLabels = {
|
||||
1: game.i18n.localize("VERMINE.mode_survie"),
|
||||
2: game.i18n.localize("VERMINE.mode_cauchemar"),
|
||||
3: game.i18n.localize("VERMINE.mode_apocalypse")
|
||||
};
|
||||
const levels = [];
|
||||
for (let value = 1; value <= 4; value++) {
|
||||
levels.push({ value, cost: value });
|
||||
}
|
||||
return {
|
||||
evolutionDice: actor.system?.experience?.evolution || 0,
|
||||
defaultName: game.i18n.localize("ITEMS.new_evolution"),
|
||||
levels,
|
||||
totalLevels: VermineExperience.evolutionTotalLevels(actor),
|
||||
modeMax: VermineExperience.evolutionMaxForMode(mode),
|
||||
modeLabel: modeLabels[mode] || ""
|
||||
};
|
||||
}
|
||||
|
||||
static async #onCancel(event, target) {
|
||||
this.close();
|
||||
}
|
||||
|
||||
static async #onBuy(event, target) {
|
||||
const actor = this.#actor;
|
||||
const name = this.element.querySelector("#evolution-name")?.value?.trim() || "";
|
||||
const cost = parseInt(this.element.querySelector("#evolution-level")?.value, 10) || 0;
|
||||
const mutation = this.element.querySelector("#evolution-mutation")?.checked || false;
|
||||
|
||||
if (!name) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_evolution_no_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
const error = VermineExperience.validateEvolutionPurchase(actor, cost, mutation);
|
||||
if (error) {
|
||||
ui.notifications.warn(game.i18n.localize(error));
|
||||
return;
|
||||
}
|
||||
|
||||
const created = await actor.createEmbeddedDocuments("Item", [{
|
||||
name,
|
||||
type: "evolution",
|
||||
system: {
|
||||
level: { value: cost },
|
||||
mutation
|
||||
}
|
||||
}]);
|
||||
// N'efface les Dés d'Évolution que si l'item a bien été créé.
|
||||
if (!created || created.length === 0) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_evolution_create"));
|
||||
return;
|
||||
}
|
||||
await actor.update({ "system.experience.evolution": 0 });
|
||||
|
||||
ui.notifications.info(game.i18n.localize("VERMINE.buy_evolution_done"));
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
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 malus = VermineExchange.woundMalus(this.actor);
|
||||
const total = Math.max(0, this.basePool + selfControl + pools + specialty + help + tooling + group - malus);
|
||||
return { ability, skill, selfControl, pools, specialty, help, tooling, group, malus, 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 - b.malus;
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
const rollParams = {
|
||||
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 }
|
||||
};
|
||||
|
||||
// Fermeture immédiate du dialogue : le jet (et l'animation 3D) continue
|
||||
// en arrière-plan sans bloquer la fermeture.
|
||||
this.close();
|
||||
|
||||
await VermineUtils.roll(rollParams);
|
||||
}
|
||||
|
||||
#getRangeKey() {
|
||||
const el = this.#el.querySelector('input[name="combat-difficulty"]:checked');
|
||||
return el?.dataset?.rangeKey ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
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");
|
||||
// Associe chaque personnage à son premier groupe (même logique que RollDialog.#findGroup)
|
||||
const memberOf = new Map();
|
||||
for (const g of groups) {
|
||||
for (const id of g.system?.members || []) {
|
||||
if (!memberOf.has(id)) memberOf.set(id, g.id);
|
||||
}
|
||||
}
|
||||
return {
|
||||
characters: characters.map(a => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
dice: a.system?.experience?.dice || 0,
|
||||
groupId: memberOf.get(a.id) || ""
|
||||
})),
|
||||
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.element.querySelector("#group-select")?.addEventListener("change", () => this.#updateGroupFilter());
|
||||
this.#updateUI();
|
||||
this.#updateGroupFilter();
|
||||
}
|
||||
|
||||
#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`;
|
||||
}
|
||||
|
||||
/** Filtre les personnages selon le groupe sélectionné (vide = tous). */
|
||||
#updateGroupFilter() {
|
||||
const groupId = this.element.querySelector("#group-select")?.value || "";
|
||||
for (const row of this.element.querySelectorAll("[data-character-id]")) {
|
||||
const show = !groupId || row.dataset.groupId === groupId;
|
||||
row.hidden = !show;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
const phase = (group.system?.experience?.phase || 0) + 1;
|
||||
await group.update({
|
||||
"system.experience.dice": current + collective,
|
||||
"system.experience.phase": phase,
|
||||
"system.experience.learned.phase": phase,
|
||||
"system.experience.learned.level": false,
|
||||
"system.experience.learned.reserve": false,
|
||||
"system.experience.learned.ability": false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const row of this.element.querySelectorAll("[data-character-id]")) {
|
||||
// Quand un groupe précis est choisi, ne récompenser que ses membres.
|
||||
if (groupId && row.dataset.groupId !== groupId) continue;
|
||||
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,
|
||||
"system.experience.learned.ability": false,
|
||||
"system.experience.learned.background": false,
|
||||
"system.experience.learned.reputation": false
|
||||
};
|
||||
await actor.update(update);
|
||||
results.push({ name: actor.name, gained });
|
||||
}
|
||||
|
||||
const lines = [`${game.i18n.localize("VERMINE.experience_phase")} +1`];
|
||||
if (groupId) {
|
||||
const group = game.actors.get(groupId);
|
||||
lines.push(`${group?.name ?? ""} (${game.i18n.localize("VERMINE.collective")}): +${collective}D`);
|
||||
}
|
||||
for (const r of results) {
|
||||
lines.push(`${r.name}: +${r.gained}D`);
|
||||
}
|
||||
await ChatMessage.create({
|
||||
user: game.user?._id,
|
||||
speaker: ChatMessage.getSpeaker(),
|
||||
content: `<div class="vermine-roll-message"><h3>${game.i18n.localize("VERMINE.experience_rewards")}</h3><div class="roll-context flexrow flex-wrap">${lines.map(l => `<span class="context-item">${l}</span>`).join("")}</div></div>`
|
||||
});
|
||||
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import { VermineUtils } from '../roll.mjs';
|
||||
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
/**
|
||||
* Dialogue d'apprentissage de Groupe (phase d'Expérience).
|
||||
* Permet de dépenser des Dés d'Expérience du Groupe pour tenter d'améliorer
|
||||
* le Niveau de Groupe, la Réserve de Groupe ou de développer une Capacité
|
||||
* collective. La progression est appliquée automatiquement en cas de réussite.
|
||||
*/
|
||||
export default class GroupLearningDialog 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: 560,
|
||||
},
|
||||
actions: {
|
||||
roll: GroupLearningDialog.#onRoll,
|
||||
cancel: GroupLearningDialog.#onCancel,
|
||||
},
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
main: { template: 'systems/vermine2047/templates/dialogs/group-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 GroupLearningDialog({ actor });
|
||||
}
|
||||
|
||||
constructor(options = {}) {
|
||||
super(options);
|
||||
this.#actor = options.actor;
|
||||
}
|
||||
|
||||
get title() {
|
||||
return game.i18n.localize('VERMINE.open_learning');
|
||||
}
|
||||
|
||||
_prepareContext() {
|
||||
const actor = this.#actor;
|
||||
return {
|
||||
actor,
|
||||
system: actor.system,
|
||||
dice: actor.system?.experience?.dice || 0,
|
||||
abilities: actor.itemTypes.ability
|
||||
.filter((i) => i.system.type !== 'totem' && !i.system.learned)
|
||||
.map((a) => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
threshold: a.system?.learn?.threshold ?? 5,
|
||||
level: a.system?.level?.value ?? 1,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
_onRender() {
|
||||
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 ?? 'level';
|
||||
}
|
||||
|
||||
#getAbilityId() {
|
||||
return this.#el.querySelector('#learning-ability')?.value ?? '';
|
||||
}
|
||||
|
||||
#getDiceSpent() {
|
||||
return Math.max(1, parseInt(this.#el.querySelector('#learning-dice')?.value, 10) || 1);
|
||||
}
|
||||
|
||||
#compute() {
|
||||
const actor = this.#actor;
|
||||
const type = this.#getType();
|
||||
const key = type === 'ability' ? this.#getAbilityId() : '';
|
||||
let difficulty = 7;
|
||||
let handicap = 0;
|
||||
let targetLevel = null;
|
||||
let targetLabel = '';
|
||||
|
||||
if (type === 'level') {
|
||||
const current = actor.system?.level?.value || 1;
|
||||
difficulty = actor.system?.members?.length || 0;
|
||||
handicap = current;
|
||||
targetLevel = current + 1;
|
||||
} else if (type === 'reserve') {
|
||||
const current = actor.system?.reserve?.value || 0;
|
||||
difficulty = current;
|
||||
handicap = 2;
|
||||
targetLevel = current + 1;
|
||||
} else {
|
||||
const ability = key ? actor.items.get(key) : null;
|
||||
if (ability) {
|
||||
difficulty = ability.system?.learn?.threshold ?? 5;
|
||||
handicap = ability.system?.level?.value ?? 1;
|
||||
targetLabel = ability.name;
|
||||
}
|
||||
}
|
||||
|
||||
const labels = {
|
||||
level: 'VERMINE.learn_level',
|
||||
reserve: 'VERMINE.learn_group_reserve',
|
||||
ability: 'VERMINE.learn_ability',
|
||||
};
|
||||
return {
|
||||
type,
|
||||
key,
|
||||
targetLevel,
|
||||
difficulty,
|
||||
handicap,
|
||||
poolModifier: 0,
|
||||
label: game.i18n.localize(labels[type] || labels.level),
|
||||
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-level-section', type === 'level');
|
||||
show('#learning-reserve-section', type === 'reserve');
|
||||
show('#learning-ability-section', type === 'ability');
|
||||
|
||||
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 #onCancel() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
/** Renvoie la clé i18n d'erreur si le jet est impossible, sinon null. */
|
||||
#getError(c) {
|
||||
const actor = this.#actor;
|
||||
const ex = actor.system?.experience;
|
||||
// Limite : un seul Niveau, une seule Réserve et une seule Capacité par phase.
|
||||
if (ex?.learned?.phase === ex?.phase && ex.learned[c.type]) {
|
||||
return 'VERMINE.error_learning_limit';
|
||||
}
|
||||
if (c.type === 'level' && c.targetLevel !== null && (actor.system?.level?.value || 0) >= 10) {
|
||||
return 'VERMINE.error_group_level_max';
|
||||
}
|
||||
if (c.type === 'reserve' && c.targetLevel !== null &&
|
||||
(actor.system?.reserve?.value || 0) >= (actor.system?.reserve?.max || 10)) {
|
||||
return 'VERMINE.error_group_reserve_max';
|
||||
}
|
||||
if (c.type === 'ability') {
|
||||
if (!c.key) {
|
||||
return 'VERMINE.no_group_ability';
|
||||
}
|
||||
const ability = c.key ? actor.items.get(c.key) : null;
|
||||
const requiredLevel = ability?.system?.level?.value ?? 1;
|
||||
if (ability && (actor.system?.level?.value || 1) < requiredLevel) {
|
||||
return 'VERMINE.error_group_level_requirement';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static async #onRoll() {
|
||||
const actor = this.#actor;
|
||||
const ex = actor.system?.experience;
|
||||
const spent = Math.min(this.#getDiceSpent(), ex?.dice || 0);
|
||||
if (spent < 1) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_experience_dice'));
|
||||
return;
|
||||
}
|
||||
|
||||
const c = this.#compute();
|
||||
const errorKey = this.#getError(c);
|
||||
if (errorKey) {
|
||||
ui.notifications.warn(game.i18n.localize(errorKey));
|
||||
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;
|
||||
if ((roll._total ?? 0) >= required) {
|
||||
await this.#applySuccess(c);
|
||||
}
|
||||
// Les Dés d'Expérience sont perdus que le jet soit réussi ou raté.
|
||||
await actor.update({ "system.experience.dice": Math.max(0, (actor.system.experience.dice || 0) - spent) });
|
||||
this.close();
|
||||
}
|
||||
|
||||
async #applySuccess(c) {
|
||||
const actor = this.#actor;
|
||||
const updates = {};
|
||||
if (c.type === 'level') {
|
||||
const current = actor.system.level.value || 1;
|
||||
updates['system.level.value'] = Math.min(10, current + 1);
|
||||
updates['system.experience.learned.level'] = true;
|
||||
} else if (c.type === 'reserve') {
|
||||
const current = actor.system.reserve.value || 0;
|
||||
updates['system.reserve.value'] = Math.min(actor.system.reserve.max || 10, current + 1);
|
||||
updates['system.experience.learned.reserve'] = true;
|
||||
} else {
|
||||
if (c.key) {
|
||||
await actor.updateEmbeddedDocuments('Item', [{ _id: c.key, 'system.learned': true }]);
|
||||
}
|
||||
updates['system.experience.learned.ability'] = true;
|
||||
}
|
||||
updates['system.experience.learned.phase'] = actor.system.experience.phase;
|
||||
await actor.update(updates);
|
||||
ui.notifications.info(game.i18n.localize('VERMINE.learning_success'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,544 @@
|
||||
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)
|
||||
}));
|
||||
const totem = actor.system?.identity?.totem || "";
|
||||
const gameMode = this.#getGameMode();
|
||||
const groupLevel = this.#getGroupLevel();
|
||||
const totemAbilities = (actor.itemTypes?.ability || [])
|
||||
.filter(i =>
|
||||
i.system?.type === "character"
|
||||
&& !i.system?.learned
|
||||
&& i.system?.totem === totem
|
||||
&& (i.system?.level?.value || 0) <= gameMode
|
||||
&& groupLevel >= (i.system?.level?.value || 0)
|
||||
)
|
||||
.map(a => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
level: a.system.level.value,
|
||||
threshold: a.system.learn.threshold,
|
||||
hindrance: a.system.learn.hindrance
|
||||
}));
|
||||
const backgrounds = (actor.itemTypes?.background || [])
|
||||
.filter(i => !i.system?.learned)
|
||||
.map(b => ({ id: b.id, name: b.name, asterisk: b.system?.asterisk || false }));
|
||||
const human = actor.system?.adaptation?.totems?.human?.value || 0;
|
||||
const adapted = actor.system?.adaptation?.totems?.adapted?.value || 0;
|
||||
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,
|
||||
totem,
|
||||
gameMode,
|
||||
groupLevel,
|
||||
totemAbilities,
|
||||
backgrounds,
|
||||
reputation: actor.system?.attributes?.reputation?.value ?? 0,
|
||||
human,
|
||||
adapted,
|
||||
canUseAdaptedDie: VermineExperience.canUseAdaptedDie(human, adapted)
|
||||
};
|
||||
}
|
||||
|
||||
#getGameMode() {
|
||||
return parseInt(game.settings.get("vermine2047", "game-mode") || "1", 10) || 1;
|
||||
}
|
||||
|
||||
#getGroupLevel() {
|
||||
const groups = game.actors.filter(a => a.type === "group" && (a.system?.members || []).includes(this.#actor.id));
|
||||
return Math.max(0, ...groups.map(g => g.system?.level?.value || 0));
|
||||
}
|
||||
|
||||
#canUseAdaptedDie() {
|
||||
const a = this.#actor.system?.adaptation?.totems;
|
||||
return VermineExperience.canUseAdaptedDie(a?.human?.value || 0, a?.adapted?.value || 0);
|
||||
}
|
||||
|
||||
#totemCapacityCounts() {
|
||||
// Ne comptabilise que les Capacités du Totem courant du personnage
|
||||
// (les Capacités liées à un autre Totem ne bloquent pas les limites).
|
||||
const totem = this.#actor.system?.identity?.totem;
|
||||
const items = (this.#actor.itemTypes?.ability || []).filter(i =>
|
||||
i.system?.type === "character" && i.system?.learned && i.system?.totem === totem);
|
||||
const byLevel = { 1: 0, 2: 0, 3: 0 };
|
||||
for (const i of items) {
|
||||
const level = i.system?.level?.value || 0;
|
||||
byLevel[level] = (byLevel[level] || 0) + 1;
|
||||
}
|
||||
return { total: items.length, byLevel };
|
||||
}
|
||||
|
||||
async _onRender() {
|
||||
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 ?? "";
|
||||
if (type === "totem") return this.#el.querySelector("#learning-totem")?.value ?? "";
|
||||
if (type === "background") return this.#el.querySelector("#learning-background")?.value ?? "";
|
||||
return this.#el.querySelector("#learning-target")?.value ?? "";
|
||||
}
|
||||
|
||||
#getReputationDirection() {
|
||||
return this.#el.querySelector("#learning-reputation-direction")?.value ?? "reduce";
|
||||
}
|
||||
|
||||
#isAdaptedDieChecked() {
|
||||
return Boolean(this.#el.querySelector("#learning-adapted-die")?.checked);
|
||||
}
|
||||
|
||||
#getDiceSpent() {
|
||||
return Math.max(1, parseInt(this.#el.querySelector("#learning-dice")?.value, 10) || 1);
|
||||
}
|
||||
|
||||
#getSpecialtyName() {
|
||||
// Le dialogue peut avoir été fermé pendant le jet (this.#el null).
|
||||
const el = this.#el?.querySelector("#specialty-name");
|
||||
return el?.value?.trim() || game.i18n.localize("ITEMS.new_specialty");
|
||||
}
|
||||
|
||||
#compute() {
|
||||
const actor = this.#actor;
|
||||
const type = this.#getType();
|
||||
const key = this.#getTargetKey();
|
||||
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,
|
||||
adaptedDie: false,
|
||||
label,
|
||||
targetLabel: key ? game.i18n.localize(key === "self_control" ? "VERMINE.self_control" : "VERMINE.effort") : ""
|
||||
};
|
||||
}
|
||||
if (type === "totem") {
|
||||
const item = actor.itemTypes?.ability?.find(i => i.id === key);
|
||||
const data = item?.system;
|
||||
label = game.i18n.localize("VERMINE.learn_totem_ability");
|
||||
return {
|
||||
type,
|
||||
key,
|
||||
targetLevel: data?.level?.value || 0,
|
||||
difficulty: VermineExperience.totemAbilityLearningDifficulty(data),
|
||||
handicap: VermineExperience.totemAbilityLearningHandicap(data),
|
||||
poolModifier: 0,
|
||||
adaptedDie: false,
|
||||
label,
|
||||
targetLabel: item?.name || ""
|
||||
};
|
||||
}
|
||||
if (type === "background") {
|
||||
const item = actor.itemTypes?.background?.find(i => i.id === key);
|
||||
const data = item?.system;
|
||||
label = game.i18n.localize("VERMINE.learn_background");
|
||||
return {
|
||||
type,
|
||||
key,
|
||||
targetLevel: 0,
|
||||
difficulty: VermineExperience.backgroundLearningDifficulty(),
|
||||
handicap: VermineExperience.backgroundLearningHandicap(data?.asterisk),
|
||||
poolModifier: 0,
|
||||
adaptedDie: false,
|
||||
label,
|
||||
targetLabel: item?.name || ""
|
||||
};
|
||||
}
|
||||
if (type === "reputation") {
|
||||
const current = actor.system?.attributes?.reputation?.value ?? 0;
|
||||
const direction = this.#getReputationDirection();
|
||||
if (direction === "increase") {
|
||||
return {
|
||||
type,
|
||||
key: "increase",
|
||||
targetLevel: current,
|
||||
difficulty: VermineExperience.reputationIncreaseDifficulty(current),
|
||||
handicap: 0,
|
||||
poolModifier: 0,
|
||||
adaptedDie: false,
|
||||
label: game.i18n.localize("VERMINE.learn_reputation"),
|
||||
targetLabel: game.i18n.localize("VERMINE.reputation_increase")
|
||||
};
|
||||
}
|
||||
return {
|
||||
type,
|
||||
key: "reduce",
|
||||
targetLevel: Math.max(0, current - 1),
|
||||
difficulty: VermineExperience.reputationReduceDifficulty(current),
|
||||
handicap: 0,
|
||||
poolModifier: 0,
|
||||
adaptedDie: false,
|
||||
label: game.i18n.localize("VERMINE.learn_reputation"),
|
||||
targetLabel: game.i18n.localize("VERMINE.reputation_reduce")
|
||||
};
|
||||
}
|
||||
// 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),
|
||||
adaptedDie: this.#canUseAdaptedDie() && this.#isAdaptedDieChecked(),
|
||||
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");
|
||||
show("#learning-totem-section", type === "totem");
|
||||
show("#learning-background-section", type === "background");
|
||||
show("#learning-reputation-section", type === "reputation");
|
||||
show("#learning-adapted-die-row", type === "evolution" && this.#canUseAdaptedDie());
|
||||
|
||||
// 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);
|
||||
let poolText = `${pool}D${c.poolModifier ? ` (${c.poolModifier >= 0 ? "+" : ""}${c.poolModifier}D)` : ""}`;
|
||||
if (c.adaptedDie) poolText += " — " + game.i18n.localize("VERMINE.adapted_die_x2");
|
||||
set("#pool-value", poolText);
|
||||
if (type === "reputation") {
|
||||
const current = this.#actor.system?.attributes?.reputation?.value ?? 0;
|
||||
const direction = this.#getReputationDirection();
|
||||
set("#reputation-target", direction === "increase"
|
||||
? `${current} → ${Math.min(10, current + 1)}+`
|
||||
: `${current} → ${Math.max(0, current - 1)}`);
|
||||
}
|
||||
const poolInput = this.#el.querySelector("#learning-dice");
|
||||
if (poolInput) poolInput.max = this.#actor.system?.experience?.dice || 0;
|
||||
}
|
||||
|
||||
static async #onCancel() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
static async #onRoll() {
|
||||
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,
|
||||
// un seul Dé d'Évolution, Capacité de Totem, Historique ou jet de
|
||||
// Réputation par phase.
|
||||
if (ex?.learned?.phase === ex?.phase) {
|
||||
const flagMap = { skill: "skill", specialty: "specialty", reserve: "reserve", characteristic: "characteristic", evolution: "evolution", totem: "ability", background: "background", reputation: "reputation" };
|
||||
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;
|
||||
}
|
||||
if (c.type === "totem") {
|
||||
const item = actor.itemTypes?.ability?.find(i => i.id === c.key);
|
||||
if (!item || item.system?.learned) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.no_totem_ability"));
|
||||
return;
|
||||
}
|
||||
const level = item.system?.level?.value || 0;
|
||||
if (level > this.#getGameMode()) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_mode"));
|
||||
return;
|
||||
}
|
||||
if (item.system?.totem !== actor.system?.identity?.totem) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_totem"));
|
||||
return;
|
||||
}
|
||||
if (this.#getGroupLevel() < level) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_group_level"));
|
||||
return;
|
||||
}
|
||||
const counts = this.#totemCapacityCounts();
|
||||
if (level === 1 && (counts.byLevel[1] || 0) >= 3) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_limit"));
|
||||
return;
|
||||
}
|
||||
if (level >= 2 && (counts.byLevel[level] || 0) >= 1) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_limit"));
|
||||
return;
|
||||
}
|
||||
if (counts.total >= 5) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_limit"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (c.type === "background") {
|
||||
const item = actor.itemTypes?.background?.find(i => i.id === c.key);
|
||||
if (!item || item.system?.learned) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.no_background"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (c.type === "reputation") {
|
||||
const current = actor.system?.attributes?.reputation?.value ?? 0;
|
||||
if (c.key === "reduce" && current <= 0) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_reputation_min"));
|
||||
return;
|
||||
}
|
||||
if (c.key === "increase" && current >= 10) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_reputation_max"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (c.type === "evolution" && c.adaptedDie && !this.#canUseAdaptedDie()) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_evolution_no_adapted"));
|
||||
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,
|
||||
totems: c.adaptedDie ? { adapted: true } : { human: false, adapted: false },
|
||||
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, specialtyName, roll._total ?? 0);
|
||||
}
|
||||
// Les Dés d'Expérience sont perdus que le jet soit réussi ou raté.
|
||||
await actor.update({ "system.experience.dice": Math.max(0, (actor.system.experience.dice || 0) - spent) });
|
||||
this.close();
|
||||
}
|
||||
|
||||
async #applySuccess(c, specialtyName, successes = 0) {
|
||||
const actor = this.#actor;
|
||||
const updates = {};
|
||||
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 if (c.type === "evolution") {
|
||||
updates["system.experience.evolution"] = (actor.system.experience.evolution || 0) + 1;
|
||||
updates["system.experience.learned.evolution"] = true;
|
||||
} else if (c.type === "totem") {
|
||||
updates["system.experience.learned.ability"] = true;
|
||||
await actor.updateEmbeddedDocuments("Item", [{ _id: c.key, "system.learned": true }]);
|
||||
} else if (c.type === "background") {
|
||||
updates["system.experience.learned.background"] = true;
|
||||
await actor.updateEmbeddedDocuments("Item", [{ _id: c.key, "system.learned": true }]);
|
||||
} else if (c.type === "reputation") {
|
||||
const current = actor.system?.attributes?.reputation?.value ?? 0;
|
||||
if (c.key === "increase") {
|
||||
updates["system.attributes.reputation.value"] = Math.min(10, current + successes);
|
||||
} else {
|
||||
updates["system.attributes.reputation.value"] = Math.max(0, current - 1);
|
||||
}
|
||||
updates["system.experience.learned.reputation"] = true;
|
||||
}
|
||||
updates["system.experience.learned.phase"] = actor.system.experience.phase;
|
||||
await actor.update(updates);
|
||||
ui.notifications.info(game.i18n.localize("VERMINE.learning_success"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import { VermineUtils } from "../roll.mjs"
|
||||
import { VermineExchange } from "../exchange.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()
|
||||
const malus = VermineExchange.woundMalus(this.#actor)
|
||||
return { pool: this.pool, help, group, malus, total: Math.max(0, this.pool + help + group - malus) }
|
||||
}
|
||||
|
||||
#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 - b.malus
|
||||
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
|
||||
|
||||
const rollParams = {
|
||||
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
|
||||
}
|
||||
|
||||
// Fermeture immédiate du dialogue : le jet (et l'animation 3D) continue
|
||||
// en arrière-plan sans bloquer la fermeture.
|
||||
this.close()
|
||||
|
||||
await VermineUtils.roll(rollParams)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { VermineUtils } from "../roll.mjs";
|
||||
import { VermineExchange } from "../exchange.mjs";
|
||||
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
@@ -23,7 +24,8 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
},
|
||||
actions: {
|
||||
roll: RollDialog.#onRoll,
|
||||
cancel: RollDialog.#onCancel
|
||||
cancel: RollDialog.#onCancel,
|
||||
giveToGroup: RollDialog.#onGiveToGroup
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,6 +34,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 +47,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 +55,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 +77,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 +121,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 +138,45 @@ 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 malus = VermineExchange.woundMalus(this.#actor);
|
||||
const total = Math.max(0, ability + skillPool + selfControl + specialty + help + tooling + group + experience - malus);
|
||||
return { ability, skill: skillPool, selfControl, specialty, help, tooling, group, experience, malus, 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 +253,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 +294,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 - b.malus;
|
||||
}
|
||||
|
||||
#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 +386,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,7 +430,27 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
await this.#actor.update({ "system.attributes.self_control.value": newVal });
|
||||
}
|
||||
|
||||
await VermineUtils.roll({
|
||||
// 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 });
|
||||
}
|
||||
|
||||
const rollParams = {
|
||||
actor: this.#actor,
|
||||
NoD: this.getDicePool(),
|
||||
Reroll: this.getReroll(),
|
||||
@@ -318,10 +463,19 @@ 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
|
||||
};
|
||||
|
||||
// Fermeture immédiate du dialogue : le jet (et l'animation 3D) continue
|
||||
// en arrière-plan sans bloquer la fermeture.
|
||||
this.close();
|
||||
|
||||
await VermineUtils.roll(rollParams);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { VermineTotemDice } from "../totem-dice.mjs";
|
||||
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
/**
|
||||
* Dialogue de gestion des Dés de Totems (Humain / Adapté).
|
||||
* Permet d'appliquer les gains et pertes liés aux Instincts et Interdits
|
||||
* (1D ou 2D selon la gravité de l'acte), en respectant les limites
|
||||
* (3 Dés du même Totem, 5 Dés au maximum) et les règles d'échange.
|
||||
*/
|
||||
export default class TotemDiceDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
||||
|
||||
#actor;
|
||||
#serious = false;
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["vermine-roll", "totem-dice"],
|
||||
tag: "form",
|
||||
window: {
|
||||
icon: "fas fa-dice-d10",
|
||||
resizable: false
|
||||
},
|
||||
position: {
|
||||
width: 480,
|
||||
height: 420
|
||||
},
|
||||
actions: {
|
||||
gainHuman: TotemDiceDialog.#onGainHuman,
|
||||
gainAdapted: TotemDiceDialog.#onGainAdapted,
|
||||
loseHuman: TotemDiceDialog.#onLoseHuman,
|
||||
close: TotemDiceDialog.#onClose
|
||||
}
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/vermine2047/templates/dialogs/totem-dice-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 TotemDiceDialog({ actor });
|
||||
}
|
||||
|
||||
constructor(options = {}) {
|
||||
super(options);
|
||||
this.#actor = options.actor;
|
||||
}
|
||||
|
||||
get title() {
|
||||
return game.i18n.localize("VERMINE.totem_dice");
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const actor = this.#actor;
|
||||
const influence = VermineTotemDice.getInfluence(actor);
|
||||
const influenceLabels = {
|
||||
human: game.i18n.localize("TOTEMS.human.name"),
|
||||
adapted: game.i18n.localize("TOTEMS.adapted.name"),
|
||||
neutral: game.i18n.localize("VERMINE.totem_neutral")
|
||||
};
|
||||
return {
|
||||
humanValue: actor.system?.adaptation?.totems?.human?.value ?? 0,
|
||||
humanMax: actor.system?.adaptation?.totems?.human?.max ?? 3,
|
||||
adaptedValue: actor.system?.adaptation?.totems?.adapted?.value ?? 0,
|
||||
adaptedMax: actor.system?.adaptation?.totems?.adapted?.max ?? 3,
|
||||
influenceLabel: influenceLabels[influence] || "",
|
||||
serious: this.#serious
|
||||
};
|
||||
}
|
||||
|
||||
_onRender(context, options) {
|
||||
super._onRender(context, options);
|
||||
const checkbox = this.element.querySelector("#totem-serious");
|
||||
if (checkbox) {
|
||||
checkbox.checked = this.#serious;
|
||||
checkbox.addEventListener("change", () => { this.#serious = checkbox.checked; });
|
||||
}
|
||||
}
|
||||
|
||||
#getCount() {
|
||||
return this.element.querySelector("#totem-serious")?.checked ? 2 : 1;
|
||||
}
|
||||
|
||||
#notify(result) {
|
||||
const key = result.totem === "human" ? "TOTEMS.human.name" : "TOTEMS.adapted.name";
|
||||
const totemName = game.i18n.localize(key);
|
||||
const parts = [];
|
||||
if (result.gained) parts.push(game.i18n.localize("VERMINE.totem_gained").replace("{totem}", totemName));
|
||||
if (result.exchanged) parts.push(game.i18n.localize("VERMINE.totem_exchanged").replace("{totem}", totemName));
|
||||
if (result.replaced) parts.push(game.i18n.localize("VERMINE.totem_replaced").replace("{totem}", totemName));
|
||||
if (result.lost) parts.push(game.i18n.localize("VERMINE.totem_lost"));
|
||||
if (result.gainedAdapted) parts.push(game.i18n.localize("VERMINE.totem_gained_adapted"));
|
||||
if (result.refused) parts.push(game.i18n.localize("VERMINE.totem_limit_reached"));
|
||||
const text = parts.join(" ");
|
||||
if (result.refused && !result.gained && !result.lost && !result.exchanged && !result.replaced) {
|
||||
ui.notifications.warn(text || game.i18n.localize("VERMINE.totem_limit_reached"));
|
||||
} else if (text) {
|
||||
ui.notifications.info(text);
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
||||
static async #onGainHuman() {
|
||||
const result = await VermineTotemDice.gainDie(this.#actor, "human", this.#getCount());
|
||||
this.#notify(result);
|
||||
}
|
||||
|
||||
static async #onGainAdapted() {
|
||||
const result = await VermineTotemDice.gainDie(this.#actor, "adapted", this.#getCount());
|
||||
this.#notify(result);
|
||||
}
|
||||
|
||||
static async #onLoseHuman() {
|
||||
const result = await VermineTotemDice.loseDie(this.#actor, "human", this.#getCount());
|
||||
this.#notify(result);
|
||||
}
|
||||
|
||||
static #onClose() {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,549 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
/** Ordre de gravité des blessures (croissant). */
|
||||
static WOUND_ORDER = ['minorWound', 'majorWound', 'deadlyWound']
|
||||
|
||||
/** Malus de la pire blessure (règles p.42) : -1D Légère, -2D Grave, -3D Mortelle. */
|
||||
static WOUND_MALUS = { minorWound: 1, majorWound: 2, deadlyWound: 3 }
|
||||
|
||||
/**
|
||||
* Malus de blessure applicable à toutes les actions : seul le Malus de la
|
||||
* Blessure la plus grave compte (non cumulatif).
|
||||
* @param {Actor} actor
|
||||
* @returns {number} 0, 1, 2 ou 3
|
||||
*/
|
||||
static woundMalus(actor) {
|
||||
if (!actor?.system) return 0
|
||||
if (actor.system.deadlyWound?.value > 0) return 3
|
||||
if (actor.system.majorWound?.value > 0) return 2
|
||||
if (actor.system.minorWound?.value > 0) return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique +1 cercle de blessure, en montant en gravité si les cercles du
|
||||
* niveau correspondant sont déjà pleins (règles p.42 : si tous les cercles
|
||||
* d'une Blessure sont cochés, la suivante est de gravité supérieure).
|
||||
* @param {Actor} actor
|
||||
* @param {string} woundCategory 'minorWound' | 'majorWound' | 'deadlyWound'
|
||||
* @returns {Promise<Object|boolean|null>}
|
||||
* - false si refusé (permissions) ou null si invalide
|
||||
* - { category, value, escalated, death } si appliqué (death si tout est plein)
|
||||
*/
|
||||
static async applyWound(actor, woundCategory) {
|
||||
if (!actor || !woundCategory) return null
|
||||
if (!this.canApplyWound(actor)) return false
|
||||
const startIdx = this.WOUND_ORDER.indexOf(woundCategory)
|
||||
if (startIdx === -1) return null
|
||||
for (let i = startIdx; i < this.WOUND_ORDER.length; i++) {
|
||||
const cat = this.WOUND_ORDER[i]
|
||||
const sys = actor.system?.[cat]
|
||||
const max = sys?.max ?? Number.POSITIVE_INFINITY
|
||||
const value = sys?.value || 0
|
||||
if (value < max) {
|
||||
await actor.update({ [`system.${cat}.value`]: value + 1 })
|
||||
return { category: cat, value: value + 1, escalated: cat !== woundCategory, death: false }
|
||||
}
|
||||
}
|
||||
return { category: woundCategory, value: null, escalated: false, death: true }
|
||||
}
|
||||
|
||||
// ── Résolution ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Résout l'échange pour une cible donnée (défense la plus récente liée
|
||||
* à cette cible, ou abstention si aucune défense).
|
||||
* @param {ChatMessage} attackMessage
|
||||
* @param {string|null} targetId id d'acteur de la cible (défaut : première cible)
|
||||
* @returns {Promise<Object|null>} résultat de la résolution
|
||||
*/
|
||||
static async resolve(attackMessage, targetId = null) {
|
||||
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||
if (!exchange) return null
|
||||
|
||||
const target = (targetId && exchange.targets?.find(t => t.id === targetId)) || exchange.targets?.[0] || null
|
||||
if (!target) return null
|
||||
|
||||
const defs = this.#defenseMessagesFor(exchange.id, target.id)
|
||||
const defenderMessage = defs[defs.length - 1]
|
||||
const defense = defenderMessage ? defenderMessage.getFlag('world', FLAG_DEFENSE) : null
|
||||
|
||||
const attackerSuccesses = this.#messageTotal(attackMessage)
|
||||
const defenderSuccesses = defense ? this.#messageTotal(defenderMessage) : 0
|
||||
const hit = attackerSuccesses > defenderSuccesses
|
||||
|
||||
// Le flag de défense porte actorId/actorUuid (clés différentes de celles
|
||||
// des cibles) : on normalise la référence avant résolution de l'acteur.
|
||||
const defenderRef = defense
|
||||
? { id: defense.actorId ?? null, uuid: defense.actorUuid ?? null, name: defense.actorName ?? '' }
|
||||
: target
|
||||
const defender = await this.#resolveDefender(defenderRef)
|
||||
|
||||
const damage = (exchange.baseDamage || 0) + attackerSuccesses
|
||||
const parryLevel = defense?.type === 'parade' ? (defense.parryLevel ?? 0) : null
|
||||
const protection = hit && parryLevel !== null
|
||||
? parryLevel
|
||||
: (defender ? this.protectionTotal(defender, hit ? null : parryLevel) : 0)
|
||||
const netDamage = hit ? Math.max(0, damage - protection) : 0
|
||||
const woundCategory = hit && defender ? this.highestWound(netDamage, defender) : null
|
||||
|
||||
return {
|
||||
exchangeId: exchange.id,
|
||||
defenderId: defenderRef.id ?? null,
|
||||
defenderUuid: defenderRef.uuid ?? null,
|
||||
defenderName: defense?.actorName || target.name || '',
|
||||
defenseType: defense?.type || null,
|
||||
attackerSuccesses,
|
||||
defenderSuccesses,
|
||||
hit,
|
||||
damage,
|
||||
protection,
|
||||
netDamage,
|
||||
woundCategory,
|
||||
woundThreshold: woundCategory && defender ? defender.system[woundCategory].threshold : null,
|
||||
canApply: Boolean(defender) && this.canApplyWound(defender)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rend le bloc de résolution (HTML) pour insertion dans le message.
|
||||
* @param {Object} r Résultat de VermineExchange.resolve
|
||||
* @returns {string}
|
||||
*/
|
||||
static renderResolution(r) {
|
||||
const t = (key) => game.i18n.localize(key)
|
||||
const verdict = !r.hit
|
||||
? (r.defenseType === 'parade'
|
||||
? `<span class="res-parried">${t('VERMINE.missed_parade')}</span>`
|
||||
: `<span class="res-dodged">${t('VERMINE.missed_esquive')}</span>`)
|
||||
: (r.defenseType === 'parade'
|
||||
? `<span class="res-hit">${t('VERMINE.hit_parade')}</span>`
|
||||
: `<span class="res-hit">${t('VERMINE.hit')}</span>`)
|
||||
|
||||
const lines = [
|
||||
`<div class="resolution-line">${t('VERMINE.attacker_successes')}: ${r.attackerSuccesses} · ${t('VERMINE.defender_successes')}: ${r.defenderSuccesses} → ${verdict}</div>`
|
||||
]
|
||||
if (r.hit) {
|
||||
lines.push(`<div class="resolution-line">${t('VERMINE.raw_damage')}: <strong>${r.damage}</strong>${r.protection ? ` − ${t('ADVERSITY.protection')}: ${r.protection}` : ''} = ${t('VERMINE.net_damage')}: <strong>${r.netDamage}</strong></div>`)
|
||||
if (r.woundCategory) {
|
||||
lines.push(`<div class="resolution-line res-wound">${t('VERMINE.wound')}: <strong>${t(this.woundLabelKey(r.woundCategory))}</strong> (${t('VERMINE.wounds.threshold')} ${r.woundThreshold}D)</div>`)
|
||||
} else {
|
||||
lines.push(`<div class="resolution-line res-noshock">${t('VERMINE.no_wound')}</div>`)
|
||||
}
|
||||
} else {
|
||||
lines.push(`<div class="resolution-line res-nodamage">${t('VERMINE.no_damage')}</div>`)
|
||||
}
|
||||
|
||||
const apply = (r.hit && r.woundCategory && r.canApply)
|
||||
? `<button type="button" class="exchange-apply" data-action="exchange-apply" data-defender-id="${r.defenderId}" data-defender-uuid="${r.defenderUuid}" data-wound="${r.woundCategory}"><i class="fas fa-briefcase-medical"></i> ${t('VERMINE.apply_wound')}</button>`
|
||||
: ''
|
||||
|
||||
return `<div class="exchange-resolution">
|
||||
<div class="resolution-header"><i class="fas fa-exchange-alt"></i> ${t('VERMINE.resolve_result')}</div>
|
||||
${lines.join('')}
|
||||
${apply}
|
||||
</div>`
|
||||
}
|
||||
|
||||
// ── Liaison chat ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Lie les interactions d'échange de coups sur un message de chat.
|
||||
* @param {HTMLElement} html élément du message
|
||||
* @param {ChatMessage} message
|
||||
*/
|
||||
static bindChat(html, message) {
|
||||
const exchange = message.getFlag('world', FLAG_EXCHANGE)
|
||||
if (exchange) {
|
||||
html.querySelectorAll('.exchange-defend').forEach(btn => {
|
||||
btn.addEventListener('click', ev => this.#onDefense(ev, message))
|
||||
})
|
||||
html.querySelectorAll('.exchange-resolve').forEach(btn => {
|
||||
btn.addEventListener('click', ev => this.#onResolve(ev, message))
|
||||
})
|
||||
}
|
||||
html.querySelectorAll('[data-action="exchange-apply"]').forEach(btn => {
|
||||
btn.addEventListener('click', ev => this.#onApply(ev, message))
|
||||
})
|
||||
}
|
||||
|
||||
static async #onDefense(ev, attackMessage) {
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
const btn = ev.currentTarget
|
||||
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||
if (!exchange) return
|
||||
const type = btn.dataset.type
|
||||
const actor = await this.#resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
|
||||
if (!actor) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
|
||||
return
|
||||
}
|
||||
if (!game.user.isGM && !actor.isOwner) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
|
||||
return
|
||||
}
|
||||
if (actor.type === 'creature') {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_creature_no_defense'))
|
||||
return
|
||||
}
|
||||
const defense = {
|
||||
exchangeId: exchange.id,
|
||||
type,
|
||||
actorId: actor.id,
|
||||
actorUuid: actor.uuid,
|
||||
actorName: actor.name,
|
||||
difficulty: exchange.difficulty,
|
||||
parryLevel: type === 'parade' ? this.#parryLevel(actor) : null
|
||||
}
|
||||
// PNJ : pas de caractéristiques/compétences — la défense utilise la valeur
|
||||
// d'Action (Expérience), difficulté verrouillée.
|
||||
if (actor.type === 'npc') {
|
||||
const { default: NpcRollDialog } = await import('./dialogs/npcRollDialog.mjs')
|
||||
const dialog = await NpcRollDialog.create({
|
||||
actor,
|
||||
rolltype: 'action',
|
||||
locks: { difficulty: exchange.difficulty },
|
||||
defense
|
||||
})
|
||||
if (dialog) dialog.render(true)
|
||||
return
|
||||
}
|
||||
const { default: RollDialog } = await import('./dialogs/rollDialog.mjs')
|
||||
const locks = this.#defenseLocks(actor, exchange, type)
|
||||
const dialog = await RollDialog.create({
|
||||
actor,
|
||||
rolltype: 'skill',
|
||||
label: locks.skill,
|
||||
locks,
|
||||
defense
|
||||
})
|
||||
if (dialog) dialog.render(true)
|
||||
}
|
||||
|
||||
static #defenseLocks(actor, exchange, type) {
|
||||
if (type === 'parade') {
|
||||
return { ability: 'vigor', skill: this.#parrySkill(actor), difficulty: exchange.difficulty }
|
||||
}
|
||||
return {
|
||||
ability: 'reflexes',
|
||||
skill: exchange.mode === 'ranged' ? 'alertness' : 'close',
|
||||
difficulty: exchange.difficulty
|
||||
}
|
||||
}
|
||||
|
||||
static #parrySkill(actor) {
|
||||
const hasMelee = actor.itemTypes?.weapon?.some(w => w.system.equipped && w.system.skill === 'melee')
|
||||
return hasMelee ? 'melee' : 'close'
|
||||
}
|
||||
|
||||
static #parryLevel(actor) {
|
||||
const defenses = actor.itemTypes?.defense?.filter(d => d.system.equipped) || []
|
||||
return defenses.reduce((best, d) => Math.max(best, d.system.level || 0), 0)
|
||||
}
|
||||
|
||||
static async #onResolve(ev, attackMessage) {
|
||||
ev.preventDefault()
|
||||
const btn = ev.currentTarget
|
||||
if (!game.user.isGM && !attackMessage.isOwner) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
|
||||
return
|
||||
}
|
||||
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||
if (!exchange) return
|
||||
if (!exchange.targets?.length) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_target'))
|
||||
return
|
||||
}
|
||||
const wrapper = btn.closest('.exchange-actions')
|
||||
const container = wrapper?.parentElement
|
||||
if (!container) return
|
||||
container.querySelectorAll('.exchange-resolution').forEach(el => el.remove())
|
||||
|
||||
const targets = exchange.targets?.length ? exchange.targets : [null]
|
||||
for (const target of targets) {
|
||||
const result = await this.resolve(attackMessage, target?.id ?? null)
|
||||
if (!result) continue
|
||||
const block = document.createElement('div')
|
||||
block.innerHTML = this.renderResolution(result)
|
||||
const node = block.firstElementChild
|
||||
container.appendChild(node)
|
||||
this.bindChat(node, attackMessage)
|
||||
}
|
||||
|
||||
const msgEl = btn.closest('.vermine-roll-message')
|
||||
if (msgEl) await attackMessage.update({ content: msgEl.outerHTML })
|
||||
}
|
||||
|
||||
static async #onApply(ev, attackMessage) {
|
||||
ev.preventDefault()
|
||||
const btn = ev.currentTarget
|
||||
const defender = await this.#resolveDefender({ id: btn.dataset.defenderId, uuid: btn.dataset.defenderUuid })
|
||||
const wound = btn.dataset.wound
|
||||
if (!defender || !wound) return
|
||||
if (!this.canApplyWound(defender)) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
|
||||
return
|
||||
}
|
||||
const result = await this.applyWound(defender, wound)
|
||||
if (result === false) return
|
||||
btn.disabled = true
|
||||
let label = game.i18n.localize('VERMINE.wound_applied')
|
||||
if (result?.death) {
|
||||
label = game.i18n.localize('VERMINE.wound_death')
|
||||
} else if (result?.escalated) {
|
||||
const from = game.i18n.localize(this.woundLabelKey(wound))
|
||||
const to = game.i18n.localize(this.woundLabelKey(result.category))
|
||||
label = game.i18n.format('VERMINE.wound_escalated', { from, to })
|
||||
}
|
||||
btn.innerHTML = `<i class="fas fa-check"></i> ${label}`
|
||||
// Persiste l'état du bouton seulement si l'utilisateur peut modifier le message
|
||||
if (game.user.isGM || attackMessage.isOwner) {
|
||||
const msgEl = btn.closest('.vermine-roll-message')
|
||||
if (msgEl) await attackMessage.update({ content: msgEl.outerHTML })
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers internes ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Résout un acteur depuis son id d'acteur monde (acteurs liés) ou son
|
||||
* uuid (tokens non liés via fromUuid).
|
||||
* @param {{id: string|null, uuid: string|null}} ref
|
||||
* @returns {Promise<Actor|null>}
|
||||
*/
|
||||
static async #resolveDefender({ id = null, uuid = null }) {
|
||||
if (id) {
|
||||
const actor = game.actors.get(id)
|
||||
if (actor) return actor
|
||||
}
|
||||
if (uuid) {
|
||||
const doc = await fromUuid(uuid)
|
||||
if (!doc) return null
|
||||
if (doc.documentType === 'Actor') return doc
|
||||
if (doc.documentType === 'Token') return doc.actor ?? null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
static #defenseMessagesFor(exchangeId, targetId = null) {
|
||||
return game.messages.contents.filter(m => {
|
||||
const f = m.getFlag('world', FLAG_DEFENSE)
|
||||
if (!f || f.exchangeId !== exchangeId) return false
|
||||
if (targetId) return f.actorId === targetId
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
static #messageTotal(message) {
|
||||
const el = document.createElement('div')
|
||||
el.innerHTML = message.content || ''
|
||||
const totalEl = el.querySelector('#total')
|
||||
const val = parseInt(totalEl?.innerText, 10)
|
||||
return Number.isNaN(val) ? 0 : val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Un Dé de Totem Adapté peut remplacer un Dé d'Expérience (compte ×2)
|
||||
* uniquement si le personnage possède strictement plus de Dés Adaptés
|
||||
* que Humains.
|
||||
* @param {number} human human totem dice
|
||||
* @param {number} adapted adapted totem dice
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static canUseAdaptedDie(human, adapted) {
|
||||
return adapted > human
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet d'apprentissage de Capacité de Totem : valeur
|
||||
* d'Apprentissage de la capacité.
|
||||
* @param {object} abilityData system data de l'item ability
|
||||
* @returns {number}
|
||||
*/
|
||||
static totemAbilityLearningDifficulty(abilityData) {
|
||||
return parseInt(abilityData?.learn?.threshold, 10) || 5
|
||||
}
|
||||
|
||||
/**
|
||||
* Handicap d'un jet d'apprentissage de Capacité de Totem : valeur
|
||||
* d'Apprentissage de la capacité.
|
||||
* @param {object} abilityData system data de l'item ability
|
||||
* @returns {number}
|
||||
*/
|
||||
static totemAbilityLearningHandicap(abilityData) {
|
||||
return parseInt(abilityData?.learn?.hindrance, 10) || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet d'obtention d'un Élément d'Historique.
|
||||
* @returns {number}
|
||||
*/
|
||||
static backgroundLearningDifficulty() {
|
||||
return 9
|
||||
}
|
||||
|
||||
/**
|
||||
* Handicap d'un jet d'obtention d'un Élément d'Historique : (I) si
|
||||
* l'Historique est listé avec un astérisque.
|
||||
* @param {boolean} asterisk
|
||||
* @returns {number}
|
||||
*/
|
||||
static backgroundLearningHandicap(asterisk) {
|
||||
return asterisk ? 1 : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet visant à réduire le score de Réputation (devenir
|
||||
* plus célèbre) : 10 moins le score que le personnage souhaite atteindre.
|
||||
* @param {number} current réputation actuelle
|
||||
* @returns {number}
|
||||
*/
|
||||
static reputationReduceDifficulty(current) {
|
||||
return 10 - Math.max(0, (parseInt(current, 10) || 0) - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet visant à augmenter le score de Réputation (se faire
|
||||
* oublier) : égale à la Réputation actuelle.
|
||||
* @param {number} current réputation actuelle
|
||||
* @returns {number}
|
||||
*/
|
||||
static reputationIncreaseDifficulty(current) {
|
||||
return parseInt(current, 10) || 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Réinitialise les limites d'apprentissage (learned.*) de la phase
|
||||
* courante, pour un personnage ou un Groupe, sans toucher au compteur
|
||||
* de phase (avancé uniquement par le dialogue d'attribution du MJ).
|
||||
* @param {foundry.abstract.Document} actor personnage ou Groupe
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async resetPhaseLimits(actor) {
|
||||
const ex = actor.system?.experience
|
||||
if (!ex) return
|
||||
const update = {}
|
||||
if (ex.learned && typeof ex.learned === "object") {
|
||||
for (const key of Object.keys(ex.learned)) {
|
||||
if (key === "phase") continue
|
||||
update[`system.experience.learned.${key}`] = false
|
||||
}
|
||||
}
|
||||
if (Object.keys(update).length) await actor.update(update)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode de jeu actif (1=Survie, 2=Cauchemar, 3=Apocalypse).
|
||||
* @returns {number}
|
||||
*/
|
||||
static gameMode() {
|
||||
return parseInt(game.settings.get("vermine2047", "game-mode"), 10) || 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Total de Dés d'Évolution maximum selon le Mode de jeu.
|
||||
* @param {number} mode 1=Survie, 2=Cauchemar, 3=Apocalypse
|
||||
* @returns {number}
|
||||
*/
|
||||
static evolutionMaxForMode(mode) {
|
||||
switch (parseInt(mode, 10)) {
|
||||
case 2: return 6
|
||||
case 3: return 7
|
||||
default: return 5
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Somme des niveaux des Adaptations/Mutations déjà possédées.
|
||||
* @param {foundry.abstract.Document} actor personnage
|
||||
* @returns {number}
|
||||
*/
|
||||
static evolutionTotalLevels(actor) {
|
||||
return (actor?.itemTypes?.evolution || []).reduce((sum, item) => sum + (item.system?.level?.value || 0), 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide l'achat d'une Adaptation/Mutation contre les Dés d'Évolution
|
||||
* accumulés. Retourne une clé i18n d'erreur, ou null si l'achat est permis.
|
||||
* Règles p. 116-117 : coût = niveau ; total limité par le Mode ; Mutations
|
||||
* 3D/4D interdites en Mode Survie ; une seule Mutation à 4D.
|
||||
* @param {foundry.abstract.Document} actor personnage
|
||||
* @param {number} cost niveau de l'Adaptation/Mutation (1-4)
|
||||
* @param {boolean} mutation vrai si c'est une Mutation
|
||||
* @returns {string|null}
|
||||
*/
|
||||
static validateEvolutionPurchase(actor, cost, mutation) {
|
||||
const level = parseInt(cost, 10) || 0
|
||||
const accumulated = actor.system?.experience?.evolution || 0
|
||||
const mode = this.gameMode()
|
||||
if (accumulated < level) return "VERMINE.error_evolution_not_enough_dice"
|
||||
if (level < 1 || level > 4) return "VERMINE.error_evolution_invalid_level"
|
||||
const total = this.evolutionTotalLevels(actor) + level
|
||||
if (total > this.evolutionMaxForMode(mode)) return "VERMINE.error_evolution_mode_limit"
|
||||
if (mutation && level >= 3 && mode < 2) return "VERMINE.error_evolution_mutation_mode"
|
||||
if (mutation && level === 4) {
|
||||
const hasFourDMutation = (actor?.itemTypes?.evolution || []).some(
|
||||
(item) => item.system?.mutation && (item.system?.level?.value || 0) === 4
|
||||
)
|
||||
if (hasFourDMutation) return "VERMINE.error_evolution_mutation_4d"
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -96,8 +98,24 @@ export const registerHandlebarsHelpers = function () {
|
||||
});
|
||||
|
||||
// search translation with variables
|
||||
Handlebars.registerHelper('smarttl', function (arrayLabel, objectLabel, options) {
|
||||
return game.i18n.localize(arrayLabel + "." + objectLabel + ".name");
|
||||
Handlebars.registerHelper('smarttl', function (arrayLabel, objectLabel, key) {
|
||||
if (!key || typeof key !== 'string') key = 'name';
|
||||
return game.i18n.localize(arrayLabel + "." + objectLabel + "." + key);
|
||||
});
|
||||
|
||||
// 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) {
|
||||
@@ -347,6 +365,20 @@ 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);
|
||||
});
|
||||
// Chiffre → chiffre romain (handicaps, entraves). 0 → chaîne vide.
|
||||
Handlebars.registerHelper('roman', function (n) {
|
||||
const table = [[10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']]
|
||||
let num = Number(n)
|
||||
if (!Number.isFinite(num) || num <= 0) return ''
|
||||
let out = ''
|
||||
for (const [v, s] of table) {
|
||||
while (num >= v) { out += s; num -= v }
|
||||
}
|
||||
return out
|
||||
});
|
||||
|
||||
|
||||
//math operations
|
||||
@@ -388,4 +420,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 "";
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -127,6 +131,15 @@ export const registerHooks = function () {
|
||||
}
|
||||
});
|
||||
|
||||
// Un Groupe démarre avec une Réserve à son maximum (3D + 1D par membre).
|
||||
Hooks.on("createActor", async function (actor) {
|
||||
if (actor.type !== "group") return;
|
||||
const reserve = actor.system?.reserve;
|
||||
if (reserve && reserve.value === 0 && reserve.max > 0) {
|
||||
await actor.update({ "system.reserve.value": reserve.max });
|
||||
}
|
||||
});
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Combat Hooks */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { VermineTotemDice } from "./totem-dice.mjs";
|
||||
|
||||
export class VermineUtils {
|
||||
/**
|
||||
* Rolls dice with Vermine2047-specific rules.
|
||||
@@ -29,7 +31,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) {
|
||||
@@ -42,50 +51,31 @@ export class VermineUtils {
|
||||
// Declare variables
|
||||
let formula = "";
|
||||
let modFormula = null;
|
||||
let totemBonus = { human: 0, adapted: 0 };
|
||||
|
||||
// Calculate domain bonuses for totems
|
||||
if (skillCategory) {
|
||||
totemBonus = this._calculateTotemDomainBonuses(skillCategory, actor);
|
||||
}
|
||||
// Influence des Dés de Totems : Bonus/Malus de 1D selon le Totem dominant
|
||||
// et le Domaine de la Compétence (règles p. 121). S'applique même sans
|
||||
// utiliser un Dé de Totem.
|
||||
const influenceMod = this._calculateTotemInfluence(skillCategory, actor);
|
||||
NoD += influenceMod;
|
||||
|
||||
// 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
|
||||
if (totemBonus.human !== 0) {
|
||||
NoD += totemBonus.human;
|
||||
}
|
||||
|
||||
modFormula = humanFormula;
|
||||
}
|
||||
|
||||
// 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
|
||||
if (totemBonus.adapted !== 0) {
|
||||
NoD += totemBonus.adapted;
|
||||
}
|
||||
|
||||
// Build combined formula
|
||||
if (modFormula !== null) {
|
||||
modFormula = `${modFormula}+${adaptedFormula}`;
|
||||
@@ -105,8 +95,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 +104,22 @@ export class VermineUtils {
|
||||
// Build final formula
|
||||
formula = modFormula !== null ? `${baseFormula}+${modFormula}` : baseFormula;
|
||||
|
||||
// Reconcile the breakdown displayed in chat with the dice actually rolled.
|
||||
// L'influence s'applique au pool ; les Dés de Totem remplacent (n'ajoutent
|
||||
// pas) des dés du pool de base.
|
||||
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;
|
||||
}
|
||||
poolBreakdown.totemDice = totemDice;
|
||||
poolBreakdown.domainBonus = influenceMod;
|
||||
// Total = nombre réel de dés lancés (NoD réguliers + dés de totem).
|
||||
poolBreakdown.total = NoD + totemDice;
|
||||
}
|
||||
|
||||
// Create the roll
|
||||
const roll = new Roll(formula, actor.getRollData());
|
||||
|
||||
@@ -126,13 +132,12 @@ export class VermineUtils {
|
||||
skillCategory: skillCategory,
|
||||
skillLevel: skillLevel,
|
||||
hasSpecialty: hasSpecialty,
|
||||
autoSuccesses: autoSuccesses,
|
||||
totemBonuses: { ...totemBonus },
|
||||
totemInfluence: influenceMod,
|
||||
baseNoD: NoD,
|
||||
rerolls: Reroll,
|
||||
selfControl: self_control
|
||||
selfControl: self_control,
|
||||
bonusSuccesses: bonusSuccesses
|
||||
};
|
||||
|
||||
// Evaluate the roll
|
||||
await roll.evaluate();
|
||||
|
||||
@@ -152,7 +157,15 @@ export class VermineUtils {
|
||||
skillCategory,
|
||||
keepTotem,
|
||||
skillLevel,
|
||||
hasSpecialty
|
||||
hasSpecialty,
|
||||
handicap,
|
||||
bonusSuccesses,
|
||||
poolBreakdown,
|
||||
specialtyName,
|
||||
weapon,
|
||||
targets,
|
||||
attack,
|
||||
messageFlags
|
||||
});
|
||||
|
||||
return roll;
|
||||
@@ -162,113 +175,21 @@ export class VermineUtils {
|
||||
* Calculates domain bonuses/penalties for totems.
|
||||
* @param {string} skillCategory - The skill category
|
||||
* @param {Actor} actor - The actor
|
||||
* @returns {Object} Bonuses for each totem {human: number, adapted: number}
|
||||
* @returns {number} Modificateur d'influence (-1, 0 ou +1)
|
||||
*/
|
||||
static _calculateTotemDomainBonuses(skillCategory, actor) {
|
||||
const bonuses = { human: 0, adapted: 0 };
|
||||
|
||||
// Validate inputs
|
||||
if (!CONFIG.VERMINE?.totemDomains || !actor?.system?.identity?.totem) {
|
||||
return bonuses;
|
||||
}
|
||||
|
||||
const actorTotem = actor.system.identity.totem;
|
||||
|
||||
// Check if actor's totem exists in configuration
|
||||
if (!CONFIG.VERMINE.totemDomains[actorTotem]) {
|
||||
return bonuses;
|
||||
}
|
||||
|
||||
const totemConfig = CONFIG.VERMINE.totemDomains[actorTotem];
|
||||
|
||||
if (!totemConfig?.domains) {
|
||||
return bonuses;
|
||||
}
|
||||
|
||||
// Get actor's preferred skill category
|
||||
const preferredCategory = actor.system.skill_categories?.preferred;
|
||||
|
||||
// Bonus for actor's totem if preferred category is in its domains
|
||||
if (preferredCategory && totemConfig.domains.includes(preferredCategory)) {
|
||||
bonuses[actorTotem] = totemConfig.bonus || 1;
|
||||
}
|
||||
|
||||
// Penalty for opposite totem if preferred category is in its domains
|
||||
const oppositeTotem = CONFIG.VERMINE.totem_opposites?.[actorTotem];
|
||||
if (oppositeTotem && CONFIG.VERMINE.totemDomains[oppositeTotem]) {
|
||||
const oppositeConfig = CONFIG.VERMINE.totemDomains[oppositeTotem];
|
||||
if (preferredCategory && oppositeConfig?.domains?.includes(preferredCategory)) {
|
||||
bonuses[oppositeTotem] = -(oppositeConfig.bonus || 1);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
static _calculateTotemInfluence(skillCategory, actor) {
|
||||
// Les Dés de Totems n'existent que sur les personnages.
|
||||
if (actor?.type !== "character" || !skillCategory) return 0;
|
||||
return VermineTotemDice.influenceModifier(actor, skillCategory);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +198,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 +208,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 +252,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 +342,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 +365,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 +373,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 +431,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* VermineTotemDice - Gestion des Dés de Totems (Humain / Adapté).
|
||||
*
|
||||
* Règles p. 121 :
|
||||
* - Influence : si un joueur possède strictement plus de Dés d'un Totem que
|
||||
* de l'autre, ce Totem prend l'ascendant (+1D/-1D sur certains Domaines).
|
||||
* - Gains et pertes via les Instincts et les Interdits (1D ou 2D selon la
|
||||
* gravité), avec des limites (3 Dés du même Totem, 5 Dés au maximum).
|
||||
*/
|
||||
export class VermineTotemDice {
|
||||
|
||||
/**
|
||||
* Totem dominant d'un personnage.
|
||||
* @param {foundry.abstract.Document} actor
|
||||
* @returns {'human'|'adapted'|'neutral'}
|
||||
*/
|
||||
static getInfluence(actor) {
|
||||
const human = actor.system?.adaptation?.totems?.human?.value || 0;
|
||||
const adapted = actor.system?.adaptation?.totems?.adapted?.value || 0;
|
||||
if (human > adapted) return "human";
|
||||
if (adapted > human) return "adapted";
|
||||
return "neutral";
|
||||
}
|
||||
|
||||
/**
|
||||
* Modificateur (Bonus/Malus) d'influence pour une catégorie de compétence.
|
||||
* @param {foundry.abstract.Document} actor
|
||||
* @param {string} skillCategory catégorie (man, animal, tool, weapon, survival, world)
|
||||
* @returns {number} -1, 0 ou +1
|
||||
*/
|
||||
static influenceModifier(actor, skillCategory) {
|
||||
const influence = this.getInfluence(actor);
|
||||
if (influence === "neutral" || !skillCategory) return 0;
|
||||
const cfg = CONFIG.VERMINE?.totemInfluence?.[influence];
|
||||
if (!cfg) return 0;
|
||||
if ((cfg.bonus || []).includes(skillCategory)) return 1;
|
||||
if ((cfg.malus || []).includes(skillCategory)) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique un gain d'un Dé de Totem selon les règles d'échange et de limites.
|
||||
* Retourne un objet décrivant ce qui s'est passé (pour notification).
|
||||
* @param {foundry.abstract.Document} actor
|
||||
* @param {'human'|'adapted'} totem
|
||||
* @param {number} [count=1] 1 ou 2 dés (acte grave)
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
static async gainDie(actor, totem, count = 1) {
|
||||
const human = actor.system.adaptation.totems.human;
|
||||
const adapted = actor.system.adaptation.totems.adapted;
|
||||
const actions = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const step = this._gainOneDie(human.value, adapted.value, totem);
|
||||
if (step.unchanged) {
|
||||
actions.push({ ok: false, reason: "max" });
|
||||
break;
|
||||
}
|
||||
human.value = step.human;
|
||||
adapted.value = step.adapted;
|
||||
actions.push({ ok: true, exchanged: step.exchanged, replaced: step.replaced });
|
||||
}
|
||||
|
||||
await actor.update({
|
||||
"system.adaptation.totems.human.value": human.value,
|
||||
"system.adaptation.totems.adapted.value": adapted.value
|
||||
});
|
||||
|
||||
return this._summarize(actions, totem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique une perte d'un Dé de Totem.
|
||||
* Règle : seul l'Humain a des Interdits. Briser un Interdit Humain fait
|
||||
* perdre 1D Humain ; si le personnage n'en possède pas, il gagne 1D Adapté.
|
||||
* @param {foundry.abstract.Document} actor
|
||||
* @param {'human'} totem
|
||||
* @param {number} [count=1] 1 ou 2 dés (acte grave)
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
static async loseDie(actor, totem, count = 1) {
|
||||
const human = actor.system.adaptation.totems.human;
|
||||
const adapted = actor.system.adaptation.totems.adapted;
|
||||
const actions = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (human.value > 0) {
|
||||
human.value -= 1;
|
||||
actions.push({ ok: true, lost: true });
|
||||
} else if (adapted.value < adapted.max && (human.value + adapted.value) < 5) {
|
||||
// Pas de Dé Humain : gagne un Dé Adapté à la place (limites appliquées).
|
||||
adapted.value += 1;
|
||||
actions.push({ ok: true, gainedAdapted: true });
|
||||
} else {
|
||||
actions.push({ ok: false, reason: "max" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await actor.update({
|
||||
"system.adaptation.totems.human.value": human.value,
|
||||
"system.adaptation.totems.adapted.value": adapted.value
|
||||
});
|
||||
|
||||
return this._summarize(actions, totem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute un Dé du Totem donné en respectant les limites et les échanges.
|
||||
* Règles :
|
||||
* - 3 Dés max du même Totem, 5 Dés max au total.
|
||||
* - Si le joueur gagne un Dé qu'il ne peut conserver, il peut supprimer un
|
||||
* Dé du Totem opposé à la place ; sinon le gain est annulé.
|
||||
* - Gagner un 4e Dé Adapté est impossible, mais supprime un Dé Humain.
|
||||
* @param {number} human
|
||||
* @param {number} adapted
|
||||
* @param {'human'|'adapted'} totem
|
||||
* @returns {Object}
|
||||
*/
|
||||
static _gainOneDie(human, adapted, totem) {
|
||||
const limitTotem = 3;
|
||||
const opposite = totem === "human" ? "adapted" : "human";
|
||||
const total = human + adapted;
|
||||
const current = totem === "human" ? human : adapted;
|
||||
const oppositeVal = totem === "human" ? adapted : human;
|
||||
|
||||
// Le Totem cible est déjà au maximum (3) : impossible d'empiler.
|
||||
if (current >= limitTotem) {
|
||||
if (oppositeVal > 0) {
|
||||
// Supprime un Dé du Totem opposé à la place (le gain n'augmente pas).
|
||||
const result = { human, adapted, unchanged: false, replaced: true };
|
||||
result[opposite] -= 1;
|
||||
return result;
|
||||
}
|
||||
return { human, adapted, unchanged: true };
|
||||
}
|
||||
|
||||
// Limite totale atteinte (5) : peut remplacer un Dé opposé.
|
||||
if (total >= 5) {
|
||||
if (oppositeVal > 0) {
|
||||
const result = { human, adapted, unchanged: false, exchanged: true };
|
||||
result[totem] += 1;
|
||||
result[opposite] -= 1;
|
||||
return result;
|
||||
}
|
||||
return { human, adapted, unchanged: true };
|
||||
}
|
||||
|
||||
// Gain normal.
|
||||
const result = { human, adapted, unchanged: false };
|
||||
result[totem] += 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Résume les actions appliquées en un libellé localisé.
|
||||
* @param {Array} actions
|
||||
* @param {'human'|'adapted'} totem
|
||||
* @returns {Object}
|
||||
*/
|
||||
static _summarize(actions, totem) {
|
||||
const gained = actions.filter(a => a.ok && !a.exchanged && !a.replaced && !a.lost && !a.gainedAdapted).length;
|
||||
const exchanged = actions.filter(a => a.exchanged).length;
|
||||
const replaced = actions.filter(a => a.replaced).length;
|
||||
const lost = actions.filter(a => a.lost).length;
|
||||
const gainedAdapted = actions.filter(a => a.gainedAdapted).length;
|
||||
const refused = actions.some(a => !a.ok);
|
||||
return { totem, gained, exchanged, replaced, lost, gainedAdapted, refused };
|
||||
}
|
||||
}
|
||||
@@ -37,12 +37,23 @@ 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.creaturecapacity = models.VermineCreatureCapacityData;
|
||||
CONFIG.Item.dataModels.specialty = models.VermineSpecialtyData;
|
||||
CONFIG.Item.dataModels.background = models.VermineBackgroundData;
|
||||
CONFIG.Item.dataModels.trauma = models.VermineTraumaData;
|
||||
@@ -71,8 +82,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 +100,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,12 +117,12 @@ 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
|
||||
})
|
||||
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineCreatureCapacitySheetV2, {
|
||||
types: ["creaturecapacity"], makeDefault: true
|
||||
})
|
||||
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineSpecialtySheetV2, {
|
||||
types: ["specialty"], makeDefault: true
|
||||
})
|
||||
@@ -178,9 +195,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 +208,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"
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 241 KiB |
|
Before Width: | Height: | Size: 329 KiB |
@@ -0,0 +1,37 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Acuité auditive
|
||||
type: evolution
|
||||
_id: vPA7gekUx8kUNOWB
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>Le personnage est doté d’une ouïe extraordinaire. Il perçoit des ondes
|
||||
sonores très aigües, ce qui lui permet d’entendre les ultrasons. Son acuité
|
||||
est aussi surdéveloppée. Il peut ainsi entendre un son à une distance cinq
|
||||
fois supérieure aux autres personnages. Enfin, sa sensibilité lui permet de
|
||||
distinguer les sons entre eux, et ainsi d’entendre distinctement un son qui
|
||||
serait couvert par le vent, par exemple. Son ouïe extraordinaire lui offre
|
||||
un Bonus de 3D à tous les jets de Perception basés sur l’audition.</p>
|
||||
level:
|
||||
value: 3
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198965
|
||||
modifiedTime: 1785775198965
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!vPA7gekUx8kUNOWB'
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Adrénaline
|
||||
type: evolution
|
||||
_id: DItkLLGSyjAGzruO
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>Le corps du personnage s’est adapté au stress du combat. Quand le
|
||||
personnage subit des Blessures lors d’un combat, il ne subit pas la perte de
|
||||
Dés de Réserve associée. En revanche, à la fin du combat, il perd 2D
|
||||
d’Effort et 1D de Sang-Froid pour chaque Blessure subie.</p>
|
||||
level:
|
||||
value: 2
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198966
|
||||
modifiedTime: 1785775198966
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!DItkLLGSyjAGzruO'
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Analgésie
|
||||
type: evolution
|
||||
_id: Cs6ibCrcPxim7QHR
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>Pour le personnage, la douleur n’est qu’une information dont il ne subit
|
||||
pas les conséquences néfastes. Lorsqu’il subit une Blessure, il ne perd
|
||||
aucun Dé de Sang- Froid. En outre, le Malus qu’il subit à la suite de
|
||||
Blessures est diminué de 1D.</p>
|
||||
level:
|
||||
value: 3
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198966
|
||||
modifiedTime: 1785775198966
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!Cs6ibCrcPxim7QHR'
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Anticipation
|
||||
type: evolution
|
||||
_id: pP9hfvk5JKsoFcnt
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>En décryptant le langage corporel – ou peut-être en percevant les
|
||||
phéromones – de son adversaire, le personnage ressent ses intentions. Il
|
||||
peut utiliser la caractéristique Empathie plutôt que Réflexes pour esquiver
|
||||
les attaques d’un adversaire humain. S’il réussit une telle esquive, il
|
||||
regagne 1D dans sa Réserve de Sang-Froid.</p>
|
||||
level:
|
||||
value: 1
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198967
|
||||
modifiedTime: 1785775198967
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!pP9hfvk5JKsoFcnt'
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Au bout de l’effort
|
||||
type: evolution
|
||||
_id: h3Q6LwnnVuj6MH4Y
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>Le personnage peut dépasser ses limites. Il peut dépenser, sur un jet, un
|
||||
nombre de Dés de Réserve égal à la Caractéristique utilisée +1 (contre un
|
||||
seul normalement).</p>
|
||||
level:
|
||||
value: 1
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198967
|
||||
modifiedTime: 1785775198967
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!h3Q6LwnnVuj6MH4Y'
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Aura d’optimisme
|
||||
type: evolution
|
||||
_id: JrtHAUqFgA75e6pq
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>Le personnage dégage une imperceptible aura bienfaisante autour de lui.
|
||||
La présence réconfortante du personnage réduit de 2D les pertes de Dés de la
|
||||
Réserve de Groupe à la suite d’un événement traumatisant. Le personnage doit
|
||||
être témoin de l’événement pour que sa Capacité fonctionne.</p>
|
||||
level:
|
||||
value: 2
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198967
|
||||
modifiedTime: 1785775198967
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!JrtHAUqFgA75e6pq'
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Camouflage
|
||||
type: evolution
|
||||
_id: t4p4n73kwwVoeSMe
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>Le personnage ne dégage aucune odeur et émet des phéromones qui le
|
||||
rendent insignifiant aux yeux des créatures qui l’entourent. Il peut ajouter
|
||||
son score en Volonté aux jets destinés à camoufler sa présence, son odeur,
|
||||
ses traces ou un objet lui appartenant. S’il camoufle sa présence et qu’il
|
||||
reste immobile, il bénéficie en outre de trois Réussites supplémentaires.
|
||||
Dès qu’il se déplace, il doit retenter le jet sans pouvoir bénéficier des
|
||||
Réussites supplémentaires.</p>
|
||||
level:
|
||||
value: 3
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198967
|
||||
modifiedTime: 1785775198967
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!t4p4n73kwwVoeSMe'
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Cicatrisation rapide
|
||||
type: evolution
|
||||
_id: 2h0xUozy1xqLdKcE
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>La Difficulté des Actions de soin sur le personnage est réduite de 2. Le
|
||||
coût en Dés de Réserves pour guérir est diminué de 1D pour chaque type de
|
||||
Blessure.</p>
|
||||
level:
|
||||
value: 2
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198968
|
||||
modifiedTime: 1785775198968
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!2h0xUozy1xqLdKcE'
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Défenses immunitaires
|
||||
type: evolution
|
||||
_id: oNfjRGGRVFGtfbK0
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>La Virulence d’une infection affectant le personnage est réduite de 1 –
|
||||
voir Les Règles, « Maladies et infections », page 105. Lorsque le personnage
|
||||
reçoit des soins pour soigner une infection, le soigneur reçoit une Réussite
|
||||
supplémentaire en cas de jet réussi.</p>
|
||||
level:
|
||||
value: 2
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198968
|
||||
modifiedTime: 1785775198968
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!oNfjRGGRVFGtfbK0'
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Dur au mal
|
||||
type: evolution
|
||||
_id: vgYLoiEqEOxrGdy6
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: <p>Le Seuil de Blessure Légère du personnage augmente de 1.</p>
|
||||
level:
|
||||
value: 1
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198968
|
||||
modifiedTime: 1785775198968
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!vgYLoiEqEOxrGdy6'
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Endurance
|
||||
type: evolution
|
||||
_id: uGG0FrBn1QtMcxbz
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>Le personnage a toujours été particulièrement résistant. Toutes les
|
||||
pertes de Réserve d’Effort qu’il subit sont réduites de 1D, jusqu’à un
|
||||
minimum de 1D.</p>
|
||||
level:
|
||||
value: 1
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198968
|
||||
modifiedTime: 1785775198968
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!uGG0FrBn1QtMcxbz'
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Exosquelette
|
||||
type: evolution
|
||||
_id: bV6V4i1v4ORWDJSQ
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>Le personnage a développé un exosquelette fait de chitine. De nombreux
|
||||
endroits de son corps et de son visage sont recouverts de plaques rigides
|
||||
qui lui procurent une Protection de 3/5 (L) sans lui infliger de Handicap de
|
||||
Mobilité – voir Les Règles, « Handicap de Mobilité », page 70. Il bénéficie
|
||||
en outre d’un Bonus de 2D aux Actions dont le but est d’intimider ou
|
||||
d’effrayer. En revanche, il subit un Malus identique pour toutes les autres
|
||||
Actions sociales.</p>
|
||||
level:
|
||||
value: 4
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198968
|
||||
modifiedTime: 1785775198968
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!bV6V4i1v4ORWDJSQ'
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Force mentale
|
||||
type: evolution
|
||||
_id: S8AwTKDxIdez7vHB
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>Le personnage a des ressources mentales insoupçonnées. Il ne subit pas de
|
||||
Malus d’Épuisement aux Actions mentales et sociales quand sa Réserve de
|
||||
Sang-Froid est vide. Si ses deux Réserves sont vides, il subit néanmoins le
|
||||
Malus normal de 2D à toutes ses Actions.</p>
|
||||
level:
|
||||
value: 1
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198969
|
||||
modifiedTime: 1785775198969
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!S8AwTKDxIdez7vHB'
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Frappe lourde
|
||||
type: evolution
|
||||
_id: RjdZB4Qd2V8mp44x
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>Lorsque le personnage attaque à mains nues, ses dommages sont égaux à
|
||||
Vigueur (C).</p>
|
||||
level:
|
||||
value: 1
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198969
|
||||
modifiedTime: 1785775198969
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!RjdZB4Qd2V8mp44x'
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Griffes
|
||||
type: evolution
|
||||
_id: kPoioCvlK7H0uJB1
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>Les ongles du personnage peuvent pousser en un instant pour devenir des
|
||||
armes dangereuses. Ces griffes infligent des Dommages de 2 (L) et
|
||||
bénéficient du Trait Rapide (2). Quand le personnage réussit une attaque et
|
||||
passe la protection de sa cible, il peut dépenser jusqu’à 3D de sa Réserve
|
||||
d’Effort pour infliger 1 point de Dommages supplémentaire par dé dépensé.
|
||||
Ces griffes se brisent à la fin du combat et ne peuvent repousser que
|
||||
lorsque le personnage aura intégralement reconstitué sa Réserve
|
||||
d’Effort.</p>
|
||||
level:
|
||||
value: 3
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198969
|
||||
modifiedTime: 1785775198969
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!kPoioCvlK7H0uJB1'
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Habitué à la faim
|
||||
type: evolution
|
||||
_id: EqeONvUCK7OhJaNN
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>Le premier jour de rationnement ou de privation totale de nourriture n’a
|
||||
pas d’effet sur le personnage. Ainsi, il ne subira aucune perte de Dés de
|
||||
Réserves avant le deuxième jour de privation totale ou le troisième jour de
|
||||
rationnement.</p>
|
||||
level:
|
||||
value: 2
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198969
|
||||
modifiedTime: 1785775198969
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!EqeONvUCK7OhJaNN'
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Hyperosmie
|
||||
type: evolution
|
||||
_id: 8ShwJQCWKHsfXLko
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>Le personnage est doté d’un odorat extrêmement développé, proche de celui
|
||||
d’un chien. Cette acuité lui permet de distinguer des odeurs imperceptibles
|
||||
pour ses semblables. Il peut ainsi reconnaître une personne ou un animal à
|
||||
son odeur et savoir si cet individu est passé dans un endroit jusqu’à une
|
||||
semaine après son passage. Cela lui offre un talent incroyable pour suivre
|
||||
une piste et traquer une proie : il bénéficie d’un Bonus de 3D aux jets de
|
||||
Perception quand l’odorat entre en jeu. Cette Mutation s’accompagne d’une
|
||||
mémoire olfactive très développée. Une fois qu’il a senti une odeur, le
|
||||
personnage ne l’oublie jamais et peut la distinguer et la reconnaître sans
|
||||
faillir.</p>
|
||||
level:
|
||||
value: 3
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198969
|
||||
modifiedTime: 1785775198969
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!8ShwJQCWKHsfXLko'
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Immunité absolue
|
||||
type: evolution
|
||||
_id: ivx8gS0XBEcoqtdl
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>Le personnage est immunisé à toutes les Infections et toutes les Toxines.
|
||||
Il ne peut par ailleurs pas développer d’addiction. En contrepartie, les
|
||||
médicaments et autres stimulants sont totalement inefficaces sur lui et
|
||||
n’octroient aucun bénéfice que ce soit.</p>
|
||||
level:
|
||||
value: 4
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198970
|
||||
modifiedTime: 1785775198970
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!ivx8gS0XBEcoqtdl'
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Indestructible
|
||||
type: evolution
|
||||
_id: HfC2oOARrpKnpoQf
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>Le corps du personnage a gagné en densité, mais son poids augmente de 25
|
||||
% et ses besoins nutritionnels explosent. Il gagne un cercle dans chaque
|
||||
type de Blessure, subit (I) Handicap permanent de Mobilité, et a besoin de
|
||||
deux rations de nourriture plutôt qu’une. En cas de rationnement de
|
||||
nourriture, il subit des pertes de Réserves équivalentes à une absence
|
||||
totale de nourriture.</p>
|
||||
level:
|
||||
value: 4
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198970
|
||||
modifiedTime: 1785775198970
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!HfC2oOARrpKnpoQf'
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Insensible
|
||||
type: evolution
|
||||
_id: Re0aeZzupnz3EL6E
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>Le corps du personnage sécrète de grandes quantités d’hormones en cas de
|
||||
stress intense, ce qui lui permet d’y résister plus facilement. L’Intensité
|
||||
des événements stressants est diminuée d’un niveau pour le personnage. Il
|
||||
n’a même pas à tenter de jet pour résister à un événement Mineur – voir Les
|
||||
Règles, « Événements », page 92.</p>
|
||||
level:
|
||||
value: 2
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198970
|
||||
modifiedTime: 1785775198970
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!Re0aeZzupnz3EL6E'
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
folder: WTyHT2h0fsSnUHFT
|
||||
name: Instinct
|
||||
type: evolution
|
||||
_id: UxNguZd8K62nt1GH
|
||||
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||
system:
|
||||
description: >-
|
||||
<p>Un jet d’Empathie + Vigilance contre une Difficulté de 9 permet au
|
||||
personnage de pressentir un danger ou une menace imminente d’origine
|
||||
naturelle ou animale. Chaque Réussite obtenue lui permet d’obtenir une
|
||||
information sur le danger pressenti. Si celui-ci tourne au combat, le
|
||||
personnage sera considéré comme Actif pour son jet de Réaction. S’il a le
|
||||
temps d’informer ses compagnons du danger, ces derniers seront Actifs
|
||||
également.</p>
|
||||
|
||||
<p>En Modes <strong>Cauchemar </strong>et <strong>Apocalypse</strong>,
|
||||
chaque Réussite au jet de Vigilance octroie au personnage 1D de Bonus au jet
|
||||
de Réaction ou au jet permettant d’éviter le danger perçu.</p>
|
||||
level:
|
||||
value: 2
|
||||
min: 1
|
||||
max: 4
|
||||
effects: []
|
||||
sort: 0
|
||||
ownership:
|
||||
default: 0
|
||||
GXSW26kNdNoaabl8: 3
|
||||
kb05teJOwuTQVHwZ: 3
|
||||
flags: {}
|
||||
_stats:
|
||||
compendiumSource: null
|
||||
duplicateSource: null
|
||||
coreVersion: '14.365'
|
||||
systemId: vermine2047
|
||||
systemVersion: 0.1.13
|
||||
createdTime: 1785775198970
|
||||
modifiedTime: 1785775198970
|
||||
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||
exportSource: null
|
||||
_key: '!items!UxNguZd8K62nt1GH'
|
||||
|
||||