35 Commits

Author SHA1 Message Date
uberwald 29b6a98c89 Fix dummy bug
Release Creation / build (release) Failing after 1m46s
2026-07-17 22:09:53 +02:00
uberwald b1d73e6b08 chore: remove CHANGELOG.md, add LICENSE.md (CC BY-NC-SA 4.0) 2026-07-12 22:32:48 +02:00
uberwald 509a5efd9d Actualiser README.md 2026-07-12 22:31:15 +02:00
uberwald fea7c1c271 docs: translate README to French with English summary 2026-07-12 22:30:16 +02:00
uberwald 412573fc4d chore: stop tracking regles/ (keep local) 2026-07-12 22:29:00 +02:00
uberwald 47e2ebe45b docs: rewrite README with system architecture, types, commands 2026-07-12 22:28:16 +02:00
uberwald 90de66d668 fix: Dice So Nice import v14 compat, override Foundry text colors for readability
Release Creation / build (release) Failing after 1m28s
- hooks.mjs: replace static dice-so-nice import with dynamic import
  using game.modules.get('dice-so-nice').id (path removed in v14)
- hooks.mjs: fix permission condition (|| -> &&), jQuery -> vanilla JS
- less/base.less: override --color-text-* and --button-text-color
  for both .themed.theme-dark (AppV2) and body.theme-dark (legacy apps)
- target #settings-config buttons + labels + hints for dark grays
2026-07-12 22:05:45 +02:00
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
221 changed files with 19551 additions and 5929 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',
'@typescript-eslint/no-magic-numbers': 'off',
'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',
'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'],
'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'],
'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'
+64
View File
@@ -0,0 +1,64 @@
# Vermine 2047 — Copilot Instructions
## Build & Development
Requires Node.js v16.15.1 (see `.nvmrc`).
```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 — browser-sync proxies Foundry over HTTPS and live-reloads SCSS, `.hbs`, and `.html` templates on change. `system.json` declares `hotReload` for `.css`, `.scss`, `.hbs`, and `.json` extensions, so Foundry auto-refreshes those assets client-side when the dev server is running.
## Architecture
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 |
| `dice3d.mjs` | Dice So Nice 3D dice configuration (appearance, colors, themes for totem/standard dice) |
| `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 (still AppV1) |
| `applications/sheets/` | ApplicationV2 sheet classes: `base-actor-sheet.mjs`, `base-item-sheet.mjs` + 4 actor + 12 item sheets |
**Data model**: Defined via Foundry DataModels (`module/models/`) extending `foundry.abstract.TypeDataModel`. Type-specific derived data is computed in each DataModel's `prepareDerivedData()`. The legacy `template.json` has been removed in favor of `system.json``documentTypes`.
**Sheet inheritance**: ApplicationV2 (`HandlebarsApplicationMixin(ActorSheetV2)`/`ItemSheetV2`) in `module/applications/sheets/`. `VermineBaseActorSheet``VermineCharacterSheetV2`, etc. Sheets expose `CONFIG.VERMINE` as `context.config` in template data. Each sheet is split into parts via `static PARTS`; tab navigation uses Core's `templates/generic/tab-navigation.hbs`.
**Data preparation**: `VermineActor.prepareDerivedData()` delegates to the type-specific DataModel. Each DataModel implements its own `prepareDerivedData()` with type-specific computation logic.
**Global access**: On `init`, `game.vermine2047` is populated with `{ VermineActor, VermineItem, VermineUtils, VermineCombat, GroupLink }` for easy access in macros and scripts.
**Compendium packs**: Stored in `packs/` as LevelDB databases. Edit source data in `src/packs/` as YAML, then `npm run pullYAMLtoLDB` to build. The CI release workflow runs this automatically.
## Key Conventions
- **All `.mjs` files**: ES module syntax only. Foundry globals (`game`, `Hooks`, `CONFIG`, `Actor`, `Item`, `ChatMessage`, `Roll`, `Handlebars`, `renderTemplate`, `foundry`, `ui`, `Canvas`, `Dialog`) are available but declared as readonly globals in `.eslintrc.js`.
- **ESLint conventions**: 2-space indent, single quotes, semicolons required, `===` equality (except `== null`). Constants named with `VERMINE_` prefix are exempt from camelCase. Max 1 class per file, 500 lines per file, 100 lines per function, 5 levels of nesting max. No `console.log` calls — use `ui.notifications`.
- **Code language**: Source code and comments are in French. UI strings in `lang/fr.json` and `lang/en.json`. Use `game.i18n.localize()` for all user-visible text.
- **Template naming**: Actor sheets use ApplicationV2 part templates in `templates/actor/appv2/{type}-{part}.hbs` (e.g. `character-main.hbs`, `npc-characteristics.hbs`). Item sheets at `templates/item/item-{type}-sheet.html`. Chat card templates at `templates/item/chatCards/{type}.hbs`. Legacy templates in `templates/actor/` subdirectories are preserved for dialog/chat use.
- **Dice system**: d10 pools with success counting (result ≥ difficulty). Totem dice (human/adapted) count double on success. Formula syntax: `{N}d10cs>={threshold}[label]`. Totem dice: `(1d10cs>={threshold}[totem_label]*2)`.
- **CSS**: SCSS sources in `scss/` compile to `css/vermine2047.css`. Entry point: `vermine2047.scss` which imports partials: `_app.scss`, `_flex.scss`, `dialog.scss`, `item-sheet.scss`, `itemCards.scss`, `roll.scss`, `special-applications.scss`, `special-inputs.scss`, `style.scss`, `base_work.scss`. Add new SCSS partials as `_component.scss`, import in `vermine2047.scss`.
- **Data tools**: The `pushLDBtoYAML` / `pullYAMLtoLDB` scripts use `@foundryvtt/foundryvtt-cli` to convert between LevelDB packs and editable YAML. When adding pack content, edit YAML in `src/packs/` then rebuild.
- **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`.
+5 -1
View File
@@ -31,12 +31,16 @@ jobs:
- name: Install Dependencies
run: npm ci
# Build CSS from LESS
- name: Build CSS
run: npm run build:less
# Pull YAML to LDB packs
- name: Build Packs
run: npm run pullYAMLtoLDB
# Create a zip file with all files required by the module to add to the release
- run: zip -r ./system.zip system.json template.json assets/ css/ lang/ module/ templates/ packs/
- run: zip -r ./system.zip system.json assets/ css/ lang/ module/ templates/ packs/
# Create a release for this specific version
- name: Update Release with Files
+11 -1
View File
@@ -1,6 +1,7 @@
# IDE
.idea/
.vsode
.vscode/
.github/
# Node Modules
node_modules
@@ -13,3 +14,12 @@ packs/*/*
*.lock
jsconfig.json
foundry
.history/
# Ancien dossier SCSS (archivé)
scss_old/
# Fichiers CSS générés
css/vermine2047.*.css
!css/vermine2047.css
regles/
+48
View File
@@ -0,0 +1,48 @@
{
"atom": false,
"borderZero": {
"style": "none"
},
"colorVariables": true,
"comment": false,
"depthLevel": 10,
"duplicateProperty": true,
"emptyRule": true,
"finalNewline": true,
"hexLength": "long",
"hexNotation": "lowercase",
"hexValidation": true,
"idSelector": false,
"important": true,
"importPath": [
"./less"
],
"maxCharPerLine": 120,
"newlineAfterBlock": true,
"propertyOrdering": false,
"propertyUnits": true,
"qualifyingElement": [
"button",
"input",
"select",
"textarea"
],
"selectorNaming": false,
"singleLinePerProperty": true,
"singleLinePerSelector": false,
"spaceAfterComma": true,
"spaceAfterComment": true,
"spaceAfterPropertyColon": "one",
"spaceAfterPropertyValue": true,
"spaceAroundCombinator": true,
"spaceAroundOperator": true,
"spaceBeforeBrace": "one",
"spaceBetweenParens": false,
"stringQuotes": "double",
"trailingSemicolon": true,
"trailingWhitespace": true,
"urlFormat": "relative",
"urlQuotes": true,
"variableNaming": false,
"zeroUnit": false
}
+63
View File
@@ -0,0 +1,63 @@
# AGENTS.md — Vermine2047 FoundryVTT system
## Project
FoundryVTT v14 game system for the French post-apocalyptic TTRPG "Vermine 2047". Bilingual FR/EN.
- **Entrypoint**: `module/vermine2047.mjs` (declared in `system.json` esmodules)
- **CSS**: `css/vermine2047.min.css` (compiled from LESS, **not** SCSS)
- **System ID**: `vermine2047`
## Architecture
| Layer | Location | Notes |
|-------|----------|-------|
| Entry/init | `module/vermine2047.mjs` | `Hooks.once('init',...)` — register DataModels before Document classes |
| DataModels | `module/models/*.mjs` | FoundryV2 `TypeDataModel` — 4 Actor types, 12 Item types |
| Documents | `module/documents/*.mjs` | `VermineActor`, `VermineItem` (thin wrappers) |
| Sheets (AppV2) | `module/applications/sheets/*.mjs` | ApplicationV2 sheets per type |
| System | `module/system/*.mjs` | config, roll, fight, dialogs, hooks, settings, dice3d, group-link |
| Templates (HBS) | `templates/actor/appv2/*.hbs` | ApplicationV2 templates |
| Legacy templates | `templates/actor/*.hbs`, `templates/actor/{character,group,npc,creature}/*.hbs` | Coexisting legacy templates |
### Actor types: character, npc, group, creature
### Item types: item, weapon, defense, vehicle, ability, specialty, background, trauma, evolution, rumor, target, rite
Key objects at runtime:
- `game.vermine2047``{ VermineUtils, VermineCombat, GroupLink }`
- `CONFIG.VERMINE` → game configuration (totems, traits, skills, etc.)
- `CONFIG.ui.combat` → custom `VermineCombatTracker`
Dice system: d10-based, success-counting (`Xd10cs>=N`). Totems give domain bonuses. Initiative formula:
`(@abilities.reflexes.value + @skills.alertness.value)d10cs>=@combatStatus.difficulty`
## Commands
```sh
npm run build:less # LESS → vermine2047.min.css
npm run build:less:dev # LESS → vermine2047.dev.css (unminified)
npm run build:all-css # both
npm run watch # gulp watch + browser-sync proxy to localhost:30000
npm run lint:less # lesshint
npm run pushLDBtoYAML # extract Foundry packs → src/packs/*.yml
npm run pullYAMLtoLDB # compile src/packs/*.yml → packs/ LevelDB
make build / watch / lint # Makefile wrappers for above
```
- Node version required: `v16.15.1` (per `.nvmrc`)
## Gotchas
- **DataModels before Document classes**: In `init` hook, register `CONFIG.Actor.dataModels.*` and `CONFIG.Item.dataModels.*` **before** setting `CONFIG.Actor.documentClass` / `CONFIG.Item.documentClass`.
- **`no-console: 'error'`**: ESLint blocks `console.log/warn/info`. Use `console.error` sparingly or disable the rule inline.
- **ESLint is very verbose**: ~350 rules. Common failures: missing semicolons, trailing commas, single quotes, 2-space indent, `max-len: 120`. Run `npx eslint module/` before committing.
- **Two template systems**: Both legacy (`templates/actor/*.hbs`) and ApplicationV2 (`templates/actor/appv2/*.hbs`) are preloaded. Don't confuse them.
- **Packs in two places**: `src/packs/*.yml` (YAML source, tracked) ↔ `packs/*` (LevelDB binary, gitignored). Edit YAML source, then run `pullYAMLtoLDB` to compile.
- **CSS auto-loaded**: `system.json` declares styles; **do not** inject `<link>` manually (causes MIME type errors).
- **`game-mode` setting**: Default in code is `'e'` (likely a typo for `'1'`). Values: `1`=Survie, `2`=Cauchemar, `3`=Apocalypse.
- **editMode flag disabled**: No auto-set on actor creation (causes ActiveEffect errors). Enable manually via sheet checkbox.
- **GroupLink auto-syncs**: `GroupLink.registerHooks()` runs on init. Actor updates propagate to groups and vice versa.
- **Custom dice**: Dice So Nice integration uses user color (regular/human/adapted color sets) with custom d10 face images.
- **No tests**: `npm test` is a stub (exits 1). No test framework installed.
- **No CI**: `.github/` is gitignored; no workflows configured.
- **HotReload**: `system.json` flags hotReload for css, scss, hbs, json extensions.
-39
View File
@@ -1,39 +0,0 @@
# CHANGELOG
## 0.1.13
- ajout des historiques
- ajout des rites
## 0.1.12
- passage des packs en LevelDB
## 0.1.11
- passage à une compatibilité v11
## 0.1.10
- révision du template évolution
- révision du template traumatisme
- ajout du type rite
- test import de pack
## 0.1.9
- versions condensées des méthodes (@kristov)
- renommage des templates
- image actors par défaut
- image des créatures
- ajout du totem et de l'apprentissage dans les capacités
## 0.1.8
- refactoring sheet vers actor
## 0.1.7
- sang-froid, effort, et santé
## 0.1.6
- finition de l'onglet totem
- ajout du logo de totem dans la sidebar
- ajout de traductions manquantes
## 0.1.5
- début de mise en forme des feuilles créature et pnj
- possibilité de changer le type de capacité (pour ajouter des capacités de totem)
+7
View File
@@ -0,0 +1,7 @@
Code Author :
- Developed by LeRatierBretonnien / www.uberwald.me
Code LICENCE :
C BY-NC-SA 4.0
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
This license requires that reusers give credit to the creator. It allows reusers to distribute, remix, adapt, and build upon the material in any medium or format, for noncommercial purposes only. If others modify or adapt the material, they must license the modified material under identical terms.
+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
+123 -47
View File
@@ -1,60 +1,136 @@
# Vermine 2047 System
# Vermine 2047 Système Foundry VTT
![Foundry v14](https://img.shields.io/badge/Foundry-v14-blue)
![Version](https://img.shields.io/badge/version-0.1.13-green)
##TODO
Système non-officiel pour le jeu de rôle **Vermine 2047**, univers post-apo français par **Studio Agate**.
### fiche de perso
► Achetez le jeu : https://www.kickandgo.net/boutique-agate/category.php?cl=UyQcRrFtYcYrAnLbOfDj&idp=20
- [X] création de spécialitées depuis le (+) des compétences
- [X] fixer les cases hexas , comprtement chelou , piste = un click à l'air d'envoyer 2 update, verifier le onChange
- [X] dialog d'edition des min-max
Interface bilingue **FR/EN**.
### fiche de groupe
- [ ] pas encore penché dessus
---
#### Members et encounters
## Types d'Acteurs
- [ ] 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
| Type | Fiche | Description |
|------|-------|-------------|
| **character** | AppV2 | Personnage joueur — compétences, capacités, totem, équipement |
| **npc** | AppV2 | PNJ avec rôle/menace, seuils de blessure |
| **group** | AppV2 | Groupe avec membres, rencontres, synchro liée |
| **creature** | AppV2 | Créature avec gabarit/taille/rôle, attaques custom |
### fiche de pnj créature
- [ ] à faire,
- [ ] lister les gabarit/taille/roles(creatures) et menace/experience/role(pnj)... stocker les modifs dans CONFIG.VERMINE,
## Types d'Objets
| Type | Utilisation |
|------|-------------|
| **item** | Objets génériques (matériel, ressources) |
| **weapon** | Armes — dégâts, portée, fiabilité, rareté |
| **defense** | Armures/boucliers — protection, couverture |
| **vehicle** | Véhicules — vitesse, structure, armement |
| **ability** | Capacités spéciales (apprentissage, domaine) |
| **specialty** | Spécialités de compétence |
| **background** | Historiques de personnage |
| **trauma** | Traumatismes physiques/mentaux |
| **evolution** | Évolutions du personnage |
| **rumor** | Rumeurs (groupe) |
| **target** | Cibles (groupe) |
| **rite** | Rites (magie/rituels) |
### les jets de dés
- [ ] 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] refacto des template chat de roll
- [X] gérer les dés de totems humains et adapté : couleur différente/double succès +update actor
- [X] gérer les rerolls depuis chat(cf noc)
- [X] gérer les rerolls après le jet en fonction du score d'effort et de la carac
- [X] faire l'update l' l'actor juste après s'etre accorder des rerolls, et avoir utiliser le sang-froid
- [X] update des reserves de sang-froids lors de jets
- [X] ajout des domaines de prédilections
- [ ] gérer les réussites auto
- [ ] gérer les seuils auto si compétence non maitrisée
## Système de Dés
### le combat
- [ ] modifier la difficulté en fonction de l'état du combatant /offensif/actif/passif/
- **Comptage de succès sur d10** : `Xd10cs>=N`
- Difficulté = nombre de succès requis
- Les totems donnent des bonus de domaine (dés humains/adaptés)
- Relances via l'Effort et le Sang-froid
- Succès automatiques et seuils d'échec
- Intégration **Dice So Nice!** avec faces de d10 personnalisées
### Initiative
Formule : `(@abilities.reflexes.value + @skills.alertness.value)d10cs>=@combatStatus.difficulty`
### les items
- [X]ajouter apprentissage aux abilities
- [X] passer le type d'arme en select/options
- [X] ajouter handicap de rareté
- [ ] 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] verifier le selector de traits (trait pratique cf : msg pretre)
- [X] construire une selecteur de traits, traits= CONFIG.VERMINE.traits
traits:[
key:{
name:string,
description:string,
value:number
}
]
- [X] ajouter des dégats sur chaques fiches item
---
## Architecture
```
module/
├── vermine2047.mjs # Point d'entrée (Hooks.once init)
├── models/ # DataModels (Foundry v2 TypeDataModel)
│ ├── actor/ # character.mjs, npc.mjs, group.mjs, creature.mjs
│ └── item/ # Les 12 types d'objets
├── documents/ # VermineActor, VermineItem
├── applications/
│ └── sheets/ # Fiches AppV2 par type
├── system/
│ ├── config.mjs # CONFIG.VERMINE (totems, traits, compétences…)
│ ├── roll.mjs # VermineUtils.roll()
│ ├── fight.mjs # Système de combat, VermineCombat, VermineCombatant
│ ├── functions.mjs # Fonctions partagées
│ ├── hooks.mjs # Hooks (DiceSoNice, chat, pause…)
│ ├── dice3d.mjs # Couleurs Dice So Nice, initUserDice
│ ├── dialogs/rollDialog.mjs
│ ├── settings.mjs # Paramètres du système (mode de jeu…)
│ ├── tour.mjs # Visites guidées (stub)
│ └── group-link.mjs # GroupLink : synchro acteurs↔groupes
templates/
├── actor/appv2/ # Templates AppV2 des fiches acteurs
├── actor/ # Templates legacy (coexistence)
└── item/ # Templates des fiches objets
css/
└── vermine2047.min.css # Compilé depuis LESS
less/
└── base.less # Source LESS principale
```
## Objets Clés
- `game.vermine2047``{ VermineUtils, VermineCombat, GroupLink }`
- `CONFIG.VERMINE` → totems, traits, compétences, domaines, états de combat…
- `CONFIG.ui.combat``VermineCombatTracker` personnalisé
## Commandes
```sh
npm run build:less # LESS → css/vermine2047.min.css
npm run build:less:dev # LESS → css/vermine2047.dev.css (non minifié)
npm run build:all-css # Les deux
npm run watch # Gulp watch + browser-sync → localhost:30000
npm run lint:less # Vérification LESS
npm run pushLDBtoYAML # Extraire les packs → src/packs/*.yml
npm run pullYAMLtoLDB # Compiler les packs → packs/* (LevelDB)
make build|watch|lint # Raccourcis Makefile
```
### Prérequis
- Node.js `v16.15.1` (cf `.nvmrc`)
- Foundry VTT **v14**
## Notes de Développement
- **DataModels avant Document classes** : Dans le hook `init`, enregistrer `CONFIG.Actor.dataModels.*` et `CONFIG.Item.dataModels.*` **avant** de définir `CONFIG.Actor.documentClass` / `CONFIG.Item.documentClass`.
- **Deux systèmes de templates** : AppV2 (`templates/actor/appv2/`) et legacy (`templates/actor/*.hbs`) coexistent.
- **Packs** : Éditer les sources YAML dans `src/packs/*.yml`, puis lancer `pullYAMLtoLDB`.
- **CSS** : Déclaré dans `system.json` — ne **pas** injecter de `<link>` manuellement.
- **ESLint** : ~350 règles. Lancer `npx eslint module/` avant chaque commit. Pas de `console.log`.
- **editMode** : Ne se met plus automatiquement à la création d'acteur (cause des erreurs ActiveEffect). Activer manuellement via la case à cocher.
- **GroupLink** : Synchronisation automatique bidirectionnelle acteurs↔groupes.
- **HotReload** : Activé pour css, scss, hbs, json dans `system.json`.
---
## English Summary
**Vermine 2047** is a French post-apocalyptic TTRPG. This is an unofficial Foundry VTT system (v14).
- **4 actor types**: character, npc, group, creature
- **12 item types**: item, weapon, defense, vehicle, ability, specialty, background, trauma, evolution, rumor, target, rite
- **Dice**: d10 success-counting with totem domains, Effort rerolls, Dice So Nice! 3D integration
- **Sheets**: AppV2 with tab navigation, edit/play mode toggle
- **Bilingual**: FR/EN interface
---
## Licence
Projet fan non-officiel. L'univers et les marques Vermine 2047 appartiennent aux Éditions John Doe.
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

+34
View File
@@ -0,0 +1,34 @@
# vermine2047 Audit Findings
Security audit of `vermine2047` at commit `b1d73e6b08bf`. Each finding links to a detailed write-up (`NNN-*.md`) and a proposed patch (`NNN-*.patch`).
## Summary
**Total findings: 0**
## Findings
## Dropped in adjudication
These findings reproduced in Phase 4 but the Phase 4.5 gate judged them false positives or out of a realistic threat model.
- **Unpinned mutable CI action enabling supply chain hijacking** (high, .gitea/workflows/release.yaml): Malformed URL in `uses` breaks workflow parsing; step is unreachable and prevents execution entirely.; Relies on improbable upstream compromise; standard configuration gap rather than immediate vulnerability.
## Run details
- run id: `aa4595a1`
- commit: `b1d73e6b08bf7ed0157a03137cc3bb50c8d3a26c`
- branch: `main`
- scope: 214 file(s)
- files triaged: 214
- files escalated: 14
- files deep-reviewed: 14
- findings proposed: 1
- findings verified: 0
- findings discarded: 0
- findings dropped in adjudication: 1
- artifacts: 0 written, 0 failed, 0 pending
## How to read this directory
Each finding has a paired `NNN-<slug>.md` (the human report) and `NNN-<slug>.patch` (the proposed fix as a unified diff). The report carries the narrative; the patch is the suggested change. Regenerate a single finding's artifacts with `/audit --regen --finding N`.
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; }
}
}
}
+691
View File
@@ -0,0 +1,691 @@
@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;
}
// Override Foundry Core default CSS custom properties
// for better readability on the dark Vermine2047 theme.
// AppV2 windows (sheets) use .themed.theme-dark on their content
.system-vermine2047 .themed.theme-dark {
--color-text-primary: #888888;
--color-text-emphatic: #999999;
--color-text-secondary: #777777;
--color-text-subtle: #666666;
--color-text-light-highlight: #888888;
--color-text-light-heading: #777777;
--color-text-light-primary: #666666;
}
// Old-style Application windows (settings-config, dialogs) inherit from body.theme-dark
body.system-vermine2047.theme-dark {
--color-text-primary: #888888;
--color-text-emphatic: #999999;
--color-text-secondary: #777777;
--color-text-subtle: #666666;
--color-text-light-highlight: #888888;
--color-text-light-heading: #777777;
--color-text-light-primary: #666666;
}
#settings-config button,
#settings-config a.button {
--button-text-color: #000000;
--button-border-color: #666666;
color: #000000;
}
body.system-vermine2047.theme-dark .form-group > label {
color: #777777;
}
#settings-config p,
#settings-config .hint {
color: #777777;
}
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";
+33 -90
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,42 +15,55 @@ 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;
}
}
/**
* Prepare Character type specific data
*/
_prepareCharacterData(actorData) {
if (actorData.type !== 'character') return;
// Make modifications to data here. For example:
const systemData = actorData.system;
// Loop through ability scores, and add their modifiers to our sheet output.
for (let [key, ability] of Object.entries(systemData.abilities)) {
// Calculate the modifier using d20 rules.
ability.mod = Math.floor((ability.value - 10) / 2);
}
// Character derived data is computed by VermineCharacterData.prepareDerivedData()
}
/**
* Prepare NPC type specific data.
*/
_prepareNpcData(actorData) {
if (actorData.type !== 'npc') return;
// NPC derived data is computed by VermineNpcData.prepareDerivedData()
}
// Make modifications to data here. For example:
const systemData = actorData.system;
systemData.xp = (systemData.cr * systemData.cr) * 100;
/**
* Prepare Group type specific data.
*/
_prepareGroupData(actorData) {
// Group derived data is computed by VermineGroupData.prepareDerivedData()
}
/**
* Prepare Creature type specific data.
*/
_prepareCreatureData(actorData) {
// Creature derived data is computed by VermineCreatureData.prepareDerivedData()
}
/**
@@ -118,53 +108,6 @@ export class VermineActor extends Actor {
// Process additional NPC data here.
}
_setCharacterSelfControl() {
this.system.attributes.self_control.max = 0 + Object.values(this.system.abilities).filter(i => i.category === "mental" || i.category === "social").map((i) => i.value).reduce((acc, curr) => acc + curr, 0) + this.modFromAgeSelfControl;
}
_setCharacterEffort() {
this.system.attributes.effort.max = 0 + Object.values(this.system.abilities).filter(i => i.category === "physical" || i.category === "manual").map((i) => i.value).reduce((acc, curr) => acc + curr, 0) + this.modFromAgeEffort;
}
_setCharacterThresholds() {
const health = this.system.abilities.health.value;
this.system.minorWound.threshold = health;
this.system.majorWound.threshold = health + 3;
this.system.deadlyWound.threshold = (health + 7 < 11) ? health + 7 : 10;
this.system.minorWound.max = 4 + this.modFromAgeWounds.l;
this.system.majorWound.max = 3 + this.modFromAgeWounds.h;
this.system.deadlyWound.max = 2 + this.modFromAgeWounds.d;
}
_setAgeType() {
Object.keys(CONFIG.VERMINE.AgeTypes).forEach((type) => {
if (this.system.identity.age >= parseInt(CONFIG.VERMINE.AgeTypes[type].beginning, 10)) {
this.system.identity.ageType = type;
}
});
}
get ageType() {
return this.system.identity.ageType;
}
get modFromAgeSelfControl() {
return this.ageType == 1 ? -1 : 0;
}
get modFromAgeEffort() {
if (this.ageType == 1) return -1;
if (this.ageType == 3) return -2;
return 0;
}
get modFromAgeWounds() {
if (this.ageType == 1) return { l: 0, h: 0, d: -1 };
if (this.ageType == 3) return { l: -1, h: -1, d: -1 };
return { l: 0, h: 0, d: 0 };
}
// Character derived-data methods removed — handled by VermineCharacterData.prepareDerivedData()
}
+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() {
// ability data
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 })
}
}
}
+198
View File
@@ -0,0 +1,198 @@
/**
* 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()
this._setAgeType()
this._setSelfControlMax()
this._setEffortMax()
this._setWoundThresholds()
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
const entries = Object.entries(ageTypes).reverse()
for (const [type, cfg] of entries) {
if (age >= parseInt(cfg.beginning, 10)) {
this.identity.ageType = parseInt(type, 10)
break
}
}
}
/**
* 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 = health
this.majorWound.threshold = health + 3
this.deadlyWound.threshold = Math.min(health + 7, 10)
this.minorWound.max = threatConfig.minorWound || 1
this.majorWound.max = threatConfig.majorWound || 1
this.deadlyWound.max = threatConfig.deadlyWound || 1
}
/**
* Définit les attributs dérivés (effort, sang-froid) selon le niveau de rôle.
* Utilise CONFIG.VERMINE.npcRoleLevels.
*/
_setNpcAttributes() {
const roleLevel = this.role?.value || 1
const roleConfig = CONFIG.VERMINE.npcRoleLevels[roleLevel] || {}
this.attributes.effort.max = roleConfig.pools || 0
this.attributes.self_control.max = roleConfig.reaction_bonus || 0
}
/**
* Définit les libellés localisés des caractéristiques.
*/
_setAbilityLabels() {
for (const [k, v] of Object.entries(this.abilities)) {
v.label = game.i18n.localize(CONFIG.VERMINE.abilities[k]) ?? k
}
}
/**
* Met à jour le label du statut de combat en fonction de la difficulté.
*/
_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: "" })
}
}
}

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