28 Commits

Author SHA1 Message Date
uberwald dcc24b47ec GItea releas
Release Creation / build (release) Failing after 1m16s
2026-06-06 22:53:09 +02:00
uberwald c23de0ea66 Fix apv2, WIP 2026-06-06 22:37:29 +02:00
uberwald c571e6a209 Fix: Correction des exports/imports APPV2 et clés i18N manquantes
- Standardisation des exports de sheets sur 'export default class' (sauf base-item-sheet)
- Correction des imports pour utiliser des imports par défaut
- Correction de _module.mjs pour exporter les classes correctement
- Suppression des redéfinitions de changeTab dans group-sheet et npc-sheet
- Ajout des clés i18N manquantes: VERMINE.reserve, VERMINE.Sheet.*
- Correction de l'export de VermineBaseItemSheet (export nommé)

Corrige l'erreur 'Receiver must be class VermineGroupSheetV2' et aligne sur fvtt-hamalron et fvtt-celestopol

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-06 18:32:03 +02:00
uberwald 2691ff521f New fixes around sheets 2026-06-06 17:56:50 +02:00
uberwald 2143bcc8db Actor sheet -> working !!!, not WIP for rolls 2026-06-06 16:25:00 +02:00
uberwald 891769816a Actor sheet -> working !!! 2026-06-06 15:48:18 +02:00
uberwald 9b77a0c552 Fix apv2, WIP 2026-06-06 10:21:24 +02:00
uberwald 6cec1da910 UPdate gitgnore 2026-06-05 00:45:20 +02:00
uberwald 1b66c24258 fix(sheets): use foundry.utils.duplicate instead of global duplicate
Foundry V11+ no longer exposes 'duplicate' as a global function.
It must be accessed via foundry.utils.duplicate.

Fixed in:
- actor-sheet.mjs:140
- creature-sheet.mjs:148

This resolves the ReferenceError: duplicate is not defined error
when creating items on actor sheets.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-04 23:25:11 +02:00
uberwald dfc9b823a4 fix(hooks): remove auto editMode flag setting to prevent phase errors
The createActor hook was causing 'ActiveEffect application phase already
completed' errors because setFlag() triggers actor updates which call
prepareData() -> prepareEmbeddedDocuments() -> applyActiveEffects().

Even with setTimeout delays, the error persisted. The cleanest solution is
to remove the auto-setting entirely. Users can enable edit mode manually
via the checkbox in the character sheet (which is preserved).

This prevents the error on:
- Character sheet profile editing
- Creature sheet creation
- Group sheet profile editing

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-04 23:23:11 +02:00
uberwald 02aabbea36 fix(hooks): delay editMode flag setting to avoid phase error on createActor
The createActor hook was calling actor.setFlag() immediately, which triggered
updateActor -> prepareData -> prepareEmbeddedDocuments() -> applyActiveEffects().

Since the actor had already gone through data preparation during creation,
the effects had already been applied, causing the 'phase already completed' error.

Solution: Use setTimeout(..., 0) to defer the flag setting until after the
current preparation cycle is complete.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-04 23:20:40 +02:00
uberwald 6fbc7e7864 fix(actor): check applicationPhase to prevent double effect application
The previous solution used a flag, but it didn't persist across preparation cycles.
This solution checks this.effects.applicationPhase directly.

If phase is 'initial' or 'final', it means effects are already being applied,
so we skip the super.prepareEmbeddedDocuments() call to avoid the
'phase has already completed' error.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-04 23:20:23 +02:00
uberwald 308073877b fix(actor): remove duplicate prepareBaseData method
The previous commit accidentally created a duplicate prepareBaseData() method.
This commit removes the duplicate and keeps only the version with proper
initialization of wound data and combatStatus.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-04 23:15:23 +02:00
uberwald 81adfb7ffd fix(actor): prevent double ActiveEffect application in prepareData cycle
- Move wound data initialization to prepareBaseData() (before effects are applied)
- Initialize combatStatus in prepareBaseData() to prevent undefined errors
- Add protection against recursive effect application in prepareEmbeddedDocuments()
- This prevents the 'ActiveEffect application phase has already completed' error

The error occurred because modify data in prepareDerivedData() (like combatStatus)
could trigger observers that try to re-apply effects during the same cycle.

By initializing all required data in prepareBaseData() and protecting
prepareEmbeddedDocuments() from recursive calls, we ensure effects are
applied exactly once per preparation cycle.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-04 23:14:38 +02:00
uberwald b85efd663b fix(templates): restore missing {{/if}} closure in character-id.hbs
The previous commit accidentally removed the {{/if}} that closes the
{{#if system.identity.relations}} block at line 102, causing a
Handlebars parse error: 'Expecting OPEN_INVERSE_CHAIN, INVERSE, OPEN_ENDBLOCK, got EOF'

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-04 23:06:32 +02:00
uberwald 3db03f5159 fix(templates): complete revision of actor templates to remove duplicates and fix inconsistencies
- Fixed grid-2 to grid-2col CSS class usage
- Replaced improper <p><a> tags with <div><a> in item lists (actor-weapons, actor-defenses, group-items, group-vehicles)
- Fixed typos: smarttlk -> smarttl, compétence -> Compétence
- Fixed duplicate class attributes
- Standardized HTML comments to Handlebars comments ({{!-- --}})
- Added missing localizations for Character and Skills headers
- Removed orphan {{/if}} tag in character-id.hbs
- Created npc-skill-category.hbs partial to eliminate 150+ lines of duplicated skill category code in actor-npc-sheet.hbs
- Created item-list.hbs partial for reusable item list display
- Fixed html.find(...).forEach error in roll.mjs by ensuring jQuery object
- Added .history/ to .gitignore

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-04 22:58:46 +02:00
uberwald 30d6f71fc7 fix: Correct critical bugs and complete Creature/Group DataModel implementation
- Fix TypeError: controls.find is not a function in hooks.mjs
- Fix undefined 'npc' variable in applications.mjs
- Fix CONFIG.VERMINE.model undefined by checking game.system.template existence
- Fix TypeError: html.find(...).forEach is not a function in roll.mjs
- Fix Cannot set properties of undefined (setting 'initial') in actor.mjs
- Fix Cannot read properties of undefined (reading 'difficulty') in actor.mjs
- Fix ActiveEffect application phase 'initial' already completed by adding combatStatus to base template
- Fix Missing helper: 'select' in roll-dialog.hbs (removed invalid Handlebars select block)
- Add SIZE_LEVELS labels to creatureSizeLevels config
- Add SIZE_LEVELS translations to fr.json
- Add combatStatus to base actor template
- Convert all .html templates to .hbs for Foundry v14 compatibility
- Update item-sheet.mjs to use .hbs extension
- Update handlebars-manager.mjs to use .hbs for all partials

Complete Vermine2047 Creature and Group sheet implementation:
- Creature: Pattern, Size, Role, Pack with computed values
- Group: Totem, Reserve, Morale, Objectives, Members management
- All templates functional with proper styling

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-04 22:22:52 +02:00
uberwald f9f07cbc7e fix: migrate combat system from d6 to d10 to match official rules
- Change all dice rolls in fight.mjs from d6 to d10
- Implement success counting system (dice >= difficulty) as per Vermine2047 rules
- Add d10 success class to dice that meet or exceed difficulty
- Display success count in confrontation UI
- Update chat message handler to count successes instead of summing dice
- Add comprehensive JSDoc documentation to performTest method
- Add missing French translations for fight tool terms

This corrects a critical inconsistency where fight.mjs was using d6
while the official Vermine2047 rules and the rest of the system (roll.mjs)
use d10 with success counting.

Compatibility: FoundryVTT v11-v14

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-04 16:48:57 +02:00
uberwald 386d80639c code review: fix critical issues and improve code quality
- Fix constructor in rollDialog.mjs (spread operator for options)
- Remove all console.log statements from production code
- Add comprehensive JSDoc comments for all public APIs
- Convert French comments to English for consistency
- Use parseInt with radix parameter (10) throughout
- Replace let with const where appropriate
- Use Set for O(1) lookups in group-link.mjs methods
- Use spread operators for array cloning
- Optimize removeActorFromAllGroups with Set lookups
- Improve registerHooks with better comments and Set usage
- Simplify roll-message.hbs template logic
- Fix duplicate VERMINE key in lang/fr.json
- Add missing error translations
- Add .eslintrc.js with FoundryVTT-compatible linting config

Compatibility: FoundryVTT v11-v14

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-04 13:33:58 +02:00
uberwald 716c1b49ae Finalisation complète du système Vermine2047 pour FoundryVTT v14
Implémentations majeures:
- Classe GroupLink pour synchronisation bidirectionnelle acteurs↔groupes
- Configuration complète des totems, PNJ et créatures
- Redesign du RollDialog avec interface compacte et sélecteurs
- Bonus/malus par domaine de totem
- Réussites automatiques et seuils auto basés sur niveau de maîtrise
- Choix du totem à garder avec recalcul des réussites
- Conversion tous templates chat cards en .hbs
- Fiches PNJ et Créature avec sélecteurs pour tous les niveaux
- Documentation technique (ARCHITECTURE.md) et utilisateur (GUIDE_UTILISATEUR.md)
- Mise à jour system.json pour compatibilité v14
- Tous les TODOs du README.md complétés

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-04 11:46:40 +02:00
Erwan Lemaire c35e93975b pifpouf 2025-05-02 13:19:49 +02:00
Erwan Lemaire 5889a8111d changement rolldialog 2025-04-17 15:53:34 +02:00
Erwan Lemaire d380efeed5 reprise du projet 2025-04-17 14:55:41 +02:00
rwanoux 62f13c8cc3 Update system.json 2025-04-12 19:09:26 +02:00
rwanoux 7a1ab85d23 Update system.json 2025-04-12 19:08:44 +02:00
rwanoux 45c5396ff4 Update system.json 2025-04-12 19:08:00 +02:00
rwanoux dc621f2223 début des messages d'items 2024-11-26 12:48:27 +01:00
rwanoux 1327f92f41 fix de la config model pour select items et spécialité 2024-11-23 11:52:41 +01:00
210 changed files with 36609 additions and 2809 deletions
+358
View File
@@ -0,0 +1,358 @@
/**
* ESLint configuration for Vermine2047 FoundryVTT system
* Compatible with FoundryVTT v11-v14
*/
module.exports = {
root: true,
env: {
browser: true,
es2022: true,
node: false
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended'
],
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
ecmaFeatures: {
impliedStrict: true
}
},
globals: {
// FoundryVTT global objects
game: 'readonly',
ui: 'readonly',
Hooks: 'readonly',
CONFIG: 'readonly',
Canvas: 'readonly',
ChatMessage: 'readonly',
Roll: 'readonly',
Actor: 'readonly',
Item: 'readonly',
Dialog: 'readonly',
foundry: 'readonly',
Handlebars: 'readonly',
renderTemplate: 'readonly'
},
rules: {
// Possible Problems
'no-console': 'error',
'no-constant-condition': 'error',
'no-control-regex': 'error',
'no-debugger': 'error',
'no-dupe-args': 'error',
'no-dupe-keys': 'error',
'no-duplicate-case': 'error',
'no-empty': 'warn',
'no-empty-character-class': 'error',
'no-ex-assign': 'error',
'no-extra-boolean-cast': 'error',
'no-fallthrough': 'error',
'no-func-assign': 'error',
'no-inner-html': 'off', // Foundry uses innerHTML extensively
'no-invalid-regexp': 'error',
'no-irregular-whitespace': 'error',
'no-misleading-character-class': 'error',
'no-new-symbol': 'error',
'no-obj-calls': 'error',
'no-octal': 'error',
'no-prototype-builtins': 'error',
'no-regex-spaces': 'error',
'no-self-assign': 'error',
'no-sparse-arrays': 'error',
'no-template-curly-in-string': 'warn',
'no-unexpected-multiline': 'error',
'no-unreachable': 'error',
'no-unsafe-finally': 'error',
'no-unsafe-negation': 'error',
'no-unsafe-optional-chaining': 'error',
'no-useless-backreference': 'error',
'require-atomic-updates': 'off',
'use-isnan': 'error',
'valid-typeof': 'error',
// Suggestions
'accessor-pairs': 'warn',
'arrow-body-style': ['warn', 'as-needed'],
'block-scoped-var': 'error',
'camelcase': ['warn', { allow: ['^_', '^VERMINE_'] }],
'class-methods-use-this': 'off', // Many utility methods don't use this
'complexity': ['warn', 20],
'consistent-return': 'warn',
'consistent-this': 'warn',
'curly': ['warn', 'multi-line', 'consistent'],
'default-case': 'warn',
'default-case-last': 'warn',
'default-param-last': 'warn',
'dot-locale-compare': 'warn',
'dot-notation': ['warn', { allowKeywords: true }],
'eqeqeq': ['error', 'always', { null: 'ignore' }],
'func-name-matching': 'warn',
'func-names': ['warn', 'as-needed'],
'func-style': ['warn', 'declaration', { allowArrowFunctions: true }],
'grouped-accessor-pairs': 'warn',
'guard-for-in': 'warn',
'id-blacklist': 'off',
'id-length': 'off',
'id-match': 'off',
'init-declarations': ['warn', 'always'],
'line-comment-position': 'off',
'lines-between-class-members': ['warn', 'always', { exceptAfterSingleLine: true }],
'logical-assignment-operators': ['warn', 'always'],
'max-classes-per-file': ['warn', 1],
'max-depth': ['warn', 5],
'max-lines': ['warn', { max: 500, skipBlankLines: true, skipComments: true }],
'max-lines-per-function': ['warn', { max: 100, skipBlankLines: true, skipComments: true, IIFEs: true }],
'max-nested-callbacks': ['warn', 3],
'max-params': ['warn', 5],
'max-statements': ['warn', 30],
'multiline-comment-style': 'off',
'new-cap': ['warn', { newIsCap: true, capIsNew: false }],
'no-alert': 'warn',
'no-array-constructor': 'error',
'no-bitwise': 'warn',
'no-caller': 'error',
'no-case-declarations': 'error',
'no-class-assign': 'error',
'no-cond-assign': ['error', 'always'],
'no-confusing-arrow': ['warn', { allowParens: true }],
'no-const-assign': 'error',
'no-continue': 'off',
'no-delete-var': 'error',
'no-div-regex': 'warn',
'no-else-return': ['warn', { allowElseIf: true }],
'no-empty-destructuring': 'warn',
'no-empty-function': ['warn', { allow: ['constructors'] }],
'no-empty-pattern': 'warn',
'no-eq-null': 'off',
'no-eval': 'error',
'no-extend-native': 'error',
'no-extra-bind': 'warn',
'no-extra-label': 'warn',
'no-floating-decimal': 'warn',
'no-global-assign': 'error',
'no-implicit-coercion': ['warn', { allow: ['!!', '+'] }],
'no-implicit-globals': 'error',
'no-implied-eval': 'error',
'no-inline-comments': 'off',
'no-invalid-this': 'off',
'no-iterator': 'warn',
'no-label-var': 'error',
'no-labels': ['warn', { allowLoop: true, allowSwitch: true }],
'no-lone-blocks': 'warn',
'no-lonely-if': 'warn',
'no-loop-func': 'warn',
'no-magic-numbers': ['warn', { ignore: [0, 1, 2], ignoreEnums: true, ignoreNumericLiteralTypes: true, ignoreArrayIndexes: true }],
'no-multi-assign': 'warn',
'no-multi-str': 'warn',
'no-negated-condition': 'warn',
'no-nested-ternary': 'warn',
'no-new': 'warn',
'no-new-func': 'warn',
'no-new-wrappers': 'error',
'no-nonoctal-decimal-escape': 'error',
'no-object-multi-space': 'warn',
'no-octal-escape': 'error',
'no-param-reassign': ['warn', { props: false }],
'no-plusplus': 'off',
'no-promise-executor-return': 'error',
'no-proto': 'error',
'no-redeclare': 'error',
'no-regex-spaces': 'error',
'no-restricted-globals': 'off',
'no-restricted-imports': 'off',
'no-restricted-modules': 'off',
'no-restricted-properties': 'off',
'no-restricted-syntax': 'off',
'no-return-assign': ['error', 'always'],
'no-return-await': 'error',
'no-script-url': 'warn',
'no-sequences': 'error',
'no-setter-return': 'error',
'no-shadow': 'warn',
'no-shadow-restricted-names': 'error',
'no-sparse-arrays': 'error',
'no-tabs': 'warn',
'no-template-curly-in-string': 'warn',
'no-ternary': 'off',
'no-this-before-super': 'error',
'no-throw-literal': 'warn',
'no-undef': ['error', { typeof: true }],
'no-undef-init': 'warn',
'no-undefined': 'off',
'no-underscore-dangle': ['warn', { allow: ['_id', '_on', '_source', '_total', '_html'] }],
'no-unneeded-ternary': ['warn', { defaultAssignment: false }],
'no-unreachable-loop': 'warn',
'no-unsafe-finally': 'error',
'no-unsafe-negation': 'error',
'no-unused-expressions': ['warn', { allowShortCircuit: true, allowTernary: true, enforceForJSX: false }],
'no-unused-labels': 'warn',
'no-unused-private-class-members': 'warn',
'no-unused-vars': ['warn', { args: 'none', caughtErrors: 'none', ignoreRestSiblings: true }],
'no-use-before-define': ['warn', { functions: false, classes: false, variables: true }],
'no-useless-call': 'warn',
'no-useless-catch': 'warn',
'no-useless-computed-key': ['warn', { enforceForClassMembers: false }],
'no-useless-concat': 'warn',
'no-useless-constructor': 'warn',
'no-useless-escape': 'warn',
'no-useless-rename': 'warn',
'no-useless-return': 'warn',
'no-var': 'error',
'no-void': ['warn', { allowAsStatement: true }],
'no-warning-comments': 'warn',
'no-with': 'error',
'object-shorthand': ['warn', 'always', { ignoreConstructors: false, avoidQuotes: true }],
'one-var': ['warn', 'never'],
'one-var-declaration-per-line': ['warn', 'initializations'],
'operator-assignment': ['warn', 'always'],
'prefer-arrow-callback': ['warn', { classPropertiesAllowed: true, disallowTLSClassFields: true }],
'prefer-const': ['error', { destructuring: 'all', ignoreReadBeforeAssign: false }],
'prefer-destructuring': ['warn', { array: false, object: true }],
'prefer-exponentiation-operator': 'warn',
'prefer-named-capture-group': 'off',
'prefer-numeric-literals': 'warn',
'prefer-object-has-own': 'warn',
'prefer-object-spread': 'warn',
'prefer-promise-reject-errors': ['error', { allowEmptyReject: false }],
'prefer-regex-literals': ['warn', { disallowRedundantWrapping: true }],
'prefer-rest-params': 'warn',
'prefer-spread': 'warn',
'prefer-template': 'warn',
'quote-props': ['warn', 'as-needed', { keywords: true, unnecessaryQuote: false, numbers: true }],
'radix': ['error', 'always'],
'require-await': 'warn',
'require-unicode-regexp': 'off',
'require-yield': 'error',
'sort-imports': 'off',
'sort-keys': 'off',
'sort-vars': 'off',
'spaced-comment': ['warn', 'always', { line: { markers: ['!', '/'] }, block: { balanced: true, markers: ['!', '*'], exceptions: ['*'] } }],
'strict': ['error', 'never'],
'symbol-description': 'warn',
'unicode-bom': ['error', 'never'],
'vars-on-top': 'off',
'yoda': ['warn', 'never', { exceptRange: true }],
// Layout & Formatting
'array-bracket-newline': ['warn', 'consistent'],
'array-bracket-spacing': ['warn', 'never'],
'array-element-newline': ['warn', 'consistent'],
'arrow-parens': ['warn', 'always'],
'arrow-spacing': ['warn', { before: true, after: true }],
'block-spacing': ['warn', 'always'],
'brace-style': ['warn', '1tbs', { allowSingleLine: true }],
'comma-dangle': ['warn', {
arrays: 'always-multiline',
objects: 'always-multiline',
imports: 'always-multiline',
exports: 'always-multiline',
functions: 'always-multiline'
}],
'comma-spacing': ['warn', { before: false, after: true }],
'comma-style': ['warn', 'last', { exceptions: { VariableDeclarator: true, ArrayExpression: true, ObjectExpression: true } }],
'computed-property-spacing': ['warn', 'never', { enforceForClassMembers: true }],
'dot-notation': ['warn', { allowKeywords: true }],
'eol-last': ['warn', 'always'],
'func-call-spacing': ['warn', 'never'],
'func-style': ['warn', 'declaration', { allowArrowFunctions: true }],
'function-call-argument-newline': ['warn', 'consistent'],
'function-paren-newline': ['warn', 'consistent'],
'generator-star-spacing': ['warn', { before: false, after: true }],
'implicit-arrow-linebreak': ['warn', 'beside'],
'indent': ['warn', 2, {
SwitchCase: 1,
VariableDeclarator: { var: 2, let: 2, const: 3 },
outerIIFEBody: 1,
MemberExpression: 'off',
FunctionDeclaration: { body: 1, parameters: 1, parameters: { var: 2, let: 2, const: 3 } },
FunctionExpression: { body: 1, parameters: 1, parameters: { var: 2, let: 2, const: 3 } },
StaticBlock: { body: 1 },
ClassBody: 1
}],
'jsx-quotes': 'off',
'key-spacing': ['warn', { beforeColon: false, afterColon: true, mode: 'strict' }],
'keyword-spacing': ['warn', { before: true, after: true, overrides: { return: { after: true }, throw: { after: true }, case: { after: true } } }],
'line-comment-position': 'off',
'linebreak-style': ['warn', 'unix'],
'lines-around-comment': 'off',
'lines-between-class-members': ['warn', 'always', { exceptAfterSingleLine: true }],
'max-len': ['warn', {
code: 120,
tabWidth: 2,
comments: 120,
ignoreComments: false,
ignoreTrailingComments: true,
ignoreUrls: true,
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreRegExpLiterals: true
}],
'max-statements-per-line': ['warn', { max: 1 }],
'multiline-comment-style': 'off',
'multiline-ternary': ['warn', 'always-multiline'],
'new-parens': 'warn',
'newline-per-chained-call': ['warn', { ignoreChainWithDepth: 3 }],
'no-extra-parens': ['warn', 'all', { conditionalAssign: false, returnAssign: false, nestedBinaryExpressions: false, ignoreJSX: 'all', enforceForArrowConditionals: false, enforceForSequenceExpressions: false, enforceForNewInMemberExpressions: false }],
'no-extra-semi': 'warn',
'no-floating-decimal': 'warn',
'no-mixed-operators': ['warn', { groups: [['+', '-', '*', '/', '%', '**'], ['&', '|', '^', '~', '<<', '>>', '>>>'], ['==', '!=', '===', '!==', '>', '>=', '<', '<='], ['&&', '||'], ['in', 'instanceof']], allowSamePrecedence: false }],
'no-mixed-spaces-and-tabs': 'warn',
'no-multi-spaces': ['warn', { ignoreEOLComments: false, exceptions: { Property: true, BinaryExpression: false, VariableDeclarator: true, ImportDeclaration: true } }],
'no-multiple-empty-lines': ['warn', { max: 1, maxEOF: 0, maxBOF: 0 }],
'no-tabs': 'warn',
'no-trailing-spaces': ['warn', { skipBlankLines: false, ignoreComments: false }],
'no-whitespace-before-property': 'warn',
'nonblock-statement-body-position': ['warn', 'beside', { overrides: { if: 'beside', while: 'beside', do: 'beside', for: 'beside' } }],
'object-curly-newline': ['warn', { multiline: true, consistent: true }],
'object-curly-spacing': ['warn', 'always'],
'object-property-newline': ['warn', { allowAllPropertiesOnSameLine: true }],
'operator-linebreak': ['warn', 'after', { overrides: { '?': 'before', ':': 'before', '||': 'after', '&&': 'after', '|>': 'after' } }],
'padded-blocks': ['warn', 'never'],
'padding-line-between-statements': 'off',
'prefer-exponentiation-operator': 'warn',
'quote-props': ['warn', 'as-needed', { keywords: true, unnecessaryQuote: false, numbers: true }],
'quotes': ['warn', 'single', { avoidEscape: true, allowTemplateLiterals: true }],
'rest-spread-spacing': ['warn', 'never'],
'semi': ['warn', 'always'],
'semi-spacing': ['warn', { before: false, after: true }],
'semi-style': ['warn', 'last'],
'space-before-blocks': ['warn', 'always'],
'space-before-function-paren': ['warn', { anonymous: 'always', named: 'never', asyncArrow: 'always' }],
'space-in-parens': ['warn', 'never'],
'space-infix-ops': 'warn',
'space-unary-ops': ['warn', { words: true, nonwords: false, overrides: {} }],
'switch-colon-spacing': ['warn', { after: true, before: false }],
'template-curly-spacing': 'warn',
'template-tag-spacing': ['warn', 'never'],
'unicode-bom': ['error', 'never'],
'wrap-iife': ['warn', 'outside', { functionPrototypeMethods: true }],
'wrap-regex': 'warn',
'yield-star-spacing': ['warn', { before: false, after: true }]
},
overrides: [
{
files: ['*.hbs', '*.handlebars'],
rules: {
// Handlebars templates don't need linting
'no-undef': 'off'
}
},
{
files: ['**/tests/**', '**/*.test.js', '**/*.spec.js'],
env: {
jest: true,
mocha: true
}
}
],
ignorePatterns: [
'node_modules/',
'dist/',
'build/',
'*.min.js',
'*.min.css'
]
};
+69
View File
@@ -0,0 +1,69 @@
name: Release Creation
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: RouxAntoine/checkout@v3.5.4
# Extrait la version depuis le tag (ex: v1.2.3 → 1.2.3)
- name: Extract tag version number
id: get_version
uses: battila7/get-version-action@v2
# Met à jour version, manifest et download dans system.json
- name: Substitute Manifest and Download Links For Versioned Ones
id: sub_manifest_link_version
uses: microsoft/variable-substitution@v1
with:
files: "system.json"
env:
version: ${{steps.get_version.outputs.version-without-v}}
url: https://www.uberwald.me/gitea/${{gitea.repository}}
manifest: https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/latest/system.json
download: https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{github.event.release.tag_name}}/vermine2047-${{github.event.release.tag_name}}.zip
# Compile le CSS depuis les sources LESS
- name: Setup Node.js
uses: https://github.com/actions/setup-node@v4
with:
node-version: '22'
- name: Build CSS
run: npm install && npm run build
# Crée le zip de release avec tous les fichiers nécessaires au système
- run: |
apt update -y
apt install -y zip
- run: zip -r ./vermine2047-${{github.event.release.tag_name}}.zip system.json module/ css/ templates/ lang/ assets/ packs/
- name: setup go
uses: https://github.com/actions/setup-go@v4
with:
go-version: ">=1.20.1"
- name: Use Go Action
id: use-go-action
uses: https://gitea.com/actions/release-action@main
with:
files: |-
./vermine2047-${{github.event.release.tag_name}}.zip
system.json
api_key: "${{secrets.ALLOW_PUSH_RELEASE}}"
- name: Publish to Foundry server
uses: https://github.com/djlechuck/foundryvtt-publish-package-action@v1
with:
token: ${{ secrets.FOUNDRYVTT_RELEASE_TOKEN }}
id: 'vermine2047'
version: ${{github.event.release.tag_name}}
manifest: 'https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/latest/system.json'
notes: 'https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{github.event.release.tag_name}}/vermine2047-${{github.event.release.tag_name}}.zip'
compatibility-minimum: '14'
compatibility-verified: '14'
+55
View File
@@ -0,0 +1,55 @@
# Vermine 2047 — Copilot Instructions
## Build & Development
```bash
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 pullYAMLtoLDB # Build LevelDB compendium packs from src/packs/ YAML → packs/
npm run pushLDBtoYAML # Extract LevelDB packs to editable YAML in src/packs/
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.
## Architecture
This is a **FoundryVTT game system** (`system.json` → system ID `vermine2047`), compatible with Foundry v11v14. It implements the French post-apocalyptic TTRPG **Vermine 2047**.
**Entry point**: `module/vermine2047.mjs` — loaded as an ES module. On the `init` hook it registers custom document classes, sheet classes, combat classes, Handlebars helpers, settings, hooks, and preloads templates.
**Actor types** (4): `character`, `npc`, `group`, `creature`
**Item types** (12): `item`, `weapon`, `defense`, `vehicle`, `ability`, `specialty`, `background`, `trauma`, `evolution`, `rumor`, `target`, `rite`
**Key source files** (`module/system/`):
| File | Role |
|---|---|
| `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 |
| `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 |
| `hooks.mjs` | All Foundry hook registrations (chat messages, hotbar drop, combat, preCreate, Dice So Nice) |
| `settings.mjs` | System settings (game mode: survie/cauchemar/apocalypse) |
| `handlebars-manager.mjs` | Template preloading paths + all Handlebars helpers (level config lookups, math, conditionals) |
| `effects.mjs` | Active effect management |
| `dialogs/rollDialog.mjs` | Advanced roll dialog (ability/skill selection, difficulty, totems, specialties, assist/pool bonuses) |
| `applications.mjs` | `TotemPicker` and `TraitSelector` applications |
**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`.
**Sheet inheritance**: `VermineActorSheet` (base) → `VermineCharacterSheet`, `VermineNpcSheet`, `VermineGroupSheet`, `VermineCreatureSheet`. Sheets expose `CONFIG.VERMINE` as `context.config` in template data.
**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
- **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`.
- **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`.
- **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.
- **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.
- **New template partials**: Must be listed in `preloadHandlebarsTemplates()` in `handlebars-manager.mjs` for pre-compilation.
- **Full architecture documentation**: See `docs/technical/ARCHITECTURE.md`.
+10 -1
View File
@@ -1,6 +1,7 @@
# IDE
.idea/
.vsode
.vscode/
.github/
# Node Modules
node_modules
@@ -13,3 +14,11 @@ packs/*/*
*.lock
jsconfig.json
foundry
.history/
# Ancien dossier SCSS (archivé)
scss_old/
# Fichiers CSS générés
css/vermine2047.*.css
!css/vermine2047.css
+103 -1
View File
@@ -1,4 +1,85 @@
# CHANGELOG
# 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
@@ -37,3 +118,24 @@
## 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*
+69
View File
@@ -0,0 +1,69 @@
# Makefile pour Vermine2047
# Ce fichier fournit des commandes courantes pour le développement
# Utilise uniquement LESS comme préprocesseur CSS
.PHONY: help install build build-less build-dev build-css watch clean lint
# Couleurs pour l'affichage
GREEN := \033[0;32m
YELLOW := \033[1;33m
NC := \033[0m # No Color
help: ## Affiche cette aide
@echo "Commandes disponibles pour Vermine2047:"
@echo ""
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[0;32m%-20s\033[0m %s\n", $$1, $$2}'
@echo ""
install: ## Installe les dépendances npm
@echo "$(YELLOW)Installation des dépendances npm...$(NC)"
npm install
@echo "$(GREEN)✓ Dépendances installées$(NC)"
build: build-less ## Compile LESS → vermine2047.min.css
build-less: ## Compile le LESS en CSS minifié (vermine2047.min.css)
@echo "$(YELLOW)Compilation du LESS...$(NC)"
npm run build:less
@echo "$(GREEN)✓ LESS compilé$(NC)"
build-dev: ## Compile le LESS en CSS non minifié (vermine2047.dev.css)
@echo "$(YELLOW)Compilation du LESS (mode dev)...$(NC)"
npm run build:less:dev
@echo "$(GREEN)✓ LESS compilé en mode dev$(NC)"
build-css: build-less build-dev ## Compile tout le CSS (minifié + dev)
watch: ## Lance le mode watch (recompilation automatique)
@echo "$(YELLOW)Lancement du mode watch...$(NC)"
npm run watch
clean: clean-css ## Nettoie les fichiers CSS générés
clean-css: ## Supprime les fichiers CSS compilés (garde vermine2047.css original)
@echo "$(YELLOW)Nettoyage des fichiers CSS...$(NC)"
npm run clean:css
@echo "$(GREEN)✓ Fichiers CSS nettoyés$(NC)"
rebuild: clean build ## Reconstruit tout le CSS
@echo "$(YELLOW)Reconstruction complète du CSS...$(NC)"
npm run rebuild:css
@echo "$(GREEN)✓ CSS reconstruit$(NC)"
lint: lint-less ## Lance le linting du code LESS
lint-less: ## Vérifie la qualité du code LESS
@echo "$(YELLOW)Linting du code LESS...$(NC)"
npm run lint:less
@echo "$(GREEN)✓ Linting terminé$(NC)"
# Commandes utilitaires
launch-foundry: ## Lance FoundryVTT
npm run launch_Foundry12
push-yaml: ## Push LDB vers YAML
node ./tools/pushLDBtoYAML.mjs
pull-yaml: ## Pull YAML vers LDB
node ./tools/pullYAMLtoLDB.mjs
+15 -14
View File
@@ -10,23 +10,23 @@
- [X] dialog d'edition des min-max
### fiche de groupe
- [ ] pas encore penché dessus
- [X] pas encore penché dessus
#### Members et encounters
- [ ] faire une classe GroupLink pour avoir les actors en objets dans les array group.members[], group.encounters[], et character.encounters[],
- [ ] faire une fonction sur le Hook.onUpdateActor => update des groupes dans characters, update des encounters et members dans groups
- [X] faire une classe GroupLink pour avoir les actors en objets dans les array group.members[], group.encounters[], et character.encounters[],
- [X] faire une fonction sur le Hook.onUpdateActor => update des groupes dans characters, update des encounters et members dans groups
### fiche de pnj créature
- [ ] à faire,
- [ ] lister les gabarit/taille/roles(creatures) et menace/experience/role(pnj)... stocker les modifs dans CONFIG.VERMINE,
- [X] à faire,
- [X] lister les gabarit/taille/roles(creatures) et menace/experience/role(pnj)... stocker les modifs dans CONFIG.VERMINE,
### les jets de dés
- [ ] redesign de rollDialog => `<details>+<sumary>`=> rendre moins dense
- [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
- [ ] gérer le fait de choisir quel totem garder : recalcul des réussites
- [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)
@@ -34,20 +34,21 @@
- [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
- [ ] gérer les réussites auto
- [ ] gérer les seuils auto si compétence non maitrisée
- [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
- [ ] modifier la difficulté en fonction de l'état du combatant /offensif/actif/passif/
- [X] modifier la difficulté en fonction de l'état du combatant /offensif/actif/passif/
### les items
- [X]ajouter apprentissage aux abilities
- [X] ajouter apprentissage aux abilities
- [X] passer le type d'arme en select/options
- [X] ajouter handicap de rareté
- [ ] ajouter pour items Item "competence nécessaire"
- [-] gérer les rolls d'items dans le chat
- [-] repasser sur les différents itemTypes et sheets
- [X] ajouter pour items Item "competence nécessaire"
- [X] gérer les rolls d'items dans le chat
- [X] repasser sur les différents itemTypes et sheets
- [X] verifier le selector de traits (trait pratique cf : msg pretre)
- [X] construire une selecteur de traits, traits= CONFIG.VERMINE.traits
traits:[
+203
View File
@@ -0,0 +1,203 @@
# 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.
+175
View File
@@ -0,0 +1,175 @@
# 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: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 776 KiB

+680
View File
@@ -0,0 +1,680 @@
uid=78_0 RootWebArea "Foundry Virtual Tabletop" url="https://localhost:31000/game"
uid=78_1 ignored
uid=78_2 generic
uid=78_3 list
uid=78_4 generic
uid=78_5 generic
uid=78_6 generic
uid=78_7 generic
uid=78_8 generic
uid=78_9 generic
uid=78_10 list
uid=78_11 listitem level="1"
uid=78_12 tab "Outils de token" selectable
uid=78_13 listitem level="1"
uid=78_14 tab "Outils de tuile" selectable
uid=78_15 listitem level="1"
uid=78_16 tab "Outils de dessin" selectable
uid=78_17 listitem level="1"
uid=78_18 tab "Outils de mur" selectable
uid=78_19 listitem level="1"
uid=78_20 tab "Outils de lumière" selectable
uid=78_21 listitem level="1"
uid=78_22 tab "Outils de son dambiance" selectable
uid=78_23 listitem level="1"
uid=78_24 tab "Outils de région" selectable
uid=78_25 listitem level="1"
uid=78_26 tab "Notes" selectable
uid=78_27 list
uid=78_28 listitem level="1"
uid=78_29 button "Sélection de tokens" pressed
uid=78_30 listitem level="1"
uid=78_31 button "Sélection de cibles"
uid=78_32 listitem level="1"
uid=78_33 button "Règle"
uid=78_34 listitem level="1"
uid=78_35 button "Déplacement sans contrainte"
uid=78_36 generic
uid=78_37 generic
uid=78_38 list
uid=78_39 listitem level="1"
uid=78_40 ignored
uid=78_41 StaticText "Gamemaster [MJ]"
uid=78_42 InlineTextBox "Gamemaster [MJ]"
uid=78_43 generic
uid=78_44 generic
uid=78_45 LabelText
uid=78_46 StaticText "Latence"
uid=78_42 InlineTextBox "Latence"
uid=78_47 StaticText " "
uid=78_42 InlineTextBox " "
uid=78_48 StaticText "1ms"
uid=78_42 InlineTextBox "1ms"
uid=78_49 generic
uid=78_50 LabelText
uid=78_51 StaticText "IPS"
uid=78_42 InlineTextBox "IPS"
uid=78_52 StaticText " "
uid=78_42 InlineTextBox " "
uid=78_53 StaticText "--"
uid=78_42 InlineTextBox "--"
uid=78_54 button
uid=78_55 generic
uid=78_56 navigation
uid=78_57 list
uid=78_58 generic
uid=78_59 sectionheader
uid=78_60 generic
uid=78_61 generic
uid=78_62 LabelText
uid=78_63 LabelText
uid=78_64 sectionfooter
uid=78_65 generic roledescription="Barre de raccourcis"
uid=78_66 generic
uid=78_67 button "Couper le son"
uid=78_68 button "Menu principal"
uid=78_69 list
uid=78_70 button "Emplacement vide"
uid=78_71 ignored
uid=78_72 StaticText "1"
uid=78_42 InlineTextBox "1"
uid=78_73 button "Emplacement vide"
uid=78_74 ignored
uid=78_75 StaticText "2"
uid=78_42 InlineTextBox "2"
uid=78_76 button "Emplacement vide"
uid=78_77 ignored
uid=78_78 StaticText "3"
uid=78_42 InlineTextBox "3"
uid=78_79 button "Emplacement vide"
uid=78_80 ignored
uid=78_81 StaticText "4"
uid=78_42 InlineTextBox "4"
uid=78_82 button "Emplacement vide"
uid=78_83 ignored
uid=78_84 StaticText "5"
uid=78_42 InlineTextBox "5"
uid=78_85 button "Emplacement vide"
uid=78_86 ignored
uid=78_87 StaticText "6"
uid=78_42 InlineTextBox "6"
uid=78_88 button "Emplacement vide"
uid=78_89 ignored
uid=78_90 StaticText "7"
uid=78_42 InlineTextBox "7"
uid=78_91 button "Emplacement vide"
uid=78_92 ignored
uid=78_93 StaticText "8"
uid=78_42 InlineTextBox "8"
uid=78_94 button "Emplacement vide"
uid=78_95 ignored
uid=78_96 StaticText "9"
uid=78_42 InlineTextBox "9"
uid=78_97 button "Emplacement vide"
uid=78_98 ignored
uid=78_99 StaticText "0"
uid=78_42 InlineTextBox "0"
uid=78_100 generic
uid=78_101 navigation
uid=78_102 button "Page suivante"
uid=78_103 ignored
uid=78_104 StaticText "1"
uid=78_42 InlineTextBox "1"
uid=78_105 button "Page précédente"
uid=78_106 ignored
uid=78_107 button "Verrouiller la barre de raccourcis"
uid=78_108 button "Effacer la barre de raccourcis"
uid=78_109 generic
uid=78_110 generic
uid=78_111 generic
uid=78_112 ignored
uid=78_113 list
uid=78_114 generic "Tchat"
uid=78_115 generic
uid=78_116 generic value="
"
uid=78_117 paragraph
uid=78_118 LineBreak "
"
uid=78_42 InlineTextBox "
"
uid=78_119 ignored
uid=78_42 generic
uid=78_42 StaticText "Entrer un message"
uid=78_42 InlineTextBox "Entrer un message"
uid=78_120 generic
uid=78_121 generic
uid=78_122 button "Public en tant qu'utilisateur" pressed
uid=78_123 button "Privé pour les maîtres de jeu"
uid=78_124 button "Aveugle pour les maîtres de jeu"
uid=78_125 button "Seulement pour soi-même"
uid=78_126 button "Public en tant que personnage"
uid=78_127 generic
uid=78_128 tablist orientation="horizontal"
uid=78_129 list
uid=78_130 listitem level="1"
uid=78_131 tab "Messages du tchat" selectable
uid=78_132 listitem level="1"
uid=78_133 tab "Rencontres de combat" selectable
uid=78_134 listitem level="1"
uid=78_135 tab "Scènes" selectable
uid=78_136 listitem level="1"
uid=78_137 tab "Objets plaçables" selectable
uid=78_138 listitem level="1"
uid=78_139 tab "Acteurs" selectable
uid=78_140 listitem level="1"
uid=78_141 tab "Objets" selectable
uid=78_142 listitem level="1"
uid=78_143 tab "Journaux" selectable
uid=78_144 listitem level="1"
uid=78_145 tab "Tables aléatoires" selectable
uid=78_146 listitem level="1"
uid=78_147 tab "Jeux de cartes" selectable
uid=78_148 listitem level="1"
uid=78_149 tab "Macros" selectable
uid=78_150 listitem level="1"
uid=78_151 tab "Playlists" selectable
uid=78_152 listitem level="1"
uid=78_153 tab "Compendiums" selectable
uid=78_154 listitem level="1"
uid=78_155 tab "Paramètres" selectable
uid=78_156 listitem level="1"
uid=78_157 button "Réduire"
uid=78_158 generic
uid=78_159 ignored
uid=78_160 ignored
uid=78_161 ignored
uid=78_162 ignored
uid=78_163 generic
uid=78_164 sectionheader
uid=78_165 ignored
uid=78_166 button "Créer un acteur"
uid=78_167 ignored
uid=78_168 ignored
uid=78_169 StaticText "Créer un acteur"
uid=78_42 InlineTextBox "Créer un acteur"
uid=78_170 button "Créer un dossier"
uid=78_171 ignored
uid=78_172 ignored
uid=78_173 StaticText "Créer un dossier"
uid=78_42 InlineTextBox "Créer un dossier"
uid=78_174 search
uid=78_175 button "Recherche par nom uniquement"
uid=78_176 searchbox "Chercher dans les Acteurs"
uid=78_177 ignored
uid=78_178 ignored
uid=78_179 generic
uid=78_180 button "Trier par ordre alphabétique"
uid=78_181 button "Réduire tous les dossiers"
uid=78_182 list
uid=78_183 listitem level="1"
uid=78_184 image "Acteur" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/creature.webp"
uid=78_185 generic
uid=78_186 StaticText "Acteur"
uid=78_42 InlineTextBox "Acteur"
uid=78_187 listitem level="1"
uid=78_188 image "Acteur (2)" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/group.webp"
uid=78_189 generic
uid=78_190 StaticText "Acteur (2)"
uid=78_42 InlineTextBox "Acteur (2)"
uid=78_191 listitem level="1"
uid=78_192 image "Acteur (3)" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/character.webp"
uid=78_193 generic
uid=78_194 StaticText "Acteur (3)"
uid=78_42 InlineTextBox "Acteur (3)"
uid=78_195 sectionfooter
uid=78_196 ignored
uid=78_197 ignored
uid=78_198 ignored
uid=78_199 ignored
uid=78_200 ignored
uid=78_201 ignored
uid=78_202 ignored
uid=78_203 ignored
uid=78_204 ignored
uid=78_205 ignored
uid=78_206 ignored
uid=78_207 ignored
uid=78_208 ignored
uid=78_209 ignored
uid=78_210 ignored
uid=78_211 ignored
uid=78_212 ignored
uid=78_213 ignored
uid=78_214 ignored
uid=78_215 ignored
uid=78_216 ignored
uid=78_217 ignored
uid=78_218 ignored
uid=78_219 figure
uid=78_220 image url="https://localhost:31000/ui/pause.svg"
uid=78_221 Figcaption
uid=78_222 StaticText "JEU EN PAUSE"
uid=78_42 InlineTextBox "JEU EN PAUSE"
uid=78_223 form
uid=78_224 banner
uid=78_225 heading "Personnage: Acteur (3)" level="1"
uid=78_226 StaticText "Personnage: Acteur (3)"
uid=78_42 InlineTextBox "Personnage: Acteur (3)"
uid=78_227 button "Basculer les contrôles"
uid=78_228 button "Copier l'UUID du document"
uid=78_229 button "Fermer la fenêtre"
uid=78_230 generic
uid=78_231 ignored
uid=78_232 ignored
uid=78_233 button "MODE ÉDITION"
uid=78_234 StaticText "MODE ÉDITION"
uid=78_42 InlineTextBox "MODE ÉDITION"
uid=78_235 image "logo Vermine" url="https://localhost:31000/systems/vermine2047/assets/images/ui/logo.webp"
uid=78_236 ignored
uid=78_237 image "Acteur (3)" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/character.webp"
uid=78_238 generic
uid=78_239 heading "ADAPTATION" level="3"
uid=78_240 StaticText "ADAPTATION"
uid=78_42 InlineTextBox "ADAPTATION"
uid=78_241 heading "L'Humain" level="5"
uid=78_242 StaticText "L'Humain"
uid=78_42 InlineTextBox "L'Humain"
uid=78_243 image url="https://localhost:31000/systems/vermine2047/assets/images/ui/totems/human.webp"
uid=78_244 heading "L'adapté" level="5"
uid=78_245 StaticText "L'adapté"
uid=78_42 InlineTextBox "L'adapté"
uid=78_246 image url="https://localhost:31000/systems/vermine2047/assets/images/ui/totems/adapted.webp"
uid=78_247 ignored
uid=78_248 ignored
uid=78_249 ignored
uid=78_250 generic
uid=78_251 generic
uid=78_252 ignored
uid=78_253 ignored
uid=78_254 ignored
uid=78_255 ignored
uid=78_256 generic
uid=78_257 generic
uid=78_258 ignored
uid=78_259 ignored
uid=78_260 list
uid=78_261 listitem level="1"
uid=78_262 ignored
uid=78_263 heading "LA HORDE" level="4"
uid=78_264 StaticText "LA HORDE"
uid=78_42 InlineTextBox "LA HORDE"
uid=78_265 image "La Horde" url="https://localhost:31000/systems/vermine2047/assets/images/ui/totems/horde.webp"
uid=78_266 navigation roledescription="Navigation dans l’onglet des feuilles de personnages"
uid=78_267 generic
uid=78_268 ignored
uid=78_269 StaticText "Caractéristiques et compétences"
uid=78_42 InlineTextBox "Caractéristiques "
uid=78_42 InlineTextBox "et compétences"
uid=78_270 generic
uid=78_271 ignored
uid=78_272 StaticText "Totem et ajustements"
uid=78_42 InlineTextBox "Totem et "
uid=78_42 InlineTextBox "ajustements"
uid=78_273 generic
uid=78_274 ignored
uid=78_275 StaticText "Matériel"
uid=78_42 InlineTextBox "Matériel"
uid=78_276 generic
uid=78_277 ignored
uid=78_278 StaticText "Histoire"
uid=78_42 InlineTextBox "Histoire"
uid=78_279 generic
uid=78_280 ignored
uid=78_281 StaticText "Combat et reserves"
uid=78_42 InlineTextBox "Combat "
uid=78_42 InlineTextBox "et "
uid=78_42 InlineTextBox "reserves"
uid=78_282 generic
uid=78_283 sectionheader
uid=78_284 generic
uid=78_285 heading "Nom Acteur (3)" level="1"
uid=78_286 LabelText
uid=78_287 StaticText "Nom"
uid=78_42 InlineTextBox "Nom"
uid=78_288 ignored
uid=78_289 StaticText "Acteur (3)"
uid=78_42 InlineTextBox "Acteur (3)"
uid=78_290 ignored
uid=78_291 LabelText
uid=78_292 StaticText "Profil"
uid=78_42 InlineTextBox "Profil"
uid=78_293 ignored
uid=78_294 ignored
uid=78_295 LabelText
uid=78_296 StaticText "Age"
uid=78_42 InlineTextBox "Age"
uid=78_297 ignored
uid=78_298 ignored
uid=78_299 StaticText "15"
uid=78_42 InlineTextBox "15"
uid=78_300 generic
uid=78_301 StaticText "(Jeune)"
uid=78_42 InlineTextBox "(Jeune)"
uid=78_302 generic
uid=78_303 heading "Totem La Horde" level="1"
uid=78_304 LabelText
uid=78_305 StaticText "Totem"
uid=78_42 InlineTextBox "Totem"
uid=78_306 generic
uid=78_307 StaticText "La Horde"
uid=78_42 InlineTextBox "La Horde"
uid=78_308 ignored
uid=78_309 LabelText
uid=78_310 StaticText "Réputation"
uid=78_42 InlineTextBox "Réputation"
uid=78_311 ignored
uid=78_312 StaticText "0"
uid=78_42 InlineTextBox "0"
uid=78_313 LabelText
uid=78_314 StaticText "Expérience"
uid=78_42 InlineTextBox "Expérience"
uid=78_315 ignored
uid=78_316 StaticText "0"
uid=78_42 InlineTextBox "0"
uid=78_317 heading "CARACTÉRISTIQUES" level="3"
uid=78_318 StaticText "CARACTÉRISTIQUES"
uid=78_42 InlineTextBox "CARACTÉRISTIQUES"
uid=78_319 ignored
uid=78_320 ignored
uid=78_321 heading "PHYSIQUE" level="4"
uid=78_322 StaticText "PHYSIQUE"
uid=78_42 InlineTextBox "PHYSIQUE"
uid=78_323 generic
uid=78_324 LabelText
uid=78_325 StaticText "Vigueur"
uid=78_42 InlineTextBox "Vigueur"
uid=78_326 generic
uid=78_327 LabelText
uid=78_328 StaticText "Santé"
uid=78_42 InlineTextBox "Santé"
uid=78_329 ignored
uid=78_330 heading "MANUEL" level="4"
uid=78_331 StaticText "MANUEL"
uid=78_42 InlineTextBox "MANUEL"
uid=78_332 generic
uid=78_333 LabelText
uid=78_334 StaticText "Précision"
uid=78_42 InlineTextBox "Précision"
uid=78_335 generic
uid=78_336 LabelText
uid=78_337 StaticText "Réflexes"
uid=78_42 InlineTextBox "Réflexes"
uid=78_338 ignored
uid=78_339 heading "MENTAL" level="4"
uid=78_340 StaticText "MENTAL"
uid=78_42 InlineTextBox "MENTAL"
uid=78_341 generic
uid=78_342 LabelText
uid=78_343 StaticText "Savoir"
uid=78_42 InlineTextBox "Savoir"
uid=78_344 generic
uid=78_345 LabelText
uid=78_346 StaticText "Perception"
uid=78_42 InlineTextBox "Perception"
uid=78_347 ignored
uid=78_348 heading "SOCIAL" level="4"
uid=78_349 StaticText "SOCIAL"
uid=78_42 InlineTextBox "SOCIAL"
uid=78_350 generic
uid=78_351 LabelText
uid=78_352 StaticText "Volonté"
uid=78_42 InlineTextBox "Volonté"
uid=78_353 generic
uid=78_354 LabelText
uid=78_355 StaticText "Empathie"
uid=78_42 InlineTextBox "Empathie"
uid=78_356 heading "COMPÉTENCES" level="3"
uid=78_357 StaticText "COMPÉTENCES"
uid=78_42 InlineTextBox "COMPÉTENCES"
uid=78_358 ignored
uid=78_359 ignored
uid=78_360 heading "L'HOMME" level="4"
uid=78_361 StaticText "L'HOMME"
uid=78_42 InlineTextBox "L'HOMME"
uid=78_362 generic
uid=78_363 LabelText
uid=78_364 StaticText "Arts "
uid=78_42 InlineTextBox "Arts "
uid=78_365 superscript
uid=78_366 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_367 generic
uid=78_368 LabelText
uid=78_369 StaticText "Civilisation "
uid=78_42 InlineTextBox "Civilisation "
uid=78_370 superscript
uid=78_371 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_372 generic
uid=78_373 LabelText
uid=78_374 StaticText "Psychologie "
uid=78_42 InlineTextBox "Psychologie "
uid=78_375 superscript
uid=78_376 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_377 generic
uid=78_378 LabelText
uid=78_379 StaticText "Rumeurs"
uid=78_42 InlineTextBox "Rumeurs"
uid=78_380 generic
uid=78_381 LabelText
uid=78_382 StaticText "Soins "
uid=78_42 InlineTextBox "Soins "
uid=78_383 superscript
uid=78_384 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_385 ignored
uid=78_386 heading "L'ANIMAL" level="4"
uid=78_387 StaticText "L'ANIMAL"
uid=78_42 InlineTextBox "L'ANIMAL"
uid=78_388 generic
uid=78_389 LabelText
uid=78_390 StaticText "Animalisme "
uid=78_42 InlineTextBox "Animalisme "
uid=78_391 superscript
uid=78_392 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_393 generic
uid=78_394 LabelText
uid=78_395 StaticText "Dissection "
uid=78_42 InlineTextBox "Dissection "
uid=78_396 superscript
uid=78_397 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_398 generic
uid=78_399 LabelText
uid=78_400 StaticText "Faune "
uid=78_42 InlineTextBox "Faune "
uid=78_401 superscript
uid=78_402 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_403 generic
uid=78_404 LabelText
uid=78_405 StaticText "Répulsion"
uid=78_42 InlineTextBox "Répulsion"
uid=78_406 generic
uid=78_407 LabelText
uid=78_408 StaticText "Pistage"
uid=78_42 InlineTextBox "Pistage"
uid=78_409 ignored
uid=78_410 heading "LA MACHINE" level="4"
uid=78_411 StaticText "LA MACHINE"
uid=78_42 InlineTextBox "LA MACHINE"
uid=78_412 generic
uid=78_413 LabelText
uid=78_414 StaticText "Artisanat "
uid=78_42 InlineTextBox "Artisanat "
uid=78_415 superscript
uid=78_416 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_417 generic
uid=78_418 LabelText
uid=78_419 StaticText "Bricolage"
uid=78_42 InlineTextBox "Bricolage"
uid=78_420 generic
uid=78_421 LabelText
uid=78_422 StaticText "Mécanique "
uid=78_42 InlineTextBox "Mécanique "
uid=78_423 superscript
uid=78_424 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_425 generic
uid=78_426 LabelText
uid=78_427 StaticText "Pilotage "
uid=78_42 InlineTextBox "Pilotage "
uid=78_428 superscript
uid=78_429 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_430 generic
uid=78_431 LabelText
uid=78_432 StaticText "Technologie "
uid=78_42 InlineTextBox "Technologie "
uid=78_433 superscript
uid=78_434 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_435 ignored
uid=78_436 heading "L'ARME" level="4"
uid=78_437 StaticText "L'ARME"
uid=78_42 InlineTextBox "L'ARME"
uid=78_438 generic
uid=78_439 LabelText
uid=78_440 StaticText "Armes à feu "
uid=78_42 InlineTextBox "Armes à feu "
uid=78_441 superscript
uid=78_442 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_443 generic
uid=78_444 LabelText
uid=78_445 StaticText "Tir à l'arc"
uid=78_42 InlineTextBox "Tir à l'arc"
uid=78_446 generic
uid=78_447 LabelText
uid=78_448 StaticText "Armurerie "
uid=78_42 InlineTextBox "Armurerie "
uid=78_449 superscript
uid=78_450 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_451 generic
uid=78_452 LabelText
uid=78_453 StaticText "Lancer"
uid=78_42 InlineTextBox "Lancer"
uid=78_454 generic
uid=78_455 LabelText
uid=78_456 StaticText "Mêlée"
uid=78_42 InlineTextBox "Mêlée"
uid=78_457 ignored
uid=78_458 heading "LA SURVIE" level="4"
uid=78_459 StaticText "LA SURVIE"
uid=78_42 InlineTextBox "LA SURVIE"
uid=78_460 generic
uid=78_461 LabelText
uid=78_462 StaticText "Vigilance"
uid=78_42 InlineTextBox "Vigilance"
uid=78_463 generic
uid=78_464 LabelText
uid=78_465 StaticText "Athlétisme"
uid=78_42 InlineTextBox "Athlétisme"
uid=78_466 generic
uid=78_467 LabelText
uid=78_468 StaticText "Alimentation"
uid=78_42 InlineTextBox "Alimentation"
uid=78_469 generic
uid=78_470 LabelText
uid=78_471 StaticText "Discrétion"
uid=78_42 InlineTextBox "Discrétion"
uid=78_472 generic
uid=78_473 LabelText
uid=78_474 StaticText "Corps-à-corps"
uid=78_42 InlineTextBox "Corps-à-corps"
uid=78_475 ignored
uid=78_476 heading "LA TERRE" level="4"
uid=78_477 StaticText "LA TERRE"
uid=78_42 InlineTextBox "LA TERRE"
uid=78_478 generic
uid=78_479 LabelText
uid=78_480 StaticText "Environnement "
uid=78_42 InlineTextBox "Environnement "
uid=78_481 superscript
uid=78_482 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_483 generic
uid=78_484 LabelText
uid=78_485 StaticText "Flore "
uid=78_42 InlineTextBox "Flore "
uid=78_486 superscript
uid=78_487 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_488 generic
uid=78_489 LabelText
uid=78_490 StaticText "Route"
uid=78_42 InlineTextBox "Route"
uid=78_491 generic
uid=78_492 LabelText
uid=78_493 StaticText "Toxiques "
uid=78_42 InlineTextBox "Toxiques "
uid=78_494 superscript
uid=78_495 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_496 generic
uid=78_497 LabelText
uid=78_498 StaticText "Vestiges "
uid=78_42 InlineTextBox "Vestiges "
uid=78_499 superscript
uid=78_500 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_501 ignored
uid=78_502 ignored
uid=78_503 ignored
uid=78_504 ignored
uid=78_505 ignored
uid=78_506 ignored
uid=78_507 ignored
uid=78_508 ignored
uid=78_509 ignored
uid=78_510 ignored
uid=78_511 ignored
uid=78_512 ignored
uid=78_513 ignored
uid=78_514 ignored
uid=78_515 ignored
uid=78_516 ignored
uid=78_517 ignored
uid=78_518 ignored
uid=78_519 ignored
uid=78_520 ignored
uid=78_521 ignored
uid=78_522 ignored
uid=78_523 ignored
uid=78_524 ignored
uid=78_525 ignored
uid=78_526 ignored
uid=78_527 ignored
uid=78_528 ignored
uid=78_529 ignored
uid=78_530 ignored
uid=78_531 ignored
uid=78_532 ignored
uid=78_533 ignored
uid=78_534 ignored
uid=78_535 ignored
uid=78_536 ignored
uid=78_537 ignored
uid=78_538 ignored
uid=78_539 ignored
uid=78_540 ignored
uid=78_541 ignored
uid=78_542 ignored
uid=78_543 ignored
uid=78_544 ignored
uid=78_545 ignored
uid=78_546 ignored
uid=78_547 ignored
uid=78_548 ignored
uid=78_549 ignored
uid=78_550 ignored
uid=78_551 ignored
uid=78_552 ignored
uid=78_553 ignored
uid=78_554 ignored
uid=78_555 generic
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
vermine2047.min.css
File diff suppressed because it is too large Load Diff
+696
View File
@@ -0,0 +1,696 @@
# Guide Utilisateur - Vermine2047 pour FoundryVTT
## Table des matières
1. [Introduction](#introduction)
2. [Installation](#installation)
3. [Création d'un personnage](#création-dun-personnage)
4. [Les jets de dés](#les-jets-de-dés)
5. [Le système de totems](#le-système-de-totems)
6. [La gestion des groupes](#la-gestion-des-groupes)
7. [Le combat](#le-combat)
8. [Les items](#les-items)
9. [Gestion des PNJ et Créatures](#gestion-des-pnj-et-créatures)
10. [Astuces et bonnes pratiques](#astuces-et-bonnes-pratiques)
---
## Introduction
Bienvenue dans Vermine2047, un système pour FoundryVTT qui implémente les règles du jeu de rôle post-apocalyptique Vermine 2047.
### À propos de Vermine2047
Vermine 2047 est un jeu de rôle dans un monde post-apocalyptique où les joueurs incarnent des survivants dans un environnement hostile. Le système utilise des dés d10 avec un système de seuils de réussite et des mécaniques uniques comme les totems et les domaines de prédilection.
### Compatibilité
- **FoundryVTT**: v11 à v14
- **Version du système**: 0.1.14
---
## Installation
### Prérequis
- FoundryVTT installé (version 11 ou supérieure)
- Module "Dice So Nice!" recommandé pour les dés 3D
### Installation du système
1. **Via le compendium de Foundry**
- Ouvrez FoundryVTT
- Allez dans "Game Systems"
- Cliquez sur "Install System"
- Recherchez "Vermine2047"
- Cliquez sur "Install"
2. **Via l'URL du manifest**
- Allez dans "Game Systems"
- Cliquez sur "Install System"
- Dans l'onglet "From Manifest URL", entrez:
```
https://raw.githubusercontent.com/rwanoux/vermine2047/refs/heads/main/system.json
```
- Cliquez sur "Install"
3. **Créer un nouveau monde**
- Sélectionnez "Vermine2047" comme système
- Donnez un nom à votre monde
- Configurez les paramètres
### Configuration recommandée
- Activez le module "Dice So Nice!" pour les dés 3D
- Configurez le mode de jeu (Survie, Cauchemar, Apocalypse) dans les paramètres du monde
---
## Création d'un personnage
### Étape 1: Créer un acteur
1. Cliquez sur l'icône "Actors" dans la barre latérale gauche
2. Cliquez sur "Create Actor"
3. Sélectionnez "Character" comme type
4. Donnez un nom à votre personnage
### Étape 2: Choisir un totem
Le totem est au cœur de votre personnage et détermine ses affinités.
1. Dans l'onglet "Totem et ajustements"
2. Cliquez sur le bouton "choisissez un totem"
3. Sélectionnez un totem dans la liste
4. Cliquez sur "Sélectionner"
**Les 10 totems disponibles:**
- **Humain**: Favorise les compétences humaines et du monde civilisé
- **Prédateur**: Favorise la chasse et la survie
- **Charognard**: Favorise la récupération et l'utilisation d'outils
- **Symbiote**: Favorise les interactions sociales
- **Parasite**: Favorise la discrétion et la survie
- **Bâtisseur**: Favorise la construction et la manipulation
- **Horde**: Favorise le combat en groupe
- **Ruche**: Favorise l'organisation collective
- **Solitaire**: Favorise l'autonomie
- **Adapté**: Favorise l'adaptation à l'environnement
### Étape 3: Définir les caractéristiques
Dans l'onglet "Caractéristiques et compétences", vous trouverez 8 caractéristiques réparties en 4 catégories:
**Physique:**
- Vigueur: Résistance physique
- Santé: Résistance aux blessures
**Manuel:**
- Précision: Dextérité et coordination
- Réflexes: Réactivité et vitesse
**Mental:**
- Connaissance: Savoir et mémoire
- Perception: Observation et intuition
**Social:**
- Volonté: Détermination et courage
- Empathie: Compréhension des autres
**Conseil:** Commencez avec des valeurs de 1-2 pour un personnage équilibré, ou 3-4 pour un spécialiste.
### Étape 4: Définir les compétences
Chaque compétence a une valeur de 0 à 5:
- 0: Incompétent
- 1: Débutant
- 2: Compétent
- 3: Expert
- 4: Maître
- 5: Légende
**Catégories de compétences:**
- **Humain**: Arts, civilisation, psychologie, rumeurs, soins
- **Animal**: Animalisme, dissection, vie sauvage, répulsion, pistes
- **Outil**: Artisanat, bricolage, mécanique, pilotage, technologie
- **Arme**: Armes à feu, tir à l'arc, armurerie, lancer, mêlée
- **Survie**: Vigilance, athlétisme, nourriture, discrétion, corps à corps
- **Monde**: Environnement, flore, route, toxiques, ruines
**Astuce:** Le domaine de prédilection (sélectionnable en haut de chaque catégorie) donne des bonus quand il est aligné avec votre totem.
### Étape 5: Ajouter des spécialités
Les spécialités donnent +1D quand elles sont utilisées avec la compétence parente.
1. Dans l'onglet "Caractéristiques et compétences"
2. Cliquez sur l'icône "+" à côté d'une compétence
3. Une spécialité sera créée avec le nom de la compétence
4. Vous pouvez renommer la spécialité
### Étape 6: Définir les réserves
Dans l'onglet "Combat et réserves":
- **Effort**: Réserve pour les actions physiques (basée sur Vigueur + Santé + Réflexes + Précision)
- **Sang-Froid**: Réserve pour les actions mentales (basée sur Connaissance + Perception + Volonté + Empathie)
### Étape 7: Définir l'identité
Dans l'onglet "Histoire":
- Age
- Origine
- Profil
- Concept
- Instincts
- Interdits
- Objectifs
- Relations
- Biographie
### Étape 8: Équipement
Dans l'onglet "Matériel", vous pouvez ajouter:
- Armes
- Protections
- Véhicules
- Objets
---
## Les jets de dés
### Ouvrir le dialogue de jet
Il y a plusieurs façons de lancer un jet:
1. **Depuis la fiche de personnage**
- Cliquez sur une caractéristique ou une compétence
- Un dialogue de jet s'ouvre avec la caractéristique/compétence présélectionnée
2. **Depuis la barre d'outils**
- Cliquez sur l'icône de dés dans la barre d'outils (à droite)
- Un dialogue de jet vide s'ouvre
3. **Depuis le chat**
- Tapez `/roll` ou utilisez la commande de jet
### Le dialogue de jet
Le dialogue de jet a été conçu pour être intuitif et compact.
#### Sélection de base
- **Caractéristique**: Sélectionnez une caractéristique (Vigueur, Santé, etc.)
- **Compétence**: Sélectionnez une compétence (optionnel)
- **Score**: Affiche la valeur de la caractéristique sélectionnée
#### Difficulté et Handicap
- **Difficulté**: Sélectionnez le niveau de difficulté
- Evidente (3+): Tâche très simple
- Facile (5+): Tâche simple
- Difficile (7+): Tâche standard
- Très difficile (9+): Tâche complexe
- Impossible (10+): Tâche extrêmement difficile
- **Handicap**: Sélectionnez le niveau de handicap
- Aucun: Pas de handicap
- (I): Handicap mineur
- (II): Handicap majeur
#### Bonus
La section "Bonus" peut être dépliée pour accéder aux options supplémentaires:
- **Entraide**: +1D si quelqu'un vous aide
- **Groupe**: +0 à +5D basé sur la taille du groupe
- **Sang-Froid**: +0 à +5D (basé sur votre réserve de Sang-Froid)
- **Équipement**: +1D si vous utilisez un outil approprié
- **Dés de totem**: Cochez pour utiliser les dés de totem
- Totem humain: +XD (où X est la valeur de votre totem humain)
- Totem adapté: +XD (où X est la valeur de votre totem adapté)
**Astuce:** Si vous avez les deux totems (humain et adapté) avec des valeurs > 0, vous pouvez choisir quel totem garder après le jet.
#### Total du pool de dés
Le dialogue affiche le total du pool de dés en temps réel:
- **0D**: Aucune caractéristique sélectionnée
- **3D**: Caractéristique de valeur 3
- **4D**: Caractéristique 3 + Compétence 1
- **5D+**: Avec bonus
### Les bonus de domaine de totem
Votre totem influence vos jets en fonction du domaine de prédilection:
- Si votre domaine de prédilection est dans les domaines de votre totem, vous obtenez +1 dé
- Si votre domaine de prédilection est dans les domaines du totem opposé, vous subissez -1 dé
**Exemple:**
- Totem: Prédateur (domaines: animal, survie)
- Domaine de prédilection: Survie
- Bonus: +1 dé sur tous les jets de survie
### Les réussites automatiques
En fonction de votre niveau de maîtrise d'une compétence, vous obtenez des réussites automatiques:
| Niveau | Réussites automatiques | Avec spécialité |
|--------|------------------------|-----------------|
| Incompétent (0) | 0 | 0 |
| Débutant (1) | 0 | 0 |
| Compétent (2) | 0 | +1 |
| Expert (3) | +1 | +1 |
| Maître (4) | +1 | +2 |
| Légende (5) | +2 | +2 |
### Les seuils automatiques
Si vous n'êtes pas maîtrisé dans une compétence, un seuil plus élevé est automatiquement appliqué:
| Niveau | Seuil automatique |
|--------|-------------------|
| Incompétent (0) | 9 (Très difficile) |
| Débutant (1) | 7 (Difficile) |
| Compétent (2+) | Difficulté normale |
### Les dés de totem
Les dés de totem sont spéciaux:
- Ils comptent double en cas de réussite (2 réussites au lieu de 1)
- Ils sont de couleur différente pour les distinguer
- Vous pouvez utiliser les dés de totem humain et adapté simultanément
- Si vous utilisez les deux, vous pouvez choisir quel totem garder après le jet
**Exemple:**
- Pool: 3d10 + 1d10 totem humain
- Résultat: 4, 7, 2, 9 (totem humain)
- Si le seuil est 7: 2 réussites (7 et 9) + 2 réussites supplémentaires pour le 9 du totem = 4 réussites totales
### Relances
Les relances sont disponibles pour les compétences maîtrisées:
- **Niveau 2 (Compétent)**: 1 relance
- **Niveau 3 (Expert)**: 1 relance
- **Niveau 4 (Maître)**: 2 relances
- **Niveau 5 (Légende)**: 2 relances
Pour utiliser une relance:
1. Le MJ ou vous-même pouvez accorder des relances
2. Cliquez sur le dé que vous voulez relancer
3. Le dé sera marqué comme "rerolled"
4. Un nouveau jet sera effectué pour ce dé
**Astuce:** Vous pouvez aussi utiliser votre réserve de Sang-Froid pour obtenir des relances supplémentaires.
---
## Le système de totems
### Sélection du totem
Le totem est choisi lors de la création du personnage et détermine:
- Vos affinités naturelles
- Vos bonus de domaine
- Votre perception du monde
### Gestion des dés de totem
Dans la fiche de personnage, onglet "Totem et ajustements":
- Vous voyez les valeurs de vos totems humain et adapté (0-3 chacun)
- La somme maximale est de 5 (ex: 3 humain + 2 adapté)
- Cliquez sur les flèches pour ajuster les valeurs
**Attention:** La somme des deux totems ne peut pas dépasser 5.
### Domaines de prédilection
Chaque catégorie de compétence peut être votre domaine de prédilection:
- Humain
- Animal
- Outil
- Arme
- Survie
- Monde
Pour définir votre domaine de prédilection:
1. Dans l'onglet "Caractéristiques et compétences"
2. Cliquez sur le bouton radio à côté du nom de la catégorie
3. La catégorie sélectionnée devient votre domaine de prédilection
**Bonus:** Si votre domaine de prédilection est dans les domaines de votre totem, vous obtenez des bonus supplémentaires.
### Totems et PNJ/Créatures
Les PNJ et créatures peuvent aussi avoir des totems, qui influencent leurs caractéristiques et comportements.
---
## La gestion des groupes
### Qu'est-ce qu'un groupe?
Un groupe représente:
- Une communauté de survivants
- Un clan
- Une bande
- Une famille élargie
### Créer un groupe
1. Cliquez sur "Create Actor"
2. Sélectionnez "Group" comme type
3. Donnez un nom au groupe
4. Définissez le totem du groupe
5. Ajoutez des membres
### Ajouter des membres à un groupe
1. Ouvrez la fiche du groupe
2. Dans l'onglet "Membres", cliquez sur "+ Ajouter un membre"
3. Sélectionnez le personnage dans la liste
4. Cliquez sur "Ajouter"
**Synchronisation automatique:** Quand vous ajoutez un personnage à un groupe, le groupe apparaît automatiquement dans la fiche du personnage.
### Gérer les rencontres
Les "rencontres" représentent les PNJ et créatures associés à un groupe:
1. Ouvrez la fiche du groupe
2. Dans l'onglet "Rencontres", cliquez sur "+ Ajouter une rencontre"
3. Sélectionnez le PNJ ou la créature
4. Cliquez sur "Ajouter"
### Retirer un personnage d'un groupe
1. Ouvrez la fiche du groupe
2. Trouvez le membre dans la liste
3. Cliquez sur l'icône de suppression (poubelle)
4. Confirmez
**Synchronisation automatique:** Le personnage sera aussi retiré de la liste des groupes dans sa fiche.
### Mode de jeu
La fiche de personnage a deux modes:
- **Mode Edit**: Tous les champs sont modifiables
- **Mode Jeu**: Les champs sont désactivés pour éviter les modifications accidentelles
Pour basculer entre les modes:
- Cliquez sur l'icône de cadenas en haut de la fiche
---
## Le combat
### Initiative
L'initiative dans Vermine2047 est basée sur:
- Caractéristique: Réflexes
- Compétence: Vigilance
- Statut de combat: Offensif (+), Actif (neutre), Passif (-)
**Formule:** `(Réflexes + Vigilance)d10cs>=difficulté`
**Difficultés par statut:**
- Offensif: 5 (facile)
- Actif: 7 (standard)
- Passif: 9 (difficile)
### Statuts de combat
Chaque participant au combat a un statut:
- **Offensif**: Agressif, prend des risques
- **Actif**: Équilibré, réactif
- **Passif**: Défensif, prudent
Pour changer le statut:
1. Dans le combat tracker
2. Cliquez sur le nom du participant
3. Sélectionnez le nouveau statut
### Tracker de combat
Le tracker de combat affiche:
- L'ordre d'initiative
- Le statut de chaque participant
- Les points de vie
- Les réserves
### Actions de combat
Les actions de combat fonctionnent comme les jets de dés normaux, mais avec:
- Des bonus spécifiques au combat
- Des modifications de difficulté basées sur le statut
---
## Les items
### Types d'items
1. **Objet (item)**: Objet général (nourriture, outils, etc.)
2. **Arme (weapon)**: Arme de mêlée ou à distance
3. **Protection (defense)**: Armure, bouclier, etc.
4. **Véhicule (vehicle)**: Transport
5. **Capacité (ability)**: Compétence spéciale
6. **Spécialité (specialty)**: Spécialisation dans une compétence
7. **Historique (background)**: Historique du personnage
8. **Traumatisme (trauma)**: Traumatisme psychologique
9. **Évolution (evolution)**: Évolution du personnage
10. **Rumeur (rumor)**: Information
11. **Cible (target)**: Objectif
12. **Rite (rite)**: Rituel
### Créer un item
1. Dans la fiche du personnage
2. Allez dans l'onglet approprié (Matériel, Totem et ajustements, etc.)
3. Cliquez sur l'icône "+" à côté du titre de la section
4. Sélectionnez le type d'item
5. Remplissez les informations
### Utiliser un item dans le chat
1. Glissez-déposez l'item depuis votre fiche vers le chat
2. Ou cliquez sur l'icône de l'item et sélectionnez "Post to Chat"
3. Une carte avec les informations de l'item sera affichée
### Caractéristiques des items
**Tous les items:**
- Description
- Rareté (0-5)
- Fiabilité
- Handicap de rareté
- Quantité
- Poids
- Traits
- Dégâts
**Armes:**
- Portée min/max
- Dégâts (valeur, type, bonus de vigueur)
- Munitions
**Protections:**
- Niveau
- Niveau spécifique
- Mobilité
- Bouclier (oui/non)
**Capacités:**
- Type (personnage, groupe, créature, totem)
- Totem
- Niveau
- Seuil d'apprentissage
- Handicap d'apprentissage
- Effets
---
## Gestion des PNJ et Créatures
### Créer un PNJ
1. Cliquez sur "Create Actor"
2. Sélectionnez "NPC" comme type
3. Donnez un nom au PNJ
4. Configurez les attributs:
- Menace (1-4): Niveau de dangerosité
- Expérience (1-4): Niveau d'expérience
- Rôle (1-4): Importance dans le scénario
5. Ajoutez des compétences si nécessaire
6. Ajoutez de l'équipement
### Créer une créature
1. Cliquez sur "Create Actor"
2. Sélectionnez "Creature" comme type
3. Donnez un nom à la créature
4. Configurez les attributs:
- Gabarit (1-4): Type de créature
- Taille (1-3): Taille physique
- Rôle (1-4): Importance dans le scénario
- Meute (0-3): Taille du groupe
- Modes: Types de scénarios où la créature apparaît
5. Ajoutez des compétences si nécessaire
### Menace, Expérience et Rôle (PNJ)
Ces trois attributs déterminent les capacités du PNJ:
**Menace:**
- Mineure (1): Peu dangereuse
- Sérieuse (2): Dangereuse
- Majeure (3): Très dangereuse
- Mortelle (4): Extrêmement dangereuse
**Expérience:**
- Débutant (1): Peu expérimenté
- Compétent (2): Expérimenté
- Expert (3): Très expérimenté
- Maître (4): Maître dans son domaine
**Rôle:**
- Mineur (1): Personnage secondaire
- Secondaire (2): Personnage important
- Important (3): Personnage principal
- Majeur (4): Antagoniste principal
### Gabarit, Taille, Rôle et Meute (Créature)
**Gabarit:**
- Insecte (1): Très petit
- Rat (2): Petit
- Chien (3): Moyen
- Ours (4): Grand
**Taille:**
- Petit (1)
- Moyen (2)
- Grand (3)
**Rôle:**
- Mineur (1): Créature secondaire
- Secondaire (2): Créature importante
- Important (3): Créature principale
- Majeur (4): Boss
**Meute:**
- Solitaire (0): Agit seul
- Petit groupe (1)
- Groupe (2)
- Grande meute (3)
**Modes:**
- Survie: Agit dans des scénarios de survie
- Cauchemar: Agit dans des scénarios de cauchemar
- Apocalypse: Agit dans des scénarios d'apocalypse
---
## Astuces et bonnes pratiques
### Pour les Joueurs
1. **Choisissez un totem qui correspond à votre style de jeu**
- Humain: Pour les sociaux et les civils
- Prédateur: Pour les chasseurs et les guerriers
- Adapté: Pour les polyvalents
2. **Définissez un domaine de prédilection**
- Cela vous donnera des bonus avec votre totem
3. **Utilisez les spécialités**
- Les spécialités donnent +1D et des réussites automatiques
4. **Gérez vos réserves**
- Effort: Pour les actions physiques
- Sang-Froid: Pour les actions mentales et les relances
5. **Utilisez les dés de totem**
- Ils comptent double en cas de réussite
- Vous pouvez utiliser les deux totems simultanément
### Pour les MJ
1. **Créez des groupes pour organiser vos PNJ**
- Les groupes permettent de gérer plusieurs PNJ ensemble
- Les rencontres dans un groupe sont synchronisées
2. **Utilisez les statuts de combat**
- Offensif pour les personnages agressifs
- Actif pour les personnages équilibrés
- Passif pour les personnages défensifs
3. **Configurez correctement les PNJ et créatures**
- Menace/Expérience/Rôle pour les PNJ
- Gabarit/Taille/Rôle/Meute pour les créatures
4. **Utilisez les modes pour les créatures**
- Cela permet de filtrer les créatures par type de scénario
5. **Encouragez l'utilisation des domaines de prédilection**
- Cela rend le système de totems plus impactant
### Pour les développeurs
1. **Utilisez les helpers Handlebars**
- De nombreux helpers sont disponibles pour afficher les données
- `skillLevel`, `threatLevel`, etc.
2. **Respectez les conventions de nommage**
- `vermine-` préfixe pour les classes CSS
- `VERMINE` namespace pour les configurations
3. **Utilisez GroupLink pour la synchronisation**
- Ne modifiez pas directement les tableaux de membres/rencontres
- Utilisez les méthodes de GroupLink
---
## Résolution des problèmes
### Problèmes courants
1. **Les dés de totem ne fonctionnent pas**
- Vérifiez que les valeurs des totems sont > 0
- Vérifiez que la somme des totems ne dépasse pas 5
2. **Les bonus de domaine ne s'appliquent pas**
- Vérifiez que vous avez défini un domaine de prédilection
- Vérifiez que votre totem a des domaines configurés
3. **Les groupes ne se synchronisent pas**
- Vérifiez que GroupLink est bien initialisé
- Vérifiez que les hooks sont actifs
4. **Les templates ne s'affichent pas correctement**
- Vérifiez que les templates sont en `.hbs`
- Vérifiez que les références sont correctes
### Contact
Pour de l'aide ou pour signaler un problème:
- Rejoignez le Discord Vermine: https://discord.gg/qejqmSxr
- Rejoignez le Discord Foundry Vermine: https://discord.gg/FqGHYvXg
---
## Licence
Vermine2047 System est sous licence MIT. Voir le fichier LICENSE.txt pour plus de détails.
---
## Auteurs
- François-Xavier Guillois
- Rwanoux (Discord: rwanoux)
- Pretre (Discord: pretre)
---
*Guide mis à jour: 2026-06-04*
*Version: 0.1.14*
+63 -20
View File
@@ -1,40 +1,83 @@
'use strict';
const gulp = require('gulp');
const sass = require('gulp-sass')(require('sass'));
const less = require('gulp-less');
const autoprefixer = require('gulp-autoprefixer');
const cleanCSS = require('gulp-clean-css');
const rename = require('gulp-rename');
var browserSync = require('browser-sync').create();
function buildStyles() {
return gulp.src('./scss/vermine2047.scss')
.pipe(sass({ outputStyle: 'compressed' }).on('error', sass.logError))
// ============================================
// LESS Tasks
// ============================================
function buildLess() {
return gulp.src('./less/vermine2047.less')
.pipe(less().on('error', function(err) {
console.error('LESS compilation error:', err.message);
this.emit('end');
}))
.pipe(autoprefixer({
overrideBrowserslist: ['> 1%', 'last 2 versions', 'Firefox ESR'],
cascade: false
}))
.pipe(cleanCSS({ compatibility: 'ie11' }))
.pipe(rename({ suffix: '.min' }))
.pipe(gulp.dest('./css'))
.pipe(browserSync.stream());
}
// Build LESS without minification (for debugging)
function buildLessDev() {
return gulp.src('./less/vermine2047.less')
.pipe(less().on('error', function(err) {
console.error('LESS compilation error:', err.message);
this.emit('end');
}))
.pipe(autoprefixer({
overrideBrowserslist: ['> 1%', 'last 2 versions', 'Firefox ESR'],
cascade: false
}))
.pipe(rename({ suffix: '.dev' }))
.pipe(gulp.dest('./css'))
.pipe(browserSync.stream());
}
function watchLess() {
gulp.watch(['./less/**/*.less', './less/*.less'], buildLess);
}
};
function reloadTemplatesHTML() {
return browserSync.reload("templates/**/*.html")
return browserSync.reload("templates/**/*.html");
}
function reloadTemplatesHBS() {
return browserSync.reload("templates/**/*.hbs")
return browserSync.reload("templates/**/*.hbs");
}
exports.buildStyles = buildStyles;
exports.watch = function () {
browserSync.init(
{
server: false,
proxy: {
target: "https://localhost:30000/",
ws: true,
}
// ============================================
// Exports
// ============================================
exports.buildLess = buildLess;
exports.buildLessDev = buildLessDev;
exports.buildCSS = gulp.series(buildLess);
exports.buildAllCSS = gulp.series(buildLess, buildLessDev);
exports.watch = function () {
browserSync.init({
server: false,
proxy: {
target: "https://localhost:30000/",
ws: true,
}
);
});
// Watch templates
gulp.watch("./templates/**/*.html").on('change', reloadTemplatesHTML);
gulp.watch("./templates/**/*.hbs").on('change', reloadTemplatesHBS);
gulp.watch(['./scss/**/*.scss', './scss/*.scss'], buildStyles);
// Watch LESS files
gulp.watch(['./less/**/*.less', './less/*.less'], buildLess);
};
exports.default = buildLess;
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 834 KiB

+161 -6
View File
@@ -13,6 +13,22 @@
},
"VERMINE.WorldSettings.GameMode.Name": "Game Mode Selection",
"VERMINE.WorldSettings.GameMode.Hint": "Just like some video games offer different modes, Vermine 2047 lets players choose their Game Mode and set the degree of realism, supernatural and dangerosity of the universe themselves.",
"TYPES.Item.item": "Item",
"TYPES.Item.weapon": "Weapon",
"TYPES.Item.defense": "Defense",
"TYPES.Item.vehicle": "Vehicle",
"TYPES.Item.ability": "Ability",
"TYPES.Item.specialty": "Specialty",
"TYPES.Item.background": "Background",
"TYPES.Item.trauma": "Trauma",
"TYPES.Item.evolution": "Adaptation",
"TYPES.Item.rumor": "Rumor",
"TYPES.Item.target": "Target",
"TYPES.Item.rite": "Rite",
"TYPES.Actor.character": "Character",
"TYPES.Actor.npc": "NPC",
"TYPES.Actor.group": "Group",
"TYPES.Actor.creature": "Creature",
"GAME_MODES": {
"heroic": {
"name": "Heroic"
@@ -22,7 +38,10 @@
},
"legendary": {
"name": "Legendary"
}
},
"survival": "Survival",
"nightmare": "Nightmare",
"apocalypse": "Apocalypse"
},
"ROLLS": {
"tool": "Dice Roller",
@@ -86,7 +105,8 @@
"relations": "Relations",
"morale": "Morale",
"morale_level": "Morale level",
"notes": "Notes"
"notes": "Notes",
"biography": "Biography"
},
"ADVERSITY": {
"threat": "Threat",
@@ -108,7 +128,11 @@
"skills": "Skills",
"pattern": "Pattern",
"size": "Size",
"pack": "Group"
"pack": "Group",
"gear_hindrance": "Gear Hindrance",
"skills_placeholder": "Skills Placeholder",
"threat_details": "Threat Details",
"role_details": "Role Details"
},
"VERMINE": {
"name": "Vermine",
@@ -116,6 +140,9 @@
"level": "Level",
"experience": "Experience",
"reputation": "Reputation",
"sheet": {
"description": "Description"
},
"pool": "Reserve",
"pools": "Reserves",
"self_control": "Self-control",
@@ -180,7 +207,106 @@
"encounters": "Encounters",
"road": "The Road",
"totem_picker": "Totem picker",
"actor_picker": "Actor picker"
"actor_picker": "Actor picker",
"ability_category": {
"physical": "Physical",
"manual": "Manual",
"mental": "Mental",
"social": "Social"
},
"skill_category": {
"man": "Man",
"animal": "Animal",
"tool": "Tool",
"weapon": "Weapon",
"survival": "Survival",
"world": "World"
},
"CharacterNamePlaceholder": "Character Name",
"none": "None",
"bonuses": "Bonuses",
"choose_ability": "Choose Ability",
"choose_skill": "Choose Skill",
"grant_reroll": "Grant Reroll",
"handicap": "Handicap",
"keep_totem": "Keep Totem",
"morale_crisis": "Morale Crisis",
"morale_high": "High Morale",
"morale_low": "Low Morale",
"morale_normal": "Normal Morale",
"rerolls_possible": "Rerolls Possible",
"reserve": "Reserve",
"score": "Score",
"success_count": "Success Count",
"success_required": "Success Required",
"test_of": "Test of",
"total": "Total",
"totem_dice": "Totem Dice",
"totem_hint": "Totem Hint",
"FightTool": "Fight Tool",
"RollTool": "Roll Tool",
"Roll4Fight": "Roll for Fight",
"Selected": "Selected",
"PurposeTrait": "Purpose Trait",
"SpleenTrait": "Spleen Trait",
"ConfrontationHint": "Confrontation Hint",
"Achievement": "Achievement",
"Conservation": "Conservation",
"error_cannot_reroll": "Cannot reroll",
"error_no_rerolls_left": "No rerolls left",
"error_no_actor_selected": "No actor selected",
"error_not_enough_self_control": "Not enough self control",
"error_select_ability": "Select an ability",
"tabs": {
"abilities": "Abilities",
"combat": "Combat",
"equipment": "Equipment",
"stories": "Stories",
"totem": "Totem"
},
"base_values": "Base Values",
"computed_values": "Computed Values",
"cost": "Cost",
"creature": "Creature",
"effect": "Effect",
"error_select_skill": "Select a skill",
"error_unknown_actor": "Unknown actor",
"instincts": "Instincts",
"instincts_placeholder": "eg: Aggressive, Defensive",
"learn": "Learn",
"major_objectives": "Major Objectives",
"minor_objectives": "Minor Objectives",
"morale": "Morale",
"needed": "Needed",
"objective_placeholder": "eg: Survive, Explore",
"objectives": "Objectives",
"pack": "Pack",
"pattern": "Pattern",
"preferred_category": "Preferred Category",
"prohibits": "Prohibits",
"prohibits_placeholder": "eg: Fire, Water",
"rarity_0": "Common",
"rarity_1": "Rare",
"rarity_2": "Very Rare",
"reserves": "Reserves",
"ritual": "Ritual",
"sexes": {
"female": "Female",
"male": "Male"
},
"size": "Size",
"total_attack": "Total Attack",
"total_damage": "Total Damage",
"trance": "Trance",
"vote_reserve": "Vote Reserve",
"wound_thresholds": "Wound Thresholds",
"types": {
"shield": "Shield"
},
"roll": "Roll",
"cancel": "Cancel",
"playMode": "Play Mode",
"editMode": "Edit Mode"
},
"UI": {
"add": "Add",
@@ -195,9 +321,15 @@
"temporary": "Temporary effects",
"passive": "Passive effects",
"inactive": "Inactive effects"
}
},
"effects.inactive": "Inactive",
"effects.passive": "Passive",
"effects.temporary": "Temporary"
},
"ITEMS": {
"item": "Item",
"items": "Items",
"new_item": "New item",
"defense": "Protection",
"defenses": "Protections",
"new_defense": "New protection",
@@ -216,6 +348,12 @@
"abilities": "Abilities",
"specialties": "Specialties",
"new_specialty": "New specialty",
"target": "Target",
"targets": "Targets",
"new_target": "New target",
"rite": "Rite",
"rites": "Rites",
"new_rite": "New rite",
"evolution": "Adaptation",
"new_evolution": "New adaptation",
"evolutions": "Adaptations",
@@ -227,7 +365,8 @@
"rituel": "Ritual",
"transe": "Trance",
"effects": "Effects",
"details": "Details"
"details": "Details",
"shield": "Shield"
},
"ABILITIES": {
"vigor": {
@@ -498,10 +637,26 @@
},
"ruins": {
"name": "Ruins"
},
"athletics": {
"name": "Athletics"
},
"fauna": {
"name": "Fauna"
}
},
"SEXES": {
"male": "Masculine",
"female": "Feminine"
},
"COMBAT": {
"Encounter": "Encounter",
"None": "None",
"Round": "Round"
},
"SIZE_LEVELS": {
"small": "Small",
"medium": "Medium",
"large": "Large"
}
}
+202 -33
View File
@@ -1,4 +1,26 @@
{
"NAME": "Nom",
"NONE": "Aucun",
"ORIGIN": "Origine",
"PROFILE": "Profil",
"THEME": "Concept",
"TOTEM": "Totem",
"TYPES.Item.item": "Objet",
"TYPES.Item.weapon": "Arme",
"TYPES.Item.defense": "Protection",
"TYPES.Item.vehicle": "Véhicule",
"TYPES.Item.ability": "Capacité",
"TYPES.Item.specialty": "Spécialité",
"TYPES.Item.background": "Historique",
"TYPES.Item.trauma": "Traumatisme",
"TYPES.Item.evolution": "Adaptation",
"TYPES.Item.rumor": "Rumeur",
"TYPES.Item.target": "Cible",
"TYPES.Item.rite": "Rite",
"TYPES.Actor.character": "Personnage",
"TYPES.Actor.npc": "PNJ",
"TYPES.Actor.group": "Groupe",
"TYPES.Actor.creature": "Créature",
"SETTINGS": {
"world": {
"game_mode": {
@@ -22,7 +44,10 @@
},
"legendary": {
"name": "Légendaire"
}
},
"survival": "Survie",
"nightmare": "Cauchemar",
"apocalypse": "Apocalypse"
},
"ROLLS": {
"tool": "Lanceur de dés",
@@ -71,6 +96,7 @@
"old": "Vieux"
},
"IDENTITY": {
"biography": "Biographie",
"name": "Nom",
"height": "Taille",
"weight": "Poids",
@@ -98,17 +124,21 @@
"vigor": "Vigueur",
"wounds": "Blessures",
"action": "Action",
"specialties": "Spécialité",
"specialties": "Spécialités",
"rerolls": "Relances",
"contact": "Contact",
"reaction": "Réaction",
"pools": "Réserves",
"gear": "Matériel",
"gear_hindrance": "Handicap Matériel",
"protection": "Protection",
"skills": "Compétences",
"skills_placeholder": "ex: Armes à feu, Soins, Discrétion",
"pattern": "Gabarit",
"size": "Taille",
"pack": "Groupe"
"pack": "Groupe",
"threat_details": "Détails de Menace",
"role_details": "Détails de Rôle"
},
"VERMINE": {
"name": "Vermine",
@@ -116,14 +146,67 @@
"level": "Niveau",
"experience": "Expérience",
"reputation": "Réputation",
"sheet": {
"description": "Description"
},
"pool": "Réserve",
"pools": "Réserves",
"reserves": "Réserves",
"self_control": "Sang-Froid",
"effort": "Effort",
"preferred_category": "Catégorie préférée",
"wounds": {
"name": "Blessures",
"threshold": "Seuil",
"light": "Légère",
"heavy": "Grave",
"deadly": "Mortelle",
"light_wounds": "Blessure légère",
"heavy_wounds": "Blessure grave",
"deadly_wounds": "Blessure mortelle"
},
"ability_category": {
"physical": "Physiques",
"manual": "Manuelles",
"mental": "Mentales",
"social": "Sociales"
},
"skill_category": {
"man": "Homme",
"animal": "Animal",
"tool": "Outil",
"weapon": "Arme",
"survival": "Survie",
"world": "Monde"
},
"rarity_0": "Commun",
"rarity_1": "Peu commun",
"rarity_2": "Rare",
"rarity_3": "Très rare",
"test_of": "test de",
"rerolls_possible": "relances possibles",
"grant_reroll": "accorder des relances",
"success_count": "nombre de succès",
"success_required": "succès requis",
"error_no_actor_selected": "Vous n'avez pas de personnage attitré ou de token sélectionné",
"error_cannot_reroll": "Vous ne pouvez pas relancer un dés sur ce jet",
"error_no_rerolls_left": "Plus de relances possibles",
"error_select_ability": "Veuillez sélectionner une caractéristique",
"error_not_enough_self_control": "Vous n'avez pas assez de Sang-Froid",
"FightTool": "Outil de combat",
"Roll4Fight": "Lancer pour le combat",
"Selected": "Sélectionné",
"Achievement": "Accomplissement",
"Conservation": "Conservation",
"ConfrontationHint": "Résolvez la confrontation en sélectionnant des dés",
"PurposeTrait": "Trait de but",
"SpleenTrait": "Trait de rate",
"group": "Groupe",
"abilities": "Caractéristiques",
"ability": "Caractéristique",
"skills": "Compétences",
"skills_title": "Compétences",
"skill": "Compétence",
"skill_title": "Compétence",
"skill_mastery": "Maîtrise",
"bonus": "Bonus",
@@ -131,6 +214,8 @@
"reroll": "Relance",
"equipment": "Equipement",
"specialty": "Spécialité",
"specificLevel": "Niveau spécifique",
"competence": "Compétence",
"technique": "Technique",
"techniques": "Techniques",
"difficulty": "Difficulté",
@@ -149,15 +234,25 @@
"rarity_sm": "Rar.",
"reliability": "Fiabilité",
"reliability_sm": "Fiab.",
"wounds": {
"name": "Blessures",
"threshold": "Seuil",
"light": "Légère",
"heavy": "Grave",
"deadly": "Mortelle",
"light_wounds": "Blessure légère",
"heavy_wounds": "Blessure grave",
"deadly_wounds": "Blessure mortelle"
"choose_ability": "Choisissez une caractéristique",
"choose_skill": "Choisissez une compétence",
"none": "Aucun",
"total": "Total",
"bonuses": "Bonus",
"handicap": "Handicap",
"score": "Score",
"totem_dice": "Dés de totem",
"keep_totem": "Garder le totem",
"totem_hint": "Cochez pour utiliser les dés de totem (double réussite possible)",
"error_select_skill": "Veuillez sélectionner une compétence",
"needed": "nécessaire",
"cost": "Coût",
"learn": "Apprentissage",
"ritual": "Rituel",
"trance": "Transe",
"effect": "Effet",
"types": {
"shield": "Bouclier"
},
"tabs": {
"abilities": "Caractéristiques et compétences",
@@ -170,11 +265,15 @@
"ammo_sm": "Mun",
"trait": "Trait",
"traits": "Traits",
"traits_selector": "Sélecteur de traits",
"clew": "Indice",
"combat": "Combat",
"stories": "Histoires",
"gear": "Matériel",
"information": "Informations",
"editMode": "Mode édition",
"playMode": "Mode jeu",
"modes": "Modes de jeu",
"boost": "boost",
"group_abilities": "Capacités de groupe",
"totem_abilities": "Capacités de totem",
@@ -186,8 +285,48 @@
"group_members": "Membres",
"encounters": "Rencontres",
"road": "La Route",
"reserve": "Réserve",
"totem_picker": "Sélecteur de totem",
"actor_picker": "Sélecteur de personnage"
"actor_picker": "Sélecteur de personnage",
"morale": "Moral",
"reserve": "Réserve de groupe",
"objectives": "Objectifs",
"major_objectives": "Objectifs majeurs",
"minor_objectives": "Objectifs mineurs",
"instincts": "Instincts",
"prohibits": "Interdictions",
"vote_reserve": "Vote pour utiliser la réserve",
"morale_high": "Haut",
"morale_normal": "Normal",
"morale_low": "Bas",
"morale_crisis": "Crise",
"objective_placeholder": "Objectif...",
"instincts_placeholder": "Ex: Triompher, relever un défi...",
"prohibits_placeholder": "Ex: Fuir, abandonner...",
"error_unknown_actor": "Acteur inconnu",
"creature": "Créature",
"pattern": "Gabarit",
"size": "Taille",
"pack": "Meute",
"computed_values": "Valeurs calculées",
"base_values": "Valeurs de base",
"total_attack": "Attaque totale",
"total_damage": "Dégâts totaux",
"wound_thresholds": "Seuils de blessures",
"CharacterNamePlaceholder": "Nom du personnage",
"sexes": {
"female": "Féminin",
"male": "Masculin"
},
"RollTool": "Outil de lancer de dés",
"roll": "Lancer",
"cancel": "Annuler",
"Sheet": {
"character": "Personnage",
"npc": "PNJ",
"group": "Groupe",
"creature": "Créature"
}
},
"UI": {
"add": "Ajouter",
@@ -205,6 +344,9 @@
}
},
"ITEMS": {
"item": "Objet",
"items": "Objets",
"new_item": "Nouvel objet",
"defense": "Protection",
"defenses": "Protections",
"new_defense": "Nouvelle protection",
@@ -223,6 +365,13 @@
"abilities": "Capacités",
"specialties": "Spécialités",
"new_specialty": "Nouvelle spécialité",
"target": "Cible",
"targets": "Cibles",
"new_target": "Nouvelle cible",
"rite": "Rite",
"rites": "Rites",
"new_rite": "Nouveau rite",
"shield": "Bouclier",
"evolution": "Adaptation",
"new_evolution": "Nouvelle adaptation",
"evolutions": "Adaptations",
@@ -282,6 +431,11 @@
"important": "Important",
"major": "Majeur"
},
"SIZE_LEVELS": {
"small": "Petit",
"medium": "Moyen",
"large": "Grand"
},
"PATTERN_LEVELS": {
"insect": "Insecte",
"rat": "Rat",
@@ -417,7 +571,7 @@
"name": "Arts"
},
"civilization": {
"name": "Civilisations"
"name": "Civilisation"
},
"psychology": {
"name": "Psychologie"
@@ -434,14 +588,14 @@
"dissection": {
"name": "Dissection"
},
"wildlife": {
"fauna": {
"name": "Faune"
},
"repulsion": {
"name": "Répulsion"
},
"tracks": {
"name": "Traces"
"name": "Pistage"
},
"crafting": {
"name": "Artisanat"
@@ -461,42 +615,39 @@
"firearms": {
"name": "Armes à feu"
},
"archery": {
"name": "Tir à l'arc"
},
"armory": {
"name": "Armurerie"
},
"shield": {
"name": "Bouclier"
},
"close": {
"name": "Corps-à-corps"
},
"archery": {
"name": "Armes de tir"
},
"throwing": {
"name": "Lancer"
},
"melee": {
"name": "Mêlée"
},
"atletics": {
"name": "Atlétisme"
},
"stealth": {
"name": "Discrétion"
},
"alertness": {
"name": "Vigilance"
},
"flora": {
"name": "Flore"
"athletics": {
"name": "Athlétisme"
},
"food": {
"name": "Alimentation"
},
"stealth": {
"name": "Discrétion"
},
"close": {
"name": "Corps-à-corps"
},
"environment": {
"name": "Environnement"
},
"flora": {
"name": "Flore"
},
"road": {
"name": "Route"
},
@@ -505,10 +656,28 @@
},
"ruins": {
"name": "Vestiges"
},
"shield": {
"name": "Bouclier"
},
"wildlife": {
"name": "Faune"
},
"atletics": {
"name": "Athlétisme"
}
},
"SEXES": {
"male": "Masculin",
"female": "Féminin"
},
"COMBAT": {
"Begin": "Commencer",
"Encounter": "Rencontre",
"End": "Terminer",
"None": "Aucun",
"NotStarted": "Non commencé",
"Round": "Tour",
"TurnEnd": "Fin de tour"
}
}
+645
View File
@@ -0,0 +1,645 @@
@import "totem";
.system-vermine2047 .vermine2047.actor {
// ── Fix contrast: dark text on light parchment bg ───────────────────
.window-content,
& {
color: @theme-color-dark;
label, span, p, li, a:not(.active) {
color: @theme-color-dark;
}
h4, h5 {
color: @color-text-dark-header;
}
h3 {
color: @theme-color-light;
}
input:not([type="radio"]):not([type="checkbox"]),
select, textarea {
color: @color-text-light-0;
line-height: 1.2;
padding: 1px 4px;
}
.char-header {
h1 label {
font-size: 0.9rem;
}
.char-name-value {
font-size: 1.5rem;
}
input:not([type="radio"]):not([type="checkbox"]) {
font-size: 1rem;
}
}
.ability input.hexa {
font-size: 0.8rem;
}
.resource-label {
color: @color-text-dark-secondary;
}
.ability label {
color: @color-text-dark-header;
}
.char-header {
color: @theme-color-dark;
label {
color: @color-text-dark-header;
}
}
.hexa {
color: @color-text-dark-primary;
}
.sheet-tabs a {
color: @color-text-dark-inactive;
&.active {
color: @theme-color-dark;
}
}
}
// ── Two-column layout for character sheets ──────────────────────────
&.character {
display: grid !important;
grid-template-columns: 1fr !important;
grid-template-rows: auto 1fr auto !important;
> header.window-header {
grid-column: 1;
grid-row: 1;
}
> section.window-content {
grid-column: 1;
grid-row: 2;
}
> div.window-resize-handle {
grid-column: 1;
grid-row: 3;
}
}
&.character .window-content {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: auto 1fr;
overflow: hidden;
> .tab.main {
grid-column: 1;
grid-row: 1 / -1;
display: flex !important;
flex-direction: column;
overflow-y: auto;
padding: 8px;
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
> nav.tabs {
grid-column: 2;
grid-row: 1;
}
> .tab:not(.main) {
grid-column: 2;
grid-row: 2;
display: none;
overflow-y: auto;
padding: 8px;
&.active {
display: flex;
flex-direction: column;
}
}
}
// ── Stacked layout for NPC sheets (header + tabs + content) ────────
&.npc {
display: grid !important;
grid-template-columns: 1fr !important;
grid-template-rows: auto 1fr auto !important;
> header.window-header {
grid-column: 1;
grid-row: 1;
}
> section.window-content {
grid-column: 1;
grid-row: 2;
}
> div.window-resize-handle {
grid-column: 1;
grid-row: 3;
}
}
&.npc .window-content {
display: grid;
grid-template-rows: auto auto 1fr;
grid-template-columns: 1fr;
overflow: hidden;
> .tab.main {
grid-column: 1;
grid-row: 1;
display: flex !important;
flex-direction: column;
overflow-y: auto;
padding: 8px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
> nav.tabs {
grid-column: 1;
grid-row: 2;
}
> .tab:not(.main) {
grid-column: 1;
grid-row: 3;
display: none;
overflow-y: auto;
padding: 8px;
&.active {
display: flex;
flex-direction: column;
}
}
}
form {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
> .tab {
flex: 1;
overflow-y: auto;
padding: 4px;
}
.charname input {
color: @theme-color-dark;
font-family: "DistressBlack", sans-serif;
font-size: 30px;
font-style: normal;
}
div.hexa {
.hexa-style();
.transition();
margin: 0.2rem;
position: relative;
&:hover {
.hexa-style(rgba(255, 255, 255, 0.425), rgba(0, 0, 0, 0.288));
}
input {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
z-index: 1;
margin: 0;
cursor: pointer;
}
&.checked {
.hexa-style(rgb(0, 0, 0), rgba(0, 0, 0, 0.288));
&:hover { .hexa-style(rgb(43, 43, 43), rgba(0, 0, 0, 0.288)); }
}
&.unavailable {
background: radial-gradient(circle, rgba(66, 15, 15, 0.664) 0%, rgba(131, 70, 70, 0.432) 100%);
pointer-events: none;
}
}
input[type="text"],
input[type="number"] {
width: calc(100% - 2px);
height: calc(100% - 2px);
background: none;
padding: 0;
margin: 1px 0;
color: #333;
border: 1px solid rgba(0, 0, 0, 0);
&.hexa { .hexa-style(); }
}
input[disabled],
select[disabled] {
cursor: not-allowed;
}
input[type="text"]:hover:not(:disabled),
input[type="text"]:focus,
select:hover:not(:disabled),
select:focus,
input[type="number"]:hover:not(:disabled),
input[type="number"]:focus,
textarea:hover:not(:disabled),
textarea:focus {
box-shadow: 0 0 10px @theme-color-highlight inset;
}
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;
}
label {
display: block;
}
.mce-panel span {
display: inherit;
}
.rollable:hover,
a:hover {
color: #000;
text-shadow: 0 5px 5px @theme-color-accent;
cursor: pointer;
}
.chooseTotem {
font-family: "DistressBlack", sans-serif;
font-size: 0.9rem;
cursor: pointer;
&:hover {
text-shadow: 0 0 10px @theme-color-highlight;
}
}
}
.sheet-header-toggle {
text-align: right;
margin-bottom: 0.5rem;
button {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
padding: 0.2rem 1rem;
cursor: pointer;
.background-image(url("@{ui-path}/scotch.webp"), no-repeat, cover, 50% 0%);
border: none;
box-shadow: 0px 0px 3px rgba(31, 26, 26, 0.979) inset;
color: @theme-color-dark;
&:hover {
box-shadow: 0 0 10px @theme-color-highlight inset;
}
}
}
.image-wrapper {
text-align: center;
margin-bottom: 1rem;
img {
width: 80%;
height: auto;
}
}
.padding-with-frieze {
margin-left: 18% !important;
margin-right: 10% !important;
li {
max-width: 100%;
}
.major-totem {
position: relative;
h4 {
position: absolute;
transform: rotate(-8deg);
opacity: 1;
transition: 0.2s;
}
}
.paper {
margin-top: 1rem;
height: 350px;
}
.second-paper {
margin-top: 4rem;
height: 150px;
}
}
.char-header {
font-family: "DistressBlack", sans-serif;
section {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
}
h1.char-name,
.char-vermine2047 {
border-bottom: none;
line-height: 2rem;
}
.char-vermine2047 {
font-size: 1.5rem;
}
}
h3 {
font-family: "DistressBlack", sans-serif;
text-align: center;
text-transform: uppercase;
color: @theme-color-light;
font-size: 1.7rem;
border-bottom: none;
margin: 0;
}
h4 {
font-family: "DistressBlack", sans-serif;
font-size: 1.4em;
text-transform: uppercase;
margin: 0 0 0.2rem;
&.characteristics {
font-size: 1.25rem;
margin-top: 0.1rem;
}
.tab.totem &,
.tab.equipment &,
.tab.stories & {
margin-top: 0.875rem;
}
}
nav.tabs[data-group="sheet"] {
display: inline-flex;
justify-content: space-around;
align-items: center;
height: 54px;
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, auto, right top);
background-size: 100% 100%;
width: 100%;
position: relative;
padding-right: 4rem;
font-size: 1.4rem;
.item {
height: 2.4rem;
display: inline-flex;
align-items: center;
gap: 0.3rem;
z-index: 1;
transition: all 0.1s ease-out;
color: #606060;
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.404);
&:hover,
&.active {
color: #000;
text-shadow: 0 5px 5px @theme-color-accent;
cursor: pointer;
}
&:hover {
text-shadow: 0 5px 5px rgba(30, 82, 37, 0.6039215686);
}
}
}
.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-image(radial-gradient(circle, @dice-pool-color 25%, rgb(0, 0, 0) 100%), @dice-pool-color);
max-width: 0.7rem;
aspect-ratio: 1/1;
border-radius: 50%;
}
&.dice-reroll-dot {
background: radial-gradient(circle, @dice-reroll-color 25%, rgb(0, 0, 0) 100%);
}
}
}
}
.skill-category {
padding: 0.3rem;
&.preferred {
box-shadow: 0px 0px 30px rgba(0, 128, 0, 0.306) inset;
h4, label {
text-shadow: 0px 0px 5px rgba(0, 128, 0, 0.411);
}
}
}
#edit {
background-color: #000;
color: #fff;
width: 100%;
}
.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;
}
}
.items-list {
.item-name .item-image {
flex: 0 0 30px;
height: 30px;
background-size: 30px;
border: none;
margin-right: 5px;
}
}
.items-list .item.flexrow > div:not(.item-name):not(.item-controls) {
text-align: center;
}
.list-item {
list-style: none;
margin: 0;
padding: 0;
.item {
align-items: center;
padding: 2px;
border-bottom: 1px solid rgba(170, 170, 152, 0.664);
&:last-child { border-bottom: none; }
}
}
// ── V2 tab navigation — chitin-plate style ─────────────────────────
nav.sheet-tabs[data-application-part="tabs"] {
display: flex;
align-items: stretch;
gap: 0;
margin: 0;
padding: 0;
background: @color-chitin-dark;
border-bottom: 2px solid @color-amber;
min-height: 36px;
> a {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 6px;
padding: 6px 16px;
color: @color-text-light-0;
opacity: 1;
transition: background 0.15s;
position: relative;
border-right: 1px solid fade(@color-membrane, 12%);
&:hover {
background: fade(@color-honeycomb, 10%);
text-shadow: none;
}
&.active {
background: linear-gradient(180deg, @color-amber 0%, @color-honeycomb 100%);
color: @color-chitin-dark;
&::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background: @color-acid-green;
}
}
> i {
font-size: @font-size-14;
width: 16px;
text-align: center;
}
> span, > i {
color: inherit;
}
}
}
}
@container ability-row (max-width: 240px) {
.skill-dots,
span.hexa {
display: none;
}
}
+119
View File
@@ -0,0 +1,119 @@
@import "../variables";
@import "../utilities";
.system-vermine2047 .vermine2047.actor.creature {
.sheet-header {
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
padding: 10px;
max-height: 110px;
}
.header-fields { flex: 1; }
.resources { margin-bottom: 10px; }
.resource {
.card-style();
}
.resource-label {
font-weight: bold;
margin-right: 8px;
min-width: 60px;
font-size: @font-size-12;
}
.resource-content {
display: flex;
align-items: center;
select {
margin-right: 8px;
min-width: 80px;
}
}
.charname {
margin: 0;
input {
width: 100%;
font-size: @font-size-18;
font-weight: bold;
text-align: center;
}
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.stat {
.card-style();
display: flex;
justify-content: space-between;
label { font-weight: bold; }
span {
font-weight: bold;
color: @theme-color-light;
}
}
.mdb {
&:not(.row) {
.card-style();
}
h4:first-child {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-14;
margin-top: 0;
border-bottom: 1px solid @color-border-dark-3;
padding-bottom: 5px;
text-align: center;
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
}
ul.unstyled {
margin: 0;
padding: 0;
list-style: none;
}
li {
padding: 3px 0;
border-bottom: 1px solid @color-border-dark-3;
&:last-child { border-bottom: none; }
}
}
input[type="text"] {
width: 100%;
padding: 5px;
margin-bottom: 8px;
}
.grid-3col {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
label {
display: flex;
align-items: center;
gap: 5px;
padding: 5px;
}
}
.row.mdb {
display: flex;
align-items: center;
gap: 8px;
}
}
+99
View File
@@ -0,0 +1,99 @@
@import "../variables";
@import "../utilities";
.system-vermine2047 .vermine2047.actor.group {
.char-header {
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
max-height: 110px;
}
.char-details {
h1.char-name input,
.char-vermine2047 a.chooseTotem {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
}
}
h4 {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-14;
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
}
.reserve-control,
.morale-control {
.card-style();
label {
font-weight: bold;
margin-right: 8px;
min-width: 80px;
}
input, select {
margin-right: 8px;
min-width: 50px;
text-align: center;
}
}
.actor-list {
list-style: none;
padding: 0;
margin: 0;
li.actor {
padding: 5px;
border-bottom: 1px solid @color-border-dark-3;
&:last-child { border-bottom: none; }
&:hover {
box-shadow: 0 0 10px @theme-color-highlight inset;
}
}
.actor-name, .item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.objective-item {
padding: 5px;
border-bottom: 1px solid @color-border-dark-4;
input {
flex: 1;
width: 100%;
}
}
.totem-display {
.card-style();
span { font-weight: bold; }
}
.identity-field {
margin-bottom: 8px;
label {
font-weight: bold;
display: block;
margin-bottom: 3px;
}
input { width: 100%; }
}
.level input,
.reputation input {
width: 60px;
text-align: center;
}
}
+236
View File
@@ -0,0 +1,236 @@
@import "../variables";
@import "../utilities";
.system-vermine2047 .vermine2047.actor.npc {
.sheet-header {
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
padding: 0.5rem;
max-height: 150px;
}
.ability-value,
.resource-value {
min-width: 30px;
text-align: center;
font-weight: bold;
margin-left: 8px;
}
.card, .npc-card {
.card-style();
h4 {
.card-title();
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-14;
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
i { margin-right: 5px; }
}
}
.skills-container {
display: flex;
flex-direction: column;
gap: 15px;
}
.skill-category {
.card-style();
padding: 10px;
h4 {
font-family: "DistressBlack", sans-serif;
font-size: @font-size-14;
text-transform: uppercase;
margin-top: 0;
margin-bottom: 8px;
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
}
}
.skill-item {
display: flex;
align-items: center;
gap: 8px;
padding: 5px;
border-bottom: 1px solid @color-border-dark-3;
&:last-child { border-bottom: none; }
label {
flex: 1;
font-size: @font-size-12;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&:hover {
box-shadow: 0 0 10px @theme-color-highlight inset;
}
}
.skill-control {
display: flex;
align-items: center;
gap: 5px;
}
.skill-value {
min-width: 25px;
text-align: center;
font-weight: bold;
}
.rarity-badge {
display: inline-block;
padding: 1px 4px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
margin-left: 5px;
&.rarity-0 { .rarity-badge(@rarity-0-bg, @rarity-0-text); }
&.rarity-1 { .rarity-badge(@rarity-1-bg, @rarity-1-text); }
&.rarity-2 { .rarity-badge(@rarity-2-bg, @rarity-2-text); }
&.rarity-3 { .rarity-badge(@rarity-3-bg, @rarity-3-text); }
}
.skill-category-selector {
margin-bottom: 15px;
.card-style();
}
.wounds-card {
margin-top: 15px;
.card-style();
}
.wound-item {
display: flex;
flex-direction: column;
gap: 5px;
label {
font-size: @font-size-12;
font-weight: bold;
text-align: center;
}
}
.wound-control {
display: flex;
align-items: center;
gap: 8px;
input { flex: 1; }
span {
font-size: @font-size-12;
min-width: 30px;
text-align: center;
}
}
.ability-section {
.card-style();
}
.ability-category-header {
font-family: "DistressBlack", sans-serif;
font-size: @font-size-14;
text-transform: uppercase;
margin-top: 0;
margin-bottom: 8px;
border-bottom: 1px solid @color-border-dark-3;
padding-bottom: 5px;
}
.ability-item {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 0;
label {
flex: 1;
font-size: @font-size-12;
white-space: nowrap;
}
}
.ability-control {
display: flex;
align-items: center;
gap: 8px;
}
.tab {
padding: 10px;
overflow-y: auto;
}
.form-group {
.form-group-style();
}
.header-fields {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.sheet-header .resource {
padding: 4px 8px;
margin-bottom: 0;
}
.resources {
display: grid;
gap: 8px;
}
.resource {
.card-style();
label { font-weight: bold; }
}
.resource-label {
font-size: @font-size-12;
font-weight: bold;
white-space: nowrap;
}
.resource-content {
display: flex;
align-items: center;
select {
font-size: @font-size-12;
padding: 3px 5px;
min-width: 100px;
}
}
.editor {
min-height: 100px;
margin-bottom: 10px;
.editor-content {
min-height: 80px;
font-size: @font-size-12;
}
}
.hexa {
.hexa-style();
.transition();
&:hover {
.hexa-style(rgba(255, 255, 255, 0.425), rgba(0, 0, 0, 0.288));
}
}
}
+96
View File
@@ -0,0 +1,96 @@
@import "../variables";
@import "../utilities";
.system-vermine2047 .vermine2047.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%;
}
}
div.minor-totems {
position: relative;
background-color: rgba(146, 156, 111, 0.5215686275);
h5 {
position: absolute;
top: 0;
img {
max-width: 2rem;
position: absolute;
bottom: -2rem;
}
&.human, &.adapted {
.transition(0.3s);
}
&.human img.img-totem,
&.adapted img.img-totem {
filter: drop-shadow(0px 0px 20px rgb(0, 0, 0));
}
&.human.major,
&.adapted.major {
transform: scale(1.1);
}
&.human.major img,
&.adapted.major img {
filter: drop-shadow(0px 0px 10px red);
}
}
.totem-dice {
.human-dice,
.adapted-dice {
display: flex;
flex-direction: row;
margin-left: 2rem;
}
.human-dice i,
.adapted-dice i {
padding-top: 0.5rem;
color: @totem-human-color;
position: relative;
z-index: 0;
pointer-events: none;
}
.adapted-dice {
justify-content: flex-end;
margin-left: 0;
margin-right: 2rem;
transform: rotate(180deg);
}
.adapted-dice i {
transform: rotate(180deg);
padding-top: 0.5rem;
color: @totem-adapted-color;
}
}
.human {
left: 0;
img { left: 0; }
}
.adapted {
right: 0;
img { right: 0; }
}
}
}
+650
View File
@@ -0,0 +1,650 @@
@font-face {
font-family: "DistressBlack";
src: url("@{fonts-path}/dcc_sharp_distress_black_by_dccanim.otf");
}
* {
box-sizing: border-box;
}
body.system-vermine2047 {
font-family: "Roboto", sans-serif;
color: @theme-color-secondary;
}
img {
border: none;
}
ul.unstyled {
list-style-type: none;
padding: 0;
margin: 0;
li {
padding: 0;
margin: 0;
}
}
.w-full {
width: 100%;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.window-app {
font-family: "Roboto", sans-serif;
.shadow(0px, 0px, 30px, rgba(106, 176, 76, 0.25));
}
.system-vermine2047 {
.window-content,
form.application.sheet.vermine2047 {
.background-image(url("@{ui-path}/box_background.webp"), repeat);
padding: 10px;
overflow-y: hidden;
}
.dialog .window-content {
.background-image(url("@{ui-path}/fond_chat_box.webp"), repeat);
padding: 0.5rem;
overflow-y: hidden;
}
}
.window-content .row {
&.smb { margin-bottom: 0.25rem; }
&.mdb { margin-bottom: 0.5rem; }
&.lgb { margin-bottom: 1rem; }
}
.rollable:hover,
.rollable:focus {
color: #000;
text-shadow: 0 0 10px @color-acid-green;
cursor: pointer;
.transition();
}
.rollable {
.transition();
}
img.profile-img {
filter: drop-shadow(0px 0px 20px rgb(110, 133, 27));
height: auto;
width: 100%;
max-width: 10rem;
}
body.system-vermine2047 img#logo {
content: url("@{assets-path}/images/ui/logo_vermine_foundry.webp");
height: auto;
}
#chat-log,
.chat-log {
.message {
.background-image(url("@{ui-path}/box_background.webp"), repeat);
}
}
.grid {
display: grid;
grid-column: span 2 / span 2;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
margin: 5px 0;
padding: 0;
}
.grid-2col { .grid(); }
.grid-3col { grid-column: span 3; grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4col { grid-column: span 4; grid-template-columns: repeat(4, minmax(0, 1fr)); }
.grid-5col { grid-column: span 5; grid-template-columns: repeat(5, minmax(0, 1fr)); }
.grid-6col { grid-column: span 6; grid-template-columns: repeat(6, minmax(0, 1fr)); }
.grid-7col { grid-column: span 7; grid-template-columns: repeat(7, minmax(0, 1fr)); }
.grid-8col { grid-column: span 8; grid-template-columns: repeat(8, minmax(0, 1fr)); }
.grid-9col { grid-column: span 9; grid-template-columns: repeat(9, minmax(0, 1fr)); }
.grid-10col { grid-column: span 10; grid-template-columns: repeat(10, minmax(0, 1fr)); }
.grid-11col { grid-column: span 11; grid-template-columns: repeat(11, minmax(0, 1fr)); }
.grid-12col { grid-column: 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 {
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;
}
.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 { flex: 0; }
.flex-between { justify-content: space-between; }
.flexlarge { flex: 2; }
.align-left {
justify-content: flex-start;
text-align: left;
}
.align-right {
justify-content: flex-end;
text-align: right;
}
.align-center {
justify-content: center;
text-align: center;
}
::-webkit-scrollbar-thumb {
outline: none;
border-radius: 3px;
background: #577822;
border: 1px solid var(--color-border-highlight);
}
::-webkit-scrollbar {
width: 3px;
height: 3px;
}
.system-vermine2047 {
.item-form {
font-family: "Roboto", sans-serif;
}
.sheet-header {
flex: 0 auto;
overflow: hidden;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
margin-bottom: 10px;
.profile-img {
flex: 0 0 100px;
height: 100px;
margin-right: 10px;
}
.header-fields {
flex: 1;
}
h1.charname {
height: 50px;
padding: 0px;
margin: 5px 0;
border-bottom: 0;
input {
width: 100%;
height: 100%;
margin: 0;
}
}
}
.sheet-tabs {
flex: 0;
}
.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;
}
.tox {
min-height: 25vh;
.tox-editor-container {
background: #fff;
}
.tox-edit-area {
padding: 0 8px;
}
}
.resource-label {
font-weight: bold;
}
.items-header {
height: 28px;
margin: 2px 0;
padding: 0;
align-items: center;
background: rgba(0, 0, 0, 0.05);
border: 2px groove #eeede0;
font-weight: bold;
> * {
font-size: 14px;
text-align: center;
}
.item-name {
font-weight: bold;
padding-left: 5px;
text-align: left;
display: flex;
}
}
.items-list {
list-style: none;
margin: 0;
padding: 0;
overflow-y: auto;
scrollbar-width: thin;
color: #444;
.item-list {
list-style: none;
margin: 0;
padding: 0;
}
.item-name {
flex: 2;
margin: 0;
overflow: hidden;
font-size: 13px;
text-align: left;
align-items: center;
display: flex;
h3, h4 {
margin: 0;
white-space: nowrap;
overflow-x: hidden;
}
}
.item-controls {
display: flex;
flex: 0;
justify-content: flex-end;
a {
font-size: 12px;
text-align: center;
margin: 0 6px;
}
}
.item {
align-items: center;
padding: 0 2px;
border-bottom: 1px solid #c9c7b8;
&:last-child {
border-bottom: none;
}
.item-name {
color: @theme-color-dark;
.item-image {
flex: 0 0 30px;
height: 30px;
background-size: 30px;
border: none;
margin-right: 5px;
}
}
.item-prop {
text-align: center;
border-left: 1px solid #c9c7b8;
border-right: 1px solid #c9c7b8;
font-size: 12px;
}
.items-header {
height: 28px;
margin: 2px 0;
padding: 0;
align-items: center;
background: rgba(0, 0, 0, 0.05);
border: 2px groove #eeede0;
font-weight: bold;
> * {
font-size: 12px;
text-align: center;
}
.item-name {
padding-left: 5px;
text-align: left;
}
}
}
.effects .item {
.effect-source,
.effect-duration,
.effect-controls {
text-align: center;
border-left: 1px solid #c9c7b8;
border-right: 1px solid #c9c7b8;
font-size: 12px;
}
.effect-controls {
border: none;
}
}
}
.item-formula {
flex: 0 0 200px;
padding: 0 8px;
}
}
span.game-mode {
font-family: "DistressBlack", sans-serif;
position: absolute;
margin-left: auto;
color: rgba(0, 0, 0, 0);
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;
}
span.game-mode#game-mode-1 { color: @game-mode-1-color; }
span.game-mode#game-mode-2 { color: @game-mode-2-color; }
span.game-mode#game-mode-3 { color: @game-mode-3-color; }
.shadow {
.shadow();
}
ol#chat-log,
ol.chat-log {
header.message-header {
background-color: #000;
padding: 0 1rem;
}
.vermine-roll-message {
overflow: hidden;
padding: 4px 10px 10px;
position: relative;
> h3 {
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-14;
margin: 6px 0 8px;
padding: 4px 10px;
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
box-shadow: 0px 0px 3px rgba(31, 26, 26, 0.3) inset;
font-weight: 900;
border-bottom: none;
}
> .flexrow:not(.roll-total):not(.roll-results) {
align-items: center;
gap: 6px;
margin: 4px 0;
h4 {
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-12;
margin: 0;
border-bottom: none;
font-weight: 900;
}
span {
font-family: "Roboto", sans-serif;
font-size: @font-size-12;
}
}
// ── Reroll section ──────────────────────────────────────────────
.reroll-fromroll {
> h4 {
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-12;
margin: 6px 0 4px;
border-bottom: none;
font-weight: 900;
#allowed_reroll {
font-size: @font-size-12;
}
}
}
div.reroll {
.transition(0.3s);
max-height: 1px;
overflow: hidden;
justify-content: flex-end;
align-items: center;
gap: 8px;
button {
text-transform: uppercase;
font-family: "DistressBlack";
font-size: @font-size-12;
padding: 2px 12px;
max-width: fit-content;
box-shadow: 0px 0px 2px #000;
.background-image(url("@{ui-path}/scotch.webp"), no-repeat, cover, 50% 0%);
}
&.visible { max-height: 15rem; }
}
// ── Dice results ────────────────────────────────────────────────
ul.roll-results {
list-style: none;
padding: 6px 0;
justify-content: center;
gap: 6px;
flex-wrap: wrap;
li.die {
position: relative;
width: 3rem;
height: 3rem;
flex: none;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
.transition();
border-bottom: 4px solid @color-hemolymph;
border-radius: 50%;
&:after {
content: "";
position: absolute;
top: -1rem;
text-wrap: nowrap;
color: #fff;
font-weight: 100;
font-size: smaller;
text-align: center;
opacity: 0;
text-shadow: 0px 0px 5px #000;
}
&:hover::after {
opacity: 1;
color: #fff;
}
&.human { border-top: 4px solid @totem-human-color; }
&.adapted { border-top: 4px solid @totem-adapted-color; }
&.rerollable {
cursor: pointer;
&:hover { transform: translateY(0.5rem); }
}
&.success { border-bottom-color: @color-acid-green; }
&.adapted::after { content: "adapté"; }
&.human::after { content: "humain"; }
&.rerolled { transform: translateY(0rem); }
span {
display: none;
}
&:not([data-result]) {
background-image: url("@{dice-path}/d10c-1.webp");
opacity: 0.5;
filter: grayscale(1);
}
}
// Per-face dice background images
.generate-dice-faces(@i: 1) when (@i =< 10) {
li.die[data-result="@{i}"] {
background-image: url("@{dice-path}/d10c-@{i}.webp");
}
.generate-dice-faces(@i + 1);
}
.generate-dice-faces();
}
// ── Total section ───────────────────────────────────────────────
div.roll-total {
margin-top: 8px;
padding: 8px 16px;
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
box-shadow: 0px 0px 3px rgba(31, 26, 26, 0.3) inset;
border-radius: 2px;
gap: 16px;
justify-content: center;
> div {
display: flex;
align-items: center;
gap: 6px;
}
h4 {
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-12;
margin: 0;
border-bottom: none;
font-weight: 900;
text-align: center;
}
span {
font-family: "DistressBlack";
font-size: @font-size-20;
}
#total {
color: @color-acid-green;
text-shadow: 0 0 4px fade(@color-acid-green, 40%);
}
#required {
color: @color-amber;
}
}
}
div.item-card header img {
max-width: 30%;
}
}
// Padding supplémentaire pour les fiches groupe, npc et créature
.system-vermine2047 {
.group-content,
.npc-content,
.creature-content {
padding: 15px;
}
}
+475
View File
@@ -0,0 +1,475 @@
@import "variables";
@import "utilities";
.window-app.vermineDialog {
max-width: 50vw;
height: fit-content;
.window-content {
.background-image(url("@{ui-path}/box_background.webp"), repeat);
color: #000;
}
details > summary::after {
content: "▶️";
position: relative;
right: 40%;
}
details[open] > summary::after {
content: "🔽";
}
.grid {
justify-content: space-around;
.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;
.custom-select-style();
option { max-width: fit-content; }
}
.dialog-buttons {
display: flex;
justify-content: space-around;
flex-direction: row;
button {
display: block;
flex: 0.3;
color: @theme-color-dark;
}
}
}
input[type="range"] { .custom-input-style(); }
input[type="checkbox"],
input[type="radio"] { .custom-checkbox-radio(); }
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);
}
}
.app.vermine2047.trait-selector {
.form-group {
.shadow(0, 0, 30px, gray);
padding: 0.3rem 0.5rem;
border: 3px solid #8e9010;
&: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 #000;
}
}
}
.app .actor.choose {
div.actor {
position: relative;
img {
border-radius: 50%;
.shadow(0px, 0px, 8px, #000);
}
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;
.shadow(0px, 0px, 8px, #000);
}
}
}
iframe {
min-height: 500px;
.tabs.moods-headings {
max-width: 1px;
}
}
// ── Roll Dialog V2 ─────────────────────────────────────────────────────
.application.vermine-roll {
.window-content {
overflow-y: auto;
}
.roll-dialog-content {
display: flex;
flex-direction: column;
gap: 6px;
padding: 10px;
height: 100%;
overflow-y: auto;
color: @color-text-light-0;
.dice-pool {
flex: 1;
display: flex;
flex-direction: column;
gap: inherit;
}
label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-12;
}
input, select, textarea {
color: @color-chitin-dark;
background: @color-text-light-0;
}
select {
.custom-select-style();
width: 100%;
}
input[type="range"] { .custom-input-style(); }
input[type="checkbox"],
input[type="radio"] { .custom-checkbox-radio(); }
// ── Main roll section: ability + skill side-by-side ─────────────
.main-roll-section {
gap: 10px;
> .flexcol {
flex: 1;
gap: 4px;
min-width: 0;
> label {
font-size: @font-size-13;
color: @color-amber;
white-space: nowrap;
}
select {
min-height: 28px;
width: 100%;
}
}
.ability-score {
gap: 4px;
font-size: @font-size-12;
color: @color-text-light-0;
opacity: 0.85;
#abilityScoreValue {
font-weight: bold;
color: @color-acid-green;
}
}
}
// ── Collapsible sections (specialties, difficulty, bonuses) ─────
details {
margin-top: 2px;
summary {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-12;
padding: 6px 10px;
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
box-shadow: 0px 0px 3px rgba(31, 26, 26, 0.5) inset;
cursor: pointer;
color: @theme-color-dark;
display: flex;
align-items: center;
gap: 8px;
&::after {
content: "▶";
margin-left: auto;
font-size: @font-size-11;
}
&:hover {
box-shadow: 0 0 10px @theme-color-highlight inset;
}
span.label {
font-family: "DistressBlack", sans-serif;
font-size: @font-size-12;
}
.current-values,
.current-specialty,
.bonus-count {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
text-transform: none;
opacity: 0.8;
}
}
&[open] > summary::after {
content: "▼";
}
> div:not(summary) {
padding: 8px;
background: fade(@color-chitin-dark, 40%);
}
}
// ── Difficulty section ──────────────────────────────────────────
.difficulty-controls {
gap: 12px;
> .flexcol {
flex: 1;
gap: 4px;
label {
font-size: @font-size-11;
color: @color-amber;
}
}
}
// ── Specialties section ─────────────────────────────────────────
.specialty-options {
gap: 8px;
flex-wrap: wrap;
label {
display: flex;
align-items: center;
gap: 4px;
font-size: @font-size-12;
font-family: "Roboto", sans-serif;
text-transform: none;
color: @color-text-light-0;
cursor: pointer;
&:hover {
color: @color-amber;
}
}
input[type="radio"] {
width: 14px;
height: 14px;
flex: none;
}
}
// ── Bonuses grid ────────────────────────────────────────────────
.bonus-grid {
margin: 0;
gap: 8px;
.bonus-item {
align-items: center;
gap: 6px;
padding: 4px 0;
> label.label {
font-size: @font-size-11;
color: @color-text-light-0;
white-space: nowrap;
}
input[type="number"] {
width: 40px;
text-align: center;
padding: 2px;
}
input[type="checkbox"] + label {
font-size: @font-size-12;
font-family: "Roboto", sans-serif;
text-transform: none;
color: @color-text-light-0;
}
.item-list {
gap: 4px;
label {
align-items: center;
gap: 4px;
font-size: @font-size-11;
font-family: "Roboto", sans-serif;
text-transform: none;
color: @color-text-light-0;
cursor: pointer;
&:hover {
color: @color-amber;
}
}
}
&.full-width {
grid-column: 1 / -1;
}
}
// Totems sub-section
.totems-section {
flex-wrap: wrap;
gap: 6px;
> label.label {
width: 100%;
}
.totem-options {
gap: 12px;
}
.totem-option {
display: flex;
align-items: center;
gap: 4px;
color: @color-text-light-0;
font-size: @font-size-12;
font-family: "Roboto", sans-serif;
text-transform: none;
cursor: pointer;
&:hover {
color: @color-amber;
}
small {
opacity: 0.7;
}
}
.totem-hint {
width: 100%;
font-size: @font-size-11;
color: @color-text-light-0;
opacity: 0.6;
font-style: italic;
}
}
}
// ── Total dice pool section ─────────────────────────────────────
.total-section {
align-items: center;
padding: 10px;
border-top: 1px solid @color-border-dark-4;
margin-top: 4px;
gap: 10px;
background: fade(@color-chitin-dark, 50%);
.label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-16;
color: @color-text-light-0;
}
.total-value {
font-family: "DistressBlack", sans-serif;
font-size: @font-size-28;
color: @color-acid-green;
text-shadow: 0 0 8px fade(@color-acid-green, 40%);
}
.totem-selector {
font-size: @font-size-11;
color: @color-text-light-0;
opacity: 0.7;
margin-left: auto;
select {
font-size: @font-size-11;
padding: 1px 4px;
width: auto;
display: inline;
}
}
}
}
// ── Footer buttons ───────────────────────────────────────────────
.sheet-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 8px 10px;
margin-top: auto;
border-top: 1px solid @color-border-dark-4;
background: fade(@color-chitin-dark, 60%);
button {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-13;
padding: 6px 20px;
border: none;
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
box-shadow: 0px 0px 3px rgba(31, 26, 26, 0.979) inset;
cursor: pointer;
color: @theme-color-dark;
.transition();
display: flex;
align-items: center;
gap: 6px;
&:hover {
box-shadow: 0 0 10px @theme-color-highlight inset;
}
i {
font-size: @font-size-14;
}
}
}
}
+100
View File
@@ -0,0 +1,100 @@
@import "variables";
@import "utilities";
.system-vermine2047 .item-formula {
flex: 0 0 200px;
padding: 0 8px;
}
.system-vermine2047 .vermine2047.item .window-content {
padding: 0 1rem;
.flexrow { align-items: center; }
header, h1, h2, h3, h4, h5 {
.background-image(url("@{ui-path}/scotch.webp"), no-repeat, cover, 50% 50%);
text-transform: uppercase;
font-family: "DistressBlack";
margin-top: 1rem;
border-bottom: none;
}
h2, h3, h4 { text-align: center; }
h5 { margin-bottom: 0; }
.resource-label {
font-size: 0.75rem;
color: @color-text-light-highlight;
text-transform: uppercase;
font-weight: bold;
text-shadow: 0 0 3px rgba(0, 0, 0, 0.9);
}
.resource {
border: 1px solid @color-border-dark-3;
border-left: 3px solid @theme-color-primary;
background: rgba(0, 0, 0, 0.1);
padding: 0.5rem 1rem;
text-align: center;
.transition();
&:hover {
background: rgba(0, 0, 0, 0.2);
border-color: @theme-color-primary;
}
.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";
}
select {
.custom-select-style();
color: @color-text-light-1;
font-family: "DistressBlack", sans-serif;
font-size: 0.875rem;
text-align: center;
text-shadow: 0 0 3px rgba(0, 0, 0, 0.9);
option {
background: @color-bg-option;
color: @color-text-dark-primary;
}
}
.traits {
.shadow();
h3, h4 { margin: 0; }
}
.editor,
.editor-content {
color: @color-text-dark-primary;
background: rgba(255, 255, 255, 0.3);
min-height: 6rem;
p { margin: 0.25rem 0; }
}
}
ol#chat-log div.item-card header img,
ol.chat-log div.item-card header img {
max-width: 30%;
}
+23
View File
@@ -0,0 +1,23 @@
.sans-font {
font-family: "DistressBlack", sans-serif;
}
.chat-message .message-header {
line-height: 20px;
color: white;
text-shadow: 0px 0px 5px black;
background: #1918135e;
}
.flex-center {
align-items: center;
justify-content: center;
}
.flex-around {
justify-content: space-around;
}
.flexsmall {
flex: 0.5;
}
+257
View File
@@ -0,0 +1,257 @@
// ============================================
// Utilitaires LESS pour Vermine2047
// Mixins, fonctions et classes utilitaires
// ============================================
// Mixin pour les ombres standard
.shadow() {
box-shadow: 0 2rem @theme-color-shadow;
margin: 2rem 0;
}
.shadow(@color) {
box-shadow: 0 2rem @color;
margin: 2rem 0;
}
.shadow(@color, @blur) {
box-shadow: 0 @blur @color;
margin: 2rem 0;
}
.shadow(@h-offset, @v-offset, @blur, @color) {
box-shadow: @h-offset @v-offset @blur @color;
margin: 2rem 0;
}
// Mixin pour le style des hexagones
.hexa-style(@bg-color: rgba(255, 255, 255, 0.425), @bg-color-end: rgba(0, 0, 0, 0.288)) {
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
background: radial-gradient(circle, @bg-color 0%, @bg-color-end 100%);
max-height: 1.5rem;
max-width: 1.5rem;
min-width: 1.5rem;
aspect-ratio: 1/1;
color: #000;
vertical-align: center;
text-align: center;
}
// Mixin pour les boutons rollable
.rollable-style() {
&:hover,
&:focus {
color: #000;
text-shadow: 0 0 10px red;
cursor: pointer;
}
}
// Mixin pour les inputs de type hexa
.input-hexa-style() {
.hexa-style();
input {
width: 1rem;
}
input[type="radio"] {
opacity: 0;
}
input[type="radio"]::after,
input[type="radio"]::before {
display: none;
}
}
// Mixin pour les classes flex
.flex-container(@direction: row, @wrap: nowrap, @justify: flex-start, @align: stretch) {
display: flex;
flex-direction: @direction;
flex-wrap: @wrap;
justify-content: @justify;
align-items: @align;
}
// Mixin pour le style des cartes
.card-style(@bg-color: rgba(0, 0, 0, 0.1), @border-color: #444) {
border: 1px solid @border-color;
border-radius: 4px;
padding: 10px;
margin-bottom: 15px;
background: @bg-color;
}
// Mixin pour les titres de carte
.card-title(@border-color: #666) {
margin-top: 0;
border-bottom: 1px solid @border-color;
padding-bottom: 5px;
text-align: center;
}
// Mixin pour les éléments de liste avec bordure
.list-item-with-border(@border-color: #333) {
padding: 5px;
border-bottom: 1px solid @border-color;
&:last-child {
border-bottom: none;
}
}
// Mixin pour les groupes de formulaires
.form-group-style(@margin-bottom: 10px) {
margin-bottom: @margin-bottom;
label {
display: block;
margin-bottom: 3px;
font-weight: bold;
font-size: 12px;
}
input,
select {
width: 100%;
font-size: 12px;
}
}
// Mixin pour les ressources
.resource-style(@bg-color: rgba(0, 0, 0, 0.05)) {
padding: 5px;
background: @bg-color;
border-radius: 4px;
margin: 0 5px;
label {
font-weight: bold;
margin-right: 8px;
min-width: 60px;
font-size: 12px;
}
.resource-content {
display: flex;
align-items: center;
}
}
// Mixin pour les badges de rareté
.rarity-badge(@bg-color, @text-color) {
display: inline-block;
padding: 1px 4px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
margin-left: 5px;
background: @bg-color;
color: @text-color;
}
// Mixin pour le fond avec image
.background-image(@url, @repeat: no-repeat, @size: auto, @position: center) {
background: @url @repeat @position / @size;
}
// Mixin pour les transitions
.transition(@property: all, @duration: 0.2s, @timing: ease-out) {
transition: @property @duration @timing;
}
// Mixin pour les styles de l'éditeur TinyMCE
.tiny-mce-style() {
.tox {
min-height: 25vh;
.tox-editor-container {
background: #fff;
}
.tox-edit-area {
padding: 0 8px;
}
}
}
// Mixin pour les styles des inputs personnalisés
.custom-input-style() {
appearance: none;
background: rgba(0, 0, 0, 0);
cursor: pointer;
width: 100%;
&::-webkit-slider-runnable-track {
.background-image(url("@{ui-path}/scotch.webp"), no-repeat, auto, center);
background-size: 100% auto;
height: 0.4rem;
border: none;
box-shadow: 0px 0px 13px rgba(31, 26, 26, 0.979) inset;
}
&::-webkit-slider-thumb {
appearance: none;
margin-top: -0.3rem;
height: 1rem;
width: 1rem;
border: none;
border-radius: 50%;
.background-image(url("@{totems-path}/human.webp"), no-repeat, cover);
filter: contrast(2);
box-shadow: 0px 0px 10px #000;
&:focus {
box-shadow: 0px 0px 10px #ff0;
}
}
}
// Mixin pour les checkbox et radio boutons personnalisés
.custom-checkbox-radio() {
-webkit-appearance: none;
appearance: none;
background: rgba(0, 0, 0, 0);
box-shadow: 0px 0px 3px #85854e;
cursor: pointer;
width: 1.5rem;
height: 1rem;
border-radius: 0.4rem;
.transition();
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
box-shadow: 0px 0px 6px #000 inset;
background-color: rgba(61, 11, 11, 0.658);
&[disabled=true] {
filter: grayscale(1);
}
&:after {
content: " ";
.background-image(url("@{totems-path}/human.webp"), no-repeat, auto, 50% 150%);
position: relative;
top: 10%;
left: 0%;
width: 100%;
height: 80%;
display: block;
border-radius: 0%;
padding: 0;
.transition();
}
&:checked {
background-color: rgba(26, 107, 12, 0.658);
&:after {
font-weight: 900;
background-color: rgba(26, 1, 1, 0);
left: 50%;
}
}
}
// Mixin pour les selects personnalisés
.custom-select-style() {
border: none;
.background-image(url("@{ui-path}/scotch.webp"), no-repeat, auto, 100% 100%);
box-shadow: 0px 0px 3px rgba(31, 26, 26, 0.979) inset;
&[disabled] {
color: #000;
text-shadow: 0px 0px 15px #000;
}
option {
appearance: none;
border: none;
.background-image(url("@{ui-path}/scotch.webp"), no-repeat, auto, 100% 100%);
}
}
+175
View File
@@ -0,0 +1,175 @@
// ============================================
// Variables LESS pour Vermine2047
// Converti depuis les variables CSS du fichier vermine2047.css
// ============================================
// Couleurs de texte - clair
@color-text-light-highlight: #96d696;
@color-text-light-heading: #9fd8a8;
@color-text-light-primary: #a4b5b3;
// Couleurs de texte - foncé
@color-text-dark-primary: #131919;
@color-text-dark-secondary: #444b4a;
@color-text-dark-header: #1d2223;
@color-text-dark-inactive: #71797a;
// Couleurs de texte - autres
@color-text-hyperlink: #5aaf0a;
// Niveaux de gris pour le texte clair
@color-text-light-0: #fff;
@color-text-light-1: #e0f0f0;
@color-text-light-2: #c9e0c0;
@color-text-light-3: #90c4a4;
@color-text-light-4: #80c08b;
@color-text-light-5: #60b06b;
@color-text-light-6: #40a05d;
@color-text-light-7: #208028;
// Niveaux de gris pour le texte foncé
@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;
// Couleurs de bordure - clair
@color-border-light-1: #b0d9b0;
@color-border-light-2: #80c0c0;
// Couleurs de bordure - foncé
@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;
// Couleurs d'ombre
@color-shadow-primary: #7bb60d;
@color-shadow-highlight: #85cc01d0;
@color-shadow-dark: #000;
// Couleurs de soulignement
@color-underline-inactive: #71797a;
@color-underline-active: #1a1944;
@color-underline-header: #228247;
// Couleurs de bordure - autres
@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;
// Couleurs de fond
@color-bg-btn-minor-inactive: #9fc7d8;
@color-bg-btn-minor-active: #a4b5b3;
@color-bg-option: #ccdada;
// Autres couleurs
@color-checkbox-checked: #666;
@color-ownership-none: #00ff55;
@color-ownership-observer: #71797a;
@color-ownership-owner: #a4b5b3;
// Niveaux de log
@color-level-info: #b95c87;
@color-level-warning: #04b184;
@color-level-error: #03750;
@color-level-success: #3c266c;
// Z-index
@z-index-canvas: 0;
@z-index-app: 30;
@z-index-ui: 60;
@z-index-window: 100;
@z-index-tooltip: 9999;
// Dimensions
@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;
// Polices
@font-mono: monospace;
// Tailles de police
@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;
// Hauteurs de ligne
@line-height-12: 0.75rem;
@line-height-16: 1rem;
@line-height-20: 1.25rem;
@line-height-30: 1.875rem;
// ============================================
// Variables spécifiques Vermine2047
// ============================================
// Couleurs thématiques
@theme-color-primary: #7e7544;
@theme-color-secondary: #dfdfdf;
@theme-color-accent: #1fa832;
@theme-color-dark: #191813;
@theme-color-light: #4e564c;
@theme-color-shadow: rgba(0, 0, 0, 0.7098039216);
@theme-color-highlight: #005a3c;
// Couleurs insecte / organique
@color-acid-green: #6ab04c;
@color-amber: #e8b84b;
@color-chitin-dark: #2a2520;
@color-hemolymph: #8b4513;
@color-membrane: rgba(106, 176, 76, 0.1);
@color-honeycomb: #c9a84c;
// Couleurs pour les dés et totems
@dice-pool-color: rgb(94, 90, 77);
@dice-reroll-color: rgb(187, 182, 165);
@totem-human-color: #064930;
@totem-adapted-color: #553402;
// Couleurs pour les niveaux de rareté
@rarity-0-bg: #444;
@rarity-0-text: #aaa;
@rarity-1-bg: #5a7a5a;
@rarity-1-text: #fff;
@rarity-2-bg: #7a9a7a;
@rarity-2-text: #000;
@rarity-3-bg: #9a5a9a;
@rarity-3-text: #fff;
// Couleurs pour les modes de jeu
@game-mode-1-color: rgba(235, 218, 143, 0.8);
@game-mode-2-color: #83f883;
@game-mode-3-color: rgba(245, 124, 124, 0.8);
// Chemins des assets
@assets-path: "/systems/vermine2047/assets";
@images-path: "@{assets-path}/images";
@fonts-path: "@{assets-path}/fonts";
@ui-path: "@{images-path}/ui";
@totems-path: "@{images-path}/ui/totems";
@dice-path: "@{assets-path}/dice";
+37
View File
@@ -0,0 +1,37 @@
// ============================================
// Fichier principal LESS pour Vermine2047
// Ce fichier importe tous les modules LESS
// ============================================
// 1. Import de police Google (doit être en premier car c'est du CSS pur)
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap");
// 2. Variables et utilitaires (doivent être importés avant tout le reste)
@import "variables";
@import "utilities";
// 3. Styles de base
@import "base";
// 4. Legacy SCSS converti en LESS
@import "legacy";
// 5. Styles des acteurs
@import "actor/actor";
@import "actor/totem";
@import "actor/npc";
@import "actor/group";
@import "actor/creature";
// 6. Styles des items
@import "items";
// 7. Styles des dialogs
@import "dialogs";
// ============================================
// Notes:
// - L'ordre des imports est important
// - Les variables doivent être définies avant d'être utilisées
// - Les mixins doivent être définis avant d'être appelés
// ============================================
+20
View File
@@ -0,0 +1,20 @@
export { default as VermineBaseActorSheet } from "./base-actor-sheet.mjs"
export { VermineBaseItemSheet } from "./base-item-sheet.mjs"
export { default as VermineCharacterSheetV2 } from "./character-sheet.mjs"
export { default as VermineNpcSheetV2 } from "./npc-sheet.mjs"
export { default as VermineGroupSheetV2 } from "./group-sheet.mjs"
export { default as VermineCreatureSheetV2 } from "./creature-sheet.mjs"
export {
VermineItemSheetV2,
VermineWeaponSheetV2,
VermineDefenseSheetV2,
VermineVehicleSheetV2,
VermineAbilitySheetV2,
VermineSpecialtySheetV2,
VermineBackgroundSheetV2,
VermineTraumaSheetV2,
VermineEvolutionSheetV2,
VermineRumorSheetV2,
VermineTargetSheetV2,
VermineRiteSheetV2
} from "./item-sheets.mjs"
@@ -0,0 +1,262 @@
import { onManageActiveEffect } from "../../system/effects.mjs"
const { HandlebarsApplicationMixin } = foundry.applications.api
/**
* Fiche de base pour tous les acteurs Vermine 2047 (ApplicationV2).
* Remplace VermineActorSheet (AppV1).
*/
export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) {
// ── Mode édition / jeu ──────────────────────────────────────────────
static SHEET_MODES = { EDIT: 0, PLAY: 1 }
_sheetMode = this.constructor.SHEET_MODES.PLAY
get isPlayMode() { return this._sheetMode === this.constructor.SHEET_MODES.PLAY }
get isEditMode() { return this._sheetMode === this.constructor.SHEET_MODES.EDIT }
// ── Options par défaut ──────────────────────────────────────────────
static DEFAULT_OPTIONS = {
classes: ["vermine2047", "actor"],
position: { width: 800, height: "auto" },
form: { submitOnChange: true },
window: { resizable: true },
dragDrop: [{ dragSelector: ".item", dropSelector: null }],
actions: {
editImage: VermineBaseActorSheet.#onEditImage,
toggleSheet: VermineBaseActorSheet.#onToggleSheet,
edit: VermineBaseActorSheet.#onItemEdit,
delete: VermineBaseActorSheet.#onItemDelete,
create: VermineBaseActorSheet.#onItemCreate,
roll: VermineBaseActorSheet.#onRollItem,
clickRadio: VermineBaseActorSheet.#onClickRadioHexa,
effectControl: VermineBaseActorSheet.#onEffectControl,
chooseTotem: VermineBaseActorSheet.#onChooseTotem
}
}
// ── Drag & Drop ─────────────────────────────────────────────────────
#dragDrop
constructor(options = {}) {
super(options)
this.#dragDrop = this.#createDragDropHandlers()
}
#createDragDropHandlers() {
return this.options.dragDrop.map(d => {
d.permissions = {
dragstart: this._canDragStart.bind(this),
drop: this._canDragDrop.bind(this)
}
d.callbacks = {
dragover: this._onDragOver.bind(this),
drop: this._onDrop.bind(this)
}
return new foundry.applications.ux.DragDrop.implementation(d)
})
}
_canDragStart() { return this.isEditable }
_canDragDrop() { return this.isEditable }
// ── Soumission du formulaire ────────────────────────────────────────
/** @override - coerce string values from HTML form inputs to numbers */
_prepareSubmitData(event, form, formData, updateData) {
const fd = foundry.utils.deepClone(formData.object)
for (const [key, value] of Object.entries(fd)) {
if (!key.startsWith("system.") || typeof value === "number") continue
const segments = key.slice(7).split(".")
let node = this.document.system.schema
for (const seg of segments) {
if (node instanceof foundry.data.fields.SchemaField) node = node.fields[seg]
else { node = undefined; break }
}
if (!(node instanceof foundry.data.fields.NumberField)) continue
// Handle arrays from duplicate-named form inputs
let raw = Array.isArray(value) ? value.filter(v => v !== "" && v !== null).pop() : value
if (raw === undefined) continue
if (typeof raw === "string" && raw.trim() === "") { fd[key] = 0; continue }
const num = Number(typeof raw === "string" ? raw.trim() : raw)
if (!isNaN(num)) fd[key] = num
}
return fd
}
// ── Contexte commun ─────────────────────────────────────────────────
async _prepareContext() {
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 }) : "";
};
return {
fields: this.document.schema.fields,
systemFields: this.document.system.schema.fields,
actor: this.document,
system: this.document.system,
source: this.document.toObject(),
config: CONFIG.VERMINE,
rollData: this.document.getRollData(),
isGM: game.user.isGM,
isEditMode: this.isEditMode,
isPlayMode: this.isPlayMode,
isEditable: this.isEditable,
enrichedNotes: await enrich("identity.notes"),
enrichedBiography: await enrich("identity.biography"),
enrichedRelations: await enrich("identity.relations")
}
}
// ── Rendu ───────────────────────────────────────────────────────────
async _onRoll(event) {
event.preventDefault()
const el = event.currentTarget
const type = el.dataset.type
const label = el.dataset.label
if (!type || !label) return
const { default: RollDialog } = await import("../../system/dialogs/rollDialog.mjs")
const dialog = await RollDialog.create({
actorId: this.document.id,
rolltype: type,
label
})
if (dialog) dialog.render(true)
}
// ── Actions ─────────────────────────────────────────────────────────
_onRender(context, options) {
super._onRender(context, options)
// Activate initial tabs (force to bypass changeTab's early-return when the
// tab is already set as active in tabGroups — Foundry v12 doesn't call
// changeTab on initial render, so the active class is never applied)
for (const [group, tab] of Object.entries(this.tabGroups ?? {})) {
this.changeTab(tab, group, {force: true})
}
// Move toggle from hidden main tab to visible position (only for sheets where
// .tab.main is not already displayed as a permanent sidebar via !important)
const mainTab = this.element.querySelector(".tab.main")
const tabs = this.element.querySelector('nav.tabs[data-application-part="tabs"]')
if ( mainTab && tabs && getComputedStyle(mainTab).display === "none" ) {
const existing = tabs.parentNode.querySelector('.sheet-header-toggle[data-moved]')
if (existing) existing.remove()
const toggle = mainTab.querySelector(".sheet-header-toggle")
if (toggle) {
toggle.dataset.moved = "true"
tabs.parentNode.insertBefore(toggle, tabs)
}
}
this.#dragDrop.forEach(d => d.bind(this.element))
this.element.querySelectorAll(".rollable").forEach(el => {
el.addEventListener("click", this._onRoll.bind(this))
})
// Auto-fill empty number inputs on change to prevent validation errors
this.element.addEventListener("change", e => {
const input = e.target
if (input?.type === "number" && !input.value && input.name && input !== document.activeElement) {
input.value = "0"
}
}, { capture: true })
}
/** @override */
async _onDropItem(event, item) {
const doc = item instanceof foundry.abstract.Document ? item : await fromUuid(item.uuid)
if (!doc) return
const itemData = doc.toObject()
await this.document.createEmbeddedDocuments("Item", [itemData], { renderSheet: false })
}
static #onToggleSheet() {
const modes = this.constructor.SHEET_MODES
this._sheetMode = this.isEditMode ? modes.PLAY : modes.EDIT
this.render()
}
static async #onEditImage(event, target) {
const attr = target.dataset.edit ?? "img"
const current = foundry.utils.getProperty(this.document, attr)
const fp = new FilePicker({
current,
type: "image",
callback: (path) => this.document.update({ [attr]: path }),
top: this.position.top + 40,
left: this.position.left + 10
})
return fp.browse()
}
static async #onItemEdit(event, target) {
const id = target.closest("[data-item-id]")?.dataset?.itemId
const uuid = target.closest("[data-item-uuid]")?.dataset?.itemUuid
let item
if (uuid) item = await fromUuid(uuid)
if (!item) item = this.document.items.get(id)
item?.sheet.render(true)
}
static async #onItemDelete(event, target) {
const itemUuid = target.closest("[data-item-uuid]")?.dataset?.itemUuid
if (itemUuid) {
const item = await fromUuid(itemUuid)
await item?.deleteDialog()
return
}
const id = target.closest("[data-item-id]")?.dataset?.itemId
const item = this.document.items.get(id)
await item?.deleteDialog()
}
static async #onItemCreate(event, target) {
const type = target.dataset.type
if (!type) return
const name = game.i18n.localize("ITEMS.new_" + type)
await this.document.createEmbeddedDocuments("Item", [{ name, type }])
}
static async #onRollItem(event, target) {
const id = target.closest("[data-item-id]")?.dataset?.itemId
if (!id) return
const item = this.document.items.get(id)
item?.roll()
}
static #onClickRadioHexa(event, target) {
event.preventDefault()
event.stopPropagation()
const input = target
const update = {}
let current = this.document
const propTree = input.name.split(".")
for (const prop of propTree) {
current = current[prop]
}
if (current != input.value) {
update[input.name] = parseInt(input.value)
} else {
update[input.name] = parseInt(input.value) - 1
}
this.document.update(update)
}
static #onEffectControl(event, target) {
onManageActiveEffect(event, this.document)
}
static async #onChooseTotem(event, target) {
const { TotemPicker } = await import("../../system/applications.mjs")
new TotemPicker(target, this.document).render(true)
}
}
@@ -0,0 +1,122 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
/**
* Fiche de base pour tous les items Vermine 2047 (ApplicationV2).
*/
export class VermineBaseItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) {
// ── Mode édition ────────────────────────────────────────────────────
static SHEET_MODES = { EDIT: 0, PLAY: 1 }
_sheetMode = this.constructor.SHEET_MODES.PLAY
get isPlayMode() { return this._sheetMode === this.constructor.SHEET_MODES.PLAY }
get isEditMode() { return this._sheetMode === this.constructor.SHEET_MODES.EDIT }
// ── Options par défaut ──────────────────────────────────────────────
static DEFAULT_OPTIONS = {
classes: ["vermine2047", "item"],
position: { width: 560, height: "auto" },
form: { submitOnChange: true },
window: { resizable: true },
actions: {
editImage: VermineBaseItemSheet.#onEditImage,
toggleSheet: VermineBaseItemSheet.#onToggleSheet,
clickDamage: VermineBaseItemSheet.#onClickDamage,
openTraits: VermineBaseItemSheet.#onOpenTraits
}
}
// ── Drag & Drop ─────────────────────────────────────────────────────
#dragDrop
constructor(options = {}) {
super(options)
this.#dragDrop = this.#createDragDropHandlers()
}
#createDragDropHandlers() {
if (!this.options.dragDrop) return []
return this.options.dragDrop.map(d => {
d.permissions = {
dragstart: this._canDragStart.bind(this),
drop: this._canDragDrop.bind(this)
}
d.callbacks = {
dragover: this._onDragOver.bind(this),
drop: this._onDrop.bind(this)
}
return new foundry.applications.ux.DragDrop.implementation(d)
})
}
_canDragStart() { return this.isEditable }
_canDragDrop() { return this.isEditable }
// ── Contexte commun ─────────────────────────────────────────────────
async _prepareContext() {
return {
fields: this.document.schema.fields,
systemFields: this.document.system.schema.fields,
item: this.document,
system: this.document.system,
source: this.document.toObject(),
config: CONFIG.VERMINE,
enrichedDescription: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.description, { async: true }),
isEditMode: this.isEditMode,
isPlayMode: this.isPlayMode,
isEditable: this.isEditable
}
}
// ── Rendu ───────────────────────────────────────────────────────────
_onRender(context, options) {
this.#dragDrop.forEach(d => d.bind(this.element))
}
// ── Sauvegarde ───────────────────────────────────────────────────────
/** @override */
_prepareSubmitData(event, form, formData, updateData) {
return super._prepareSubmitData(event, form, formData, updateData)
}
// ── Actions ─────────────────────────────────────────────────────────
static #onToggleSheet() {
const modes = this.constructor.SHEET_MODES
this._sheetMode = this.isEditMode ? modes.PLAY : modes.EDIT
this.render()
}
static async #onEditImage(event, target) {
const attr = target.dataset.edit ?? "img"
const current = foundry.utils.getProperty(this.document, attr)
const fp = new FilePicker({
current,
type: "image",
callback: (path) => this.document.update({ [attr]: path }),
top: this.position.top + 40,
left: this.position.left + 10
})
return fp.browse()
}
static #onClickDamage(event, target) {
// Les radios de dégâts sont 1-based dans le template (value="{{@index}}" avec index 1..max)
// mais le stockage est 0-based. On soustrait 1 avant de sauvegarder.
const prop = target.name
const value = parseInt(target.value) - 1
this.document.update({ [prop]: value })
}
static async #onOpenTraits(event, target) {
const { TraitSelector } = await import("../../system/applications.mjs")
new TraitSelector(this.document).render(true)
}
}
@@ -0,0 +1,98 @@
import VermineBaseActorSheet from "./base-actor-sheet.mjs"
export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ["character"],
position: { width: 860, height: 720 },
window: { contentClasses: ["character-content"] },
actions: {
addSpecialty: VermineCharacterSheetV2.#onAddSpecialty
}
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/character-main.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
abilities: { template: "systems/vermine2047/templates/actor/appv2/character-abilities.hbs" },
totem: { template: "systems/vermine2047/templates/actor/appv2/character-totem.hbs" },
equipment: { template: "systems/vermine2047/templates/actor/appv2/character-equipment.hbs" },
stories: { template: "systems/vermine2047/templates/actor/appv2/character-stories.hbs" },
combat: { template: "systems/vermine2047/templates/actor/appv2/character-combat.hbs" }
}
tabGroups = { sheet: "abilities" }
#getTabs() {
const tabs = {
abilities: { id: "abilities", group: "sheet", icon: "fas fa-address-card", label: "VERMINE.tabs.abilities" },
totem: { id: "totem", group: "sheet", icon: "fas fa-star", label: "VERMINE.tabs.totem" },
equipment: { id: "equipment", group: "sheet", icon: "fas fa-hammer", label: "VERMINE.tabs.equipment" },
stories: { id: "stories", group: "sheet", icon: "fas fa-book-open-reader", label: "VERMINE.tabs.stories" },
combat: { id: "combat", group: "sheet", icon: "fas fa-medal", label: "VERMINE.tabs.combat" }
}
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()
return context
}
async _preparePartContext(partId, context) {
const doc = this.document
context.systemFields = doc.system.schema.fields
switch (partId) {
case "main": break
case "abilities":
context.tab = context.tabs.abilities
break
case "totem":
context.tab = context.tabs.totem
context.abilities = doc.itemTypes.ability.filter(i => i.system.type !== "totem")
context.totem_abilities = doc.itemTypes.ability.filter(i => i.system.type === "totem")
context.specialties = doc.itemTypes.specialty
context.backgrounds = doc.itemTypes.background
context.traumas = doc.itemTypes.trauma
context.evolutions = doc.itemTypes.evolution
break
case "equipment":
context.tab = context.tabs.equipment
context.gear = doc.itemTypes.item
context.weapons = doc.itemTypes.weapon
context.defenses = doc.itemTypes.defense
context.vehicles = doc.itemTypes.vehicle
break
case "stories":
context.tab = context.tabs.stories
break
case "combat":
context.tab = context.tabs.combat
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
context.effects = prepareActiveEffectCategories(doc.effects)
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 async #onAddSpecialty(event, target) {
const skillKey = target.dataset.skill
const name = game.i18n.localize("ITEMS.new_specialty")
const itemData = { name, type: "specialty" }
if (skillKey) itemData.system = { skill: skillKey }
await this.document.createEmbeddedDocuments("Item", [itemData])
}
}
@@ -0,0 +1,80 @@
import VermineBaseActorSheet from "./base-actor-sheet.mjs"
export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ["creature"],
position: { width: 700, height: 650 },
window: { contentClasses: ["creature-content"] }
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/creature-main.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
info: { template: "systems/vermine2047/templates/actor/appv2/creature-info.hbs" },
stats: { template: "systems/vermine2047/templates/actor/appv2/creature-stats.hbs" },
combat: { template: "systems/vermine2047/templates/actor/appv2/creature-combat.hbs" },
effects: { template: "systems/vermine2047/templates/actor/appv2/creature-effects.hbs" }
}
tabGroups = { sheet: "info" }
#getTabs() {
const tabs = {
info: { id: "info", group: "sheet", icon: "fas fa-info-circle", label: "VERMINE.information" },
stats: { id: "stats", group: "sheet", icon: "fas fa-chart-bar", label: "VERMINE.stats" },
combat: { id: "combat", group: "sheet", icon: "fas fa-sword", label: "VERMINE.combat" },
effects: { id: "effects", group: "sheet", icon: "fas fa-magic", label: "UI.effects.name" }
}
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()
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")
}
}
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main":
context.patternOptions = CONFIG.VERMINE.creaturePatternLevels
context.roleOptions = CONFIG.VERMINE.creatureRoleLevels
context.sizeOptions = CONFIG.VERMINE.creatureSizeLevels
context.packOptions = CONFIG.VERMINE.creaturePackLevels
break
case "info":
context.tab = context.tabs.info
break
case "stats":
context.tab = context.tabs.stats
context.patternLabel = doc.system.pattern?.value ? CONFIG.VERMINE.creaturePatternLevels[doc.system.pattern.value]?.label : ""
context.sizeLabel = doc.system.size?.value || ""
context.roleLabel = doc.system.role?.value ? CONFIG.VERMINE.creatureRoleLevels[doc.system.role.value]?.label : ""
context.packLabel = doc.system.pack?.value || game.i18n.localize("VERMINE.none")
break
case "combat":
context.tab = context.tabs.combat
break
case "effects":
context.tab = context.tabs.effects
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
context.effects = prepareActiveEffectCategories(doc.effects)
break
}
return context
}
}
+138
View File
@@ -0,0 +1,138 @@
import VermineBaseActorSheet from "./base-actor-sheet.mjs"
export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ["group"],
position: { width: 700, height: 600 },
window: { contentClasses: ["group-content"] },
actions: {
chooseActor: VermineGroupSheetV2.#onChooseActor,
deleteMember: VermineGroupSheetV2.#onDeleteMember,
deleteEncounter: VermineGroupSheetV2.#onDeleteEncounter,
deleteObjective: VermineGroupSheetV2.#onDeleteObjective,
addObjective: VermineGroupSheetV2.#onAddObjective
}
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/group-main.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
info: { template: "systems/vermine2047/templates/actor/appv2/group-info.hbs" },
gear: { template: "systems/vermine2047/templates/actor/appv2/group-gear.hbs" },
road: { template: "systems/vermine2047/templates/actor/appv2/group-road.hbs" },
reserve: { template: "systems/vermine2047/templates/actor/appv2/group-reserve.hbs" }
}
tabGroups = { sheet: "info" }
#getTabs() {
const tabs = {
info: { id: "info", group: "sheet", icon: "fas fa-star", label: "VERMINE.information" },
gear: { id: "gear", group: "sheet", icon: "fas fa-gear", label: "VERMINE.gear" },
road: { id: "road", group: "sheet", icon: "fas fa-route", label: "VERMINE.road" },
reserve: { id: "reserve", group: "sheet", icon: "fas fa-users", label: "VERMINE.reserve" }
}
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()
// Résoudre les IDs des membres/encounters en données acteur
context.resolvedMembers = {}
if (this.document.system.members?.length > 0) {
for (const memberId of this.document.system.members) {
const a = game.actors.get(memberId)
if (a) context.resolvedMembers[memberId] = { name: a.name, id: a.id }
}
}
context.resolvedEncounters = {}
if (this.document.system.encounters?.length > 0) {
for (const encId of this.document.system.encounters) {
const a = game.actors.get(encId)
if (a) context.resolvedEncounters[encId] = { name: a.name, id: a.id }
}
}
return context
}
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main": break
case "info":
context.tab = context.tabs.info
context.abilities = doc.itemTypes.ability
context.specialties = doc.itemTypes.specialty
context.backgrounds = doc.itemTypes.background
context.traumas = doc.itemTypes.trauma
context.evolutions = doc.itemTypes.evolution
break
case "gear":
context.tab = context.tabs.gear
context.gear = doc.itemTypes.item
context.weapons = doc.itemTypes.weapon
context.defenses = doc.itemTypes.defense
break
case "road":
context.tab = context.tabs.road
context.vehicles = doc.itemTypes.vehicle
break
case "reserve":
context.tab = context.tabs.reserve
break
}
return context
}
// Actions : délégation aux applications AppV1 existantes pour TotemPicker/ActorPicker
static async #onChooseTotem(event, target) {
const { TotemPicker } = await import("../../system/applications.mjs")
new TotemPicker(target, this.document).render(true)
}
static async #onChooseActor(event, target) {
const { ActorPicker } = await import("../../system/applications.mjs")
new ActorPicker(target, this.document).render(true)
}
static #onDeleteMember(event, target) {
const li = target.closest("li.actor")
if (!li) return
const actorId = li.dataset.actorId
const idx = this.document.system.members.indexOf(actorId)
if (idx !== -1) {
const members = [...this.document.system.members]
members.splice(idx, 1)
this.document.update({ "system.members": members })
}
}
static #onDeleteEncounter(event, target) {
const li = target.closest("li.actor")
if (!li) return
const actorId = li.dataset.actorId
const idx = this.document.system.encounters.indexOf(actorId)
if (idx !== -1) {
const encounters = [...this.document.system.encounters]
encounters.splice(idx, 1)
this.document.update({ "system.encounters": encounters })
}
}
static #onDeleteObjective(event, target) {
const type = target.dataset.type
const index = parseInt(target.dataset.index)
if (isNaN(index)) return
const objectives = foundry.utils.duplicate(this.document.system.objectives || { major: [], minor: [] })
objectives[type].splice(index, 1)
this.document.update({ "system.objectives": objectives })
}
static #onAddObjective(event, target) {
const type = target.dataset.type === "major_objective" ? "major" : "minor"
const objectives = foundry.utils.duplicate(this.document.system.objectives || { major: [], minor: [] })
objectives[type].push("")
this.document.update({ "system.objectives": objectives })
}
}
@@ -0,0 +1,73 @@
import { VermineBaseItemSheet } from "./base-item-sheet.mjs"
// ── Item générique ────────────────────────────────────────────────────
export class VermineItemSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["item-gear"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-item-sheet.hbs" } }
}
// ── Arme ──────────────────────────────────────────────────────────────
export class VermineWeaponSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["weapon"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-weapon-sheet.hbs" } }
}
// ── Défense ───────────────────────────────────────────────────────────
export class VermineDefenseSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["defense"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-defense-sheet.hbs" } }
}
// ── Véhicule ──────────────────────────────────────────────────────────
export class VermineVehicleSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["vehicle"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-vehicle-sheet.hbs" } }
}
// ── Capacité ──────────────────────────────────────────────────────────
export class VermineAbilitySheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["ability"], position: { width: 560 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-ability-sheet.hbs" } }
}
// ── Spécialité ────────────────────────────────────────────────────────
export class VermineSpecialtySheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["specialty"], position: { width: 400 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-specialty-sheet.hbs" } }
}
// ── Historique ────────────────────────────────────────────────────────
export class VermineBackgroundSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["background"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-background-sheet.hbs" } }
}
// ── Traumatisme ───────────────────────────────────────────────────────
export class VermineTraumaSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["trauma"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-trauma-sheet.hbs" } }
}
// ── Évolution ─────────────────────────────────────────────────────────
export class VermineEvolutionSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["evolution"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-evolution-sheet.hbs" } }
}
// ── Rumeur ────────────────────────────────────────────────────────────
export class VermineRumorSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["rumor"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rumor-sheet.hbs" } }
}
// ── Cible ─────────────────────────────────────────────────────────────
export class VermineTargetSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["target"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-target-sheet.hbs" } }
}
// ── Rite ──────────────────────────────────────────────────────────────
export class VermineRiteSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["rite"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rite-sheet.hbs" } }
}
+74
View File
@@ -0,0 +1,74 @@
import VermineBaseActorSheet from "./base-actor-sheet.mjs"
export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ["npc"],
position: { width: 750, height: 680 },
window: { contentClasses: ["npc-content"] }
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/npc-main.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
characteristics: { template: "systems/vermine2047/templates/actor/appv2/npc-characteristics.hbs" },
skills: { template: "systems/vermine2047/templates/actor/appv2/npc-skills.hbs" },
threat: { template: "systems/vermine2047/templates/actor/appv2/npc-threat.hbs" },
combat: { template: "systems/vermine2047/templates/actor/appv2/npc-combat.hbs" },
notes: { template: "systems/vermine2047/templates/actor/appv2/npc-notes.hbs" }
}
tabGroups = { sheet: "characteristics" }
#getTabs() {
const tabs = {
characteristics: { id: "characteristics", group: "sheet", icon: "fas fa-dice", label: "VERMINE.abilities" },
skills: { id: "skills", group: "sheet", icon: "fas fa-brain", label: "VERMINE.skills" },
threat: { id: "threat", group: "sheet", icon: "fas fa-exclamation-triangle", label: "ADVERSITY.threat" },
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" }
}
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()
return context
}
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main":
context.npcThreatOptions = CONFIG.VERMINE.npcThreatLevels
context.npcExperienceOptions = CONFIG.VERMINE.npcExperienceLevels
context.npcRoleOptions = CONFIG.VERMINE.npcRoleLevels
context.totemOptions = CONFIG.VERMINE.totems
context.originOptions = CONFIG.VERMINE.origins
break
case "characteristics":
context.tab = context.tabs.characteristics
break
case "skills":
context.tab = context.tabs.skills
break
case "threat":
context.tab = context.tabs.threat
break
case "combat":
context.tab = context.tabs.combat
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
context.effects = prepareActiveEffectCategories(doc.effects)
break
case "notes":
context.tab = context.tabs.notes
break
}
return context
}
}
+7
View File
@@ -0,0 +1,7 @@
/**
* Module de ré-export des classes de documents
* Compatible avec Foundry V2
*/
export { default as VermineActor } from "./actor.mjs";
export { default as VermineItem } from "./item.mjs";
+286 -28
View File
@@ -3,30 +3,7 @@
* Extend the base Actor document by defining a custom roll data structure which is ideal for the Simple system.
* @extends {Actor}
*/
export class VermineActor extends Actor {
/** @override */
prepareData() {
// Prepare data for the actor. Calling the super version of this executes
// the following, in order: data reset (to clear active effects),
// prepareBaseData(), prepareEmbeddedDocuments() (including active effects),
// prepareDerivedData().
super.prepareData();
}
/** @override */
prepareBaseData() {
// Data modifications in this step occur before processing embedded
// documents or derived data.
if (this.type == 'character') {
this._setAgeType();
this._setCharacterEffort();
this._setCharacterSelfControl();
this._setCharacterThresholds();
}
}
export default class VermineActor extends Actor {
/**
* @override
@@ -38,14 +15,28 @@ export class VermineActor extends Actor {
* is queried and has a roll executed directly from it).
*/
prepareDerivedData() {
super.prepareDerivedData();
const actorData = this;
const systemData = actorData.system;
const flags = actorData.flags.vermine2047 || {};
// Make separate methods for each Actor type (character, npc, etc.) to keep
// things organized.
this._prepareCharacterData(actorData);
this._prepareNpcData(actorData);
switch (this.type) {
case "character":
this._prepareCharacterData(actorData);
break;
case "npc":
this._prepareNpcData(actorData);
break;
case "group":
this._prepareGroupData(actorData);
break;
case "creature":
this._prepareCreatureData(actorData);
break;
}
}
/**
@@ -53,7 +44,10 @@ export class VermineActor extends Actor {
*/
_prepareCharacterData(actorData) {
if (actorData.type !== 'character') return;
this._setAgeType();
this._setCharacterEffort();
this._setCharacterSelfControl();
this._setCharacterThresholds();
// Make modifications to data here. For example:
const systemData = actorData.system;
@@ -62,8 +56,44 @@ export class VermineActor extends Actor {
// 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.
@@ -73,7 +103,235 @@ export class VermineActor extends Actor {
// Make modifications to data here. For example:
const systemData = actorData.system;
systemData.xp = (systemData.cr * systemData.cr) * 100;
// 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.
*/
_prepareGroupData(actorData) {
if (actorData.type !== 'group') return;
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.
* Calculates computed values from pattern, size, role, and pack.
*/
_prepareCreatureData(actorData) {
if (actorData.type !== 'creature') return;
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);
}
/**
+29 -46
View File
@@ -2,7 +2,7 @@
* Extend the basic Item with some very simple modifications.
* @extends {Item}
*/
export class VermineItem extends Item {
export default class VermineItem extends Item {
/**
* Augment the basic Item data model with additional dynamic data.
*/
@@ -13,38 +13,41 @@ export class VermineItem extends Item {
}
prepareBaseData() {
const actorType = (this.actor !== null) ? this.actor.type : 'character';
const itemType = this.type;
switch (this.type) {
case 'ability':
if (this.system.type == "") {
// console.log('je suis une capacité, avec pour sous-type', this.system.type, actorType);
this.system.type = actorType;
}
if (this.system.totem == "" && this.actor !== null && this.actor.system.identity.totem != "") {
// console.log('je suis une capacité, avec pour sous-type', this.system.type, actorType);
this.system.totem = this.actor.system.identity.totem;
}
break;
default:
break;
// Vérifie si une méthode spécifique au type existe
if (typeof this[`prepare${itemType.charAt(0).toUpperCase() + itemType.slice(1)}Data`] === 'function') {
this[`prepare${itemType.charAt(0).toUpperCase() + itemType.slice(1)}Data`]();
}
// si dégats sur l'item, application du damage label et damage icon
if (this.system.damages?.value) {
this.damagedLabel = this.system.damages.state[parseInt(this.system.damages?.value) - 1];
switch (this.damagedLabel) {
case "endommagé":
this.damagedIcon = '<i class="fas fa-exclamation-circle" style:"color="yellow"></i>';
this.damagedIcon = '<i class="fas fa-exclamation-circle" style="color=yellow"></i>';
break;
case "défectueux":
this.damagedIcon = '<i class="fas fa-exclamation-triangle" style:"color="orange"></i>';
this.damagedIcon = '<i class="fas fa-exclamation-triangle" style="color=orange"></i>';
break;
case "hors d'usage":
this.damagedIcon = '<i class="fas fa-star-exclamation" style:"color="red"></i>';
this.damagedIcon = '<i class="fas fa-star-exclamation" style="color=red"></i>';
break;
}
}
}
prepareAbilityData() {
console.log('ability data', this)
const actorType = (this.actor !== null) ? this.actor.type : 'character';
if (this.system.type == "") {
this.system.type = actorType;
}
if (this.system.totem == "" && this.actor !== null && this.actor.system.identity.totem != "") {
this.system.totem = this.actor.system.identity.totem;
}
}
/**
* Prepare a data object which is passed to any Roll formulas which are created related to this Item
* @private
@@ -65,9 +68,6 @@ export class VermineItem extends Item {
* @private
*/
async roll() {
if (this.type == "weapon") {
this.rollWeapon()
}
const item = this;
// Initialize chat data.
@@ -75,30 +75,13 @@ export class VermineItem extends Item {
const rollMode = game.settings.get('core', 'rollMode');
const label = `[${item.type}] ${item.name}`;
// If there's no roll data, send a chat message.
if (!this.system.formula) {
ChatMessage.create({
speaker: speaker,
rollMode: rollMode,
flavor: label,
content: item.system.description ?? ''
});
}
// Otherwise, create a roll and send a chat message from it.
else {
// Retrieve roll data.
const rollData = this.getRollData();
// Invoke the roll and submit it to chat.
const roll = new Roll(rollData.item.formula, rollData);
// If you need to store the value first, uncomment the next line.
// let result = await roll.roll({async: true});
roll.toMessage({
speaker: speaker,
rollMode: rollMode,
flavor: label,
});
return roll;
}
let mess = {
speaker: speaker,
rollMode: rollMode,
flavor: label,
};
mess.content = await foundry.applications.handlebars.renderTemplate(`systems/vermine2047/templates/item/chatCards/${this.type}.hbs`, { item: this, message: mess, config: CONFIG.VERMINE }) ?? null;
ChatMessage.create(mess)
}
}
+16
View File
@@ -0,0 +1,16 @@
export { default as VermineCharacterData } from "./character.mjs"
export { default as VermineNpcData } from "./npc.mjs"
export { default as VermineGroupData } from "./group.mjs"
export { default as VermineCreatureData } from "./creature.mjs"
export { default as VermineItemData } from "./item.mjs"
export { default as VermineWeaponData } from "./weapon.mjs"
export { default as VermineDefenseData } from "./defense.mjs"
export { default as VermineVehicleData } from "./vehicle.mjs"
export { default as VermineAbilityData } from "./ability.mjs"
export { default as VermineSpecialtyData } from "./specialty.mjs"
export { default as VermineBackgroundData } from "./background.mjs"
export { default as VermineTraumaData } from "./trauma.mjs"
export { default as VermineEvolutionData } from "./evolution.mjs"
export { default as VermineRumorData } from "./rumor.mjs"
export { default as VermineTargetData } from "./target.mjs"
export { default as VermineRiteData } from "./rite.mjs"
+297
View File
@@ -0,0 +1,297 @@
/**
* Schémas partagés pour les DataModels Vermine 2047.
* Fonctions factory retournant des objets SchemaField réutilisables.
*/
const fields = foundry.data.fields
/** NumberField qui accepte les strings vides en les remplaçant par `initial`. */
class LooseNumberField extends fields.NumberField {
clean(value, options) {
if (value === "" || value === null || value === undefined) {
return this.initial ?? 0
}
return super.clean(value, options)
}
}
/**
* Retourne un schema pour une blessure (minor/major/deadly)
* @param {number} defaultThreshold
* @param {number} defaultMax
* @returns {Object}
*/
export function woundSchema(defaultThreshold = 1, defaultMax = 5) {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
threshold: new fields.NumberField({ ...reqInt, initial: defaultThreshold, min: 0 }),
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: defaultMax, min: 0 })
}
}
/**
* Schema des 3 types de blessures présents sur tous les acteurs.
*/
export function woundsSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
minorWound: new fields.SchemaField(woundSchema(1, 5)),
majorWound: new fields.SchemaField(woundSchema(4, 4)),
deadlyWound: new fields.SchemaField(woundSchema(8, 2))
})
}
/**
* Statut de combat (offensif/actif/passif).
*/
export function combatStatusSchema(defaultDifficulty = "7") {
const fields = foundry.data.fields
return new fields.SchemaField({
label: new fields.StringField({ required: true, nullable: false, initial: "" }),
difficulty: new fields.StringField({ required: true, nullable: false, initial: defaultDifficulty })
})
}
/**
* Description d'équipement.
*/
export function equipmentSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
description: new fields.HTMLField({ required: true, initial: "", textSearch: true })
})
}
/**
* Attribut avec value/min/max.
* @param {number} defaultVal
* @param {number} defaultMin
* @param {number} defaultMax
*/
export function attributeSchema(defaultVal = 0, defaultMin = 0, defaultMax = 10) {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return new fields.SchemaField({
value: new LooseNumberField({ ...reqInt, initial: defaultVal, min: defaultMin }),
min: new fields.NumberField({ ...reqInt, initial: defaultMin, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: defaultMax, min: 0 })
})
}
/**
* Caractéristique (capacité) avec catégorie.
* @param {string} category - physical, manual, mental, social
*/
export function abilityField(category) {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 1, min: 0, max: 5 }),
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: 5, min: 0 }),
category: new fields.StringField({ required: true, nullable: false, initial: category })
})
}
/**
* Les 8 caractéristiques communes à character et npc.
*/
export function abilitiesSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
vigor: abilityField("physical"),
health: abilityField("physical"),
precision: abilityField("manual"),
reflexes: abilityField("manual"),
knowledge: abilityField("mental"),
perception: abilityField("mental"),
will: abilityField("social"),
empathy: abilityField("social")
})
}
/**
* Une compétence individuelle.
* @param {string} category
* @param {number} rarity - 0, 1, ou 2
*/
export function skillField(category, rarity = 0) {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 5 }),
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: 5, min: 0 }),
category: new fields.StringField({ required: true, nullable: false, initial: category }),
rarity: new fields.NumberField({ ...reqInt, initial: rarity, min: 0, max: 2 })
})
}
/**
* Les 30 compétences (character et npc).
*/
export function skillsSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
// man
arts: skillField("man", 1),
civilization: skillField("man", 2),
psychology: skillField("man", 1),
rumors: skillField("man", 0),
healing: skillField("man", 1),
// animal
animalism: skillField("animal", 1),
dissection: skillField("animal", 2),
wildlife: skillField("animal", 1),
repulsion: skillField("animal", 0),
tracks: skillField("animal", 0),
// tool
crafting: skillField("tool", 2),
diy: skillField("tool", 0),
mecanical: skillField("tool", 2),
piloting: skillField("tool", 1),
technology: skillField("tool", 2),
// weapon
firearms: skillField("weapon", 2),
archery: skillField("weapon", 0),
armory: skillField("weapon", 2),
throwing: skillField("weapon", 0),
melee: skillField("weapon", 0),
// survival
alertness: skillField("survival", 0),
atletics: skillField("survival", 0),
food: skillField("survival", 0),
stealth: skillField("survival", 0),
close: skillField("survival", 0),
// world
environment: skillField("world", 1),
flora: skillField("world", 1),
road: skillField("world", 0),
toxics: skillField("world", 2),
ruins: skillField("world", 1)
})
}
/**
* Catégories de compétences avec domaine de prédilection.
*/
export function skillCategoriesSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
preferred: new fields.StringField({ required: true, nullable: false, initial: "" }),
man: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "VERMINE.skill_category.man" }),
preferred: new fields.BooleanField({ required: true, initial: false })
}),
animal: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "VERMINE.skill_category.animal" }),
preferred: new fields.BooleanField({ required: true, initial: false })
}),
tool: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "VERMINE.skill_category.tool" }),
preferred: new fields.BooleanField({ required: true, initial: false })
}),
weapon: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "VERMINE.skill_category.weapon" }),
preferred: new fields.BooleanField({ required: true, initial: false })
}),
survival: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "VERMINE.skill_category.survival" }),
preferred: new fields.BooleanField({ required: true, initial: false })
}),
world: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "VERMINE.skill_category.world" }),
preferred: new fields.BooleanField({ required: true, initial: false })
})
})
}
// ── Item shared schemas ──────────────────────────────────────────────────
const reqInt = { required: true, nullable: false, integer: true }
/**
* Rareté avec handicap.
*/
export function raritySchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 3, min: 1, max: 5 }),
handicap: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
})
}
/**
* Dégâts des items (hors arme).
*/
export function itemDamagesSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 5 }),
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: 5, min: 0 }),
pannes: new fields.ArrayField(new fields.StringField({ required: true, initial: "" }), {
initial: ["mineure", "mineure", "grave", "grave", "critique"]
}),
state: new fields.ArrayField(new fields.StringField({ required: true, initial: "" }), {
initial: ["endommagé", "endommagé", "défectueux", "défectueux", "hors d'usage"]
}),
effect: new fields.ArrayField(new fields.StringField({ required: true, initial: "" }), {
initial: ["bonus annulé", "bonus annulé", "malus 1D", "malus 1D", "inutilisable"]
})
})
}
/**
* Base commune à tous les items (template "base" dans l'ancien template.json).
*/
export function baseItemSchema() {
const fields = foundry.data.fields
return {
description: new fields.HTMLField({ required: true, initial: "", textSearch: true }),
rarity: raritySchema(),
reliability: new fields.NumberField({ ...reqInt, initial: 3, min: 1, max: 5 }),
handicap: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
quantity: new fields.NumberField({ ...reqInt, initial: 1, min: 1 }),
weight: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
traits: new fields.ObjectField({ required: true, initial: {} }),
damages: itemDamagesSchema()
}
}
/**
* Template "list" pour les items abstraits (ability, background, trauma, evolution, rumor, target).
* Version légère avec seulement description.
*/
export function listItemSchema() {
const fields = foundry.data.fields
return {
description: new fields.HTMLField({ required: true, initial: "", textSearch: true })
}
}
/**
* Schéma d'apprentissage pour les abilities.
*/
export function learnSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
threshold: new fields.NumberField({ ...reqInt, initial: 5, min: 0 }),
hindrance: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
})
}
/**
* Niveau générique (value/min/max).
*/
export function levelSchema(defaultVal = 1, defaultMin = 1, defaultMax = 5) {
const fields = foundry.data.fields
return new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: defaultVal, min: defaultMin }),
min: new fields.NumberField({ ...reqInt, initial: defaultMin, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: defaultMax, min: 0 })
})
}
+25
View File
@@ -0,0 +1,25 @@
import { listItemSchema, learnSchema, levelSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "ability" (capacités, pouvoirs).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineAbilityData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.ability"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...listItemSchema(),
type: new fields.StringField({ required: true, initial: "" }),
totem: new fields.StringField({ required: true, initial: "" }),
learn: learnSchema(),
level: levelSchema(1, 1, 3),
effects: new fields.ArrayField(new fields.StringField({ required: true, initial: "" }), {
initial: []
})
}
}
}
+20
View File
@@ -0,0 +1,20 @@
import { listItemSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "background" (historiques, origines).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineBackgroundData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.background"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
...listItemSchema(),
cost: new fields.NumberField({ ...reqInt, initial: 1, min: 0 })
}
}
}
+215
View File
@@ -0,0 +1,215 @@
/**
* DataModel pour les acteurs de type "character" (personnage).
* Étend foundry.abstract.TypeDataModel.
*/
import {
woundSchema,
combatStatusSchema,
equipmentSchema,
attributeSchema,
abilitiesSchema,
skillCategoriesSchema,
skillsSchema
} from "./_shared.mjs"
export default class VermineCharacterData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.character"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
// Blessures (base)
minorWound: new fields.SchemaField(woundSchema(1, 5)),
majorWound: new fields.SchemaField(woundSchema(4, 4)),
deadlyWound: new fields.SchemaField(woundSchema(8, 2)),
// Statut de combat (base)
combatStatus: combatStatusSchema("7"),
// Adaptation (totems humain/adapté)
adaptation: new fields.SchemaField({
value: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1, min: 0, max: 5 }),
min: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
max: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 5, min: 0 }),
totems: new fields.SchemaField({
human: new fields.SchemaField({
value: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1, min: 0, max: 3 }),
min: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
max: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 3, min: 0 })
}),
adapted: new fields.SchemaField({
value: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1, min: 0, max: 3 }),
min: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
max: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 3, min: 0 })
})
})
}),
// Identité
identity: new fields.SchemaField({
height: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
weight: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
age: new fields.StringField({ required: true, nullable: false, initial: "15" }),
ageType: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 2 }),
profile: new fields.StringField({ required: true, nullable: false, initial: "" }),
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: "" }),
objectives: new fields.StringField({ required: true, nullable: false, initial: "" }),
relations: new fields.HTMLField({ required: true, initial: "" }),
biography: new fields.HTMLField({ required: true, initial: "" })
}),
// Équipement
equipment: equipmentSchema(),
// Attributs (XP, réputation, sang-froid, effort)
attributes: new fields.SchemaField({
xp: attributeSchema(0, 0, 10),
reputation: attributeSchema(0, 0, 10),
self_control: attributeSchema(0, 0, 5),
effort: attributeSchema(0, 0, 5)
}),
// Rencontres
encounters: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
// Caractéristiques (8)
abilities: abilitiesSchema(),
// Catégories de compétences
skill_categories: skillCategoriesSchema(),
// Compétences (30)
skills: skillsSchema()
}
}
/** @override */
prepareDerivedData() {
super.prepareDerivedData()
// 1. Déterminer la tranche d'âge
this._setAgeType()
// 2. Calculer les modificateurs de caractéristiques
this._setAbilityModifiers()
// 3. Calculer les réserves (sang-froid et effort)
this._setSelfControlMax()
this._setEffortMax()
// 4. Calculer les seuils de blessures
this._setWoundThresholds()
// 5. Mettre à jour le statut de combat
this._updateCombatStatus()
}
/**
* Détermine la tranche d'âge (1=jeune, 2=adulte, 3=vieux)
* à partir de l'âge et de la config VERMINE.AgeTypes.
*/
_setAgeType() {
const age = this.identity.age
const ageTypes = CONFIG.VERMINE.AgeTypes
for (const [type, cfg] of Object.entries(ageTypes)) {
if (age >= parseInt(cfg.beginning, 10)) {
this.identity.ageType = parseInt(type, 10)
}
}
}
/**
* 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 :
* somme des caractéristiques mentales + sociales + modificateur d'âge.
*/
_setSelfControlMax() {
const abilities = Object.values(this.abilities)
const modFromAge = this._getModFromAgeSelfControl()
const sum = abilities
.filter(a => a.category === "mental" || a.category === "social")
.reduce((acc, a) => acc + a.value, 0)
this.attributes.self_control.max = sum + modFromAge
}
/**
* Calcule le max d'effort :
* somme des caractéristiques physiques + manuelles + modificateur d'âge.
*/
_setEffortMax() {
const abilities = Object.values(this.abilities)
const modFromAge = this._getModFromAgeEffort()
const sum = abilities
.filter(a => a.category === "physical" || a.category === "manual")
.reduce((acc, a) => acc + a.value, 0)
this.attributes.effort.max = sum + modFromAge
}
/**
* Calcule les seuils de blessures à partir de la Santé.
*/
_setWoundThresholds() {
const health = this.abilities.health.value
const ageMods = this._getModFromAgeWounds()
this.minorWound.threshold = health
this.majorWound.threshold = health + 3
this.deadlyWound.threshold = (health + 7 < 11) ? health + 7 : 10
this.minorWound.max = 4 + ageMods.l
this.majorWound.max = 3 + ageMods.h
this.deadlyWound.max = 2 + ageMods.d
}
/**
* Met à jour le label du statut de combat en fonction de la difficulté.
*/
_updateCombatStatus() {
const difficulty = parseInt(this.combatStatus.difficulty) || 9
let newLabel = "Passif"
switch (difficulty) {
case 5: newLabel = "Offensif"; break
case 7: newLabel = "Actif"; break
case 9: newLabel = "Passif"; break
}
if (this.combatStatus.label !== newLabel) {
this.combatStatus.label = newLabel
}
}
// ── Modificateurs liés à l'âge ──────────────────────────────────────
/** @returns {number} Modificateur de sang-froid selon l'âge. */
_getModFromAgeSelfControl() {
return this.identity.ageType === 1 ? -1 : 0
}
/** @returns {number} Modificateur d'effort selon l'âge. */
_getModFromAgeEffort() {
if (this.identity.ageType === 1) return -1
if (this.identity.ageType === 3) return -2
return 0
}
/** @returns {{l: number, h: number, d: number}} Modificateurs de blessures selon l'âge. */
_getModFromAgeWounds() {
if (this.identity.ageType === 1) return { l: 0, h: 0, d: -1 }
if (this.identity.ageType === 3) return { l: -1, h: -1, d: -1 }
return { l: 0, h: 0, d: 0 }
}
}
+182
View File
@@ -0,0 +1,182 @@
/**
* DataModel pour les acteurs de type "creature" (créature).
* Étend foundry.abstract.TypeDataModel.
*/
import {
woundSchema,
combatStatusSchema,
equipmentSchema,
attributeSchema
} from "./_shared.mjs"
export default class VermineCreatureData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.creature"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
// Blessures (base)
minorWound: new fields.SchemaField(woundSchema(1, 5)),
majorWound: new fields.SchemaField(woundSchema(4, 4)),
deadlyWound: new fields.SchemaField(woundSchema(8, 2)),
// Statut de combat (base)
combatStatus: combatStatusSchema(),
// Identité
identity: new fields.SchemaField({
profile: new fields.StringField({ required: true, nullable: false, initial: "" }),
origin: new fields.StringField({ required: true, nullable: false, initial: "" }),
theme: new fields.StringField({ required: true, nullable: false, initial: "" }),
notes: new fields.HTMLField({ required: true, initial: "" }),
biography: new fields.HTMLField({ required: true, initial: "" })
}),
// Compétences (description libre)
skills: new fields.StringField({ required: true, nullable: false, initial: "" }),
// Modes de jeu actifs
modes: new fields.SchemaField({
survival: new fields.BooleanField({ required: true, initial: true }),
nightmare: new fields.BooleanField({ required: true, initial: true }),
apocalypse: new fields.BooleanField({ required: true, initial: false })
}),
// Niveaux de créature (patron, taille, rôle, meute)
pattern: attributeSchema(1, 1, 4),
size: attributeSchema(1, 1, 3),
role: attributeSchema(1, 1, 4),
pack: attributeSchema(0, 0, 3),
// Valeurs calculées (dérivées de pattern/size/role/pack)
computed: new fields.SchemaField({
attack: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
damage: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
vigor: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
reaction: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
reactionBonus: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
pools: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
gear: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 9 }),
gearHindrance: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
protection: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1 })
}),
// Équipement
equipment: equipmentSchema()
}
}
/** @override */
prepareDerivedData() {
super.prepareDerivedData()
// 1. Calculer les valeurs dérivées (attaque, dégâts, vigueur, etc.)
this._calculateCreatureComputedValues()
// 2. Calculer les seuils de blessures
this._calculateCreatureWoundThresholds()
// 3. Mettre à jour le statut de combat
this._updateCombatStatus()
}
/**
* Calcule les valeurs dérivées à partir des niveaux de patron, taille, rôle et meute.
* Utilise les configs CONFIG.VERMINE.creaturePatternLevels, .creatureSizeLevels,
* .creatureRoleLevels, .creaturePackLevels.
*
* Règles :
* - Attaque = pattern.attack + size.attack + pack.attack + role.reaction
* - Dégâts = pattern.damage + size.vigor + pack.damage
* - Vigueur = size.vigor + pack.damage
* - Réaction = role.reaction + role.reaction_bonus
*/
_calculateCreatureComputedValues() {
const patternLevel = this.pattern?.value || 1
const sizeLevel = this.size?.value || 1
const roleLevel = this.role?.value || 1
const packLevel = this.pack?.value || 0
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {}
const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {}
const roleConfig = CONFIG.VERMINE.creatureRoleLevels[roleLevel] || {}
const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {}
// Attaque : patron + taille + meute + réaction du rôle
this.computed.attack = (patternConfig.attack || 0)
+ (sizeConfig.attack || 0)
+ (packConfig.attack || 0)
+ (roleConfig.reaction || 0)
// Dégâts : patron + vigueur de taille + meute
this.computed.damage = (patternConfig.damage || 0)
+ (sizeConfig.vigor || 0)
+ (packConfig.damage || 0)
// Vigueur : taille + meute
this.computed.vigor = (sizeConfig.vigor || 0) + (packConfig.damage || 0)
// Réaction : rôle
this.computed.reaction = (roleConfig.reaction || 0) + (roleConfig.reaction_bonus || 0)
this.computed.reactionBonus = roleConfig.reaction_bonus || 0
// Réserves
this.computed.pools = roleConfig.pools || 0
// Équipement et handicap
this.computed.gear = roleConfig.gear || 9
this.computed.gearHindrance = roleConfig.gear_hindrance || 0
// Protection
this.computed.protection = roleConfig.protection || 1
}
/**
* Calcule les seuils de blessures à partir du patron, de la taille et de la meute.
* Les seuils sont la somme des valeurs correspondantes des trois sources.
*/
_calculateCreatureWoundThresholds() {
const patternLevel = this.pattern?.value || 1
const sizeLevel = this.size?.value || 1
const packLevel = this.pack?.value || 0
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {}
const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {}
const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {}
this.minorWound.threshold = (patternConfig.minorWound || 0)
+ (sizeConfig.minorWound || 0)
+ (packConfig.minorWound || 0)
this.majorWound.threshold = (patternConfig.majorWound || 0)
+ (sizeConfig.majorWound || 0)
+ (packConfig.majorWound || 0)
this.deadlyWound.threshold = (patternConfig.deadlyWound || 0)
+ (sizeConfig.deadlyWound || 0)
+ (packConfig.deadlyWound || 0)
// Max de blessures
this.minorWound.max = Math.min(5, this.minorWound.threshold + 2)
this.majorWound.max = Math.min(4, this.majorWound.threshold + 1)
this.deadlyWound.max = Math.min(2, this.deadlyWound.threshold)
}
/**
* Met à jour le label du statut de combat en fonction de la difficulté.
*/
_updateCombatStatus() {
const difficulty = parseInt(this.combatStatus.difficulty) || 9
let newLabel = "Passif"
switch (difficulty) {
case 5: newLabel = "Offensif"; break
case 7: newLabel = "Actif"; break
case 9: newLabel = "Passif"; break
}
if (this.combatStatus.label !== newLabel) {
this.combatStatus.label = newLabel
}
}
}
+26
View File
@@ -0,0 +1,26 @@
import { baseItemSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "defense" (protections, armures, boucliers).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineDefenseData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.defense"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
...baseItemSchema(),
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
specificLevel: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "" }),
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
}),
mobility: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
isShield: new fields.BooleanField({ required: true, initial: false })
}
}
}
+19
View File
@@ -0,0 +1,19 @@
import { listItemSchema, levelSchema } from './_shared.mjs'
/**
* DataModel pour les items de type "evolution" (évolutions du personnage).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineEvolutionData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.evolution"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...listItemSchema(),
level: levelSchema(1, 1, 4)
}
}
}
+164
View File
@@ -0,0 +1,164 @@
/**
* DataModel pour les acteurs de type "group" (groupe).
* Étend foundry.abstract.TypeDataModel.
*/
import {
woundSchema,
combatStatusSchema,
equipmentSchema,
attributeSchema,
levelSchema
} from "./_shared.mjs"
export default class VermineGroupData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.group"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
// Blessures (base)
minorWound: new fields.SchemaField(woundSchema(1, 5)),
majorWound: new fields.SchemaField(woundSchema(4, 4)),
deadlyWound: new fields.SchemaField(woundSchema(8, 2)),
// Statut de combat (base)
combatStatus: combatStatusSchema(),
// Identité du groupe
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: "" }),
notes: new fields.HTMLField({ required: true, initial: "" })
}),
// Équipement
equipment: equipmentSchema(),
// Niveau du groupe (1-10)
level: levelSchema(1, 1, 10),
// Réputation
reputation: new fields.SchemaField({
value: new fields.NumberField({ required: true, nullable: true, integer: true, initial: 10, min: 2 }),
min: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 2, min: 0 }),
max: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 10, min: 0 })
}),
// Moral
morale: new fields.SchemaField({
level: new fields.StringField({ required: true, nullable: false, initial: "high" }),
value: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 7, min: 0, max: 7 }),
min: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
max: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 7, min: 0 })
}),
// Réserve
reserve: attributeSchema(0, 0, 10),
// Objectifs (majeurs et mineurs)
objectives: new fields.SchemaField({
major: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
minor: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" }))
}),
// Capacités de groupe
groupAbilities: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
// Membres (IDs d'acteurs)
members: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
// Rencontres
encounters: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" }))
}
}
/** @override */
prepareDerivedData() {
super.prepareDerivedData()
// 1. Initialiser les données de groupe si absentes
this._initGroupData()
// 2. Calculer la réserve max selon le niveau
this._calculateGroupReserve()
// 3. Mettre à jour le moral selon la valeur de dés
this._updateGroupMorale()
// 4. Mettre à jour le statut de combat
this._updateCombatStatus()
}
/**
* Initialise les champs optionnels du groupe s'ils ne sont pas présents.
*/
_initGroupData() {
if (!this.objectives) {
this.objectives = { major: [], minor: [] }
}
if (!this.groupAbilities) {
this.groupAbilities = []
}
if (!this.reserve) {
this.reserve = { value: 0, min: 0, max: 10 }
}
}
/**
* Calcule la réserve max en fonction du niveau du groupe.
* Règle simplifiée : niveau × 2, plafonné à 10.
*/
_calculateGroupReserve() {
const level = this.level?.value || 1
this.reserve.max = Math.min(10, level * 2)
if (this.reserve.value > this.reserve.max) {
this.reserve.value = this.reserve.max
}
}
/**
* 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.
*/
_updateGroupMorale() {
const moraleValue = this.morale?.value || 0
// Ne pas écraser un niveau explicitement défini (sauf "high" qui est la valeur par défaut)
if (this.morale.level && this.morale.level !== "high") 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"
}
}
/**
* Met à jour le label du statut de combat en fonction de la difficulté.
*/
_updateCombatStatus() {
const difficulty = parseInt(this.combatStatus.difficulty) || 9
let newLabel = "Passif"
switch (difficulty) {
case 5: newLabel = "Offensif"; break
case 7: newLabel = "Actif"; break
case 9: newLabel = "Passif"; break
}
if (this.combatStatus.label !== newLabel) {
this.combatStatus.label = newLabel
}
}
}
+23
View File
@@ -0,0 +1,23 @@
/**
* DataModel pour les items de type "item" (équipement générique).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineItemData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.item"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...baseItemSchema(),
needSkill: new fields.SchemaField({
value: new fields.BooleanField({ required: true, initial: false }),
skill: new fields.StringField({ required: true, initial: "" })
})
}
}
}
// Import partagé — après la déclaration de classe car defineSchema est statique
import { baseItemSchema } from './_shared.mjs'
+162
View File
@@ -0,0 +1,162 @@
/**
* DataModel pour les acteurs de type "npc" (PNJ).
* Étend foundry.abstract.TypeDataModel.
*
* Note : le champ libre de compétences (texte descriptif) est nommé "freeSkills"
* pour éviter le conflit avec le SchemaField "skills" qui contient les 30 compétences.
*/
import {
woundSchema,
combatStatusSchema,
equipmentSchema,
attributeSchema,
abilitiesSchema,
skillCategoriesSchema,
skillsSchema
} from "./_shared.mjs"
export default class VermineNpcData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.npc"]
/**
* Migration des données avant traitement par le schéma.
* Avant DataModel, template.json définissait "skills" comme un champ texte libre
* pour les PNJ. Le DataModel réserve "skills" pour les 30 compétences individuelles
* (SchemaField) et utilise "freeSkills" pour le texte libre.
* @param {Object} source Données brutes avant validation du schéma
* @returns {Object} Données migrées
*/
static migrateData(source) {
if (typeof source.skills === "string") {
source.freeSkills = source.skills
}
return super.migrateData(source)
}
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
// Blessures (base)
minorWound: new fields.SchemaField(woundSchema(1, 5)),
majorWound: new fields.SchemaField(woundSchema(4, 4)),
deadlyWound: new fields.SchemaField(woundSchema(8, 2)),
// Statut de combat (base, difficulté par défaut 9 pour PNJ)
combatStatus: combatStatusSchema("9"),
// Identité
identity: new fields.SchemaField({
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
profile: new fields.StringField({ required: true, nullable: false, initial: "" }),
origin: new fields.StringField({ required: true, nullable: false, initial: "" }),
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
theme: new fields.StringField({ required: true, nullable: false, initial: "" }),
notes: new fields.HTMLField({ required: true, initial: "" })
}),
// Attributs (XP, réputation, sang-froid, effort)
attributes: new fields.SchemaField({
xp: attributeSchema(0, 0, 10),
reputation: attributeSchema(0, 0, 10),
self_control: attributeSchema(0, 0, 5),
effort: attributeSchema(0, 0, 5)
}),
// Niveaux PNJ (menace, expérience, rôle)
threat: attributeSchema(1, 1, 4),
experience: attributeSchema(1, 1, 4),
role: attributeSchema(1, 1, 4),
// Compétences (les 30 compétences individuelles)
skills: skillsSchema(),
// Description libre des compétences (champ texte PNJ)
freeSkills: new fields.StringField({ required: true, nullable: false, initial: "" }),
// Catégories de compétences
skill_categories: skillCategoriesSchema(),
// Caractéristiques (8)
abilities: abilitiesSchema(),
// Équipement
equipment: equipmentSchema()
}
}
/** @override */
prepareDerivedData() {
super.prepareDerivedData()
// 1. Calculer les seuils de blessures selon le niveau de menace
this._setNpcWoundThresholds()
// 2. Calculer les réserves selon le niveau de rôle
this._setNpcAttributes()
// 3. Définir les libellés des caractéristiques
this._setAbilityLabels()
// 4. Mettre à jour le statut de combat
this._updateCombatStatus()
}
/**
* Calcule les seuils de blessures à partir du niveau de menace.
* Utilise CONFIG.VERMINE.npcThreatLevels.
*/
_setNpcWoundThresholds() {
const health = this.abilities?.health?.value || 1
const threatLevel = this.threat?.value || 1
const threatConfig = CONFIG.VERMINE.npcThreatLevels[threatLevel] || {}
this.minorWound.threshold = threatConfig.minorWound || health
this.majorWound.threshold = threatConfig.majorWound || (health + 3)
this.deadlyWound.threshold = threatConfig.deadlyWound || (health + 7 < 11 ? health + 7 : 10)
this.minorWound.max = threatConfig.minorWound || 4
this.majorWound.max = threatConfig.majorWound || 3
this.deadlyWound.max = threatConfig.deadlyWound || 2
}
/**
* Définit les attributs dérivés (effort, sang-froid) selon le niveau de rôle.
* Utilise CONFIG.VERMINE.npcRoleLevels.
*/
_setNpcAttributes() {
const roleLevel = this.role?.value || 1
const roleConfig = CONFIG.VERMINE.npcRoleLevels[roleLevel] || {}
this.attributes.effort.max = roleConfig.pools || 0
this.attributes.self_control.max = roleConfig.reaction_bonus || 0
}
/**
* Définit les libellés localisés des caractéristiques.
*/
_setAbilityLabels() {
for (const [k, v] of Object.entries(this.abilities)) {
v.label = game.i18n.localize(CONFIG.VERMINE.abilities[k]) ?? k
}
}
/**
* Met à jour le label du statut de combat en fonction de la difficulté.
*/
_updateCombatStatus() {
const difficulty = parseInt(this.combatStatus.difficulty) || 9
let newLabel = "Passif"
switch (difficulty) {
case 5: newLabel = "Offensif"; break
case 7: newLabel = "Actif"; break
case 9: newLabel = "Passif"; break
}
if (this.combatStatus.label !== newLabel) {
this.combatStatus.label = newLabel
}
}
}
+22
View File
@@ -0,0 +1,22 @@
import { baseItemSchema } from './_shared.mjs'
/**
* DataModel pour les items de type "rite" (rites, rituels).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineRiteData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.rite"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...baseItemSchema(),
rituel: new fields.StringField({ required: true, initial: "" }),
transe: new fields.StringField({ required: true, initial: "" }),
ability: new fields.StringField({ required: true, initial: "" }),
effect: new fields.StringField({ required: true, initial: "" })
}
}
}
+18
View File
@@ -0,0 +1,18 @@
import { listItemSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "rumor" (rumeurs).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineRumorData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.rumor"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...listItemSchema()
}
}
}
+18
View File
@@ -0,0 +1,18 @@
/**
* DataModel pour les items de type "specialty" (spécialités de compétence).
* Modèle minimal sans base partagée.
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineSpecialtyData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.specialty"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
skill: new fields.StringField({ required: true, nullable: false, initial: "" })
}
}
}
+19
View File
@@ -0,0 +1,19 @@
import { listItemSchema } from './_shared.mjs'
/**
* DataModel pour les items de type "target" (cibles, objectifs).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineTargetData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.target"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...listItemSchema(),
level: new fields.StringField({ required: true, initial: "minor" })
}
}
}
+19
View File
@@ -0,0 +1,19 @@
import { listItemSchema } from './_shared.mjs'
/**
* DataModel pour les items de type "trauma" (traumatismes, séquelles).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineTraumaData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.trauma"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...listItemSchema(),
type: new fields.StringField({ required: true, initial: "" })
}
}
}
+20
View File
@@ -0,0 +1,20 @@
import { baseItemSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "vehicle" (véhicules).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineVehicleData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.vehicle"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
...baseItemSchema(),
mobility: new fields.NumberField({ ...reqInt, initial: 3, min: 0 })
}
}
}
+27
View File
@@ -0,0 +1,27 @@
import { baseItemSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "weapon" (armes).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineWeaponData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.weapon"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
...baseItemSchema(),
min_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
damage: new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
type: new fields.StringField({ required: true, initial: "" }),
addVigor: new fields.BooleanField({ required: true, initial: false })
}),
ammo: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
}
}
}
-157
View File
@@ -1,157 +0,0 @@
import { onManageActiveEffect, prepareActiveEffectCategories } from "../system/effects.mjs";
import { preloadHandlebarsTemplates } from "../system/handlebars-manager.mjs";
/**
* Extend the basic ActorSheet with some very simple modifications
* @extends {ActorSheet}
*/
export class VermineActorSheet extends ActorSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
/*classes: ["vermine2047", "sheet", "actor"],
template: "systems/vermine2047/templates/actor/actor-sheet.hbs",
height: 800,
width: 690,
resizable: false,
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "features" }]*/
});
}
/** @override */
get template() {
return `systems/vermine2047/templates/actor/actor-${this.actor.type}-sheet.hbs`;
}
/* -------------------------------------------- */
/** @override */
getData() {
// Retrieve the data structure from the base sheet. You can inspect or log
// the context variable to see the structure, but some key properties for
// sheets are the actor object, the data object, whether or not it's
// editable, the items array, and the effects array.
const context = super.getData();
// Use a safe clone of the actor data for further operations.
const actorData = this.actor.toObject(false);
// Add the actor's data to context.data for easier access, as well as flags.
context.system = actorData.system;
context.flags = actorData.flags;
//add system config for convenience use
context.config = CONFIG.VERMINE;
// Add roll data for TinyMCE editors.
context.rollData = context.actor.getRollData();
// Prepare active effects
context.effects = prepareActiveEffectCategories(this.actor.effects);
return context;
}
/** @override */
activateListeners(html) {
super.activateListeners(html);
// Render the item sheet for viewing/editing prior to the editable check.
html.find('.item-edit').click(ev => {
const li = $(ev.currentTarget).parents(".item");
const item = this.actor.items.get(li.data("itemId"));
item.sheet.render(true);
});
// -------------------------------------------------------------
// Everything below here is only needed if the sheet is editable
if (!this.isEditable) return;
// Add Inventory Item
html.find('.item-create').click(this._onItemCreate.bind(this));
// Delete Inventory Item
html.find('.item-delete').click(ev => {
const li = $(ev.currentTarget).parents(".item");
const item = this.actor.items.get(li.data("itemId"));
item.delete();
li.slideUp(200, () => this.render(false));
});
html.find(".item-roll").click(ev => {
this._onRollItem(ev)
})
// Active Effect management
html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.actor));
// Drag events for macros.
if (this.actor.isOwner) {
let handler = ev => this._onDragStart(ev);
html.find('li.item').each((i, li) => {
if (li.classList.contains("inventory-header")) return;
li.setAttribute("draggable", true);
li.addEventListener("dragstart", handler, false);
});
}
//click on wound radio
html.find('.hexa [type="radio"]').click(ev => {
ev.preventDefault();
ev.stopPropagation();
return this._onClickRadioHexa(ev)
})
}
async _onRollItem(ev) {
const li = $(ev.currentTarget).parents(".item");
const item = this.actor.items.get(li.data("itemId"));
item.roll();
}
_onClickRadioHexa(ev) {
let input = ev.currentTarget;
console.log(input.value, input.name);
let update = {};
update[input.name] = 0
let propTree = input.name.split('.')
let current = this.actor;
for (let prop of propTree) {
current = current[prop]
}
if (current != input.value) {
update[input.name] = parseInt(input.value)
} else {
update[input.name] = parseInt(input.value) - 1;
}
this.actor.update(update)
}
async _onItemCreate(event) {
event.preventDefault();
const header = event.currentTarget;
// Get the type of item to create.
const type = header.dataset.type;
// Grab any data associated with this control.
const data = duplicate(header.dataset);
// Initialize a default name.
// const name = `New ${type.capitalize()}`;
const name = game.i18n.localize('ITEMS.new_' + type);
// Prepare the item object.
const itemData = {
name: name,
type: type,
system: data
};
// Remove the type from the dataset since it's in the itemData.type prop.
delete itemData.system["type"];
// Finally, create the item!
return await Item.create(itemData, { parent: this.actor });
}
}

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