Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea3322e843 | ||
|
|
6247f65590 | ||
|
|
8d8428cad3 | ||
|
|
9cf7d9ac68 | ||
|
|
f2c8cfe246 | ||
|
|
31f2f4530c | ||
|
|
9c7ebc6df9 | ||
|
|
05f668acf0 | ||
|
|
10b01614e6 | ||
|
|
89d9d71395 | ||
|
|
1e298ed809 | ||
|
|
4ffaebf52b | ||
|
|
de7691c4dc | ||
|
|
c87b25fbaf | ||
|
|
6ce51ed8cf | ||
|
|
df852f17d1 | ||
|
|
8916600bf6 | ||
|
|
0189f62734 | ||
|
|
29b6a98c89 | ||
|
|
b1d73e6b08 | ||
|
|
509a5efd9d | ||
|
|
fea7c1c271 | ||
|
|
412573fc4d | ||
|
|
47e2ebe45b | ||
|
|
90de66d668 |
+4
-4
@@ -144,7 +144,7 @@ module.exports = {
|
|||||||
'no-lone-blocks': 'warn',
|
'no-lone-blocks': 'warn',
|
||||||
'no-lonely-if': 'warn',
|
'no-lonely-if': 'warn',
|
||||||
'no-loop-func': 'warn',
|
'no-loop-func': 'warn',
|
||||||
'no-magic-numbers': ['warn', { ignore: [0, 1, 2], ignoreEnums: true, ignoreNumericLiteralTypes: true, ignoreArrayIndexes: true }],
|
'@typescript-eslint/no-magic-numbers': 'off',
|
||||||
'no-multi-assign': 'warn',
|
'no-multi-assign': 'warn',
|
||||||
'no-multi-str': 'warn',
|
'no-multi-str': 'warn',
|
||||||
'no-negated-condition': 'warn',
|
'no-negated-condition': 'warn',
|
||||||
@@ -208,7 +208,7 @@ module.exports = {
|
|||||||
'one-var': ['warn', 'never'],
|
'one-var': ['warn', 'never'],
|
||||||
'one-var-declaration-per-line': ['warn', 'initializations'],
|
'one-var-declaration-per-line': ['warn', 'initializations'],
|
||||||
'operator-assignment': ['warn', 'always'],
|
'operator-assignment': ['warn', 'always'],
|
||||||
'prefer-arrow-callback': ['warn', { classPropertiesAllowed: true, disallowTLSClassFields: true }],
|
'prefer-arrow-callback': 'warn',
|
||||||
'prefer-const': ['error', { destructuring: 'all', ignoreReadBeforeAssign: false }],
|
'prefer-const': ['error', { destructuring: 'all', ignoreReadBeforeAssign: false }],
|
||||||
'prefer-destructuring': ['warn', { array: false, object: true }],
|
'prefer-destructuring': ['warn', { array: false, object: true }],
|
||||||
'prefer-exponentiation-operator': 'warn',
|
'prefer-exponentiation-operator': 'warn',
|
||||||
@@ -221,7 +221,7 @@ module.exports = {
|
|||||||
'prefer-rest-params': 'warn',
|
'prefer-rest-params': 'warn',
|
||||||
'prefer-spread': 'warn',
|
'prefer-spread': 'warn',
|
||||||
'prefer-template': 'warn',
|
'prefer-template': 'warn',
|
||||||
'quote-props': ['warn', 'as-needed', { keywords: true, unnecessaryQuote: false, numbers: true }],
|
'quote-props': ['warn', 'as-needed'],
|
||||||
'radix': ['error', 'always'],
|
'radix': ['error', 'always'],
|
||||||
'require-await': 'warn',
|
'require-await': 'warn',
|
||||||
'require-unicode-regexp': 'off',
|
'require-unicode-regexp': 'off',
|
||||||
@@ -313,7 +313,7 @@ module.exports = {
|
|||||||
'padded-blocks': ['warn', 'never'],
|
'padded-blocks': ['warn', 'never'],
|
||||||
'padding-line-between-statements': 'off',
|
'padding-line-between-statements': 'off',
|
||||||
'prefer-exponentiation-operator': 'warn',
|
'prefer-exponentiation-operator': 'warn',
|
||||||
'quote-props': ['warn', 'as-needed', { keywords: true, unnecessaryQuote: false, numbers: true }],
|
'quote-props': ['warn', 'as-needed'],
|
||||||
'quotes': ['warn', 'single', { avoidEscape: true, allowTemplateLiterals: true }],
|
'quotes': ['warn', 'single', { avoidEscape: true, allowTemplateLiterals: true }],
|
||||||
'rest-spread-spacing': ['warn', 'never'],
|
'rest-spread-spacing': ['warn', 'never'],
|
||||||
'semi': ['warn', 'always'],
|
'semi': ['warn', 'always'],
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ jobs:
|
|||||||
- name: Build CSS
|
- name: Build CSS
|
||||||
run: npm install && npm run build
|
run: npm install && npm run build
|
||||||
|
|
||||||
|
# Recompile les compendiums LevelDB depuis les sources YAML (src/packs/ → packs/)
|
||||||
|
- name: Compile Compendiums
|
||||||
|
run: npm run pullYAMLtoLDB
|
||||||
|
|
||||||
# Crée le zip de release avec tous les fichiers nécessaires au système
|
# Crée le zip de release avec tous les fichiers nécessaires au système
|
||||||
- run: |
|
- run: |
|
||||||
apt update -y
|
apt update -y
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
## Build & Development
|
## Build & Development
|
||||||
|
|
||||||
|
Requires Node.js v16.15.1 (see `.nvmrc`).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run watch # Watch SCSS + templates, proxy Foundry at localhost:30000 (requires Foundry running)
|
npm run watch # Watch SCSS + templates, proxy Foundry at localhost:30000 (requires Foundry running)
|
||||||
npm run buildStyle # Compile SCSS once to css/vermine2047.css
|
npm run buildStyle # Compile SCSS once to css/vermine2047.css
|
||||||
@@ -10,7 +12,7 @@ npm run pushLDBtoYAML # Extract LevelDB packs to editable YAML in src/packs/
|
|||||||
npx eslint module/ # Lint JavaScript (ESLint configured in .eslintrc.js)
|
npx eslint module/ # Lint JavaScript (ESLint configured in .eslintrc.js)
|
||||||
```
|
```
|
||||||
|
|
||||||
There are no automated tests. Use `npm run watch` during development to live-reload SCSS and Handlebars templates via browser-sync.
|
There are no automated tests. Use `npm run watch` during development — browser-sync proxies Foundry over HTTPS and live-reloads SCSS, `.hbs`, and `.html` templates on change. `system.json` declares `hotReload` for `.css`, `.scss`, `.hbs`, and `.json` extensions, so Foundry auto-refreshes those assets client-side when the dev server is running.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -27,6 +29,7 @@ This is a **FoundryVTT game system** (`system.json` → system ID `vermine2047`)
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `config.mjs` | All game constants (`CONFIG.VERMINE`) — abilities, skills, totems, threat/role/pattern levels, traits, damage types |
|
| `config.mjs` | All game constants (`CONFIG.VERMINE`) — abilities, skills, totems, threat/role/pattern levels, traits, damage types |
|
||||||
| `roll.mjs` | `VermineUtils` class — d10 dice pool system with success counting, totem mechanics, rerolls, Dice So Nice integration |
|
| `roll.mjs` | `VermineUtils` class — d10 dice pool system with success counting, totem mechanics, rerolls, Dice So Nice integration |
|
||||||
|
| `dice3d.mjs` | Dice So Nice 3D dice configuration (appearance, colors, themes for totem/standard dice) |
|
||||||
| `fight.mjs` | Confrontation system (`VermineFight`), plus `VermineCombat`, `VermineCombatant`, `VermineCombatTracker` |
|
| `fight.mjs` | Confrontation system (`VermineFight`), plus `VermineCombat`, `VermineCombatant`, `VermineCombatTracker` |
|
||||||
| `group-link.mjs` | `GroupLink` — bidirectional sync of members/encounters between group actors and character/NPC actors via Foundry hooks |
|
| `group-link.mjs` | `GroupLink` — bidirectional sync of members/encounters between group actors and character/NPC actors via Foundry hooks |
|
||||||
| `hooks.mjs` | All Foundry hook registrations (chat messages, hotbar drop, combat, preCreate, Dice So Nice) |
|
| `hooks.mjs` | All Foundry hook registrations (chat messages, hotbar drop, combat, preCreate, Dice So Nice) |
|
||||||
@@ -34,21 +37,27 @@ This is a **FoundryVTT game system** (`system.json` → system ID `vermine2047`)
|
|||||||
| `handlebars-manager.mjs` | Template preloading paths + all Handlebars helpers (level config lookups, math, conditionals) |
|
| `handlebars-manager.mjs` | Template preloading paths + all Handlebars helpers (level config lookups, math, conditionals) |
|
||||||
| `effects.mjs` | Active effect management |
|
| `effects.mjs` | Active effect management |
|
||||||
| `dialogs/rollDialog.mjs` | Advanced roll dialog (ability/skill selection, difficulty, totems, specialties, assist/pool bonuses) |
|
| `dialogs/rollDialog.mjs` | Advanced roll dialog (ability/skill selection, difficulty, totems, specialties, assist/pool bonuses) |
|
||||||
| `applications.mjs` | `TotemPicker` and `TraitSelector` applications |
|
| `applications.mjs` | `TotemPicker` and `TraitSelector` applications (still AppV1) |
|
||||||
|
| `applications/sheets/` | ApplicationV2 sheet classes: `base-actor-sheet.mjs`, `base-item-sheet.mjs` + 4 actor + 12 item sheets |
|
||||||
|
|
||||||
**Data model**: `template.json` defines the Actor/Item schemas (Foundry's template system). Derived data is computed in `prepareDerivedData()` in `module/documents/actor.mjs` and `module/documents/item.mjs`.
|
**Data model**: Defined via Foundry DataModels (`module/models/`) extending `foundry.abstract.TypeDataModel`. Type-specific derived data is computed in each DataModel's `prepareDerivedData()`. The legacy `template.json` has been removed in favor of `system.json` → `documentTypes`.
|
||||||
|
|
||||||
**Sheet inheritance**: `VermineActorSheet` (base) → `VermineCharacterSheet`, `VermineNpcSheet`, `VermineGroupSheet`, `VermineCreatureSheet`. Sheets expose `CONFIG.VERMINE` as `context.config` in template data.
|
**Sheet inheritance**: ApplicationV2 (`HandlebarsApplicationMixin(ActorSheetV2)`/`ItemSheetV2`) in `module/applications/sheets/`. `VermineBaseActorSheet` → `VermineCharacterSheetV2`, etc. Sheets expose `CONFIG.VERMINE` as `context.config` in template data. Each sheet is split into parts via `static PARTS`; tab navigation uses Core's `templates/generic/tab-navigation.hbs`.
|
||||||
|
|
||||||
|
**Data preparation**: `VermineActor.prepareDerivedData()` delegates to the type-specific DataModel. Each DataModel implements its own `prepareDerivedData()` with type-specific computation logic.
|
||||||
|
|
||||||
|
**Global access**: On `init`, `game.vermine2047` is populated with `{ VermineActor, VermineItem, VermineUtils, VermineCombat, GroupLink }` for easy access in macros and scripts.
|
||||||
|
|
||||||
**Compendium packs**: Stored in `packs/` as LevelDB databases. Edit source data in `src/packs/` as YAML, then `npm run pullYAMLtoLDB` to build. The CI release workflow runs this automatically.
|
**Compendium packs**: Stored in `packs/` as LevelDB databases. Edit source data in `src/packs/` as YAML, then `npm run pullYAMLtoLDB` to build. The CI release workflow runs this automatically.
|
||||||
|
|
||||||
## Key Conventions
|
## Key Conventions
|
||||||
|
|
||||||
- **All `.mjs` files**: ES module syntax only. Foundry globals (`game`, `Hooks`, `CONFIG`, `Actor`, `Item`, `ChatMessage`, `Roll`, `Handlebars`, `renderTemplate`, `foundry`) are available but declared as readonly globals in `.eslintrc.js`.
|
- **All `.mjs` files**: ES module syntax only. Foundry globals (`game`, `Hooks`, `CONFIG`, `Actor`, `Item`, `ChatMessage`, `Roll`, `Handlebars`, `renderTemplate`, `foundry`, `ui`, `Canvas`, `Dialog`) are available but declared as readonly globals in `.eslintrc.js`.
|
||||||
|
- **ESLint conventions**: 2-space indent, single quotes, semicolons required, `===` equality (except `== null`). Constants named with `VERMINE_` prefix are exempt from camelCase. Max 1 class per file, 500 lines per file, 100 lines per function, 5 levels of nesting max. No `console.log` calls — use `ui.notifications`.
|
||||||
- **Code language**: Source code and comments are in French. UI strings in `lang/fr.json` and `lang/en.json`. Use `game.i18n.localize()` for all user-visible text.
|
- **Code language**: Source code and comments are in French. UI strings in `lang/fr.json` and `lang/en.json`. Use `game.i18n.localize()` for all user-visible text.
|
||||||
- **Template naming**: Actor sheets at `templates/actor/actor-{type}-sheet.hbs`, item sheets at `templates/item/item-{type}-sheet.html` (note: item sheets use `.html`, everything else uses `.hbs`). Chat card templates at `templates/item/chatCards/{type}.hbs`.
|
- **Template naming**: Actor sheets use ApplicationV2 part templates in `templates/actor/appv2/{type}-{part}.hbs` (e.g. `character-main.hbs`, `npc-characteristics.hbs`). Item sheets at `templates/item/item-{type}-sheet.html`. Chat card templates at `templates/item/chatCards/{type}.hbs`. Legacy templates in `templates/actor/` subdirectories are preserved for dialog/chat use.
|
||||||
- **Dice system**: d10 pools with success counting (result ≥ difficulty). Totem dice (human/adapted) count double on success. Formula syntax: `{N}d10cs>={threshold}[label]`. Totem dice: `(1d10cs>={threshold}[totem_label]*2)`.
|
- **Dice system**: d10 pools with success counting (result ≥ difficulty). Totem dice (human/adapted) count double on success. Formula syntax: `{N}d10cs>={threshold}[label]`. Totem dice: `(1d10cs>={threshold}[totem_label]*2)`.
|
||||||
- **CSS**: SCSS sources in `scss/` compile to `css/vermine2047.css`. Organized by concern: `_app.scss`, `item-sheet.scss`, `roll.scss`, `dialog.scss`, etc.
|
- **CSS**: SCSS sources in `scss/` compile to `css/vermine2047.css`. Entry point: `vermine2047.scss` which imports partials: `_app.scss`, `_flex.scss`, `dialog.scss`, `item-sheet.scss`, `itemCards.scss`, `roll.scss`, `special-applications.scss`, `special-inputs.scss`, `style.scss`, `base_work.scss`. Add new SCSS partials as `_component.scss`, import in `vermine2047.scss`.
|
||||||
- **Data tools**: The `pushLDBtoYAML` / `pullYAMLtoLDB` scripts use `@foundryvtt/foundryvtt-cli` to convert between LevelDB packs and editable YAML. When adding pack content, edit YAML in `src/packs/` then rebuild.
|
- **Data tools**: The `pushLDBtoYAML` / `pullYAMLtoLDB` scripts use `@foundryvtt/foundryvtt-cli` to convert between LevelDB packs and editable YAML. When adding pack content, edit YAML in `src/packs/` then rebuild.
|
||||||
- **Actor updates**: Use `actor.update({...})` with dot-notation paths (e.g., `'system.adaptation.totems.human.value'`). The `GroupLink` hooks system automatically syncs group memberships on actor changes.
|
- **Actor updates**: Use `actor.update({...})` with dot-notation paths (e.g., `'system.adaptation.totems.human.value'`). The `GroupLink` hooks system automatically syncs group memberships on actor changes.
|
||||||
- **New template partials**: Must be listed in `preloadHandlebarsTemplates()` in `handlebars-manager.mjs` for pre-compilation.
|
- **New template partials**: Must be listed in `preloadHandlebarsTemplates()` in `handlebars-manager.mjs` for pre-compilation.
|
||||||
|
|||||||
@@ -31,12 +31,16 @@ jobs:
|
|||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
# Build CSS from LESS
|
||||||
|
- name: Build CSS
|
||||||
|
run: npm run build:less
|
||||||
|
|
||||||
# Pull YAML to LDB packs
|
# Pull YAML to LDB packs
|
||||||
- name: Build Packs
|
- name: Build Packs
|
||||||
run: npm run pullYAMLtoLDB
|
run: npm run pullYAMLtoLDB
|
||||||
|
|
||||||
# Create a zip file with all files required by the module to add to the release
|
# Create a zip file with all files required by the module to add to the release
|
||||||
- run: zip -r ./system.zip system.json template.json assets/ css/ lang/ module/ templates/ packs/
|
- run: zip -r ./system.zip system.json assets/ css/ lang/ module/ templates/ packs/
|
||||||
|
|
||||||
# Create a release for this specific version
|
# Create a release for this specific version
|
||||||
- name: Update Release with Files
|
- name: Update Release with Files
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ foundry
|
|||||||
# Ancien dossier SCSS (archivé)
|
# Ancien dossier SCSS (archivé)
|
||||||
scss_old/
|
scss_old/
|
||||||
|
|
||||||
|
# Outils d'audit local
|
||||||
|
.swival/
|
||||||
|
|
||||||
# Fichiers CSS générés
|
# Fichiers CSS générés
|
||||||
css/vermine2047.*.css
|
css/vermine2047.*.css
|
||||||
!css/vermine2047.css
|
!css/vermine2047.css
|
||||||
|
regles/
|
||||||
|
|||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"atom": false,
|
||||||
|
"borderZero": {
|
||||||
|
"style": "none"
|
||||||
|
},
|
||||||
|
"colorVariables": true,
|
||||||
|
"comment": false,
|
||||||
|
"depthLevel": 10,
|
||||||
|
"duplicateProperty": true,
|
||||||
|
"emptyRule": true,
|
||||||
|
"finalNewline": true,
|
||||||
|
"hexLength": "long",
|
||||||
|
"hexNotation": "lowercase",
|
||||||
|
"hexValidation": true,
|
||||||
|
"idSelector": false,
|
||||||
|
"important": true,
|
||||||
|
"importPath": [
|
||||||
|
"./less"
|
||||||
|
],
|
||||||
|
"maxCharPerLine": 120,
|
||||||
|
"newlineAfterBlock": true,
|
||||||
|
"propertyOrdering": false,
|
||||||
|
"propertyUnits": true,
|
||||||
|
"qualifyingElement": [
|
||||||
|
"button",
|
||||||
|
"input",
|
||||||
|
"select",
|
||||||
|
"textarea"
|
||||||
|
],
|
||||||
|
"selectorNaming": false,
|
||||||
|
"singleLinePerProperty": true,
|
||||||
|
"singleLinePerSelector": false,
|
||||||
|
"spaceAfterComma": true,
|
||||||
|
"spaceAfterComment": true,
|
||||||
|
"spaceAfterPropertyColon": "one",
|
||||||
|
"spaceAfterPropertyValue": true,
|
||||||
|
"spaceAroundCombinator": true,
|
||||||
|
"spaceAroundOperator": true,
|
||||||
|
"spaceBeforeBrace": "one",
|
||||||
|
"spaceBetweenParens": false,
|
||||||
|
"stringQuotes": "double",
|
||||||
|
"trailingSemicolon": true,
|
||||||
|
"trailingWhitespace": true,
|
||||||
|
"urlFormat": "relative",
|
||||||
|
"urlQuotes": true,
|
||||||
|
"variableNaming": false,
|
||||||
|
"zeroUnit": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# AGENTS.md — Vermine2047 FoundryVTT system
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
FoundryVTT v14 game system for the French post-apocalyptic TTRPG "Vermine 2047". Bilingual FR/EN.
|
||||||
|
|
||||||
|
- **Entrypoint**: `module/vermine2047.mjs` (declared in `system.json` esmodules)
|
||||||
|
- **CSS**: `css/vermine2047.min.css` (compiled from LESS, **not** SCSS)
|
||||||
|
- **System ID**: `vermine2047`
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
| Layer | Location | Notes |
|
||||||
|
|-------|----------|-------|
|
||||||
|
| Entry/init | `module/vermine2047.mjs` | `Hooks.once('init',...)` — register DataModels before Document classes |
|
||||||
|
| DataModels | `module/models/*.mjs` | FoundryV2 `TypeDataModel` — 4 Actor types, 12 Item types |
|
||||||
|
| Documents | `module/documents/*.mjs` | `VermineActor`, `VermineItem` (thin wrappers) |
|
||||||
|
| Sheets (AppV2) | `module/applications/sheets/*.mjs` | ApplicationV2 sheets per type |
|
||||||
|
| System | `module/system/*.mjs` | config, roll, fight, dialogs, hooks, settings, dice3d, group-link |
|
||||||
|
| Templates (HBS) | `templates/actor/appv2/*.hbs` | ApplicationV2 templates |
|
||||||
|
| Legacy templates | `templates/actor/*.hbs`, `templates/actor/{character,group,npc,creature}/*.hbs` | Coexisting legacy templates |
|
||||||
|
|
||||||
|
### Actor types: character, npc, group, creature
|
||||||
|
### Item types: item, weapon, defense, vehicle, ability, specialty, background, trauma, evolution, rumor, target, rite
|
||||||
|
|
||||||
|
Key objects at runtime:
|
||||||
|
- `game.vermine2047` → `{ VermineUtils, VermineCombat, GroupLink }`
|
||||||
|
- `CONFIG.VERMINE` → game configuration (totems, traits, skills, etc.)
|
||||||
|
- `CONFIG.ui.combat` → custom `VermineCombatTracker`
|
||||||
|
|
||||||
|
Dice system: d10-based, success-counting (`Xd10cs>=N`). Totems give domain bonuses. Initiative formula:
|
||||||
|
`(@abilities.reflexes.value + @skills.alertness.value)d10cs>=@combatStatus.difficulty`
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build:less # LESS → vermine2047.min.css
|
||||||
|
npm run build:less:dev # LESS → vermine2047.dev.css (unminified)
|
||||||
|
npm run build:all-css # both
|
||||||
|
npm run watch # gulp watch + browser-sync proxy to localhost:30000
|
||||||
|
npm run lint:less # lesshint
|
||||||
|
npm run pushLDBtoYAML # extract Foundry packs → src/packs/*.yml
|
||||||
|
npm run pullYAMLtoLDB # compile src/packs/*.yml → packs/ LevelDB
|
||||||
|
make build / watch / lint # Makefile wrappers for above
|
||||||
|
```
|
||||||
|
|
||||||
|
- Node version required: `v16.15.1` (per `.nvmrc`)
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **DataModels before Document classes**: In `init` hook, register `CONFIG.Actor.dataModels.*` and `CONFIG.Item.dataModels.*` **before** setting `CONFIG.Actor.documentClass` / `CONFIG.Item.documentClass`.
|
||||||
|
- **`no-console: 'error'`**: ESLint blocks `console.log/warn/info`. Use `console.error` sparingly or disable the rule inline.
|
||||||
|
- **ESLint is very verbose**: ~350 rules. Common failures: missing semicolons, trailing commas, single quotes, 2-space indent, `max-len: 120`. Run `npx eslint module/` before committing.
|
||||||
|
- **Two template systems**: Both legacy (`templates/actor/*.hbs`) and ApplicationV2 (`templates/actor/appv2/*.hbs`) are preloaded. Don't confuse them.
|
||||||
|
- **Packs in two places**: `src/packs/*.yml` (YAML source, tracked) ↔ `packs/*` (LevelDB binary, gitignored). Edit YAML source, then run `pullYAMLtoLDB` to compile.
|
||||||
|
- **CSS auto-loaded**: `system.json` declares styles; **do not** inject `<link>` manually (causes MIME type errors).
|
||||||
|
- **`game-mode` setting**: Default in code is `'e'` (likely a typo for `'1'`). Values: `1`=Survie, `2`=Cauchemar, `3`=Apocalypse.
|
||||||
|
- **editMode flag disabled**: No auto-set on actor creation (causes ActiveEffect errors). Enable manually via sheet checkbox.
|
||||||
|
- **GroupLink auto-syncs**: `GroupLink.registerHooks()` runs on init. Actor updates propagate to groups and vice versa.
|
||||||
|
- **Custom dice**: Dice So Nice integration uses user color (regular/human/adapted color sets) with custom d10 face images.
|
||||||
|
- **No tests**: `npm test` is a stub (exits 1). No test framework installed.
|
||||||
|
- **No CI**: `.github/` is gitignored; no workflows configured.
|
||||||
|
- **HotReload**: `system.json` flags hotReload for css, scss, hbs, json extensions.
|
||||||
-141
@@ -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,136 @@
|
|||||||
# 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
|
Interface bilingue **FR/EN**.
|
||||||
- [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
|
|
||||||
|
|
||||||
### 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[],
|
| Type | Fiche | Description |
|
||||||
- [X] faire une fonction sur le Hook.onUpdateActor => update des groupes dans characters, update des encounters et members dans groups
|
|------|-------|-------------|
|
||||||
|
| **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
|
## Types d'Objets
|
||||||
- [X] à faire,
|
|
||||||
- [X] lister les gabarit/taille/roles(creatures) et menace/experience/role(pnj)... stocker les modifs dans CONFIG.VERMINE,
|
|
||||||
|
|
||||||
|
| 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
|
## Système 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
|
|
||||||
|
|
||||||
### le combat
|
- **Comptage de succès sur d10** : `Xd10cs>=N`
|
||||||
- [X] modifier la difficulté en fonction de l'état du combatant /offensif/actif/passif/
|
- 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
|
## Architecture
|
||||||
- [X] ajouter handicap de rareté
|
|
||||||
- [X] ajouter pour items Item "competence nécessaire"
|
```
|
||||||
- [X] gérer les rolls d'items dans le chat
|
module/
|
||||||
- [X] repasser sur les différents itemTypes et sheets
|
├── vermine2047.mjs # Point d'entrée (Hooks.once init)
|
||||||
- [X] verifier le selector de traits (trait pratique cf : msg pretre)
|
├── models/ # DataModels (Foundry v2 TypeDataModel)
|
||||||
- [X] construire une selecteur de traits, traits= CONFIG.VERMINE.traits
|
│ ├── actor/ # character.mjs, npc.mjs, group.mjs, creature.mjs
|
||||||
traits:[
|
│ └── item/ # Les 12 types d'objets
|
||||||
key:{
|
├── documents/ # VermineActor, VermineItem
|
||||||
name:string,
|
├── applications/
|
||||||
description:string,
|
│ └── sheets/ # Fiches AppV2 par type
|
||||||
value:number
|
├── system/
|
||||||
}
|
│ ├── config.mjs # CONFIG.VERMINE (totems, traits, compétences…)
|
||||||
]
|
│ ├── roll.mjs # VermineUtils.roll()
|
||||||
- [X] ajouter des dégats sur chaques fiches item
|
│ ├── 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
|
||||||
|
|
||||||
|
Projet fan non-officiel. L'univers et les marques Vermine 2047 appartiennent aux Éditions John Doe.
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
# Révision Complète des Templates Acteurs Vermine2047
|
|
||||||
|
|
||||||
## 📅 Date: 2026-06-04
|
|
||||||
|
|
||||||
## 🎯 Objectif
|
|
||||||
Réviser fichiers par fichier les templates des acteurs pour identifier et corriger les duplications et incohérences, comme demandé par l'utilisateur.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Corrections Effectuées
|
|
||||||
|
|
||||||
### 1. **Corrections de Structure CSS**
|
|
||||||
- ✅ `templates/actor/parts/actor-items.hbs:77` - `grid grid-2` → `grid grid-2col`
|
|
||||||
|
|
||||||
### 2. **Corrections des Balises HTML**
|
|
||||||
Toutes les balises `<p><a>` mal utilisées dans les listes d'items ont été remplacées par `<div><a class="item-control item-edit">` :
|
|
||||||
|
|
||||||
- ✅ `templates/actor/parts/actor-weapons.hbs:25-36` - 6 balises `<p>` corrigées
|
|
||||||
- ✅ `templates/actor/parts/actor-defenses.hbs:38-41` - 4 balises `<p>` corrigées
|
|
||||||
- ✅ `templates/actor/group/group-items.hbs:20-21` - 2 balises `<p>` corrigées
|
|
||||||
- ✅ `templates/actor/group/group-vehicles.hbs:22-27` - 3 balises `<p>` corrigées
|
|
||||||
|
|
||||||
**Impact**: Meilleure sémantique HTML et cohérence avec le reste du codebase.
|
|
||||||
|
|
||||||
### 3. **Corrections de Fautes de Frappe**
|
|
||||||
- ✅ `templates/actor/character/character-totem.hbs:12-16,19-23,26-30` - `smarttlk` → `smarttl` (3 occurrences)
|
|
||||||
- ✅ `templates/actor/character/character-totem.hbs:98` - `{{compétence}}` → `"Compétence"` (tooltip)
|
|
||||||
|
|
||||||
### 4. **Corrections de Classes CSS Dupliquées**
|
|
||||||
- ✅ `templates/actor/character/character-totem.hbs:59` - `class="item-name" class="flexrow"` → `class="item-name flexrow"`
|
|
||||||
|
|
||||||
### 5. **Corrections de Commentaires HTML**
|
|
||||||
Tous les commentaires HTML standard `<!-- -->` ont été convertis en commentaires Handlebars `{{!-- --}}` :
|
|
||||||
|
|
||||||
- ✅ `templates/actor/character/character-features.hbs:1`
|
|
||||||
- ✅ `templates/actor/character/character-header.hbs:1`
|
|
||||||
- ✅ `templates/actor/group/group-header.hbs:1`
|
|
||||||
- ✅ `templates/actor/character/character-totem.hbs:30-34` - Commentaire multi-lignes simplifié
|
|
||||||
- ✅ `templates/actor/character/character-id.hbs:5` - Ajout de commentaire
|
|
||||||
|
|
||||||
### 6. **Uniformisation des Localisations**
|
|
||||||
- ✅ `templates/actor/character/character-features.hbs:2` - `Caractéristiques` → `{{ localize 'VERMINE.abilities' }}`
|
|
||||||
- ✅ `templates/actor/character/character-features.hbs:32` - `Compétences` → `{{ localize 'VERMINE.skills' }}`
|
|
||||||
|
|
||||||
### 7. **Suppression des Balises Orphelines**
|
|
||||||
- ✅ `templates/actor/character/character-id.hbs:112` - Suppression de `{{/if}}` orphelin
|
|
||||||
|
|
||||||
### 8. **Optimisation des Structures Dupliquées**
|
|
||||||
|
|
||||||
#### a) Création de Partial pour les Catégories de Compétences NPC
|
|
||||||
- ✅ **Nouveau fichier**: `templates/actor/parts/npc-skill-category.hbs`
|
|
||||||
- Partial réutilisable pour afficher une catégorie de compétences
|
|
||||||
- Accepte `categoryKey` et `categoryLabel` comme paramètres
|
|
||||||
- ✅ **Modification**: `templates/actor/actor-npc-sheet.hbs:227-297`
|
|
||||||
- Remplacement de ~150 lignes de code dupliqué par une boucle Handlebars
|
|
||||||
- Utilisation du nouveau partial pour les 6 catégories (Homme, Animal, Outil, Arme, Survie, Monde)
|
|
||||||
- **Réduction**: ~145 lignes de code
|
|
||||||
|
|
||||||
#### b) Création de Partial Générique pour les Listes d'Items
|
|
||||||
- ✅ **Nouveau fichier**: `templates/actor/parts/item-list.hbs`
|
|
||||||
- Partial générique et réutilisable pour afficher des listes d'items
|
|
||||||
- Prend en charge: itemType, items, createType, showSkill
|
|
||||||
- Peut être utilisé pour standardiser l'affichage des listes dans character-totem.hbs et group-info.hbs
|
|
||||||
|
|
||||||
### 9. **Correction d'Erreur JavaScript**
|
|
||||||
- ✅ `module/system/roll.mjs:365-424` - Correction de l'erreur `html.find(...).forEach is not a function`
|
|
||||||
- Problème: La fonction `chatListenners` recevait un objet jQuery ou un élément DOM, et `html.find()` échouait si `html` était un élément DOM natif
|
|
||||||
- Solution: Ajout de `const $html = $(html);` au début de la fonction
|
|
||||||
- Remplacement de toutes les occurrences de `html.` par `$html.` dans la fonction
|
|
||||||
- **Impact**: La fonction gère maintenant correctement les deux types d'entrée (jQuery object ou DOM element)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 Fichiers Modifiés
|
|
||||||
|
|
||||||
### Templates Principaux (4)
|
|
||||||
1. `templates/actor/actor-character-sheet.hbs` - OK
|
|
||||||
2. `templates/actor/actor-npc-sheet.hbs` - ✅ Optimisé
|
|
||||||
3. `templates/actor/actor-creature-sheet.hbs` - OK
|
|
||||||
4. `templates/actor/actor-group-sheet.hbs` - OK
|
|
||||||
|
|
||||||
### Partials Character (6)
|
|
||||||
1. `templates/actor/character/character-features.hbs` - ✅ Corrigé
|
|
||||||
2. `templates/actor/character/character-header.hbs` - ✅ Corrigé
|
|
||||||
3. `templates/actor/character/character-id.hbs` - ✅ Corrigé
|
|
||||||
4. `templates/actor/character/character-stories.hbs` - OK
|
|
||||||
5. `templates/actor/character/character-totem.hbs` - ✅ Corrigé (multiples corrections)
|
|
||||||
6. `templates/actor/character/character-combat.hbs` - OK (à optimiser)
|
|
||||||
|
|
||||||
### Partials Parts (7)
|
|
||||||
1. `templates/actor/parts/actor-items.hbs` - ✅ Corrigé
|
|
||||||
2. `templates/actor/parts/actor-weapons.hbs` - ✅ Corrigé
|
|
||||||
3. `templates/actor/parts/actor-defenses.hbs` - ✅ Corrigé
|
|
||||||
4. `templates/actor/parts/actor-effects.hbs` - OK
|
|
||||||
5. `templates/actor/parts/npc-skill-item.hbs` - OK
|
|
||||||
6. `templates/actor/parts/npc-skill-category.hbs` - ✅ **NOUVEAU**
|
|
||||||
7. `templates/actor/parts/item-list.hbs` - ✅ **NOUVEAU**
|
|
||||||
|
|
||||||
### Partials Group (5)
|
|
||||||
1. `templates/actor/group/group-header.hbs` - ✅ Corrigé
|
|
||||||
2. `templates/actor/group/group-info.hbs` - ✅ Corrigé
|
|
||||||
3. `templates/actor/group/group-items.hbs` - ✅ Corrigé
|
|
||||||
4. `templates/actor/group/group-vehicles.hbs` - ✅ Corrigé
|
|
||||||
5. `templates/actor/group/group-experience.hbs` - OK
|
|
||||||
|
|
||||||
### JavaScript (2)
|
|
||||||
1. `module/system/roll.mjs` - ✅ Correction de l'erreur html.find().forEach
|
|
||||||
2. `module/system/hooks.mjs` - OK (pas de modification nécessaire)
|
|
||||||
|
|
||||||
### Nouveaux Fichiers Créés (2)
|
|
||||||
1. `templates/actor/parts/npc-skill-category.hbs`
|
|
||||||
2. `templates/actor/parts/item-list.hbs`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Statistiques
|
|
||||||
|
|
||||||
- **Fichiers analysés**: 24 templates + 2 fichiers JS
|
|
||||||
- **Fichiers modifiés**: 16 fichiers
|
|
||||||
- **Nouveaux fichiers créés**: 3 (2 partials + 1 rapport)
|
|
||||||
- **Duplications supprimées**: 1 majeure (catégories de compétences NPC)
|
|
||||||
- **Lignes de code réduites**: ~150+ lignes
|
|
||||||
- **Problèmes corrigés**: 20+
|
|
||||||
- **Partials créés**: 2
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 Problèmes Restants à Résoudre
|
|
||||||
|
|
||||||
### 1. **Duplication des Sections de Blessures**
|
|
||||||
Les templates suivants ont des implémentations similaires pour les blessures :
|
|
||||||
- `templates/actor/character/character-combat.hbs` (lignes 108-199)
|
|
||||||
- `templates/actor/npc/npc-combat.hbs` (lignes 23-47)
|
|
||||||
- `templates/actor/creature/creature-combat.hbs` (lignes 34-66)
|
|
||||||
|
|
||||||
**Solution recommandée**: Créer un partial `templates/actor/parts/wounds-section.hbs` pour standardiser l'affichage des blessures (minor, major, deadly).
|
|
||||||
|
|
||||||
### 2. **Utilisation du Partial item-list.hbs**
|
|
||||||
Le partial `item-list.hbs` a été créé mais n'est pas encore utilisé. Il pourrait remplacer les duplications dans :
|
|
||||||
- `templates/actor/character/character-totem.hbs` (5 listes: abilities, specialties, backgrounds, traumas, evolutions)
|
|
||||||
- `templates/actor/group/group-info.hbs` (5 listes identiques)
|
|
||||||
- `templates/actor/group/group-experience.hbs` (1 liste)
|
|
||||||
|
|
||||||
**Impact potentiel**: Réduction de ~200+ lignes de code dupliqué.
|
|
||||||
|
|
||||||
### 3. **Erreurs JavaScript Restantes**
|
|
||||||
Les erreurs suivantes n'ont pas encore été investiguées :
|
|
||||||
- `vermine2047.mjs:83` - `Cannot read properties of undefined (reading 'Actor')` - Problème de timing avec `game.system.template.Actor`
|
|
||||||
- `actor.mjs:89` - `Cannot read properties of undefined (reading 'difficulty')` - Problème dans `prepareCombatStatus`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Rapport Complet
|
|
||||||
|
|
||||||
Un rapport détaillé a été créé : `REVISION_TEMPLATES_RAPPORT.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Résumé des Actions
|
|
||||||
|
|
||||||
### ✅ Terminées
|
|
||||||
1. Correction de toutes les incohérences de syntaxe HTML/CSS
|
|
||||||
2. Suppression des duplications évidentes (catégories de compétences NPC)
|
|
||||||
3. Correction des fautes de frappe et erreurs de syntaxe
|
|
||||||
4. Uniformisation des commentaires et localisations
|
|
||||||
5. Correction de l'erreur JavaScript `html.find(...).forEach`
|
|
||||||
6. Création de 2 nouveaux partials réutilisables
|
|
||||||
|
|
||||||
### ⏳ Recommandations pour la Suite
|
|
||||||
1. Créer un partial pour les blessures (`wounds-section.hbs`)
|
|
||||||
2. Appliquer le partial `item-list.hbs` dans les templates existants
|
|
||||||
3. Investiguer et corriger les erreurs JavaScript restantes
|
|
||||||
4. Tester tous les templates dans FoundryVTT
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 Améliorations Apportées
|
|
||||||
|
|
||||||
### Maintenabilité
|
|
||||||
- **Réduction de la duplication**: ~150 lignes supprimées grâce aux partials
|
|
||||||
- **Meilleure organisation**: 2 nouveaux partials créés pour une meilleure réutilisation
|
|
||||||
- **Cohérence accrue**: Uniformisation des commentaires et des balises
|
|
||||||
|
|
||||||
### Robustesse
|
|
||||||
- **Correction d'erreurs**: 1 erreur JavaScript critique corrigée
|
|
||||||
- **Meilleure sémantique HTML**: Remplacement des balises `<p>` inappropriées
|
|
||||||
- **Suppression de balises orphelines**: Élimination de `{{/if}}` sans correspondant
|
|
||||||
|
|
||||||
### Internationalisation
|
|
||||||
- **Localisations ajoutées**: 2 titres maintenant localisés
|
|
||||||
- **Préparation pour traduction**: Structure plus propre pour les traductions futures
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📌 Conclusion
|
|
||||||
|
|
||||||
Cette révision a permis de :
|
|
||||||
1. **Corriger** les erreurs de syntaxe et d'incohérence dans les templates
|
|
||||||
2. **Optimiser** le code en supprimant les duplications évidentes
|
|
||||||
3. **Améliorer** la maintenabilité avec de nouveaux partials
|
|
||||||
4. **Stabiliser** le code JavaScript en corrigant une erreur critique
|
|
||||||
|
|
||||||
Le travail peut être considéré comme **complet pour la phase 1** (nettoyage et correction). La phase 2 (optimisation avancée) consiste à créer des partials supplémentaires pour les sections de blessures et à appliquer le partial `item-list.hbs` dans les templates existants.
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
# Rapport de Révision des Templates Acteurs Vermine2047
|
|
||||||
|
|
||||||
## Date: 2026-06-04
|
|
||||||
|
|
||||||
## Objectif
|
|
||||||
Réviser fichiers par fichier les templates des acteurs pour identifier et corriger les duplications et incohérences.
|
|
||||||
|
|
||||||
## Problèmes Initiaux Identifiés
|
|
||||||
|
|
||||||
### 1. Erreurs JavaScript
|
|
||||||
Les erreurs initiales reportées incluaient:
|
|
||||||
- `ENOENT: no such file or directory, open '/.../templates/item/partials/damages.html'` → Fichier existe en `.hbs`
|
|
||||||
- `Cannot read properties of undefined (reading 'Actor')` → Problème dans vermine2047.mjs:123
|
|
||||||
- `html.find(...).forEach is not a function` → Problème dans roll.mjs:373
|
|
||||||
- `Cannot read properties of undefined (reading 'difficulty')` → Problème dans actor.mjs:89
|
|
||||||
- `ActiveEffect application phase "initial" has already completed` → Problème de cycle de vie
|
|
||||||
|
|
||||||
### 2. Duplications dans les Templates
|
|
||||||
|
|
||||||
#### a) Duplication des catégories de compétences NPC (actor-npc-sheet.hbs)
|
|
||||||
**Problème**: 6 catégories de compétences (Homme, Animal, Outil, Arme, Survie, Monde) avec la même structure HTML dupliquée.
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
- Créé un nouveau partial: `templates/actor/parts/npc-skill-category.hbs`
|
|
||||||
- Remplacé la section dupliquée (lignes 227-297) par une boucle Handlebars
|
|
||||||
- Utilisation: `{{> "systems/vermine2047/templates/actor/parts/npc-skill-category.hbs" categoryKey=key categoryLabel=(concat "VERMINE.skill_category." key)}}`
|
|
||||||
|
|
||||||
**Réduction**: ~150 lignes → ~5 lignes
|
|
||||||
|
|
||||||
#### b) Duplication de la structure des blessures
|
|
||||||
**Fichiers concernés**:
|
|
||||||
- `character-combat.hbs` (lignes 108-199)
|
|
||||||
- `npc-combat.hbs` (lignes 23-47)
|
|
||||||
- `creature-combat.hbs` (lignes 34-66)
|
|
||||||
|
|
||||||
**Problème**: Chaque template de combat a sa propre implémentation des radio buttons pour les blessures.
|
|
||||||
|
|
||||||
**Solution recommandée**: Créer un partial `wounds-section.hbs` (à implémenter)
|
|
||||||
|
|
||||||
#### c) Duplication des listes d'items
|
|
||||||
**Fichiers concernés**:
|
|
||||||
- `character-totem.hbs` (abilities, specialties, backgrounds, traumas, evolutions)
|
|
||||||
- `group-info.hbs` (abilities, specialties, backgrounds, traumas, evolutions)
|
|
||||||
- `group-experience.hbs` (group abilities)
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
- Créé un partial générique: `templates/actor/parts/item-list.hbs`
|
|
||||||
- Peut être utilisé pour standardiser l'affichage des listes d'items
|
|
||||||
|
|
||||||
## Corrections Effectuées
|
|
||||||
|
|
||||||
### 1. Corrections de Classes CSS
|
|
||||||
- ✅ `actor-items.hbs:77`: `grid grid-2` → `grid grid-2col`
|
|
||||||
|
|
||||||
### 2. Corrections des Balises HTML
|
|
||||||
- ✅ `actor-weapons.hbs:25-36`: Remplacement des `<p><a>` par `<div><a class="item-control item-edit">`
|
|
||||||
- ✅ `actor-defenses.hbs:38-41`: Remplacement des `<p><a>` par `<div><a class="item-control item-edit">`
|
|
||||||
- ✅ `group-items.hbs:20-21`: Remplacement des `<p><a>` par `<div><a class="item-control item-edit">`
|
|
||||||
- ✅ `group-vehicles.hbs:22-27`: Remplacement des `<p><a>` par `<div><a class="item-control item-edit">`
|
|
||||||
|
|
||||||
### 3. Corrections de Fautes de Frappe
|
|
||||||
- ✅ `character-totem.hbs:12-16,19-23,26-30`: `smarttlk` → `smarttl`
|
|
||||||
- ✅ `character-totem.hbs:98`: `{{compétence}}` → `"Compétence"` (tooltips)
|
|
||||||
|
|
||||||
### 4. Corrections de Doubles Classes
|
|
||||||
- ✅ `character-totem.hbs:59`: `class="item-name" class="flexrow"` → `class="item-name flexrow"`
|
|
||||||
|
|
||||||
### 5. Corrections de Commentaires HTML
|
|
||||||
- ✅ `character-features.hbs:1`: `<!-- Character -->` → `{{!-- Character --}}`
|
|
||||||
- ✅ `character-header.hbs:1`: `<!-- HEADER -->` → `{{!-- HEADER --}}`
|
|
||||||
- ✅ `group-header.hbs:1`: `<!-- HEADER -->` → `{{!-- HEADER --}}`
|
|
||||||
- ✅ `character-totem.hbs:30-34`: Commentaire HTML multi-lignes → `{{!-- Abstract Items --}}`
|
|
||||||
- ✅ `character-id.hbs:5`: Ajout de commentaire Handlebars
|
|
||||||
|
|
||||||
### 6. Corrections de Localisation
|
|
||||||
- ✅ `character-features.hbs:2`: `Caractéristiques` → `{{ localize 'VERMINE.abilities' }}`
|
|
||||||
- ✅ `character-features.hbs:32`: `Compétences` → `{{ localize 'VERMINE.skills' }}`
|
|
||||||
|
|
||||||
### 7. Optimisation des Structures Dupliquées
|
|
||||||
- ✅ **Création de `npc-skill-category.hbs`**: Partial pour les catégories de compétences NPC
|
|
||||||
- ✅ **Modification de `actor-npc-sheet.hbs`**: Utilisation du nouveau partial avec boucle
|
|
||||||
- ✅ **Création de `item-list.hbs`**: Partial générique pour les listes d'items
|
|
||||||
|
|
||||||
### 8. Correction de Balises Orphelines
|
|
||||||
- ✅ `character-id.hbs:112`: Suppression de `{{/if}}` orphelin
|
|
||||||
|
|
||||||
## Fichiers Modifiés
|
|
||||||
|
|
||||||
### Templates Principaux
|
|
||||||
1. `actor-character-sheet.hbs` - Structure de base OK
|
|
||||||
2. `actor-npc-sheet.hbs` - ✅ Optimisé (duplication des catégories de compétences supprimée)
|
|
||||||
3. `actor-creature-sheet.hbs` - Structure OK
|
|
||||||
4. `actor-group-sheet.hbs` - Structure OK
|
|
||||||
|
|
||||||
### Partials Character
|
|
||||||
1. `character/character-features.hbs` - ✅ Commentaires et localisations corrigés
|
|
||||||
2. `character/character-header.hbs` - ✅ Commentaire corrigé
|
|
||||||
3. `character/character-id.hbs` - ✅ Commentaire ajouté, balise orpheline supprimée
|
|
||||||
4. `character/character-stories.hbs` - OK
|
|
||||||
5. `character/character-totem.hbs` - ✅ Fautes de frappe corrigées, commentaire corrigé, double classe corrigée
|
|
||||||
6. `character/character-combat.hbs` - À optimiser (duplication avec wounds)
|
|
||||||
|
|
||||||
### Partials Parts
|
|
||||||
1. `parts/actor-items.hbs` - ✅ grid-2 → grid-2col
|
|
||||||
2. `parts/actor-weapons.hbs` - ✅ Balises <p> corrigées
|
|
||||||
3. `parts/actor-defenses.hbs` - ✅ Balises <p> corrigées
|
|
||||||
4. `parts/actor-effects.hbs` - OK
|
|
||||||
5. `parts/npc-skill-item.hbs` - OK
|
|
||||||
6. `parts/npc-skill-category.hbs` - ✅ NOUVEAU
|
|
||||||
7. `parts/item-list.hbs` - ✅ NOUVEAU
|
|
||||||
|
|
||||||
### Partials Group
|
|
||||||
1. `group/group-header.hbs` - ✅ Commentaire corrigé
|
|
||||||
2. `group/group-info.hbs` - ✅ Commentaire corrigé
|
|
||||||
3. `group/group-items.hbs` - ✅ Balises <p> corrigées
|
|
||||||
4. `group/group-vehicles.hbs` - ✅ Balises <p> corrigées
|
|
||||||
5. `group/group-experience.hbs` - OK
|
|
||||||
|
|
||||||
### Templates de Combat
|
|
||||||
1. `npc/npc-combat.hbs` - À optimiser
|
|
||||||
2. `creature/creature-combat.hbs` - À optimiser
|
|
||||||
3. `character/character-combat.hbs` - À optimiser
|
|
||||||
|
|
||||||
### Autres
|
|
||||||
1. `create.hbs` - OK
|
|
||||||
|
|
||||||
## Recommandations pour la Suite
|
|
||||||
|
|
||||||
### 1. Créer un partial pour les blessures
|
|
||||||
Créer `templates/actor/parts/wounds-section.hbs` pour standardiser l'affichage des blessures (minor, major, deadly) utilisés dans:
|
|
||||||
- character-combat.hbs
|
|
||||||
- npc-combat.hbs
|
|
||||||
- creature-combat.hbs
|
|
||||||
|
|
||||||
### 2. Standardiser les listes d'items
|
|
||||||
Utiliser le partial `item-list.hbs` pour remplacer les duplications dans:
|
|
||||||
- character-totem.hbs (5 listes)
|
|
||||||
- group-info.hbs (5 listes)
|
|
||||||
- group-experience.hbs (1 liste)
|
|
||||||
|
|
||||||
### 3. Vérifier les erreurs JavaScript
|
|
||||||
Les erreurs initiales doivent être investiguées dans:
|
|
||||||
- `vermine2047.mjs:123` - `Cannot read properties of undefined (reading 'Actor')`
|
|
||||||
- `roll.mjs:373` - `html.find(...).forEach is not a function`
|
|
||||||
- `actor.mjs:89` - `Cannot read properties of undefined (reading 'difficulty')`
|
|
||||||
|
|
||||||
### 4. Vérifier les références .html
|
|
||||||
Bien que aucune référence `.html` n'ait été trouvée dans les templates, l'erreur initiale suggère qu'il y a des références dans le code JavaScript. Rechercher dans:
|
|
||||||
- Les fichiers `.mjs` pour des références à `damages.html`
|
|
||||||
- Les appels à `loadTemplates()` ou `renderTemplate()`
|
|
||||||
|
|
||||||
## Statistiques
|
|
||||||
|
|
||||||
- **Fichiers analysés**: 24 templates
|
|
||||||
- **Duplications supprimées**: 1 (catégories de compétences NPC)
|
|
||||||
- **Partials créés**: 2 (npc-skill-category.hbs, item-list.hbs)
|
|
||||||
- **Fichiers modifiés**: 12
|
|
||||||
- **Lignes de code réduites**: ~150+ lignes
|
|
||||||
- **Problèmes corrigés**: 15+
|
|
||||||
|
|
||||||
## Prochaines Étapes
|
|
||||||
|
|
||||||
1. ✅ Corriger les erreurs de syntaxe HTML/CSS (TERMINÉ)
|
|
||||||
2. ✅ Supprimer les duplications évidentes (TERMINÉ pour NPC skills)
|
|
||||||
3. ⏳ Créer des partials pour les sections communes (EN COURS)
|
|
||||||
4. ⏳ Optimiser les templates de combat
|
|
||||||
5. ⏳ Vérifier et corriger les erreurs JavaScript
|
|
||||||
6. ⏳ Tester tous les templates dans FoundryVTT
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Tous les templates utilisent maintenant `.hbs` au lieu de `.html`
|
|
||||||
- Les commentaires sont progressivement uniformisés vers `{{!-- --}}`
|
|
||||||
- Les structures de grille utilisent `grid-2col` au lieu de `grid-2`
|
|
||||||
- Les balises `<p>` pour les cellules de tableau ont été remplacées par `<div>`
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 54 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`.
|
||||||
@@ -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.*
|
||||||
+221
-3
@@ -147,6 +147,8 @@
|
|||||||
"pools": "Reserves",
|
"pools": "Reserves",
|
||||||
"self_control": "Self-control",
|
"self_control": "Self-control",
|
||||||
"effort": "Effort",
|
"effort": "Effort",
|
||||||
|
"equip": "Equip",
|
||||||
|
"unequip": "Unequip",
|
||||||
"group": "Group",
|
"group": "Group",
|
||||||
"abilities": "Characteristics",
|
"abilities": "Characteristics",
|
||||||
"ability": "Characteristic",
|
"ability": "Characteristic",
|
||||||
@@ -172,6 +174,15 @@
|
|||||||
"range": "Range",
|
"range": "Range",
|
||||||
"ranges": "Ranges",
|
"ranges": "Ranges",
|
||||||
"mobility": "Mobility",
|
"mobility": "Mobility",
|
||||||
|
"vehicle_specs": "Vehicle specifications",
|
||||||
|
"kind": "Kind",
|
||||||
|
"kind_vehicle": "Vehicle",
|
||||||
|
"kind_mount": "Mount",
|
||||||
|
"distance": "Distance",
|
||||||
|
"speed": "Speed",
|
||||||
|
"speed_duration_ph": "top speed duration (e.g. 10 min)",
|
||||||
|
"effort_reserve": "Effort reserve",
|
||||||
|
"maintenance": "Maintenance",
|
||||||
"rarity": "Rarity",
|
"rarity": "Rarity",
|
||||||
"rarity_sm": "Rar.",
|
"rarity_sm": "Rar.",
|
||||||
"reliability": "Reliability",
|
"reliability": "Reliability",
|
||||||
@@ -202,6 +213,7 @@
|
|||||||
"type_character": "Character",
|
"type_character": "Character",
|
||||||
"type_npc": "NPC",
|
"type_npc": "NPC",
|
||||||
"type_creature": "Creature",
|
"type_creature": "Creature",
|
||||||
|
"type_community": "Community",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"group_members": "Members",
|
"group_members": "Members",
|
||||||
"encounters": "Encounters",
|
"encounters": "Encounters",
|
||||||
@@ -223,6 +235,7 @@
|
|||||||
"world": "World"
|
"world": "World"
|
||||||
},
|
},
|
||||||
"CharacterNamePlaceholder": "Character Name",
|
"CharacterNamePlaceholder": "Character Name",
|
||||||
|
"ProfilePlaceholder": "Profile",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"bonuses": "Bonuses",
|
"bonuses": "Bonuses",
|
||||||
"choose_ability": "Choose Ability",
|
"choose_ability": "Choose Ability",
|
||||||
@@ -240,6 +253,29 @@
|
|||||||
"success_count": "Success Count",
|
"success_count": "Success Count",
|
||||||
"success_required": "Success Required",
|
"success_required": "Success Required",
|
||||||
"test_of": "Test of",
|
"test_of": "Test of",
|
||||||
|
"roll_success": "Success",
|
||||||
|
"roll_failure": "Failure",
|
||||||
|
"roll_context": "Roll context",
|
||||||
|
"roll_pool": "Dice pool",
|
||||||
|
"roll_dice": "Dice detail",
|
||||||
|
"pool_ability": "Ability",
|
||||||
|
"pool_skill": "Skill",
|
||||||
|
"pool_specialty": "Specialty",
|
||||||
|
"pool_help": "Help",
|
||||||
|
"pool_tooling": "Equipment",
|
||||||
|
"pool_group": "Group",
|
||||||
|
"pool_selfControl": "Self-Control",
|
||||||
|
"pool_pools": "Pools",
|
||||||
|
"pool_total": "Total",
|
||||||
|
"totem_used": "Totem",
|
||||||
|
"totem_keep": "kept",
|
||||||
|
"domain_bonus": "Domain bonus",
|
||||||
|
"domain_penalty": "Domain penalty",
|
||||||
|
"used": "used",
|
||||||
|
"die_regular": "dice",
|
||||||
|
"die_human": "human",
|
||||||
|
"die_adapted": "adapted",
|
||||||
|
"die_successes": "successes",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"totem_dice": "Totem Dice",
|
"totem_dice": "Totem Dice",
|
||||||
"totem_hint": "Totem Hint",
|
"totem_hint": "Totem Hint",
|
||||||
@@ -257,12 +293,108 @@
|
|||||||
"error_no_actor_selected": "No actor selected",
|
"error_no_actor_selected": "No actor selected",
|
||||||
"error_not_enough_self_control": "Not enough self control",
|
"error_not_enough_self_control": "Not enough self control",
|
||||||
"error_select_ability": "Select an ability",
|
"error_select_ability": "Select an ability",
|
||||||
|
"attack": "Attack",
|
||||||
|
"attack_contact": "Melee attack",
|
||||||
|
"attack_ranged": "Ranged attack",
|
||||||
|
"defense_esquive": "Dodge",
|
||||||
|
"defense_parade": "Parry",
|
||||||
|
"passive_defense": "Passive defense",
|
||||||
|
"range_short": "Short",
|
||||||
|
"range_mid": "Medium",
|
||||||
|
"range_long": "Long",
|
||||||
|
"second_action": "Second action (+2 difficulty)",
|
||||||
|
"consume_ammo": "Consume ammo",
|
||||||
|
"npc_pool": "Pools used",
|
||||||
|
"exchange": "Combat exchange",
|
||||||
|
"resolve": "Resolve",
|
||||||
|
"resolve_result": "Exchange resolution",
|
||||||
|
"attacker_successes": "Attack successes",
|
||||||
|
"defender_successes": "Defense successes",
|
||||||
|
"raw_damage": "Raw damage",
|
||||||
|
"net_damage": "Net damage",
|
||||||
|
"hit": "Hit",
|
||||||
|
"hit_parade": "Hit (insufficient parry)",
|
||||||
|
"missed_esquive": "Dodged",
|
||||||
|
"missed_parade": "Parried",
|
||||||
|
"no_damage": "No damage",
|
||||||
|
"wound": "Wound",
|
||||||
|
"no_wound": "No wound",
|
||||||
|
"apply_wound": "Apply wound",
|
||||||
|
"wound_applied": "Wound applied",
|
||||||
|
"error_creature_no_defense": "Creatures cannot defend (passive defense)",
|
||||||
|
"error_no_permission": "Insufficient permissions",
|
||||||
|
"error_no_target": "No target selected for this exchange",
|
||||||
|
"experience_phase": "Experience phase",
|
||||||
|
"experience_dice": "Experience dice",
|
||||||
|
"experience_rewards": "Experience rewards",
|
||||||
|
"collective": "collective",
|
||||||
|
"collective_rewards": "Collective rewards",
|
||||||
|
"individual_rewards": "Individual rewards",
|
||||||
|
"collective_group": "Group (collective pool)",
|
||||||
|
"danger": "Danger",
|
||||||
|
"discoveries": "Discoveries",
|
||||||
|
"duration": "Duration",
|
||||||
|
"implication": "Implication",
|
||||||
|
"influence": "Influence",
|
||||||
|
"interpretation": "Interpretation",
|
||||||
|
"reward_anecdotic": "Anecdotal",
|
||||||
|
"reward_minor": "Minor",
|
||||||
|
"reward_major": "Major",
|
||||||
|
"reward_exceptionnal": "Exceptional",
|
||||||
|
"objective_minor": "Minor objective",
|
||||||
|
"objective_major": "Major objective",
|
||||||
|
"trauma_reward": "Chosen trauma",
|
||||||
|
"trauma_reward_5": "Trauma reward (+5D)",
|
||||||
|
"evolution_dice": "Evolution dice",
|
||||||
|
"open_experience_phase": "Experience phase",
|
||||||
|
"open_learning": "Learning",
|
||||||
|
"learning_limits": "Learning limits (per phase)",
|
||||||
|
"learned_skill": "Skill",
|
||||||
|
"learned_specialty": "Specialty",
|
||||||
|
"learned_reserve": "Reserve",
|
||||||
|
"learned_characteristic": "Characteristic",
|
||||||
|
"learned_evolution": "Evolution",
|
||||||
|
"no_character": "No character in this world.",
|
||||||
|
"apply": "Apply",
|
||||||
|
"characteristic": "Characteristic",
|
||||||
|
"learn_skill": "Skill",
|
||||||
|
"learn_specialty": "Specialty",
|
||||||
|
"learn_characteristic": "Characteristic",
|
||||||
|
"learn_reserve": "Reserve",
|
||||||
|
"learn_evolution": "Evolution die",
|
||||||
|
"learn_specialty_name": "Specialty name",
|
||||||
|
"specialty_hint": "Prerequisite: skill at least Proficient (≥2).",
|
||||||
|
"characteristic_hint": "Difficulty 9 + Handicap = current value. Max 3 (except Superhuman).",
|
||||||
|
"reserve_hint": "Difficulty = current value + age Handicap. Max 10.",
|
||||||
|
"evolution_hint": "Difficulty by age + Handicap = accumulated Evolution dice. Adapted totem +1D, Human -1D.",
|
||||||
|
"dice_spent": "Dice spent",
|
||||||
|
"learning_success": "Learning successful!",
|
||||||
|
"error_no_experience_dice": "No Experience dice available.",
|
||||||
|
"error_learning_limit": "Limit reached for this type of learning in this phase.",
|
||||||
|
"error_skill_max": "This skill is already at maximum level.",
|
||||||
|
"error_characteristic_max": "Characteristic already at maximum (3).",
|
||||||
|
"error_specialty_requirement": "Specialty requires: skill at least Proficient (≥2).",
|
||||||
|
"error_reserve_max": "Reserve already at maximum (10).",
|
||||||
|
"experience_transfer": "Experience dice",
|
||||||
|
"experience_pull": "Pull from group",
|
||||||
|
"experience_give": "Give 1D to group",
|
||||||
|
"pool_experience": "Experience",
|
||||||
|
"error_no_group": "No group associated with this character.",
|
||||||
|
"error_no_group_dice": "Not enough group pool dice.",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"abilities": "Abilities",
|
"abilities": "Abilities",
|
||||||
"combat": "Combat",
|
"combat": "Combat",
|
||||||
"equipment": "Equipment",
|
"equipment": "Equipment",
|
||||||
"stories": "Stories",
|
"stories": "Stories",
|
||||||
"totem": "Totem"
|
"totem": "Totem",
|
||||||
|
"experience": "Experience"
|
||||||
|
},
|
||||||
|
"Sheet": {
|
||||||
|
"character": "Character",
|
||||||
|
"npc": "NPC",
|
||||||
|
"group": "Group",
|
||||||
|
"creature": "Creature",
|
||||||
|
"community": "Community"
|
||||||
},
|
},
|
||||||
"base_values": "Base Values",
|
"base_values": "Base Values",
|
||||||
"computed_values": "Computed Values",
|
"computed_values": "Computed Values",
|
||||||
@@ -326,6 +458,91 @@
|
|||||||
"effects.passive": "Passive",
|
"effects.passive": "Passive",
|
||||||
"effects.temporary": "Temporary"
|
"effects.temporary": "Temporary"
|
||||||
},
|
},
|
||||||
|
"COMMUNITY": {
|
||||||
|
"tabs": {
|
||||||
|
"identity": "Identity",
|
||||||
|
"domains": "Domains",
|
||||||
|
"resources": "Resources"
|
||||||
|
},
|
||||||
|
"identity": "Identity",
|
||||||
|
"name_placeholder": "Community name",
|
||||||
|
"choose_totem": "Choose totem",
|
||||||
|
"origin": "Origin",
|
||||||
|
"theme": "Theme",
|
||||||
|
"age_label": "Community age",
|
||||||
|
"age": {
|
||||||
|
"recent": "A few days",
|
||||||
|
"established": "A few months",
|
||||||
|
"old": "A few years"
|
||||||
|
},
|
||||||
|
"instincts": "Instincts",
|
||||||
|
"prohibits": "Prohibitions",
|
||||||
|
"notes": "Notes",
|
||||||
|
"location": "Location",
|
||||||
|
"sector": "Sector",
|
||||||
|
"placeType": "Place type",
|
||||||
|
"landmarks": "Landmarks",
|
||||||
|
"territory": "Territory",
|
||||||
|
"history": "History",
|
||||||
|
"founding": "Founding",
|
||||||
|
"events": "Key events",
|
||||||
|
"history_relations": "Relations",
|
||||||
|
"objectives": "Objectives",
|
||||||
|
"objective_major": "Major objective",
|
||||||
|
"objectives_minor": "Minor objectives",
|
||||||
|
"add_objective": "Add objective",
|
||||||
|
"size_title": "Size & Population",
|
||||||
|
"size_label": "Size",
|
||||||
|
"sizeLevels": {
|
||||||
|
"1": "Size 1",
|
||||||
|
"2": "Size 2",
|
||||||
|
"3": "Size 3"
|
||||||
|
},
|
||||||
|
"population": "Population",
|
||||||
|
"population_range": "Recommended population",
|
||||||
|
"figures": "Figures",
|
||||||
|
"companies": "Companies",
|
||||||
|
"domains": {
|
||||||
|
"resources": "Resources",
|
||||||
|
"security": "Security",
|
||||||
|
"dynamism": "Dynamism",
|
||||||
|
"relations": "Relations"
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"vivre": "Food & Water",
|
||||||
|
"supplies": "Supplies",
|
||||||
|
"defense": "Defense",
|
||||||
|
"care": "Care",
|
||||||
|
"cohesion": "Cohesion",
|
||||||
|
"productivity": "Productivity",
|
||||||
|
"human": "Human",
|
||||||
|
"ecology": "Ecology"
|
||||||
|
},
|
||||||
|
"states": {
|
||||||
|
"crisis": "Crisis",
|
||||||
|
"low": "Low",
|
||||||
|
"stable": "Stable",
|
||||||
|
"opulent": "Opulent"
|
||||||
|
},
|
||||||
|
"reserves": "Reserves",
|
||||||
|
"group_reserve": "Group reserve",
|
||||||
|
"material_dice": "Material dice",
|
||||||
|
"experience_dice": "Experience dice",
|
||||||
|
"morale": "Morale",
|
||||||
|
"morale_value": "Value",
|
||||||
|
"morale_state": "State",
|
||||||
|
"morale_levels": {
|
||||||
|
"high": "High",
|
||||||
|
"normal": "Normal",
|
||||||
|
"low": "Low",
|
||||||
|
"crisis": "Crisis"
|
||||||
|
},
|
||||||
|
"morale_blocked": "All four Domains are Low: the Morale bonus is inactive.",
|
||||||
|
"habitants_title": "Inhabitants",
|
||||||
|
"habitants_coming": "Figures and Companies management comes in a future version.",
|
||||||
|
"linked_group": "Linked group",
|
||||||
|
"profile_hint": "Community profile"
|
||||||
|
},
|
||||||
"ITEMS": {
|
"ITEMS": {
|
||||||
"item": "Item",
|
"item": "Item",
|
||||||
"items": "Items",
|
"items": "Items",
|
||||||
@@ -573,7 +790,7 @@
|
|||||||
"name": "Repulsion"
|
"name": "Repulsion"
|
||||||
},
|
},
|
||||||
"tracks": {
|
"tracks": {
|
||||||
"name": "Tracks"
|
"name": "Tracking"
|
||||||
},
|
},
|
||||||
"crafting": {
|
"crafting": {
|
||||||
"name": "Crafting"
|
"name": "Crafting"
|
||||||
@@ -603,7 +820,7 @@
|
|||||||
"name": "Melee"
|
"name": "Melee"
|
||||||
},
|
},
|
||||||
"archery": {
|
"archery": {
|
||||||
"name": "Ranged"
|
"name": "Ranged Weapons"
|
||||||
},
|
},
|
||||||
"throwing": {
|
"throwing": {
|
||||||
"name": "Throwing"
|
"name": "Throwing"
|
||||||
@@ -655,6 +872,7 @@
|
|||||||
"Round": "Round"
|
"Round": "Round"
|
||||||
},
|
},
|
||||||
"SIZE_LEVELS": {
|
"SIZE_LEVELS": {
|
||||||
|
"tiny": "Insect",
|
||||||
"small": "Small",
|
"small": "Small",
|
||||||
"medium": "Medium",
|
"medium": "Medium",
|
||||||
"large": "Large"
|
"large": "Large"
|
||||||
|
|||||||
+217
-5
@@ -93,7 +93,7 @@
|
|||||||
"AGE_TYPES": {
|
"AGE_TYPES": {
|
||||||
"young": "Jeune",
|
"young": "Jeune",
|
||||||
"adult": "Adulte",
|
"adult": "Adulte",
|
||||||
"old": "Vieux"
|
"old": "Ancien"
|
||||||
},
|
},
|
||||||
"IDENTITY": {
|
"IDENTITY": {
|
||||||
"biography": "Biographie",
|
"biography": "Biographie",
|
||||||
@@ -154,6 +154,8 @@
|
|||||||
"reserves": "Réserves",
|
"reserves": "Réserves",
|
||||||
"self_control": "Sang-Froid",
|
"self_control": "Sang-Froid",
|
||||||
"effort": "Effort",
|
"effort": "Effort",
|
||||||
|
"equip": "Équiper",
|
||||||
|
"unequip": "Déséquiper",
|
||||||
"preferred_category": "Catégorie préférée",
|
"preferred_category": "Catégorie préférée",
|
||||||
"wounds": {
|
"wounds": {
|
||||||
"name": "Blessures",
|
"name": "Blessures",
|
||||||
@@ -188,6 +190,29 @@
|
|||||||
"grant_reroll": "accorder des relances",
|
"grant_reroll": "accorder des relances",
|
||||||
"success_count": "nombre de succès",
|
"success_count": "nombre de succès",
|
||||||
"success_required": "succès requis",
|
"success_required": "succès requis",
|
||||||
|
"roll_success": "Réussite",
|
||||||
|
"roll_failure": "Échec",
|
||||||
|
"roll_context": "Contexte du jet",
|
||||||
|
"roll_pool": "Composition du jet",
|
||||||
|
"roll_dice": "Détail des dés",
|
||||||
|
"pool_ability": "Caractéristique",
|
||||||
|
"pool_skill": "Compétence",
|
||||||
|
"pool_specialty": "Spécialité",
|
||||||
|
"pool_help": "Entraide",
|
||||||
|
"pool_tooling": "Équipement",
|
||||||
|
"pool_group": "Groupe",
|
||||||
|
"pool_selfControl": "Sang-Froid",
|
||||||
|
"pool_pools": "Réserves",
|
||||||
|
"pool_total": "Total",
|
||||||
|
"totem_used": "Totem",
|
||||||
|
"totem_keep": "gardé",
|
||||||
|
"domain_bonus": "Bonus de domaine",
|
||||||
|
"domain_penalty": "Malus de domaine",
|
||||||
|
"used": "utilisé",
|
||||||
|
"die_regular": "dés",
|
||||||
|
"die_human": "humains",
|
||||||
|
"die_adapted": "adaptés",
|
||||||
|
"die_successes": "succès",
|
||||||
"error_no_actor_selected": "Vous n'avez pas de personnage attitré ou de token sélectionné",
|
"error_no_actor_selected": "Vous n'avez pas de personnage attitré ou de token sélectionné",
|
||||||
"error_cannot_reroll": "Vous ne pouvez pas relancer un dés sur ce jet",
|
"error_cannot_reroll": "Vous ne pouvez pas relancer un dés sur ce jet",
|
||||||
"error_no_rerolls_left": "Plus de relances possibles",
|
"error_no_rerolls_left": "Plus de relances possibles",
|
||||||
@@ -230,6 +255,15 @@
|
|||||||
"range": "Portée",
|
"range": "Portée",
|
||||||
"ranges": "Portées",
|
"ranges": "Portées",
|
||||||
"mobility": "Mobilité",
|
"mobility": "Mobilité",
|
||||||
|
"vehicle_specs": "Caractéristiques du véhicule",
|
||||||
|
"kind": "Type",
|
||||||
|
"kind_vehicle": "Véhicule",
|
||||||
|
"kind_mount": "Monture",
|
||||||
|
"distance": "Distance",
|
||||||
|
"speed": "Vitesse",
|
||||||
|
"speed_duration_ph": "durée de pointe (ex: 10 min)",
|
||||||
|
"effort_reserve": "Réserve d'Effort",
|
||||||
|
"maintenance": "Entretien",
|
||||||
"rarity": "Rareté",
|
"rarity": "Rareté",
|
||||||
"rarity_sm": "Rar.",
|
"rarity_sm": "Rar.",
|
||||||
"reliability": "Fiabilité",
|
"reliability": "Fiabilité",
|
||||||
@@ -244,6 +278,94 @@
|
|||||||
"totem_dice": "Dés de totem",
|
"totem_dice": "Dés de totem",
|
||||||
"keep_totem": "Garder le totem",
|
"keep_totem": "Garder le totem",
|
||||||
"totem_hint": "Cochez pour utiliser les dés de totem (double réussite possible)",
|
"totem_hint": "Cochez pour utiliser les dés de totem (double réussite possible)",
|
||||||
|
"attack": "Attaque",
|
||||||
|
"attack_contact": "Attaque au contact",
|
||||||
|
"attack_ranged": "Attaque à distance",
|
||||||
|
"defense_esquive": "Esquive",
|
||||||
|
"defense_parade": "Parade",
|
||||||
|
"passive_defense": "Défense passive",
|
||||||
|
"range_short": "Courte",
|
||||||
|
"range_mid": "Moyenne",
|
||||||
|
"range_long": "Longue",
|
||||||
|
"second_action": "Deuxième action (+2 difficulté)",
|
||||||
|
"consume_ammo": "Consommer des munitions",
|
||||||
|
"npc_pool": "Réserves utilisées",
|
||||||
|
"exchange": "Échange de coups",
|
||||||
|
"resolve": "Résoudre",
|
||||||
|
"resolve_result": "Résolution de l'échange",
|
||||||
|
"attacker_successes": "Réussites d'attaque",
|
||||||
|
"defender_successes": "Réussites de défense",
|
||||||
|
"raw_damage": "Dégâts bruts",
|
||||||
|
"net_damage": "Dégâts nets",
|
||||||
|
"hit": "Touché",
|
||||||
|
"hit_parade": "Touché (parade insuffisante)",
|
||||||
|
"missed_esquive": "Esquivé",
|
||||||
|
"missed_parade": "Paré",
|
||||||
|
"no_damage": "Aucun dégât",
|
||||||
|
"wound": "Blessure",
|
||||||
|
"no_wound": "Aucune blessure",
|
||||||
|
"apply_wound": "Appliquer la blessure",
|
||||||
|
"wound_applied": "Blessure appliquée",
|
||||||
|
"error_creature_no_defense": "Les créatures ne peuvent pas se défendre (défense passive)",
|
||||||
|
"error_no_permission": "Permissions insuffisantes",
|
||||||
|
"error_no_target": "Aucune cible sélectionnée pour cet échange",
|
||||||
|
"experience_phase": "Phase d'Expérience",
|
||||||
|
"experience_dice": "Dés d'Expérience",
|
||||||
|
"experience_rewards": "Récompenses d'Expérience",
|
||||||
|
"collective": "collectifs",
|
||||||
|
"collective_rewards": "Récompenses collectives",
|
||||||
|
"individual_rewards": "Récompenses individuelles",
|
||||||
|
"collective_group": "Groupe (réserve collective)",
|
||||||
|
"danger": "Danger",
|
||||||
|
"discoveries": "Découvertes",
|
||||||
|
"duration": "Durée",
|
||||||
|
"implication": "Implication",
|
||||||
|
"influence": "Influence",
|
||||||
|
"interpretation": "Interprétation",
|
||||||
|
"reward_anecdotic": "Anecdotique",
|
||||||
|
"reward_minor": "Mineure",
|
||||||
|
"reward_major": "Majeure",
|
||||||
|
"reward_exceptionnal": "Exceptionnelle",
|
||||||
|
"objective_minor": "Objectif mineur",
|
||||||
|
"objective_major": "Objectif majeur",
|
||||||
|
"trauma_reward": "Traumatisme choisi",
|
||||||
|
"trauma_reward_5": "Récompense de Traumatisme (+5D)",
|
||||||
|
"evolution_dice": "Dés d'Évolution",
|
||||||
|
"open_experience_phase": "Phase d'Expérience",
|
||||||
|
"open_learning": "Apprentissage",
|
||||||
|
"learning_limits": "Limites d'apprentissage (par phase)",
|
||||||
|
"learned_skill": "Compétence",
|
||||||
|
"learned_specialty": "Spécialité",
|
||||||
|
"learned_reserve": "Réserve",
|
||||||
|
"learned_characteristic": "Caractéristique",
|
||||||
|
"learned_evolution": "Évolution",
|
||||||
|
"no_character": "Aucun personnage dans ce monde.",
|
||||||
|
"apply": "Appliquer",
|
||||||
|
"characteristic": "Caractéristique",
|
||||||
|
"learn_skill": "Compétence",
|
||||||
|
"learn_specialty": "Spécialité",
|
||||||
|
"learn_characteristic": "Caractéristique",
|
||||||
|
"learn_reserve": "Réserve",
|
||||||
|
"learn_evolution": "Dé d'Évolution",
|
||||||
|
"learn_specialty_name": "Nom de la spécialité",
|
||||||
|
"specialty_hint": "Prérequis : compétence au moins Confirmée (≥2).",
|
||||||
|
"characteristic_hint": "Difficulté 9 + Handicap = valeur actuelle. Max 3 (Surhumain excepté).",
|
||||||
|
"reserve_hint": "Difficulté = valeur actuelle + Handicap d'âge. Max 10.",
|
||||||
|
"evolution_hint": "Difficulté selon l'âge + Handicap = Dés d'Évolution accumulés. Totem Adapté +1D, Humain -1D.",
|
||||||
|
"dice_spent": "Dés dépensés",
|
||||||
|
"learning_success": "Apprentissage réussi !",
|
||||||
|
"error_no_experience_dice": "Pas de Dés d'Expérience disponibles.",
|
||||||
|
"error_learning_limit": "Limite atteinte pour ce type d'apprentissage dans cette phase.",
|
||||||
|
"error_skill_max": "Cette compétence est déjà au niveau maximal.",
|
||||||
|
"error_characteristic_max": "Caractéristique déjà au maximum (3).",
|
||||||
|
"error_specialty_requirement": "Spécialité requise : compétence au moins Confirmée (≥2).",
|
||||||
|
"error_reserve_max": "Réserve déjà au maximum (10).",
|
||||||
|
"experience_transfer": "Dés d'Expérience",
|
||||||
|
"experience_pull": "Puiser du Groupe",
|
||||||
|
"experience_give": "Donner 1D au Groupe",
|
||||||
|
"pool_experience": "Expérience",
|
||||||
|
"error_no_group": "Aucun Groupe associé à ce personnage.",
|
||||||
|
"error_no_group_dice": "Réserve de Groupe insuffisante.",
|
||||||
"error_select_skill": "Veuillez sélectionner une compétence",
|
"error_select_skill": "Veuillez sélectionner une compétence",
|
||||||
"needed": "nécessaire",
|
"needed": "nécessaire",
|
||||||
"cost": "Coût",
|
"cost": "Coût",
|
||||||
@@ -259,7 +381,8 @@
|
|||||||
"totem": "Totem et ajustements",
|
"totem": "Totem et ajustements",
|
||||||
"combat": "Combat et reserves",
|
"combat": "Combat et reserves",
|
||||||
"equipment": "Matériel",
|
"equipment": "Matériel",
|
||||||
"stories": "Histoire"
|
"stories": "Histoire",
|
||||||
|
"experience": "Expérience"
|
||||||
},
|
},
|
||||||
"ammo": "Munitions",
|
"ammo": "Munitions",
|
||||||
"ammo_sm": "Mun",
|
"ammo_sm": "Mun",
|
||||||
@@ -281,6 +404,7 @@
|
|||||||
"type_character": "Personnage",
|
"type_character": "Personnage",
|
||||||
"type_npc": "PNJ",
|
"type_npc": "PNJ",
|
||||||
"type_creature": "Créature",
|
"type_creature": "Créature",
|
||||||
|
"type_community": "Communauté",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"group_members": "Membres",
|
"group_members": "Membres",
|
||||||
"encounters": "Rencontres",
|
"encounters": "Rencontres",
|
||||||
@@ -314,6 +438,7 @@
|
|||||||
"total_damage": "Dégâts totaux",
|
"total_damage": "Dégâts totaux",
|
||||||
"wound_thresholds": "Seuils de blessures",
|
"wound_thresholds": "Seuils de blessures",
|
||||||
"CharacterNamePlaceholder": "Nom du personnage",
|
"CharacterNamePlaceholder": "Nom du personnage",
|
||||||
|
"ProfilePlaceholder": "Profil",
|
||||||
"sexes": {
|
"sexes": {
|
||||||
"female": "Féminin",
|
"female": "Féminin",
|
||||||
"male": "Masculin"
|
"male": "Masculin"
|
||||||
@@ -325,7 +450,8 @@
|
|||||||
"character": "Personnage",
|
"character": "Personnage",
|
||||||
"npc": "PNJ",
|
"npc": "PNJ",
|
||||||
"group": "Groupe",
|
"group": "Groupe",
|
||||||
"creature": "Créature"
|
"creature": "Créature",
|
||||||
|
"community": "Communauté"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"UI": {
|
"UI": {
|
||||||
@@ -343,6 +469,91 @@
|
|||||||
"inactive": "Effets inactifs"
|
"inactive": "Effets inactifs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"COMMUNITY": {
|
||||||
|
"tabs": {
|
||||||
|
"identity": "Identité",
|
||||||
|
"domains": "Domaines",
|
||||||
|
"resources": "Ressources"
|
||||||
|
},
|
||||||
|
"identity": "Identité",
|
||||||
|
"name_placeholder": "Nom de la Communauté",
|
||||||
|
"choose_totem": "Choisir le totem",
|
||||||
|
"origin": "Origine",
|
||||||
|
"theme": "Thème",
|
||||||
|
"age_label": "Âge de la Communauté",
|
||||||
|
"age": {
|
||||||
|
"recent": "Quelques jours",
|
||||||
|
"established": "Quelques mois",
|
||||||
|
"old": "Quelques années"
|
||||||
|
},
|
||||||
|
"instincts": "Instincts",
|
||||||
|
"prohibits": "Interdits",
|
||||||
|
"notes": "Notes",
|
||||||
|
"location": "Localisation",
|
||||||
|
"sector": "Secteur",
|
||||||
|
"placeType": "Type de lieu",
|
||||||
|
"landmarks": "Éléments marquants",
|
||||||
|
"territory": "Territoire",
|
||||||
|
"history": "Histoire",
|
||||||
|
"founding": "Fondation",
|
||||||
|
"events": "Événements marquants",
|
||||||
|
"history_relations": "Relations",
|
||||||
|
"objectives": "Objectifs",
|
||||||
|
"objective_major": "Objectif majeur",
|
||||||
|
"objectives_minor": "Objectifs mineurs",
|
||||||
|
"add_objective": "Ajouter un objectif",
|
||||||
|
"size_title": "Taille & Effectif",
|
||||||
|
"size_label": "Taille",
|
||||||
|
"sizeLevels": {
|
||||||
|
"1": "Taille 1",
|
||||||
|
"2": "Taille 2",
|
||||||
|
"3": "Taille 3"
|
||||||
|
},
|
||||||
|
"population": "Effectif",
|
||||||
|
"population_range": "Population recommandée",
|
||||||
|
"figures": "Figures",
|
||||||
|
"companies": "Compagnies",
|
||||||
|
"domains": {
|
||||||
|
"resources": "Ressources",
|
||||||
|
"security": "Sécurité",
|
||||||
|
"dynamism": "Dynamisme",
|
||||||
|
"relations": "Rapports"
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"vivre": "Vivres",
|
||||||
|
"supplies": "Fournitures",
|
||||||
|
"defense": "Défense",
|
||||||
|
"care": "Soins",
|
||||||
|
"cohesion": "Cohésion",
|
||||||
|
"productivity": "Productivité",
|
||||||
|
"human": "Humain",
|
||||||
|
"ecology": "Écologie"
|
||||||
|
},
|
||||||
|
"states": {
|
||||||
|
"crisis": "Crise",
|
||||||
|
"low": "Bas",
|
||||||
|
"stable": "Stable",
|
||||||
|
"opulent": "Opulent"
|
||||||
|
},
|
||||||
|
"reserves": "Réserves",
|
||||||
|
"group_reserve": "Réserve de Groupe",
|
||||||
|
"material_dice": "Dés de Matériel",
|
||||||
|
"experience_dice": "Dés d'Expérience",
|
||||||
|
"morale": "Moral",
|
||||||
|
"morale_value": "Valeur",
|
||||||
|
"morale_state": "État",
|
||||||
|
"morale_levels": {
|
||||||
|
"high": "Haut",
|
||||||
|
"normal": "Normal",
|
||||||
|
"low": "Bas",
|
||||||
|
"crisis": "Crise"
|
||||||
|
},
|
||||||
|
"morale_blocked": "Les quatre Domaines sont Bas : le Bonus de Moral est inactif.",
|
||||||
|
"habitants_title": "Habitants",
|
||||||
|
"habitants_coming": "La gestion des Figures et des Compagnies arrive dans une prochaine version.",
|
||||||
|
"linked_group": "Groupe rattaché",
|
||||||
|
"profile_hint": "Profil de la Communauté"
|
||||||
|
},
|
||||||
"ITEMS": {
|
"ITEMS": {
|
||||||
"item": "Objet",
|
"item": "Objet",
|
||||||
"items": "Objets",
|
"items": "Objets",
|
||||||
@@ -432,6 +643,7 @@
|
|||||||
"major": "Majeur"
|
"major": "Majeur"
|
||||||
},
|
},
|
||||||
"SIZE_LEVELS": {
|
"SIZE_LEVELS": {
|
||||||
|
"tiny": "Insecte",
|
||||||
"small": "Petit",
|
"small": "Petit",
|
||||||
"medium": "Moyen",
|
"medium": "Moyen",
|
||||||
"large": "Grand"
|
"large": "Grand"
|
||||||
@@ -595,7 +807,7 @@
|
|||||||
"name": "Répulsion"
|
"name": "Répulsion"
|
||||||
},
|
},
|
||||||
"tracks": {
|
"tracks": {
|
||||||
"name": "Pistage"
|
"name": "Traces"
|
||||||
},
|
},
|
||||||
"crafting": {
|
"crafting": {
|
||||||
"name": "Artisanat"
|
"name": "Artisanat"
|
||||||
@@ -616,7 +828,7 @@
|
|||||||
"name": "Armes à feu"
|
"name": "Armes à feu"
|
||||||
},
|
},
|
||||||
"archery": {
|
"archery": {
|
||||||
"name": "Tir à l'arc"
|
"name": "Armes de Tir"
|
||||||
},
|
},
|
||||||
"armory": {
|
"armory": {
|
||||||
"name": "Armurerie"
|
"name": "Armurerie"
|
||||||
|
|||||||
+41
-12
@@ -31,7 +31,7 @@
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
.char-name-value {
|
.char-name-value {
|
||||||
font-size: 1.5rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
input:not([type="radio"]):not([type="checkbox"]) {
|
input:not([type="radio"]):not([type="checkbox"]) {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@@ -106,6 +106,10 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> nav.tabs {
|
> nav.tabs {
|
||||||
@@ -291,7 +295,7 @@
|
|||||||
|
|
||||||
.chooseTotem {
|
.chooseTotem {
|
||||||
font-family: "DistressBlack", sans-serif;
|
font-family: "DistressBlack", sans-serif;
|
||||||
font-size: 0.9rem;
|
font-size: 0.8rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -331,21 +335,39 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.padding-with-frieze {
|
.padding-with-frieze {
|
||||||
margin-left: 18% !important;
|
padding: 0.25rem;
|
||||||
margin-right: 10% !important;
|
|
||||||
|
|
||||||
li {
|
li {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.major-totem {
|
.resource-label {
|
||||||
position: relative;
|
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
position: absolute;
|
font-size: 1em;
|
||||||
transform: rotate(-8deg);
|
}
|
||||||
opacity: 1;
|
}
|
||||||
transition: 0.2s;
|
|
||||||
|
.major-totem {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
padding: 0.6rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.12);
|
||||||
|
border: 2px groove #eeede0;
|
||||||
|
|
||||||
|
.flexcol {
|
||||||
|
gap: 0.3rem;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-totem {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
filter: drop-shadow(0px 0px 10px rgb(0, 0, 0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +399,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.char-vermine2047 {
|
.char-vermine2047 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,6 +431,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Onglet Histoire : doubler la hauteur des zones Notes et Relations ──
|
||||||
|
.tab.stories {
|
||||||
|
prose-mirror.editor {
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nav.tabs[data-group="sheet"] {
|
nav.tabs[data-group="sheet"] {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
@import "../variables";
|
||||||
|
@import "../utilities";
|
||||||
|
|
||||||
|
.system-vermine2047 .vermine2047.actor.community {
|
||||||
|
.char-header {
|
||||||
|
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
|
||||||
|
max-height: 130px;
|
||||||
|
|
||||||
|
.community-portrait {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-name,
|
||||||
|
.char-vermine2047 {
|
||||||
|
border-bottom: none;
|
||||||
|
line-height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-vermine2047 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile,
|
||||||
|
.origin,
|
||||||
|
.level,
|
||||||
|
.population {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level input,
|
||||||
|
.population input {
|
||||||
|
width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.community-section {
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid @color-border-dark-4;
|
||||||
|
background: fade(@color-chitin-dark, 20%);
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-family: "DistressBlack", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-13;
|
||||||
|
color: @theme-color-dark;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.community-linked-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px dashed @color-hemolymph;
|
||||||
|
background: fade(@color-hemolymph, 8%);
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-family: "DistressBlack", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
color: @color-amber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.community-size-info {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
color: @color-text-light-0;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.community-warning {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
font-weight: 700;
|
||||||
|
color: @color-hemolymph;
|
||||||
|
background: fade(@color-hemolymph, 12%);
|
||||||
|
border: 1px solid @color-hemolymph;
|
||||||
|
}
|
||||||
|
|
||||||
|
.community-hint {
|
||||||
|
margin: 0;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
font-style: italic;
|
||||||
|
color: @color-text-light-0;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jauges de Domaines
|
||||||
|
.gauge {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid @color-border-dark-4;
|
||||||
|
border-left: 4px solid @color-border-dark-4;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-family: "DistressBlack", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
color: @color-amber;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"] {
|
||||||
|
width: 48px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-value {
|
||||||
|
font-family: "DistressBlack", sans-serif;
|
||||||
|
font-size: @font-size-16;
|
||||||
|
color: @color-text-light-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-state {
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.crisis { border-left-color: @color-hemolymph; .gauge-state { color: @color-hemolymph; } }
|
||||||
|
&.low { border-left-color: @color-amber; .gauge-state { color: @color-amber; } }
|
||||||
|
&.stable { border-left-color: @color-acid-green; .gauge-state { color: @color-acid-green; } }
|
||||||
|
&.opulent { border-left-color: @theme-color-accent; .gauge-state { color: @theme-color-accent; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
.morale-level {
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: @font-size-12;
|
||||||
|
font-weight: 700;
|
||||||
|
|
||||||
|
&.high { color: @color-acid-green; }
|
||||||
|
&.normal { color: @color-text-light-0; }
|
||||||
|
&.low { color: @color-amber; }
|
||||||
|
&.crisis { color: @color-hemolymph; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
-29
@@ -19,17 +19,39 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
div.minor-totems {
|
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);
|
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 {
|
h5 {
|
||||||
position: absolute;
|
margin: 0;
|
||||||
top: 0;
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: 2rem;
|
max-width: 1.5rem;
|
||||||
position: absolute;
|
height: auto;
|
||||||
bottom: -2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.human, &.adapted {
|
&.human, &.adapted {
|
||||||
@@ -53,44 +75,31 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.totem-dice {
|
.totem-dice {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.human-dice,
|
.human-dice,
|
||||||
.adapted-dice {
|
.adapted-dice {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
margin-left: 2rem;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.human-dice i,
|
.human-dice i,
|
||||||
.adapted-dice i {
|
.adapted-dice i {
|
||||||
padding-top: 0.5rem;
|
|
||||||
color: @totem-human-color;
|
color: @totem-human-color;
|
||||||
position: relative;
|
|
||||||
z-index: 0;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.adapted-dice {
|
.adapted-dice {
|
||||||
justify-content: flex-end;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 2rem;
|
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
|
|
||||||
|
i {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
color: @totem-adapted-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.adapted-dice i {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
padding-top: 0.5rem;
|
|
||||||
color: @totem-adapted-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.human {
|
|
||||||
left: 0;
|
|
||||||
img { left: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.adapted {
|
|
||||||
right: 0;
|
|
||||||
img { right: 0; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+502
-3
@@ -12,6 +12,65 @@ body.system-vermine2047 {
|
|||||||
color: @theme-color-secondary;
|
color: @theme-color-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override Foundry Core default CSS custom properties
|
||||||
|
// for better readability on the dark Vermine2047 theme.
|
||||||
|
|
||||||
|
// AppV2 windows (sheets) use .themed.theme-dark on their content
|
||||||
|
.system-vermine2047 .themed.theme-dark {
|
||||||
|
--color-text-primary: #888888;
|
||||||
|
--color-text-emphatic: #999999;
|
||||||
|
--color-text-secondary: #777777;
|
||||||
|
--color-text-subtle: #666666;
|
||||||
|
--color-text-light-highlight: #888888;
|
||||||
|
--color-text-light-heading: #777777;
|
||||||
|
--color-text-light-primary: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old-style Application windows (settings-config, dialogs) inherit from body.theme-dark
|
||||||
|
body.system-vermine2047.theme-dark {
|
||||||
|
--color-text-primary: #888888;
|
||||||
|
--color-text-emphatic: #999999;
|
||||||
|
--color-text-secondary: #777777;
|
||||||
|
--color-text-subtle: #666666;
|
||||||
|
--color-text-light-highlight: #888888;
|
||||||
|
--color-text-light-heading: #777777;
|
||||||
|
--color-text-light-primary: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings-config button,
|
||||||
|
#settings-config a.button {
|
||||||
|
--button-text-color: #000000;
|
||||||
|
--button-border-color: #666666;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings-config p,
|
||||||
|
#settings-config .hint {
|
||||||
|
color: #777777;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
@@ -56,6 +115,22 @@ ul.unstyled {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Les fenêtres de choix (totem, acteur) peuvent dépasser la hauteur fixe :
|
||||||
|
// on autorise le défilement pour accéder à la totalité du contenu.
|
||||||
|
.application#TOTEM_PICKER .window-content,
|
||||||
|
.application#ACTOR_PICKER .window-content,
|
||||||
|
.application#TRAIT_SELECTOR .window-content {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// La fiche de groupe contient beaucoup de zones (membres, objectifs, totem…) :
|
||||||
|
// dès que le contenu dépasse la hauteur de la fenêtre, on autorise le défilement
|
||||||
|
// pour ne pas perdre l'accès aux zones du bas.
|
||||||
|
.application.sheet.vermine2047.group .window-content,
|
||||||
|
.application.sheet.vermine2047.community .window-content {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.window-content .row {
|
.window-content .row {
|
||||||
&.smb { margin-bottom: 0.25rem; }
|
&.smb { margin-bottom: 0.25rem; }
|
||||||
&.mdb { margin-bottom: 0.5rem; }
|
&.mdb { margin-bottom: 0.5rem; }
|
||||||
@@ -72,6 +147,29 @@ ul.unstyled {
|
|||||||
|
|
||||||
.rollable {
|
.rollable {
|
||||||
.transition();
|
.transition();
|
||||||
|
|
||||||
|
// Affordance légère au repos : la zone est cliquable.
|
||||||
|
i.fa-dice-d10,
|
||||||
|
i.fa-dice {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: @color-acid-green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sur la fiche créature, les valeurs lançables (Attaque…) sont fortement
|
||||||
|
// matérialisées : pastille de dé + bordure pointillée.
|
||||||
|
.application.sheet.vermine2047.creature {
|
||||||
|
.rollable {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: @theme-color-light;
|
||||||
|
border: 1px dashed fade(@color-acid-green, 45%);
|
||||||
|
background: fade(@color-acid-green, 8%);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img.profile-img {
|
img.profile-img {
|
||||||
@@ -379,7 +477,7 @@ body.system-vermine2047 img#logo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item-name {
|
.item-name {
|
||||||
padding-left: 5px;
|
padding: 0 2px 0 5px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -633,6 +731,296 @@ ol.chat-log {
|
|||||||
color: @color-amber;
|
color: @color-amber;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Verdict ───────────────────────────────────────────────────────
|
||||||
|
.roll-verdict {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 6px 0;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: "DistressBlack";
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-16;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
color: @color-acid-green;
|
||||||
|
background: fade(@color-acid-green, 15%);
|
||||||
|
border: 1px solid @color-acid-green;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.failure {
|
||||||
|
color: @color-hemolymph;
|
||||||
|
background: fade(@color-hemolymph, 15%);
|
||||||
|
border: 1px solid @color-hemolymph;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Contexte du jet ───────────────────────────────────────────────
|
||||||
|
div.roll-context {
|
||||||
|
gap: 6px 12px;
|
||||||
|
margin: 4px 0 6px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.context-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
.context-label {
|
||||||
|
font-family: "DistressBlack";
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-value {
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: @font-size-12;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.totem-human .context-value { color: @totem-human-color; }
|
||||||
|
&.totem-adapted .context-value { color: @totem-adapted-color; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Arme du jet ───────────────────────────────────────────────────
|
||||||
|
div.weapon-block {
|
||||||
|
margin: 4px 0 6px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid @color-hemolymph;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.weapon-block-header {
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
|
||||||
|
.weapon-icon {
|
||||||
|
color: @color-hemolymph;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weapon-name {
|
||||||
|
font-family: "DistressBlack";
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-12;
|
||||||
|
font-weight: 900;
|
||||||
|
color: @theme-color-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weapon-targets {
|
||||||
|
margin-left: auto;
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
color: @theme-color-accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.weapon-details {
|
||||||
|
gap: 4px 12px;
|
||||||
|
|
||||||
|
.weapon-detail {
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Échange de coups ─────────────────────────────────────────────
|
||||||
|
div.exchange-actions {
|
||||||
|
margin: 4px 0 6px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px dashed @color-hemolymph;
|
||||||
|
background: fade(@color-hemolymph, 8%);
|
||||||
|
|
||||||
|
.exchange-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-family: "DistressBlack";
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-12;
|
||||||
|
font-weight: 900;
|
||||||
|
color: @theme-color-dark;
|
||||||
|
|
||||||
|
i { color: @color-hemolymph; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-target {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 3px 0;
|
||||||
|
|
||||||
|
.exchange-target-name {
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
color: @theme-color-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.exchange-defend {
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
padding: 2px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: @color-chitin-dark;
|
||||||
|
background: @color-text-light-0;
|
||||||
|
border: 1px solid @color-border-dark-4;
|
||||||
|
border-radius: 3px;
|
||||||
|
.transition();
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @color-text-light-0;
|
||||||
|
background: @color-hemolymph;
|
||||||
|
border-color: @color-hemolymph;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-passive {
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
color: @color-amber;
|
||||||
|
opacity: 0.85;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.exchange-resolve {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-family: "DistressBlack";
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
padding: 3px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: @color-text-light-0;
|
||||||
|
background: fade(@color-hemolymph, 25%);
|
||||||
|
border: 1px solid @color-hemolymph;
|
||||||
|
border-radius: 3px;
|
||||||
|
.transition();
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @theme-color-dark;
|
||||||
|
background: @color-hemolymph;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.exchange-resolution {
|
||||||
|
margin: 4px 0 6px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid @color-amber;
|
||||||
|
background: fade(@color-amber, 8%);
|
||||||
|
|
||||||
|
.resolution-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-family: "DistressBlack";
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-12;
|
||||||
|
font-weight: 900;
|
||||||
|
color: @theme-color-dark;
|
||||||
|
|
||||||
|
i { color: @color-amber; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.resolution-line {
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
padding: 1px 0;
|
||||||
|
|
||||||
|
strong { font-weight: 700; }
|
||||||
|
|
||||||
|
.res-hit { color: @color-hemolymph; font-weight: 700; }
|
||||||
|
.res-dodged,
|
||||||
|
.res-parried { color: @color-acid-green; font-weight: 700; }
|
||||||
|
&.res-wound { color: @color-hemolymph; font-weight: 700; }
|
||||||
|
&.res-noshock,
|
||||||
|
&.res-nodamage { color: @color-acid-green; }
|
||||||
|
}
|
||||||
|
|
||||||
|
button.exchange-apply {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-family: "DistressBlack";
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
padding: 3px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: @color-text-light-0;
|
||||||
|
background: fade(@color-hemolymph, 25%);
|
||||||
|
border: 1px solid @color-hemolymph;
|
||||||
|
border-radius: 3px;
|
||||||
|
.transition();
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
color: @theme-color-dark;
|
||||||
|
background: @color-hemolymph;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: default;
|
||||||
|
color: @color-acid-green;
|
||||||
|
border-color: @color-acid-green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Composition du jet ────────────────────────────────────────────
|
||||||
|
div.roll-pool {
|
||||||
|
margin: 4px 0;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-family: "DistressBlack";
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
margin: 4px 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-breakdown {
|
||||||
|
gap: 4px 10px;
|
||||||
|
|
||||||
|
.pool-item {
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
|
||||||
|
&.pool-total {
|
||||||
|
font-weight: 700;
|
||||||
|
color: @theme-color-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Résumé des dés ────────────────────────────────────────────────
|
||||||
|
div.roll-dice-summary {
|
||||||
|
gap: 6px 12px;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 4px 0 2px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.die-summary {
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
|
||||||
|
&.human { color: @totem-human-color; }
|
||||||
|
&.adapted { color: @totem-adapted-color; }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div.item-card header img {
|
div.item-card header img {
|
||||||
@@ -640,11 +1028,122 @@ ol.chat-log {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Padding supplémentaire pour les fiches groupe, npc et créature
|
// Padding supplémentaire pour les fiches groupe, npc, créature et communauté
|
||||||
.system-vermine2047 {
|
.system-vermine2047 {
|
||||||
.group-content,
|
.group-content,
|
||||||
.npc-content,
|
.npc-content,
|
||||||
.creature-content {
|
.creature-content,
|
||||||
|
.community-content {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Onglet Expérience ─────────────────────────────────────────────────
|
||||||
|
.tab.experience {
|
||||||
|
.experience-summary {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.exp-stat {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid @color-border-dark-4;
|
||||||
|
background: fade(@color-chitin-dark, 30%);
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-family: "DistressBlack", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
color: @color-amber;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exp-value {
|
||||||
|
font-family: "DistressBlack", sans-serif;
|
||||||
|
font-size: @font-size-24;
|
||||||
|
color: @color-acid-green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.exp-actions {
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-family: "DistressBlack", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-12;
|
||||||
|
padding: 5px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: @color-text-light-0;
|
||||||
|
background: fade(@color-hemolymph, 20%);
|
||||||
|
border: 1px solid @color-hemolymph;
|
||||||
|
border-radius: 3px;
|
||||||
|
.transition();
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @theme-color-dark;
|
||||||
|
background: @color-hemolymph;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.exp-rewards {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid @color-border-dark-4;
|
||||||
|
background: fade(@color-chitin-dark, 20%);
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-family: "DistressBlack", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-12;
|
||||||
|
color: @theme-color-dark;
|
||||||
|
margin: 0 0 6px;
|
||||||
|
|
||||||
|
.exp-total {
|
||||||
|
color: @color-amber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.exp-reward-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: @font-size-12;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-family: "DistressBlack", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
color: @color-amber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.exp-learned {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
border: 1px solid @color-border-dark-4;
|
||||||
|
border-radius: 3px;
|
||||||
|
opacity: 0.6;
|
||||||
|
|
||||||
|
&.learned {
|
||||||
|
opacity: 1;
|
||||||
|
color: @color-acid-green;
|
||||||
|
border-color: @color-acid-green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -399,6 +399,254 @@ iframe {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Combat dialog (attaque) ──────────────────────────────────────
|
||||||
|
.combat-mode-label {
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: @font-size-14;
|
||||||
|
color: @color-hemolymph;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locked-value {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: @font-size-12;
|
||||||
|
color: @color-chitin-dark;
|
||||||
|
background: @color-text-light-0;
|
||||||
|
border-radius: 3px;
|
||||||
|
.custom-select-style();
|
||||||
|
}
|
||||||
|
|
||||||
|
.combat-dialog-weapon {
|
||||||
|
border: 1px solid @color-hemolymph;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: fade(@color-chitin-dark, 40%);
|
||||||
|
|
||||||
|
.weapon-block-header {
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.weapon-icon { color: @color-hemolymph; }
|
||||||
|
|
||||||
|
.weapon-name {
|
||||||
|
font-family: "DistressBlack", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-12;
|
||||||
|
font-weight: 900;
|
||||||
|
color: @theme-color-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.weapon-details {
|
||||||
|
gap: 4px 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.weapon-detail {
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
color: @color-text-light-0;
|
||||||
|
|
||||||
|
strong { text-transform: uppercase; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.combat-difficulty-options {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.difficulty-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
text-transform: none;
|
||||||
|
font-size: @font-size-12;
|
||||||
|
color: @color-text-light-0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
input[type="radio"] { flex: none; }
|
||||||
|
|
||||||
|
&.locked {
|
||||||
|
opacity: 0.8;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.combat-targets {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 2px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: @font-size-11;
|
||||||
|
color: @color-amber;
|
||||||
|
}
|
||||||
|
|
||||||
|
.combat-target-tag {
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
color: @color-text-light-0;
|
||||||
|
background: fade(@color-hemolymph, 20%);
|
||||||
|
border: 1px solid fade(@color-hemolymph, 50%);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Phase d'Expérience ───────────────────────────────────────────
|
||||||
|
.exp-phase-characters {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
details > div:not(summary) {
|
||||||
|
padding: 8px;
|
||||||
|
background: fade(@color-chitin-dark, 30%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dialogue d'apprentissage ─────────────────────────────────────
|
||||||
|
.learning-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid @color-hemolymph;
|
||||||
|
background: fade(@color-hemolymph, 10%);
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: @font-size-12;
|
||||||
|
color: @color-amber;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exp-value {
|
||||||
|
font-family: "DistressBlack", sans-serif;
|
||||||
|
font-size: @font-size-20;
|
||||||
|
color: @color-acid-green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.learning-type {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 2px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
text-transform: none;
|
||||||
|
font-size: @font-size-12;
|
||||||
|
color: @color-text-light-0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
input[type="radio"] { flex: none; }
|
||||||
|
|
||||||
|
&:hover { color: @color-amber; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.learning-targets {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid @color-border-dark-4;
|
||||||
|
background: fade(@color-chitin-dark, 30%);
|
||||||
|
|
||||||
|
.learning-hint {
|
||||||
|
margin: 0;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
font-style: italic;
|
||||||
|
color: @color-text-light-0;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.learning-result {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 16px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid @color-amber;
|
||||||
|
background: fade(@color-amber, 8%);
|
||||||
|
|
||||||
|
.context-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.context-label {
|
||||||
|
font-family: "DistressBlack", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
color: @color-amber;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-value {
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: @font-size-13;
|
||||||
|
font-weight: 700;
|
||||||
|
color: @color-text-light-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Transfert de Dés d'Expérience ─────────────────────────────────
|
||||||
|
.experience-section {
|
||||||
|
.experience-controls {
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.experience-group {
|
||||||
|
font-size: @font-size-11;
|
||||||
|
color: @color-text-light-0;
|
||||||
|
opacity: 0.8;
|
||||||
|
|
||||||
|
#exp-group {
|
||||||
|
font-weight: 700;
|
||||||
|
color: @color-acid-green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button[data-action="giveToGroup"] {
|
||||||
|
font-family: "DistressBlack", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
padding: 3px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: @color-text-light-0;
|
||||||
|
background: fade(@color-acid-green, 15%);
|
||||||
|
border: 1px solid @color-acid-green;
|
||||||
|
border-radius: 3px;
|
||||||
|
.transition();
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
color: @theme-color-dark;
|
||||||
|
background: @color-acid-green;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Total dice pool section ─────────────────────────────────────
|
// ── Total dice pool section ─────────────────────────────────────
|
||||||
.total-section {
|
.total-section {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
@import "actor/npc";
|
@import "actor/npc";
|
||||||
@import "actor/group";
|
@import "actor/group";
|
||||||
@import "actor/creature";
|
@import "actor/creature";
|
||||||
|
@import "actor/community";
|
||||||
|
|
||||||
// 6. Styles des items
|
// 6. Styles des items
|
||||||
@import "items";
|
@import "items";
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export { default as VermineCharacterSheetV2 } from "./character-sheet.mjs"
|
|||||||
export { default as VermineNpcSheetV2 } from "./npc-sheet.mjs"
|
export { default as VermineNpcSheetV2 } from "./npc-sheet.mjs"
|
||||||
export { default as VermineGroupSheetV2 } from "./group-sheet.mjs"
|
export { default as VermineGroupSheetV2 } from "./group-sheet.mjs"
|
||||||
export { default as VermineCreatureSheetV2 } from "./creature-sheet.mjs"
|
export { default as VermineCreatureSheetV2 } from "./creature-sheet.mjs"
|
||||||
|
export { default as VermineCommunitySheetV2 } from "./community-sheet.mjs"
|
||||||
export {
|
export {
|
||||||
VermineItemSheetV2,
|
VermineItemSheetV2,
|
||||||
VermineWeaponSheetV2,
|
VermineWeaponSheetV2,
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
|
|||||||
toggleSheet: VermineBaseActorSheet.#onToggleSheet,
|
toggleSheet: VermineBaseActorSheet.#onToggleSheet,
|
||||||
edit: VermineBaseActorSheet.#onItemEdit,
|
edit: VermineBaseActorSheet.#onItemEdit,
|
||||||
delete: VermineBaseActorSheet.#onItemDelete,
|
delete: VermineBaseActorSheet.#onItemDelete,
|
||||||
|
toggleEquip: VermineBaseActorSheet.#onToggleEquip,
|
||||||
create: VermineBaseActorSheet.#onItemCreate,
|
create: VermineBaseActorSheet.#onItemCreate,
|
||||||
roll: VermineBaseActorSheet.#onRollItem,
|
roll: VermineBaseActorSheet.#onRollItem,
|
||||||
|
attack: VermineBaseActorSheet.#onAttack,
|
||||||
clickRadio: VermineBaseActorSheet.#onClickRadioHexa,
|
clickRadio: VermineBaseActorSheet.#onClickRadioHexa,
|
||||||
effectControl: VermineBaseActorSheet.#onEffectControl,
|
effectControl: VermineBaseActorSheet.#onEffectControl,
|
||||||
chooseTotem: VermineBaseActorSheet.#onChooseTotem
|
chooseTotem: VermineBaseActorSheet.#onChooseTotem
|
||||||
@@ -114,7 +116,8 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
|
|||||||
isEditable: this.isEditable,
|
isEditable: this.isEditable,
|
||||||
enrichedNotes: await enrich("identity.notes"),
|
enrichedNotes: await enrich("identity.notes"),
|
||||||
enrichedBiography: await enrich("identity.biography"),
|
enrichedBiography: await enrich("identity.biography"),
|
||||||
enrichedRelations: await enrich("identity.relations")
|
enrichedRelations: await enrich("identity.relations"),
|
||||||
|
enrichedEquipmentNotes: await enrich("equipment.description")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,9 +233,39 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
|
|||||||
const id = target.closest("[data-item-id]")?.dataset?.itemId
|
const id = target.closest("[data-item-id]")?.dataset?.itemId
|
||||||
if (!id) return
|
if (!id) return
|
||||||
const item = this.document.items.get(id)
|
const item = this.document.items.get(id)
|
||||||
|
if (!item) return
|
||||||
|
if (item.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()
|
item?.roll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async #onAttack(event, target) {
|
||||||
|
const id = target.closest("[data-item-id]")?.dataset?.itemId
|
||||||
|
if (!id) return
|
||||||
|
const item = this.document.items.get(id)
|
||||||
|
if (!item) return
|
||||||
|
const { default: CombatDialog } = await import("../../system/dialogs/combatDialog.mjs")
|
||||||
|
const dialog = await CombatDialog.create({ actorId: this.document.id, weapon: item })
|
||||||
|
if (dialog) dialog.render(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onToggleEquip(event, target) {
|
||||||
|
const id = target.closest("[data-item-id]")?.dataset?.itemId
|
||||||
|
if (!id) return
|
||||||
|
const item = this.document.items.get(id)
|
||||||
|
if (!item) return
|
||||||
|
await item.update({ "system.equipped": !item.system.equipped })
|
||||||
|
}
|
||||||
|
|
||||||
static #onClickRadioHexa(event, target) {
|
static #onClickRadioHexa(event, target) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export class VermineBaseItemSheet extends HandlebarsApplicationMixin(foundry.app
|
|||||||
// ── Rendu ───────────────────────────────────────────────────────────
|
// ── Rendu ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
_onRender(context, options) {
|
_onRender(context, options) {
|
||||||
|
super._onRender(context, options);
|
||||||
this.#dragDrop.forEach(d => d.bind(this.element))
|
this.#dragDrop.forEach(d => d.bind(this.element))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,18 +7,21 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
|||||||
position: { width: 860, height: 720 },
|
position: { width: 860, height: 720 },
|
||||||
window: { contentClasses: ["character-content"] },
|
window: { contentClasses: ["character-content"] },
|
||||||
actions: {
|
actions: {
|
||||||
addSpecialty: VermineCharacterSheetV2.#onAddSpecialty
|
addSpecialty: VermineCharacterSheetV2.#onAddSpecialty,
|
||||||
|
openExperiencePhase: VermineCharacterSheetV2.#onOpenExperiencePhase,
|
||||||
|
openLearning: VermineCharacterSheetV2.#onOpenLearning
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
main: { template: "systems/vermine2047/templates/actor/appv2/character-main.hbs" },
|
main: { template: "systems/vermine2047/templates/actor/appv2/character-main.hbs", scrollable: [""] },
|
||||||
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||||
abilities: { template: "systems/vermine2047/templates/actor/appv2/character-abilities.hbs" },
|
abilities: { template: "systems/vermine2047/templates/actor/appv2/character-abilities.hbs", scrollable: [""] },
|
||||||
totem: { template: "systems/vermine2047/templates/actor/appv2/character-totem.hbs" },
|
totem: { template: "systems/vermine2047/templates/actor/appv2/character-totem.hbs", scrollable: [""] },
|
||||||
equipment: { template: "systems/vermine2047/templates/actor/appv2/character-equipment.hbs" },
|
equipment: { template: "systems/vermine2047/templates/actor/appv2/character-equipment.hbs", scrollable: [""] },
|
||||||
stories: { template: "systems/vermine2047/templates/actor/appv2/character-stories.hbs" },
|
stories: { template: "systems/vermine2047/templates/actor/appv2/character-stories.hbs", scrollable: [""] },
|
||||||
combat: { template: "systems/vermine2047/templates/actor/appv2/character-combat.hbs" }
|
combat: { template: "systems/vermine2047/templates/actor/appv2/character-combat.hbs", scrollable: [""] },
|
||||||
|
experience: { template: "systems/vermine2047/templates/actor/appv2/character-experience.hbs", scrollable: [""] }
|
||||||
}
|
}
|
||||||
|
|
||||||
tabGroups = { sheet: "abilities" }
|
tabGroups = { sheet: "abilities" }
|
||||||
@@ -29,7 +32,8 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
|||||||
totem: { id: "totem", group: "sheet", icon: "fas fa-star", label: "VERMINE.tabs.totem" },
|
totem: { id: "totem", group: "sheet", icon: "fas fa-star", label: "VERMINE.tabs.totem" },
|
||||||
equipment: { id: "equipment", group: "sheet", icon: "fas fa-hammer", label: "VERMINE.tabs.equipment" },
|
equipment: { id: "equipment", group: "sheet", icon: "fas fa-hammer", label: "VERMINE.tabs.equipment" },
|
||||||
stories: { id: "stories", group: "sheet", icon: "fas fa-book-open-reader", label: "VERMINE.tabs.stories" },
|
stories: { id: "stories", group: "sheet", icon: "fas fa-book-open-reader", label: "VERMINE.tabs.stories" },
|
||||||
combat: { id: "combat", group: "sheet", icon: "fas fa-medal", label: "VERMINE.tabs.combat" }
|
combat: { id: "combat", group: "sheet", icon: "fas fa-medal", label: "VERMINE.tabs.combat" },
|
||||||
|
experience: { id: "experience", group: "sheet", icon: "fas fa-graduation-cap", label: "VERMINE.tabs.experience" }
|
||||||
}
|
}
|
||||||
for (const v of Object.values(tabs)) {
|
for (const v of Object.values(tabs)) {
|
||||||
v.active = this.tabGroups[v.group] === v.id
|
v.active = this.tabGroups[v.group] === v.id
|
||||||
@@ -51,6 +55,7 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
|||||||
case "main": break
|
case "main": break
|
||||||
case "abilities":
|
case "abilities":
|
||||||
context.tab = context.tabs.abilities
|
context.tab = context.tabs.abilities
|
||||||
|
context.specialties = doc.itemTypes.specialty
|
||||||
break
|
break
|
||||||
case "totem":
|
case "totem":
|
||||||
context.tab = context.tabs.totem
|
context.tab = context.tabs.totem
|
||||||
@@ -73,9 +78,21 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
|||||||
break
|
break
|
||||||
case "combat":
|
case "combat":
|
||||||
context.tab = context.tabs.combat
|
context.tab = context.tabs.combat
|
||||||
|
context.equippedWeapons = doc.itemTypes.weapon.filter(i => i.system.equipped)
|
||||||
|
context.equippedDefenses = doc.itemTypes.defense.filter(i => i.system.equipped)
|
||||||
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
|
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
|
||||||
context.effects = prepareActiveEffectCategories(doc.effects)
|
context.effects = prepareActiveEffectCategories(doc.effects)
|
||||||
break
|
break
|
||||||
|
case "experience":
|
||||||
|
context.tab = context.tabs.experience
|
||||||
|
context.specialtyCount = doc.itemTypes.specialty.length
|
||||||
|
const ex = doc.system.experience
|
||||||
|
context.experience = ex
|
||||||
|
context.collectiveTotal = (ex.collective.danger + ex.collective.discoveries + ex.collective.duration)
|
||||||
|
context.individualTotal = (ex.individual.implication + ex.individual.influence + ex.individual.interpretation)
|
||||||
|
context.currentPhase = ex.phase
|
||||||
|
context.learnedCurrent = ex.learned.phase === ex.phase
|
||||||
|
break
|
||||||
}
|
}
|
||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
@@ -95,4 +112,16 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
|||||||
if (skillKey) itemData.system = { skill: skillKey }
|
if (skillKey) itemData.system = { skill: skillKey }
|
||||||
await this.document.createEmbeddedDocuments("Item", [itemData])
|
await this.document.createEmbeddedDocuments("Item", [itemData])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async #onOpenExperiencePhase(event, target) {
|
||||||
|
const { default: ExperiencePhaseDialog } = await import("../../system/dialogs/experiencePhaseDialog.mjs")
|
||||||
|
const dialog = await ExperiencePhaseDialog.create()
|
||||||
|
if (dialog) dialog.render(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onOpenLearning(event, target) {
|
||||||
|
const { default: LearningDialog } = await import("../../system/dialogs/learningDialog.mjs")
|
||||||
|
const dialog = await LearningDialog.create({ actorId: this.document.id })
|
||||||
|
if (dialog) dialog.render(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import VermineBaseActorSheet from "./base-actor-sheet.mjs"
|
||||||
|
|
||||||
|
export default class VermineCommunitySheetV2 extends VermineBaseActorSheet {
|
||||||
|
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
classes: ["community"],
|
||||||
|
position: { width: 760, height: 640 },
|
||||||
|
window: { contentClasses: ["community-content"] },
|
||||||
|
actions: {
|
||||||
|
addObjective: VermineCommunitySheetV2.#onAddObjective,
|
||||||
|
deleteObjective: VermineCommunitySheetV2.#onDeleteObjective
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
main: { template: "systems/vermine2047/templates/actor/appv2/community-main.hbs", scrollable: [""] },
|
||||||
|
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||||
|
identity: { template: "systems/vermine2047/templates/actor/appv2/community-identity.hbs", scrollable: [""] },
|
||||||
|
domains: { template: "systems/vermine2047/templates/actor/appv2/community-domains.hbs", scrollable: [""] },
|
||||||
|
resources: { template: "systems/vermine2047/templates/actor/appv2/community-resources.hbs", scrollable: [""] }
|
||||||
|
}
|
||||||
|
|
||||||
|
tabGroups = { sheet: "identity" }
|
||||||
|
|
||||||
|
#getTabs() {
|
||||||
|
const tabs = {
|
||||||
|
identity: { id: "identity", group: "sheet", icon: "fas fa-info-circle", label: "COMMUNITY.tabs.identity" },
|
||||||
|
domains: { id: "domains", group: "sheet", icon: "fas fa-chart-simple", label: "COMMUNITY.tabs.domains" },
|
||||||
|
resources: { id: "resources", group: "sheet", icon: "fas fa-boxes-stacked", label: "COMMUNITY.tabs.resources" }
|
||||||
|
}
|
||||||
|
for (const v of Object.values(tabs)) {
|
||||||
|
v.active = this.tabGroups[v.group] === v.id
|
||||||
|
v.cssClass = v.active ? "active" : ""
|
||||||
|
}
|
||||||
|
return tabs
|
||||||
|
}
|
||||||
|
|
||||||
|
async _prepareContext() {
|
||||||
|
const context = await super._prepareContext()
|
||||||
|
context.tabs = this.#getTabs()
|
||||||
|
// Groupe de personnages rattaché
|
||||||
|
context.resolvedGroup = null
|
||||||
|
if (this.document.system.groupId) {
|
||||||
|
const g = game.actors.get(this.document.system.groupId)
|
||||||
|
if (g) context.resolvedGroup = { id: g.id, name: g.name }
|
||||||
|
}
|
||||||
|
// Enrichissement HTML des champs longs (même logique que la fiche PNJ)
|
||||||
|
const enrich = async (path) => {
|
||||||
|
const val = foundry.utils.getProperty(this.document.system, path)
|
||||||
|
return val ? await foundry.applications.ux.TextEditor.implementation.enrichHTML(val, { async: true }) : ""
|
||||||
|
}
|
||||||
|
context.enrichedNotes = await enrich("identity.notes")
|
||||||
|
context.enrichedLandmarks = await enrich("location.landmarks")
|
||||||
|
context.enrichedTerritory = await enrich("location.territory")
|
||||||
|
context.enrichedEvents = await enrich("history.events")
|
||||||
|
context.enrichedRelations = await enrich("history.relations")
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
async _preparePartContext(partId, context) {
|
||||||
|
const doc = this.document
|
||||||
|
switch (partId) {
|
||||||
|
case "main": break
|
||||||
|
case "identity":
|
||||||
|
context.tab = context.tabs.identity
|
||||||
|
context.ageOptions = Object.entries(CONFIG.VERMINE.communityAges).map(([key, label]) => ({ key, label: game.i18n.localize(label) }))
|
||||||
|
break
|
||||||
|
case "domains":
|
||||||
|
context.tab = context.tabs.domains
|
||||||
|
context.sizeOptions = CONFIG.VERMINE.communitySizeLevels
|
||||||
|
context.sizeLabel = doc.system.size?.value ? game.i18n.localize(CONFIG.VERMINE.communitySizeLevels[doc.system.size.value]?.label ?? "") : ""
|
||||||
|
context.sizeInfo = doc.system.size?.value ? CONFIG.VERMINE.communitySizeLevels[doc.system.size.value] : null
|
||||||
|
// Préparer les 4 Domaines avec leurs 2 sections (jauges) et leur état
|
||||||
|
context.domains = []
|
||||||
|
for (const [dkey, dcfg] of Object.entries(CONFIG.VERMINE.communityDomains)) {
|
||||||
|
const domain = doc.system.domains?.[dkey]
|
||||||
|
context.domains.push({
|
||||||
|
key: dkey,
|
||||||
|
label: game.i18n.localize(dcfg.label),
|
||||||
|
sections: Object.entries(dcfg.sections).map(([skey, label]) => {
|
||||||
|
const gauge = domain?.[skey] ?? { value: 0 }
|
||||||
|
const state = gauge.state || "crisis"
|
||||||
|
return {
|
||||||
|
domainKey: dkey,
|
||||||
|
key: skey,
|
||||||
|
label: game.i18n.localize(label),
|
||||||
|
gauge,
|
||||||
|
state,
|
||||||
|
stateLabel: game.i18n.localize(CONFIG.VERMINE.communityGaugeStates[state]?.label ?? "COMMUNITY.states.crisis")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "resources":
|
||||||
|
context.tab = context.tabs.resources
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
changeTab(tab, group, options = {}) {
|
||||||
|
super.changeTab(tab, group, options)
|
||||||
|
if (group === "sheet") {
|
||||||
|
const main = this.element?.querySelector('[data-group="sheet"][data-tab="main"]')
|
||||||
|
if (main) main.classList.add("active")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static #onDeleteObjective(event, target) {
|
||||||
|
const index = parseInt(target.dataset.index)
|
||||||
|
if (isNaN(index)) return
|
||||||
|
const objectives = foundry.utils.duplicate(this.document.system.objectives || { major: "", minor: [] })
|
||||||
|
objectives.minor.splice(index, 1)
|
||||||
|
this.document.update({ "system.objectives": objectives })
|
||||||
|
}
|
||||||
|
|
||||||
|
static #onAddObjective(event, target) {
|
||||||
|
const objectives = foundry.utils.duplicate(this.document.system.objectives || { major: "", minor: [] })
|
||||||
|
objectives.minor.push("")
|
||||||
|
this.document.update({ "system.objectives": objectives })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,10 @@ import VermineBaseActorSheet from "./base-actor-sheet.mjs"
|
|||||||
|
|
||||||
export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
|
export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
|
||||||
|
|
||||||
|
// La fiche créature s'ouvre en mode édition : Gabarit, Taille, Rôle et Meute
|
||||||
|
// se règlent dès l'ouverture (le mode jeu reste accessible via le toggle).
|
||||||
|
_sheetMode = this.constructor.SHEET_MODES.EDIT
|
||||||
|
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
classes: ["creature"],
|
classes: ["creature"],
|
||||||
position: { width: 700, height: 650 },
|
position: { width: 700, height: 650 },
|
||||||
@@ -9,12 +13,12 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
main: { template: "systems/vermine2047/templates/actor/appv2/creature-main.hbs" },
|
main: { template: "systems/vermine2047/templates/actor/appv2/creature-main.hbs", scrollable: [""] },
|
||||||
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||||
info: { template: "systems/vermine2047/templates/actor/appv2/creature-info.hbs" },
|
info: { template: "systems/vermine2047/templates/actor/appv2/creature-info.hbs", scrollable: [""] },
|
||||||
stats: { template: "systems/vermine2047/templates/actor/appv2/creature-stats.hbs" },
|
stats: { template: "systems/vermine2047/templates/actor/appv2/creature-stats.hbs", scrollable: [""] },
|
||||||
combat: { template: "systems/vermine2047/templates/actor/appv2/creature-combat.hbs" },
|
combat: { template: "systems/vermine2047/templates/actor/appv2/creature-combat.hbs", scrollable: [""] },
|
||||||
effects: { template: "systems/vermine2047/templates/actor/appv2/creature-effects.hbs" }
|
effects: { template: "systems/vermine2047/templates/actor/appv2/creature-effects.hbs", scrollable: [""] }
|
||||||
}
|
}
|
||||||
|
|
||||||
tabGroups = { sheet: "info" }
|
tabGroups = { sheet: "info" }
|
||||||
@@ -61,13 +65,28 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
|
|||||||
break
|
break
|
||||||
case "stats":
|
case "stats":
|
||||||
context.tab = context.tabs.stats
|
context.tab = context.tabs.stats
|
||||||
context.patternLabel = doc.system.pattern?.value ? CONFIG.VERMINE.creaturePatternLevels[doc.system.pattern.value]?.label : ""
|
context.patternLabel = doc.system.pattern?.value ? game.i18n.localize(CONFIG.VERMINE.creaturePatternLevels[doc.system.pattern.value]?.label ?? "") : ""
|
||||||
context.sizeLabel = doc.system.size?.value || ""
|
context.sizeLabel = doc.system.size?.value !== undefined ? game.i18n.localize(CONFIG.VERMINE.creatureSizeLevels[doc.system.size.value]?.label ?? "") : ""
|
||||||
context.roleLabel = doc.system.role?.value ? CONFIG.VERMINE.creatureRoleLevels[doc.system.role.value]?.label : ""
|
context.roleLabel = doc.system.role?.value ? game.i18n.localize(CONFIG.VERMINE.creatureRoleLevels[doc.system.role.value]?.label ?? "") : ""
|
||||||
context.packLabel = doc.system.pack?.value || game.i18n.localize("VERMINE.none")
|
context.packLabel = doc.system.pack?.value || game.i18n.localize("VERMINE.none")
|
||||||
|
// Contribution cumulée de la Taille (les modificateurs se cumulent : Taille N = somme 1..N)
|
||||||
|
const sizeLevels = CONFIG.VERMINE.creatureSizeLevels || {}
|
||||||
|
const sizeLevel = doc.system.size?.value ?? 0
|
||||||
|
context.sizeCumulative = { attack: 0, vigor: 0, minorWound: 0, majorWound: 0, deadlyWound: 0 }
|
||||||
|
for (let i = 1; i <= sizeLevel; i++) {
|
||||||
|
const cfg = sizeLevels[i] || {}
|
||||||
|
context.sizeCumulative.attack += cfg.attack || 0
|
||||||
|
context.sizeCumulative.vigor += cfg.vigor || 0
|
||||||
|
context.sizeCumulative.minorWound += cfg.minorWound || 0
|
||||||
|
context.sizeCumulative.majorWound += cfg.majorWound || 0
|
||||||
|
context.sizeCumulative.deadlyWound += cfg.deadlyWound || 0
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case "combat":
|
case "combat":
|
||||||
context.tab = context.tabs.combat
|
context.tab = context.tabs.combat
|
||||||
|
context.weapons = doc.itemTypes.weapon
|
||||||
|
context.equippedWeapons = doc.itemTypes.weapon.filter(i => i.system.equipped)
|
||||||
|
context.equippedDefenses = doc.itemTypes.defense.filter(i => i.system.equipped)
|
||||||
break
|
break
|
||||||
case "effects":
|
case "effects":
|
||||||
context.tab = context.tabs.effects
|
context.tab = context.tabs.effects
|
||||||
@@ -77,4 +96,15 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
|
|||||||
}
|
}
|
||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @override - Les créatures n'ont pas de caractéristiques/compétences : on ouvre le dialogue d'attaque (pool calculé). */
|
||||||
|
async _onRoll(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
const el = event.currentTarget
|
||||||
|
const type = el.dataset.type
|
||||||
|
if (type !== "creature-attack") return super._onRoll(event)
|
||||||
|
const { default: CombatDialog } = await import("../../system/dialogs/combatDialog.mjs")
|
||||||
|
const dialog = await CombatDialog.create({ actorId: this.document.id })
|
||||||
|
if (dialog) dialog.render(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,17 +11,18 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
|||||||
deleteMember: VermineGroupSheetV2.#onDeleteMember,
|
deleteMember: VermineGroupSheetV2.#onDeleteMember,
|
||||||
deleteEncounter: VermineGroupSheetV2.#onDeleteEncounter,
|
deleteEncounter: VermineGroupSheetV2.#onDeleteEncounter,
|
||||||
deleteObjective: VermineGroupSheetV2.#onDeleteObjective,
|
deleteObjective: VermineGroupSheetV2.#onDeleteObjective,
|
||||||
addObjective: VermineGroupSheetV2.#onAddObjective
|
addObjective: VermineGroupSheetV2.#onAddObjective,
|
||||||
|
createTotemAbility: VermineGroupSheetV2.#onCreateTotemAbility
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
main: { template: "systems/vermine2047/templates/actor/appv2/group-main.hbs" },
|
main: { template: "systems/vermine2047/templates/actor/appv2/group-main.hbs", scrollable: [""] },
|
||||||
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||||
info: { template: "systems/vermine2047/templates/actor/appv2/group-info.hbs" },
|
info: { template: "systems/vermine2047/templates/actor/appv2/group-info.hbs", scrollable: [""] },
|
||||||
gear: { template: "systems/vermine2047/templates/actor/appv2/group-gear.hbs" },
|
gear: { template: "systems/vermine2047/templates/actor/appv2/group-gear.hbs", scrollable: [""] },
|
||||||
road: { template: "systems/vermine2047/templates/actor/appv2/group-road.hbs" },
|
road: { template: "systems/vermine2047/templates/actor/appv2/group-road.hbs", scrollable: [""] },
|
||||||
reserve: { template: "systems/vermine2047/templates/actor/appv2/group-reserve.hbs" }
|
reserve: { template: "systems/vermine2047/templates/actor/appv2/group-reserve.hbs", scrollable: [""] }
|
||||||
}
|
}
|
||||||
|
|
||||||
tabGroups = { sheet: "info" }
|
tabGroups = { sheet: "info" }
|
||||||
@@ -67,7 +68,8 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
|||||||
case "main": break
|
case "main": break
|
||||||
case "info":
|
case "info":
|
||||||
context.tab = context.tabs.info
|
context.tab = context.tabs.info
|
||||||
context.abilities = doc.itemTypes.ability
|
context.abilities = doc.itemTypes.ability.filter(i => i.system.type !== "totem")
|
||||||
|
context.totem_abilities = doc.itemTypes.ability.filter(i => i.system.type === "totem")
|
||||||
context.specialties = doc.itemTypes.specialty
|
context.specialties = doc.itemTypes.specialty
|
||||||
context.backgrounds = doc.itemTypes.background
|
context.backgrounds = doc.itemTypes.background
|
||||||
context.traumas = doc.itemTypes.trauma
|
context.traumas = doc.itemTypes.trauma
|
||||||
@@ -135,4 +137,9 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
|||||||
objectives[type].push("")
|
objectives[type].push("")
|
||||||
this.document.update({ "system.objectives": objectives })
|
this.document.update({ "system.objectives": objectives })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async #onCreateTotemAbility(event, target) {
|
||||||
|
const name = game.i18n.localize("ITEMS.new_ability")
|
||||||
|
await this.document.createEmbeddedDocuments("Item", [{ name, type: "ability", system: { type: "totem" } }])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,71 +3,93 @@ import { VermineBaseItemSheet } from "./base-item-sheet.mjs"
|
|||||||
// ── Item générique ────────────────────────────────────────────────────
|
// ── Item générique ────────────────────────────────────────────────────
|
||||||
export class VermineItemSheetV2 extends VermineBaseItemSheet {
|
export class VermineItemSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["item-gear"], position: { width: 520 } }
|
static DEFAULT_OPTIONS = { classes: ["item-gear"], position: { width: 520 } }
|
||||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-item-sheet.hbs" } }
|
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-item-sheet.hbs", scrollable: [""] } }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Arme ──────────────────────────────────────────────────────────────
|
// ── Arme ──────────────────────────────────────────────────────────────
|
||||||
export class VermineWeaponSheetV2 extends VermineBaseItemSheet {
|
export class VermineWeaponSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["weapon"], position: { width: 520 } }
|
static DEFAULT_OPTIONS = { classes: ["weapon"], position: { width: 520 } }
|
||||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-weapon-sheet.hbs" } }
|
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-weapon-sheet.hbs", scrollable: [""] } }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Défense ───────────────────────────────────────────────────────────
|
// ── Défense ───────────────────────────────────────────────────────────
|
||||||
export class VermineDefenseSheetV2 extends VermineBaseItemSheet {
|
export class VermineDefenseSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["defense"], position: { width: 520 } }
|
static DEFAULT_OPTIONS = { classes: ["defense"], position: { width: 520 } }
|
||||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-defense-sheet.hbs" } }
|
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-defense-sheet.hbs", scrollable: [""] } }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Véhicule ──────────────────────────────────────────────────────────
|
// ── Véhicule ──────────────────────────────────────────────────────────
|
||||||
export class VermineVehicleSheetV2 extends VermineBaseItemSheet {
|
export class VermineVehicleSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["vehicle"], position: { width: 520 } }
|
static DEFAULT_OPTIONS = { classes: ["vehicle"], position: { width: 520 } }
|
||||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-vehicle-sheet.hbs" } }
|
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-vehicle-sheet.hbs", scrollable: [""] } }
|
||||||
|
|
||||||
|
async _prepareContext() {
|
||||||
|
const context = await super._prepareContext()
|
||||||
|
// Compétences d'Entretien possibles (mécanique, technologie, artisanat, animalisme…)
|
||||||
|
const maintenanceKeys = ["mecanical", "technology", "crafting", "animalism", "environment", "wildlife", "flora", "repulsion", "toxics"]
|
||||||
|
const current = this.document.system.maintenance || []
|
||||||
|
context.maintenanceSkills = maintenanceKeys.map(key => ({
|
||||||
|
key,
|
||||||
|
label: game.i18n.localize("SKILLS." + key + ".name"),
|
||||||
|
checked: current.includes(key)
|
||||||
|
}))
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @override - nettoie le tableau d'Entretien (retire les cases non cochées). */
|
||||||
|
_prepareSubmitData(event, form, formData, updateData) {
|
||||||
|
const result = super._prepareSubmitData(event, form, formData, updateData)
|
||||||
|
if (Array.isArray(result?.system?.maintenance)) {
|
||||||
|
result.system.maintenance = result.system.maintenance.filter(v => typeof v === "string" && v.trim() !== "")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Capacité ──────────────────────────────────────────────────────────
|
// ── Capacité ──────────────────────────────────────────────────────────
|
||||||
export class VermineAbilitySheetV2 extends VermineBaseItemSheet {
|
export class VermineAbilitySheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["ability"], position: { width: 560 } }
|
static DEFAULT_OPTIONS = { classes: ["ability"], position: { width: 560 } }
|
||||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-ability-sheet.hbs" } }
|
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-ability-sheet.hbs", scrollable: [""] } }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Spécialité ────────────────────────────────────────────────────────
|
// ── Spécialité ────────────────────────────────────────────────────────
|
||||||
export class VermineSpecialtySheetV2 extends VermineBaseItemSheet {
|
export class VermineSpecialtySheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["specialty"], position: { width: 400 } }
|
static DEFAULT_OPTIONS = { classes: ["specialty"], position: { width: 400 } }
|
||||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-specialty-sheet.hbs" } }
|
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-specialty-sheet.hbs", scrollable: [""] } }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Historique ────────────────────────────────────────────────────────
|
// ── Historique ────────────────────────────────────────────────────────
|
||||||
export class VermineBackgroundSheetV2 extends VermineBaseItemSheet {
|
export class VermineBackgroundSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["background"], position: { width: 520 } }
|
static DEFAULT_OPTIONS = { classes: ["background"], position: { width: 520 } }
|
||||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-background-sheet.hbs" } }
|
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-background-sheet.hbs", scrollable: [""] } }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Traumatisme ───────────────────────────────────────────────────────
|
// ── Traumatisme ───────────────────────────────────────────────────────
|
||||||
export class VermineTraumaSheetV2 extends VermineBaseItemSheet {
|
export class VermineTraumaSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["trauma"], position: { width: 520 } }
|
static DEFAULT_OPTIONS = { classes: ["trauma"], position: { width: 520 } }
|
||||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-trauma-sheet.hbs" } }
|
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-trauma-sheet.hbs", scrollable: [""] } }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Évolution ─────────────────────────────────────────────────────────
|
// ── Évolution ─────────────────────────────────────────────────────────
|
||||||
export class VermineEvolutionSheetV2 extends VermineBaseItemSheet {
|
export class VermineEvolutionSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["evolution"], position: { width: 520 } }
|
static DEFAULT_OPTIONS = { classes: ["evolution"], position: { width: 520 } }
|
||||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-evolution-sheet.hbs" } }
|
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-evolution-sheet.hbs", scrollable: [""] } }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Rumeur ────────────────────────────────────────────────────────────
|
// ── Rumeur ────────────────────────────────────────────────────────────
|
||||||
export class VermineRumorSheetV2 extends VermineBaseItemSheet {
|
export class VermineRumorSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["rumor"], position: { width: 520 } }
|
static DEFAULT_OPTIONS = { classes: ["rumor"], position: { width: 520 } }
|
||||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rumor-sheet.hbs" } }
|
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rumor-sheet.hbs", scrollable: [""] } }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Cible ─────────────────────────────────────────────────────────────
|
// ── Cible ─────────────────────────────────────────────────────────────
|
||||||
export class VermineTargetSheetV2 extends VermineBaseItemSheet {
|
export class VermineTargetSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["target"], position: { width: 520 } }
|
static DEFAULT_OPTIONS = { classes: ["target"], position: { width: 520 } }
|
||||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-target-sheet.hbs" } }
|
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-target-sheet.hbs", scrollable: [""] } }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Rite ──────────────────────────────────────────────────────────────
|
// ── Rite ──────────────────────────────────────────────────────────────
|
||||||
export class VermineRiteSheetV2 extends VermineBaseItemSheet {
|
export class VermineRiteSheetV2 extends VermineBaseItemSheet {
|
||||||
static DEFAULT_OPTIONS = { classes: ["rite"], position: { width: 520 } }
|
static DEFAULT_OPTIONS = { classes: ["rite"], position: { width: 520 } }
|
||||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rite-sheet.hbs" } }
|
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rite-sheet.hbs", scrollable: [""] } }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import VermineBaseActorSheet from "./base-actor-sheet.mjs"
|
|||||||
|
|
||||||
export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
|
export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
|
||||||
|
|
||||||
|
// La fiche PNJ s'ouvre en mode édition : caractéristiques, compétences et
|
||||||
|
// niveaux se règlent dès l'ouverture (le mode jeu reste accessible via le toggle).
|
||||||
|
_sheetMode = this.constructor.SHEET_MODES.EDIT
|
||||||
|
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
classes: ["npc"],
|
classes: ["npc"],
|
||||||
position: { width: 750, height: 680 },
|
position: { width: 750, height: 680 },
|
||||||
@@ -9,13 +13,14 @@ export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
main: { template: "systems/vermine2047/templates/actor/appv2/npc-main.hbs" },
|
main: { template: "systems/vermine2047/templates/actor/appv2/npc-main.hbs", scrollable: [""] },
|
||||||
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||||
characteristics: { template: "systems/vermine2047/templates/actor/appv2/npc-characteristics.hbs" },
|
characteristics: { template: "systems/vermine2047/templates/actor/appv2/npc-characteristics.hbs", scrollable: [""] },
|
||||||
skills: { template: "systems/vermine2047/templates/actor/appv2/npc-skills.hbs" },
|
skills: { template: "systems/vermine2047/templates/actor/appv2/npc-skills.hbs", scrollable: [""] },
|
||||||
threat: { template: "systems/vermine2047/templates/actor/appv2/npc-threat.hbs" },
|
equipment: { template: "systems/vermine2047/templates/actor/appv2/npc-equipment.hbs", scrollable: [""] },
|
||||||
combat: { template: "systems/vermine2047/templates/actor/appv2/npc-combat.hbs" },
|
threat: { template: "systems/vermine2047/templates/actor/appv2/npc-threat.hbs", scrollable: [""] },
|
||||||
notes: { template: "systems/vermine2047/templates/actor/appv2/npc-notes.hbs" }
|
combat: { template: "systems/vermine2047/templates/actor/appv2/npc-combat.hbs", scrollable: [""] },
|
||||||
|
notes: { template: "systems/vermine2047/templates/actor/appv2/npc-notes.hbs", scrollable: [""] }
|
||||||
}
|
}
|
||||||
|
|
||||||
tabGroups = { sheet: "characteristics" }
|
tabGroups = { sheet: "characteristics" }
|
||||||
@@ -24,6 +29,7 @@ export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
|
|||||||
const tabs = {
|
const tabs = {
|
||||||
characteristics: { id: "characteristics", group: "sheet", icon: "fas fa-dice", label: "VERMINE.abilities" },
|
characteristics: { id: "characteristics", group: "sheet", icon: "fas fa-dice", label: "VERMINE.abilities" },
|
||||||
skills: { id: "skills", group: "sheet", icon: "fas fa-brain", label: "VERMINE.skills" },
|
skills: { id: "skills", group: "sheet", icon: "fas fa-brain", label: "VERMINE.skills" },
|
||||||
|
equipment: { id: "equipment", group: "sheet", icon: "fas fa-hammer", label: "VERMINE.tabs.equipment" },
|
||||||
threat: { id: "threat", group: "sheet", icon: "fas fa-exclamation-triangle", label: "ADVERSITY.threat" },
|
threat: { id: "threat", group: "sheet", icon: "fas fa-exclamation-triangle", label: "ADVERSITY.threat" },
|
||||||
combat: { id: "combat", group: "sheet", icon: "fas fa-sword", label: "VERMINE.combat" },
|
combat: { id: "combat", group: "sheet", icon: "fas fa-sword", label: "VERMINE.combat" },
|
||||||
notes: { id: "notes", group: "sheet", icon: "fas fa-sticky-note", label: "IDENTITY.notes" }
|
notes: { id: "notes", group: "sheet", icon: "fas fa-sticky-note", label: "IDENTITY.notes" }
|
||||||
@@ -57,11 +63,18 @@ export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
|
|||||||
case "skills":
|
case "skills":
|
||||||
context.tab = context.tabs.skills
|
context.tab = context.tabs.skills
|
||||||
break
|
break
|
||||||
|
case "equipment":
|
||||||
|
context.tab = context.tabs.equipment
|
||||||
|
context.weapons = doc.itemTypes.weapon
|
||||||
|
context.defenses = doc.itemTypes.defense
|
||||||
|
break
|
||||||
case "threat":
|
case "threat":
|
||||||
context.tab = context.tabs.threat
|
context.tab = context.tabs.threat
|
||||||
break
|
break
|
||||||
case "combat":
|
case "combat":
|
||||||
context.tab = context.tabs.combat
|
context.tab = context.tabs.combat
|
||||||
|
context.equippedWeapons = doc.itemTypes.weapon.filter(i => i.system.equipped)
|
||||||
|
context.equippedDefenses = doc.itemTypes.defense.filter(i => i.system.equipped)
|
||||||
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
|
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
|
||||||
context.effects = prepareActiveEffectCategories(doc.effects)
|
context.effects = prepareActiveEffectCategories(doc.effects)
|
||||||
break
|
break
|
||||||
|
|||||||
+5
-320
@@ -43,295 +43,27 @@ export default class VermineActor extends Actor {
|
|||||||
* Prepare Character type specific data
|
* Prepare Character type specific data
|
||||||
*/
|
*/
|
||||||
_prepareCharacterData(actorData) {
|
_prepareCharacterData(actorData) {
|
||||||
if (actorData.type !== 'character') return;
|
// Character derived data is computed by VermineCharacterData.prepareDerivedData()
|
||||||
this._setAgeType();
|
|
||||||
this._setCharacterEffort();
|
|
||||||
this._setCharacterSelfControl();
|
|
||||||
this._setCharacterThresholds();
|
|
||||||
// Make modifications to data here. For example:
|
|
||||||
const systemData = actorData.system;
|
|
||||||
|
|
||||||
// Loop through ability scores, and add their modifiers to our sheet output.
|
|
||||||
for (let [key, ability] of Object.entries(systemData.abilities)) {
|
|
||||||
// Calculate the modifier using d20 rules.
|
|
||||||
ability.mod = Math.floor((ability.value - 10) / 2);
|
|
||||||
}
|
|
||||||
this.prepareCombatStatus();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
prepareCombatStatus() {
|
|
||||||
// Ensure combatStatus exists (defined in base template)
|
|
||||||
if (!this.system.combatStatus) {
|
|
||||||
this.system.combatStatus = { difficulty: "9", label: "Passif" };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure difficulty exists
|
|
||||||
if (!this.system.combatStatus.difficulty) {
|
|
||||||
this.system.combatStatus.difficulty = "9";
|
|
||||||
}
|
|
||||||
|
|
||||||
//combat initiative reaction difficulty
|
|
||||||
const difficulty = parseInt(this.system.combatStatus.difficulty) || 9;
|
|
||||||
|
|
||||||
// Only update if values are different to avoid triggering unnecessary updates
|
|
||||||
const currentLabel = this.system.combatStatus.label;
|
|
||||||
let newLabel = "Passif";
|
|
||||||
|
|
||||||
switch (difficulty) {
|
|
||||||
case 5: newLabel = "Offensif"; break;
|
|
||||||
case 7: newLabel = "Actif"; break;
|
|
||||||
case 9: newLabel = "Passif"; break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only update if label changed
|
|
||||||
if (currentLabel !== newLabel) {
|
|
||||||
this.system.combatStatus.label = newLabel;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only update difficulty if it was undefined or invalid
|
|
||||||
if (!this.system.combatStatus.difficulty || isNaN(parseInt(this.system.combatStatus.difficulty))) {
|
|
||||||
this.system.combatStatus.difficulty = "9";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare NPC type specific data.
|
* Prepare NPC type specific data.
|
||||||
*/
|
*/
|
||||||
_prepareNpcData(actorData) {
|
_prepareNpcData(actorData) {
|
||||||
if (actorData.type !== 'npc') return;
|
// NPC derived data is computed by VermineNpcData.prepareDerivedData()
|
||||||
|
|
||||||
// Make modifications to data here. For example:
|
|
||||||
const systemData = actorData.system;
|
|
||||||
|
|
||||||
// Set wound thresholds based on threat level
|
|
||||||
this._setNpcThresholds();
|
|
||||||
|
|
||||||
// Set reserve max values based on role
|
|
||||||
this._setNpcAttributes();
|
|
||||||
|
|
||||||
this.prepareCombatStatus();
|
|
||||||
|
|
||||||
// Prepare abilities with labels
|
|
||||||
for (let [k, v] of Object.entries(systemData.abilities)) {
|
|
||||||
v.label = game.i18n.localize(CONFIG.VERMINE.abilities[k]) ?? k;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set NPC wound thresholds based on threat level
|
|
||||||
*/
|
|
||||||
_setNpcThresholds() {
|
|
||||||
const health = this.system.abilities?.health?.value || 1;
|
|
||||||
const threatLevel = this.system.threat?.value || 1;
|
|
||||||
const threatConfig = CONFIG.VERMINE.npcThreatLevels[threatLevel] || {};
|
|
||||||
|
|
||||||
// Use threat-based wounds or fall back to health-based
|
|
||||||
this.system.minorWound.threshold = threatConfig.minorWound || health;
|
|
||||||
this.system.majorWound.threshold = threatConfig.majorWound || (health + 3);
|
|
||||||
this.system.deadlyWound.threshold = threatConfig.deadlyWound || (health + 7 < 11 ? health + 7 : 10);
|
|
||||||
|
|
||||||
// Set max wounds based on threat level
|
|
||||||
this.system.minorWound.max = threatConfig.minorWound || 4;
|
|
||||||
this.system.majorWound.max = threatConfig.majorWound || 3;
|
|
||||||
this.system.deadlyWound.max = threatConfig.deadlyWound || 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set NPC attributes from role level
|
|
||||||
*/
|
|
||||||
_setNpcAttributes() {
|
|
||||||
const roleLevel = this.system.role?.value || 1;
|
|
||||||
const roleConfig = CONFIG.VERMINE.npcRoleLevels[roleLevel] || {};
|
|
||||||
|
|
||||||
// Set effort and self_control based on role
|
|
||||||
this.system.attributes.effort.max = roleConfig.pools || 0;
|
|
||||||
this.system.attributes.self_control.max = roleConfig.reaction_bonus || 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare Group type specific data.
|
* Prepare Group type specific data.
|
||||||
*/
|
*/
|
||||||
_prepareGroupData(actorData) {
|
_prepareGroupData(actorData) {
|
||||||
if (actorData.type !== 'group') return;
|
// Group derived data is computed by VermineGroupData.prepareDerivedData()
|
||||||
|
|
||||||
this.prepareCombatStatus();
|
|
||||||
|
|
||||||
// Initialize group-specific data if not present
|
|
||||||
this._initGroupData();
|
|
||||||
|
|
||||||
// Calculate reserve max based on group level
|
|
||||||
this._calculateGroupReserve();
|
|
||||||
|
|
||||||
// Update morale level based on dice value
|
|
||||||
this._updateGroupMorale();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize group data with defaults
|
|
||||||
*/
|
|
||||||
_initGroupData() {
|
|
||||||
if (this.type !== 'group') return;
|
|
||||||
|
|
||||||
const system = this.system;
|
|
||||||
|
|
||||||
// Initialize objectives if not present
|
|
||||||
if (!system.objectives) {
|
|
||||||
system.objectives = { major: [], minor: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize groupAbilities if not present
|
|
||||||
if (!system.groupAbilities) {
|
|
||||||
system.groupAbilities = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize reserve if not present
|
|
||||||
if (!system.reserve) {
|
|
||||||
system.reserve = { value: 0, min: 0, max: 10 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate group reserve max based on level
|
|
||||||
* Rules: Group level determines reserve size
|
|
||||||
*/
|
|
||||||
_calculateGroupReserve() {
|
|
||||||
if (this.type !== 'group') return;
|
|
||||||
|
|
||||||
const level = this.system.level?.value || 1;
|
|
||||||
// Reserve max is based on group level (simplified: level * 1D for now)
|
|
||||||
// Can be customized based on specific rules
|
|
||||||
this.system.reserve.max = Math.min(10, level * 2);
|
|
||||||
|
|
||||||
// Ensure value doesn't exceed max
|
|
||||||
if (this.system.reserve.value > this.system.reserve.max) {
|
|
||||||
this.system.reserve.value = this.system.reserve.max;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update group morale level based on dice value
|
|
||||||
* Rules: 7D+ = Haut, 6-3D = Normal, 2D- = Bas, 0D = Crise
|
|
||||||
*/
|
|
||||||
_updateGroupMorale() {
|
|
||||||
if (this.type !== 'group') return;
|
|
||||||
|
|
||||||
const moraleValue = this.system.morale?.value || 0;
|
|
||||||
const moraleLevel = this.system.morale?.level;
|
|
||||||
|
|
||||||
// If level is already explicitly set, keep it
|
|
||||||
if (moraleLevel && moraleLevel !== "high") return;
|
|
||||||
|
|
||||||
// Determine morale level based on dice value
|
|
||||||
if (moraleValue >= 7) {
|
|
||||||
this.system.morale.level = "high";
|
|
||||||
} else if (moraleValue >= 3) {
|
|
||||||
this.system.morale.level = "normal";
|
|
||||||
} else if (moraleValue >= 1) {
|
|
||||||
this.system.morale.level = "low";
|
|
||||||
} else {
|
|
||||||
this.system.morale.level = "crisis";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare Creature type specific data.
|
* Prepare Creature type specific data.
|
||||||
* Calculates computed values from pattern, size, role, and pack.
|
|
||||||
*/
|
*/
|
||||||
_prepareCreatureData(actorData) {
|
_prepareCreatureData(actorData) {
|
||||||
if (actorData.type !== 'creature') return;
|
// Creature derived data is computed by VermineCreatureData.prepareDerivedData()
|
||||||
|
|
||||||
this.prepareCombatStatus();
|
|
||||||
|
|
||||||
// Calculate computed values from pattern, size, role, and pack
|
|
||||||
this._calculateCreatureComputedValues();
|
|
||||||
|
|
||||||
// Set wound thresholds from creature characteristics
|
|
||||||
this._calculateCreatureWoundThresholds();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate creature computed values from pattern, size, role, and pack.
|
|
||||||
* Rules: Attack = pattern + size + pack + role.reaction
|
|
||||||
* Damage = pattern.damage + size.vigor + pack.damage
|
|
||||||
* Reaction = role.reaction + role.reaction_bonus
|
|
||||||
*/
|
|
||||||
_calculateCreatureComputedValues() {
|
|
||||||
if (this.type !== 'creature') return;
|
|
||||||
|
|
||||||
const patternLevel = this.system.pattern?.value || 1;
|
|
||||||
const sizeLevel = this.system.size?.value || 1;
|
|
||||||
const roleLevel = this.system.role?.value || 1;
|
|
||||||
const packLevel = this.system.pack?.value || 0;
|
|
||||||
|
|
||||||
// Get config values
|
|
||||||
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {};
|
|
||||||
const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {};
|
|
||||||
const roleConfig = CONFIG.VERMINE.creatureRoleLevels[roleLevel] || {};
|
|
||||||
const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {};
|
|
||||||
|
|
||||||
// Calculate computed values
|
|
||||||
this.system.computed = this.system.computed || {};
|
|
||||||
|
|
||||||
// Attack: pattern + size + pack + role.reaction
|
|
||||||
this.system.computed.attack = (patternConfig.attack || 0) +
|
|
||||||
(sizeConfig.attack || 0) +
|
|
||||||
(packConfig.attack || 0) +
|
|
||||||
(roleConfig.reaction || 0);
|
|
||||||
|
|
||||||
// Damage: pattern + size.vigor + pack
|
|
||||||
this.system.computed.damage = (patternConfig.damage || 0) +
|
|
||||||
(sizeConfig.vigor || 0) +
|
|
||||||
(packConfig.damage || 0);
|
|
||||||
|
|
||||||
// Vigor: size.vigor + pack.damage
|
|
||||||
this.system.computed.vigor = (sizeConfig.vigor || 0) + (packConfig.damage || 0);
|
|
||||||
|
|
||||||
// Reaction: role.reaction + role.reaction_bonus
|
|
||||||
this.system.computed.reaction = (roleConfig.reaction || 0) + (roleConfig.reaction_bonus || 0);
|
|
||||||
this.system.computed.reactionBonus = roleConfig.reaction_bonus || 0;
|
|
||||||
|
|
||||||
// Pools (reserves)
|
|
||||||
this.system.computed.pools = roleConfig.pools || 0;
|
|
||||||
|
|
||||||
// Gear and hindrance
|
|
||||||
this.system.computed.gear = roleConfig.gear || 9;
|
|
||||||
this.system.computed.gearHindrance = roleConfig.gear_hindrance || 0;
|
|
||||||
|
|
||||||
// Protection
|
|
||||||
this.system.computed.protection = roleConfig.protection || 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate creature wound thresholds from pattern, size, and pack.
|
|
||||||
* Rules: Thresholds are sum of minorWound, majorWound, deadlyWound from all sources
|
|
||||||
*/
|
|
||||||
_calculateCreatureWoundThresholds() {
|
|
||||||
if (this.type !== 'creature') return;
|
|
||||||
|
|
||||||
const patternLevel = this.system.pattern?.value || 1;
|
|
||||||
const sizeLevel = this.system.size?.value || 1;
|
|
||||||
const packLevel = this.system.pack?.value || 0;
|
|
||||||
|
|
||||||
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {};
|
|
||||||
const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {};
|
|
||||||
const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {};
|
|
||||||
|
|
||||||
// Calculate wound thresholds (sum of all sources)
|
|
||||||
this.system.minorWound.threshold = (patternConfig.minorWound || 0) +
|
|
||||||
(sizeConfig.minorWound || 0) +
|
|
||||||
(packConfig.minorWound || 0);
|
|
||||||
this.system.majorWound.threshold = (patternConfig.majorWound || 0) +
|
|
||||||
(sizeConfig.majorWound || 0) +
|
|
||||||
(packConfig.majorWound || 0);
|
|
||||||
this.system.deadlyWound.threshold = (patternConfig.deadlyWound || 0) +
|
|
||||||
(sizeConfig.deadlyWound || 0) +
|
|
||||||
(packConfig.deadlyWound || 0);
|
|
||||||
|
|
||||||
// Set max wounds
|
|
||||||
this.system.minorWound.max = Math.min(5, this.system.minorWound.threshold + 2);
|
|
||||||
this.system.majorWound.max = Math.min(4, this.system.majorWound.threshold + 1);
|
|
||||||
this.system.deadlyWound.max = Math.min(2, this.system.deadlyWound.threshold);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -376,53 +108,6 @@ export default class VermineActor extends Actor {
|
|||||||
// Process additional NPC data here.
|
// Process additional NPC data here.
|
||||||
}
|
}
|
||||||
|
|
||||||
_setCharacterSelfControl() {
|
// Character derived-data methods removed — handled by VermineCharacterData.prepareDerivedData()
|
||||||
this.system.attributes.self_control.max = 0 + Object.values(this.system.abilities).filter(i => i.category === "mental" || i.category === "social").map((i) => i.value).reduce((acc, curr) => acc + curr, 0) + this.modFromAgeSelfControl;
|
|
||||||
}
|
|
||||||
|
|
||||||
_setCharacterEffort() {
|
|
||||||
this.system.attributes.effort.max = 0 + Object.values(this.system.abilities).filter(i => i.category === "physical" || i.category === "manual").map((i) => i.value).reduce((acc, curr) => acc + curr, 0) + this.modFromAgeEffort;
|
|
||||||
}
|
|
||||||
|
|
||||||
_setCharacterThresholds() {
|
|
||||||
const health = this.system.abilities.health.value;
|
|
||||||
|
|
||||||
this.system.minorWound.threshold = health;
|
|
||||||
this.system.majorWound.threshold = health + 3;
|
|
||||||
this.system.deadlyWound.threshold = (health + 7 < 11) ? health + 7 : 10;
|
|
||||||
|
|
||||||
this.system.minorWound.max = 4 + this.modFromAgeWounds.l;
|
|
||||||
this.system.majorWound.max = 3 + this.modFromAgeWounds.h;
|
|
||||||
this.system.deadlyWound.max = 2 + this.modFromAgeWounds.d;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_setAgeType() {
|
|
||||||
Object.keys(CONFIG.VERMINE.AgeTypes).forEach((type) => {
|
|
||||||
if (this.system.identity.age >= parseInt(CONFIG.VERMINE.AgeTypes[type].beginning, 10)) {
|
|
||||||
this.system.identity.ageType = type;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get ageType() {
|
|
||||||
return this.system.identity.ageType;
|
|
||||||
}
|
|
||||||
|
|
||||||
get modFromAgeSelfControl() {
|
|
||||||
return this.ageType == 1 ? -1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
get modFromAgeEffort() {
|
|
||||||
if (this.ageType == 1) return -1;
|
|
||||||
if (this.ageType == 3) return -2;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
get modFromAgeWounds() {
|
|
||||||
if (this.ageType == 1) return { l: 0, h: 0, d: -1 };
|
|
||||||
if (this.ageType == 3) return { l: -1, h: -1, d: -1 };
|
|
||||||
return { l: 0, h: 0, d: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ export default class VermineItem extends Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prepareAbilityData() {
|
prepareAbilityData() {
|
||||||
console.log('ability data', this)
|
// ability data
|
||||||
const actorType = (this.actor !== null) ? this.actor.type : 'character';
|
const actorType = (this.actor !== null) ? this.actor.type : 'character';
|
||||||
|
|
||||||
if (this.system.type == "") {
|
if (this.system.type == "") {
|
||||||
@@ -54,7 +54,7 @@ export default class VermineItem extends Item {
|
|||||||
*/
|
*/
|
||||||
getRollData() {
|
getRollData() {
|
||||||
// If present, return the actor's roll data.
|
// If present, return the actor's roll data.
|
||||||
if (!this.actor) return null;
|
if (!this.actor) return this.system;
|
||||||
const rollData = this.actor.getRollData();
|
const rollData = this.actor.getRollData();
|
||||||
// Grab the item's system data as well.
|
// Grab the item's system data as well.
|
||||||
rollData.item = foundry.utils.deepClone(this.system);
|
rollData.item = foundry.utils.deepClone(this.system);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export { default as VermineCharacterData } from "./character.mjs"
|
|||||||
export { default as VermineNpcData } from "./npc.mjs"
|
export { default as VermineNpcData } from "./npc.mjs"
|
||||||
export { default as VermineGroupData } from "./group.mjs"
|
export { default as VermineGroupData } from "./group.mjs"
|
||||||
export { default as VermineCreatureData } from "./creature.mjs"
|
export { default as VermineCreatureData } from "./creature.mjs"
|
||||||
|
export { default as VermineCommunityData } from "./community.mjs"
|
||||||
export { default as VermineItemData } from "./item.mjs"
|
export { default as VermineItemData } from "./item.mjs"
|
||||||
export { default as VermineWeaponData } from "./weapon.mjs"
|
export { default as VermineWeaponData } from "./weapon.mjs"
|
||||||
export { default as VermineDefenseData } from "./defense.mjs"
|
export { default as VermineDefenseData } from "./defense.mjs"
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ const reqInt = { required: true, nullable: false, integer: true }
|
|||||||
export function raritySchema() {
|
export function raritySchema() {
|
||||||
const fields = foundry.data.fields
|
const fields = foundry.data.fields
|
||||||
return new fields.SchemaField({
|
return new fields.SchemaField({
|
||||||
value: new fields.NumberField({ ...reqInt, initial: 3, min: 1, max: 5 }),
|
value: new fields.NumberField({ ...reqInt, initial: 3, min: 1, max: 10 }),
|
||||||
handicap: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
|
handicap: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-24
@@ -72,8 +72,40 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
|
|||||||
attributes: new fields.SchemaField({
|
attributes: new fields.SchemaField({
|
||||||
xp: attributeSchema(0, 0, 10),
|
xp: attributeSchema(0, 0, 10),
|
||||||
reputation: attributeSchema(0, 0, 10),
|
reputation: attributeSchema(0, 0, 10),
|
||||||
self_control: attributeSchema(0, 0, 5),
|
self_control: attributeSchema(0, 0, 10),
|
||||||
effort: attributeSchema(0, 0, 5)
|
effort: attributeSchema(0, 0, 10)
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Expérience (dés, récompenses, évolution, apprentissage)
|
||||||
|
experience: new fields.SchemaField({
|
||||||
|
dice: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||||
|
phase: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||||
|
collective: new fields.SchemaField({
|
||||||
|
danger: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
|
||||||
|
discoveries: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
|
||||||
|
duration: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 })
|
||||||
|
}),
|
||||||
|
individual: new fields.SchemaField({
|
||||||
|
implication: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
|
||||||
|
influence: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
|
||||||
|
interpretation: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 })
|
||||||
|
}),
|
||||||
|
evolution: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||||
|
traumaReward: new fields.BooleanField({ required: true, initial: false }),
|
||||||
|
// Bonus de Réserve acquis par apprentissage (s'ajoute au max dérivé)
|
||||||
|
reserveBonus: new fields.SchemaField({
|
||||||
|
self_control: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||||
|
effort: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 })
|
||||||
|
}),
|
||||||
|
// Limites d'apprentissage par phase
|
||||||
|
learned: new fields.SchemaField({
|
||||||
|
phase: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||||
|
skill: new fields.BooleanField({ required: true, initial: false }),
|
||||||
|
specialty: new fields.BooleanField({ required: true, initial: false }),
|
||||||
|
reserve: new fields.BooleanField({ required: true, initial: false }),
|
||||||
|
characteristic: new fields.BooleanField({ required: true, initial: false }),
|
||||||
|
evolution: new fields.BooleanField({ required: true, initial: false })
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Rencontres
|
// Rencontres
|
||||||
@@ -94,20 +126,10 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
|
|||||||
prepareDerivedData() {
|
prepareDerivedData() {
|
||||||
super.prepareDerivedData()
|
super.prepareDerivedData()
|
||||||
|
|
||||||
// 1. Déterminer la tranche d'âge
|
|
||||||
this._setAgeType()
|
this._setAgeType()
|
||||||
|
|
||||||
// 2. Calculer les modificateurs de caractéristiques
|
|
||||||
this._setAbilityModifiers()
|
|
||||||
|
|
||||||
// 3. Calculer les réserves (sang-froid et effort)
|
|
||||||
this._setSelfControlMax()
|
this._setSelfControlMax()
|
||||||
this._setEffortMax()
|
this._setEffortMax()
|
||||||
|
|
||||||
// 4. Calculer les seuils de blessures
|
|
||||||
this._setWoundThresholds()
|
this._setWoundThresholds()
|
||||||
|
|
||||||
// 5. Mettre à jour le statut de combat
|
|
||||||
this._updateCombatStatus()
|
this._updateCombatStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,22 +140,15 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
|
|||||||
_setAgeType() {
|
_setAgeType() {
|
||||||
const age = this.identity.age
|
const age = this.identity.age
|
||||||
const ageTypes = CONFIG.VERMINE.AgeTypes
|
const ageTypes = CONFIG.VERMINE.AgeTypes
|
||||||
for (const [type, cfg] of Object.entries(ageTypes)) {
|
const entries = Object.entries(ageTypes).reverse()
|
||||||
|
for (const [type, cfg] of entries) {
|
||||||
if (age >= parseInt(cfg.beginning, 10)) {
|
if (age >= parseInt(cfg.beginning, 10)) {
|
||||||
this.identity.ageType = parseInt(type, 10)
|
this.identity.ageType = parseInt(type, 10)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Calcule les modificateurs de caractéristiques (règle d20).
|
|
||||||
*/
|
|
||||||
_setAbilityModifiers() {
|
|
||||||
for (const ability of Object.values(this.abilities)) {
|
|
||||||
ability.mod = Math.floor((ability.value - 10) / 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calcule le max de sang-froid :
|
* Calcule le max de sang-froid :
|
||||||
* somme des caractéristiques mentales + sociales + modificateur d'âge.
|
* somme des caractéristiques mentales + sociales + modificateur d'âge.
|
||||||
@@ -144,7 +159,8 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
|
|||||||
const sum = abilities
|
const sum = abilities
|
||||||
.filter(a => a.category === "mental" || a.category === "social")
|
.filter(a => a.category === "mental" || a.category === "social")
|
||||||
.reduce((acc, a) => acc + a.value, 0)
|
.reduce((acc, a) => acc + a.value, 0)
|
||||||
this.attributes.self_control.max = sum + modFromAge
|
const bonus = this.experience?.reserveBonus?.self_control || 0
|
||||||
|
this.attributes.self_control.max = Math.min(10, sum + modFromAge + bonus)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -157,7 +173,8 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
|
|||||||
const sum = abilities
|
const sum = abilities
|
||||||
.filter(a => a.category === "physical" || a.category === "manual")
|
.filter(a => a.category === "physical" || a.category === "manual")
|
||||||
.reduce((acc, a) => acc + a.value, 0)
|
.reduce((acc, a) => acc + a.value, 0)
|
||||||
this.attributes.effort.max = sum + modFromAge
|
const bonus = this.experience?.reserveBonus?.effort || 0
|
||||||
|
this.attributes.effort.max = Math.min(10, sum + modFromAge + bonus)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
/**
|
||||||
|
* DataModel pour les acteurs de type "community" (Communauté).
|
||||||
|
* La Communauté est l'extension sédentarisée d'un Groupe : elle englobe le
|
||||||
|
* Groupe de personnages, des Figures et des Habitants répartis en Compagnies.
|
||||||
|
* Le socle couvre l'identité, la géographie, l'histoire, les objectifs, la
|
||||||
|
* taille/l'effectif, les quatre Domaines (8 jauges), le Moral et les réserves.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
attributeSchema,
|
||||||
|
levelSchema,
|
||||||
|
combatStatusSchema
|
||||||
|
} from "./_shared.mjs"
|
||||||
|
|
||||||
|
export default class VermineCommunityData extends foundry.abstract.TypeDataModel {
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
static LOCALIZATION_PREFIXES = ["VERMINE.community"]
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields
|
||||||
|
const reqInt = { required: true, nullable: false, integer: true }
|
||||||
|
const gauge = () => attributeSchema(0, 0, 10)
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Statut de combat (base, peu utilisé pour une Communauté)
|
||||||
|
combatStatus: combatStatusSchema("9"),
|
||||||
|
|
||||||
|
// Identité
|
||||||
|
identity: new fields.SchemaField({
|
||||||
|
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
profile: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
origin: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
theme: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
instincts: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
prohibits: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
// Âge de la Communauté : recent | established | old
|
||||||
|
age: new fields.StringField({ required: true, nullable: false, initial: "recent" }),
|
||||||
|
notes: new fields.HTMLField({ required: true, initial: "", textSearch: true })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Géographie / localisation
|
||||||
|
location: new fields.SchemaField({
|
||||||
|
sector: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
placeType: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
landmarks: new fields.HTMLField({ required: true, initial: "", textSearch: true }),
|
||||||
|
territory: new fields.HTMLField({ required: true, initial: "", textSearch: true })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Histoire
|
||||||
|
history: new fields.SchemaField({
|
||||||
|
founding: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
events: new fields.HTMLField({ required: true, initial: "", textSearch: true }),
|
||||||
|
relations: new fields.HTMLField({ required: true, initial: "", textSearch: true })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Objectifs : un majeur + des mineurs
|
||||||
|
objectives: new fields.SchemaField({
|
||||||
|
major: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
minor: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" }))
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Taille (1-3) et effectif
|
||||||
|
size: attributeSchema(1, 1, 3),
|
||||||
|
population: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||||
|
|
||||||
|
// Domaines : 4 Domaines × 2 sections (jauges en Dés)
|
||||||
|
domains: new fields.SchemaField({
|
||||||
|
resources: new fields.SchemaField({
|
||||||
|
vivre: gauge(),
|
||||||
|
supplies: gauge()
|
||||||
|
}),
|
||||||
|
security: new fields.SchemaField({
|
||||||
|
defense: gauge(),
|
||||||
|
care: gauge()
|
||||||
|
}),
|
||||||
|
dynamism: new fields.SchemaField({
|
||||||
|
cohesion: gauge(),
|
||||||
|
productivity: gauge()
|
||||||
|
}),
|
||||||
|
relations: new fields.SchemaField({
|
||||||
|
human: gauge(),
|
||||||
|
ecology: gauge()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Moral (comme le Groupe) : value 0-7, level dérivé
|
||||||
|
morale: new fields.SchemaField({
|
||||||
|
level: new fields.StringField({ required: true, nullable: false, initial: "normal" }),
|
||||||
|
value: new fields.NumberField({ ...reqInt, initial: 7, min: 0, max: 7 }),
|
||||||
|
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||||
|
max: new fields.NumberField({ ...reqInt, initial: 7, min: 0 })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Réserve de Groupe
|
||||||
|
reserve: attributeSchema(0, 0, 10),
|
||||||
|
|
||||||
|
// Dés collectifs de la Communauté
|
||||||
|
material: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||||
|
experience: new fields.SchemaField({
|
||||||
|
dice: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Niveau de la Communauté (pour l'expérience des habitants)
|
||||||
|
level: levelSchema(1, 1, 10),
|
||||||
|
|
||||||
|
// Groupe de personnages rattaché (id d'acteur)
|
||||||
|
groupId: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
|
||||||
|
// Habitants (phase 2) : Figures et Compagnies
|
||||||
|
figures: new fields.ArrayField(new fields.SchemaField({
|
||||||
|
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
role: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
specialty: new fields.SchemaField({
|
||||||
|
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
score: new fields.NumberField({ ...reqInt, initial: 3, min: 0, max: 10 })
|
||||||
|
})
|
||||||
|
})),
|
||||||
|
companies: new fields.ArrayField(new fields.SchemaField({
|
||||||
|
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
specialty: new fields.SchemaField({
|
||||||
|
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
score: new fields.NumberField({ ...reqInt, initial: 3, min: 0, max: 10 })
|
||||||
|
}),
|
||||||
|
level: levelSchema(1, 1, 4),
|
||||||
|
members: new fields.NumberField({ ...reqInt, initial: 1, min: 0 }),
|
||||||
|
figureName: new fields.StringField({ required: true, nullable: false, initial: "" })
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
prepareDerivedData() {
|
||||||
|
super.prepareDerivedData()
|
||||||
|
|
||||||
|
// 1. État (Crise/Bas/Stable/Opulent) de chaque jauge de Domaine
|
||||||
|
this._computeGaugeStates()
|
||||||
|
|
||||||
|
// 2. Les quatre Domaines sont-ils Bas ? (Moral inactif)
|
||||||
|
this._computeAllDomainsLow()
|
||||||
|
|
||||||
|
// 3. Mettre à jour le niveau de Moral selon la valeur
|
||||||
|
this._updateMorale()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* État d'une jauge selon sa valeur en Dés.
|
||||||
|
* Crise 0D / Bas 1-2D / Stable 3-5D / Opulent 6D+.
|
||||||
|
* @param {number} value
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static gaugeState(value) {
|
||||||
|
const v = parseInt(value, 10) || 0
|
||||||
|
if (v >= 6) return "opulent"
|
||||||
|
if (v >= 3) return "stable"
|
||||||
|
if (v >= 1) return "low"
|
||||||
|
return "crisis"
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeGaugeStates() {
|
||||||
|
for (const domain of Object.values(this.domains)) {
|
||||||
|
for (const gauge of Object.values(domain)) {
|
||||||
|
gauge.state = VermineCommunityData.gaugeState(gauge.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Un Domaine est "Bas" lorsque ses deux sections sont à 2D ou moins.
|
||||||
|
* Si les quatre Domaines sont Bas, le Bonus de Moral est inactif.
|
||||||
|
*/
|
||||||
|
_computeAllDomainsLow() {
|
||||||
|
const domainLow = (domain) => Object.values(domain).every(g => (parseInt(g.value, 10) || 0) <= 2)
|
||||||
|
this.allDomainsLow = Object.values(this.domains).every(domainLow)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour le niveau de Moral en fonction de la valeur de dés.
|
||||||
|
* Règles : 7D = Haut, 6-3D = Normal, 2-1D = Bas, 0D = Crise.
|
||||||
|
*/
|
||||||
|
_updateMorale() {
|
||||||
|
const moraleValue = parseInt(this.morale?.value, 10) || 0
|
||||||
|
if (this.morale.level && this.morale.level !== "normal") return
|
||||||
|
if (moraleValue >= 7) {
|
||||||
|
this.morale.level = "high"
|
||||||
|
} else if (moraleValue >= 3) {
|
||||||
|
this.morale.level = "normal"
|
||||||
|
} else if (moraleValue >= 1) {
|
||||||
|
this.morale.level = "low"
|
||||||
|
} else {
|
||||||
|
this.morale.level = "crisis"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
-11
@@ -48,7 +48,7 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
|
|||||||
|
|
||||||
// Niveaux de créature (patron, taille, rôle, meute)
|
// Niveaux de créature (patron, taille, rôle, meute)
|
||||||
pattern: attributeSchema(1, 1, 4),
|
pattern: attributeSchema(1, 1, 4),
|
||||||
size: attributeSchema(1, 1, 3),
|
size: attributeSchema(0, 0, 3),
|
||||||
role: attributeSchema(1, 1, 4),
|
role: attributeSchema(1, 1, 4),
|
||||||
pack: attributeSchema(0, 0, 3),
|
pack: attributeSchema(0, 0, 3),
|
||||||
|
|
||||||
@@ -97,28 +97,38 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
|
|||||||
*/
|
*/
|
||||||
_calculateCreatureComputedValues() {
|
_calculateCreatureComputedValues() {
|
||||||
const patternLevel = this.pattern?.value || 1
|
const patternLevel = this.pattern?.value || 1
|
||||||
const sizeLevel = this.size?.value || 1
|
const sizeLevel = this.size?.value || 0
|
||||||
const roleLevel = this.role?.value || 1
|
const roleLevel = this.role?.value || 1
|
||||||
const packLevel = this.pack?.value || 0
|
const packLevel = this.pack?.value || 0
|
||||||
|
|
||||||
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {}
|
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {}
|
||||||
const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {}
|
|
||||||
const roleConfig = CONFIG.VERMINE.creatureRoleLevels[roleLevel] || {}
|
const roleConfig = CONFIG.VERMINE.creatureRoleLevels[roleLevel] || {}
|
||||||
const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {}
|
const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {}
|
||||||
|
|
||||||
|
// Les modificateurs de Taille se cumulent : une créature de Taille N applique
|
||||||
|
// les modificateurs de chaque Taille de 1 à N (Taille 0 = insecte, aucun).
|
||||||
|
const sizeLevels = CONFIG.VERMINE.creatureSizeLevels || {}
|
||||||
|
let sizeAttack = 0
|
||||||
|
let sizeVigor = 0
|
||||||
|
for (let i = 1; i <= sizeLevel; i++) {
|
||||||
|
const cfg = sizeLevels[i] || {}
|
||||||
|
sizeAttack += cfg.attack || 0
|
||||||
|
sizeVigor += cfg.vigor || 0
|
||||||
|
}
|
||||||
|
|
||||||
// Attaque : patron + taille + meute + réaction du rôle
|
// Attaque : patron + taille + meute + réaction du rôle
|
||||||
this.computed.attack = (patternConfig.attack || 0)
|
this.computed.attack = (patternConfig.attack || 0)
|
||||||
+ (sizeConfig.attack || 0)
|
+ sizeAttack
|
||||||
+ (packConfig.attack || 0)
|
+ (packConfig.attack || 0)
|
||||||
+ (roleConfig.reaction || 0)
|
+ (roleConfig.reaction || 0)
|
||||||
|
|
||||||
// Dégâts : patron + vigueur de taille + meute
|
// Dégâts : patron + vigueur de taille + meute
|
||||||
this.computed.damage = (patternConfig.damage || 0)
|
this.computed.damage = (patternConfig.damage || 0)
|
||||||
+ (sizeConfig.vigor || 0)
|
+ sizeVigor
|
||||||
+ (packConfig.damage || 0)
|
+ (packConfig.damage || 0)
|
||||||
|
|
||||||
// Vigueur : taille + meute
|
// Vigueur : taille + meute
|
||||||
this.computed.vigor = (sizeConfig.vigor || 0) + (packConfig.damage || 0)
|
this.computed.vigor = sizeVigor + (packConfig.damage || 0)
|
||||||
|
|
||||||
// Réaction : rôle
|
// Réaction : rôle
|
||||||
this.computed.reaction = (roleConfig.reaction || 0) + (roleConfig.reaction_bonus || 0)
|
this.computed.reaction = (roleConfig.reaction || 0) + (roleConfig.reaction_bonus || 0)
|
||||||
@@ -141,21 +151,32 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
|
|||||||
*/
|
*/
|
||||||
_calculateCreatureWoundThresholds() {
|
_calculateCreatureWoundThresholds() {
|
||||||
const patternLevel = this.pattern?.value || 1
|
const patternLevel = this.pattern?.value || 1
|
||||||
const sizeLevel = this.size?.value || 1
|
const sizeLevel = this.size?.value || 0
|
||||||
const packLevel = this.pack?.value || 0
|
const packLevel = this.pack?.value || 0
|
||||||
|
|
||||||
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {}
|
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {}
|
||||||
const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {}
|
|
||||||
const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {}
|
const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {}
|
||||||
|
|
||||||
|
// Les modificateurs de Taille se cumulent (Taille N = somme des tailles 1..N).
|
||||||
|
const sizeLevels = CONFIG.VERMINE.creatureSizeLevels || {}
|
||||||
|
let sizeMinor = 0
|
||||||
|
let sizeMajor = 0
|
||||||
|
let sizeDeadly = 0
|
||||||
|
for (let i = 1; i <= sizeLevel; i++) {
|
||||||
|
const cfg = sizeLevels[i] || {}
|
||||||
|
sizeMinor += cfg.minorWound || 0
|
||||||
|
sizeMajor += cfg.majorWound || 0
|
||||||
|
sizeDeadly += cfg.deadlyWound || 0
|
||||||
|
}
|
||||||
|
|
||||||
this.minorWound.threshold = (patternConfig.minorWound || 0)
|
this.minorWound.threshold = (patternConfig.minorWound || 0)
|
||||||
+ (sizeConfig.minorWound || 0)
|
+ sizeMinor
|
||||||
+ (packConfig.minorWound || 0)
|
+ (packConfig.minorWound || 0)
|
||||||
this.majorWound.threshold = (patternConfig.majorWound || 0)
|
this.majorWound.threshold = (patternConfig.majorWound || 0)
|
||||||
+ (sizeConfig.majorWound || 0)
|
+ sizeMajor
|
||||||
+ (packConfig.majorWound || 0)
|
+ (packConfig.majorWound || 0)
|
||||||
this.deadlyWound.threshold = (patternConfig.deadlyWound || 0)
|
this.deadlyWound.threshold = (patternConfig.deadlyWound || 0)
|
||||||
+ (sizeConfig.deadlyWound || 0)
|
+ sizeDeadly
|
||||||
+ (packConfig.deadlyWound || 0)
|
+ (packConfig.deadlyWound || 0)
|
||||||
|
|
||||||
// Max de blessures
|
// Max de blessures
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default class VermineDefenseData extends foundry.abstract.TypeDataModel {
|
|||||||
return {
|
return {
|
||||||
...baseItemSchema(),
|
...baseItemSchema(),
|
||||||
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||||
|
equipped: new fields.BooleanField({ required: true, initial: false }),
|
||||||
specificLevel: new fields.SchemaField({
|
specificLevel: new fields.SchemaField({
|
||||||
label: new fields.StringField({ required: true, initial: "" }),
|
label: new fields.StringField({ required: true, initial: "" }),
|
||||||
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
|
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ export default class VermineGroupData extends foundry.abstract.TypeDataModel {
|
|||||||
// Membres (IDs d'acteurs)
|
// Membres (IDs d'acteurs)
|
||||||
members: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
|
members: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
|
||||||
|
|
||||||
|
// Dés d'Expérience collectifs (réserve de Groupe)
|
||||||
|
experience: new fields.SchemaField({
|
||||||
|
dice: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 })
|
||||||
|
}),
|
||||||
|
|
||||||
// Rencontres
|
// Rencontres
|
||||||
encounters: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" }))
|
encounters: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,13 +114,13 @@ export default class VermineNpcData extends foundry.abstract.TypeDataModel {
|
|||||||
const threatLevel = this.threat?.value || 1
|
const threatLevel = this.threat?.value || 1
|
||||||
const threatConfig = CONFIG.VERMINE.npcThreatLevels[threatLevel] || {}
|
const threatConfig = CONFIG.VERMINE.npcThreatLevels[threatLevel] || {}
|
||||||
|
|
||||||
this.minorWound.threshold = threatConfig.minorWound || health
|
this.minorWound.threshold = health
|
||||||
this.majorWound.threshold = threatConfig.majorWound || (health + 3)
|
this.majorWound.threshold = health + 3
|
||||||
this.deadlyWound.threshold = threatConfig.deadlyWound || (health + 7 < 11 ? health + 7 : 10)
|
this.deadlyWound.threshold = Math.min(health + 7, 10)
|
||||||
|
|
||||||
this.minorWound.max = threatConfig.minorWound || 4
|
this.minorWound.max = threatConfig.minorWound || 1
|
||||||
this.majorWound.max = threatConfig.majorWound || 3
|
this.majorWound.max = threatConfig.majorWound || 1
|
||||||
this.deadlyWound.max = threatConfig.deadlyWound || 2
|
this.deadlyWound.max = threatConfig.deadlyWound || 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ export default class VermineSpecialtyData extends foundry.abstract.TypeDataModel
|
|||||||
/** @override */
|
/** @override */
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
const fields = foundry.data.fields
|
const fields = foundry.data.fields
|
||||||
|
const reqInt = { required: true, nullable: false, integer: true }
|
||||||
return {
|
return {
|
||||||
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
skill: new fields.StringField({ required: true, nullable: false, initial: "" })
|
skill: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||||
|
level: new fields.NumberField({ ...reqInt, initial: 1, min: 0, max: 5 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { baseItemSchema } from "./_shared.mjs"
|
import { baseItemSchema } from "./_shared.mjs"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DataModel pour les items de type "vehicle" (véhicules).
|
* DataModel pour les items de type "vehicle" (véhicules et montures).
|
||||||
|
* Caractéristiques : Rareté, Fiabilité, Distance, Vitesse (croisière/pointe),
|
||||||
|
* Entretien (compétences) et Réserve d'Effort (montures).
|
||||||
* @augments {foundry.abstract.TypeDataModel}
|
* @augments {foundry.abstract.TypeDataModel}
|
||||||
*/
|
*/
|
||||||
export default class VermineVehicleData extends foundry.abstract.TypeDataModel {
|
export default class VermineVehicleData extends foundry.abstract.TypeDataModel {
|
||||||
@@ -14,6 +16,27 @@ export default class VermineVehicleData extends foundry.abstract.TypeDataModel {
|
|||||||
const reqInt = { required: true, nullable: false, integer: true }
|
const reqInt = { required: true, nullable: false, integer: true }
|
||||||
return {
|
return {
|
||||||
...baseItemSchema(),
|
...baseItemSchema(),
|
||||||
|
// Fiabilité étendue (1-10) pour les véhicules
|
||||||
|
reliability: new fields.NumberField({ ...reqInt, initial: 5, min: 1, max: 10 }),
|
||||||
|
// Type : "vehicle" (véhicule) ou "mount" (monture)
|
||||||
|
kind: new fields.StringField({ required: true, nullable: false, initial: "vehicle" }),
|
||||||
|
// Distance : km parcourus avec un "plein"
|
||||||
|
distance: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||||
|
// Vitesse : croisière / pointe (durée de la pointe)
|
||||||
|
speed: new fields.SchemaField({
|
||||||
|
cruise: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||||
|
top: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||||
|
topDuration: new fields.StringField({ required: true, nullable: false, initial: "10 min" })
|
||||||
|
}),
|
||||||
|
// Entretien : compétences nécessaires (clés de CONFIG.VERMINE.skills)
|
||||||
|
maintenance: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
|
||||||
|
// Réserve d'Effort (montures)
|
||||||
|
effortReserve: new fields.SchemaField({
|
||||||
|
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||||
|
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||||
|
max: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
|
||||||
|
}),
|
||||||
|
// Mobilité (héritée)
|
||||||
mobility: new fields.NumberField({ ...reqInt, initial: 3, min: 0 })
|
mobility: new fields.NumberField({ ...reqInt, initial: 3, min: 0 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export default class VermineWeaponData extends foundry.abstract.TypeDataModel {
|
|||||||
const reqInt = { required: true, nullable: false, integer: true }
|
const reqInt = { required: true, nullable: false, integer: true }
|
||||||
return {
|
return {
|
||||||
...baseItemSchema(),
|
...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 }),
|
min_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||||
max_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
max_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||||
damage: new fields.SchemaField({
|
damage: new fields.SchemaField({
|
||||||
|
|||||||
+103
-115
@@ -1,5 +1,6 @@
|
|||||||
export class TotemPicker extends Application {
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api
|
||||||
|
|
||||||
|
export class TotemPicker extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
|
||||||
constructor(linkEl, actor) {
|
constructor(linkEl, actor) {
|
||||||
super();
|
super();
|
||||||
@@ -7,45 +8,39 @@ export class TotemPicker extends Application {
|
|||||||
this.actor = actor;
|
this.actor = actor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
static get DEFAULT_OPTIONS() {
|
||||||
static get defaultOptions() {
|
|
||||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
|
||||||
id: "TOTEM_PICKER",
|
|
||||||
title: game.i18n.localize("VERMINE.totem_picker"),
|
|
||||||
template: 'systems/vermine2047/templates/applications/choose-totem.hbs',
|
|
||||||
popOut: true,
|
|
||||||
resizable: true,
|
|
||||||
height: "800",
|
|
||||||
width: "800"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getData() {
|
|
||||||
// Send data to the template
|
|
||||||
return {
|
return {
|
||||||
config: CONFIG.VERMINE,
|
id: "TOTEM_PICKER",
|
||||||
}
|
position: { width: 800, height: 800 },
|
||||||
|
window: { title: game.i18n.localize("VERMINE.totem_picker"), resizable: true }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
activateListeners(html) {
|
static PARTS = {
|
||||||
super.activateListeners(html);
|
main: {
|
||||||
html.find('.totem').click(event => {
|
template: 'systems/vermine2047/templates/applications/choose-totem.hbs'
|
||||||
const totem = $(event.target).parent('a').data('totem');
|
}
|
||||||
if (totem != null) {
|
};
|
||||||
this.actor.update({ 'system.identity.totem': totem });
|
|
||||||
}
|
_prepareContext() {
|
||||||
this.close();
|
return { config: CONFIG.VERMINE };
|
||||||
|
}
|
||||||
|
|
||||||
|
_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 });
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*async _updateObject(event, formData) {
|
|
||||||
// console.log(formData.exampleInput);
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class ActorPicker extends Application {
|
export class ActorPicker extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
|
||||||
|
|
||||||
constructor(linkEl, actor) {
|
constructor(linkEl, actor) {
|
||||||
super();
|
super();
|
||||||
@@ -53,152 +48,145 @@ export class ActorPicker extends Application {
|
|||||||
this.actor = actor;
|
this.actor = actor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
static get DEFAULT_OPTIONS() {
|
||||||
static get defaultOptions() {
|
return {
|
||||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
|
||||||
id: "ACTOR_PICKER",
|
id: "ACTOR_PICKER",
|
||||||
title: game.i18n.localize("VERMINE.actor_picker"),
|
position: { width: 600, height: 350 },
|
||||||
template: 'systems/vermine2047/templates/applications/choose-actor.hbs',
|
window: { title: game.i18n.localize("VERMINE.actor_picker"), resizable: true }
|
||||||
popOut: true,
|
};
|
||||||
resizable: true,
|
|
||||||
height: "350",
|
|
||||||
width: "600"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
main: {
|
||||||
|
template: 'systems/vermine2047/templates/applications/choose-actor.hbs'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_prepareContext() {
|
||||||
getData() {
|
|
||||||
// Send data to the template
|
|
||||||
const npcs = game.actors.filter(a => a.type == "npc");
|
const npcs = game.actors.filter(a => a.type == "npc");
|
||||||
const characters = game.actors.filter(a => a.type == "character");
|
const characters = game.actors.filter(a => a.type == "character");
|
||||||
const all = game.actors.filter(a => a.type == "npc" || a.type == 'character');
|
const all = game.actors.filter(a => a.type == "npc" || a.type == 'character');
|
||||||
const type = $(this.linkEl).data('type');
|
const type = this.linkEl.dataset.type;
|
||||||
|
|
||||||
let actorsList = [];
|
let actorsList = [];
|
||||||
if (type == 'members') {
|
if (type == 'members') {
|
||||||
actorsList = characters;
|
actorsList = characters;
|
||||||
} else if (type == 'relations') {
|
} else if (type == 'relations') {
|
||||||
actorsList = npcs;//[...npcs, ...characters];
|
actorsList = npcs;
|
||||||
} else {
|
} else {
|
||||||
actorsList = all;
|
actorsList = all;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
config: CONFIG.VERMINE,
|
config: CONFIG.VERMINE,
|
||||||
actorsList: actorsList
|
actorsList: actorsList
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async activateListeners(html) {
|
_onRender(context, options) {
|
||||||
super.activateListeners(html);
|
super._onRender(context, options);
|
||||||
html.find('.actor').click(async (event) => {
|
this.element.querySelectorAll('.actor').forEach(el => {
|
||||||
const actorId = $(event.target).parent('div').data('actor-id');
|
el.addEventListener('click', async event => {
|
||||||
const type = $(this.linkEl).data('type');
|
const div = event.target.closest('div');
|
||||||
|
const actorId = div?.dataset.actorId;
|
||||||
|
const type = this.linkEl.dataset.type;
|
||||||
|
if (!actorId) return;
|
||||||
|
|
||||||
if (!actorId) return;
|
let actorsList = [];
|
||||||
|
|
||||||
let actorsList = [];
|
if (type == 'members') {
|
||||||
|
actorsList = foundry.utils.duplicate(this.actor.system.members || []);
|
||||||
|
} else if (type == 'encounters') {
|
||||||
|
actorsList = foundry.utils.duplicate(this.actor.system.encounters || []);
|
||||||
|
}
|
||||||
|
|
||||||
if (type == 'members') {
|
if (!Array.isArray(actorsList)) {
|
||||||
actorsList = foundry.utils.duplicate(this.actor.system.members || []);
|
actorsList = [];
|
||||||
} else if (type == 'encounters') {
|
}
|
||||||
actorsList = foundry.utils.duplicate(this.actor.system.encounters || []);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(actorsList)) {
|
if (!actorsList.includes(actorId)) {
|
||||||
actorsList = [];
|
actorsList.push(actorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add actor if not already present
|
if (type == 'members') {
|
||||||
if (!actorsList.includes(actorId)) {
|
await this.actor.update({ 'system.members': actorsList });
|
||||||
actorsList.push(actorId);
|
} else if (type == 'encounters') {
|
||||||
}
|
await this.actor.update({ 'system.encounters': actorsList });
|
||||||
|
}
|
||||||
|
|
||||||
if (type == 'members') {
|
this.close();
|
||||||
await this.actor.update({ 'system.members': actorsList });
|
});
|
||||||
} else if (type == 'encounters') {
|
|
||||||
await this.actor.update({ 'system.encounters': actorsList });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.close();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class TraitSelector extends Application {
|
export class TraitSelector extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
|
||||||
constructor(targetItem) {
|
constructor(targetItem) {
|
||||||
super();
|
super();
|
||||||
this.targetItem = targetItem;
|
this.targetItem = targetItem;
|
||||||
this.traits = CONFIG.VERMINE.traits
|
this.traits = CONFIG.VERMINE.traits;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
static get DEFAULT_OPTIONS() {
|
||||||
static get defaultOptions() {
|
return {
|
||||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
id: "TRAIT_SELECTOR",
|
||||||
classes: ["vermine2047", "trait-selector"],
|
position: { width: 500, height: 500 },
|
||||||
title: game.i18n.localize("VERMINE.traits_selector"),
|
window: { title: game.i18n.localize("VERMINE.traits_selector"), resizable: true },
|
||||||
template: 'systems/vermine2047/templates/applications/choose-traits.hbs',
|
classes: ["vermine2047", "trait-selector"]
|
||||||
popOut: true,
|
};
|
||||||
resizable: true,
|
|
||||||
height: "500",
|
|
||||||
width: "500"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
getData() {
|
|
||||||
|
static PARTS = {
|
||||||
|
main: {
|
||||||
|
template: 'systems/vermine2047/templates/applications/choose-traits.hbs'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_prepareContext() {
|
||||||
return {
|
return {
|
||||||
traits: this.traits,
|
traits: this.traits,
|
||||||
item: this.targetItem
|
item: this.targetItem
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
async activateListeners(html) {
|
|
||||||
super.activateListeners(html);
|
|
||||||
this.validateTraits(html);
|
|
||||||
html.find('input').change(ev => {
|
|
||||||
this.onChangeInput(ev)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
_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) {
|
|
||||||
let checks = html.find("input.trait-selector");
|
_validateTraits(html) {
|
||||||
for (let inp of checks) {
|
const checks = html.querySelectorAll("input.trait-selector");
|
||||||
|
for (const inp of checks) {
|
||||||
if (this.targetItem.system.traits[inp.dataset.trait]) {
|
if (this.targetItem.system.traits[inp.dataset.trait]) {
|
||||||
if (inp.type == "checkbox") {
|
if (inp.type == "checkbox") {
|
||||||
inp.checked = true
|
inp.checked = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.render(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onChangeInput(ev) {
|
async _onChangeInput(ev) {
|
||||||
let el = ev.currentTarget;
|
const el = ev.currentTarget;
|
||||||
|
const traitKey = el.dataset.trait;
|
||||||
let traitKey = el.dataset.trait; // Récupère la clé du trait à partir de l'attribut data-trait
|
const traitValue = parseInt(el.value) || null;
|
||||||
let traitValue = parseInt(el.value) || null
|
const traits = this.targetItem.system.traits || {};
|
||||||
let traits = this.targetItem.system.traits || {}; // Récupère les traits actuels, ou un objet vide si aucun trait n'est défini
|
|
||||||
|
|
||||||
|
|
||||||
console.log(traitKey, traitValue, traits)
|
|
||||||
|
|
||||||
if (el.classList.contains('trait-selector')) {
|
if (el.classList.contains('trait-selector')) {
|
||||||
if (!traits[traitKey]) {
|
if (!traits[traitKey]) {
|
||||||
// Si la case est cochée, ajoute le trait
|
|
||||||
await this.targetItem.update({ [`system.traits.${traitKey}`]: this.traits[traitKey] });
|
await this.targetItem.update({ [`system.traits.${traitKey}`]: this.traits[traitKey] });
|
||||||
} else {
|
} else {
|
||||||
// Si la case est décochée, retire le trait
|
|
||||||
await this.targetItem.update({ [`system.traits.${traitKey}`]: null });
|
await this.targetItem.update({ [`system.traits.${traitKey}`]: null });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (traitValue) {
|
if (traitValue) {
|
||||||
console.log(el.value)
|
|
||||||
// Logique pour les valeurs des traits si nécessaire
|
|
||||||
el.closest("label").querySelector('.trait-selector').checked = true;
|
el.closest("label").querySelector('.trait-selector').checked = true;
|
||||||
} else {
|
} else {
|
||||||
el.closest("label").querySelector('.trait-selector').checked = false;
|
el.closest("label").querySelector('.trait-selector').checked = false;
|
||||||
}
|
}
|
||||||
this.render(true)
|
this.render(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+98
-43
@@ -26,46 +26,6 @@ VERMINE.DifficultyLevels = {
|
|||||||
3: { "label": "DIFFICULTY_LEVELS.hard", "difficulty": 7 },
|
3: { "label": "DIFFICULTY_LEVELS.hard", "difficulty": 7 },
|
||||||
4: { "label": "DIFFICULTY_LEVELS.very_hard", "difficulty": 9 },
|
4: { "label": "DIFFICULTY_LEVELS.very_hard", "difficulty": 9 },
|
||||||
5: { "label": "DIFFICULTY_LEVELS.impossible", "difficulty": 10 }
|
5: { "label": "DIFFICULTY_LEVELS.impossible", "difficulty": 10 }
|
||||||
},
|
|
||||||
|
|
||||||
VERMINE.ThreatLevels = {
|
|
||||||
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 },
|
|
||||||
}
|
|
||||||
|
|
||||||
VERMINE.ExperienceLevels = {
|
|
||||||
1: { "label": "SKILL_LEVELS.beginner", "action": 3, "specialties": 4, "rerolls": 0, "contact": "7" },
|
|
||||||
2: { "label": "SKILL_LEVELS.proficient", "action": 3, "specialties": 5, "rerolls": 0, "contact": "5 ou 7" },
|
|
||||||
3: { "label": "SKILL_LEVELS.expert", "action": 4, "specialties": 6, "rerolls": 1, "contact": "5,7 ou 9" },
|
|
||||||
4: { "label": "SKILL_LEVELS.master", "action": 4, "specialties": 6, "rerolls": 2, "contact": "3,5,7 ou 9" },
|
|
||||||
}
|
|
||||||
|
|
||||||
VERMINE.RoleLevels = {
|
|
||||||
1: { "label": "ROLE_LEVELS.minor", "reaction": 3, "reaction_bonus": 0, "pools": 0, "gear": 9, "gear_hindrance": 0, "protection": 1 },
|
|
||||||
2: { "label": "ROLE_LEVELS.secondary", "reaction": 3, "reaction_bonus": 1, "pools": 1, "gear": 9, "gear_hindrance": 1, "protection": 2 },
|
|
||||||
3: { "label": "ROLE_LEVELS.important", "reaction": 3, "reaction_bonus": 2, "pools": 2, "gear": 9, "gear_hindrance": 2, "protection": 3 },
|
|
||||||
4: { "label": "ROLE_LEVELS.major", "reaction": 4, "reaction_bonus": 2, "pools": 4, "gear": 10, "gear_hindrance": 2, "protection": 3 },
|
|
||||||
}
|
|
||||||
|
|
||||||
VERMINE.PatternLevels = {
|
|
||||||
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 },
|
|
||||||
}
|
|
||||||
|
|
||||||
VERMINE.SizeLevels = {
|
|
||||||
1: { "attack": 2, "vigor": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
|
|
||||||
2: { "attack": 3, "vigor": 2, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
|
|
||||||
3: { "attack": 4, "vigor": 3, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
VERMINE.PackLevels = {
|
|
||||||
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 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -179,9 +139,10 @@ VERMINE.creaturePatternLevels = {
|
|||||||
* Creature Size Levels configuration
|
* Creature Size Levels configuration
|
||||||
*/
|
*/
|
||||||
VERMINE.creatureSizeLevels = {
|
VERMINE.creatureSizeLevels = {
|
||||||
1: { "label": "SIZE_LEVELS.small", "attack": 2, "vigor": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
|
0: { "label": "SIZE_LEVELS.tiny", "attack": 0, "vigor": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 },
|
||||||
2: { "label": "SIZE_LEVELS.medium", "attack": 3, "vigor": 2, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
|
1: { "label": "SIZE_LEVELS.small", "attack": 1, "vigor": 1, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
|
||||||
3: { "label": "SIZE_LEVELS.large", "attack": 4, "vigor": 3, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 }
|
2: { "label": "SIZE_LEVELS.medium", "attack": 2, "vigor": 2, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 },
|
||||||
|
3: { "label": "SIZE_LEVELS.large", "attack": 3, "vigor": 3, "minorWound": 3, "majorWound": 3, "deadlyWound": 3 }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -251,6 +212,49 @@ VERMINE.skillCategories = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skills mapping: each skill belongs to a category.
|
||||||
|
* Used by templates to render skill-select dropdowns.
|
||||||
|
*/
|
||||||
|
VERMINE.skills = {
|
||||||
|
// man
|
||||||
|
arts: { category: "man" },
|
||||||
|
civilization: { category: "man" },
|
||||||
|
psychology: { category: "man" },
|
||||||
|
rumors: { category: "man" },
|
||||||
|
healing: { category: "man" },
|
||||||
|
// animal
|
||||||
|
animalism: { category: "animal" },
|
||||||
|
dissection: { category: "animal" },
|
||||||
|
wildlife: { category: "animal" },
|
||||||
|
repulsion: { category: "animal" },
|
||||||
|
tracks: { category: "animal" },
|
||||||
|
// tool
|
||||||
|
crafting: { category: "tool" },
|
||||||
|
diy: { category: "tool" },
|
||||||
|
mecanical: { category: "tool" },
|
||||||
|
piloting: { category: "tool" },
|
||||||
|
technology: { category: "tool" },
|
||||||
|
// weapon
|
||||||
|
firearms: { category: "weapon" },
|
||||||
|
archery: { category: "weapon" },
|
||||||
|
armory: { category: "weapon" },
|
||||||
|
throwing: { category: "weapon" },
|
||||||
|
melee: { category: "weapon" },
|
||||||
|
// survival
|
||||||
|
alertness: { category: "survival" },
|
||||||
|
atletics: { category: "survival" },
|
||||||
|
food: { category: "survival" },
|
||||||
|
stealth: { category: "survival" },
|
||||||
|
close: { category: "survival" },
|
||||||
|
// world
|
||||||
|
environment: { category: "world" },
|
||||||
|
flora: { category: "world" },
|
||||||
|
road: { category: "world" },
|
||||||
|
toxics: { category: "world" },
|
||||||
|
ruins: { category: "world" }
|
||||||
|
}
|
||||||
|
|
||||||
VERMINE.sexes = { "male": "SEXES.male", "female": "SEXES.female" };
|
VERMINE.sexes = { "male": "SEXES.male", "female": "SEXES.female" };
|
||||||
|
|
||||||
VERMINE.totems = {
|
VERMINE.totems = {
|
||||||
@@ -465,3 +469,54 @@ VERMINE.totemEffects = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Communauté — Domaines et sections (jauges).
|
||||||
|
*/
|
||||||
|
VERMINE.communityDomains = {
|
||||||
|
resources: {
|
||||||
|
label: "COMMUNITY.domains.resources",
|
||||||
|
sections: { vivre: "COMMUNITY.sections.vivre", supplies: "COMMUNITY.sections.supplies" }
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
label: "COMMUNITY.domains.security",
|
||||||
|
sections: { defense: "COMMUNITY.sections.defense", care: "COMMUNITY.sections.care" }
|
||||||
|
},
|
||||||
|
dynamism: {
|
||||||
|
label: "COMMUNITY.domains.dynamism",
|
||||||
|
sections: { cohesion: "COMMUNITY.sections.cohesion", productivity: "COMMUNITY.sections.productivity" }
|
||||||
|
},
|
||||||
|
relations: {
|
||||||
|
label: "COMMUNITY.domains.relations",
|
||||||
|
sections: { human: "COMMUNITY.sections.human", ecology: "COMMUNITY.sections.ecology" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Communauté — Tailles (effectif, Figures, Compagnies recommandés).
|
||||||
|
*/
|
||||||
|
VERMINE.communitySizeLevels = {
|
||||||
|
1: { label: "COMMUNITY.sizeLevels.1", population: "7-50", figures: 3, companies: 5 },
|
||||||
|
2: { label: "COMMUNITY.sizeLevels.2", population: "51-75", figures: 5, companies: 7 },
|
||||||
|
3: { label: "COMMUNITY.sizeLevels.3", population: "100+", figures: 7, companies: 9 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Communauté — États d'une jauge de Domaine.
|
||||||
|
* Crise 0D / Bas 1-2D / Stable 3-5D / Opulent 6D+.
|
||||||
|
*/
|
||||||
|
VERMINE.communityGaugeStates = {
|
||||||
|
crisis: { label: "COMMUNITY.states.crisis" },
|
||||||
|
low: { label: "COMMUNITY.states.low" },
|
||||||
|
stable: { label: "COMMUNITY.states.stable" },
|
||||||
|
opulent: { label: "COMMUNITY.states.opulent" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Communauté — Âge (notion de temps).
|
||||||
|
*/
|
||||||
|
VERMINE.communityAges = {
|
||||||
|
recent: "COMMUNITY.age.recent",
|
||||||
|
established: "COMMUNITY.age.established",
|
||||||
|
old: "COMMUNITY.age.old"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import { VermineUtils } from "./roll.mjs";
|
|
||||||
export class CombatResultDialog extends Dialog {
|
|
||||||
|
|
||||||
constructor(dialogData, options) {
|
|
||||||
/*let options = { classes: ["combat", "result"], ...options };
|
|
||||||
let conf = {
|
|
||||||
title: "Résultat de la confrontation",
|
|
||||||
content: dialogData.content
|
|
||||||
};
|
|
||||||
super(conf, options);
|
|
||||||
this.dialogData = dialogData;*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
activateListeners(html) {
|
|
||||||
/*super.activateListeners(html);
|
|
||||||
this.html = html;
|
|
||||||
this.setEphemere(this.dialogData.signe.system.ephemere);
|
|
||||||
html.find(".signe-aleatoire").click(event => this.setSigneAleatoire());
|
|
||||||
html.find("[name='signe.system.ephemere']").change((event) => this.setEphemere(event.currentTarget.checked));
|
|
||||||
html.find(".signe-xp-sort").change((event) => this.onValeurXpSort(event));
|
|
||||||
html.find("input.select-actor").change((event) => this.onSelectActor(event));
|
|
||||||
html.find("input.select-tmr").change((event) => this.onSelectTmr(event));*/
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async onSelectActor(event) {
|
|
||||||
/*const actorId = this.html.find(event.currentTarget)?.data("actor-id");
|
|
||||||
const actor = this.dialogData.actors.find(it => it.id == actorId);
|
|
||||||
if (actor) {
|
|
||||||
actor.selected = event.currentTarget.checked;
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
import { VermineUtils } from "../roll.mjs";
|
||||||
|
import { VermineExchange } from "../exchange.mjs";
|
||||||
|
|
||||||
|
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialogue d'attaque pour les échanges de coups.
|
||||||
|
* Prépare le pool, la difficulté (contact / distance) et le contexte
|
||||||
|
* d'échange (cibles, dégâts de base) selon le type d'acteur :
|
||||||
|
* - Personnage : capacité + compétence de l'arme.
|
||||||
|
* - PNJ : pool d'attaque du niveau de menace + réserves de rôle.
|
||||||
|
* - Créature : pool d'attaque calculé, difficulté du statut de combat.
|
||||||
|
*/
|
||||||
|
export default class CombatDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
||||||
|
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
classes: ["vermine-roll"],
|
||||||
|
tag: "form",
|
||||||
|
window: {
|
||||||
|
icon: "fas fa-crosshairs",
|
||||||
|
resizable: false
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
width: 520,
|
||||||
|
height: 600
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
roll: CombatDialog.#onRoll,
|
||||||
|
cancel: CombatDialog.#onCancel
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
main: { template: "systems/vermine2047/templates/dialogs/combat-dialog.hbs" }
|
||||||
|
};
|
||||||
|
|
||||||
|
static async create({ actorId, weapon = null }) {
|
||||||
|
const actor = await game.actors.get(actorId);
|
||||||
|
if (!actor) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const mode = actor.type === "creature" ? "contact" : VermineExchange.modeForWeapon(weapon);
|
||||||
|
return new CombatDialog({ actor, weapon, mode });
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options = {}) {
|
||||||
|
super(options);
|
||||||
|
this.actor = options.actor;
|
||||||
|
this.weapon = options.weapon ?? null;
|
||||||
|
this.mode = options.mode ?? "contact";
|
||||||
|
this.abilityKey = VermineExchange.attackAbility(this.mode);
|
||||||
|
this.skillKey = this.weapon?.system?.skill || null;
|
||||||
|
this.skillValue = this.skillKey ? (this.actor.system?.skills?.[this.skillKey]?.value || 0) : 0;
|
||||||
|
this.skillPool = this.skillValue ? (CONFIG.VERMINE.SkillLevels?.[this.skillValue]?.dicePool || 0) : 0;
|
||||||
|
this.skillReroll = this.skillValue ? (CONFIG.VERMINE.SkillLevels?.[this.skillValue]?.reroll || 0) : 0;
|
||||||
|
this.basePool = VermineExchange.baseAttackPool(this.actor, this.weapon, this.mode);
|
||||||
|
this.baseDamage = VermineExchange.baseDamage(this.actor, this.weapon);
|
||||||
|
}
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
return game.i18n.localize("VERMINE.attack");
|
||||||
|
}
|
||||||
|
|
||||||
|
async _prepareContext() {
|
||||||
|
const actor = this.actor;
|
||||||
|
const isCharacter = actor.type === "character";
|
||||||
|
const isNpc = actor.type === "npc";
|
||||||
|
const isCreature = actor.type === "creature";
|
||||||
|
|
||||||
|
const targets = [...game.user.targets].map(t => ({
|
||||||
|
id: t.actor?.id ?? null,
|
||||||
|
uuid: t.actor?.uuid ?? null,
|
||||||
|
name: t.name,
|
||||||
|
canDefend: t.actor?.type !== "creature"
|
||||||
|
}));
|
||||||
|
|
||||||
|
let difficultyOptions = [];
|
||||||
|
let lockedDifficulty = null;
|
||||||
|
let defaultDifficulty = null;
|
||||||
|
|
||||||
|
if (isCreature) {
|
||||||
|
const d = parseInt(actor.system?.combatStatus?.difficulty, 10) || 9;
|
||||||
|
lockedDifficulty = d;
|
||||||
|
difficultyOptions = [{ difficulty: d, label: "", locked: true }];
|
||||||
|
defaultDifficulty = d;
|
||||||
|
} else if (this.mode === "ranged") {
|
||||||
|
difficultyOptions = VermineExchange.rangeDifficultyOptions().map(o => ({
|
||||||
|
difficulty: o.difficulty,
|
||||||
|
label: game.i18n.localize(o.label),
|
||||||
|
rangeKey: o.key
|
||||||
|
}));
|
||||||
|
defaultDifficulty = difficultyOptions[0]?.difficulty ?? 7;
|
||||||
|
} else if (isNpc) {
|
||||||
|
difficultyOptions = VermineExchange.npcDifficultyOptions(actor).map(d => ({
|
||||||
|
difficulty: d,
|
||||||
|
label: String(d)
|
||||||
|
}));
|
||||||
|
defaultDifficulty = difficultyOptions[0]?.difficulty ?? 7;
|
||||||
|
} else {
|
||||||
|
difficultyOptions = VermineExchange.contactDifficultyOptions(this.skillValue).map(d => ({
|
||||||
|
difficulty: d,
|
||||||
|
label: String(d)
|
||||||
|
}));
|
||||||
|
defaultDifficulty = difficultyOptions[0]?.difficulty ?? 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cible de type créature : défense passive, la difficulté d'attaque est
|
||||||
|
// forcée au statut de combat de la créature (toute réussite ≥1 touche).
|
||||||
|
const creatureTargets = targets.filter(t => t.id && !t.canDefend);
|
||||||
|
const creatureLocked = !isCreature && creatureTargets.length && creatureTargets.length === targets.length;
|
||||||
|
if (creatureLocked) {
|
||||||
|
const ref = creatureTargets[0];
|
||||||
|
const creature = ref.id ? game.actors.get(ref.id) : null;
|
||||||
|
const actor = creature ?? (ref.uuid ? await fromUuid(ref.uuid) : null);
|
||||||
|
const d = parseInt(actor?.system?.combatStatus?.difficulty, 10) || 7;
|
||||||
|
lockedDifficulty = d;
|
||||||
|
difficultyOptions = [{ difficulty: d, label: "", locked: true }];
|
||||||
|
defaultDifficulty = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
const threat = actor.system?.threat?.value || 1;
|
||||||
|
const role = actor.system?.role?.value || 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
actor,
|
||||||
|
system: actor.system,
|
||||||
|
config: CONFIG.VERMINE,
|
||||||
|
actorType: actor.type,
|
||||||
|
mode: this.mode,
|
||||||
|
weapon: this.weapon,
|
||||||
|
abilityKey: this.abilityKey,
|
||||||
|
abilityValue: actor.system?.abilities?.[this.abilityKey]?.value || 0,
|
||||||
|
skillKey: this.skillKey,
|
||||||
|
skillValue: this.skillValue,
|
||||||
|
basePool: this.basePool,
|
||||||
|
baseDamage: this.baseDamage,
|
||||||
|
isCharacter,
|
||||||
|
isNpc,
|
||||||
|
isCreature,
|
||||||
|
creatureLocked,
|
||||||
|
npcThreatPool: isNpc ? (CONFIG.VERMINE.npcThreatLevels?.[threat]?.attack || 0) : 0,
|
||||||
|
npcPoolMax: isNpc ? (CONFIG.VERMINE.npcRoleLevels?.[role]?.pools || 0) : 0,
|
||||||
|
difficultyOptions,
|
||||||
|
lockedDifficulty,
|
||||||
|
defaultDifficulty,
|
||||||
|
hasAmmo: this.mode === "ranged" && (this.weapon?.system?.ammo ?? 0) > 0,
|
||||||
|
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
|
||||||
|
availableItems: actor.items.filter(i => i.type === "item"),
|
||||||
|
targets
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onRender(context, options) {
|
||||||
|
this.element.dataset.actorId = this.actor.id;
|
||||||
|
this.element.dataset.weaponId = this.weapon?.id ?? "";
|
||||||
|
this.element.dataset.mode = this.mode;
|
||||||
|
|
||||||
|
for (const inp of this.element.querySelectorAll("[data-roll]")) {
|
||||||
|
inp.addEventListener("change", this.#onInputChange.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
const selfControl = this.element.querySelector("#self_control");
|
||||||
|
if (selfControl) {
|
||||||
|
selfControl.addEventListener("change", this.#onChangeSelfControl.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.element.querySelector("#second-action")?.addEventListener("change", () => this.#updateUI());
|
||||||
|
this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI());
|
||||||
|
this.element.querySelector("#human-totem")?.addEventListener("change", () => this.#updateUI());
|
||||||
|
this.element.querySelector("#adapted-totem")?.addEventListener("change", () => this.#updateUI());
|
||||||
|
this.element.querySelector("#pools-spent")?.addEventListener("change", () => this.#updateUI());
|
||||||
|
|
||||||
|
const ability = this.element.querySelector("#ability");
|
||||||
|
if (ability && this.actor.type === "character") {
|
||||||
|
const sc = this.element.querySelector("#self_control");
|
||||||
|
if (sc) sc.max = ability.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Getters ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
get #el() { return this.element; }
|
||||||
|
|
||||||
|
#getDifficultyBase() {
|
||||||
|
const el = this.#el.querySelector('input[name="combat-difficulty"]:checked');
|
||||||
|
return parseInt(el?.value, 10) || 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
#getSecondAction() {
|
||||||
|
return this.#el.querySelector("#second-action")?.checked ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#getHandicap() {
|
||||||
|
const sel = this.#el.querySelector("#handicap");
|
||||||
|
return Math.max(0, (parseInt(sel?.value, 10) || 1) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#getSelfCtrl() {
|
||||||
|
return parseInt(this.#el.querySelector("#self_control")?.value, 10) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#getPoolsSpent() {
|
||||||
|
return parseInt(this.#el.querySelector("#pools-spent")?.value, 10) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#getUseAmmo() {
|
||||||
|
return this.#el.querySelector("#use-ammo")?.checked ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#getTotems() {
|
||||||
|
return {
|
||||||
|
human: this.#el.querySelector("#human-totem")?.checked ?? false,
|
||||||
|
adapted: this.#el.querySelector("#adapted-totem")?.checked ?? false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#getKeepTotem() {
|
||||||
|
return this.#el.querySelector("#keep-totem-select")?.value ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDifficultySelect() {
|
||||||
|
return this.#getDifficultyBase() + (this.#getSecondAction() ? 2 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSkillCategory() {
|
||||||
|
if (this.actor.type !== "character" || !this.skillKey) return null;
|
||||||
|
return this.actor.system.skills[this.skillKey]?.category ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSkillLevel() {
|
||||||
|
return this.actor.type === "character" && this.skillValue ? this.skillValue : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getReroll() {
|
||||||
|
if (this.actor.type === "npc") {
|
||||||
|
const exp = this.actor.system?.experience?.value || 1;
|
||||||
|
return CONFIG.VERMINE.npcExperienceLevels?.[exp]?.rerolls || 0;
|
||||||
|
}
|
||||||
|
if (this.actor.type === "creature") return 0;
|
||||||
|
return this.skillReroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMaxEffort() {
|
||||||
|
if (this.actor.type === "character") return this.actor.system?.abilities?.[this.abilityKey]?.value || 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPoolBreakdown() {
|
||||||
|
const ability = this.actor.type === "character"
|
||||||
|
? (this.actor.system?.abilities?.[this.abilityKey]?.value || 0)
|
||||||
|
: 0;
|
||||||
|
const skill = this.actor.type === "character" ? this.skillPool : 0;
|
||||||
|
const selfControl = this.#getSelfCtrl();
|
||||||
|
const pools = this.actor.type === "npc" ? this.#getPoolsSpent() : 0;
|
||||||
|
const specialty = this.#hasSpecialtySelected() ? 1 : 0;
|
||||||
|
const help = this.#el.querySelector("#helped")?.checked ? 1 : 0;
|
||||||
|
const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked");
|
||||||
|
const tooling = toolsChecked && toolsChecked.value !== "0" ? 1 : 0;
|
||||||
|
const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
|
||||||
|
const total = this.basePool + selfControl + pools + specialty + help + tooling + group;
|
||||||
|
return { ability, skill, selfControl, pools, specialty, help, tooling, group, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
getDicePool() {
|
||||||
|
return this.getPoolBreakdown().total;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hasSpecialtySelected() {
|
||||||
|
const checked = this.#el.querySelector("input[name='usingSpecialization']:checked");
|
||||||
|
return Boolean(checked && checked.value !== "aucune");
|
||||||
|
}
|
||||||
|
|
||||||
|
getSpecialtyName() {
|
||||||
|
const checked = this.#el.querySelector("input[name='usingSpecialization']:checked");
|
||||||
|
return checked && checked.value !== "aucune" ? checked.value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLabel() {
|
||||||
|
const base = this.mode === "ranged"
|
||||||
|
? game.i18n.localize("VERMINE.attack_ranged")
|
||||||
|
: game.i18n.localize("VERMINE.attack_contact");
|
||||||
|
return this.weapon ? `${base} (${this.weapon.name})` : base;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── UI ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#updateUI() {
|
||||||
|
const total = this.getDicePool();
|
||||||
|
const totalEl = this.#el.querySelector("#dice-pool-total");
|
||||||
|
if (totalEl) totalEl.textContent = `${total}D`;
|
||||||
|
|
||||||
|
const finalEls = this.#el.querySelectorAll("#final-difficulty");
|
||||||
|
for (const el of finalEls) el.textContent = this.getDifficultySelect();
|
||||||
|
|
||||||
|
const b = this.getPoolBreakdown();
|
||||||
|
const bonusEl = this.#el.querySelector("#total-bonus");
|
||||||
|
if (bonusEl) bonusEl.textContent = b.selfControl + b.pools + b.specialty + b.help + b.tooling + b.group;
|
||||||
|
|
||||||
|
const handSel = this.#el.querySelector("#handicap");
|
||||||
|
const handEl = this.#el.querySelector("#current-handicap");
|
||||||
|
if (handSel && handEl) {
|
||||||
|
handEl.textContent = handSel.options[handSel.selectedIndex]?.text ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#onInputChange() {
|
||||||
|
this.#updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
#onChangeSelfControl(ev) {
|
||||||
|
const valEl = this.#el.querySelector("#self_control_value");
|
||||||
|
if (valEl) valEl.textContent = ev.currentTarget.value;
|
||||||
|
this.#updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onCancel(event, target) {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onRoll(event, target) {
|
||||||
|
const selfCtrl = this.getPoolBreakdown().selfControl;
|
||||||
|
if (selfCtrl > 0) {
|
||||||
|
const current = this.actor?.system?.attributes?.self_control?.value ?? 0;
|
||||||
|
if (current < selfCtrl) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_not_enough_self_control"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalDifficulty = this.getDifficultySelect();
|
||||||
|
|
||||||
|
// Consommation de munitions (armes à distance)
|
||||||
|
if (this.mode === "ranged" && this.#getUseAmmo() && this.weapon) {
|
||||||
|
const ammo = this.weapon.system.ammo || 0;
|
||||||
|
if (ammo > 0) {
|
||||||
|
await this.weapon.update({ "system.ammo": ammo - 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selfCtrl > 0) {
|
||||||
|
const newVal = this.actor.system.attributes.self_control.value - selfCtrl;
|
||||||
|
await this.actor.update({ "system.attributes.self_control.value": newVal });
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets = [...game.user.targets].map(t => ({
|
||||||
|
id: t.actor?.id ?? null,
|
||||||
|
uuid: t.actor?.uuid ?? null,
|
||||||
|
name: t.name,
|
||||||
|
canDefend: t.actor?.type !== "creature"
|
||||||
|
}));
|
||||||
|
|
||||||
|
const attack = {
|
||||||
|
id: foundry.utils.randomID(),
|
||||||
|
attackerId: this.actor.id,
|
||||||
|
attackerName: this.actor.name,
|
||||||
|
mode: this.mode,
|
||||||
|
difficulty: finalDifficulty,
|
||||||
|
range: this.mode === "ranged" ? this.#getRangeKey() : null,
|
||||||
|
secondAction: this.#getSecondAction(),
|
||||||
|
baseDamage: this.baseDamage,
|
||||||
|
targets,
|
||||||
|
weapon: this.weapon
|
||||||
|
? { name: this.weapon.name, img: this.weapon.img, damage: foundry.utils.duplicate(this.weapon.system.damage) }
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
|
||||||
|
await VermineUtils.roll({
|
||||||
|
actor: this.actor,
|
||||||
|
NoD: this.getDicePool(),
|
||||||
|
Reroll: this.getReroll(),
|
||||||
|
difficulty: finalDifficulty,
|
||||||
|
handicap: this.#getHandicap(),
|
||||||
|
rollLabel: this.getLabel(),
|
||||||
|
totems: this.#getTotems(),
|
||||||
|
self_control: selfCtrl,
|
||||||
|
max_effort: this.getMaxEffort(),
|
||||||
|
keepTotem: this.#getKeepTotem(),
|
||||||
|
skillCategory: this.getSkillCategory(),
|
||||||
|
skillLevel: this.getSkillLevel(),
|
||||||
|
hasSpecialty: this.#hasSpecialtySelected(),
|
||||||
|
poolBreakdown: this.getPoolBreakdown(),
|
||||||
|
specialtyName: this.getSpecialtyName(),
|
||||||
|
weapon: this.weapon?.toObject() ?? null,
|
||||||
|
targets: targets.map(t => t.name),
|
||||||
|
attack,
|
||||||
|
messageFlags: { "vermine-exchange": attack }
|
||||||
|
});
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
#getRangeKey() {
|
||||||
|
const el = this.#el.querySelector('input[name="combat-difficulty"]:checked');
|
||||||
|
return el?.dataset?.rangeKey ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialogue du meneur pour ouvrir une phase d'Expérience.
|
||||||
|
* Permet d'attribuer les Dés d'Expérience collectifs (Danger, Découvertes,
|
||||||
|
* Durée) au Groupe et aux personnages, ainsi que les Dés individuels
|
||||||
|
* (Implication, Influence, Interprétation), les récompenses d'Objectifs et
|
||||||
|
* la récompense de Traumatisme choisi.
|
||||||
|
*/
|
||||||
|
export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
||||||
|
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
classes: ["vermine-roll"],
|
||||||
|
tag: "form",
|
||||||
|
window: {
|
||||||
|
icon: "fas fa-star",
|
||||||
|
resizable: true
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
width: 620,
|
||||||
|
height: 700
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
apply: ExperiencePhaseDialog.#onApply,
|
||||||
|
cancel: ExperiencePhaseDialog.#onCancel
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
main: { template: "systems/vermine2047/templates/dialogs/experience-phase-dialog.hbs", scrollable: [""] }
|
||||||
|
};
|
||||||
|
|
||||||
|
static async create() {
|
||||||
|
return new ExperiencePhaseDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
return game.i18n.localize("VERMINE.experience_phase");
|
||||||
|
}
|
||||||
|
|
||||||
|
async _prepareContext() {
|
||||||
|
const characters = game.actors.filter(a => a.type === "character");
|
||||||
|
const groups = game.actors.filter(a => a.type === "group");
|
||||||
|
return {
|
||||||
|
characters: characters.map(a => ({
|
||||||
|
id: a.id,
|
||||||
|
name: a.name,
|
||||||
|
dice: a.system?.experience?.dice || 0
|
||||||
|
})),
|
||||||
|
groups: groups.map(a => ({
|
||||||
|
id: a.id,
|
||||||
|
name: a.name,
|
||||||
|
dice: a.system?.experience?.dice || 0
|
||||||
|
})),
|
||||||
|
rewards: [
|
||||||
|
{ value: 0, label: game.i18n.localize("VERMINE.reward_anecdotic") },
|
||||||
|
{ value: 1, label: game.i18n.localize("VERMINE.reward_minor") },
|
||||||
|
{ value: 2, label: game.i18n.localize("VERMINE.reward_major") },
|
||||||
|
{ value: 3, label: game.i18n.localize("VERMINE.reward_exceptionnal") }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onRender(context, options) {
|
||||||
|
this.element.querySelectorAll("[data-roll]").forEach(inp => inp.addEventListener("change", () => this.#updateUI()));
|
||||||
|
this.#updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateUI() {
|
||||||
|
const danger = parseInt(this.element.querySelector("#collective-danger")?.value, 10) || 0;
|
||||||
|
const discoveries = parseInt(this.element.querySelector("#collective-discoveries")?.value, 10) || 0;
|
||||||
|
const duration = parseInt(this.element.querySelector("#collective-duration")?.value, 10) || 0;
|
||||||
|
const el = this.element.querySelector("#collective-total");
|
||||||
|
if (el) el.textContent = `${danger + discoveries + duration}D`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onCancel(event, target) {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onApply(event, target) {
|
||||||
|
const collective = (parseInt(this.element.querySelector("#collective-danger")?.value, 10) || 0)
|
||||||
|
+ (parseInt(this.element.querySelector("#collective-discoveries")?.value, 10) || 0)
|
||||||
|
+ (parseInt(this.element.querySelector("#collective-duration")?.value, 10) || 0);
|
||||||
|
|
||||||
|
const groupId = this.element.querySelector("#group-select")?.value || null;
|
||||||
|
if (groupId) {
|
||||||
|
const group = game.actors.get(groupId);
|
||||||
|
if (group && collective > 0) {
|
||||||
|
const current = group.system?.experience?.dice || 0;
|
||||||
|
await group.update({ "system.experience.dice": current + collective });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
for (const row of this.element.querySelectorAll("[data-character-id]")) {
|
||||||
|
const id = row.dataset.characterId;
|
||||||
|
const actor = game.actors.get(id);
|
||||||
|
if (!actor) continue;
|
||||||
|
const implication = parseInt(row.querySelector(`[name="implication-${id}"]`)?.value, 10) || 0;
|
||||||
|
const influence = parseInt(row.querySelector(`[name="influence-${id}"]`)?.value, 10) || 0;
|
||||||
|
const interpretation = parseInt(row.querySelector(`[name="interpretation-${id}"]`)?.value, 10) || 0;
|
||||||
|
const objectiveMinor = row.querySelector(`[name="objective-minor-${id}"]`)?.checked ? 3 : 0;
|
||||||
|
const objectiveMajor = row.querySelector(`[name="objective-major-${id}"]`)?.checked ? 5 : 0;
|
||||||
|
const trauma = row.querySelector(`[name="trauma-${id}"]`)?.checked ? 5 : 0;
|
||||||
|
|
||||||
|
const individual = implication + influence + interpretation;
|
||||||
|
const bonus = objectiveMinor + objectiveMajor + trauma;
|
||||||
|
const gained = collective + individual + bonus;
|
||||||
|
|
||||||
|
const ex = actor.system?.experience;
|
||||||
|
const nextPhase = (ex?.phase || 0) + 1;
|
||||||
|
const update = {
|
||||||
|
"system.experience.phase": nextPhase,
|
||||||
|
"system.experience.dice": (ex?.dice || 0) + gained,
|
||||||
|
"system.experience.collective.danger": parseInt(this.element.querySelector("#collective-danger")?.value, 10) || 0,
|
||||||
|
"system.experience.collective.discoveries": parseInt(this.element.querySelector("#collective-discoveries")?.value, 10) || 0,
|
||||||
|
"system.experience.collective.duration": parseInt(this.element.querySelector("#collective-duration")?.value, 10) || 0,
|
||||||
|
"system.experience.individual.implication": implication,
|
||||||
|
"system.experience.individual.influence": influence,
|
||||||
|
"system.experience.individual.interpretation": interpretation,
|
||||||
|
"system.experience.traumaReward": trauma > 0,
|
||||||
|
"system.experience.learned.phase": nextPhase,
|
||||||
|
"system.experience.learned.skill": false,
|
||||||
|
"system.experience.learned.specialty": false,
|
||||||
|
"system.experience.learned.reserve": false,
|
||||||
|
"system.experience.learned.characteristic": false,
|
||||||
|
"system.experience.learned.evolution": false
|
||||||
|
};
|
||||||
|
await actor.update(update);
|
||||||
|
results.push({ name: actor.name, gained });
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = [`${game.i18n.localize("VERMINE.experience_phase")} +1`];
|
||||||
|
if (groupId) {
|
||||||
|
const group = game.actors.get(groupId);
|
||||||
|
lines.push(`${group?.name ?? ""} (${game.i18n.localize("VERMINE.collective")}): +${collective}D`);
|
||||||
|
}
|
||||||
|
for (const r of results) {
|
||||||
|
lines.push(`${r.name}: +${r.gained}D`);
|
||||||
|
}
|
||||||
|
await ChatMessage.create({
|
||||||
|
user: game.user?._id,
|
||||||
|
speaker: ChatMessage.getSpeaker(),
|
||||||
|
content: `<div class="vermine-roll-message"><h3>${game.i18n.localize("VERMINE.experience_rewards")}</h3><div class="roll-context flexrow flex-wrap">${lines.map(l => `<span class="context-item">${l}</span>`).join("")}</div></div>`
|
||||||
|
});
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
import { VermineUtils } from "../roll.mjs";
|
||||||
|
import { VermineExperience } from "../experience.mjs";
|
||||||
|
|
||||||
|
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialogue d'apprentissage (phase d'Expérience).
|
||||||
|
* Permet de dépenser des Dés d'Expérience pour tenter de développer une
|
||||||
|
* Compétence, une Spécialité, une Caractéristique, une Réserve ou un
|
||||||
|
* Dé d'Évolution. La progression est appliquée automatiquement en cas de
|
||||||
|
* réussite.
|
||||||
|
*/
|
||||||
|
export default class LearningDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
||||||
|
|
||||||
|
#actor;
|
||||||
|
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
classes: ["vermine-roll"],
|
||||||
|
tag: "form",
|
||||||
|
window: {
|
||||||
|
icon: "fas fa-graduation-cap",
|
||||||
|
resizable: false
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
width: 560,
|
||||||
|
height: 640
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
roll: LearningDialog.#onRoll,
|
||||||
|
cancel: LearningDialog.#onCancel
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
main: { template: "systems/vermine2047/templates/dialogs/learning-dialog.hbs", scrollable: [""] }
|
||||||
|
};
|
||||||
|
|
||||||
|
static async create({ actorId }) {
|
||||||
|
const actor = await game.actors.get(actorId);
|
||||||
|
if (!actor) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new LearningDialog({ actor });
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options = {}) {
|
||||||
|
super(options);
|
||||||
|
this.#actor = options.actor;
|
||||||
|
}
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
return game.i18n.localize("VERMINE.open_learning");
|
||||||
|
}
|
||||||
|
|
||||||
|
async _prepareContext() {
|
||||||
|
const actor = this.#actor;
|
||||||
|
const skills = Object.entries(CONFIG.VERMINE.skills).map(([key, cfg]) => ({
|
||||||
|
key,
|
||||||
|
name: game.i18n.localize("SKILLS." + key + ".name"),
|
||||||
|
category: cfg.category,
|
||||||
|
value: actor.system?.skills?.[key]?.value || 0,
|
||||||
|
target: Math.min(5, (actor.system?.skills?.[key]?.value || 0) + 1),
|
||||||
|
rarity: actor.system?.skills?.[key]?.rarity || 0,
|
||||||
|
isDomain: VermineExperience.isPreferredDomain(actor, key)
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
actor,
|
||||||
|
system: actor.system,
|
||||||
|
config: CONFIG.VERMINE,
|
||||||
|
dice: actor.system?.experience?.dice || 0,
|
||||||
|
ageType: actor.system?.identity?.ageType || 2,
|
||||||
|
abilities: Object.entries(CONFIG.VERMINE.abilities).map(([key, label]) => ({
|
||||||
|
key,
|
||||||
|
label: game.i18n.localize(label),
|
||||||
|
value: actor.system?.abilities?.[key]?.value || 0
|
||||||
|
})),
|
||||||
|
reserves: [
|
||||||
|
{ key: "self_control", label: game.i18n.localize("VERMINE.self_control"), value: actor.system?.attributes?.self_control?.value || 0, max: actor.system?.attributes?.self_control?.max || 0 },
|
||||||
|
{ key: "effort", label: game.i18n.localize("VERMINE.effort"), value: actor.system?.attributes?.effort?.value || 0, max: actor.system?.attributes?.effort?.max || 0 }
|
||||||
|
],
|
||||||
|
specialties: actor.itemTypes?.specialty || [],
|
||||||
|
skills,
|
||||||
|
specialtyCount: (actor.itemTypes?.specialty || []).length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onRender(context, options) {
|
||||||
|
this.element.dataset.actorId = this.#actor.id;
|
||||||
|
for (const inp of this.element.querySelectorAll("[data-roll]")) {
|
||||||
|
inp.addEventListener("change", () => this.#updateUI());
|
||||||
|
}
|
||||||
|
this.element.querySelector("#learning-dice")?.addEventListener("input", () => this.#updateUI());
|
||||||
|
this.#updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
get #el() { return this.element; }
|
||||||
|
|
||||||
|
#getType() {
|
||||||
|
return this.#el.querySelector('input[name="learning-type"]:checked')?.value ?? "skill";
|
||||||
|
}
|
||||||
|
|
||||||
|
#getTargetKey() {
|
||||||
|
const type = this.#getType();
|
||||||
|
if (type === "characteristic") return this.#el.querySelector("#learning-ability")?.value ?? "";
|
||||||
|
if (type === "reserve") return this.#el.querySelector("#learning-reserve")?.value ?? "";
|
||||||
|
return this.#el.querySelector("#learning-target")?.value ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
#getDiceSpent() {
|
||||||
|
return Math.max(1, parseInt(this.#el.querySelector("#learning-dice")?.value, 10) || 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#getSpecialtyName() {
|
||||||
|
return this.#el.querySelector("#specialty-name")?.value?.trim() || game.i18n.localize("ITEMS.new_specialty");
|
||||||
|
}
|
||||||
|
|
||||||
|
#compute() {
|
||||||
|
const actor = this.#actor;
|
||||||
|
const type = this.#getType();
|
||||||
|
const key = this.#getTargetKey();
|
||||||
|
const difficulty = 7;
|
||||||
|
let handicap = 0;
|
||||||
|
let poolModifier = 0;
|
||||||
|
let label = "";
|
||||||
|
let targetLevel = null;
|
||||||
|
|
||||||
|
if (type === "skill") {
|
||||||
|
const current = key ? actor.system?.skills?.[key]?.value || 0 : 0;
|
||||||
|
targetLevel = Math.min(5, current + 1);
|
||||||
|
const rarity = key ? actor.system?.skills?.[key]?.rarity || 0 : 0;
|
||||||
|
label = game.i18n.localize(VermineExperience.skillLevelLabelKey(targetLevel));
|
||||||
|
const diff = VermineExperience.skillLearningDifficulty(targetLevel);
|
||||||
|
const domain = key ? VermineExperience.isPreferredDomain(actor, key) : false;
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
key,
|
||||||
|
targetLevel,
|
||||||
|
difficulty: domain ? Math.max(3, diff - 2) : diff,
|
||||||
|
handicap: VermineExperience.skillLearningHandicap(targetLevel, rarity),
|
||||||
|
poolModifier: 0,
|
||||||
|
label,
|
||||||
|
targetLabel: key ? game.i18n.localize("SKILLS." + key + ".name") : ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (type === "specialty") {
|
||||||
|
const current = key ? actor.system?.skills?.[key]?.value || 0 : 0;
|
||||||
|
const domain = key ? VermineExperience.isPreferredDomain(actor, key) : false;
|
||||||
|
label = game.i18n.localize("VERMINE.specialty");
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
key,
|
||||||
|
targetLevel: current,
|
||||||
|
difficulty: VermineExperience.specialtyLearningDifficulty(current, domain),
|
||||||
|
handicap: VermineExperience.specialtyLearningHandicap(actor.itemTypes?.specialty?.length || 0),
|
||||||
|
poolModifier: 0,
|
||||||
|
label,
|
||||||
|
targetLabel: key ? game.i18n.localize("SKILLS." + key + ".name") : ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (type === "characteristic") {
|
||||||
|
const current = key ? actor.system?.abilities?.[key]?.value || 0 : 0;
|
||||||
|
label = game.i18n.localize("VERMINE.characteristic");
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
key,
|
||||||
|
targetLevel: current,
|
||||||
|
difficulty: VermineExperience.characteristicLearningDifficulty(),
|
||||||
|
handicap: current,
|
||||||
|
poolModifier: 0,
|
||||||
|
label,
|
||||||
|
targetLabel: key ? game.i18n.localize(CONFIG.VERMINE.abilities[key]) : ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (type === "reserve") {
|
||||||
|
const current = key ? actor.system?.attributes?.[key]?.value || 0 : 0;
|
||||||
|
label = game.i18n.localize("VERMINE.reserve");
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
key,
|
||||||
|
targetLevel: current,
|
||||||
|
difficulty: VermineExperience.reserveLearningDifficulty(current),
|
||||||
|
handicap: VermineExperience.ageHandicap(actor.system?.identity?.ageType || 2),
|
||||||
|
poolModifier: 0,
|
||||||
|
label,
|
||||||
|
targetLabel: key ? game.i18n.localize(key === "self_control" ? "VERMINE.self_control" : "VERMINE.effort") : ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// evolution
|
||||||
|
const human = actor.system?.adaptation?.totems?.human?.value || 0;
|
||||||
|
const adapted = actor.system?.adaptation?.totems?.adapted?.value || 0;
|
||||||
|
label = game.i18n.localize("VERMINE.evolution_dice");
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
key,
|
||||||
|
targetLevel: actor.system?.experience?.evolution || 0,
|
||||||
|
difficulty: VermineExperience.evolutionDifficulty(actor.system?.identity?.ageType || 2),
|
||||||
|
handicap: actor.system?.experience?.evolution || 0,
|
||||||
|
poolModifier: VermineExperience.evolutionTotemModifier(human, adapted),
|
||||||
|
label,
|
||||||
|
targetLabel: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateUI() {
|
||||||
|
const type = this.#getType();
|
||||||
|
const show = (id, visible) => {
|
||||||
|
const el = this.#el.querySelector(id);
|
||||||
|
if (el) el.style.display = visible ? "" : "none";
|
||||||
|
};
|
||||||
|
show("#learning-skill-section", type === "skill" || type === "specialty");
|
||||||
|
show("#learning-specialty-section", type === "specialty");
|
||||||
|
show("#learning-ability-section", type === "characteristic");
|
||||||
|
show("#learning-reserve-section", type === "reserve");
|
||||||
|
show("#learning-evolution-section", type === "evolution");
|
||||||
|
|
||||||
|
// Pour une Spécialité, seule une compétence au moins Confirmée (≥2) est valable.
|
||||||
|
const skillSelect = this.#el.querySelector("#learning-target");
|
||||||
|
if (skillSelect) {
|
||||||
|
for (const opt of skillSelect.options) {
|
||||||
|
const value = parseInt(opt.dataset.value, 10) || 0;
|
||||||
|
opt.hidden = (type === "specialty") ? value < 2 : false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const c = this.#compute();
|
||||||
|
const set = (sel, txt) => { const el = this.#el.querySelector(sel); if (el) el.textContent = txt; };
|
||||||
|
set("#diff-value", c.difficulty);
|
||||||
|
set("#handicap-value", c.handicap);
|
||||||
|
set("#required-value", 1 + c.handicap);
|
||||||
|
const pool = Math.max(0, this.#getDiceSpent() + c.poolModifier);
|
||||||
|
set("#pool-value", `${pool}D${c.poolModifier ? ` (${c.poolModifier >= 0 ? "+" : ""}${c.poolModifier}D)` : ""}`);
|
||||||
|
const poolInput = this.#el.querySelector("#learning-dice");
|
||||||
|
if (poolInput) poolInput.max = this.#actor.system?.experience?.dice || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onCancel(event, target) {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onRoll(event, target) {
|
||||||
|
const actor = this.#actor;
|
||||||
|
const ex = actor.system?.experience;
|
||||||
|
const dice = ex?.dice || 0;
|
||||||
|
const spent = Math.min(this.#getDiceSpent(), dice);
|
||||||
|
if (spent < 1) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_experience_dice"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const c = this.#compute();
|
||||||
|
|
||||||
|
// Limite : une seule compétence, spécialité, réserve, caractéristique et
|
||||||
|
// un seul Dé d'Évolution par phase.
|
||||||
|
if (ex?.learned?.phase === ex?.phase) {
|
||||||
|
const flagMap = { skill: "skill", specialty: "specialty", reserve: "reserve", characteristic: "characteristic", evolution: "evolution" };
|
||||||
|
if (ex.learned[flagMap[c.type]]) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_learning_limit"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c.type === "skill" && c.targetLevel !== null && (actor.system?.skills?.[c.key]?.value || 0) >= 5) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_skill_max"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (c.type === "characteristic" && c.targetLevel !== null && (actor.system?.abilities?.[c.key]?.value || 0) >= 3) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_characteristic_max"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (c.type === "specialty" && c.targetLevel !== null && c.targetLevel < 2) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_specialty_requirement"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (c.type === "reserve" && c.targetLevel !== null && (actor.system?.attributes?.[c.key]?.max || 0) >= 10) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_reserve_max"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pool = Math.max(0, spent + c.poolModifier);
|
||||||
|
const rollLabel = `${game.i18n.localize("VERMINE.open_learning")} — ${c.label}${c.targetLabel ? " (" + c.targetLabel + ")" : ""}`;
|
||||||
|
|
||||||
|
const roll = await VermineUtils.roll({
|
||||||
|
actor,
|
||||||
|
NoD: pool,
|
||||||
|
Reroll: 0,
|
||||||
|
difficulty: c.difficulty,
|
||||||
|
handicap: c.handicap,
|
||||||
|
rollLabel,
|
||||||
|
poolBreakdown: { ability: 0, skill: 0, selfControl: 0, specialty: 0, help: 0, tooling: 0, group: 0, total: pool },
|
||||||
|
messageFlags: { "vermine-learning": { type: c.type, key: c.key, spent, success: false } }
|
||||||
|
});
|
||||||
|
|
||||||
|
const required = 1 + c.handicap;
|
||||||
|
const specialtyName = this.#getSpecialtyName();
|
||||||
|
if ((roll._total ?? 0) >= required) {
|
||||||
|
await this.#applySuccess(c, spent, specialtyName);
|
||||||
|
}
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async #applySuccess(c, spent, specialtyName) {
|
||||||
|
const actor = this.#actor;
|
||||||
|
const updates = { "system.experience.dice": (actor.system.experience.dice || 0) - spent };
|
||||||
|
if (c.type === "skill") {
|
||||||
|
const current = actor.system.skills[c.key].value || 0;
|
||||||
|
updates["system.skills." + c.key + ".value"] = Math.min(5, current + 1);
|
||||||
|
updates["system.experience.learned.skill"] = true;
|
||||||
|
} else if (c.type === "specialty") {
|
||||||
|
await actor.createEmbeddedDocuments("Item", [{ name: specialtyName, type: "specialty", system: { skill: c.key } }]);
|
||||||
|
updates["system.experience.learned.specialty"] = true;
|
||||||
|
} else if (c.type === "characteristic") {
|
||||||
|
const current = actor.system.abilities[c.key].value || 0;
|
||||||
|
updates["system.abilities." + c.key + ".value"] = Math.min(3, current + 1);
|
||||||
|
updates["system.experience.learned.characteristic"] = true;
|
||||||
|
} else if (c.type === "reserve") {
|
||||||
|
const bonus = actor.system.experience.reserveBonus?.[c.key] || 0;
|
||||||
|
updates["system.experience.reserveBonus." + c.key] = bonus + 1;
|
||||||
|
updates["system.experience.learned.reserve"] = true;
|
||||||
|
} else {
|
||||||
|
updates["system.experience.evolution"] = (actor.system.experience.evolution || 0) + 1;
|
||||||
|
updates["system.experience.learned.evolution"] = true;
|
||||||
|
}
|
||||||
|
updates["system.experience.learned.phase"] = actor.system.experience.phase;
|
||||||
|
await actor.update(updates);
|
||||||
|
ui.notifications.info(game.i18n.localize("VERMINE.learning_success"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,8 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
roll: RollDialog.#onRoll,
|
roll: RollDialog.#onRoll,
|
||||||
cancel: RollDialog.#onCancel
|
cancel: RollDialog.#onCancel,
|
||||||
|
giveToGroup: RollDialog.#onGiveToGroup
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,6 +33,9 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
};
|
};
|
||||||
|
|
||||||
static async create(data = {}) {
|
static async create(data = {}) {
|
||||||
|
if (data.actor instanceof Actor) {
|
||||||
|
return new RollDialog({ actor: data.actor, label: data.label, rolltype: data.rolltype, weapon: data.weapon, locks: data.locks, defense: data.defense });
|
||||||
|
}
|
||||||
const actorId = data.actorId ?? game.user.character?.id ?? canvas.tokens.controlled[0]?.actor?.id;
|
const actorId = data.actorId ?? game.user.character?.id ?? canvas.tokens.controlled[0]?.actor?.id;
|
||||||
if (!actorId || typeof actorId !== "string") {
|
if (!actorId || typeof actorId !== "string") {
|
||||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
|
||||||
@@ -42,7 +46,7 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return new RollDialog({ actor, label: data.label, rolltype: data.rolltype });
|
return new RollDialog({ actor, label: data.label, rolltype: data.rolltype, weapon: data.weapon, locks: data.locks, defense: data.defense });
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
@@ -50,10 +54,16 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
this.#actor = options.actor;
|
this.#actor = options.actor;
|
||||||
this.label = options.label ?? null;
|
this.label = options.label ?? null;
|
||||||
this.rolltype = options.rolltype ?? null;
|
this.rolltype = options.rolltype ?? null;
|
||||||
|
this.weapon = options.weapon ?? null;
|
||||||
|
this.locks = options.locks ?? null;
|
||||||
|
this.defense = options.defense ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _prepareContext() {
|
async _prepareContext() {
|
||||||
const actor = this.#actor;
|
const actor = this.#actor;
|
||||||
|
const isCharacter = actor.type === "character";
|
||||||
|
const group = isCharacter ? RollDialog.#findGroup(actor) : null;
|
||||||
|
const groupPool = group?.system?.experience?.dice || 0;
|
||||||
return {
|
return {
|
||||||
actor,
|
actor,
|
||||||
system: actor.system,
|
system: actor.system,
|
||||||
@@ -66,13 +76,28 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
help: false,
|
help: false,
|
||||||
specialty: false,
|
specialty: false,
|
||||||
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
|
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
|
||||||
availableItems: actor.items.filter(i => i.type === "item")
|
availableItems: actor.items.filter(i => i.type === "item"),
|
||||||
|
locks: this.locks,
|
||||||
|
experience: {
|
||||||
|
hasExperience: isCharacter,
|
||||||
|
individual: isCharacter ? actor.system?.experience?.dice || 0 : 0,
|
||||||
|
groupPool,
|
||||||
|
pullOptions: [0, 1, 2].filter(v => v <= Math.min(2, groupPool)),
|
||||||
|
canPull: Boolean(group) && (game.user.isGM || group.isOwner),
|
||||||
|
canGive: isCharacter && (game.user.isGM || actor.isOwner)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onRender(context, options) {
|
async _onRender(context, options) {
|
||||||
this.element.dataset.actorId = this.#actor.id;
|
this.element.dataset.actorId = this.#actor.id;
|
||||||
|
|
||||||
|
// Mode verrouillé (défense en échange de coups) : présélectionne et bloque
|
||||||
|
// caractéristique + difficulté, présélectionne la compétence.
|
||||||
|
if (this.locks) {
|
||||||
|
this.#applyLocks(this.locks);
|
||||||
|
}
|
||||||
|
|
||||||
for (const inp of this.element.querySelectorAll("[data-roll]")) {
|
for (const inp of this.element.querySelectorAll("[data-roll]")) {
|
||||||
inp.addEventListener("change", this.#onInputChange.bind(this));
|
inp.addEventListener("change", this.#onInputChange.bind(this));
|
||||||
}
|
}
|
||||||
@@ -95,6 +120,7 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
this.element.querySelector("#adapted-totem")?.addEventListener("change", () => this.#updateUI());
|
this.element.querySelector("#adapted-totem")?.addEventListener("change", () => this.#updateUI());
|
||||||
|
|
||||||
this.#displaySpecialties();
|
this.#displaySpecialties();
|
||||||
|
this.#refreshExperienceUI();
|
||||||
this.#updateUI();
|
this.#updateUI();
|
||||||
|
|
||||||
if (ability?.value !== "0") {
|
if (ability?.value !== "0") {
|
||||||
@@ -111,18 +137,44 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
#getDifficulty() { return this.#el.querySelector("#difficulty"); }
|
#getDifficulty() { return this.#el.querySelector("#difficulty"); }
|
||||||
#getHandicap() { return this.#el.querySelector("#handicap"); }
|
#getHandicap() { return this.#el.querySelector("#handicap"); }
|
||||||
#getSelfCtrl() { return this.#el.querySelector("#self_control"); }
|
#getSelfCtrl() { return this.#el.querySelector("#self_control"); }
|
||||||
|
#getExperiencePull() { return parseInt(this.#el.querySelector("#experience-pull")?.value, 10) || 0; }
|
||||||
|
|
||||||
getDicePool() {
|
/**
|
||||||
|
* Trouve le premier Groupe dont le personnage est membre.
|
||||||
|
* @param {Actor} actor
|
||||||
|
* @returns {Actor|null}
|
||||||
|
*/
|
||||||
|
static #findGroup(actor) {
|
||||||
|
return game.actors.filter(a => a.type === "group" && (a.system?.members ?? []).includes(actor.id))[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Décomposition du pool de dés pour affichage dans le message. */
|
||||||
|
getPoolBreakdown() {
|
||||||
const abil = this.#getAbility();
|
const abil = this.#getAbility();
|
||||||
const abilVal = parseInt(abil?.options[abil?.selectedIndex]?.value, 10) || 0;
|
const ability = parseInt(abil?.options[abil?.selectedIndex]?.value, 10) || 0;
|
||||||
const skill = this.#getSkill();
|
const skill = this.#getSkill();
|
||||||
const skillPool = parseInt(skill?.options[skill?.selectedIndex]?.dataset?.pool, 10) || 0;
|
const skillPool = parseInt(skill?.options[skill?.selectedIndex]?.dataset?.pool, 10) || 0;
|
||||||
const sc = parseInt(this.#getSelfCtrl()?.value, 10) || 0;
|
const selfControl = parseInt(this.#getSelfCtrl()?.value, 10) || 0;
|
||||||
const specChecked = this.#el.querySelector("#usingSpecialization")?.checked;
|
const specChecked = this.hasSpecialtySelected();
|
||||||
const helped = this.#el.querySelector("#helped")?.checked;
|
const helped = this.#el.querySelector("#helped")?.checked;
|
||||||
const tools = this.#el.querySelector("input[name='usingTools']:checked")?.value !== "0";
|
const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked");
|
||||||
const bonuses = (specChecked ? 1 : 0) + (helped ? 1 : 0) + (tools ? 1 : 0);
|
const tools = toolsChecked && toolsChecked.value !== "0";
|
||||||
return (abilVal + sc + skillPool + bonuses) || 0;
|
const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
|
||||||
|
const experience = this.#getExperiencePull();
|
||||||
|
const specialty = specChecked ? 1 : 0;
|
||||||
|
const help = helped ? 1 : 0;
|
||||||
|
const tooling = tools ? 1 : 0;
|
||||||
|
const total = ability + skillPool + selfControl + specialty + help + tooling + group + experience;
|
||||||
|
return { ability, skill: skillPool, selfControl, specialty, help, tooling, group, experience, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
getDicePool() {
|
||||||
|
return this.getPoolBreakdown().total;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSpecialtyName() {
|
||||||
|
const checked = this.#el.querySelector("input[name='usingSpecialization']:checked");
|
||||||
|
return checked && checked.value !== "aucune" ? checked.value : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDifficultySelect() {
|
getDifficultySelect() {
|
||||||
@@ -139,7 +191,7 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
|
|
||||||
getHandicapSelect() {
|
getHandicapSelect() {
|
||||||
const sel = this.#getHandicap();
|
const sel = this.#getHandicap();
|
||||||
return parseInt(sel?.value, 10) || 1;
|
return Math.max(0, (parseInt(sel?.value, 10) || 1) - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSkillCategory() {
|
getSkillCategory() {
|
||||||
@@ -199,6 +251,40 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
|
|
||||||
// ── UI ───────────────────────────────────────────────────────────────
|
// ── UI ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#applyLocks(locks) {
|
||||||
|
const ability = this.#getAbility();
|
||||||
|
if (ability && locks.ability) {
|
||||||
|
const opt = [...ability.options].find(o => o.dataset.label === locks.ability);
|
||||||
|
if (opt) {
|
||||||
|
opt.selected = true;
|
||||||
|
ability.disabled = true;
|
||||||
|
const scoreEl = this.#el.querySelector("#abilityScore");
|
||||||
|
if (scoreEl) scoreEl.value = opt.value;
|
||||||
|
const sc = this.#getSelfCtrl();
|
||||||
|
if (sc) sc.max = opt.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const skill = this.#getSkill();
|
||||||
|
if (skill && locks.skill) {
|
||||||
|
const opt = [...skill.options].find(o => o.dataset.label === locks.skill);
|
||||||
|
if (opt) opt.selected = true;
|
||||||
|
}
|
||||||
|
const difficulty = this.#getDifficulty();
|
||||||
|
if (difficulty && locks.difficulty !== undefined && locks.difficulty !== null) {
|
||||||
|
const locked = parseInt(locks.difficulty, 10);
|
||||||
|
let opt = [...difficulty.options].find(o => parseInt(o.value, 10) === locked);
|
||||||
|
if (!opt) {
|
||||||
|
// Difficulté hors des options (ex: 9 + 2) : ajoute une option dédiée.
|
||||||
|
opt = document.createElement("option");
|
||||||
|
opt.value = String(locked);
|
||||||
|
opt.textContent = `${game.i18n.localize("VERMINE.difficulty")} (${locked})`;
|
||||||
|
difficulty.appendChild(opt);
|
||||||
|
}
|
||||||
|
opt.selected = true;
|
||||||
|
difficulty.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#displaySpecialties() {
|
#displaySpecialties() {
|
||||||
for (const el of this.#el.querySelectorAll("[data-spec-skill]")) {
|
for (const el of this.#el.querySelectorAll("[data-spec-skill]")) {
|
||||||
el.style.display = "inline";
|
el.style.display = "inline";
|
||||||
@@ -206,18 +292,32 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
}
|
}
|
||||||
|
|
||||||
#calculateBonusCount() {
|
#calculateBonusCount() {
|
||||||
let b = 0;
|
const b = this.getPoolBreakdown();
|
||||||
if (this.#el.querySelector("#helped")?.checked) b += 1;
|
return b.specialty + b.help + b.tooling + b.group + b.selfControl + b.experience;
|
||||||
b += parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
|
}
|
||||||
b += parseInt(this.#getSelfCtrl()?.value, 10) || 0;
|
|
||||||
const tools = this.#el.querySelector("input[name='usingTools']:checked");
|
#refreshExperienceUI() {
|
||||||
if (tools && tools.value !== "0") b += 1;
|
const actor = this.#actor;
|
||||||
const human = this.#el.querySelector("#human-totem");
|
const group = RollDialog.#findGroup(actor);
|
||||||
if (human?.checked) b += parseInt(this.#actor?.system?.adaptation?.totems?.human?.value, 10) || 0;
|
const individual = actor.system?.experience?.dice || 0;
|
||||||
const adapted = this.#el.querySelector("#adapted-totem");
|
const groupPool = group?.system?.experience?.dice || 0;
|
||||||
if (adapted?.checked) b += parseInt(this.#actor?.system?.adaptation?.totems?.adapted?.value, 10) || 0;
|
const indEl = this.#el.querySelector("#exp-individual");
|
||||||
if (this.hasSpecialtySelected()) b += 1;
|
if (indEl) indEl.textContent = individual;
|
||||||
return b;
|
const grpEl = this.#el.querySelector("#exp-group");
|
||||||
|
if (grpEl) grpEl.textContent = groupPool;
|
||||||
|
const pull = this.#el.querySelector("#experience-pull");
|
||||||
|
if (pull) {
|
||||||
|
const max = Math.min(2, groupPool);
|
||||||
|
const current = Math.min(parseInt(pull.value, 10) || 0, max);
|
||||||
|
pull.innerHTML = "";
|
||||||
|
for (let i = 0; i <= max; i++) {
|
||||||
|
const opt = document.createElement("option");
|
||||||
|
opt.value = String(i);
|
||||||
|
opt.textContent = `${i}D`;
|
||||||
|
if (i === current) opt.selected = true;
|
||||||
|
pull.appendChild(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#updateUI() {
|
#updateUI() {
|
||||||
@@ -284,6 +384,29 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async #onGiveToGroup(event, target) {
|
||||||
|
const actor = this.#actor;
|
||||||
|
if (actor.type !== "character") return;
|
||||||
|
const individual = actor.system?.experience?.dice || 0;
|
||||||
|
if (individual < 1) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_experience_dice"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!game.user.isGM && !actor.isOwner) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_permission"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const group = RollDialog.#findGroup(actor);
|
||||||
|
if (!group) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_group"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const groupPool = group.system?.experience?.dice || 0;
|
||||||
|
await actor.update({ "system.experience.dice": individual - 1 });
|
||||||
|
await group.update({ "system.experience.dice": groupPool + 1 });
|
||||||
|
this.#refreshExperienceUI();
|
||||||
|
}
|
||||||
|
|
||||||
static async #onRoll(event, target) {
|
static async #onRoll(event, target) {
|
||||||
const selfCtrl = this.getSelfControl();
|
const selfCtrl = this.getSelfControl();
|
||||||
if (selfCtrl > 0) {
|
if (selfCtrl > 0) {
|
||||||
@@ -305,6 +428,26 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
await this.#actor.update({ "system.attributes.self_control.value": newVal });
|
await this.#actor.update({ "system.attributes.self_control.value": newVal });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transfert d'Expérience : puiser dans la Réserve de Groupe (1-2D).
|
||||||
|
const pulled = this.#getExperiencePull();
|
||||||
|
if (pulled > 0) {
|
||||||
|
const group = RollDialog.#findGroup(this.#actor);
|
||||||
|
if (!group) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_group"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!game.user.isGM && !group.isOwner) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_permission"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const groupPool = group.system?.experience?.dice || 0;
|
||||||
|
if (groupPool < pulled) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_group_dice"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await group.update({ "system.experience.dice": groupPool - pulled });
|
||||||
|
}
|
||||||
|
|
||||||
await VermineUtils.roll({
|
await VermineUtils.roll({
|
||||||
actor: this.#actor,
|
actor: this.#actor,
|
||||||
NoD: this.getDicePool(),
|
NoD: this.getDicePool(),
|
||||||
@@ -318,7 +461,12 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
keepTotem: this.getKeepTotem(),
|
keepTotem: this.getKeepTotem(),
|
||||||
skillCategory: this.getSkillCategory(),
|
skillCategory: this.getSkillCategory(),
|
||||||
skillLevel: this.getSkillLevel(),
|
skillLevel: this.getSkillLevel(),
|
||||||
hasSpecialty: this.hasSpecialtySelected()
|
hasSpecialty: this.hasSpecialtySelected(),
|
||||||
|
poolBreakdown: this.getPoolBreakdown(),
|
||||||
|
specialtyName: this.getSpecialtyName(),
|
||||||
|
weapon: this.weapon?.toObject() ?? null,
|
||||||
|
targets: [...game.user.targets].map(t => t.name),
|
||||||
|
messageFlags: this.defense ? { "vermine-defense": this.defense } : null
|
||||||
});
|
});
|
||||||
|
|
||||||
this.close();
|
this.close();
|
||||||
|
|||||||
@@ -43,19 +43,19 @@ export async function initUserDice(dice3d, user) {
|
|||||||
export function darkenColor(color, percent) {
|
export function darkenColor(color, percent) {
|
||||||
const num = parseInt(color.replace('#', ''), 16);
|
const num = parseInt(color.replace('#', ''), 16);
|
||||||
const amt = Math.round(2.55 * percent);
|
const amt = Math.round(2.55 * percent);
|
||||||
const R = (num >> 16) + amt;
|
const R = (num >> 16) - amt;
|
||||||
const G = ((num >> 8) & 0x00FF) + amt;
|
const G = ((num >> 8) & 0x00FF) - amt;
|
||||||
const B = (num & 0x0000FF) + amt;
|
const B = (num & 0x0000FF) - amt;
|
||||||
return '#' + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1);
|
return '#' + (0x1000000 + (R < 0 ? 0 : R > 255 ? 255 : R) * 0x10000 + (G < 0 ? 0 : G > 255 ? 255 : G) * 0x100 + (B < 0 ? 0 : B > 255 ? 255 : B)).toString(16).slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function lightenColor(color, percent) {
|
export function lightenColor(color, percent) {
|
||||||
const num = parseInt(color.replace('#', ''), 16);
|
const num = parseInt(color.replace('#', ''), 16);
|
||||||
const amt = Math.round(2.55 * percent);
|
const amt = Math.round(2.55 * percent);
|
||||||
const R = (num >> 16) - amt;
|
const R = (num >> 16) + amt;
|
||||||
const G = ((num >> 8) & 0x00FF) - amt;
|
const G = ((num >> 8) & 0x00FF) + amt;
|
||||||
const B = (num & 0x0000FF) - amt;
|
const B = (num & 0x0000FF) + amt;
|
||||||
return '#' + (0x1000000 + (R < 0 ? 0 : R > 255 ? 255 : R) * 0x10000 + (G < 0 ? 0 : G > 255 ? 255 : G) * 0x100 + (B < 0 ? 0 : B > 255 ? 255 : B)).toString(16).slice(1);
|
return '#' + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1);
|
||||||
}
|
}
|
||||||
export function oppositeColor(color) {
|
export function oppositeColor(color) {
|
||||||
const num = parseInt(color.replace('#', ''), 16);
|
const num = parseInt(color.replace('#', ''), 16);
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ export function prepareActiveEffectCategories(effects) {
|
|||||||
|
|
||||||
// Iterate over active effects, classifying them into categories
|
// Iterate over active effects, classifying them into categories
|
||||||
for ( let e of effects ) {
|
for ( let e of effects ) {
|
||||||
e._getSourceName(); // Trigger a lookup for the source name
|
|
||||||
if ( e.disabled ) categories.inactive.effects.push(e);
|
if ( e.disabled ) categories.inactive.effects.push(e);
|
||||||
else if ( e.isTemporary ) categories.temporary.effects.push(e);
|
else if ( e.isTemporary ) categories.temporary.effects.push(e);
|
||||||
else categories.passive.effects.push(e);
|
else categories.passive.effects.push(e);
|
||||||
|
|||||||
@@ -0,0 +1,495 @@
|
|||||||
|
/**
|
||||||
|
* Moteur de résolution des échanges de coups.
|
||||||
|
*
|
||||||
|
* Gère l'attaque (contact / distance), la défense (esquive / parade),
|
||||||
|
* le calcul des dégâts, la déduction des protections et l'application
|
||||||
|
* des blessures pour les personnages, PNJ et créatures.
|
||||||
|
*
|
||||||
|
* Le flux est assisté : les cartes de chat calculent et affichent tout,
|
||||||
|
* un bouton "Appliquer" coche la blessure sur l'acteur cible.
|
||||||
|
*
|
||||||
|
* Liens entre messages (flags "world") :
|
||||||
|
* - "vermine-exchange" sur le message d'attaque
|
||||||
|
* - "vermine-defense" sur le message de défense
|
||||||
|
*/
|
||||||
|
|
||||||
|
const FLAG_EXCHANGE = 'vermine-exchange'
|
||||||
|
const FLAG_DEFENSE = 'vermine-defense'
|
||||||
|
|
||||||
|
export class VermineExchange {
|
||||||
|
|
||||||
|
// ── Tables de difficulté ────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capacité d'attaque selon le mode.
|
||||||
|
* @param {string} mode 'contact' ou 'ranged'
|
||||||
|
* @returns {string} clé de capacité (vigor / precision)
|
||||||
|
*/
|
||||||
|
static attackAbility(mode) {
|
||||||
|
return mode === 'ranged' ? 'precision' : 'vigor'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Difficultés d'attaque au contact selon le niveau de compétence (0-5).
|
||||||
|
* Incompétent/Débutant = 7, Confirmé = 5/7, Expert = 5/7/9,
|
||||||
|
* Maître/Légende = 3/5/7/9.
|
||||||
|
* @param {number} skillLevel valeur de compétence (0-5)
|
||||||
|
* @returns {number[]}
|
||||||
|
*/
|
||||||
|
static contactDifficultyOptions(skillLevel) {
|
||||||
|
switch (parseInt(skillLevel, 10) || 0) {
|
||||||
|
case 2: return [5, 7]
|
||||||
|
case 3: return [5, 7, 9]
|
||||||
|
case 4:
|
||||||
|
case 5: return [3, 5, 7, 9]
|
||||||
|
default: return [7]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Difficultés d'attaque à distance par portée.
|
||||||
|
* @returns {{key: string, label: string, difficulty: number}[]}
|
||||||
|
*/
|
||||||
|
static rangeDifficultyOptions() {
|
||||||
|
return [
|
||||||
|
{ key: 'short', label: 'VERMINE.range_short', difficulty: 5 },
|
||||||
|
{ key: 'mid', label: 'VERMINE.range_mid', difficulty: 7 },
|
||||||
|
{ key: 'long', label: 'VERMINE.range_long', difficulty: 9 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Difficultés proposées au PNJ selon son niveau d'expérience
|
||||||
|
* (champ "contact" : "7", "5 ou 7", "5,7 ou 9", "3,5,7 ou 9").
|
||||||
|
* @param {Actor} actor
|
||||||
|
* @returns {number[]}
|
||||||
|
*/
|
||||||
|
static npcDifficultyOptions(actor) {
|
||||||
|
const level = actor.system?.experience?.value || 1
|
||||||
|
const cfg = CONFIG.VERMINE.npcExperienceLevels?.[level] || {}
|
||||||
|
const raw = cfg.contact || '7'
|
||||||
|
return raw.split(',').flatMap(part => part.trim().split(/\s+ou\s+/).map(Number))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mode d'attaque déduit de la compétence de l'arme.
|
||||||
|
* @param {Item|null} weapon
|
||||||
|
* @returns {string} 'contact' ou 'ranged'
|
||||||
|
*/
|
||||||
|
static modeForWeapon(weapon) {
|
||||||
|
const skill = weapon?.system?.skill || ''
|
||||||
|
return ['firearms', 'archery', 'throwing'].includes(skill) ? 'ranged' : 'contact'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pool et dégâts ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pool d'attaque de base (sans les bonus optionnels du dialogue).
|
||||||
|
* Personnage : caractéristique + pool de compétence.
|
||||||
|
* PNJ : pool d'attaque du niveau de menace.
|
||||||
|
* Créature : pool d'attaque calculé.
|
||||||
|
* @param {Actor} actor L'attaquant
|
||||||
|
* @param {Item|null} weapon
|
||||||
|
* @param {string} mode
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
static baseAttackPool(actor, weapon, mode) {
|
||||||
|
const type = actor.type
|
||||||
|
if (type === 'creature') {
|
||||||
|
return actor.system?.computed?.attack || 0
|
||||||
|
}
|
||||||
|
if (type === 'npc') {
|
||||||
|
const threat = actor.system?.threat?.value || 1
|
||||||
|
return CONFIG.VERMINE.npcThreatLevels?.[threat]?.attack || 0
|
||||||
|
}
|
||||||
|
const abilityKey = this.attackAbility(mode)
|
||||||
|
const ability = actor.system?.abilities?.[abilityKey]?.value || 0
|
||||||
|
const skillKey = weapon?.system?.skill
|
||||||
|
const skillValue = skillKey ? actor.system?.skills?.[skillKey]?.value || 0 : 0
|
||||||
|
const skillPool = skillValue ? CONFIG.VERMINE.SkillLevels?.[skillValue]?.dicePool || 0 : 0
|
||||||
|
return ability + skillPool
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dégâts de base de l'arme (hors réussites de l'attaque).
|
||||||
|
* Le trait "Vigueur+" ajoute la vigueur de l'attaquant.
|
||||||
|
* PNJ sans arme et créatures utilisent leurs dégâts propres. Une créature
|
||||||
|
* équipée d'une arme (morsure, griffes…) utilise les dégâts de celle-ci.
|
||||||
|
* @param {Actor} actor L'attaquant
|
||||||
|
* @param {Item|null} weapon
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
static baseDamage(actor, weapon) {
|
||||||
|
const type = actor.type
|
||||||
|
if (type === 'creature') {
|
||||||
|
if (weapon?.system?.damage) {
|
||||||
|
let base = weapon.system.damage.value || 0
|
||||||
|
if (weapon.system.damage.addVigor) base += actor.system?.computed?.vigor || 0
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
return actor.system?.computed?.damage || 0
|
||||||
|
}
|
||||||
|
if (weapon?.system?.damage) {
|
||||||
|
let base = weapon.system.damage.value || 0
|
||||||
|
if (weapon.system.damage.addVigor) base += this.#vigor(actor)
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
return this.#vigor(actor)
|
||||||
|
}
|
||||||
|
|
||||||
|
static #vigor(actor) {
|
||||||
|
if (actor.type === 'npc') {
|
||||||
|
const threat = actor.system?.threat?.value || 1
|
||||||
|
return CONFIG.VERMINE.npcThreatLevels?.[threat]?.vigor || 0
|
||||||
|
}
|
||||||
|
if (actor.type === 'creature') return actor.system?.computed?.vigor || 0
|
||||||
|
return actor.system?.abilities?.vigor?.value || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Protection et blessures ─────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protection du défenseur.
|
||||||
|
* Parade : seule la protection utilisée compte.
|
||||||
|
* Sinon : somme des protections équipées (créature : protection de rôle).
|
||||||
|
* @param {Actor} defender
|
||||||
|
* @param {number|null} parryLevel Protection utilisée lors d'une parade
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
static protectionTotal(defender, parryLevel = null) {
|
||||||
|
if (parryLevel !== null) return Math.max(0, parryLevel)
|
||||||
|
if (defender.type === 'creature') {
|
||||||
|
return defender.system?.computed?.protection || 0
|
||||||
|
}
|
||||||
|
const defenses = defender.itemTypes?.defense?.filter(d => d.system.equipped) || []
|
||||||
|
return defenses.reduce((sum, d) => sum + (d.system.level || 0), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catégorie de blessure : le seuil le plus élevé inférieur ou égal
|
||||||
|
* aux dégâts nets subis.
|
||||||
|
* @param {number} netDamage
|
||||||
|
* @param {Actor} defender
|
||||||
|
* @returns {string|null} 'minorWound', 'majorWound', 'deadlyWound' ou null
|
||||||
|
*/
|
||||||
|
static highestWound(netDamage, defender) {
|
||||||
|
const sys = defender.system
|
||||||
|
const deadly = sys?.deadlyWound?.threshold ?? Number.POSITIVE_INFINITY
|
||||||
|
const major = sys?.majorWound?.threshold ?? Number.POSITIVE_INFINITY
|
||||||
|
const minor = sys?.minorWound?.threshold ?? Number.POSITIVE_INFINITY
|
||||||
|
if (netDamage >= deadly) return 'deadlyWound'
|
||||||
|
if (netDamage >= major) return 'majorWound'
|
||||||
|
if (netDamage >= minor) return 'minorWound'
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clé i18n du libellé d'une catégorie de blessure.
|
||||||
|
* @param {string|null} category
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static woundLabelKey(category) {
|
||||||
|
if (category === 'deadlyWound') return 'VERMINE.wounds.deadly_wounds'
|
||||||
|
if (category === 'majorWound') return 'VERMINE.wounds.heavy_wounds'
|
||||||
|
if (category === 'minorWound') return 'VERMINE.wounds.light_wounds'
|
||||||
|
return 'VERMINE.no_wound'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie que l'utilisateur peut appliquer une blessure sur l'acteur.
|
||||||
|
* @param {Actor} actor
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
static canApplyWound(actor) {
|
||||||
|
return Boolean(actor) && (game.user.isGM || actor.isOwner)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applique +1 dans la catégorie de blessure indiquée (plafonné au max).
|
||||||
|
* @param {Actor} actor
|
||||||
|
* @param {string} woundCategory 'minorWound' | 'majorWound' | 'deadlyWound'
|
||||||
|
* @returns {Promise<number|boolean>} nouvelle valeur, ou false si refusé
|
||||||
|
*/
|
||||||
|
static async applyWound(actor, woundCategory) {
|
||||||
|
if (!actor || !woundCategory) return null
|
||||||
|
if (!this.canApplyWound(actor)) return false
|
||||||
|
const current = actor.system?.[woundCategory]?.value || 0
|
||||||
|
const max = actor.system?.[woundCategory]?.max ?? Number.POSITIVE_INFINITY
|
||||||
|
const next = Math.min(current + 1, max)
|
||||||
|
await actor.update({ [`system.${woundCategory}.value`]: next })
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Résolution ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Résout l'échange pour une cible donnée (défense la plus récente liée
|
||||||
|
* à cette cible, ou abstention si aucune défense).
|
||||||
|
* @param {ChatMessage} attackMessage
|
||||||
|
* @param {string|null} targetId id d'acteur de la cible (défaut : première cible)
|
||||||
|
* @returns {Promise<Object|null>} résultat de la résolution
|
||||||
|
*/
|
||||||
|
static async resolve(attackMessage, targetId = null) {
|
||||||
|
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||||
|
if (!exchange) return null
|
||||||
|
|
||||||
|
const target = (targetId && exchange.targets?.find(t => t.id === targetId)) || exchange.targets?.[0] || null
|
||||||
|
if (!target) return null
|
||||||
|
|
||||||
|
const defs = this.#defenseMessagesFor(exchange.id, target.id)
|
||||||
|
const defenderMessage = defs[defs.length - 1]
|
||||||
|
const defense = defenderMessage ? defenderMessage.getFlag('world', FLAG_DEFENSE) : null
|
||||||
|
|
||||||
|
const attackerSuccesses = this.#messageTotal(attackMessage)
|
||||||
|
const defenderSuccesses = defense ? this.#messageTotal(defenderMessage) : 0
|
||||||
|
const hit = attackerSuccesses > defenderSuccesses
|
||||||
|
|
||||||
|
// Le flag de défense porte actorId/actorUuid (clés différentes de celles
|
||||||
|
// des cibles) : on normalise la référence avant résolution de l'acteur.
|
||||||
|
const defenderRef = defense
|
||||||
|
? { id: defense.actorId ?? null, uuid: defense.actorUuid ?? null, name: defense.actorName ?? '' }
|
||||||
|
: target
|
||||||
|
const defender = await this.#resolveDefender(defenderRef)
|
||||||
|
|
||||||
|
const damage = (exchange.baseDamage || 0) + attackerSuccesses
|
||||||
|
const parryLevel = defense?.type === 'parade' ? (defense.parryLevel ?? 0) : null
|
||||||
|
const protection = hit && parryLevel !== null
|
||||||
|
? parryLevel
|
||||||
|
: (defender ? this.protectionTotal(defender, hit ? null : parryLevel) : 0)
|
||||||
|
const netDamage = hit ? Math.max(0, damage - protection) : 0
|
||||||
|
const woundCategory = hit && defender ? this.highestWound(netDamage, defender) : null
|
||||||
|
|
||||||
|
return {
|
||||||
|
exchangeId: exchange.id,
|
||||||
|
defenderId: defenderRef.id ?? null,
|
||||||
|
defenderUuid: defenderRef.uuid ?? null,
|
||||||
|
defenderName: defense?.actorName || target.name || '',
|
||||||
|
defenseType: defense?.type || null,
|
||||||
|
attackerSuccesses,
|
||||||
|
defenderSuccesses,
|
||||||
|
hit,
|
||||||
|
damage,
|
||||||
|
protection,
|
||||||
|
netDamage,
|
||||||
|
woundCategory,
|
||||||
|
woundThreshold: woundCategory && defender ? defender.system[woundCategory].threshold : null,
|
||||||
|
canApply: Boolean(defender) && this.canApplyWound(defender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rend le bloc de résolution (HTML) pour insertion dans le message.
|
||||||
|
* @param {Object} r Résultat de VermineExchange.resolve
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static renderResolution(r) {
|
||||||
|
const t = (key) => game.i18n.localize(key)
|
||||||
|
const verdict = !r.hit
|
||||||
|
? (r.defenseType === 'parade'
|
||||||
|
? `<span class="res-parried">${t('VERMINE.missed_parade')}</span>`
|
||||||
|
: `<span class="res-dodged">${t('VERMINE.missed_esquive')}</span>`)
|
||||||
|
: (r.defenseType === 'parade'
|
||||||
|
? `<span class="res-hit">${t('VERMINE.hit_parade')}</span>`
|
||||||
|
: `<span class="res-hit">${t('VERMINE.hit')}</span>`)
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
`<div class="resolution-line">${t('VERMINE.attacker_successes')}: ${r.attackerSuccesses} · ${t('VERMINE.defender_successes')}: ${r.defenderSuccesses} → ${verdict}</div>`
|
||||||
|
]
|
||||||
|
if (r.hit) {
|
||||||
|
lines.push(`<div class="resolution-line">${t('VERMINE.raw_damage')}: <strong>${r.damage}</strong>${r.protection ? ` − ${t('ADVERSITY.protection')}: ${r.protection}` : ''} = ${t('VERMINE.net_damage')}: <strong>${r.netDamage}</strong></div>`)
|
||||||
|
if (r.woundCategory) {
|
||||||
|
lines.push(`<div class="resolution-line res-wound">${t('VERMINE.wound')}: <strong>${t(this.woundLabelKey(r.woundCategory))}</strong> (${t('VERMINE.wounds.threshold')} ${r.woundThreshold}D)</div>`)
|
||||||
|
} else {
|
||||||
|
lines.push(`<div class="resolution-line res-noshock">${t('VERMINE.no_wound')}</div>`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lines.push(`<div class="resolution-line res-nodamage">${t('VERMINE.no_damage')}</div>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const apply = (r.hit && r.woundCategory && r.canApply)
|
||||||
|
? `<button type="button" class="exchange-apply" data-action="exchange-apply" data-defender-id="${r.defenderId}" data-defender-uuid="${r.defenderUuid}" data-wound="${r.woundCategory}"><i class="fas fa-briefcase-medical"></i> ${t('VERMINE.apply_wound')}</button>`
|
||||||
|
: ''
|
||||||
|
|
||||||
|
return `<div class="exchange-resolution">
|
||||||
|
<div class="resolution-header"><i class="fas fa-exchange-alt"></i> ${t('VERMINE.resolve_result')}</div>
|
||||||
|
${lines.join('')}
|
||||||
|
${apply}
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Liaison chat ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lie les interactions d'échange de coups sur un message de chat.
|
||||||
|
* @param {HTMLElement} html élément du message
|
||||||
|
* @param {ChatMessage} message
|
||||||
|
*/
|
||||||
|
static bindChat(html, message) {
|
||||||
|
const exchange = message.getFlag('world', FLAG_EXCHANGE)
|
||||||
|
if (exchange) {
|
||||||
|
html.querySelectorAll('.exchange-defend').forEach(btn => {
|
||||||
|
btn.addEventListener('click', ev => this.#onDefense(ev, message))
|
||||||
|
})
|
||||||
|
html.querySelectorAll('.exchange-resolve').forEach(btn => {
|
||||||
|
btn.addEventListener('click', ev => this.#onResolve(ev, message))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
html.querySelectorAll('[data-action="exchange-apply"]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', ev => this.#onApply(ev, message))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onDefense(ev, attackMessage) {
|
||||||
|
ev.preventDefault()
|
||||||
|
ev.stopPropagation()
|
||||||
|
const btn = ev.currentTarget
|
||||||
|
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||||
|
if (!exchange) return
|
||||||
|
const type = btn.dataset.type
|
||||||
|
const actor = await this.#resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
|
||||||
|
if (!actor) {
|
||||||
|
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!game.user.isGM && !actor.isOwner) {
|
||||||
|
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (actor.type === 'creature') {
|
||||||
|
ui.notifications.warn(game.i18n.localize('VERMINE.error_creature_no_defense'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { default: RollDialog } = await import('./dialogs/rollDialog.mjs')
|
||||||
|
const locks = this.#defenseLocks(actor, exchange, type)
|
||||||
|
const dialog = await RollDialog.create({
|
||||||
|
actor,
|
||||||
|
rolltype: 'skill',
|
||||||
|
label: locks.skill,
|
||||||
|
locks,
|
||||||
|
defense: {
|
||||||
|
exchangeId: exchange.id,
|
||||||
|
type,
|
||||||
|
actorId: actor.id,
|
||||||
|
actorUuid: actor.uuid,
|
||||||
|
actorName: actor.name,
|
||||||
|
difficulty: exchange.difficulty,
|
||||||
|
parryLevel: type === 'parade' ? this.#parryLevel(actor) : null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (dialog) dialog.render(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
static #defenseLocks(actor, exchange, type) {
|
||||||
|
if (type === 'parade') {
|
||||||
|
return { ability: 'vigor', skill: this.#parrySkill(actor), difficulty: exchange.difficulty }
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ability: 'reflexes',
|
||||||
|
skill: exchange.mode === 'ranged' ? 'alertness' : 'close',
|
||||||
|
difficulty: exchange.difficulty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static #parrySkill(actor) {
|
||||||
|
const hasMelee = actor.itemTypes?.weapon?.some(w => w.system.equipped && w.system.skill === 'melee')
|
||||||
|
return hasMelee ? 'melee' : 'close'
|
||||||
|
}
|
||||||
|
|
||||||
|
static #parryLevel(actor) {
|
||||||
|
const defenses = actor.itemTypes?.defense?.filter(d => d.system.equipped) || []
|
||||||
|
return defenses.reduce((best, d) => Math.max(best, d.system.level || 0), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onResolve(ev, attackMessage) {
|
||||||
|
ev.preventDefault()
|
||||||
|
const btn = ev.currentTarget
|
||||||
|
if (!game.user.isGM && !attackMessage.isOwner) {
|
||||||
|
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||||
|
if (!exchange) return
|
||||||
|
if (!exchange.targets?.length) {
|
||||||
|
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_target'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const wrapper = btn.closest('.exchange-actions')
|
||||||
|
const container = wrapper?.parentElement
|
||||||
|
if (!container) return
|
||||||
|
container.querySelectorAll('.exchange-resolution').forEach(el => el.remove())
|
||||||
|
|
||||||
|
const targets = exchange.targets?.length ? exchange.targets : [null]
|
||||||
|
for (const target of targets) {
|
||||||
|
const result = await this.resolve(attackMessage, target?.id ?? null)
|
||||||
|
if (!result) continue
|
||||||
|
const block = document.createElement('div')
|
||||||
|
block.innerHTML = this.renderResolution(result)
|
||||||
|
const node = block.firstElementChild
|
||||||
|
container.appendChild(node)
|
||||||
|
this.bindChat(node, attackMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgEl = btn.closest('.vermine-roll-message')
|
||||||
|
if (msgEl) await attackMessage.update({ content: msgEl.outerHTML })
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onApply(ev, attackMessage) {
|
||||||
|
ev.preventDefault()
|
||||||
|
const btn = ev.currentTarget
|
||||||
|
const defender = await this.#resolveDefender({ id: btn.dataset.defenderId, uuid: btn.dataset.defenderUuid })
|
||||||
|
const wound = btn.dataset.wound
|
||||||
|
if (!defender || !wound) return
|
||||||
|
if (!this.canApplyWound(defender)) {
|
||||||
|
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const next = await this.applyWound(defender, wound)
|
||||||
|
if (next === false) return
|
||||||
|
btn.disabled = true
|
||||||
|
btn.innerHTML = `<i class="fas fa-check"></i> ${game.i18n.localize('VERMINE.wound_applied')}`
|
||||||
|
// Persiste l'état du bouton seulement si l'utilisateur peut modifier le message
|
||||||
|
if (game.user.isGM || attackMessage.isOwner) {
|
||||||
|
const msgEl = btn.closest('.vermine-roll-message')
|
||||||
|
if (msgEl) await attackMessage.update({ content: msgEl.outerHTML })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers internes ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Résout un acteur depuis son id d'acteur monde (acteurs liés) ou son
|
||||||
|
* uuid (tokens non liés via fromUuid).
|
||||||
|
* @param {{id: string|null, uuid: string|null}} ref
|
||||||
|
* @returns {Promise<Actor|null>}
|
||||||
|
*/
|
||||||
|
static async #resolveDefender({ id = null, uuid = null }) {
|
||||||
|
if (id) {
|
||||||
|
const actor = game.actors.get(id)
|
||||||
|
if (actor) return actor
|
||||||
|
}
|
||||||
|
if (uuid) {
|
||||||
|
const doc = await fromUuid(uuid)
|
||||||
|
if (!doc) return null
|
||||||
|
if (doc.documentType === 'Actor') return doc
|
||||||
|
if (doc.documentType === 'Token') return doc.actor ?? null
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
static #defenseMessagesFor(exchangeId, targetId = null) {
|
||||||
|
return game.messages.contents.filter(m => {
|
||||||
|
const f = m.getFlag('world', FLAG_DEFENSE)
|
||||||
|
if (!f || f.exchangeId !== exchangeId) return false
|
||||||
|
if (targetId) return f.actorId === targetId
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static #messageTotal(message) {
|
||||||
|
const el = document.createElement('div')
|
||||||
|
el.innerHTML = message.content || ''
|
||||||
|
const totalEl = el.querySelector('#total')
|
||||||
|
const val = parseInt(totalEl?.innerText, 10)
|
||||||
|
return Number.isNaN(val) ? 0 : val
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
/**
|
||||||
|
* Règles d'apprentissage (phase d'Expérience).
|
||||||
|
* Helpers purs de difficultés, handicaps et calculs de jets d'Expérience.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class VermineExperience {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Difficulté d'un jet d'apprentissage de Compétence selon le niveau visé.
|
||||||
|
* @param {number} targetLevel niveau visé (1-5)
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
static skillLearningDifficulty(targetLevel) {
|
||||||
|
switch (parseInt(targetLevel, 10)) {
|
||||||
|
case 1: return 5 // Débutant
|
||||||
|
case 2: return 7 // Confirmé
|
||||||
|
case 3: return 9 // Expert
|
||||||
|
case 4: return 10 // Maître
|
||||||
|
case 5: return 10 // Légende
|
||||||
|
default: return 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handicap d'un jet d'apprentissage de Compétence.
|
||||||
|
* Rareté (1-3) + handicap (I) supplémentaire pour le niveau Légende.
|
||||||
|
* @param {number} targetLevel niveau visé (1-5)
|
||||||
|
* @param {number} rarity rareté de la compétence (0-3)
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
static skillLearningHandicap(targetLevel, rarity) {
|
||||||
|
let handicap = parseInt(rarity, 10) || 0
|
||||||
|
if (parseInt(targetLevel, 10) === 5) handicap += 1 // Légende : (I)
|
||||||
|
return handicap
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Difficulté d'un jet d'apprentissage de Spécialité selon le niveau actuel
|
||||||
|
* de la compétence associée. Domaine de prédilection : -2.
|
||||||
|
* @param {number} skillLevel niveau actuel de la compétence (2-5)
|
||||||
|
* @param {boolean} isDomain la compétence est dans le domaine de prédilection
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
static specialtyLearningDifficulty(skillLevel, isDomain) {
|
||||||
|
let difficulty
|
||||||
|
switch (parseInt(skillLevel, 10)) {
|
||||||
|
case 2: difficulty = 9 // Confirmé
|
||||||
|
break
|
||||||
|
case 3: difficulty = 7 // Expert
|
||||||
|
break
|
||||||
|
case 4: difficulty = 5 // Maître
|
||||||
|
break
|
||||||
|
case 5: difficulty = 5 // Légende (relance automatique)
|
||||||
|
break
|
||||||
|
default: difficulty = 9
|
||||||
|
}
|
||||||
|
return isDomain ? difficulty - 2 : difficulty
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handicap d'un jet d'apprentissage de Spécialité : nombre de spécialités
|
||||||
|
* déjà possédées (requiert au moins Confirmé dans la compétence associée).
|
||||||
|
* @param {number} specialtyCount
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
static specialtyLearningHandicap(specialtyCount) {
|
||||||
|
return Math.max(0, parseInt(specialtyCount, 10) || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Difficulté d'un jet d'apprentissage de Caractéristique.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
static characteristicLearningDifficulty() {
|
||||||
|
return 9
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Difficulté d'un jet d'apprentissage de Réserve : valeur actuelle.
|
||||||
|
* @param {number} currentValue
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
static reserveLearningDifficulty(currentValue) {
|
||||||
|
return Math.max(0, parseInt(currentValue, 10) || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handicap d'âge pour l'apprentissage des Réserves et autres jets liés à l'âge.
|
||||||
|
* @param {number} ageType 1=Jeune, 2=Adulte, 3=Ancien
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
static ageHandicap(ageType) {
|
||||||
|
switch (parseInt(ageType, 10)) {
|
||||||
|
case 1: return 1 // Jeune (I)
|
||||||
|
case 3: return 3 // Ancien (III)
|
||||||
|
default: return 2 // Adulte (II)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Difficulté d'un jet d'acquisition d'un Dé d'Évolution selon l'âge.
|
||||||
|
* @param {number} ageType 1=Jeune, 2=Adulte, 3=Ancien
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
static evolutionDifficulty(ageType) {
|
||||||
|
switch (parseInt(ageType, 10)) {
|
||||||
|
case 1: return 5
|
||||||
|
case 3: return 9
|
||||||
|
default: return 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bonus/Malus de Totem sur le jet de Dé d'Évolution.
|
||||||
|
* Plus de Dés Adaptés que Humains : +1D. Inversement : -1D.
|
||||||
|
* @param {number} human human totem dice
|
||||||
|
* @param {number} adapted adapted totem dice
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
static evolutionTotemModifier(human, adapted) {
|
||||||
|
if (adapted > human) return 1
|
||||||
|
if (human > adapted) return -1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clé i18n du libellé du niveau de compétence visé.
|
||||||
|
* @param {number} level
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static skillLevelLabelKey(level) {
|
||||||
|
return CONFIG.VERMINE.SkillLevels?.[level]?.label ?? "SKILL_LEVELS.incompetent"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Détermine si une compétence appartient au domaine de prédilection.
|
||||||
|
* @param {Actor} actor
|
||||||
|
* @param {string} skillKey
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
static isPreferredDomain(actor, skillKey) {
|
||||||
|
const category = actor.system?.skills?.[skillKey]?.category
|
||||||
|
const preferred = actor.system?.skill_categories?.preferred
|
||||||
|
return Boolean(category && preferred && category === preferred)
|
||||||
|
}
|
||||||
|
}
|
||||||
+83
-235
@@ -1,5 +1,4 @@
|
|||||||
import { VERMINE } from "./config.mjs";
|
|
||||||
import { getActorSkillScore, updateActorSkillScore } from "./functions.mjs";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles combat-related dice rolls for Vermine2047.
|
* Handles combat-related dice rolls for Vermine2047.
|
||||||
@@ -28,7 +27,8 @@ export class VermineFight {
|
|||||||
*/
|
*/
|
||||||
async performTest(enemyAchievement, enemyConservation, skillKey, skill, params, actor) {
|
async performTest(enemyAchievement, enemyConservation, skillKey, skill, params, actor) {
|
||||||
// Use d10 as per Vermine2047 official rules
|
// Use d10 as per Vermine2047 official rules
|
||||||
const dicePool = (params.spleen != undefined || params.purpose != undefined) ? '5' : '4';
|
const basePool = Math.max(skill, 1)
|
||||||
|
const dicePool = (params.spleen != undefined || params.purpose != undefined) ? String(basePool + 1) : String(basePool)
|
||||||
const difficulty = params.difficulty || 7; // Default difficulty
|
const difficulty = params.difficulty || 7; // Default difficulty
|
||||||
const r = new Roll(dicePool + `d10`);
|
const r = new Roll(dicePool + `d10`);
|
||||||
let diceString = '';
|
let diceString = '';
|
||||||
@@ -167,220 +167,26 @@ export class VermineFight {
|
|||||||
// data injected to char data
|
// data injected to char data
|
||||||
static previousValues = {
|
static previousValues = {
|
||||||
dicePool: 4,
|
dicePool: 4,
|
||||||
skills: VERMINE.skillsList,
|
skills: [],
|
||||||
cskills: VERMINE.cskills,
|
cskills: [],
|
||||||
cephalic: false,
|
cephalic: false,
|
||||||
achievementReroll: VERMINE.achievementReroll,
|
achievementReroll: 0,
|
||||||
conservationReroll: VERMINE.conservationReroll
|
conservationReroll: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
static rollerTemplate = 'systems/vermine2047/templates/fight.html';
|
static rollerTemplate = 'systems/vermine2047/templates/fight.html';
|
||||||
static CombatResultTemplate = 'systems/vermine2047/templates/fight-result.html';
|
static CombatResultTemplate = 'systems/vermine2047/templates/fight-result.html';
|
||||||
|
|
||||||
static async chatMessageHandler(message, html, data) {
|
|
||||||
// console.log("accès au fin du fin", message._id);
|
|
||||||
|
|
||||||
// sélection du dé actif
|
|
||||||
html.on("click", '.confrontation .die.d10', event => {
|
|
||||||
const diceResult = parseInt($(event.target).html(), 10);
|
|
||||||
html.find('.confrontation .die.d10').removeClass('active');
|
|
||||||
$(event.target).addClass('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
// sélection des dés d'accomplissement (achievement = successes)
|
|
||||||
html.on("click", '.confrontation .add-to-achievement', event => {
|
|
||||||
const diceResult = parseInt(html.find('.confrontation .die.d10.active').html(), 10);
|
|
||||||
html.find('.confrontation .die.d10.active').removeClass('min').addClass('max');
|
|
||||||
});
|
|
||||||
|
|
||||||
// sélection des dés de conservation
|
|
||||||
html.on("click", '.confrontation .add-to-conservation', event => {
|
|
||||||
const diceResult = parseInt(html.find('.confrontation .die.d10.active').html(), 10);
|
|
||||||
html.find('.confrontation .die.d10.active').removeClass('max').addClass('min');
|
|
||||||
});
|
|
||||||
|
|
||||||
// reset de la sélection des pools
|
|
||||||
html.on("click", '.confrontation .reset', event => {
|
|
||||||
html.find('.confrontation .die.d10')
|
|
||||||
.removeClass('max')
|
|
||||||
.removeClass('min');
|
|
||||||
});
|
|
||||||
|
|
||||||
// résolution de la confrontation
|
|
||||||
// Note: With d10 system, we count successes (dice >= difficulty) instead of summing
|
|
||||||
html.on("click", '.confrontation .resolve', async event => {
|
|
||||||
let achievementDice = 0;
|
|
||||||
let conservationDice = 0;
|
|
||||||
let achievementBasis = 0;
|
|
||||||
let conservationBasis = 0;
|
|
||||||
const difficulty = 7; // Default difficulty for legacy compatibility
|
|
||||||
|
|
||||||
// Count successes (dice >= difficulty) for achievement
|
|
||||||
html.find('.confrontation .die.d10.max').each(function (index) {
|
|
||||||
const value = parseInt($(this).html(), 10);
|
|
||||||
if (value >= difficulty) {
|
|
||||||
achievementDice++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Count successes (dice >= difficulty) for conservation
|
|
||||||
html.find('.confrontation .die.d10.min').each(function (index) {
|
|
||||||
const value = parseInt($(this).html(), 10);
|
|
||||||
if (value >= difficulty) {
|
|
||||||
conservationDice++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// saisie des résultats
|
|
||||||
achievementBasis = html.find('td.achievement-result').data('achievement-basis');
|
|
||||||
html.find('td.achievement-result').data('achievement-value', achievementDice);
|
|
||||||
html.find('td.achievement-result').html(achievementBasis + achievementDice);
|
|
||||||
|
|
||||||
conservationBasis = html.find('td.conservation-result').data('conservation-basis');
|
|
||||||
html.find('td.conservation-result').data('conservation-value', conservationDice);
|
|
||||||
html.find('td.conservation-result').html(conservationBasis + conservationDice);
|
|
||||||
|
|
||||||
// calcul des marges (now based on successes, not sum)
|
|
||||||
const achievementMargin = achievementBasis + achievementDice - parseInt(html.find('td.adv-achievement-result').html(), 10);
|
|
||||||
const conservationMargin = conservationBasis + conservationDice - parseInt(html.find('td.adv-conservation-result').html(), 10);
|
|
||||||
html.find('td.achievement-margin').html(achievementMargin);
|
|
||||||
html.find('td.conservation-margin').html(conservationMargin);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// fin de la résolution de la confrontation
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static async chatListeners(html) {
|
|
||||||
// supprime le masquage des résultats du dé
|
|
||||||
html.off("click", ".dice-roll");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* main class function
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
static async ui(externalData = {}) {
|
|
||||||
let actor = {};
|
|
||||||
|
|
||||||
// get the actor
|
|
||||||
try {
|
|
||||||
actor = game.user.character;
|
|
||||||
} catch (e) {
|
|
||||||
throw ("Aucun personnage défini !");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actor == null && externalData.speakerId != undefined && externalData.speakerId != null) {
|
|
||||||
// on récupère le speakerId, et de là l'objet actor
|
|
||||||
actor = game.actors.get(externalData.speakerId);
|
|
||||||
VermineFight.previousValues['speakerName'] = actor.name;
|
|
||||||
VermineFight.previousValues['speakerImg'] = actor.img;
|
|
||||||
} else {
|
|
||||||
VermineFight.previousValues['speakerName'] = "Anonyme";
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the data
|
|
||||||
let charData = (externalData) => {
|
|
||||||
let o = Object.assign({ _template: VermineFight.rollerTemplate }, { ...VermineFight.previousValues, ...externalData });
|
|
||||||
return o;
|
|
||||||
};
|
|
||||||
let data = charData(externalData);
|
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
// render template
|
|
||||||
let html = await foundry.applications.handlebars.renderTemplate(data._template, data);
|
|
||||||
|
|
||||||
let ui = new Dialog({
|
|
||||||
title: game.i18n.localize("VERMINE.FightTool"),
|
|
||||||
content: html,
|
|
||||||
buttons: {
|
|
||||||
roll: {
|
|
||||||
label: game.i18n.localize('VERMINE.Roll4Fight'),
|
|
||||||
callback: (html) => {
|
|
||||||
let form = html.find('#dice-pool-form');
|
|
||||||
if (!form[0].checkValidity()) {
|
|
||||||
throw "Invalid Data";
|
|
||||||
}
|
|
||||||
let enemyAchievement, enemyConservation, skillKey, skill = 5, enemySkill, params = {};
|
|
||||||
form.serializeArray().forEach(e => {
|
|
||||||
switch (e.name) {
|
|
||||||
case "skill":
|
|
||||||
case "cephalic":
|
|
||||||
if (e.value !== '') {
|
|
||||||
skillKey = e.value;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "skill-score":
|
|
||||||
skill = +e.value;
|
|
||||||
break;
|
|
||||||
case "specialization":
|
|
||||||
params.specialization = true;
|
|
||||||
break;
|
|
||||||
case "usure":
|
|
||||||
params.usure = +e.value;
|
|
||||||
break;
|
|
||||||
case "trait":
|
|
||||||
params.trait = +e.value;
|
|
||||||
break;
|
|
||||||
case "purpose":
|
|
||||||
params.purpose = true;
|
|
||||||
break;
|
|
||||||
case "spleen":
|
|
||||||
params.spleen = true;
|
|
||||||
break;
|
|
||||||
case "adv-skill":
|
|
||||||
enemySkill = +e.value;
|
|
||||||
break;
|
|
||||||
case "achievement":
|
|
||||||
enemyAchievement = +e.value;
|
|
||||||
break;
|
|
||||||
case "conservation":
|
|
||||||
enemyConservation = +e.value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// prise en compte de l'usure sur la feuille de perso
|
|
||||||
if (params.usure != undefined) {
|
|
||||||
const newSpentScore = getActorSkillScore(actor, skillKey, 'spent') + params.usure;
|
|
||||||
console.log(newSpentScore);
|
|
||||||
updateActorSkillScore(actor, skillKey, 'spent', newSpentScore);
|
|
||||||
}
|
|
||||||
|
|
||||||
return VermineFight.get().performTest(enemyAchievement + enemySkill, enemyConservation + enemySkill, skillKey, skill, params, actor);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cancel: {
|
|
||||||
label: game.i18n.localize('Close'),
|
|
||||||
callback: () => { }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function (h) {
|
|
||||||
h.on("change", 'select[name="skill"]', event => {
|
|
||||||
const skillLabel = $(event.target).val();
|
|
||||||
const currentSkillScore = getActorSkillScore(actor, skillLabel) - getActorSkillScore(actor, skillLabel, 'spent');
|
|
||||||
if (parseInt(currentSkillScore, 10) >= 0) {
|
|
||||||
h.find('input#skillScore').val(currentSkillScore);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, { width: 601, height: 'fit-content' });
|
|
||||||
ui.render(true);
|
|
||||||
return ui;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VermineCombat extends Combat {
|
export class VermineCombat extends Combat {
|
||||||
_encounterCheck() {
|
_encounterCheck() {
|
||||||
console.log('encounter combat object', this);
|
// encounter check
|
||||||
}
|
}
|
||||||
|
|
||||||
async rollInitiative(ids, formula = undefined, messageOptions = {}) {
|
async rollInitiative(ids, {formula=null, updateTurn=true, messageMode, messageOptions={}}={}) {
|
||||||
console.log(`${game.system.title} | Combat.rollInitiative()`, ids, formula, messageOptions);
|
// roll initiative
|
||||||
return super.rollInitiative(ids, formula, messageOptions)
|
return super.rollInitiative(ids, {formula, updateTurn, messageMode, messageOptions});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nextRound() {
|
nextRound() {
|
||||||
@@ -417,31 +223,76 @@ export class VermineCombat extends Combat {
|
|||||||
|
|
||||||
export class VermineCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
|
export class VermineCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
|
||||||
|
|
||||||
get template() {
|
/** @override */
|
||||||
return "systems/vermine2047/templates/combat-tracker.hbs";
|
static PARTS = {
|
||||||
}
|
main: {
|
||||||
|
template: "systems/vermine2047/templates/combat-tracker.hbs",
|
||||||
|
root: true
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
get template() {
|
|
||||||
return "systems/vermine2047/templates/combat-tracker.hbs";
|
|
||||||
}
|
|
||||||
|
|
||||||
async getData(options) {
|
|
||||||
const context = await super.getData(options);
|
|
||||||
|
|
||||||
if (!context.hasCombat) {
|
|
||||||
return context;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @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;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
activateListeners(html) {
|
/** @override */
|
||||||
super.activateListeners(html);
|
async _prepareTurnContext(combat, combatant, index) {
|
||||||
html.find("[data-attitude]").click(this._setStatut.bind(this));
|
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));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -465,23 +316,20 @@ export class VermineCombatTracker extends foundry.applications.sidebar.tabs.Comb
|
|||||||
}
|
}
|
||||||
|
|
||||||
let actor = await game.actors.get(combatant.actorId)
|
let actor = await game.actors.get(combatant.actorId)
|
||||||
console.log(actor, combatant)
|
// combatant attitude set
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VermineCombatant extends Combatant {
|
export class VermineCombatant extends Combatant {
|
||||||
constructor(data, context) {
|
constructor(data, context) {
|
||||||
super(data, context);
|
super(data, context)
|
||||||
this.setDefaultAttitude();
|
if (this.token) {
|
||||||
|
this.attitude = this.token.flags.world?.attitude || "active"
|
||||||
}
|
} else {
|
||||||
setDefaultAttitude() {
|
this.attitude = "active"
|
||||||
this.attitude = this.token.flags.world?.attitude || "active"
|
}
|
||||||
}
|
}
|
||||||
_getInitiativeFormula() {
|
_getInitiativeFormula() {
|
||||||
|
return String(CONFIG.Combat.initiative.formula || game.system.initiative)
|
||||||
return String(CONFIG.Combat.initiative.formula || game.system.initiative);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
+28
-30
@@ -1,47 +1,45 @@
|
|||||||
import { VERMINE } from './config.mjs'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* renvoie le score d'une compétence d'un actor existant
|
* Returns the score of a skill for an actor.
|
||||||
* @param {VermineActor}
|
* Skills are stored as a flat object in actor.system.skills: { [key]: { value, spent, label, category } }
|
||||||
* @return {number||null} Data for rendering or null
|
* @param {VermineActor} actor
|
||||||
|
* @param {string} skillLabel - The localized label of the skill to find
|
||||||
|
* @param {"value"|"spent"} [property="value"] - Which property to return
|
||||||
|
* @returns {number|null} The skill score or null if not found
|
||||||
*/
|
*/
|
||||||
export function getActorSkillScore(actor, skillLabel, property = "value") {
|
export function getActorSkillScore(actor, skillLabel, property = "value") {
|
||||||
let returnedValue = null;
|
const skills = actor.system?.skills;
|
||||||
|
if (!skills) return null;
|
||||||
|
|
||||||
for(let i in actor.system.skills){
|
for (const key of Object.keys(skills)) {
|
||||||
for(let j in actor.system.skills[i].data){
|
if (skills[key].label === skillLabel) {
|
||||||
if (actor.system.skills[i].data[j].label == skillLabel){
|
return skills[key][property] ?? null;
|
||||||
returnedValue = actor.system.skills[i].data[j][property];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
return returnedValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* met à jour le score d'une compétence d'un actor existant
|
* Updates the score of a skill for an actor.
|
||||||
* @param {VermineActor}
|
* @param {VermineActor} selectedActor
|
||||||
* @return {boolean} bool
|
* @param {string} skillLabel - The localized label of the skill to update
|
||||||
|
* @param {"value"|"spent"} [property="value"] - Which property to update
|
||||||
|
* @param {number} updatedValue - The new value
|
||||||
|
* @returns {boolean} Whether the update was successful
|
||||||
*/
|
*/
|
||||||
export function updateActorSkillScore(selectedActor, skillLabel, property = "value", updatedValue) {
|
export function updateActorSkillScore(selectedActor, skillLabel, property = "value", updatedValue) {
|
||||||
try {
|
try {
|
||||||
let updated = false;
|
const skills = selectedActor.system?.skills;
|
||||||
// on recherche le label parmi les compétences
|
if (!skills) return false;
|
||||||
for (let st in selectedActor.system.skills){
|
|
||||||
for (let s in selectedActor.system.skills[st].data){
|
for (const key of Object.keys(skills)) {
|
||||||
if (selectedActor.system.skills[st].data[s].label == skillLabel){
|
if (skills[key].label === skillLabel) {
|
||||||
selectedActor.system.skills[st].data[s][property] = updatedValue; // printing the new value
|
skills[key][property] = updatedValue;
|
||||||
const systemSkillKey = `system.skills.${st}.data.${s}.${property}`;
|
selectedActor.update({ [`system.skills.${key}.${property}`]: updatedValue });
|
||||||
selectedActor.update({[systemSkillKey]:updatedValue }); // updating actor's data
|
return true;
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
return updated;
|
} catch (e) {
|
||||||
} catch(e){
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const preloadHandlebarsTemplates = async function () {
|
|||||||
"systems/vermine2047/templates/actor/parts/actor-items.hbs",
|
"systems/vermine2047/templates/actor/parts/actor-items.hbs",
|
||||||
"systems/vermine2047/templates/actor/parts/actor-weapons.hbs",
|
"systems/vermine2047/templates/actor/parts/actor-weapons.hbs",
|
||||||
"systems/vermine2047/templates/actor/parts/actor-defenses.hbs",
|
"systems/vermine2047/templates/actor/parts/actor-defenses.hbs",
|
||||||
|
"systems/vermine2047/templates/actor/parts/actor-equipped-gear.hbs",
|
||||||
|
|
||||||
// Character partials.
|
// Character partials.
|
||||||
"systems/vermine2047/templates/actor/character/character-id.hbs",
|
"systems/vermine2047/templates/actor/character/character-id.hbs",
|
||||||
@@ -32,6 +33,7 @@ export const preloadHandlebarsTemplates = async function () {
|
|||||||
// npc partials
|
// npc partials
|
||||||
"systems/vermine2047/templates/actor/npc/npc-combat.hbs",
|
"systems/vermine2047/templates/actor/npc/npc-combat.hbs",
|
||||||
"systems/vermine2047/templates/actor/parts/npc-skill-category.hbs",
|
"systems/vermine2047/templates/actor/parts/npc-skill-category.hbs",
|
||||||
|
"systems/vermine2047/templates/actor/parts/npc-skill-item.hbs",
|
||||||
|
|
||||||
// creature partials
|
// creature partials
|
||||||
"systems/vermine2047/templates/actor/creature/creature-combat.hbs",
|
"systems/vermine2047/templates/actor/creature/creature-combat.hbs",
|
||||||
@@ -100,6 +102,21 @@ export const registerHandlebarsHelpers = function () {
|
|||||||
return game.i18n.localize(arrayLabel + "." + objectLabel + ".name");
|
return game.i18n.localize(arrayLabel + "." + objectLabel + ".name");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Return an object of skills ordered alphabetically by their localized
|
||||||
|
// display name (easier to find a skill quickly). Use with `{{#each}}`.
|
||||||
|
Handlebars.registerHelper('sortedSkills', function (skills) {
|
||||||
|
if (!skills || typeof skills !== 'object') return skills;
|
||||||
|
const sorted = {};
|
||||||
|
Object.keys(skills)
|
||||||
|
.sort((a, b) => {
|
||||||
|
const na = game.i18n.localize(`SKILLS.${a}.name`).toLocaleLowerCase();
|
||||||
|
const nb = game.i18n.localize(`SKILLS.${b}.name`).toLocaleLowerCase();
|
||||||
|
return na.localeCompare(nb, game.i18n.lang);
|
||||||
|
})
|
||||||
|
.forEach(k => { sorted[k] = skills[k]; });
|
||||||
|
return sorted;
|
||||||
|
});
|
||||||
|
|
||||||
Handlebars.registerHelper('smarttlk', function (arrayLabel, objectLabel, key) {
|
Handlebars.registerHelper('smarttlk', function (arrayLabel, objectLabel, key) {
|
||||||
return game.i18n.localize(arrayLabel + "." + objectLabel + "." + key);
|
return game.i18n.localize(arrayLabel + "." + objectLabel + "." + key);
|
||||||
});
|
});
|
||||||
@@ -124,7 +141,7 @@ export const registerHandlebarsHelpers = function () {
|
|||||||
Handlebars.registerHelper('threatLevel', function (property, level, options) {
|
Handlebars.registerHelper('threatLevel', function (property, level, options) {
|
||||||
if (level < 1 || level > 4)
|
if (level < 1 || level > 4)
|
||||||
return "";
|
return "";
|
||||||
let levelData = CONFIG.VERMINE.ThreatLevels[level];
|
let levelData = CONFIG.VERMINE.npcThreatLevels[level];
|
||||||
if (property == 'label') {
|
if (property == 'label') {
|
||||||
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
|
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
|
||||||
} else {
|
} else {
|
||||||
@@ -136,7 +153,7 @@ export const registerHandlebarsHelpers = function () {
|
|||||||
Handlebars.registerHelper('experienceLevel', function (property, level, options) {
|
Handlebars.registerHelper('experienceLevel', function (property, level, options) {
|
||||||
if (level < 1 || level > 4)
|
if (level < 1 || level > 4)
|
||||||
return "";
|
return "";
|
||||||
let levelData = CONFIG.VERMINE.ExperienceLevels[level];
|
let levelData = CONFIG.VERMINE.npcExperienceLevels[level];
|
||||||
if (property == 'label') {
|
if (property == 'label') {
|
||||||
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
|
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
|
||||||
} else {
|
} else {
|
||||||
@@ -148,7 +165,7 @@ export const registerHandlebarsHelpers = function () {
|
|||||||
Handlebars.registerHelper('roleLevel', function (property, level, options) {
|
Handlebars.registerHelper('roleLevel', function (property, level, options) {
|
||||||
if (level < 1 || level > 4)
|
if (level < 1 || level > 4)
|
||||||
return "";
|
return "";
|
||||||
let levelData = CONFIG.VERMINE.RoleLevels[level];
|
let levelData = CONFIG.VERMINE.npcRoleLevels[level];
|
||||||
if (property == 'label') {
|
if (property == 'label') {
|
||||||
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
|
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
|
||||||
} else {
|
} else {
|
||||||
@@ -160,7 +177,7 @@ export const registerHandlebarsHelpers = function () {
|
|||||||
Handlebars.registerHelper('patternLevel', function (property, level, options) {
|
Handlebars.registerHelper('patternLevel', function (property, level, options) {
|
||||||
if (level < 1 || level > 4)
|
if (level < 1 || level > 4)
|
||||||
return "";
|
return "";
|
||||||
let levelData = CONFIG.VERMINE.PatternLevels[level];
|
let levelData = CONFIG.VERMINE.creaturePatternLevels[level];
|
||||||
if (property == 'label') {
|
if (property == 'label') {
|
||||||
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
|
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
|
||||||
} else {
|
} else {
|
||||||
@@ -172,7 +189,7 @@ export const registerHandlebarsHelpers = function () {
|
|||||||
Handlebars.registerHelper('sizeLevel', function (property, level, options) {
|
Handlebars.registerHelper('sizeLevel', function (property, level, options) {
|
||||||
if (level < 1 || level > 4)
|
if (level < 1 || level > 4)
|
||||||
return "";
|
return "";
|
||||||
let levelData = CONFIG.VERMINE.SizeLevels[level];
|
let levelData = CONFIG.VERMINE.creatureSizeLevels[level];
|
||||||
if (property == 'label') {
|
if (property == 'label') {
|
||||||
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
|
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
|
||||||
} else {
|
} else {
|
||||||
@@ -184,7 +201,7 @@ export const registerHandlebarsHelpers = function () {
|
|||||||
Handlebars.registerHelper('packLevel', function (property, level, options) {
|
Handlebars.registerHelper('packLevel', function (property, level, options) {
|
||||||
if (level < 0 || level > 3)
|
if (level < 0 || level > 3)
|
||||||
return "";
|
return "";
|
||||||
let levelData = CONFIG.VERMINE.PackLevels[level];
|
let levelData = CONFIG.VERMINE.creaturePackLevels[level];
|
||||||
if (property == 'label') {
|
if (property == 'label') {
|
||||||
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
|
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
|
||||||
} else {
|
} else {
|
||||||
@@ -347,6 +364,9 @@ export const registerHandlebarsHelpers = function () {
|
|||||||
Handlebars.registerHelper('ifincludes', function (arg1, arg2, options) {
|
Handlebars.registerHelper('ifincludes', function (arg1, arg2, options) {
|
||||||
return (arg1.includes(arg2)) ? options.fn(this) : options.inverse(this);
|
return (arg1.includes(arg2)) ? options.fn(this) : options.inverse(this);
|
||||||
});
|
});
|
||||||
|
Handlebars.registerHelper('ifStartsWith', function (arg1, arg2, options) {
|
||||||
|
return (typeof arg1 === 'string' && arg1.startsWith(arg2)) ? options.fn(this) : options.inverse(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
//math operations
|
//math operations
|
||||||
@@ -388,4 +408,15 @@ export const registerHandlebarsHelpers = function () {
|
|||||||
Handlebars.registerHelper("setVar", function (varName, varValue, options) {
|
Handlebars.registerHelper("setVar", function (varName, varValue, options) {
|
||||||
options.data.root[varName] = varValue;
|
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 "";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
+24
-14
@@ -1,6 +1,5 @@
|
|||||||
import RollDialog from "./dialogs/rollDialog.mjs";
|
import RollDialog from "./dialogs/rollDialog.mjs";
|
||||||
import { initUserDice } from "./dice3d.mjs";
|
import { initUserDice } from "./dice3d.mjs";
|
||||||
import { DiceSystem } from '../../../../modules/dice-so-nice/api.js';
|
|
||||||
import { VermineUtils } from "./roll.mjs";
|
import { VermineUtils } from "./roll.mjs";
|
||||||
import { registerTours } from "./tour.mjs";
|
import { registerTours } from "./tour.mjs";
|
||||||
|
|
||||||
@@ -10,6 +9,9 @@ export const registerHooks = function () {
|
|||||||
*/
|
*/
|
||||||
CONFIG.debug.hooks = false;
|
CONFIG.debug.hooks = false;
|
||||||
Hooks.once('diceSoNiceReady', async (dice3d) => {
|
Hooks.once('diceSoNiceReady', async (dice3d) => {
|
||||||
|
const dsnModule = game.modules.get('dice-so-nice');
|
||||||
|
if (!dsnModule?.active) return;
|
||||||
|
const { DiceSystem } = await import(`/modules/${dsnModule.id}/api.js`);
|
||||||
const vermineSystem = new DiceSystem('Vermine2047', 'Vermine 2047', "preferred", 'totem')
|
const vermineSystem = new DiceSystem('Vermine2047', 'Vermine 2047', "preferred", 'totem')
|
||||||
dice3d.addSystem(vermineSystem);
|
dice3d.addSystem(vermineSystem);
|
||||||
|
|
||||||
@@ -39,11 +41,15 @@ export const registerHooks = function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Hooks.on('renderChatMessageHTML', async (message, html, data) => {
|
Hooks.on('renderChatMessageHTML', async (message, html, data) => {
|
||||||
|
// Échanges de coups : liaisons ouvertes à tous (propriétaires de cibles inclus)
|
||||||
|
const { VermineExchange } = await import('./exchange.mjs');
|
||||||
|
VermineExchange.bindChat(html, message);
|
||||||
|
|
||||||
let rerollTitle = html.querySelector(".reroll-fromroll h4");
|
let rerollTitle = html.querySelector(".reroll-fromroll h4");
|
||||||
if (rerollTitle) {
|
if (rerollTitle) {
|
||||||
rerollTitle.addEventListener("click", () => { html.querySelector(".reroll").classList.toggle('visible') })
|
rerollTitle.addEventListener("click", () => { html.querySelector(".reroll").classList.toggle('visible') })
|
||||||
}
|
}
|
||||||
if (message.author?._id != game.user._id || !game.user.isGM) {
|
if (message.author?._id !== game.user._id && !game.user.isGM) {
|
||||||
// désactiver les inputs pour les joueurs non-auteurs du message
|
// désactiver les inputs pour les joueurs non-auteurs du message
|
||||||
html.querySelectorAll("input").forEach(inp => inp.disabled = true);
|
html.querySelectorAll("input").forEach(inp => inp.disabled = true);
|
||||||
//cacher le boutton reroll
|
//cacher le boutton reroll
|
||||||
@@ -61,17 +67,21 @@ export const registerHooks = function () {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
Hooks.once("ready", async () => {
|
Hooks.once("ready", async () => {
|
||||||
console.info("Vermine 2047 | System Initialized.");
|
// System initialized
|
||||||
//await registerTours();
|
//await registerTours();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// changement de la pause
|
// changement de la pause
|
||||||
Hooks.on("renderPause", async function () {
|
Hooks.on("renderGamePause", async function (app, element) {
|
||||||
if ($("#pause").attr("class") !== "paused") return;
|
const pauseEl = element ?? document.getElementById("pause");
|
||||||
$(".paused img").attr("src", 'systems/vermine2047/assets/images/ui/vermine_pause.webp');
|
if (!pauseEl || pauseEl.className !== "paused") return;
|
||||||
$(".paused img").css({ "opacity": 1 });
|
pauseEl.querySelectorAll("img").forEach(img => {
|
||||||
$("#pause.paused figcaption").text("Communauté endormie...");
|
img.src = 'systems/vermine2047/assets/images/ui/vermine_pause.webp';
|
||||||
|
img.style.opacity = 1;
|
||||||
|
});
|
||||||
|
const caption = pauseEl.querySelector("figcaption");
|
||||||
|
if (caption) caption.textContent = "Communauté endormie...";
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,17 +98,17 @@ export const registerHooks = function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Hooks.on('getSceneControlButtons', async (controls) => {
|
Hooks.on('getSceneControlButtons', async (controls) => {
|
||||||
const tokenControls = controls.token;
|
const tokenControls = controls.tokens;
|
||||||
if (tokenControls && tokenControls.tools) {
|
if (tokenControls && tokenControls.tools) {
|
||||||
tokenControls.tools.push({
|
tokenControls.tools['dice-roller'] = {
|
||||||
name: 'Dice Roller',
|
name: 'Dice Roller',
|
||||||
title: game.i18n.localize("VERMINE.RollTool"),
|
title: game.i18n.localize("VERMINE.RollTool"),
|
||||||
icon: 'fas fa-dice-d10',
|
icon: 'fas fa-dice-d10',
|
||||||
button: true,
|
button: true,
|
||||||
onClick() {
|
onChange() {
|
||||||
RollDialog.create().then(d => d.render(true));
|
RollDialog.create().then(d => d.render(true));
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -107,7 +117,7 @@ export const registerHooks = function () {
|
|||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
Hooks.on("preCreateActor", function (actor) {
|
Hooks.on("preCreateActor", function (actor) {
|
||||||
console.log('pre create actor', actor.img);
|
// pre create actor
|
||||||
if (actor.img == "icons/svg/mystery-man.svg") {
|
if (actor.img == "icons/svg/mystery-man.svg") {
|
||||||
actor.updateSource({ "img": `systems/vermine2047/assets/icons/actors/${actor.type}.webp` });
|
actor.updateSource({ "img": `systems/vermine2047/assets/icons/actors/${actor.type}.webp` });
|
||||||
}
|
}
|
||||||
@@ -138,7 +148,7 @@ export const registerHooks = function () {
|
|||||||
if (game.user.isGM) {
|
if (game.user.isGM) {
|
||||||
let combatant = (game.combat.combatant) ? game.combat.combatant.actor : "";
|
let combatant = (game.combat.combatant) ? game.combat.combatant.actor : "";
|
||||||
|
|
||||||
console.log('update combat', game.combat);
|
// update combat
|
||||||
|
|
||||||
/*if (combatant.type == "marker" && combatant.system.settings.general.isCounter == true) {
|
/*if (combatant.type == "marker" && combatant.system.settings.general.isCounter == true) {
|
||||||
let step = (!combatant.system.settings.general.counting) ? -1 : combatant.system.settings.general.counting;
|
let step = (!combatant.system.settings.general.counting) ? -1 : combatant.system.settings.general.counting;
|
||||||
|
|||||||
+173
-146
@@ -28,7 +28,14 @@ export class VermineUtils {
|
|||||||
skillCategory = null,
|
skillCategory = null,
|
||||||
keepTotem = null,
|
keepTotem = null,
|
||||||
skillLevel = null,
|
skillLevel = null,
|
||||||
hasSpecialty = false
|
hasSpecialty = false,
|
||||||
|
handicap = 0,
|
||||||
|
poolBreakdown = null,
|
||||||
|
specialtyName = null,
|
||||||
|
weapon = null,
|
||||||
|
targets = [],
|
||||||
|
attack = null,
|
||||||
|
messageFlags = null
|
||||||
}) {
|
}) {
|
||||||
// Validate inputs
|
// Validate inputs
|
||||||
if (!actor) {
|
if (!actor) {
|
||||||
@@ -48,22 +55,13 @@ export class VermineUtils {
|
|||||||
totemBonus = this._calculateTotemDomainBonuses(skillCategory, actor);
|
totemBonus = this._calculateTotemDomainBonuses(skillCategory, actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply auto-successes and auto-thresholds
|
// Apply automatic successes from skill mastery
|
||||||
let autoSuccesses = 0;
|
|
||||||
let adjustedDifficulty = difficulty;
|
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
|
// Handle human totem
|
||||||
if (totems.human) {
|
if (totems.human) {
|
||||||
NoD--;
|
NoD--;
|
||||||
const humanDifficulty = skillLevel !== null ? Math.max(adjustedDifficulty, difficulty) : adjustedDifficulty;
|
const humanDifficulty = adjustedDifficulty;
|
||||||
const humanFormula = `(1D10cs>=${humanDifficulty}[human_${safeUserName}]*2)`;
|
const humanFormula = `(1D10cs>=${humanDifficulty}[human_${safeUserName}]*2)`;
|
||||||
|
|
||||||
// Apply domain bonus/malus
|
// Apply domain bonus/malus
|
||||||
@@ -77,7 +75,7 @@ export class VermineUtils {
|
|||||||
// Handle adapted totem
|
// Handle adapted totem
|
||||||
if (totems.adapted) {
|
if (totems.adapted) {
|
||||||
NoD--;
|
NoD--;
|
||||||
const adaptedDifficulty = skillLevel !== null ? Math.max(adjustedDifficulty, difficulty) : adjustedDifficulty;
|
const adaptedDifficulty = adjustedDifficulty;
|
||||||
const adaptedFormula = `(1D10cs>=${adaptedDifficulty}[adapted_${safeUserName}]*2)`;
|
const adaptedFormula = `(1D10cs>=${adaptedDifficulty}[adapted_${safeUserName}]*2)`;
|
||||||
|
|
||||||
// Apply domain bonus/malus
|
// Apply domain bonus/malus
|
||||||
@@ -100,16 +98,40 @@ export class VermineUtils {
|
|||||||
NoD++; // Cancel the decrement for adapted
|
NoD++; // Cancel the decrement for adapted
|
||||||
} else if (keepTotem === 'adapted' && totems.human) {
|
} else if (keepTotem === 'adapted' && totems.human) {
|
||||||
modFormula = `(1D10cs>=${adjustedDifficulty}[adapted_${safeUserName}]*2)`;
|
modFormula = `(1D10cs>=${adjustedDifficulty}[adapted_${safeUserName}]*2)`;
|
||||||
NoD++; // Cancel the decrement for human
|
NoD++; // Cancel the decrement for human
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the dice pool cannot go below zero
|
||||||
|
NoD = Math.max(0, NoD)
|
||||||
|
|
||||||
// Build base formula
|
// Build base formula
|
||||||
const baseFormula = `${NoD}d10cs>=${adjustedDifficulty}[regular_${safeUserName}]`;
|
const baseFormula = `${NoD}d10cs>=${adjustedDifficulty}[regular_${safeUserName}]`;
|
||||||
|
|
||||||
// Build final formula
|
// Build final formula
|
||||||
formula = modFormula !== null ? `${baseFormula}+${modFormula}` : baseFormula;
|
formula = modFormula !== null ? `${baseFormula}+${modFormula}` : baseFormula;
|
||||||
|
|
||||||
|
// Reconcile the breakdown displayed in chat with the dice actually rolled.
|
||||||
|
// Domain bonuses/malus are only applied when the corresponding totem die
|
||||||
|
// is used, and totem dice replace (not add to) base-pool dice.
|
||||||
|
if (poolBreakdown) {
|
||||||
|
// Count totem dice present in the final modifier formula.
|
||||||
|
let totemDice = 0;
|
||||||
|
if (modFormula) {
|
||||||
|
if (modFormula.includes("human_")) totemDice += 1;
|
||||||
|
if (modFormula.includes("adapted_")) totemDice += 1;
|
||||||
|
}
|
||||||
|
// Only count the domain bonus of totems actually activated in the roll.
|
||||||
|
const domainBonus = (totems.human ? totemBonus.human : 0)
|
||||||
|
+ (totems.adapted ? totemBonus.adapted : 0);
|
||||||
|
const basePool = poolBreakdown.total;
|
||||||
|
poolBreakdown.totemDice = totemDice;
|
||||||
|
poolBreakdown.domainBonus = domainBonus;
|
||||||
|
// Total = nombre réel de dés lancés (NoD réguliers + dés de totem).
|
||||||
|
// Équivaut à basePool + domainBonus sauf si le pool régulier clampe à 0.
|
||||||
|
poolBreakdown.total = NoD + totemDice;
|
||||||
|
}
|
||||||
|
|
||||||
// Create the roll
|
// Create the roll
|
||||||
const roll = new Roll(formula, actor.getRollData());
|
const roll = new Roll(formula, actor.getRollData());
|
||||||
|
|
||||||
@@ -122,7 +144,6 @@ export class VermineUtils {
|
|||||||
skillCategory: skillCategory,
|
skillCategory: skillCategory,
|
||||||
skillLevel: skillLevel,
|
skillLevel: skillLevel,
|
||||||
hasSpecialty: hasSpecialty,
|
hasSpecialty: hasSpecialty,
|
||||||
autoSuccesses: autoSuccesses,
|
|
||||||
totemBonuses: { ...totemBonus },
|
totemBonuses: { ...totemBonus },
|
||||||
baseNoD: NoD,
|
baseNoD: NoD,
|
||||||
rerolls: Reroll,
|
rerolls: Reroll,
|
||||||
@@ -148,7 +169,14 @@ export class VermineUtils {
|
|||||||
skillCategory,
|
skillCategory,
|
||||||
keepTotem,
|
keepTotem,
|
||||||
skillLevel,
|
skillLevel,
|
||||||
hasSpecialty
|
hasSpecialty,
|
||||||
|
handicap,
|
||||||
|
poolBreakdown,
|
||||||
|
specialtyName,
|
||||||
|
weapon,
|
||||||
|
targets,
|
||||||
|
attack,
|
||||||
|
messageFlags
|
||||||
});
|
});
|
||||||
|
|
||||||
return roll;
|
return roll;
|
||||||
@@ -201,70 +229,13 @@ export class VermineUtils {
|
|||||||
return bonuses;
|
return bonuses;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates automatic successes based on skill mastery level.
|
|
||||||
* @param {number} skillLevel - Skill level (0-5)
|
|
||||||
* @param {boolean} [hasSpecialty=false] - Whether a specialty is used
|
|
||||||
* @returns {number} Number of automatic successes
|
|
||||||
*/
|
|
||||||
static _calculateAutoSuccesses(skillLevel, hasSpecialty = false) {
|
|
||||||
// According to Vermine2047 rules, automatic successes are based on mastery level:
|
|
||||||
// Level 0 (Incompetent): 0 automatic successes
|
|
||||||
// Level 1 (Beginner): 0 automatic successes
|
|
||||||
// Level 2 (Proficient): 1 automatic success if specialty is used
|
|
||||||
// Level 3 (Expert): 1 automatic success
|
|
||||||
// Level 4 (Master): 1 automatic success + 1 if specialty is used
|
|
||||||
// Level 5 (Legend): 2 automatic successes
|
|
||||||
|
|
||||||
if (!skillLevel) return 0;
|
|
||||||
|
|
||||||
let autoSuccesses = 0;
|
|
||||||
|
|
||||||
switch (skillLevel) {
|
|
||||||
case 2: // Compétent
|
|
||||||
if (hasSpecialty) autoSuccesses = 1;
|
|
||||||
break;
|
|
||||||
case 3: // Expert
|
|
||||||
autoSuccesses = 1;
|
|
||||||
break;
|
|
||||||
case 4: // Maître
|
|
||||||
autoSuccesses = 1;
|
|
||||||
if (hasSpecialty) autoSuccesses += 1;
|
|
||||||
break;
|
|
||||||
case 5: // Légende
|
|
||||||
autoSuccesses = 2;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
autoSuccesses = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return autoSuccesses;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the automatic threshold if the skill is not mastered.
|
|
||||||
* @param {number} skillLevel - Skill level
|
|
||||||
* @returns {number|null} Automatic threshold or null if skill is mastered
|
|
||||||
*/
|
|
||||||
static _getAutoThreshold(skillLevel) {
|
|
||||||
// If the skill is not mastered (level 0 or 1), use a default threshold
|
|
||||||
// Level 0 (Incompetent): threshold = 9 (very hard)
|
|
||||||
// Level 1 (Beginner): threshold = 7 (hard)
|
|
||||||
// Level >= 2: null (use normal threshold)
|
|
||||||
|
|
||||||
if (skillLevel === 0) return 9; // Very hard
|
|
||||||
if (skillLevel === 1) return 7; // Hard
|
|
||||||
|
|
||||||
return null; // Utiliser le seuil normal
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles reroll events on dice in chat messages.
|
* Handles reroll events on dice in chat messages.
|
||||||
* @param {Object} message - The chat message containing the reroll event
|
* @param {Object} message - The chat message containing the reroll event
|
||||||
* @param {Object} ev - The reroll event
|
* @param {HTMLElement} target - The clicked die element
|
||||||
* @returns {Promise<boolean>} Whether the reroll was successful
|
* @returns {Promise<boolean>} Whether the reroll was successful
|
||||||
*/
|
*/
|
||||||
static async onReroll(message, ev) {
|
static async onReroll(message, target) {
|
||||||
// Verify user permissions
|
// Verify user permissions
|
||||||
const msgUserId = message.user?.id ?? message.user;
|
const msgUserId = message.user?.id ?? message.user;
|
||||||
if (msgUserId !== game.user?.id && !game.user?.isGM) {
|
if (msgUserId !== game.user?.id && !game.user?.isGM) {
|
||||||
@@ -273,7 +244,7 @@ export class VermineUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get reroll count
|
// Get reroll count
|
||||||
const rollMessage = ev.currentTarget.closest('div.vermine-roll-message');
|
const rollMessage = target.closest('div.vermine-roll-message');
|
||||||
if (!rollMessage) {
|
if (!rollMessage) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -283,22 +254,22 @@ export class VermineUtils {
|
|||||||
// Check if rerolls are available
|
// Check if rerolls are available
|
||||||
if (!rerollCount || parseInt(rerollCount, 10) < 1) {
|
if (!rerollCount || parseInt(rerollCount, 10) < 1) {
|
||||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_rerolls_left'));
|
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_rerolls_left'));
|
||||||
const rerollables = ev.currentTarget.closest('ul')?.querySelectorAll('.rerollable');
|
const rerollables = target.closest('ul')?.querySelectorAll('.rerollable');
|
||||||
if (rerollables) {
|
if (rerollables) {
|
||||||
rerollables.forEach(el => el.classList.remove('rerollable'));
|
rerollables.forEach(el => el.classList.remove('rerollable'));
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ev.currentTarget.classList.add('rerolled');
|
target.classList.add('rerolled');
|
||||||
|
|
||||||
// Set reroll flag
|
// Set reroll flag
|
||||||
await message.setFlag("world", "reroll", true);
|
await message.setFlag("world", "reroll", true);
|
||||||
|
|
||||||
// Get difficulty and dice type
|
// Get difficulty and dice type
|
||||||
const ulElement = ev.currentTarget.closest('ul');
|
const ulElement = target.closest('ul');
|
||||||
const difficulty = ulElement?.dataset.difficulty ?? 7;
|
const difficulty = ulElement?.dataset.difficulty ?? 7;
|
||||||
let diceType = ev.currentTarget.dataset.diceType;
|
let diceType = target.dataset.diceType;
|
||||||
|
|
||||||
// Sanitize user name
|
// Sanitize user name
|
||||||
const safeUserName = (game.user?.name ?? "user").replace(/[^a-zA-Z0-9_]/g, '_');
|
const safeUserName = (game.user?.name ?? "user").replace(/[^a-zA-Z0-9_]/g, '_');
|
||||||
@@ -327,30 +298,68 @@ export class VermineUtils {
|
|||||||
|
|
||||||
// Update die display
|
// Update die display
|
||||||
const result = reroll.dice[0]?.results[0]?.result ?? 0;
|
const result = reroll.dice[0]?.results[0]?.result ?? 0;
|
||||||
const dieSpan = ev.currentTarget.querySelector('span');
|
const dieSpan = target.querySelector('span');
|
||||||
if (dieSpan) {
|
if (dieSpan) {
|
||||||
dieSpan.innerText = result;
|
dieSpan.innerText = result;
|
||||||
}
|
}
|
||||||
|
// Update the face used for the CSS background image.
|
||||||
|
target.dataset.result = result;
|
||||||
|
|
||||||
// Update total if successful
|
// Set/clear the success class from the rerolled die (a previous success
|
||||||
|
// must be removed if the new result fails).
|
||||||
const success = reroll.dice[0]?.results[0]?.success;
|
const success = reroll.dice[0]?.results[0]?.success;
|
||||||
if (success) {
|
target.classList.toggle('success', !!success);
|
||||||
ev.currentTarget.classList.add('success');
|
target.classList.remove("rerollable");
|
||||||
const totalElement = rollMessage.querySelector('#total');
|
|
||||||
if (totalElement) {
|
// Recompute the total from the live dice in the DOM. This avoids the
|
||||||
const currentTotal = parseInt(totalElement.innerText, 10) || 0;
|
// cumulative-increment drift of the old logic (which never removed a
|
||||||
totalElement.innerText = currentTotal + reroll.total;
|
// removed success from #total). Totem dice (human/adapted) count double.
|
||||||
}
|
const totalEl = rollMessage.querySelector('#total');
|
||||||
|
if (totalEl) {
|
||||||
|
let total = 0;
|
||||||
|
rollMessage.querySelectorAll('li.die.success').forEach(die => {
|
||||||
|
const isTotem = die.classList.contains('human') || die.classList.contains('adapted');
|
||||||
|
total += isTotem ? 2 : 1;
|
||||||
|
});
|
||||||
|
totalEl.innerText = total;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update message content
|
// Update message content
|
||||||
ev.currentTarget.classList.remove("rerollable");
|
const messageContent = target.closest('div.message-content');
|
||||||
|
|
||||||
const messageContent = ev.currentTarget.closest('div.message-content');
|
|
||||||
if (messageContent) {
|
if (messageContent) {
|
||||||
const newRerollCount = parseInt(rerollCount, 10) - 1;
|
const newRerollCount = parseInt(rerollCount, 10) - 1;
|
||||||
rollMessage.querySelector('#allowed_reroll').innerText = newRerollCount;
|
rollMessage.querySelector('#allowed_reroll').innerText = newRerollCount;
|
||||||
|
|
||||||
|
// Recompute the verdict banner from the recomputed total.
|
||||||
|
const verdictEl = rollMessage.querySelector('.roll-verdict');
|
||||||
|
const requiredEl = rollMessage.querySelector('#required');
|
||||||
|
if (verdictEl && totalEl && requiredEl) {
|
||||||
|
const total = parseInt(totalEl.innerText, 10) || 0;
|
||||||
|
const required = parseInt(requiredEl.innerText, 10) || 1;
|
||||||
|
const won = total >= required;
|
||||||
|
verdictEl.classList.toggle('success', won);
|
||||||
|
verdictEl.classList.toggle('failure', !won);
|
||||||
|
const label = won
|
||||||
|
? game.i18n.localize('VERMINE.roll_success')
|
||||||
|
: game.i18n.localize('VERMINE.roll_failure');
|
||||||
|
verdictEl.innerHTML = `${won ? '<i class="fas fa-check-circle"></i>' : '<i class="fas fa-times-circle"></i>'}<span>${label}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recompute the dice summary from the current die list.
|
||||||
|
const summaryEl = rollMessage.querySelector('.roll-dice-summary');
|
||||||
|
if (summaryEl) {
|
||||||
|
const stats = { regular: { count: 0, success: 0 }, human: { count: 0, success: 0 }, adapted: { count: 0, success: 0 } };
|
||||||
|
rollMessage.querySelectorAll('li.die').forEach(die => {
|
||||||
|
const type = die.classList.contains('human') ? 'human' : die.classList.contains('adapted') ? 'adapted' : 'regular';
|
||||||
|
stats[type].count += 1;
|
||||||
|
if (die.classList.contains('success')) stats[type].success += 1;
|
||||||
|
});
|
||||||
|
const mk = (key, count, s) => count ? `<span class="die-summary ${key}">${count} ${game.i18n.localize(`VERMINE.die_${key}`)} · ${s} ${game.i18n.localize('VERMINE.die_successes')}</span>` : '';
|
||||||
|
summaryEl.innerHTML = mk('regular', stats.regular.count, stats.regular.success)
|
||||||
|
+ mk('human', stats.human.count, stats.human.success)
|
||||||
|
+ mk('adapted', stats.adapted.count, stats.adapted.success);
|
||||||
|
}
|
||||||
|
|
||||||
await message.update({
|
await message.update({
|
||||||
content: messageContent.outerHTML
|
content: messageContent.outerHTML
|
||||||
});
|
});
|
||||||
@@ -364,67 +373,63 @@ export class VermineUtils {
|
|||||||
* @param {HTMLElement} html - The HTML element containing chat events.
|
* @param {HTMLElement} html - The HTML element containing chat events.
|
||||||
*/
|
*/
|
||||||
static async chatListenners(html) {
|
static async chatListenners(html) {
|
||||||
// Ensure html is a jQuery object
|
|
||||||
const $html = $(html);
|
|
||||||
|
|
||||||
// Get reroll count
|
// Get reroll count
|
||||||
const rerollCountElement = $html.find('#allowed_reroll')[0];
|
const rerollCountElement = html.querySelector('#allowed_reroll');
|
||||||
const rerollCount = rerollCountElement?.innerText;
|
const rerollCount = rerollCountElement?.innerText;
|
||||||
|
|
||||||
// Enable/disable rerolls based on count
|
// Enable/disable rerolls based on count
|
||||||
if (!rerollCount || parseInt(rerollCount, 10) < 1) {
|
const dieClass = !rerollCount || parseInt(rerollCount, 10) < 1 ? 'remove' : 'add';
|
||||||
// Disable rerolls for all dice
|
html.querySelectorAll('.die').forEach(el => el.classList[dieClass]("rerollable"));
|
||||||
$html.find('.die').each(function() {
|
|
||||||
this.classList.remove("rerollable");
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Enable rerolls for all dice
|
|
||||||
$html.find('.die').each(function() {
|
|
||||||
this.classList.add("rerollable");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add click event for rerollable dice
|
// Add click event for rerollable dice
|
||||||
$html.find('.rerollable').click(async (ev) => {
|
html.querySelectorAll('.rerollable').forEach(el => {
|
||||||
ev.preventDefault();
|
el.addEventListener('click', async (ev) => {
|
||||||
const msgId = ev.currentTarget.closest("li.message")?.dataset?.messageId;
|
ev.preventDefault();
|
||||||
if (msgId) {
|
const dieEl = ev.currentTarget;
|
||||||
const message = await game.messages.get(msgId);
|
const msgId = dieEl.closest("li.message")?.dataset?.messageId;
|
||||||
await VermineUtils.onReroll(message, ev);
|
if (msgId) {
|
||||||
}
|
const message = await game.messages.get(msgId);
|
||||||
|
await VermineUtils.onReroll(message, dieEl);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update granted reroll label
|
// Update granted reroll label
|
||||||
$html.find("#effort-reroll").change(ev => {
|
const effortReroll = html.querySelector("#effort-reroll");
|
||||||
const label = $html.find("#granted-reroll")[0];
|
if (effortReroll) {
|
||||||
if (label) {
|
effortReroll.addEventListener('change', ev => {
|
||||||
label.innerText = ev.currentTarget.value;
|
const label = html.querySelector("#granted-reroll");
|
||||||
}
|
if (label) {
|
||||||
});
|
label.innerText = ev.currentTarget.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Add click event for granting rerolls
|
// Add click event for granting rerolls
|
||||||
$html.find("button.grant-reroll").click(async (ev) => {
|
html.querySelectorAll("button.grant-reroll").forEach(el => {
|
||||||
const grantedRerollElement = $html.find('#granted-reroll')[0];
|
el.addEventListener('click', async (ev) => {
|
||||||
const allowedRerollElement = $html.find("#allowed_reroll")[0];
|
const btn = ev.currentTarget;
|
||||||
|
const grantedRerollElement = html.querySelector('#granted-reroll');
|
||||||
|
const allowedRerollElement = html.querySelector("#allowed_reroll");
|
||||||
|
|
||||||
if (grantedRerollElement && allowedRerollElement) {
|
if (grantedRerollElement && allowedRerollElement) {
|
||||||
allowedRerollElement.innerText = grantedRerollElement.innerText;
|
allowedRerollElement.innerText = grantedRerollElement.innerText;
|
||||||
}
|
|
||||||
|
|
||||||
const mesEl = ev.currentTarget.closest('[data-message-id]');
|
|
||||||
const messageId = mesEl?.dataset?.messageId;
|
|
||||||
|
|
||||||
if (messageId) {
|
|
||||||
// Hide reroll grant area
|
|
||||||
ev.currentTarget.closest('.reroll-from-effort').style.display = "none";
|
|
||||||
|
|
||||||
const rollMessage = ev.currentTarget.closest(".vermine-roll-message");
|
|
||||||
if (rollMessage) {
|
|
||||||
const content = rollMessage.outerHTML;
|
|
||||||
const message = await game.messages.get(messageId);
|
|
||||||
await message.update({ content: content });
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
const mesEl = btn.closest('[data-message-id]');
|
||||||
|
const messageId = mesEl?.dataset?.messageId;
|
||||||
|
|
||||||
|
if (messageId) {
|
||||||
|
btn.closest('.reroll-from-effort').style.display = "none";
|
||||||
|
|
||||||
|
const rollMessage = btn.closest(".vermine-roll-message");
|
||||||
|
if (rollMessage) {
|
||||||
|
const content = rollMessage.outerHTML;
|
||||||
|
const message = await game.messages.get(messageId);
|
||||||
|
await message.update({ content: content });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,10 +453,10 @@ export class VermineUtils {
|
|||||||
blind = true;
|
blind = true;
|
||||||
// Falls through
|
// Falls through
|
||||||
case "gmroll": // GM + rolling player
|
case "gmroll": // GM + rolling player
|
||||||
whisper = this.getUsers(user => user.isGM);
|
whisper = game.users?.filter(user => user.isGM) || [];
|
||||||
break;
|
break;
|
||||||
case "roll": // Everybody
|
case "roll": // Everybody
|
||||||
whisper = this.getUsers(user => user.active);
|
whisper = game.users?.filter(user => user.active) || [];
|
||||||
break;
|
break;
|
||||||
case "selfroll":
|
case "selfroll":
|
||||||
whisper = [game.user.id];
|
whisper = [game.user.id];
|
||||||
@@ -469,15 +474,37 @@ export class VermineUtils {
|
|||||||
* @returns {Promise<ChatMessage>} The created chat message
|
* @returns {Promise<ChatMessage>} The created chat message
|
||||||
*/
|
*/
|
||||||
static async diplayChatRoll(roll, param) {
|
static async diplayChatRoll(roll, param) {
|
||||||
const content = await foundry.applications.handlebars.renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param });
|
// Aggregate dice stats per type (regular / human / adapted)
|
||||||
|
const diceStats = { regular: { count: 0, success: 0 }, human: { count: 0, success: 0 }, adapted: { count: 0, success: 0 } };
|
||||||
|
for (const die of roll.dice ?? []) {
|
||||||
|
// Flavors look like "<type>_<user>"; classify from the leading token.
|
||||||
|
const flavor = die.options?.flavor ?? "";
|
||||||
|
const prefix = flavor.split("_")[0];
|
||||||
|
const type = prefix === "human" ? "human" : prefix === "adapted" ? "adapted" : "regular";
|
||||||
|
for (const r of die.results ?? []) {
|
||||||
|
diceStats[type].count += 1;
|
||||||
|
if (r.success) diceStats[type].success += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Verdict
|
||||||
|
const required = 1 + (param.handicap ?? 0);
|
||||||
|
const total = roll._total ?? 0;
|
||||||
|
const verdict = {
|
||||||
|
success: total >= required,
|
||||||
|
total,
|
||||||
|
required
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = await foundry.applications.handlebars.renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param, diceStats, verdict });
|
||||||
const chatData = {
|
const chatData = {
|
||||||
user: game.user?._id,
|
user: game.user?._id,
|
||||||
speaker: ChatMessage.getSpeaker(),
|
speaker: ChatMessage.getSpeaker(),
|
||||||
content: content,
|
content: content
|
||||||
roll: roll
|
|
||||||
};
|
};
|
||||||
|
if (param.messageFlags) {
|
||||||
|
chatData.flags = { world: param.messageFlags };
|
||||||
|
}
|
||||||
const msg = await ChatMessage.create(chatData);
|
const msg = await ChatMessage.create(chatData);
|
||||||
await msg.setFlag('world', 'roll', roll);
|
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const registerSettings = function () {
|
|||||||
"2": "Cauchemar",
|
"2": "Cauchemar",
|
||||||
"3": "Apocalypse"
|
"3": "Apocalypse"
|
||||||
},
|
},
|
||||||
default: 'e',
|
default: '1',
|
||||||
onChange: value => {
|
onChange: value => {
|
||||||
let el = document.querySelector('.game-mode');
|
let el = document.querySelector('.game-mode');
|
||||||
el.id = 'game-mode-' + game.settings.get('vermine2047', 'game-mode')
|
el.id = 'game-mode-' + game.settings.get('vermine2047', 'game-mode')
|
||||||
|
|||||||
@@ -139,13 +139,15 @@ class WelcomeTour extends VermineTour {
|
|||||||
}
|
}
|
||||||
export async function registerTours() {
|
export async function registerTours() {
|
||||||
game.tours.register("vermine2047", "welcome", new WelcomeTour());
|
game.tours.register("vermine2047", "welcome", new WelcomeTour());
|
||||||
$(document).on("click", "#chat-log #vermine-tour-chat-button", (el) => {
|
document.addEventListener("click", (el) => {
|
||||||
const tour = game.tours.get("vermine2047.welcome");
|
if (el.target.closest("#chat-log #vermine-tour-chat-button")) {
|
||||||
tour == null ? void 0 : tour.start();
|
const tour = game.tours.get("vermine2047.welcome");
|
||||||
|
tour?.start();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (game.settings.get("vermine2047", "first-run-tips-shown"))
|
if (game.settings.get("vermine2047", "first-run-tips-shown"))
|
||||||
return;
|
return;
|
||||||
console.log("Posting first-start messages...");
|
// Posting first-start messages
|
||||||
const gms = ChatMessage.getWhisperRecipients("GM");
|
const gms = ChatMessage.getWhisperRecipients("GM");
|
||||||
ChatMessage.implementation;
|
ChatMessage.implementation;
|
||||||
ChatMessage.create({
|
ChatMessage.create({
|
||||||
|
|||||||
+18
-2
@@ -37,6 +37,14 @@ Hooks.once('init', async function () {
|
|||||||
CONFIG.Actor.dataModels.npc = models.VermineNpcData;
|
CONFIG.Actor.dataModels.npc = models.VermineNpcData;
|
||||||
CONFIG.Actor.dataModels.group = models.VermineGroupData;
|
CONFIG.Actor.dataModels.group = models.VermineGroupData;
|
||||||
CONFIG.Actor.dataModels.creature = models.VermineCreatureData;
|
CONFIG.Actor.dataModels.creature = models.VermineCreatureData;
|
||||||
|
CONFIG.Actor.dataModels.community = models.VermineCommunityData;
|
||||||
|
|
||||||
|
// Libellé du type "community" (TYPES.Actor.* du core ne le connaît pas)
|
||||||
|
CONFIG.Actor.typeLabels ??= {};
|
||||||
|
CONFIG.Actor.typeLabels.community = "VERMINE.type_community";
|
||||||
|
Hooks.once('i18nInit', () => {
|
||||||
|
CONFIG.Actor.typeLabels.community = "VERMINE.type_community";
|
||||||
|
});
|
||||||
|
|
||||||
CONFIG.Item.dataModels.item = models.VermineItemData;
|
CONFIG.Item.dataModels.item = models.VermineItemData;
|
||||||
CONFIG.Item.dataModels.weapon = models.VermineWeaponData;
|
CONFIG.Item.dataModels.weapon = models.VermineWeaponData;
|
||||||
@@ -71,7 +79,7 @@ Hooks.once('init', async function () {
|
|||||||
|
|
||||||
// Register sheet application classes (ApplicationV2)
|
// Register sheet application classes (ApplicationV2)
|
||||||
// Unregister core sheets
|
// Unregister core sheets
|
||||||
foundry.applications.sheets.ActorSheetV2?.unregisterSheet?.("core", "Actor", {
|
foundry.documents.collections.Actors.unregisterSheet("core", foundry.applications.sheets.ActorSheetV2, {
|
||||||
types: ["character", "npc", "group", "creature"]
|
types: ["character", "npc", "group", "creature"]
|
||||||
})
|
})
|
||||||
foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1?.sheets?.ItemSheet)
|
foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1?.sheets?.ItemSheet)
|
||||||
@@ -89,6 +97,9 @@ Hooks.once('init', async function () {
|
|||||||
foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineGroupSheetV2, {
|
foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineGroupSheetV2, {
|
||||||
types: ["group"], makeDefault: true, label: "VERMINE.Sheet.group"
|
types: ["group"], makeDefault: true, label: "VERMINE.Sheet.group"
|
||||||
})
|
})
|
||||||
|
foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineCommunitySheetV2, {
|
||||||
|
types: ["community"], makeDefault: true, label: "VERMINE.Sheet.community"
|
||||||
|
})
|
||||||
|
|
||||||
// Item sheets — un par type
|
// Item sheets — un par type
|
||||||
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineItemSheetV2, {
|
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineItemSheetV2, {
|
||||||
@@ -180,6 +191,7 @@ Hooks.once('init', async function () {
|
|||||||
"systems/vermine2047/templates/actor/appv2/npc-main.hbs",
|
"systems/vermine2047/templates/actor/appv2/npc-main.hbs",
|
||||||
"systems/vermine2047/templates/actor/appv2/npc-characteristics.hbs",
|
"systems/vermine2047/templates/actor/appv2/npc-characteristics.hbs",
|
||||||
"systems/vermine2047/templates/actor/appv2/npc-skills.hbs",
|
"systems/vermine2047/templates/actor/appv2/npc-skills.hbs",
|
||||||
|
"systems/vermine2047/templates/actor/appv2/npc-equipment.hbs",
|
||||||
"systems/vermine2047/templates/actor/appv2/npc-threat.hbs",
|
"systems/vermine2047/templates/actor/appv2/npc-threat.hbs",
|
||||||
"systems/vermine2047/templates/actor/appv2/npc-combat.hbs",
|
"systems/vermine2047/templates/actor/appv2/npc-combat.hbs",
|
||||||
"systems/vermine2047/templates/actor/appv2/npc-notes.hbs",
|
"systems/vermine2047/templates/actor/appv2/npc-notes.hbs",
|
||||||
@@ -192,7 +204,11 @@ Hooks.once('init', async function () {
|
|||||||
"systems/vermine2047/templates/actor/appv2/creature-info.hbs",
|
"systems/vermine2047/templates/actor/appv2/creature-info.hbs",
|
||||||
"systems/vermine2047/templates/actor/appv2/creature-stats.hbs",
|
"systems/vermine2047/templates/actor/appv2/creature-stats.hbs",
|
||||||
"systems/vermine2047/templates/actor/appv2/creature-combat.hbs",
|
"systems/vermine2047/templates/actor/appv2/creature-combat.hbs",
|
||||||
"systems/vermine2047/templates/actor/appv2/creature-effects.hbs"
|
"systems/vermine2047/templates/actor/appv2/creature-effects.hbs",
|
||||||
|
"systems/vermine2047/templates/actor/appv2/community-main.hbs",
|
||||||
|
"systems/vermine2047/templates/actor/appv2/community-identity.hbs",
|
||||||
|
"systems/vermine2047/templates/actor/appv2/community-domains.hbs",
|
||||||
|
"systems/vermine2047/templates/actor/appv2/community-resources.hbs"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
|||||||
.app.vermine2047.trait-selector {
|
|
||||||
.form-group {
|
|
||||||
box-shadow: 0 0 30px gray;
|
|
||||||
padding: 0.3rem 0.5rem;
|
|
||||||
|
|
||||||
border: 3px solid rgb(142, 144, 16);
|
|
||||||
|
|
||||||
&:has(input[type="checkbox"]:checked) {
|
|
||||||
border: 3px solid green
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
border-bottom: 2px solid black
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
// Flexbox.
|
|
||||||
.flex-group-center,
|
|
||||||
.flex-group-left,
|
|
||||||
.flex-group-right {
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-group-left {
|
|
||||||
justify-content: flex-start;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-group-right {
|
|
||||||
justify-content: flex-end;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flexshrink {
|
|
||||||
flex: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-center {
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-between {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-around {
|
|
||||||
justify-content: space-around;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.flexlarge {
|
|
||||||
flex: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flexsmall {
|
|
||||||
flex: 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alignment styles.
|
|
||||||
.align-left {
|
|
||||||
justify-content: flex-start;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-right {
|
|
||||||
justify-content: flex-end;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-center {
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
@@ -1,772 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: "DistressBlack";
|
|
||||||
src: url("../assets/fonts/dcc_sharp_distress_black_by_dccanim.otf");
|
|
||||||
}
|
|
||||||
|
|
||||||
.sans-font {
|
|
||||||
font-family: "DistressBlack", sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Change shadow colors and Foundry general font color!! */
|
|
||||||
.app {
|
|
||||||
box-shadow: 0 0 20px #7e7544;
|
|
||||||
/* Change the color code here for a nice shadow color! */
|
|
||||||
color: #dfdfdf;
|
|
||||||
/* Text color for window titles and menu all across Foundry */
|
|
||||||
}
|
|
||||||
|
|
||||||
.vermine2047.sheet.actor {
|
|
||||||
|
|
||||||
header.sheet-header,
|
|
||||||
.windoow-content {
|
|
||||||
min-height: fit-content
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Character and Item Name Titles Text style! */
|
|
||||||
.sheet .charname input {
|
|
||||||
color: #191813;
|
|
||||||
font-family: "DistressBlack", sans-serif;
|
|
||||||
font-size: 30px;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.profile-img {
|
|
||||||
filter: drop-shadow(0px 0px 20px rgb(110, 133, 27));
|
|
||||||
height: auto;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 10rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* custom styles */
|
|
||||||
body.system-vermine2047 img#logo {
|
|
||||||
content: url("/systems/vermine2047/assets/images/ui/logo_vermine_foundry.webp");
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Customize the chat history area! */
|
|
||||||
#chat-log .message {
|
|
||||||
background: url(/systems/vermine2047/assets/images/ui/box_background.webp) repeat;
|
|
||||||
/*color: #191813; */
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ul.unstyled {
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.unstyled li {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.padding-with-frieze {
|
|
||||||
margin-left: 18% !important;
|
|
||||||
margin-right: 10% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.padding-with-frieze li {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-full {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx-auto {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ----------------------------------------- */
|
|
||||||
/* Sheet Structure */
|
|
||||||
/* ----------------------------------------- */
|
|
||||||
.system-vermine2047 .sheet .window-content {
|
|
||||||
background: url(/systems/vermine2047/assets/images/ui/box_background.webp);
|
|
||||||
padding: 0;
|
|
||||||
overflow-y: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .dialog .window-content {
|
|
||||||
background: url(/systems/vermine2047/assets/images/ui/fond_chat_box.webp);
|
|
||||||
padding: 0.5rem;
|
|
||||||
overflow-y: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-content .row.smb {
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-content .row.mdb {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-content .row.lgb {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor.sheet form .form {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
align-items: flex-start;
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor.sheet .form aside {
|
|
||||||
min-width: max-content;
|
|
||||||
background-image: url(/systems/vermine2047/assets/images/ui/barre_laterale.webp);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: auto 200%;
|
|
||||||
border-right: 2px black;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0 0.3rem;
|
|
||||||
box-shadow: -20px 0px 100px 15px #000000b5 inset;
|
|
||||||
|
|
||||||
|
|
||||||
.major-totem {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
position: absolute;
|
|
||||||
transform: rotate(-8deg);
|
|
||||||
opacity: 1;
|
|
||||||
transition: 0.2s;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor.sheet .form main {
|
|
||||||
box-shadow: 10px 0px 100px #000000b5 inset;
|
|
||||||
padding-left: 1rem;
|
|
||||||
grid-row: span 1 / span 1;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor.sheet .form aside .image-wrapper {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor.sheet .form aside .image-wrapper img {
|
|
||||||
width: 80%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor.sheet .form aside .paper {
|
|
||||||
margin-top: 1rem;
|
|
||||||
height: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor.sheet .form aside .second-paper {
|
|
||||||
margin-top: 4rem;
|
|
||||||
height: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor.sheet .form h3 {
|
|
||||||
font-family: "DistressBlack", sans-serif;
|
|
||||||
text-align: center;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #4e564c;
|
|
||||||
font-size: 1.7rem;
|
|
||||||
border-bottom: none;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor.sheet .form h4,
|
|
||||||
.item.sheet .form h4 {
|
|
||||||
font-family: "DistressBlack", sans-serif;
|
|
||||||
font-size: 1.4em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin: 0 0 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor.sheet .form .characteristics h4 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
margin-top: 0 0.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor.sheet .form .tab.totem h4,
|
|
||||||
.actor.sheet .form .tab.equipment h4,
|
|
||||||
.actor.sheet .form .tab.stories h4 {
|
|
||||||
margin-top: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .char-header {
|
|
||||||
font-family: "DistressBlack", sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .char-header section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .char-header h1.char-name,
|
|
||||||
.system-vermine2047 .char-vermine2047 {
|
|
||||||
border-bottom: none;
|
|
||||||
line-height: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .char-vermine2047 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor form {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor form div.hexa {
|
|
||||||
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
|
|
||||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.425) 0%, rgba(0, 0, 0, 0.288) 100%);
|
|
||||||
height: 1.5rem;
|
|
||||||
max-width: 1.5rem;
|
|
||||||
color: black;
|
|
||||||
transform: rotate(90deg);
|
|
||||||
transition: 0.2s;
|
|
||||||
margin: 0.2rem;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.425) 0%, rgba(0, 0, 0, 0.288) 100%);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
opacity: 1;
|
|
||||||
min-width: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
opacity: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
&.checked {
|
|
||||||
&:hover {
|
|
||||||
background: radial-gradient(circle, rgb(43, 43, 43) 0%, rgba(0, 0, 0, 0.288) 100%);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
background: radial-gradient(circle, rgb(0, 0, 0) 0%, rgba(0, 0, 0, 0.288) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.unavailable {
|
|
||||||
background: radial-gradient(circle, rgba(66, 15, 15, 0.664) 0%, rgba(131, 70, 70, 0.432) 100%);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor .totem-details {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
img.img-totem {
|
|
||||||
transform-origin: 50% 50%;
|
|
||||||
filter: grayscale(1);
|
|
||||||
opacity: 0.15;
|
|
||||||
position: absolute;
|
|
||||||
width: 30%;
|
|
||||||
height: auto;
|
|
||||||
pointer-events: none;
|
|
||||||
aspect-ratio: 1/1;
|
|
||||||
left: 35%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor div.minor-totems {
|
|
||||||
position: relative;
|
|
||||||
background-color: #929c6f85;
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 2rem;
|
|
||||||
position: absolute;
|
|
||||||
bottom: -2rem;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
h5.human,
|
|
||||||
h5.adapted {
|
|
||||||
transition: 0.3s;
|
|
||||||
|
|
||||||
img.img-totem {
|
|
||||||
filter: drop-shadow(0px 0px 20px rgb(0, 0, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
&.major {
|
|
||||||
transform: scale(1.1);
|
|
||||||
|
|
||||||
& img {
|
|
||||||
filter: drop-shadow(0px 0px 10px red)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.totem-dice {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.human-dice,
|
|
||||||
.adapted-dice {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin-left: 2rem;
|
|
||||||
|
|
||||||
|
|
||||||
i {
|
|
||||||
padding-top: 0.5rem;
|
|
||||||
color: #064930;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.adapted-dice {
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 2rem;
|
|
||||||
transform: rotate(180deg);
|
|
||||||
|
|
||||||
i {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
padding-top: 0.5rem;
|
|
||||||
color: rgb(85, 52, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.human {
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
img {
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.adapted {
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
img {
|
|
||||||
right: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor form input[type=text],
|
|
||||||
.system-vermine2047 .sheet.actor form input[type=number] {
|
|
||||||
width: calc(100% - 2px);
|
|
||||||
height: calc(100% - 2px);
|
|
||||||
background: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 1px 0;
|
|
||||||
color: #333;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
|
|
||||||
&.hexa {
|
|
||||||
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
|
|
||||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.425) 0%, rgba(0, 0, 0, 0.288) 100%);
|
|
||||||
height: 1.5rem;
|
|
||||||
max-width: 1.5rem;
|
|
||||||
color: black
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor form input[disabled],
|
|
||||||
.system-vermine2047 .sheet.actor form select[disabled] {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor form input[type=text]:hover:not(:disabled),
|
|
||||||
.system-vermine2047 .sheet.actor form input[type=text]:focus,
|
|
||||||
.system-vermine2047 .sheet.actor form select:hover:not(:disabled),
|
|
||||||
.system-vermine2047 .sheet.actor form select:focus,
|
|
||||||
.system-vermine2047 .sheet.actor form input[type=number]:hover:not(:disabled),
|
|
||||||
.system-vermine2047 .sheet.actor form input[type=number]:focus,
|
|
||||||
.system-vermine2047 .sheet.actor form textarea:hover:not(:disabled),
|
|
||||||
.system-vermine2047 .sheet.actor form textarea:focus {
|
|
||||||
box-shadow: 0 0 10px #005a3c inset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor form select {
|
|
||||||
font-size: 0.6rem;
|
|
||||||
border: none;
|
|
||||||
appearance: none;
|
|
||||||
min-width: fit-content;
|
|
||||||
max-width: fit-content;
|
|
||||||
padding: 0 0.2rem;
|
|
||||||
margin: 0 0.2rem;
|
|
||||||
cursor: help;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor form label {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor form .mce-panel span {
|
|
||||||
display: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor form.editable .rollable:hover,
|
|
||||||
.system-vermine2047 .sheet.actor form.editable a:hover {
|
|
||||||
color: #000;
|
|
||||||
text-shadow: 0 5px 5px #1fa832;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor form .sheet-tabs {
|
|
||||||
font-weight: 500;
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor form .sheet-tabs>.list-row {
|
|
||||||
line-height: 24px;
|
|
||||||
padding-top: 3px;
|
|
||||||
font-size: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor form .sheet-tabs>.list-row:last-of-type {
|
|
||||||
padding-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor form .sheet-tabs>.list-row.active {
|
|
||||||
color: #000;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor form .tab {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor form .tag-legacy {
|
|
||||||
float: left;
|
|
||||||
margin: 0 2px 2px 0;
|
|
||||||
padding: 0 3px;
|
|
||||||
font-size: var(--font-size-10);
|
|
||||||
line-height: 16px;
|
|
||||||
border: 1px solid #999;
|
|
||||||
border-radius: 3px;
|
|
||||||
white-space: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------------------------------------- */
|
|
||||||
/* Actor Sheet */
|
|
||||||
/* ---------------------------------------- */
|
|
||||||
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor,
|
|
||||||
.system-vermine2047 .sheet.actor .window-content {
|
|
||||||
min-width: 690px;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor .sidebar {
|
|
||||||
flex: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor .floatright {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor .sheet-upper {
|
|
||||||
height: 268px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor .sheet-upper .sheet-header {
|
|
||||||
height: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor .sheet-upper .sheet-profile,
|
|
||||||
.system-vermine2047 .sheet.actor .sheet-upper .sheet-showcase {
|
|
||||||
height: 220px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor .sheet-content {
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor .sheet-sidebar {
|
|
||||||
height: calc(100% - 48px);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor .sheet-sidebar>* {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor .sheet-sidebar .sidebar-summary {
|
|
||||||
overflow-y: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor.npc-sheet .sheet-upper {
|
|
||||||
height: 220px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor.npc-sheet .sheet-upper .sheet-showcase {
|
|
||||||
height: 172px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor.npc-sheet .sheet-lower {
|
|
||||||
height: calc(100% - 220px - 32px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor .sheet-navigation {
|
|
||||||
border-top: 1px solid var(--secondary-background);
|
|
||||||
border-bottom: 1px solid var(--primary-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor .sheet-navigation .sheet-tabs>.list-row {
|
|
||||||
border-radius: 5px 5px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor .sheet-navigation .sheet-tabs>.list-row.active {
|
|
||||||
border: 1px solid #666;
|
|
||||||
border-bottom: none;
|
|
||||||
/* box-shadow: 0 0 10px inset #ff6400;
|
|
||||||
*/
|
|
||||||
background: var(--primary-background);
|
|
||||||
color: #fff;
|
|
||||||
text-shadow: none;
|
|
||||||
|
|
||||||
color: #000;
|
|
||||||
text-shadow: 0 0 10px #00005a;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.actor.sheet nav.sheet-navigation {
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
align-items: center;
|
|
||||||
height: 54px;
|
|
||||||
background: url(../assets/images/ui/barre_haut.webp) no-repeat right top;
|
|
||||||
background-size: 100% 100%;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
padding-right: 4rem;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor.sheet nav.sheet-navigation.tabs .item {
|
|
||||||
height: 2.4rem;
|
|
||||||
display: inline-block;
|
|
||||||
z-index: 1;
|
|
||||||
transition: all 0.1s ease-out;
|
|
||||||
color: #606060;
|
|
||||||
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.404);
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor.sheet nav.sheet-navigation.tabs .item:hover,
|
|
||||||
.actor.sheet nav.sheet-navigation.tabs .item.active {
|
|
||||||
color: #000;
|
|
||||||
text-shadow: 0 5px 5px #1fa832;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor.sheet nav.sheet-navigation.tabs .item:hover {
|
|
||||||
text-shadow: 0 5px 5px #1e52259a;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor form nav.sheet-navigation.sheet-tabs {
|
|
||||||
height: 54px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//Query pour les lignes de caract
|
|
||||||
@container ability-row (max-width: 240px) {
|
|
||||||
|
|
||||||
.skill-dots,
|
|
||||||
span.hexa {
|
|
||||||
display: none
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor .ability {
|
|
||||||
padding-right: 0.6rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
border-bottom: 1px solid rgba(170, 170, 152, 0.664);
|
|
||||||
box-shadow: 0px 0px 15px rgba(128, 128, 128, 0) inset;
|
|
||||||
transition: 0.2s;
|
|
||||||
position: relative;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
min-width: min-content;
|
|
||||||
container-type: inline-size;
|
|
||||||
container-name: ability-row;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: 0px 0px 15px gray inset;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
label {
|
|
||||||
min-width: 40%;
|
|
||||||
flex: 1.3;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
max-width: fit-content;
|
|
||||||
margin: 0 1rem;
|
|
||||||
|
|
||||||
flex: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
div.specialties {
|
|
||||||
position: absolute;
|
|
||||||
bottom: -0.2rem;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.skill-dots {
|
|
||||||
height: 100%;
|
|
||||||
align-self: center;
|
|
||||||
flex: 1.5;
|
|
||||||
min-width: fit-content;
|
|
||||||
|
|
||||||
&>div {
|
|
||||||
|
|
||||||
max-width: 0.7rem;
|
|
||||||
height: 0.7rem;
|
|
||||||
aspect-ratio: 1/1;
|
|
||||||
border-radius: 50%;
|
|
||||||
font-weight: 700;
|
|
||||||
text-align: center;
|
|
||||||
padding-bottom: 0.2rem;
|
|
||||||
font-style: oblique;
|
|
||||||
align-self: flex-start;
|
|
||||||
|
|
||||||
|
|
||||||
&.dice-pool-dot {
|
|
||||||
background: radial-gradient(circle, rgba(94, 90, 77, 1) 25%, rgba(0, 0, 0, 1) 100%);
|
|
||||||
max-width: 0.7rem;
|
|
||||||
aspect-ratio: 1/1;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.dice-reroll-dot {
|
|
||||||
background: radial-gradient(circle, rgb(187, 182, 165) 25%, rgba(0, 0, 0, 1) 100%);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor .skill-category {
|
|
||||||
padding: .3rem;
|
|
||||||
|
|
||||||
&.preferred {
|
|
||||||
|
|
||||||
h4,
|
|
||||||
label {
|
|
||||||
text-shadow: 0px 0px 5px rgba(0, 128, 0, 0.411)
|
|
||||||
}
|
|
||||||
|
|
||||||
box-shadow: 0px 0px 30px rgba(0, 128, 0, 0.306) inset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor #edit {
|
|
||||||
background-color: black;
|
|
||||||
color: white;
|
|
||||||
width: 100%
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet.actor .reserve-grid {
|
|
||||||
line-height: 0.5rem;
|
|
||||||
transform-origin: 0% 50%;
|
|
||||||
max-width: fit-content;
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
div.flexrow,
|
|
||||||
input,
|
|
||||||
.hexa {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
min-width: 1rem;
|
|
||||||
min-height: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&>.flexrow {
|
|
||||||
position: relative;
|
|
||||||
max-width: fit-content;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hexa {
|
|
||||||
text-align: center;
|
|
||||||
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
|
|
||||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.425) 0%, rgba(0, 0, 0, 0.288) 100%);
|
|
||||||
max-height: 1.5rem;
|
|
||||||
max-width: 1.5rem;
|
|
||||||
min-width: 1.5rem;
|
|
||||||
aspect-ratio: 1/1;
|
|
||||||
color: black;
|
|
||||||
vertical-align: center;
|
|
||||||
|
|
||||||
&.checked {
|
|
||||||
background: radial-gradient(circle, rgb(0, 0, 0) 0%, rgba(0, 0, 0, 0.288) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
;
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 1rem
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="radio"] {
|
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
|
|
||||||
&::after,
|
|
||||||
&::before {
|
|
||||||
display: none
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
.window-app.vermineDialog {
|
|
||||||
max-width: 50vw;
|
|
||||||
height: fit-content;
|
|
||||||
|
|
||||||
.window-content {
|
|
||||||
background: url(/systems/vermine2047/assets/images/ui/box_background.webp) repeat;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
details>summary::after {
|
|
||||||
content: "▶️";
|
|
||||||
position: relative;
|
|
||||||
right: 40%;
|
|
||||||
}
|
|
||||||
|
|
||||||
details[open]>summary::after {
|
|
||||||
content: "🔽"
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
justify-content: space-around;
|
|
||||||
box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.555);
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.5rem 0.2rem;
|
|
||||||
|
|
||||||
&>* {
|
|
||||||
margin: 0 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
;
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-family: "DistressBlack",
|
|
||||||
sans-serif;
|
|
||||||
font-size: larger;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
max-width: fit-content;
|
|
||||||
|
|
||||||
option {
|
|
||||||
max-width: fit-content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-buttons {
|
|
||||||
button {
|
|
||||||
display: block;
|
|
||||||
flex: 0.3
|
|
||||||
}
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
.sheet.item.vermine2047 {
|
|
||||||
.window-content {
|
|
||||||
padding: 0 1rem;
|
|
||||||
|
|
||||||
.flexrow {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
header,
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5 {
|
|
||||||
background: 50% 50%/cover no-repeat url(/systems/vermine2047/assets/images/ui/scotch.webp);
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-family: "DistressBlack";
|
|
||||||
margin-top: 1rem;
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource {
|
|
||||||
border: none;
|
|
||||||
border-left: 1px solid gray;
|
|
||||||
padding: 0.2rem 1rem;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
.flexrow {
|
|
||||||
min-width: 5rem;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.damages-row {
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
.radios {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.damage-pannes,
|
|
||||||
.damage-state,
|
|
||||||
.damage-effect {
|
|
||||||
text-align: center;
|
|
||||||
font-family: "DistressBlack";
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.traits {
|
|
||||||
box-shadow: 0px 5px 15px black;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
margin: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
ol#chat-log {
|
|
||||||
div.item-card {
|
|
||||||
header img {
|
|
||||||
max-width: 30%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-189
@@ -1,189 +0,0 @@
|
|||||||
ol#chat-log {
|
|
||||||
header.message-header {
|
|
||||||
background-color: black;
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vermine-roll-message {
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0px 0px 30px white inset;
|
|
||||||
padding: 0;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
|
|
||||||
.flexrow {
|
|
||||||
align-items: center;
|
|
||||||
box-shadow: 0px 5px 10px 0px black;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3,
|
|
||||||
h4 {
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-family: "DistressBlack";
|
|
||||||
margin-top: 1rem;
|
|
||||||
border-bottom: none;
|
|
||||||
font-weight: 900;
|
|
||||||
background: 50% 0%/cover no-repeat url(/systems/vermine2047/assets/images/ui/scotch.webp);
|
|
||||||
|
|
||||||
&+span {
|
|
||||||
font-family: "DistressBlack";
|
|
||||||
font-size: large;
|
|
||||||
text-transform: unset;
|
|
||||||
|
|
||||||
padding-left: 2rem;
|
|
||||||
background: -100% 0%/cover no-repeat url(/systems/vermine2047/assets/images/ui/scotch.webp);
|
|
||||||
|
|
||||||
&#allowed_reroll {
|
|
||||||
font-size: large;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
background: url(/systems/vermine2047/assets/images/ui/scotch.webp);
|
|
||||||
background-position: center;
|
|
||||||
background-size: 200%;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.roll-total {
|
|
||||||
transform: rotate(-3deg) scale(1.2) translateX(2rem) translateY(0.5rem);
|
|
||||||
background: url(/systems/vermine2047/assets/images/ui/scotch.webp);
|
|
||||||
background-position: center;
|
|
||||||
background-size: 200%;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
padding: 0;
|
|
||||||
z-index: +1;
|
|
||||||
width: 75%;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reroll {
|
|
||||||
button {
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-family: "DistressBlack";
|
|
||||||
padding: 0 1rem;
|
|
||||||
max-width: fit-content;
|
|
||||||
box-shadow: 0px 0px 2px black;
|
|
||||||
background: 50% 0%/cover no-repeat url(/systems/vermine2047/assets/images/ui/scotch.webp);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
transition:0.3s;
|
|
||||||
max-height:1px;
|
|
||||||
overflow: hidden;
|
|
||||||
justify-content: end;
|
|
||||||
text-align: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&.visible {
|
|
||||||
max-height: 15rem;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.roll-results {
|
|
||||||
list-style: none;
|
|
||||||
|
|
||||||
li.die {
|
|
||||||
position: relative;
|
|
||||||
max-width: 3rem;
|
|
||||||
line-height: 3rem;
|
|
||||||
float: left;
|
|
||||||
margin: 0.2rem;
|
|
||||||
background-image: url(/icons/dice/d10black.svg);
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: contain;
|
|
||||||
font-weight: 800;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: #ffffff;
|
|
||||||
text-align: center;
|
|
||||||
transition: 0.3s;
|
|
||||||
border-bottom: 5px solid rgb(255, 0, 0);
|
|
||||||
border-radius: 2rem;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: -1rem;
|
|
||||||
text-wrap: nowrap;
|
|
||||||
color: white;
|
|
||||||
font-weight: 100;
|
|
||||||
font-size: smaller;
|
|
||||||
text-align: center;
|
|
||||||
opacity: 0;
|
|
||||||
text-shadow: 0px 0px 5px black;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover::after {
|
|
||||||
opacity: 1;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
&.human,
|
|
||||||
&.adapted {
|
|
||||||
border-top: 5px solid rgb(255, 217, 0);
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.rerollable {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: translateY(0.5rem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.success {
|
|
||||||
border-bottom: 5px solid rgb(0, 143, 7);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.adapted {
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "adapté"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
&.human {
|
|
||||||
&::after {
|
|
||||||
|
|
||||||
content: "humain"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.rerolled {
|
|
||||||
transform: translateY(-0rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
text-align: center;
|
|
||||||
font-size: larger;
|
|
||||||
text-shadow: 0px 0px 8px black;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
.app .actor.choose {
|
|
||||||
div.actor {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
img {
|
|
||||||
border-radius: 50%;
|
|
||||||
box-shadow: 0px 0px 8px black;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
span.actor-name {
|
|
||||||
position: absolute;
|
|
||||||
text-align: center;
|
|
||||||
background-color: rgba(255, 255, 255, 0.562);
|
|
||||||
border: 5px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0 1rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
box-shadow: 0px 0px 8px black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
input[type="range"] {
|
|
||||||
appearance: none;
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="range"]::-webkit-slider-runnable-track {
|
|
||||||
background: url(../assets/images/ui/scotch.webp) no-repeat center;
|
|
||||||
background-size: 100% auto;
|
|
||||||
height: 0.4rem;
|
|
||||||
border: none;
|
|
||||||
box-shadow: 0px 0px 13px rgba(31, 26, 26, 0.979) inset;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="range"]::-webkit-slider-thumb {
|
|
||||||
|
|
||||||
appearance: none;
|
|
||||||
margin-top: -0.3rem;
|
|
||||||
/* Centers thumb on the track */
|
|
||||||
height: 1rem;
|
|
||||||
width: 1rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: url(/systems/vermine2047/assets/images/ui/totems/human.webp);
|
|
||||||
background-size: cover;
|
|
||||||
filter: contrast(2);
|
|
||||||
box-shadow: 0px 0px 10px black
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="range"]:focus::-webkit-slider-thumb {
|
|
||||||
box-shadow: 0px 0px 10px yellow
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
border: none;
|
|
||||||
background: url(../assets/images/ui/scotch.webp);
|
|
||||||
background-size: 100% 100%;
|
|
||||||
box-shadow: 0px 0px 3px rgba(31, 26, 26, 0.979) inset;
|
|
||||||
|
|
||||||
&[disabled] {
|
|
||||||
color: rgb(0, 0, 0);
|
|
||||||
text-shadow: 0px 0px 15px black;
|
|
||||||
}
|
|
||||||
|
|
||||||
option {
|
|
||||||
appearance: none;
|
|
||||||
border: none;
|
|
||||||
background: url(../assets/images/ui/scotch.webp);
|
|
||||||
background-size: 100% 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"],
|
|
||||||
input[type="radio"] {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
background: transparent;
|
|
||||||
box-shadow: 0px 0px 3px rgb(133, 133, 78);
|
|
||||||
cursor: pointer;
|
|
||||||
width: 1.5rem;
|
|
||||||
height: 1rem;
|
|
||||||
border-radius: 0.4rem;
|
|
||||||
transition: 0.3s;
|
|
||||||
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
|
|
||||||
box-shadow: 0px 0px 6px black inset;
|
|
||||||
background-color: rgba(61, 11, 11, 0.658);
|
|
||||||
|
|
||||||
&[disabled="true"] {
|
|
||||||
filter: grayscale(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
content: " ";
|
|
||||||
background: url(/systems/vermine2047/assets/images/ui/totems/human.webp);
|
|
||||||
background-size: 50% 150%;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
position: relative;
|
|
||||||
top: 10%;
|
|
||||||
left: 0%;
|
|
||||||
width: 100%;
|
|
||||||
height: 80%;
|
|
||||||
display: block;
|
|
||||||
border-radius: 0%;
|
|
||||||
padding: 0;
|
|
||||||
transition: 0.3s;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
&:checked {
|
|
||||||
background-color: rgba(26, 107, 12, 0.658);
|
|
||||||
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
font-weight: 900;
|
|
||||||
background-color: rgba(26, 1, 1, 0);
|
|
||||||
left: 50%;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
iframe {
|
|
||||||
min-height: 500px;
|
|
||||||
|
|
||||||
.tabs.moods-headings {
|
|
||||||
max-width: 1px
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="radio"] {
|
|
||||||
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
width: 0.8rem;
|
|
||||||
background-size: 100% 100%;
|
|
||||||
top: 5%;
|
|
||||||
left: 5%;
|
|
||||||
width: 90%;
|
|
||||||
height: 90%;
|
|
||||||
background-size: 30% 30%;
|
|
||||||
background-position: center;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not([disabled]):hover::after {
|
|
||||||
background-size: 90% 90%;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
&:checked::after {
|
|
||||||
content: "";
|
|
||||||
background-size: 70% 70%;
|
|
||||||
top: 5%;
|
|
||||||
left: 5%;
|
|
||||||
position: relative;
|
|
||||||
background-color: rgba(26, 1, 1, 0);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-546
@@ -1,546 +0,0 @@
|
|||||||
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap");
|
|
||||||
|
|
||||||
/* Global styles */
|
|
||||||
.window-app {
|
|
||||||
font-family: "Roboto", sans-serif;
|
|
||||||
box-shadow: 0px 0px 30px rgb(69, 78, 44);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.rollable:hover,
|
|
||||||
.rollable:focus {
|
|
||||||
color: #000;
|
|
||||||
text-shadow: 0 0 10px red;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid,
|
|
||||||
.grid-2col {
|
|
||||||
display: grid;
|
|
||||||
grid-column: span 2/span 2;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
margin: 5px 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-3col {
|
|
||||||
grid-column: span 3/span 3;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-4col {
|
|
||||||
grid-column: span 4/span 4;
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-5col {
|
|
||||||
grid-column: span 5/span 5;
|
|
||||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-6col {
|
|
||||||
grid-column: span 6/span 6;
|
|
||||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-7col {
|
|
||||||
grid-column: span 7/span 7;
|
|
||||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-8col {
|
|
||||||
grid-column: span 8/span 8;
|
|
||||||
grid-template-columns: repeat(8, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-9col {
|
|
||||||
grid-column: span 9/span 9;
|
|
||||||
grid-template-columns: repeat(9, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-10col {
|
|
||||||
grid-column: span 10/span 10;
|
|
||||||
grid-template-columns: repeat(10, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-11col {
|
|
||||||
grid-column: span 11/span 11;
|
|
||||||
grid-template-columns: repeat(11, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-12col {
|
|
||||||
grid-column: span 12/span 12;
|
|
||||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-start-2 {
|
|
||||||
grid-column-start: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-start-3 {
|
|
||||||
grid-column-start: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-start-4 {
|
|
||||||
grid-column-start: 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-start-5 {
|
|
||||||
grid-column-start: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-start-6 {
|
|
||||||
grid-column-start: 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-start-7 {
|
|
||||||
grid-column-start: 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-start-8 {
|
|
||||||
grid-column-start: 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-start-9 {
|
|
||||||
grid-column-start: 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-start-10 {
|
|
||||||
grid-column-start: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-start-11 {
|
|
||||||
grid-column-start: 11;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-start-12 {
|
|
||||||
grid-column-start: 12;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-span-2 {
|
|
||||||
grid-column-end: span 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-span-3 {
|
|
||||||
grid-column-end: span 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-span-4 {
|
|
||||||
grid-column-end: span 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-span-5 {
|
|
||||||
grid-column-end: span 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-span-6 {
|
|
||||||
grid-column-end: span 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-span-7 {
|
|
||||||
grid-column-end: span 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-span-8 {
|
|
||||||
grid-column-end: span 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-span-9 {
|
|
||||||
grid-column-end: span 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-span-10 {
|
|
||||||
grid-column-end: span 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-span-11 {
|
|
||||||
grid-column-end: span 11;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-span-12 {
|
|
||||||
grid-column-end: span 12;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-group-center,
|
|
||||||
.flex-group-left,
|
|
||||||
.flex-group-right {
|
|
||||||
-webkit-box-pack: center;
|
|
||||||
-ms-flex-pack: center;
|
|
||||||
justify-content: center;
|
|
||||||
-webkit-box-align: center;
|
|
||||||
-ms-flex-align: center;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-group-left {
|
|
||||||
-webkit-box-pack: start;
|
|
||||||
-ms-flex-pack: start;
|
|
||||||
justify-content: flex-start;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-group-right {
|
|
||||||
-webkit-box-pack: end;
|
|
||||||
-ms-flex-pack: end;
|
|
||||||
justify-content: flex-end;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-align-left {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-align-right {
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap-xs {
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap-sm {
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap-md {
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap-lg {
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flexshrink {
|
|
||||||
-webkit-box-flex: 0;
|
|
||||||
-ms-flex: 0;
|
|
||||||
flex: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-between {
|
|
||||||
-webkit-box-pack: justify;
|
|
||||||
-ms-flex-pack: justify;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flexlarge {
|
|
||||||
-webkit-box-flex: 2;
|
|
||||||
-ms-flex: 2;
|
|
||||||
flex: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-left {
|
|
||||||
-webkit-box-pack: start;
|
|
||||||
-ms-flex-pack: start;
|
|
||||||
justify-content: flex-start;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-right {
|
|
||||||
-webkit-box-pack: end;
|
|
||||||
-ms-flex-pack: end;
|
|
||||||
justify-content: flex-end;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-center {
|
|
||||||
-webkit-box-pack: center;
|
|
||||||
-ms-flex-pack: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .item-form {
|
|
||||||
font-family: "Roboto", sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet-header {
|
|
||||||
-webkit-box-flex: 0;
|
|
||||||
-ms-flex: 0 auto;
|
|
||||||
flex: 0 auto;
|
|
||||||
overflow: hidden;
|
|
||||||
display: -webkit-box;
|
|
||||||
display: -ms-flexbox;
|
|
||||||
display: flex;
|
|
||||||
-webkit-box-orient: horizontal;
|
|
||||||
-webkit-box-direction: normal;
|
|
||||||
-ms-flex-direction: row;
|
|
||||||
flex-direction: row;
|
|
||||||
-ms-flex-wrap: wrap;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
-webkit-box-pack: start;
|
|
||||||
-ms-flex-pack: start;
|
|
||||||
justify-content: flex-start;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet-header .profile-img {
|
|
||||||
-webkit-box-flex: 0;
|
|
||||||
-ms-flex: 0 0 100px;
|
|
||||||
flex: 0 0 100px;
|
|
||||||
height: 100px;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet-header .header-fields {
|
|
||||||
-webkit-box-flex: 1;
|
|
||||||
-ms-flex: 1;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet-header h1.charname {
|
|
||||||
height: 50px;
|
|
||||||
padding: 0px;
|
|
||||||
margin: 5px 0;
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet-header h1.charname input {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet-tabs {
|
|
||||||
-webkit-box-flex: 0;
|
|
||||||
-ms-flex: 0;
|
|
||||||
flex: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .sheet-body .tab,
|
|
||||||
.editor {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor {
|
|
||||||
min-height: 75px;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
min-width: 100%;
|
|
||||||
|
|
||||||
|
|
||||||
.editor-content {
|
|
||||||
min-width: 100%;
|
|
||||||
min-height: 3rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
editor:hover .editor-edit {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .tox {
|
|
||||||
min-height: 25vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .tox .tox-editor-container {
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .tox .tox-edit-area {
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .resource-label {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .items-header {
|
|
||||||
height: 28px;
|
|
||||||
margin: 2px 0;
|
|
||||||
padding: 0;
|
|
||||||
-webkit-box-align: center;
|
|
||||||
-ms-flex-align: center;
|
|
||||||
align-items: center;
|
|
||||||
background: rgba(0, 0, 0, 0.05);
|
|
||||||
border: 2px groove #eeede0;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .items-header>* {
|
|
||||||
font-size: 14px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .items-header .item-name {
|
|
||||||
font-weight: bold;
|
|
||||||
padding-left: 5px;
|
|
||||||
text-align: left;
|
|
||||||
display: -webkit-box;
|
|
||||||
display: -ms-flexbox;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .items-list {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .items-list .item-list {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .items-list .item-name {
|
|
||||||
-webkit-box-flex: 2;
|
|
||||||
-ms-flex: 2;
|
|
||||||
flex: 2;
|
|
||||||
margin: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 13px;
|
|
||||||
text-align: left;
|
|
||||||
-webkit-box-align: center;
|
|
||||||
-ms-flex-align: center;
|
|
||||||
align-items: center;
|
|
||||||
display: -webkit-box;
|
|
||||||
display: -ms-flexbox;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .items-list .item-name h3,
|
|
||||||
.system-vermine2047 .items-list .item-name h4 {
|
|
||||||
margin: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .items-list .item-controls {
|
|
||||||
display: -webkit-box;
|
|
||||||
display: -ms-flexbox;
|
|
||||||
display: flex;
|
|
||||||
-webkit-box-flex: 0;
|
|
||||||
-webkit-box-pack: end;
|
|
||||||
-ms-flex-pack: end;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .items-list .item-controls a {
|
|
||||||
font-size: 12px;
|
|
||||||
text-align: center;
|
|
||||||
margin: 0 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .items-list .item {
|
|
||||||
-webkit-box-align: center;
|
|
||||||
-ms-flex-align: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 2px;
|
|
||||||
border-bottom: 1px solid #c9c7b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .items-list .item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .items-list .item .item-name {
|
|
||||||
color: #191813;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .items-list .item .item-name .item-image {
|
|
||||||
-webkit-box-flex: 0;
|
|
||||||
-ms-flex: 0 0 30px;
|
|
||||||
flex: 0 0 30px;
|
|
||||||
height: 30px;
|
|
||||||
background-size: 30px;
|
|
||||||
border: none;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .items-list .item-prop {
|
|
||||||
text-align: center;
|
|
||||||
border-left: 1px solid #c9c7b8;
|
|
||||||
border-right: 1px solid #c9c7b8;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .items-list .items-header {
|
|
||||||
height: 28px;
|
|
||||||
margin: 2px 0;
|
|
||||||
padding: 0;
|
|
||||||
-webkit-box-align: center;
|
|
||||||
-ms-flex-align: center;
|
|
||||||
align-items: center;
|
|
||||||
background: rgba(0, 0, 0, 0.05);
|
|
||||||
border: 2px groove #eeede0;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .items-list .items-header>* {
|
|
||||||
font-size: 12px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .items-list .items-header .item-name {
|
|
||||||
padding-left: 5px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .item-formula {
|
|
||||||
-webkit-box-flex: 0;
|
|
||||||
-ms-flex: 0 0 200px;
|
|
||||||
flex: 0 0 200px;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .effects .item .effect-source,
|
|
||||||
.system-vermine2047 .effects .item .effect-duration,
|
|
||||||
.system-vermine2047 .effects .item .effect-controls {
|
|
||||||
text-align: center;
|
|
||||||
border-left: 1px solid #c9c7b8;
|
|
||||||
border-right: 1px solid #c9c7b8;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-vermine2047 .effects .item .effect-controls {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message .message-header {
|
|
||||||
line-height: 20px;
|
|
||||||
color: white;
|
|
||||||
text-shadow: 0px 0px 5px black;
|
|
||||||
background: #1918135e
|
|
||||||
}
|
|
||||||
|
|
||||||
span.game-mode {
|
|
||||||
font-family: "DistressBlack",
|
|
||||||
sans-serif;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
margin-left: auto;
|
|
||||||
color: transparent;
|
|
||||||
top: 1rem;
|
|
||||||
z-index: 900;
|
|
||||||
width: 55%;
|
|
||||||
text-align: center;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: 900;
|
|
||||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.767) 0%, rgba(0, 0, 0, 0.61) 17%, rgba(0, 0, 0, 0.548) 19%, rgba(222, 255, 221, 0.575) 24%, rgba(255, 255, 255, 0.637) 43%, rgba(0, 0, 0, 0.486) 47%, rgba(254, 255, 254, 0.466) 50%, rgba(0, 0, 0, 0.699) 63%, rgba(134, 160, 137, 0.479) 64%, rgba(213, 248, 210, 0.493) 100%);
|
|
||||||
background-clip: text;
|
|
||||||
|
|
||||||
&#game-mode-1 {
|
|
||||||
color: rgba(235, 218, 143, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
&#game-mode-2 {
|
|
||||||
color: rgb(131, 248, 131);
|
|
||||||
}
|
|
||||||
|
|
||||||
&#game-mode-3 {
|
|
||||||
color: rgba(245, 124, 124, 0.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadow {
|
|
||||||
box-shadow: 0 0.5rem 2rem black;
|
|
||||||
margin: 2rem 0
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
@import "./base_work.scss";
|
|
||||||
@import "./style.scss";
|
|
||||||
@import "./roll.scss";
|
|
||||||
@import "./itemCards.scss";
|
|
||||||
@import "item-sheet.scss";
|
|
||||||
@import "dialog.scss";
|
|
||||||
@import "special-inputs.scss";
|
|
||||||
@import "special-applications.scss";
|
|
||||||
@import "_flex.scss";
|
|
||||||
@import "_app.scss";
|
|
||||||
|
|
||||||
|
|
||||||
// overwrites variables
|
|
||||||
:root {
|
|
||||||
--color-text-light-highlight: #96d696;
|
|
||||||
--color-text-light-heading: #9fd8a8;
|
|
||||||
--color-text-light-primary: #a4b5b3;
|
|
||||||
--color-text-dark-primary: #131919;
|
|
||||||
--color-text-dark-secondary: #444b4a;
|
|
||||||
--color-text-dark-header: #1d2223;
|
|
||||||
--color-text-dark-inactive: #71797a;
|
|
||||||
--color-text-hyperlink: #5aaf0a;
|
|
||||||
--color-text-light-0: #fff;
|
|
||||||
--color-text-light-1: #e0f0f0;
|
|
||||||
--color-text-light-2: #c9e0c0;
|
|
||||||
--color-text-light-3: #90c4a4;
|
|
||||||
--color-text-light-4: #80c08b;
|
|
||||||
--color-text-light-5: #60b06b;
|
|
||||||
--color-text-light-6: #40a05d;
|
|
||||||
--color-text-light-7: #208028;
|
|
||||||
--color-text-dark-1: #111;
|
|
||||||
--color-text-dark-2: #222;
|
|
||||||
--color-text-dark-3: #444;
|
|
||||||
--color-text-dark-4: #555;
|
|
||||||
--color-text-dark-5: #666;
|
|
||||||
--color-text-dark-6: #777;
|
|
||||||
--color-border-light-1: #b0d9b0;
|
|
||||||
--color-border-light-2: #80c0c0;
|
|
||||||
--color-border-dark-1: #131919;
|
|
||||||
--color-border-dark-2: #1d2223;
|
|
||||||
--color-border-dark-3: #2d3333;
|
|
||||||
--color-border-dark-4: #3d4444;
|
|
||||||
--color-border-dark-5: #668888;
|
|
||||||
--color-shadow-primary: #7bb60d;
|
|
||||||
--color-shadow-highlight: #85cc01d0;
|
|
||||||
--color-shadow-dark: #000;
|
|
||||||
--color-underline-inactive: #71797a;
|
|
||||||
--color-underline-active: #1a1944;
|
|
||||||
--color-underline-header: #228247;
|
|
||||||
--color-border-light-highlight: #b0d9b0;
|
|
||||||
--color-border-light-primary: #a4b5b3;
|
|
||||||
--color-border-light-secondary: #9fc7d8;
|
|
||||||
--color-border-light-tertiary: #71797a;
|
|
||||||
--color-border-dark: #000;
|
|
||||||
--color-border-dark-primary: #131919;
|
|
||||||
--color-border-dark-secondary: #1d2223;
|
|
||||||
--color-border-dark-tertiary: #444b4a;
|
|
||||||
--color-border-highlight: #85c019;
|
|
||||||
--color-border-highlight-alt: #70c008;
|
|
||||||
--color-bg-btn-minor-inactive: #9fc7d8;
|
|
||||||
--color-bg-btn-minor-active: #a4b5b3;
|
|
||||||
--color-bg-option: #ccdada;
|
|
||||||
--color-checkbox-checked: #666;
|
|
||||||
--color-ownership-none: #00ff55;
|
|
||||||
--color-ownership-observer: #71797a;
|
|
||||||
--color-ownership-owner: #a4b5b3;
|
|
||||||
--z-index-canvas: 0;
|
|
||||||
--z-index-app: 30;
|
|
||||||
--z-index-ui: 60;
|
|
||||||
--z-index-window: 100;
|
|
||||||
--z-index-tooltip: 9999;
|
|
||||||
--sidebar-width: 300px;
|
|
||||||
--sidebar-header-height: 32px;
|
|
||||||
--sidebar-item-height: 48px;
|
|
||||||
--hotbar-height: 52px;
|
|
||||||
--hotbar-width: 578px;
|
|
||||||
--macro-size: 50px;
|
|
||||||
--players-width: 200px;
|
|
||||||
--form-field-height: 26px;
|
|
||||||
--font-primary: "Signika", sans-serif;
|
|
||||||
--font-mono: monospace;
|
|
||||||
--font-awesome: "Font Awesome 6 Pro";
|
|
||||||
--font-size-11: 0.6875rem;
|
|
||||||
--font-size-12: 0.75rem;
|
|
||||||
--font-size-13: 0.8125rem;
|
|
||||||
--font-size-14: 0.875rem;
|
|
||||||
--font-size-16: 1rem;
|
|
||||||
--font-size-18: 1.125rem;
|
|
||||||
--font-size-20: 1.25rem;
|
|
||||||
--font-size-24: 1.5rem;
|
|
||||||
--font-size-28: 1.75rem;
|
|
||||||
--font-size-32: 2rem;
|
|
||||||
--font-size-48: 3rem;
|
|
||||||
--line-height-12: 0.75rem;
|
|
||||||
--line-height-16: 1rem;
|
|
||||||
--line-height-20: 1.25rem;
|
|
||||||
--line-height-30: 1.875rem;
|
|
||||||
--color-level-info: #b95c87;
|
|
||||||
--color-level-warning: #04b184;
|
|
||||||
--color-level-error: #03750;
|
|
||||||
--color-level-success: #3c266c;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ----------------------------------------- */
|
|
||||||
/* Scrollbar
|
|
||||||
/* ----------------------------------------- */
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
outline: none;
|
|
||||||
border-radius: 3px;
|
|
||||||
background: #577822;
|
|
||||||
border: 1px solid var(--color-border-highlight);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 3px;
|
|
||||||
height: 3px;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Acuité auditive
|
||||||
|
type: evolution
|
||||||
|
_id: vPA7gekUx8kUNOWB
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage est doté d’une ouïe extraordinaire. Il perçoit des ondes
|
||||||
|
sonores très aigües, ce qui lui permet d’entendre les ultrasons. Son acuité
|
||||||
|
est aussi surdéveloppée. Il peut ainsi entendre un son à une distance cinq
|
||||||
|
fois supérieure aux autres personnages. Enfin, sa sensibilité lui permet de
|
||||||
|
distinguer les sons entre eux, et ainsi d’entendre distinctement un son qui
|
||||||
|
serait couvert par le vent, par exemple. Son ouïe extraordinaire lui offre
|
||||||
|
un Bonus de 3D à tous les jets de Perception basés sur l’audition.</p>
|
||||||
|
level:
|
||||||
|
value: 3
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198965
|
||||||
|
modifiedTime: 1785775198965
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!vPA7gekUx8kUNOWB'
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Adrénaline
|
||||||
|
type: evolution
|
||||||
|
_id: DItkLLGSyjAGzruO
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le corps du personnage s’est adapté au stress du combat. Quand le
|
||||||
|
personnage subit des Blessures lors d’un combat, il ne subit pas la perte de
|
||||||
|
Dés de Réserve associée. En revanche, à la fin du combat, il perd 2D
|
||||||
|
d’Effort et 1D de Sang-Froid pour chaque Blessure subie.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198966
|
||||||
|
modifiedTime: 1785775198966
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!DItkLLGSyjAGzruO'
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Analgésie
|
||||||
|
type: evolution
|
||||||
|
_id: Cs6ibCrcPxim7QHR
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Pour le personnage, la douleur n’est qu’une information dont il ne subit
|
||||||
|
pas les conséquences néfastes. Lorsqu’il subit une Blessure, il ne perd
|
||||||
|
aucun Dé de Sang- Froid. En outre, le Malus qu’il subit à la suite de
|
||||||
|
Blessures est diminué de 1D.</p>
|
||||||
|
level:
|
||||||
|
value: 3
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198966
|
||||||
|
modifiedTime: 1785775198966
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!Cs6ibCrcPxim7QHR'
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Anticipation
|
||||||
|
type: evolution
|
||||||
|
_id: pP9hfvk5JKsoFcnt
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>En décryptant le langage corporel – ou peut-être en percevant les
|
||||||
|
phéromones – de son adversaire, le personnage ressent ses intentions. Il
|
||||||
|
peut utiliser la caractéristique Empathie plutôt que Réflexes pour esquiver
|
||||||
|
les attaques d’un adversaire humain. S’il réussit une telle esquive, il
|
||||||
|
regagne 1D dans sa Réserve de Sang-Froid.</p>
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198967
|
||||||
|
modifiedTime: 1785775198967
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!pP9hfvk5JKsoFcnt'
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Au bout de l’effort
|
||||||
|
type: evolution
|
||||||
|
_id: h3Q6LwnnVuj6MH4Y
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage peut dépasser ses limites. Il peut dépenser, sur un jet, un
|
||||||
|
nombre de Dés de Réserve égal à la Caractéristique utilisée +1 (contre un
|
||||||
|
seul normalement).</p>
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198967
|
||||||
|
modifiedTime: 1785775198967
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!h3Q6LwnnVuj6MH4Y'
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Aura d’optimisme
|
||||||
|
type: evolution
|
||||||
|
_id: JrtHAUqFgA75e6pq
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage dégage une imperceptible aura bienfaisante autour de lui.
|
||||||
|
La présence réconfortante du personnage réduit de 2D les pertes de Dés de la
|
||||||
|
Réserve de Groupe à la suite d’un événement traumatisant. Le personnage doit
|
||||||
|
être témoin de l’événement pour que sa Capacité fonctionne.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198967
|
||||||
|
modifiedTime: 1785775198967
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!JrtHAUqFgA75e6pq'
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Camouflage
|
||||||
|
type: evolution
|
||||||
|
_id: t4p4n73kwwVoeSMe
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage ne dégage aucune odeur et émet des phéromones qui le
|
||||||
|
rendent insignifiant aux yeux des créatures qui l’entourent. Il peut ajouter
|
||||||
|
son score en Volonté aux jets destinés à camoufler sa présence, son odeur,
|
||||||
|
ses traces ou un objet lui appartenant. S’il camoufle sa présence et qu’il
|
||||||
|
reste immobile, il bénéficie en outre de trois Réussites supplémentaires.
|
||||||
|
Dès qu’il se déplace, il doit retenter le jet sans pouvoir bénéficier des
|
||||||
|
Réussites supplémentaires.</p>
|
||||||
|
level:
|
||||||
|
value: 3
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198967
|
||||||
|
modifiedTime: 1785775198967
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!t4p4n73kwwVoeSMe'
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Cicatrisation rapide
|
||||||
|
type: evolution
|
||||||
|
_id: 2h0xUozy1xqLdKcE
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>La Difficulté des Actions de soin sur le personnage est réduite de 2. Le
|
||||||
|
coût en Dés de Réserves pour guérir est diminué de 1D pour chaque type de
|
||||||
|
Blessure.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198968
|
||||||
|
modifiedTime: 1785775198968
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!2h0xUozy1xqLdKcE'
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Défenses immunitaires
|
||||||
|
type: evolution
|
||||||
|
_id: oNfjRGGRVFGtfbK0
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>La Virulence d’une infection affectant le personnage est réduite de 1 –
|
||||||
|
voir Les Règles, « Maladies et infections », page 105. Lorsque le personnage
|
||||||
|
reçoit des soins pour soigner une infection, le soigneur reçoit une Réussite
|
||||||
|
supplémentaire en cas de jet réussi.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198968
|
||||||
|
modifiedTime: 1785775198968
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!oNfjRGGRVFGtfbK0'
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Dur au mal
|
||||||
|
type: evolution
|
||||||
|
_id: vgYLoiEqEOxrGdy6
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: <p>Le Seuil de Blessure Légère du personnage augmente de 1.</p>
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198968
|
||||||
|
modifiedTime: 1785775198968
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!vgYLoiEqEOxrGdy6'
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Endurance
|
||||||
|
type: evolution
|
||||||
|
_id: uGG0FrBn1QtMcxbz
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage a toujours été particulièrement résistant. Toutes les
|
||||||
|
pertes de Réserve d’Effort qu’il subit sont réduites de 1D, jusqu’à un
|
||||||
|
minimum de 1D.</p>
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198968
|
||||||
|
modifiedTime: 1785775198968
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!uGG0FrBn1QtMcxbz'
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Exosquelette
|
||||||
|
type: evolution
|
||||||
|
_id: bV6V4i1v4ORWDJSQ
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage a développé un exosquelette fait de chitine. De nombreux
|
||||||
|
endroits de son corps et de son visage sont recouverts de plaques rigides
|
||||||
|
qui lui procurent une Protection de 3/5 (L) sans lui infliger de Handicap de
|
||||||
|
Mobilité – voir Les Règles, « Handicap de Mobilité », page 70. Il bénéficie
|
||||||
|
en outre d’un Bonus de 2D aux Actions dont le but est d’intimider ou
|
||||||
|
d’effrayer. En revanche, il subit un Malus identique pour toutes les autres
|
||||||
|
Actions sociales.</p>
|
||||||
|
level:
|
||||||
|
value: 4
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198968
|
||||||
|
modifiedTime: 1785775198968
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!bV6V4i1v4ORWDJSQ'
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Force mentale
|
||||||
|
type: evolution
|
||||||
|
_id: S8AwTKDxIdez7vHB
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage a des ressources mentales insoupçonnées. Il ne subit pas de
|
||||||
|
Malus d’Épuisement aux Actions mentales et sociales quand sa Réserve de
|
||||||
|
Sang-Froid est vide. Si ses deux Réserves sont vides, il subit néanmoins le
|
||||||
|
Malus normal de 2D à toutes ses Actions.</p>
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198969
|
||||||
|
modifiedTime: 1785775198969
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!S8AwTKDxIdez7vHB'
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Frappe lourde
|
||||||
|
type: evolution
|
||||||
|
_id: RjdZB4Qd2V8mp44x
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Lorsque le personnage attaque à mains nues, ses dommages sont égaux à
|
||||||
|
Vigueur (C).</p>
|
||||||
|
level:
|
||||||
|
value: 1
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198969
|
||||||
|
modifiedTime: 1785775198969
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!RjdZB4Qd2V8mp44x'
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Griffes
|
||||||
|
type: evolution
|
||||||
|
_id: kPoioCvlK7H0uJB1
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Les ongles du personnage peuvent pousser en un instant pour devenir des
|
||||||
|
armes dangereuses. Ces griffes infligent des Dommages de 2 (L) et
|
||||||
|
bénéficient du Trait Rapide (2). Quand le personnage réussit une attaque et
|
||||||
|
passe la protection de sa cible, il peut dépenser jusqu’à 3D de sa Réserve
|
||||||
|
d’Effort pour infliger 1 point de Dommages supplémentaire par dé dépensé.
|
||||||
|
Ces griffes se brisent à la fin du combat et ne peuvent repousser que
|
||||||
|
lorsque le personnage aura intégralement reconstitué sa Réserve
|
||||||
|
d’Effort.</p>
|
||||||
|
level:
|
||||||
|
value: 3
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198969
|
||||||
|
modifiedTime: 1785775198969
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!kPoioCvlK7H0uJB1'
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Habitué à la faim
|
||||||
|
type: evolution
|
||||||
|
_id: EqeONvUCK7OhJaNN
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le premier jour de rationnement ou de privation totale de nourriture n’a
|
||||||
|
pas d’effet sur le personnage. Ainsi, il ne subira aucune perte de Dés de
|
||||||
|
Réserves avant le deuxième jour de privation totale ou le troisième jour de
|
||||||
|
rationnement.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198969
|
||||||
|
modifiedTime: 1785775198969
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!EqeONvUCK7OhJaNN'
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Hyperosmie
|
||||||
|
type: evolution
|
||||||
|
_id: 8ShwJQCWKHsfXLko
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage est doté d’un odorat extrêmement développé, proche de celui
|
||||||
|
d’un chien. Cette acuité lui permet de distinguer des odeurs imperceptibles
|
||||||
|
pour ses semblables. Il peut ainsi reconnaître une personne ou un animal à
|
||||||
|
son odeur et savoir si cet individu est passé dans un endroit jusqu’à une
|
||||||
|
semaine après son passage. Cela lui offre un talent incroyable pour suivre
|
||||||
|
une piste et traquer une proie : il bénéficie d’un Bonus de 3D aux jets de
|
||||||
|
Perception quand l’odorat entre en jeu. Cette Mutation s’accompagne d’une
|
||||||
|
mémoire olfactive très développée. Une fois qu’il a senti une odeur, le
|
||||||
|
personnage ne l’oublie jamais et peut la distinguer et la reconnaître sans
|
||||||
|
faillir.</p>
|
||||||
|
level:
|
||||||
|
value: 3
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198969
|
||||||
|
modifiedTime: 1785775198969
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!8ShwJQCWKHsfXLko'
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Immunité absolue
|
||||||
|
type: evolution
|
||||||
|
_id: ivx8gS0XBEcoqtdl
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage est immunisé à toutes les Infections et toutes les Toxines.
|
||||||
|
Il ne peut par ailleurs pas développer d’addiction. En contrepartie, les
|
||||||
|
médicaments et autres stimulants sont totalement inefficaces sur lui et
|
||||||
|
n’octroient aucun bénéfice que ce soit.</p>
|
||||||
|
level:
|
||||||
|
value: 4
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198970
|
||||||
|
modifiedTime: 1785775198970
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!ivx8gS0XBEcoqtdl'
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Indestructible
|
||||||
|
type: evolution
|
||||||
|
_id: HfC2oOARrpKnpoQf
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le corps du personnage a gagné en densité, mais son poids augmente de 25
|
||||||
|
% et ses besoins nutritionnels explosent. Il gagne un cercle dans chaque
|
||||||
|
type de Blessure, subit (I) Handicap permanent de Mobilité, et a besoin de
|
||||||
|
deux rations de nourriture plutôt qu’une. En cas de rationnement de
|
||||||
|
nourriture, il subit des pertes de Réserves équivalentes à une absence
|
||||||
|
totale de nourriture.</p>
|
||||||
|
level:
|
||||||
|
value: 4
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198970
|
||||||
|
modifiedTime: 1785775198970
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!HfC2oOARrpKnpoQf'
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Insensible
|
||||||
|
type: evolution
|
||||||
|
_id: Re0aeZzupnz3EL6E
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le corps du personnage sécrète de grandes quantités d’hormones en cas de
|
||||||
|
stress intense, ce qui lui permet d’y résister plus facilement. L’Intensité
|
||||||
|
des événements stressants est diminuée d’un niveau pour le personnage. Il
|
||||||
|
n’a même pas à tenter de jet pour résister à un événement Mineur – voir Les
|
||||||
|
Règles, « Événements », page 92.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198970
|
||||||
|
modifiedTime: 1785775198970
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!Re0aeZzupnz3EL6E'
|
||||||
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Instinct
|
||||||
|
type: evolution
|
||||||
|
_id: UxNguZd8K62nt1GH
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Un jet d’Empathie + Vigilance contre une Difficulté de 9 permet au
|
||||||
|
personnage de pressentir un danger ou une menace imminente d’origine
|
||||||
|
naturelle ou animale. Chaque Réussite obtenue lui permet d’obtenir une
|
||||||
|
information sur le danger pressenti. Si celui-ci tourne au combat, le
|
||||||
|
personnage sera considéré comme Actif pour son jet de Réaction. S’il a le
|
||||||
|
temps d’informer ses compagnons du danger, ces derniers seront Actifs
|
||||||
|
également.</p>
|
||||||
|
|
||||||
|
<p>En Modes <strong>Cauchemar </strong>et <strong>Apocalypse</strong>,
|
||||||
|
chaque Réussite au jet de Vigilance octroie au personnage 1D de Bonus au jet
|
||||||
|
de Réaction ou au jet permettant d’éviter le danger perçu.</p>
|
||||||
|
level:
|
||||||
|
value: 2
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198970
|
||||||
|
modifiedTime: 1785775198970
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!UxNguZd8K62nt1GH'
|
||||||
|
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
folder: WTyHT2h0fsSnUHFT
|
||||||
|
name: Musculature surdéveloppée
|
||||||
|
type: evolution
|
||||||
|
_id: AAw7U5tUxbUczorz
|
||||||
|
img: systems/vermine2047/assets/icons/items/evolution.webp
|
||||||
|
system:
|
||||||
|
description: >-
|
||||||
|
<p>Le personnage est exceptionnellement fort, ce que reflète sa silhouette
|
||||||
|
impressionnante. Il bénéficie d’un Bonus de 3D aux jets de Vigueur quand une
|
||||||
|
force musculaire élevée peut l’aider (soulever ou porter un objet lourd,
|
||||||
|
enfoncer une porte, abattre un mur, etc.) Lorsqu’il porte une attaque de
|
||||||
|
corps à corps ou de mêlée, sa Vigueur est considérée comme augmentée de 3D
|
||||||
|
pour calculer les Dommages de base de ses attaques. En revanche, sa force
|
||||||
|
surhumaine n’augmente pas sa précision, il ne gagne donc aucun Bonus à
|
||||||
|
l’attaque.</p>
|
||||||
|
level:
|
||||||
|
value: 3
|
||||||
|
min: 1
|
||||||
|
max: 4
|
||||||
|
effects: []
|
||||||
|
sort: 0
|
||||||
|
ownership:
|
||||||
|
default: 0
|
||||||
|
GXSW26kNdNoaabl8: 3
|
||||||
|
kb05teJOwuTQVHwZ: 3
|
||||||
|
flags: {}
|
||||||
|
_stats:
|
||||||
|
compendiumSource: null
|
||||||
|
duplicateSource: null
|
||||||
|
coreVersion: '14.365'
|
||||||
|
systemId: vermine2047
|
||||||
|
systemVersion: 0.1.13
|
||||||
|
createdTime: 1785775198971
|
||||||
|
modifiedTime: 1785775198971
|
||||||
|
lastModifiedBy: kb05teJOwuTQVHwZ
|
||||||
|
exportSource: null
|
||||||
|
_key: '!items!AAw7U5tUxbUczorz'
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user