Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca74ff4a88 | ||
|
|
b0a3aff648 | ||
|
|
3cb96410b9 | ||
|
|
ad4daf0b00 | ||
|
|
c96e8d69d7 | ||
|
|
282fb2794a | ||
|
|
46b9512256 | ||
|
|
8316112741 | ||
|
|
75f39551f8 | ||
|
|
ef76ae1116 | ||
|
|
18180a8445 | ||
|
|
3235017a4c | ||
|
|
b2a044640c | ||
|
|
a13b0fc2d6 | ||
|
|
0ad240bd03 | ||
|
|
9d36b92fa9 | ||
|
|
d0373fab5a | ||
|
|
cf905cdcfb | ||
|
|
ea3322e843 | ||
|
|
6247f65590 | ||
|
|
8d8428cad3 | ||
|
|
9cf7d9ac68 | ||
|
|
f2c8cfe246 | ||
|
|
31f2f4530c |
@@ -34,7 +34,8 @@ module.exports = {
|
||||
Dialog: 'readonly',
|
||||
foundry: 'readonly',
|
||||
Handlebars: 'readonly',
|
||||
renderTemplate: 'readonly'
|
||||
renderTemplate: 'readonly',
|
||||
fromUuid: 'readonly'
|
||||
},
|
||||
rules: {
|
||||
// Possible Problems
|
||||
@@ -86,7 +87,6 @@ module.exports = {
|
||||
'default-case': 'warn',
|
||||
'default-case-last': 'warn',
|
||||
'default-param-last': 'warn',
|
||||
'dot-locale-compare': 'warn',
|
||||
'dot-notation': ['warn', { allowKeywords: true }],
|
||||
'eqeqeq': ['error', 'always', { null: 'ignore' }],
|
||||
'func-name-matching': 'warn',
|
||||
@@ -123,7 +123,6 @@ module.exports = {
|
||||
'no-delete-var': 'error',
|
||||
'no-div-regex': 'warn',
|
||||
'no-else-return': ['warn', { allowElseIf: true }],
|
||||
'no-empty-destructuring': 'warn',
|
||||
'no-empty-function': ['warn', { allow: ['constructors'] }],
|
||||
'no-empty-pattern': 'warn',
|
||||
'no-eq-null': 'off',
|
||||
@@ -153,7 +152,6 @@ module.exports = {
|
||||
'no-new-func': 'warn',
|
||||
'no-new-wrappers': 'error',
|
||||
'no-nonoctal-decimal-escape': 'error',
|
||||
'no-object-multi-space': 'warn',
|
||||
'no-octal-escape': 'error',
|
||||
'no-param-reassign': ['warn', { props: false }],
|
||||
'no-plusplus': 'off',
|
||||
@@ -267,10 +265,9 @@ module.exports = {
|
||||
VariableDeclarator: { var: 2, let: 2, const: 3 },
|
||||
outerIIFEBody: 1,
|
||||
MemberExpression: 'off',
|
||||
FunctionDeclaration: { body: 1, parameters: 1, parameters: { var: 2, let: 2, const: 3 } },
|
||||
FunctionExpression: { body: 1, parameters: 1, parameters: { var: 2, let: 2, const: 3 } },
|
||||
StaticBlock: { body: 1 },
|
||||
ClassBody: 1
|
||||
FunctionDeclaration: { body: 1, parameters: 1 },
|
||||
FunctionExpression: { body: 1, parameters: 1 },
|
||||
StaticBlock: { body: 1 }
|
||||
}],
|
||||
'jsx-quotes': 'off',
|
||||
'key-spacing': ['warn', { beforeColon: false, afterColon: true, mode: 'strict' }],
|
||||
|
||||
@@ -133,4 +133,10 @@ make build|watch|lint # Raccourcis Makefile
|
||||
|
||||
## Licence
|
||||
|
||||
Projet fan non-officiel. L'univers et les marques Vermine 2047 appartiennent aux Éditions John Doe.
|
||||
VERMINE 2047, Un jeu de Julien Blondel
|
||||
|
||||
© 2023 Studio Agate
|
||||
TOTEM System
|
||||
|
||||
© 2023 Julien Blondel
|
||||
Tous droits réservés
|
||||
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 855 KiB |
|
Before Width: | Height: | Size: 776 KiB |
@@ -1,680 +0,0 @@
|
||||
uid=78_0 RootWebArea "Foundry Virtual Tabletop" url="https://localhost:31000/game"
|
||||
uid=78_1 ignored
|
||||
uid=78_2 generic
|
||||
uid=78_3 list
|
||||
uid=78_4 generic
|
||||
uid=78_5 generic
|
||||
uid=78_6 generic
|
||||
uid=78_7 generic
|
||||
uid=78_8 generic
|
||||
uid=78_9 generic
|
||||
uid=78_10 list
|
||||
uid=78_11 listitem level="1"
|
||||
uid=78_12 tab "Outils de token" selectable
|
||||
uid=78_13 listitem level="1"
|
||||
uid=78_14 tab "Outils de tuile" selectable
|
||||
uid=78_15 listitem level="1"
|
||||
uid=78_16 tab "Outils de dessin" selectable
|
||||
uid=78_17 listitem level="1"
|
||||
uid=78_18 tab "Outils de mur" selectable
|
||||
uid=78_19 listitem level="1"
|
||||
uid=78_20 tab "Outils de lumière" selectable
|
||||
uid=78_21 listitem level="1"
|
||||
uid=78_22 tab "Outils de son d’ambiance" selectable
|
||||
uid=78_23 listitem level="1"
|
||||
uid=78_24 tab "Outils de région" selectable
|
||||
uid=78_25 listitem level="1"
|
||||
uid=78_26 tab "Notes" selectable
|
||||
uid=78_27 list
|
||||
uid=78_28 listitem level="1"
|
||||
uid=78_29 button "Sélection de tokens" pressed
|
||||
uid=78_30 listitem level="1"
|
||||
uid=78_31 button "Sélection de cibles"
|
||||
uid=78_32 listitem level="1"
|
||||
uid=78_33 button "Règle"
|
||||
uid=78_34 listitem level="1"
|
||||
uid=78_35 button "Déplacement sans contrainte"
|
||||
uid=78_36 generic
|
||||
uid=78_37 generic
|
||||
uid=78_38 list
|
||||
uid=78_39 listitem level="1"
|
||||
uid=78_40 ignored
|
||||
uid=78_41 StaticText "Gamemaster [MJ]"
|
||||
uid=78_42 InlineTextBox "Gamemaster [MJ]"
|
||||
uid=78_43 generic
|
||||
uid=78_44 generic
|
||||
uid=78_45 LabelText
|
||||
uid=78_46 StaticText "Latence"
|
||||
uid=78_42 InlineTextBox "Latence"
|
||||
uid=78_47 StaticText " "
|
||||
uid=78_42 InlineTextBox " "
|
||||
uid=78_48 StaticText "1ms"
|
||||
uid=78_42 InlineTextBox "1ms"
|
||||
uid=78_49 generic
|
||||
uid=78_50 LabelText
|
||||
uid=78_51 StaticText "IPS"
|
||||
uid=78_42 InlineTextBox "IPS"
|
||||
uid=78_52 StaticText " "
|
||||
uid=78_42 InlineTextBox " "
|
||||
uid=78_53 StaticText "--"
|
||||
uid=78_42 InlineTextBox "--"
|
||||
uid=78_54 button
|
||||
uid=78_55 generic
|
||||
uid=78_56 navigation
|
||||
uid=78_57 list
|
||||
uid=78_58 generic
|
||||
uid=78_59 sectionheader
|
||||
uid=78_60 generic
|
||||
uid=78_61 generic
|
||||
uid=78_62 LabelText
|
||||
uid=78_63 LabelText
|
||||
uid=78_64 sectionfooter
|
||||
uid=78_65 generic roledescription="Barre de raccourcis"
|
||||
uid=78_66 generic
|
||||
uid=78_67 button "Couper le son"
|
||||
uid=78_68 button "Menu principal"
|
||||
uid=78_69 list
|
||||
uid=78_70 button "Emplacement vide"
|
||||
uid=78_71 ignored
|
||||
uid=78_72 StaticText "1"
|
||||
uid=78_42 InlineTextBox "1"
|
||||
uid=78_73 button "Emplacement vide"
|
||||
uid=78_74 ignored
|
||||
uid=78_75 StaticText "2"
|
||||
uid=78_42 InlineTextBox "2"
|
||||
uid=78_76 button "Emplacement vide"
|
||||
uid=78_77 ignored
|
||||
uid=78_78 StaticText "3"
|
||||
uid=78_42 InlineTextBox "3"
|
||||
uid=78_79 button "Emplacement vide"
|
||||
uid=78_80 ignored
|
||||
uid=78_81 StaticText "4"
|
||||
uid=78_42 InlineTextBox "4"
|
||||
uid=78_82 button "Emplacement vide"
|
||||
uid=78_83 ignored
|
||||
uid=78_84 StaticText "5"
|
||||
uid=78_42 InlineTextBox "5"
|
||||
uid=78_85 button "Emplacement vide"
|
||||
uid=78_86 ignored
|
||||
uid=78_87 StaticText "6"
|
||||
uid=78_42 InlineTextBox "6"
|
||||
uid=78_88 button "Emplacement vide"
|
||||
uid=78_89 ignored
|
||||
uid=78_90 StaticText "7"
|
||||
uid=78_42 InlineTextBox "7"
|
||||
uid=78_91 button "Emplacement vide"
|
||||
uid=78_92 ignored
|
||||
uid=78_93 StaticText "8"
|
||||
uid=78_42 InlineTextBox "8"
|
||||
uid=78_94 button "Emplacement vide"
|
||||
uid=78_95 ignored
|
||||
uid=78_96 StaticText "9"
|
||||
uid=78_42 InlineTextBox "9"
|
||||
uid=78_97 button "Emplacement vide"
|
||||
uid=78_98 ignored
|
||||
uid=78_99 StaticText "0"
|
||||
uid=78_42 InlineTextBox "0"
|
||||
uid=78_100 generic
|
||||
uid=78_101 navigation
|
||||
uid=78_102 button "Page suivante"
|
||||
uid=78_103 ignored
|
||||
uid=78_104 StaticText "1"
|
||||
uid=78_42 InlineTextBox "1"
|
||||
uid=78_105 button "Page précédente"
|
||||
uid=78_106 ignored
|
||||
uid=78_107 button "Verrouiller la barre de raccourcis"
|
||||
uid=78_108 button "Effacer la barre de raccourcis"
|
||||
uid=78_109 generic
|
||||
uid=78_110 generic
|
||||
uid=78_111 generic
|
||||
uid=78_112 ignored
|
||||
uid=78_113 list
|
||||
uid=78_114 generic "Tchat"
|
||||
uid=78_115 generic
|
||||
uid=78_116 generic value="
|
||||
"
|
||||
uid=78_117 paragraph
|
||||
uid=78_118 LineBreak "
|
||||
"
|
||||
uid=78_42 InlineTextBox "
|
||||
"
|
||||
uid=78_119 ignored
|
||||
uid=78_42 generic
|
||||
uid=78_42 StaticText "Entrer un message"
|
||||
uid=78_42 InlineTextBox "Entrer un message"
|
||||
uid=78_120 generic
|
||||
uid=78_121 generic
|
||||
uid=78_122 button "Public en tant qu'utilisateur" pressed
|
||||
uid=78_123 button "Privé pour les maîtres de jeu"
|
||||
uid=78_124 button "Aveugle pour les maîtres de jeu"
|
||||
uid=78_125 button "Seulement pour soi-même"
|
||||
uid=78_126 button "Public en tant que personnage"
|
||||
uid=78_127 generic
|
||||
uid=78_128 tablist orientation="horizontal"
|
||||
uid=78_129 list
|
||||
uid=78_130 listitem level="1"
|
||||
uid=78_131 tab "Messages du tchat" selectable
|
||||
uid=78_132 listitem level="1"
|
||||
uid=78_133 tab "Rencontres de combat" selectable
|
||||
uid=78_134 listitem level="1"
|
||||
uid=78_135 tab "Scènes" selectable
|
||||
uid=78_136 listitem level="1"
|
||||
uid=78_137 tab "Objets plaçables" selectable
|
||||
uid=78_138 listitem level="1"
|
||||
uid=78_139 tab "Acteurs" selectable
|
||||
uid=78_140 listitem level="1"
|
||||
uid=78_141 tab "Objets" selectable
|
||||
uid=78_142 listitem level="1"
|
||||
uid=78_143 tab "Journaux" selectable
|
||||
uid=78_144 listitem level="1"
|
||||
uid=78_145 tab "Tables aléatoires" selectable
|
||||
uid=78_146 listitem level="1"
|
||||
uid=78_147 tab "Jeux de cartes" selectable
|
||||
uid=78_148 listitem level="1"
|
||||
uid=78_149 tab "Macros" selectable
|
||||
uid=78_150 listitem level="1"
|
||||
uid=78_151 tab "Playlists" selectable
|
||||
uid=78_152 listitem level="1"
|
||||
uid=78_153 tab "Compendiums" selectable
|
||||
uid=78_154 listitem level="1"
|
||||
uid=78_155 tab "Paramètres" selectable
|
||||
uid=78_156 listitem level="1"
|
||||
uid=78_157 button "Réduire"
|
||||
uid=78_158 generic
|
||||
uid=78_159 ignored
|
||||
uid=78_160 ignored
|
||||
uid=78_161 ignored
|
||||
uid=78_162 ignored
|
||||
uid=78_163 generic
|
||||
uid=78_164 sectionheader
|
||||
uid=78_165 ignored
|
||||
uid=78_166 button "Créer un acteur"
|
||||
uid=78_167 ignored
|
||||
uid=78_168 ignored
|
||||
uid=78_169 StaticText "Créer un acteur"
|
||||
uid=78_42 InlineTextBox "Créer un acteur"
|
||||
uid=78_170 button "Créer un dossier"
|
||||
uid=78_171 ignored
|
||||
uid=78_172 ignored
|
||||
uid=78_173 StaticText "Créer un dossier"
|
||||
uid=78_42 InlineTextBox "Créer un dossier"
|
||||
uid=78_174 search
|
||||
uid=78_175 button "Recherche par nom uniquement"
|
||||
uid=78_176 searchbox "Chercher dans les Acteurs"
|
||||
uid=78_177 ignored
|
||||
uid=78_178 ignored
|
||||
uid=78_179 generic
|
||||
uid=78_180 button "Trier par ordre alphabétique"
|
||||
uid=78_181 button "Réduire tous les dossiers"
|
||||
uid=78_182 list
|
||||
uid=78_183 listitem level="1"
|
||||
uid=78_184 image "Acteur" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/creature.webp"
|
||||
uid=78_185 generic
|
||||
uid=78_186 StaticText "Acteur"
|
||||
uid=78_42 InlineTextBox "Acteur"
|
||||
uid=78_187 listitem level="1"
|
||||
uid=78_188 image "Acteur (2)" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/group.webp"
|
||||
uid=78_189 generic
|
||||
uid=78_190 StaticText "Acteur (2)"
|
||||
uid=78_42 InlineTextBox "Acteur (2)"
|
||||
uid=78_191 listitem level="1"
|
||||
uid=78_192 image "Acteur (3)" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/character.webp"
|
||||
uid=78_193 generic
|
||||
uid=78_194 StaticText "Acteur (3)"
|
||||
uid=78_42 InlineTextBox "Acteur (3)"
|
||||
uid=78_195 sectionfooter
|
||||
uid=78_196 ignored
|
||||
uid=78_197 ignored
|
||||
uid=78_198 ignored
|
||||
uid=78_199 ignored
|
||||
uid=78_200 ignored
|
||||
uid=78_201 ignored
|
||||
uid=78_202 ignored
|
||||
uid=78_203 ignored
|
||||
uid=78_204 ignored
|
||||
uid=78_205 ignored
|
||||
uid=78_206 ignored
|
||||
uid=78_207 ignored
|
||||
uid=78_208 ignored
|
||||
uid=78_209 ignored
|
||||
uid=78_210 ignored
|
||||
uid=78_211 ignored
|
||||
uid=78_212 ignored
|
||||
uid=78_213 ignored
|
||||
uid=78_214 ignored
|
||||
uid=78_215 ignored
|
||||
uid=78_216 ignored
|
||||
uid=78_217 ignored
|
||||
uid=78_218 ignored
|
||||
uid=78_219 figure
|
||||
uid=78_220 image url="https://localhost:31000/ui/pause.svg"
|
||||
uid=78_221 Figcaption
|
||||
uid=78_222 StaticText "JEU EN PAUSE"
|
||||
uid=78_42 InlineTextBox "JEU EN PAUSE"
|
||||
uid=78_223 form
|
||||
uid=78_224 banner
|
||||
uid=78_225 heading "Personnage: Acteur (3)" level="1"
|
||||
uid=78_226 StaticText "Personnage: Acteur (3)"
|
||||
uid=78_42 InlineTextBox "Personnage: Acteur (3)"
|
||||
uid=78_227 button "Basculer les contrôles"
|
||||
uid=78_228 button "Copier l'UUID du document"
|
||||
uid=78_229 button "Fermer la fenêtre"
|
||||
uid=78_230 generic
|
||||
uid=78_231 ignored
|
||||
uid=78_232 ignored
|
||||
uid=78_233 button "MODE ÉDITION"
|
||||
uid=78_234 StaticText "MODE ÉDITION"
|
||||
uid=78_42 InlineTextBox "MODE ÉDITION"
|
||||
uid=78_235 image "logo Vermine" url="https://localhost:31000/systems/vermine2047/assets/images/ui/logo.webp"
|
||||
uid=78_236 ignored
|
||||
uid=78_237 image "Acteur (3)" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/character.webp"
|
||||
uid=78_238 generic
|
||||
uid=78_239 heading "ADAPTATION" level="3"
|
||||
uid=78_240 StaticText "ADAPTATION"
|
||||
uid=78_42 InlineTextBox "ADAPTATION"
|
||||
uid=78_241 heading "L'Humain" level="5"
|
||||
uid=78_242 StaticText "L'Humain"
|
||||
uid=78_42 InlineTextBox "L'Humain"
|
||||
uid=78_243 image url="https://localhost:31000/systems/vermine2047/assets/images/ui/totems/human.webp"
|
||||
uid=78_244 heading "L'adapté" level="5"
|
||||
uid=78_245 StaticText "L'adapté"
|
||||
uid=78_42 InlineTextBox "L'adapté"
|
||||
uid=78_246 image url="https://localhost:31000/systems/vermine2047/assets/images/ui/totems/adapted.webp"
|
||||
uid=78_247 ignored
|
||||
uid=78_248 ignored
|
||||
uid=78_249 ignored
|
||||
uid=78_250 generic
|
||||
uid=78_251 generic
|
||||
uid=78_252 ignored
|
||||
uid=78_253 ignored
|
||||
uid=78_254 ignored
|
||||
uid=78_255 ignored
|
||||
uid=78_256 generic
|
||||
uid=78_257 generic
|
||||
uid=78_258 ignored
|
||||
uid=78_259 ignored
|
||||
uid=78_260 list
|
||||
uid=78_261 listitem level="1"
|
||||
uid=78_262 ignored
|
||||
uid=78_263 heading "LA HORDE" level="4"
|
||||
uid=78_264 StaticText "LA HORDE"
|
||||
uid=78_42 InlineTextBox "LA HORDE"
|
||||
uid=78_265 image "La Horde" url="https://localhost:31000/systems/vermine2047/assets/images/ui/totems/horde.webp"
|
||||
uid=78_266 navigation roledescription="Navigation dans lâonglet des feuilles de personnages"
|
||||
uid=78_267 generic
|
||||
uid=78_268 ignored
|
||||
uid=78_269 StaticText "Caractéristiques et compétences"
|
||||
uid=78_42 InlineTextBox "Caractéristiques "
|
||||
uid=78_42 InlineTextBox "et compétences"
|
||||
uid=78_270 generic
|
||||
uid=78_271 ignored
|
||||
uid=78_272 StaticText "Totem et ajustements"
|
||||
uid=78_42 InlineTextBox "Totem et "
|
||||
uid=78_42 InlineTextBox "ajustements"
|
||||
uid=78_273 generic
|
||||
uid=78_274 ignored
|
||||
uid=78_275 StaticText "Matériel"
|
||||
uid=78_42 InlineTextBox "Matériel"
|
||||
uid=78_276 generic
|
||||
uid=78_277 ignored
|
||||
uid=78_278 StaticText "Histoire"
|
||||
uid=78_42 InlineTextBox "Histoire"
|
||||
uid=78_279 generic
|
||||
uid=78_280 ignored
|
||||
uid=78_281 StaticText "Combat et reserves"
|
||||
uid=78_42 InlineTextBox "Combat "
|
||||
uid=78_42 InlineTextBox "et "
|
||||
uid=78_42 InlineTextBox "reserves"
|
||||
uid=78_282 generic
|
||||
uid=78_283 sectionheader
|
||||
uid=78_284 generic
|
||||
uid=78_285 heading "Nom Acteur (3)" level="1"
|
||||
uid=78_286 LabelText
|
||||
uid=78_287 StaticText "Nom"
|
||||
uid=78_42 InlineTextBox "Nom"
|
||||
uid=78_288 ignored
|
||||
uid=78_289 StaticText "Acteur (3)"
|
||||
uid=78_42 InlineTextBox "Acteur (3)"
|
||||
uid=78_290 ignored
|
||||
uid=78_291 LabelText
|
||||
uid=78_292 StaticText "Profil"
|
||||
uid=78_42 InlineTextBox "Profil"
|
||||
uid=78_293 ignored
|
||||
uid=78_294 ignored
|
||||
uid=78_295 LabelText
|
||||
uid=78_296 StaticText "Age"
|
||||
uid=78_42 InlineTextBox "Age"
|
||||
uid=78_297 ignored
|
||||
uid=78_298 ignored
|
||||
uid=78_299 StaticText "15"
|
||||
uid=78_42 InlineTextBox "15"
|
||||
uid=78_300 generic
|
||||
uid=78_301 StaticText "(Jeune)"
|
||||
uid=78_42 InlineTextBox "(Jeune)"
|
||||
uid=78_302 generic
|
||||
uid=78_303 heading "Totem La Horde" level="1"
|
||||
uid=78_304 LabelText
|
||||
uid=78_305 StaticText "Totem"
|
||||
uid=78_42 InlineTextBox "Totem"
|
||||
uid=78_306 generic
|
||||
uid=78_307 StaticText "La Horde"
|
||||
uid=78_42 InlineTextBox "La Horde"
|
||||
uid=78_308 ignored
|
||||
uid=78_309 LabelText
|
||||
uid=78_310 StaticText "Réputation"
|
||||
uid=78_42 InlineTextBox "Réputation"
|
||||
uid=78_311 ignored
|
||||
uid=78_312 StaticText "0"
|
||||
uid=78_42 InlineTextBox "0"
|
||||
uid=78_313 LabelText
|
||||
uid=78_314 StaticText "Expérience"
|
||||
uid=78_42 InlineTextBox "Expérience"
|
||||
uid=78_315 ignored
|
||||
uid=78_316 StaticText "0"
|
||||
uid=78_42 InlineTextBox "0"
|
||||
uid=78_317 heading "CARACTÉRISTIQUES" level="3"
|
||||
uid=78_318 StaticText "CARACTÉRISTIQUES"
|
||||
uid=78_42 InlineTextBox "CARACTÉRISTIQUES"
|
||||
uid=78_319 ignored
|
||||
uid=78_320 ignored
|
||||
uid=78_321 heading "PHYSIQUE" level="4"
|
||||
uid=78_322 StaticText "PHYSIQUE"
|
||||
uid=78_42 InlineTextBox "PHYSIQUE"
|
||||
uid=78_323 generic
|
||||
uid=78_324 LabelText
|
||||
uid=78_325 StaticText "Vigueur"
|
||||
uid=78_42 InlineTextBox "Vigueur"
|
||||
uid=78_326 generic
|
||||
uid=78_327 LabelText
|
||||
uid=78_328 StaticText "Santé"
|
||||
uid=78_42 InlineTextBox "Santé"
|
||||
uid=78_329 ignored
|
||||
uid=78_330 heading "MANUEL" level="4"
|
||||
uid=78_331 StaticText "MANUEL"
|
||||
uid=78_42 InlineTextBox "MANUEL"
|
||||
uid=78_332 generic
|
||||
uid=78_333 LabelText
|
||||
uid=78_334 StaticText "Précision"
|
||||
uid=78_42 InlineTextBox "Précision"
|
||||
uid=78_335 generic
|
||||
uid=78_336 LabelText
|
||||
uid=78_337 StaticText "Réflexes"
|
||||
uid=78_42 InlineTextBox "Réflexes"
|
||||
uid=78_338 ignored
|
||||
uid=78_339 heading "MENTAL" level="4"
|
||||
uid=78_340 StaticText "MENTAL"
|
||||
uid=78_42 InlineTextBox "MENTAL"
|
||||
uid=78_341 generic
|
||||
uid=78_342 LabelText
|
||||
uid=78_343 StaticText "Savoir"
|
||||
uid=78_42 InlineTextBox "Savoir"
|
||||
uid=78_344 generic
|
||||
uid=78_345 LabelText
|
||||
uid=78_346 StaticText "Perception"
|
||||
uid=78_42 InlineTextBox "Perception"
|
||||
uid=78_347 ignored
|
||||
uid=78_348 heading "SOCIAL" level="4"
|
||||
uid=78_349 StaticText "SOCIAL"
|
||||
uid=78_42 InlineTextBox "SOCIAL"
|
||||
uid=78_350 generic
|
||||
uid=78_351 LabelText
|
||||
uid=78_352 StaticText "Volonté"
|
||||
uid=78_42 InlineTextBox "Volonté"
|
||||
uid=78_353 generic
|
||||
uid=78_354 LabelText
|
||||
uid=78_355 StaticText "Empathie"
|
||||
uid=78_42 InlineTextBox "Empathie"
|
||||
uid=78_356 heading "COMPÉTENCES" level="3"
|
||||
uid=78_357 StaticText "COMPÉTENCES"
|
||||
uid=78_42 InlineTextBox "COMPÉTENCES"
|
||||
uid=78_358 ignored
|
||||
uid=78_359 ignored
|
||||
uid=78_360 heading "L'HOMME" level="4"
|
||||
uid=78_361 StaticText "L'HOMME"
|
||||
uid=78_42 InlineTextBox "L'HOMME"
|
||||
uid=78_362 generic
|
||||
uid=78_363 LabelText
|
||||
uid=78_364 StaticText "Arts "
|
||||
uid=78_42 InlineTextBox "Arts "
|
||||
uid=78_365 superscript
|
||||
uid=78_366 StaticText "(I)"
|
||||
uid=78_42 InlineTextBox "(I)"
|
||||
uid=78_367 generic
|
||||
uid=78_368 LabelText
|
||||
uid=78_369 StaticText "Civilisation "
|
||||
uid=78_42 InlineTextBox "Civilisation "
|
||||
uid=78_370 superscript
|
||||
uid=78_371 StaticText "(II)"
|
||||
uid=78_42 InlineTextBox "(II)"
|
||||
uid=78_372 generic
|
||||
uid=78_373 LabelText
|
||||
uid=78_374 StaticText "Psychologie "
|
||||
uid=78_42 InlineTextBox "Psychologie "
|
||||
uid=78_375 superscript
|
||||
uid=78_376 StaticText "(I)"
|
||||
uid=78_42 InlineTextBox "(I)"
|
||||
uid=78_377 generic
|
||||
uid=78_378 LabelText
|
||||
uid=78_379 StaticText "Rumeurs"
|
||||
uid=78_42 InlineTextBox "Rumeurs"
|
||||
uid=78_380 generic
|
||||
uid=78_381 LabelText
|
||||
uid=78_382 StaticText "Soins "
|
||||
uid=78_42 InlineTextBox "Soins "
|
||||
uid=78_383 superscript
|
||||
uid=78_384 StaticText "(I)"
|
||||
uid=78_42 InlineTextBox "(I)"
|
||||
uid=78_385 ignored
|
||||
uid=78_386 heading "L'ANIMAL" level="4"
|
||||
uid=78_387 StaticText "L'ANIMAL"
|
||||
uid=78_42 InlineTextBox "L'ANIMAL"
|
||||
uid=78_388 generic
|
||||
uid=78_389 LabelText
|
||||
uid=78_390 StaticText "Animalisme "
|
||||
uid=78_42 InlineTextBox "Animalisme "
|
||||
uid=78_391 superscript
|
||||
uid=78_392 StaticText "(I)"
|
||||
uid=78_42 InlineTextBox "(I)"
|
||||
uid=78_393 generic
|
||||
uid=78_394 LabelText
|
||||
uid=78_395 StaticText "Dissection "
|
||||
uid=78_42 InlineTextBox "Dissection "
|
||||
uid=78_396 superscript
|
||||
uid=78_397 StaticText "(II)"
|
||||
uid=78_42 InlineTextBox "(II)"
|
||||
uid=78_398 generic
|
||||
uid=78_399 LabelText
|
||||
uid=78_400 StaticText "Faune "
|
||||
uid=78_42 InlineTextBox "Faune "
|
||||
uid=78_401 superscript
|
||||
uid=78_402 StaticText "(I)"
|
||||
uid=78_42 InlineTextBox "(I)"
|
||||
uid=78_403 generic
|
||||
uid=78_404 LabelText
|
||||
uid=78_405 StaticText "Répulsion"
|
||||
uid=78_42 InlineTextBox "Répulsion"
|
||||
uid=78_406 generic
|
||||
uid=78_407 LabelText
|
||||
uid=78_408 StaticText "Pistage"
|
||||
uid=78_42 InlineTextBox "Pistage"
|
||||
uid=78_409 ignored
|
||||
uid=78_410 heading "LA MACHINE" level="4"
|
||||
uid=78_411 StaticText "LA MACHINE"
|
||||
uid=78_42 InlineTextBox "LA MACHINE"
|
||||
uid=78_412 generic
|
||||
uid=78_413 LabelText
|
||||
uid=78_414 StaticText "Artisanat "
|
||||
uid=78_42 InlineTextBox "Artisanat "
|
||||
uid=78_415 superscript
|
||||
uid=78_416 StaticText "(II)"
|
||||
uid=78_42 InlineTextBox "(II)"
|
||||
uid=78_417 generic
|
||||
uid=78_418 LabelText
|
||||
uid=78_419 StaticText "Bricolage"
|
||||
uid=78_42 InlineTextBox "Bricolage"
|
||||
uid=78_420 generic
|
||||
uid=78_421 LabelText
|
||||
uid=78_422 StaticText "Mécanique "
|
||||
uid=78_42 InlineTextBox "Mécanique "
|
||||
uid=78_423 superscript
|
||||
uid=78_424 StaticText "(II)"
|
||||
uid=78_42 InlineTextBox "(II)"
|
||||
uid=78_425 generic
|
||||
uid=78_426 LabelText
|
||||
uid=78_427 StaticText "Pilotage "
|
||||
uid=78_42 InlineTextBox "Pilotage "
|
||||
uid=78_428 superscript
|
||||
uid=78_429 StaticText "(I)"
|
||||
uid=78_42 InlineTextBox "(I)"
|
||||
uid=78_430 generic
|
||||
uid=78_431 LabelText
|
||||
uid=78_432 StaticText "Technologie "
|
||||
uid=78_42 InlineTextBox "Technologie "
|
||||
uid=78_433 superscript
|
||||
uid=78_434 StaticText "(II)"
|
||||
uid=78_42 InlineTextBox "(II)"
|
||||
uid=78_435 ignored
|
||||
uid=78_436 heading "L'ARME" level="4"
|
||||
uid=78_437 StaticText "L'ARME"
|
||||
uid=78_42 InlineTextBox "L'ARME"
|
||||
uid=78_438 generic
|
||||
uid=78_439 LabelText
|
||||
uid=78_440 StaticText "Armes à feu "
|
||||
uid=78_42 InlineTextBox "Armes à feu "
|
||||
uid=78_441 superscript
|
||||
uid=78_442 StaticText "(II)"
|
||||
uid=78_42 InlineTextBox "(II)"
|
||||
uid=78_443 generic
|
||||
uid=78_444 LabelText
|
||||
uid=78_445 StaticText "Tir à l'arc"
|
||||
uid=78_42 InlineTextBox "Tir à l'arc"
|
||||
uid=78_446 generic
|
||||
uid=78_447 LabelText
|
||||
uid=78_448 StaticText "Armurerie "
|
||||
uid=78_42 InlineTextBox "Armurerie "
|
||||
uid=78_449 superscript
|
||||
uid=78_450 StaticText "(II)"
|
||||
uid=78_42 InlineTextBox "(II)"
|
||||
uid=78_451 generic
|
||||
uid=78_452 LabelText
|
||||
uid=78_453 StaticText "Lancer"
|
||||
uid=78_42 InlineTextBox "Lancer"
|
||||
uid=78_454 generic
|
||||
uid=78_455 LabelText
|
||||
uid=78_456 StaticText "Mêlée"
|
||||
uid=78_42 InlineTextBox "Mêlée"
|
||||
uid=78_457 ignored
|
||||
uid=78_458 heading "LA SURVIE" level="4"
|
||||
uid=78_459 StaticText "LA SURVIE"
|
||||
uid=78_42 InlineTextBox "LA SURVIE"
|
||||
uid=78_460 generic
|
||||
uid=78_461 LabelText
|
||||
uid=78_462 StaticText "Vigilance"
|
||||
uid=78_42 InlineTextBox "Vigilance"
|
||||
uid=78_463 generic
|
||||
uid=78_464 LabelText
|
||||
uid=78_465 StaticText "Athlétisme"
|
||||
uid=78_42 InlineTextBox "Athlétisme"
|
||||
uid=78_466 generic
|
||||
uid=78_467 LabelText
|
||||
uid=78_468 StaticText "Alimentation"
|
||||
uid=78_42 InlineTextBox "Alimentation"
|
||||
uid=78_469 generic
|
||||
uid=78_470 LabelText
|
||||
uid=78_471 StaticText "Discrétion"
|
||||
uid=78_42 InlineTextBox "Discrétion"
|
||||
uid=78_472 generic
|
||||
uid=78_473 LabelText
|
||||
uid=78_474 StaticText "Corps-à-corps"
|
||||
uid=78_42 InlineTextBox "Corps-à-corps"
|
||||
uid=78_475 ignored
|
||||
uid=78_476 heading "LA TERRE" level="4"
|
||||
uid=78_477 StaticText "LA TERRE"
|
||||
uid=78_42 InlineTextBox "LA TERRE"
|
||||
uid=78_478 generic
|
||||
uid=78_479 LabelText
|
||||
uid=78_480 StaticText "Environnement "
|
||||
uid=78_42 InlineTextBox "Environnement "
|
||||
uid=78_481 superscript
|
||||
uid=78_482 StaticText "(I)"
|
||||
uid=78_42 InlineTextBox "(I)"
|
||||
uid=78_483 generic
|
||||
uid=78_484 LabelText
|
||||
uid=78_485 StaticText "Flore "
|
||||
uid=78_42 InlineTextBox "Flore "
|
||||
uid=78_486 superscript
|
||||
uid=78_487 StaticText "(I)"
|
||||
uid=78_42 InlineTextBox "(I)"
|
||||
uid=78_488 generic
|
||||
uid=78_489 LabelText
|
||||
uid=78_490 StaticText "Route"
|
||||
uid=78_42 InlineTextBox "Route"
|
||||
uid=78_491 generic
|
||||
uid=78_492 LabelText
|
||||
uid=78_493 StaticText "Toxiques "
|
||||
uid=78_42 InlineTextBox "Toxiques "
|
||||
uid=78_494 superscript
|
||||
uid=78_495 StaticText "(II)"
|
||||
uid=78_42 InlineTextBox "(II)"
|
||||
uid=78_496 generic
|
||||
uid=78_497 LabelText
|
||||
uid=78_498 StaticText "Vestiges "
|
||||
uid=78_42 InlineTextBox "Vestiges "
|
||||
uid=78_499 superscript
|
||||
uid=78_500 StaticText "(I)"
|
||||
uid=78_42 InlineTextBox "(I)"
|
||||
uid=78_501 ignored
|
||||
uid=78_502 ignored
|
||||
uid=78_503 ignored
|
||||
uid=78_504 ignored
|
||||
uid=78_505 ignored
|
||||
uid=78_506 ignored
|
||||
uid=78_507 ignored
|
||||
uid=78_508 ignored
|
||||
uid=78_509 ignored
|
||||
uid=78_510 ignored
|
||||
uid=78_511 ignored
|
||||
uid=78_512 ignored
|
||||
uid=78_513 ignored
|
||||
uid=78_514 ignored
|
||||
uid=78_515 ignored
|
||||
uid=78_516 ignored
|
||||
uid=78_517 ignored
|
||||
uid=78_518 ignored
|
||||
uid=78_519 ignored
|
||||
uid=78_520 ignored
|
||||
uid=78_521 ignored
|
||||
uid=78_522 ignored
|
||||
uid=78_523 ignored
|
||||
uid=78_524 ignored
|
||||
uid=78_525 ignored
|
||||
uid=78_526 ignored
|
||||
uid=78_527 ignored
|
||||
uid=78_528 ignored
|
||||
uid=78_529 ignored
|
||||
uid=78_530 ignored
|
||||
uid=78_531 ignored
|
||||
uid=78_532 ignored
|
||||
uid=78_533 ignored
|
||||
uid=78_534 ignored
|
||||
uid=78_535 ignored
|
||||
uid=78_536 ignored
|
||||
uid=78_537 ignored
|
||||
uid=78_538 ignored
|
||||
uid=78_539 ignored
|
||||
uid=78_540 ignored
|
||||
uid=78_541 ignored
|
||||
uid=78_542 ignored
|
||||
uid=78_543 ignored
|
||||
uid=78_544 ignored
|
||||
uid=78_545 ignored
|
||||
uid=78_546 ignored
|
||||
uid=78_547 ignored
|
||||
uid=78_548 ignored
|
||||
uid=78_549 ignored
|
||||
uid=78_550 ignored
|
||||
uid=78_551 ignored
|
||||
uid=78_552 ignored
|
||||
uid=78_553 ignored
|
||||
uid=78_554 ignored
|
||||
uid=78_555 generic
|
||||
@@ -0,0 +1,126 @@
|
||||
# Combat — Échanges de coups (assisté)
|
||||
|
||||
## Principe
|
||||
|
||||
Chaque échange de coups se déroule dans le chat : un jet d'attaque, un éventuel
|
||||
jet de défense, puis une résolution assistée. Tous les calculs (réussites,
|
||||
dégâts, protection, blessure) sont effectués automatiquement ; la blessure
|
||||
s'applique ensuite d'un simple clic.
|
||||
|
||||
```
|
||||
Attaque → (Défense) → Résoudre → Appliquer
|
||||
```
|
||||
|
||||
## 1. L'attaque
|
||||
|
||||
Ouvrir le dialogue d'attaque :
|
||||
|
||||
- **Personnage / PNJ** : cliquer l'icône cible (`fa-crosshairs`) sur une arme
|
||||
dans l'onglet **Combat** (liste des armes équipées) ou dans l'onglet
|
||||
**Matériel**.
|
||||
- **Créature** : cliquer la valeur « Attaque » de l'onglet **Stats**.
|
||||
|
||||
Le dialogue est prérempli selon le type d'acteur :
|
||||
|
||||
| Type | Pool | Dégâts de base | Difficulté |
|
||||
|------|------|----------------|------------|
|
||||
| **Personnage** | Caractéristique + compétence (verrouillées) | Arme (+ Vigueur si « Vigueur+ ») | Selon maîtrise / portée |
|
||||
| **PNJ** | Pool de menace + réserves de rôle | Arme équipée, sinon `Vigueur` de menace | Selon niveau d'expérience |
|
||||
| **Créature** | `Attaque` calculée | `Dégâts` calculés | Son statut de combat (verrouillée) |
|
||||
|
||||
### Difficulté
|
||||
|
||||
- **Contact** : dépend du niveau de compétence de l'attaquant.
|
||||
- Incompétent / Débutant : `7`
|
||||
- Confirmé : `5 ou 7`
|
||||
- Expert : `5, 7 ou 9`
|
||||
- Maître / Légende : `3, 5, 7 ou 9`
|
||||
- **Distance** : par portée.
|
||||
- Courte : `5` — Moyenne : `7` — Longue : `9`
|
||||
- **Créature ciblée** : la difficulté est forcée au statut de combat de la
|
||||
créature (toute réussite ≥ 1 touche).
|
||||
|
||||
### Options du dialogue
|
||||
|
||||
- **Deuxième action** : +2 à la difficulté.
|
||||
- **Handicap** : augmente les succès requis.
|
||||
- **Sang-Froid** : dés bonus (débités du personnage).
|
||||
- **Totems** : dés de totem (+ garder un totem si les deux sont actifs).
|
||||
- **Consommation de munitions** : pour les armes à distance (coche cochée par
|
||||
défaut, la munition est décomptée au jet).
|
||||
- Les **cibles** sélectionnées sur la carte (`Ctrl` + clic / outil de ciblage)
|
||||
sont affichées et reprises dans l'échange.
|
||||
|
||||
### La carte de chat
|
||||
|
||||
Le message d'attaque affiche l'arme (dégâts, portées, munitions) puis le bloc
|
||||
**Échange de coups** avec, **par cible**, les boutons **Esquive** et **Parade**
|
||||
(contact) ou **Esquive** seule (distance), ainsi que le bouton **Résoudre**.
|
||||
|
||||
## 2. La défense
|
||||
|
||||
Seuls les acteurs **capables de se défendre** (personnages et PNJ) ont des
|
||||
boutons de défense — pas les créatures (défense passive).
|
||||
|
||||
- Cliquer **Esquive** ou **Parade** sur la carte d'attaque.
|
||||
- Le dialogue de défense est **verrouillé** :
|
||||
- **Esquive** : `Réflexes` + `Corps-à-corps` (attaque au contact) ou
|
||||
`Vigilance` (attaque à distance).
|
||||
- **Parade** : `Vigueur` + `Mêlée` (ou `Corps-à-corps`).
|
||||
- **Difficulté = celle de l'attaque** (non modifiable).
|
||||
- Le jet de défense est lié au message d'attaque (même échange).
|
||||
|
||||
> Permission : Esquive / Parade ne sont utilisables que par le **propriétaire
|
||||
> de la cible** ou le **MJ**.
|
||||
|
||||
## 3. La résolution
|
||||
|
||||
Cliquer **Résoudre** (auteur du message ou MJ). Le système compare les
|
||||
**réussites** des jets d'attaque et de défense :
|
||||
|
||||
- **Attaquant > Défenseur** → **Touché** (parade insuffisante si parade).
|
||||
- **Attaquant ≤ Défenseur** → **Esquivé / Paré** : aucun dégât.
|
||||
- **Sans défense** (abstention ou créature) : toute réussite ≥ 1 touche.
|
||||
|
||||
Les totaux sont lus en direct : une **relance** de dé effectuée avant la
|
||||
résolution est prise en compte.
|
||||
|
||||
### Dégâts
|
||||
|
||||
```
|
||||
Dégâts = dégâts de base de l'arme (+ Vigueur si « Vigueur+ ») + réussites d'attaque
|
||||
```
|
||||
|
||||
### Protection
|
||||
|
||||
- **Parade** : seule la protection utilisée pour la parade réduit.
|
||||
- **Sinon** : somme des **protections équipées** de la cible
|
||||
(créature : protection de rôle).
|
||||
|
||||
### Blessure
|
||||
|
||||
La blessure correspond au **seuil le plus élevé** inférieur ou égal aux dégâts
|
||||
nets : **Légère**, **Grave** ou **Mortelle**. Un bloc de résultat est affiché
|
||||
**par cible** (attaque multi-cibles = un bloc par cible).
|
||||
|
||||
## 4. L'application de la blessure
|
||||
|
||||
Cliquer **« Appliquer la blessure »** sur le bloc de résultat (propriétaire de
|
||||
la cible ou MJ) : le cercle de blessure correspondant est coché sur l'acteur.
|
||||
|
||||
## Rappels et permissions
|
||||
|
||||
| Action | Qui peut la faire |
|
||||
|--------|-------------------|
|
||||
| Esquive / Parade | Propriétaire de la cible ou MJ |
|
||||
| Résoudre | Auteur du message d'attaque ou MJ |
|
||||
| Appliquer la blessure | Propriétaire de la cible ou MJ |
|
||||
|
||||
- Les **créatures** ne lancent pas de défense : leur difficulté d'attaque et de
|
||||
défense est leur **statut de combat**.
|
||||
- Les **PNJ** lancent leur défense comme un personnage (capacités + compétences).
|
||||
- Les **réserves de rôle** des PNJ sont à dépenser à la main (gérées par le MJ).
|
||||
|
||||
---
|
||||
|
||||
*Guide utilisateur — Vermine2047 pour FoundryVTT.*
|
||||
|
Before Width: | Height: | Size: 834 KiB |
@@ -16,8 +16,9 @@
|
||||
"TYPES.Item.item": "Item",
|
||||
"TYPES.Item.weapon": "Weapon",
|
||||
"TYPES.Item.defense": "Defense",
|
||||
"TYPES.Item.vehicle": "Vehicle",
|
||||
"TYPES.Actor.vehicle": "Vehicle",
|
||||
"TYPES.Item.ability": "Ability",
|
||||
"TYPES.Item.creaturecapacity": "Creature capacity",
|
||||
"TYPES.Item.specialty": "Specialty",
|
||||
"TYPES.Item.background": "Background",
|
||||
"TYPES.Item.trauma": "Trauma",
|
||||
@@ -43,6 +44,20 @@
|
||||
"nightmare": "Nightmare",
|
||||
"apocalypse": "Apocalypse"
|
||||
},
|
||||
"CREATURE_CAPACITY_TYPES": {
|
||||
"base": {
|
||||
"name": "Base"
|
||||
},
|
||||
"survival": {
|
||||
"name": "Survival"
|
||||
},
|
||||
"nightmare": {
|
||||
"name": "Nightmare"
|
||||
},
|
||||
"apocalypse": {
|
||||
"name": "Apocalypse"
|
||||
}
|
||||
},
|
||||
"ROLLS": {
|
||||
"tool": "Dice Roller",
|
||||
"roll_dice": "Roll the dice"
|
||||
@@ -140,13 +155,19 @@
|
||||
"level": "Level",
|
||||
"experience": "Experience",
|
||||
"reputation": "Reputation",
|
||||
"chaman": "Shaman",
|
||||
"sheet": {
|
||||
"description": "Description"
|
||||
},
|
||||
"pool": "Reserve",
|
||||
"pools": "Reserves",
|
||||
"dice_pool": "Dice pool",
|
||||
"pool_malus": "Wound penalty",
|
||||
"auto_successes": "automatic successes",
|
||||
"self_control": "Self-control",
|
||||
"effort": "Effort",
|
||||
"equip": "Equip",
|
||||
"unequip": "Unequip",
|
||||
"group": "Group",
|
||||
"abilities": "Characteristics",
|
||||
"ability": "Characteristic",
|
||||
@@ -172,6 +193,20 @@
|
||||
"range": "Range",
|
||||
"ranges": "Ranges",
|
||||
"mobility": "Mobility",
|
||||
"vehicle": "Vehicle",
|
||||
"vehicle_specs": "Vehicle specifications",
|
||||
"kind": "Kind",
|
||||
"kind_vehicle": "Vehicle",
|
||||
"kind_mount": "Mount",
|
||||
"distance": "Distance",
|
||||
"speed": "Speed",
|
||||
"speed_duration_ph": "top speed duration (e.g. 10 min)",
|
||||
"effort_reserve": "Effort reserve",
|
||||
"maintenance": "Maintenance",
|
||||
"owner": "Owner",
|
||||
"no_vehicles": "No vehicles",
|
||||
"no_encounters": "No encounters",
|
||||
"travel_notes": "Travel notes",
|
||||
"rarity": "Rarity",
|
||||
"rarity_sm": "Rar.",
|
||||
"reliability": "Reliability",
|
||||
@@ -198,10 +233,13 @@
|
||||
"boost": "Boost",
|
||||
"group_abilities": "Group abilities",
|
||||
"totem_abilities": "Totem abilities",
|
||||
"creature_capacities": "Creature capacities",
|
||||
"type_group": "Group",
|
||||
"type_character": "Character",
|
||||
"type_npc": "NPC",
|
||||
"type_creature": "Creature",
|
||||
"type_community": "Community",
|
||||
"type_vehicle": "Vehicle",
|
||||
"type": "Type",
|
||||
"group_members": "Members",
|
||||
"encounters": "Encounters",
|
||||
@@ -241,9 +279,45 @@
|
||||
"success_count": "Success Count",
|
||||
"success_required": "Success Required",
|
||||
"test_of": "Test of",
|
||||
"roll_success": "Success",
|
||||
"roll_failure": "Failure",
|
||||
"roll_context": "Roll context",
|
||||
"roll_pool": "Dice pool",
|
||||
"roll_dice": "Dice detail",
|
||||
"pool_ability": "Ability",
|
||||
"pool_skill": "Skill",
|
||||
"pool_specialty": "Specialty",
|
||||
"pool_help": "Help",
|
||||
"pool_tooling": "Equipment",
|
||||
"pool_group": "Group",
|
||||
"pool_selfControl": "Self-Control",
|
||||
"pool_pools": "Pools",
|
||||
"pool_total": "Total",
|
||||
"totem_used": "Totem",
|
||||
"totem_keep": "kept",
|
||||
"domain_bonus": "Domain bonus",
|
||||
"domain_penalty": "Domain penalty",
|
||||
"used": "used",
|
||||
"die_regular": "dice",
|
||||
"die_human": "human",
|
||||
"die_adapted": "adapted",
|
||||
"die_successes": "successes",
|
||||
"total": "Total",
|
||||
"totem_dice": "Totem Dice",
|
||||
"totem_hint": "Totem Hint",
|
||||
"totem_hint": "Check to use totem dice (double success possible)",
|
||||
"totem_neutral": "Neutral",
|
||||
"totem_manage": "Manage Totem Dice (Instincts / Interdits)",
|
||||
"totem_serious": "Serious act (2 dice)",
|
||||
"totem_manage_hint": "Instincts: obeying the Human's Instincts gains 1 Human die; another Totem's Instincts gain 1 Adapted die. Interdit: breaking a Human Interdit loses 1 Human die (or gains 1 Adapted die if none). Max 3 dice of the same Totem and 5 dice total.",
|
||||
"totem_instinct_human": "Human Instinct (+1 Human die)",
|
||||
"totem_instinct_adapted": "Another Totem's Instinct (+1 Adapted die)",
|
||||
"totem_interdit_human": "Human Interdit (-1 Human die)",
|
||||
"totem_gained": "{totem} +1D",
|
||||
"totem_gained_adapted": "+1D Adapted",
|
||||
"totem_exchanged": "{totem} gained in exchange (1 opposite die removed)",
|
||||
"totem_replaced": "Opposite die removed instead (max 3D reached)",
|
||||
"totem_lost": "-1 Human die",
|
||||
"totem_limit_reached": "Totem dice limit reached.",
|
||||
"FightTool": "Fight Tool",
|
||||
"RollTool": "Roll Tool",
|
||||
"Roll4Fight": "Roll for Fight",
|
||||
@@ -258,12 +332,172 @@
|
||||
"error_no_actor_selected": "No actor selected",
|
||||
"error_not_enough_self_control": "Not enough self control",
|
||||
"error_select_ability": "Select an ability",
|
||||
"attack": "Attack",
|
||||
"attack_contact": "Melee attack",
|
||||
"attack_ranged": "Ranged attack",
|
||||
"defense_esquive": "Dodge",
|
||||
"defense_parade": "Parry",
|
||||
"passive_defense": "Passive defense",
|
||||
"range_short": "Short",
|
||||
"range_mid": "Medium",
|
||||
"range_long": "Long",
|
||||
"second_action": "Second action (+2 difficulty)",
|
||||
"consume_ammo": "Consume ammo",
|
||||
"npc_pool": "Pools used",
|
||||
"exchange": "Combat exchange",
|
||||
"resolve": "Resolve",
|
||||
"resolve_result": "Exchange resolution",
|
||||
"attacker_successes": "Attack successes",
|
||||
"defender_successes": "Defense successes",
|
||||
"raw_damage": "Raw damage",
|
||||
"net_damage": "Net damage",
|
||||
"hit": "Hit",
|
||||
"hit_parade": "Hit (insufficient parry)",
|
||||
"missed_esquive": "Dodged",
|
||||
"missed_parade": "Parried",
|
||||
"no_damage": "No damage",
|
||||
"wound": "Wound",
|
||||
"no_wound": "No wound",
|
||||
"apply_wound": "Apply wound",
|
||||
"wound_applied": "Wound applied",
|
||||
"wound_escalated": "{to} wound (escalated from {from})",
|
||||
"wound_death": "All wounds checked — death",
|
||||
"error_creature_no_defense": "Creatures cannot defend (passive defense)",
|
||||
"error_no_permission": "Insufficient permissions",
|
||||
"error_no_target": "No target selected for this exchange",
|
||||
"experience_phase": "Experience phase",
|
||||
"experience_dice": "Experience dice",
|
||||
"experience_rewards": "Experience rewards",
|
||||
"collective": "collective",
|
||||
"collective_rewards": "Collective rewards",
|
||||
"individual_rewards": "Individual rewards",
|
||||
"collective_group": "Group (collective pool)",
|
||||
"danger": "Danger",
|
||||
"discoveries": "Discoveries",
|
||||
"duration": "Duration",
|
||||
"implication": "Implication",
|
||||
"influence": "Influence",
|
||||
"interpretation": "Interpretation",
|
||||
"reward_anecdotic": "Anecdotal",
|
||||
"reward_minor": "Minor",
|
||||
"reward_major": "Major",
|
||||
"reward_exceptionnal": "Exceptional",
|
||||
"objective_minor": "Minor objective",
|
||||
"objective_major": "Major objective",
|
||||
"trauma_reward": "Chosen trauma",
|
||||
"trauma_reward_5": "Trauma reward (+5D)",
|
||||
"evolution_dice": "Evolution dice",
|
||||
"buy_evolution": "Buy an Adaptation/Mutation",
|
||||
"evolution_name": "Name",
|
||||
"evolution_level": "Level (cost in Evolution dice)",
|
||||
"evolution_mutation": "Mutation",
|
||||
"evolution_adaptation": "Adaptation",
|
||||
"evolution_available_dice": "Accumulated Evolution dice",
|
||||
"evolution_total": "Total levels (after purchase)",
|
||||
"evolution_mode_max": "Game mode maximum",
|
||||
"game_mode": "Game mode",
|
||||
"mode_survie": "Survival",
|
||||
"mode_cauchemar": "Nightmare",
|
||||
"mode_apocalypse": "Apocalypse",
|
||||
"evolution_buy_hint": "The purchase consumes and erases all accumulated Evolution dice. Limits: Survival 5D, Nightmare 6D, Apocalypse 7D total. Level 3/4 Mutations are forbidden in Survival; only one level 4 Mutation.",
|
||||
"buy_evolution_done": "Adaptation/Mutation acquired. Evolution dice erased.",
|
||||
"error_evolution_no_name": "Provide a name for the Adaptation/Mutation.",
|
||||
"error_evolution_invalid_level": "Invalid level (1 to 4).",
|
||||
"error_evolution_not_enough_dice": "Not enough Evolution dice for this level.",
|
||||
"error_evolution_mode_limit": "Game mode maximum Evolution dice reached.",
|
||||
"error_evolution_mutation_mode": "Level 3/4 Mutations require Nightmare or Apocalypse modes.",
|
||||
"error_evolution_mutation_4d": "The character already has a level 4 Mutation.",
|
||||
"error_evolution_create": "Could not create the Adaptation/Mutation.",
|
||||
"open_experience_phase": "Experience phase",
|
||||
"open_learning": "Learning",
|
||||
"validate_phase": "Validate phase",
|
||||
"validate_phase_done": "Experience phase validated: learning limits reset.",
|
||||
"error_not_editable": "You do not have permission to modify this element.",
|
||||
"vehicle_assign": "Attach vehicle",
|
||||
"vehicle_assign_confirm": "This vehicle already belongs to \"{group}\". Attach it to this group?",
|
||||
"vehicle_assign_failed": "Could not attach this vehicle to the group.",
|
||||
"vehicle_import_failed": "Could not import this vehicle from the compendium.",
|
||||
"learning_limits": "Learning limits (per phase)",
|
||||
"learned_skill": "Skill",
|
||||
"learned_specialty": "Specialty",
|
||||
"learned_reserve": "Reserve",
|
||||
"learned_characteristic": "Characteristic",
|
||||
"learned_evolution": "Evolution",
|
||||
"learned_ability": "Totem capacity",
|
||||
"learned_background": "Background",
|
||||
"learned_reputation": "Reputation",
|
||||
"no_character": "No character in this world.",
|
||||
"apply": "Apply",
|
||||
"characteristic": "Characteristic",
|
||||
"learn_skill": "Skill",
|
||||
"learn_specialty": "Specialty",
|
||||
"learn_characteristic": "Characteristic",
|
||||
"learn_reserve": "Reserve",
|
||||
"learn_evolution": "Evolution die",
|
||||
"learn_totem_ability": "Totem capacity",
|
||||
"learn_background": "Background element",
|
||||
"learn_reputation": "Reputation",
|
||||
"learn_adapted_die": "Replace 1 Experience die with an Adapted totem die (×2)",
|
||||
"adapted_die_x2": "1 Adapted totem die ×2",
|
||||
"learn_specialty_name": "Specialty name",
|
||||
"specialty_hint": "Prerequisite: skill at least Proficient (≥2).",
|
||||
"characteristic_hint": "Difficulty 9 + Handicap = current value. Max 3 (except Superhuman).",
|
||||
"reserve_hint": "Difficulty = current value + age Handicap. Max 10.",
|
||||
"evolution_hint": "Difficulty by age + Handicap = accumulated Evolution dice. Adapted totem +1D, Human -1D.",
|
||||
"totem_ability_hint": "Prerequisites: same Totem as the character, Group level ≥ Capacity level, game mode compatible (Survival N1, Nightmare N2, Apocalypse N3).",
|
||||
"background_hint": "Difficulty 9. Handicap (I) if the Background is marked with an asterisk (*). GM authorization required.",
|
||||
"reputation_hint": "Reduce = more famous (Difficulty 10 − target score, 1 pt max per phase). Increase = fade into obscurity (requires a major Objective, Difficulty = current Reputation, +1 per Success).",
|
||||
"reputation_reduce": "Become more famous (reduce)",
|
||||
"reputation_increase": "Fade into obscurity (increase)",
|
||||
"reputation_current": "Current Reputation",
|
||||
"dice_spent": "Dice spent",
|
||||
"learning_success": "Learning successful!",
|
||||
"error_no_experience_dice": "No Experience dice available.",
|
||||
"error_learning_limit": "Limit reached for this type of learning in this phase.",
|
||||
"error_skill_max": "This skill is already at maximum level.",
|
||||
"error_characteristic_max": "Characteristic already at maximum (3).",
|
||||
"error_specialty_requirement": "Specialty requires: skill at least Proficient (≥2).",
|
||||
"error_reserve_max": "Reserve already at maximum (10).",
|
||||
"no_totem_ability": "No Totem capacity available to learn.",
|
||||
"no_background": "No background element to acquire.",
|
||||
"error_totem_mode": "Capacity level incompatible with the game mode.",
|
||||
"error_totem_totem": "The character must share the Totem of the Capacity.",
|
||||
"error_totem_group_level": "Group level insufficient for this Capacity.",
|
||||
"error_totem_limit": "Totem capacity limit reached (3× Level 1, 1× Levels 2 and 3, max 5).",
|
||||
"error_reputation_min": "Reputation already at minimum (0).",
|
||||
"error_reputation_max": "Reputation already at maximum (10).",
|
||||
"error_evolution_no_adapted": "The character must have strictly more Adapted than Human totem dice.",
|
||||
"learn_level": "Group level",
|
||||
"learn_group_reserve": "Group reserve",
|
||||
"learn_ability": "Group capacity",
|
||||
"group_level_hint": "Difficulty = number of members. Handicap = current Group level. Max 10.",
|
||||
"group_reserve_hint": "Difficulty = current Reserve value. Handicap II. Max 10.",
|
||||
"group_ability_hint": "Difficulty = Learning. Handicap = Capacity level. Requires Group level ≥ Capacity level.",
|
||||
"no_group_ability": "No Group capacity available to develop.",
|
||||
"error_group_level_max": "Group level already at maximum (10).",
|
||||
"error_group_reserve_max": "Group reserve already at maximum.",
|
||||
"error_group_level_requirement": "Group level insufficient for this Capacity.",
|
||||
"experience_transfer": "Experience dice",
|
||||
"experience_pull": "Pull from group",
|
||||
"experience_give": "Give 1D to group",
|
||||
"pool_experience": "Experience",
|
||||
"error_no_group": "No group associated with this character.",
|
||||
"error_no_group_dice": "Not enough group pool dice.",
|
||||
"tabs": {
|
||||
"abilities": "Abilities",
|
||||
"combat": "Combat",
|
||||
"equipment": "Equipment",
|
||||
"stories": "Stories",
|
||||
"totem": "Totem"
|
||||
"totem": "Totem",
|
||||
"experience": "Experience"
|
||||
},
|
||||
"Sheet": {
|
||||
"character": "Character",
|
||||
"npc": "NPC",
|
||||
"group": "Group",
|
||||
"creature": "Creature",
|
||||
"community": "Community",
|
||||
"vehicle": "Vehicle"
|
||||
},
|
||||
"base_values": "Base Values",
|
||||
"computed_values": "Computed Values",
|
||||
@@ -280,6 +514,8 @@
|
||||
"morale": "Morale",
|
||||
"needed": "Needed",
|
||||
"objective_placeholder": "eg: Survive, Explore",
|
||||
"major_objective_placeholder": "eg: Reach a distant goal",
|
||||
"minor_objective_placeholder": "eg: Find a medic, scavenge",
|
||||
"objectives": "Objectives",
|
||||
"pack": "Pack",
|
||||
"pattern": "Pattern",
|
||||
@@ -327,6 +563,91 @@
|
||||
"effects.passive": "Passive",
|
||||
"effects.temporary": "Temporary"
|
||||
},
|
||||
"COMMUNITY": {
|
||||
"tabs": {
|
||||
"identity": "Identity",
|
||||
"domains": "Domains",
|
||||
"resources": "Resources"
|
||||
},
|
||||
"identity": "Identity",
|
||||
"name_placeholder": "Community name",
|
||||
"choose_totem": "Choose totem",
|
||||
"origin": "Origin",
|
||||
"theme": "Theme",
|
||||
"age_label": "Community age",
|
||||
"age": {
|
||||
"recent": "A few days",
|
||||
"established": "A few months",
|
||||
"old": "A few years"
|
||||
},
|
||||
"instincts": "Instincts",
|
||||
"prohibits": "Prohibitions",
|
||||
"notes": "Notes",
|
||||
"location": "Location",
|
||||
"sector": "Sector",
|
||||
"placeType": "Place type",
|
||||
"landmarks": "Landmarks",
|
||||
"territory": "Territory",
|
||||
"history": "History",
|
||||
"founding": "Founding",
|
||||
"events": "Key events",
|
||||
"history_relations": "Relations",
|
||||
"objectives": "Objectives",
|
||||
"objective_major": "Major objective",
|
||||
"objectives_minor": "Minor objectives",
|
||||
"add_objective": "Add objective",
|
||||
"size_title": "Size & Population",
|
||||
"size_label": "Size",
|
||||
"sizeLevels": {
|
||||
"1": "Size 1",
|
||||
"2": "Size 2",
|
||||
"3": "Size 3"
|
||||
},
|
||||
"population": "Population",
|
||||
"population_range": "Recommended population",
|
||||
"figures": "Figures",
|
||||
"companies": "Companies",
|
||||
"domains": {
|
||||
"resources": "Resources",
|
||||
"security": "Security",
|
||||
"dynamism": "Dynamism",
|
||||
"relations": "Relations"
|
||||
},
|
||||
"sections": {
|
||||
"vivre": "Food & Water",
|
||||
"supplies": "Supplies",
|
||||
"defense": "Defense",
|
||||
"care": "Care",
|
||||
"cohesion": "Cohesion",
|
||||
"productivity": "Productivity",
|
||||
"human": "Human",
|
||||
"ecology": "Ecology"
|
||||
},
|
||||
"states": {
|
||||
"crisis": "Crisis",
|
||||
"low": "Low",
|
||||
"stable": "Stable",
|
||||
"opulent": "Opulent"
|
||||
},
|
||||
"reserves": "Reserves",
|
||||
"group_reserve": "Group reserve",
|
||||
"material_dice": "Material dice",
|
||||
"experience_dice": "Experience dice",
|
||||
"morale": "Morale",
|
||||
"morale_value": "Value",
|
||||
"morale_state": "State",
|
||||
"morale_levels": {
|
||||
"high": "High",
|
||||
"normal": "Normal",
|
||||
"low": "Low",
|
||||
"crisis": "Crisis"
|
||||
},
|
||||
"morale_blocked": "All four Domains are Low: the Morale bonus is inactive.",
|
||||
"habitants_title": "Inhabitants",
|
||||
"habitants_coming": "Figures and Companies management comes in a future version.",
|
||||
"linked_group": "Linked group",
|
||||
"profile_hint": "Community profile"
|
||||
},
|
||||
"ITEMS": {
|
||||
"item": "Item",
|
||||
"items": "Items",
|
||||
@@ -347,6 +668,8 @@
|
||||
"ability": "Ability",
|
||||
"new_ability": "New ability",
|
||||
"abilities": "Abilities",
|
||||
"creaturecapacity": "Creature capacity",
|
||||
"new_creaturecapacity": "New creature capacity",
|
||||
"specialties": "Specialties",
|
||||
"new_specialty": "New specialty",
|
||||
"target": "Target",
|
||||
@@ -421,6 +744,11 @@
|
||||
"dog": "Dog",
|
||||
"bear": "Bear"
|
||||
},
|
||||
"PACK_LEVELS": {
|
||||
"small": "Small",
|
||||
"large": "Large",
|
||||
"giant": "Giant"
|
||||
},
|
||||
"TOTEMS": {
|
||||
"human": {
|
||||
"name": "Human",
|
||||
@@ -574,7 +902,7 @@
|
||||
"name": "Repulsion"
|
||||
},
|
||||
"tracks": {
|
||||
"name": "Tracks"
|
||||
"name": "Tracking"
|
||||
},
|
||||
"crafting": {
|
||||
"name": "Crafting"
|
||||
@@ -604,7 +932,7 @@
|
||||
"name": "Melee"
|
||||
},
|
||||
"archery": {
|
||||
"name": "Ranged"
|
||||
"name": "Ranged Weapons"
|
||||
},
|
||||
"throwing": {
|
||||
"name": "Throwing"
|
||||
@@ -656,6 +984,7 @@
|
||||
"Round": "Round"
|
||||
},
|
||||
"SIZE_LEVELS": {
|
||||
"tiny": "Insect",
|
||||
"small": "Small",
|
||||
"medium": "Medium",
|
||||
"large": "Large"
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
"TYPES.Item.item": "Objet",
|
||||
"TYPES.Item.weapon": "Arme",
|
||||
"TYPES.Item.defense": "Protection",
|
||||
"TYPES.Item.vehicle": "Véhicule",
|
||||
"TYPES.Actor.vehicle": "Véhicule",
|
||||
"TYPES.Item.ability": "Capacité",
|
||||
"TYPES.Item.creaturecapacity": "Capacité de créature",
|
||||
"TYPES.Item.specialty": "Spécialité",
|
||||
"TYPES.Item.background": "Historique",
|
||||
"TYPES.Item.trauma": "Traumatisme",
|
||||
@@ -49,6 +50,20 @@
|
||||
"nightmare": "Cauchemar",
|
||||
"apocalypse": "Apocalypse"
|
||||
},
|
||||
"CREATURE_CAPACITY_TYPES": {
|
||||
"base": {
|
||||
"name": "Base"
|
||||
},
|
||||
"survival": {
|
||||
"name": "Survie"
|
||||
},
|
||||
"nightmare": {
|
||||
"name": "Cauchemar"
|
||||
},
|
||||
"apocalypse": {
|
||||
"name": "Apocalypse"
|
||||
}
|
||||
},
|
||||
"ROLLS": {
|
||||
"tool": "Lanceur de dés",
|
||||
"roll_dice": "Jeter les dés"
|
||||
@@ -93,7 +108,7 @@
|
||||
"AGE_TYPES": {
|
||||
"young": "Jeune",
|
||||
"adult": "Adulte",
|
||||
"old": "Vieux"
|
||||
"old": "Ancien"
|
||||
},
|
||||
"IDENTITY": {
|
||||
"biography": "Biographie",
|
||||
@@ -146,14 +161,20 @@
|
||||
"level": "Niveau",
|
||||
"experience": "Expérience",
|
||||
"reputation": "Réputation",
|
||||
"chaman": "Chaman",
|
||||
"sheet": {
|
||||
"description": "Description"
|
||||
},
|
||||
"pool": "Réserve",
|
||||
"pools": "Réserves",
|
||||
"reserves": "Réserves",
|
||||
"dice_pool": "Pool de dés",
|
||||
"pool_malus": "Malus Blessure",
|
||||
"auto_successes": "réussites automatiques",
|
||||
"self_control": "Sang-Froid",
|
||||
"effort": "Effort",
|
||||
"equip": "Équiper",
|
||||
"unequip": "Déséquiper",
|
||||
"preferred_category": "Catégorie préférée",
|
||||
"wounds": {
|
||||
"name": "Blessures",
|
||||
@@ -188,6 +209,29 @@
|
||||
"grant_reroll": "accorder des relances",
|
||||
"success_count": "nombre de succès",
|
||||
"success_required": "succès requis",
|
||||
"roll_success": "Réussite",
|
||||
"roll_failure": "Échec",
|
||||
"roll_context": "Contexte du jet",
|
||||
"roll_pool": "Composition du jet",
|
||||
"roll_dice": "Détail des dés",
|
||||
"pool_ability": "Caractéristique",
|
||||
"pool_skill": "Compétence",
|
||||
"pool_specialty": "Spécialité",
|
||||
"pool_help": "Entraide",
|
||||
"pool_tooling": "Équipement",
|
||||
"pool_group": "Groupe",
|
||||
"pool_selfControl": "Sang-Froid",
|
||||
"pool_pools": "Réserves",
|
||||
"pool_total": "Total",
|
||||
"totem_used": "Totem",
|
||||
"totem_keep": "gardé",
|
||||
"domain_bonus": "Bonus de domaine",
|
||||
"domain_penalty": "Malus de domaine",
|
||||
"used": "utilisé",
|
||||
"die_regular": "dés",
|
||||
"die_human": "humains",
|
||||
"die_adapted": "adaptés",
|
||||
"die_successes": "succès",
|
||||
"error_no_actor_selected": "Vous n'avez pas de personnage attitré ou de token sélectionné",
|
||||
"error_cannot_reroll": "Vous ne pouvez pas relancer un dés sur ce jet",
|
||||
"error_no_rerolls_left": "Plus de relances possibles",
|
||||
@@ -230,6 +274,20 @@
|
||||
"range": "Portée",
|
||||
"ranges": "Portées",
|
||||
"mobility": "Mobilité",
|
||||
"vehicle": "Véhicule",
|
||||
"vehicle_specs": "Caractéristiques du véhicule",
|
||||
"kind": "Type",
|
||||
"kind_vehicle": "Véhicule",
|
||||
"kind_mount": "Monture",
|
||||
"distance": "Distance",
|
||||
"speed": "Vitesse",
|
||||
"speed_duration_ph": "durée de pointe (ex: 10 min)",
|
||||
"effort_reserve": "Réserve d'Effort",
|
||||
"maintenance": "Entretien",
|
||||
"owner": "Propriétaire",
|
||||
"no_vehicles": "Aucun véhicule",
|
||||
"no_encounters": "Aucune rencontre",
|
||||
"travel_notes": "Notes de voyage",
|
||||
"rarity": "Rareté",
|
||||
"rarity_sm": "Rar.",
|
||||
"reliability": "Fiabilité",
|
||||
@@ -244,6 +302,170 @@
|
||||
"totem_dice": "Dés de totem",
|
||||
"keep_totem": "Garder le totem",
|
||||
"totem_hint": "Cochez pour utiliser les dés de totem (double réussite possible)",
|
||||
"totem_neutral": "Neutre",
|
||||
"totem_manage": "Gérer les Dés de Totems (Instincts / Interdits)",
|
||||
"totem_serious": "Acte grave (2 dés)",
|
||||
"totem_manage_hint": "Instincts : obéir aux Instincts de l'Humain gagne 1D Humain ; ceux d'un autre Totem gagnent 1D Adapté. Interdit : briser un Interdit de l'Humain fait perdre 1D Humain (ou gagner 1D Adapté si aucun). Max 3 Dés du même Totem et 5 Dés au total.",
|
||||
"totem_instinct_human": "Instinct de l'Humain (+1D Humain)",
|
||||
"totem_instinct_adapted": "Instinct d'un autre Totem (+1D Adapté)",
|
||||
"totem_interdit_human": "Interdit de l'Humain (-1D Humain)",
|
||||
"totem_gained": "{totem} +1D",
|
||||
"totem_gained_adapted": "+1D Adapté",
|
||||
"totem_exchanged": "{totem} gagné en échange (1D opposé retiré)",
|
||||
"totem_replaced": "Dé opposé retiré à la place (max 3D atteint)",
|
||||
"totem_lost": "-1D Humain",
|
||||
"totem_limit_reached": "Limite de Dés de Totems atteinte.",
|
||||
"attack": "Attaque",
|
||||
"attack_contact": "Attaque au contact",
|
||||
"attack_ranged": "Attaque à distance",
|
||||
"defense_esquive": "Esquive",
|
||||
"defense_parade": "Parade",
|
||||
"passive_defense": "Défense passive",
|
||||
"range_short": "Courte",
|
||||
"range_mid": "Moyenne",
|
||||
"range_long": "Longue",
|
||||
"second_action": "Deuxième action (+2 difficulté)",
|
||||
"consume_ammo": "Consommer des munitions",
|
||||
"npc_pool": "Réserves utilisées",
|
||||
"exchange": "Échange de coups",
|
||||
"resolve": "Résoudre",
|
||||
"resolve_result": "Résolution de l'échange",
|
||||
"attacker_successes": "Réussites d'attaque",
|
||||
"defender_successes": "Réussites de défense",
|
||||
"raw_damage": "Dégâts bruts",
|
||||
"net_damage": "Dégâts nets",
|
||||
"hit": "Touché",
|
||||
"hit_parade": "Touché (parade insuffisante)",
|
||||
"missed_esquive": "Esquivé",
|
||||
"missed_parade": "Paré",
|
||||
"no_damage": "Aucun dégât",
|
||||
"wound": "Blessure",
|
||||
"no_wound": "Aucune blessure",
|
||||
"apply_wound": "Appliquer la blessure",
|
||||
"wound_applied": "Blessure appliquée",
|
||||
"wound_escalated": "Blessure {to} (montée depuis {from})",
|
||||
"wound_death": "Toutes les blessures sont cochées — mort",
|
||||
"error_creature_no_defense": "Les créatures ne peuvent pas se défendre (défense passive)",
|
||||
"error_no_permission": "Permissions insuffisantes",
|
||||
"error_no_target": "Aucune cible sélectionnée pour cet échange",
|
||||
"experience_phase": "Phase d'Expérience",
|
||||
"experience_dice": "Dés d'Expérience",
|
||||
"experience_rewards": "Récompenses d'Expérience",
|
||||
"collective": "collectifs",
|
||||
"collective_rewards": "Récompenses collectives",
|
||||
"individual_rewards": "Récompenses individuelles",
|
||||
"collective_group": "Groupe (réserve collective)",
|
||||
"danger": "Danger",
|
||||
"discoveries": "Découvertes",
|
||||
"duration": "Durée",
|
||||
"implication": "Implication",
|
||||
"influence": "Influence",
|
||||
"interpretation": "Interprétation",
|
||||
"reward_anecdotic": "Anecdotique",
|
||||
"reward_minor": "Mineure",
|
||||
"reward_major": "Majeure",
|
||||
"reward_exceptionnal": "Exceptionnelle",
|
||||
"objective_minor": "Objectif mineur",
|
||||
"objective_major": "Objectif majeur",
|
||||
"trauma_reward": "Traumatisme choisi",
|
||||
"trauma_reward_5": "Récompense de Traumatisme (+5D)",
|
||||
"evolution_dice": "Dés d'Évolution",
|
||||
"buy_evolution": "Acheter une Adaptation/Mutation",
|
||||
"evolution_name": "Nom",
|
||||
"evolution_level": "Niveau (coût en Dés d'Évolution)",
|
||||
"evolution_mutation": "Mutation",
|
||||
"evolution_adaptation": "Adaptation",
|
||||
"evolution_available_dice": "Dés d'Évolution accumulés",
|
||||
"evolution_total": "Total des niveaux (après achat)",
|
||||
"evolution_mode_max": "Maximum du Mode de jeu",
|
||||
"game_mode": "Mode de jeu",
|
||||
"mode_survie": "Survie",
|
||||
"mode_cauchemar": "Cauchemar",
|
||||
"mode_apocalypse": "Apocalypse",
|
||||
"evolution_buy_hint": "L'achat consomme et efface tous les Dés d'Évolution accumulés. Limites : Survie 5D, Cauchemar 6D, Apocalypse 7D au total. Mutations 3D/4D interdites en Mode Survie ; une seule Mutation à 4D.",
|
||||
"buy_evolution_done": "Adaptation/Mutation acquise. Dés d'Évolution effacés.",
|
||||
"error_evolution_no_name": "Indiquez un nom pour l'Adaptation/Mutation.",
|
||||
"error_evolution_invalid_level": "Niveau invalide (1 à 4).",
|
||||
"error_evolution_not_enough_dice": "Dés d'Évolution insuffisants pour ce niveau.",
|
||||
"error_evolution_mode_limit": "Maximum de Dés d'Évolution du Mode de jeu atteint.",
|
||||
"error_evolution_mutation_mode": "Les Mutations de Niveau 3/4 nécessitent les Modes Cauchemar ou Apocalypse.",
|
||||
"error_evolution_mutation_4d": "Le personnage possède déjà une Mutation à 4D.",
|
||||
"error_evolution_create": "Impossible de créer l'Adaptation/Mutation.",
|
||||
"open_experience_phase": "Phase d'Expérience",
|
||||
"open_learning": "Apprentissage",
|
||||
"validate_phase": "Valider la phase",
|
||||
"validate_phase_done": "Phase d'Expérience validée : limites d'apprentissage réinitialisées.",
|
||||
"error_not_editable": "Vous n'avez pas la permission de modifier cet élément.",
|
||||
"vehicle_assign": "Attacher le véhicule",
|
||||
"vehicle_assign_confirm": "Ce véhicule appartient déjà à « {group} ». L'attacher à ce Groupe ?",
|
||||
"vehicle_assign_failed": "Impossible d'attacher ce véhicule au Groupe.",
|
||||
"vehicle_import_failed": "Impossible d'importer ce véhicule depuis le compendium.",
|
||||
"learning_limits": "Limites d'apprentissage (par phase)",
|
||||
"learned_skill": "Compétence",
|
||||
"learned_specialty": "Spécialité",
|
||||
"learned_reserve": "Réserve",
|
||||
"learned_characteristic": "Caractéristique",
|
||||
"learned_evolution": "Évolution",
|
||||
"learned_ability": "Capacité de Totem",
|
||||
"learned_background": "Historique",
|
||||
"learned_reputation": "Réputation",
|
||||
"no_character": "Aucun personnage dans ce monde.",
|
||||
"apply": "Appliquer",
|
||||
"characteristic": "Caractéristique",
|
||||
"learn_skill": "Compétence",
|
||||
"learn_specialty": "Spécialité",
|
||||
"learn_characteristic": "Caractéristique",
|
||||
"learn_reserve": "Réserve",
|
||||
"learn_evolution": "Dé d'Évolution",
|
||||
"learn_totem_ability": "Capacité de Totem",
|
||||
"learn_background": "Élément d'Historique",
|
||||
"learn_reputation": "Réputation",
|
||||
"learn_adapted_die": "Remplacer 1D d'Expérience par un Dé de Totem Adapté (×2)",
|
||||
"adapted_die_x2": "1 Dé de Totem Adapté ×2",
|
||||
"learn_specialty_name": "Nom de la spécialité",
|
||||
"specialty_hint": "Prérequis : compétence au moins Confirmée (≥2).",
|
||||
"characteristic_hint": "Difficulté 9 + Handicap = valeur actuelle. Max 3 (Surhumain excepté).",
|
||||
"reserve_hint": "Difficulté = valeur actuelle + Handicap d'âge. Max 10.",
|
||||
"evolution_hint": "Difficulté selon l'âge + Handicap = Dés d'Évolution accumulés. Totem Adapté +1D, Humain -1D.",
|
||||
"totem_ability_hint": "Prérequis : même Totem que le personnage, Niveau de Groupe ≥ Niveau de la Capacité, Mode de jeu compatible (Survie N1, Cauchemar N2, Apocalypse N3).",
|
||||
"background_hint": "Difficulté 9. Handicap (I) si l'Historique est marqué d'un astérisque (*). Autorisation du MJ requise.",
|
||||
"reputation_hint": "Réduire = plus célèbre (Difficulté 10 − score visé, 1 pt max/phase). Augmenter = se faire oublier (exige un Objectif majeur, Difficulté = Réputation actuelle, +1 par Réussite).",
|
||||
"reputation_reduce": "Devenir plus célèbre (réduire)",
|
||||
"reputation_increase": "Se faire oublier (augmenter)",
|
||||
"reputation_current": "Réputation actuelle",
|
||||
"dice_spent": "Dés dépensés",
|
||||
"learning_success": "Apprentissage réussi !",
|
||||
"error_no_experience_dice": "Pas de Dés d'Expérience disponibles.",
|
||||
"error_learning_limit": "Limite atteinte pour ce type d'apprentissage dans cette phase.",
|
||||
"error_skill_max": "Cette compétence est déjà au niveau maximal.",
|
||||
"error_characteristic_max": "Caractéristique déjà au maximum (3).",
|
||||
"error_specialty_requirement": "Spécialité requise : compétence au moins Confirmée (≥2).",
|
||||
"error_reserve_max": "Réserve déjà au maximum (10).",
|
||||
"no_totem_ability": "Aucune Capacité de Totem disponible à apprendre.",
|
||||
"no_background": "Aucun élément d'Historique à acquérir.",
|
||||
"error_totem_mode": "Niveau de Capacité incompatible avec le Mode de jeu.",
|
||||
"error_totem_totem": "Le personnage doit être du même Totem que la Capacité.",
|
||||
"error_totem_group_level": "Niveau de Groupe insuffisant pour cette Capacité.",
|
||||
"error_totem_limit": "Limite de Capacités de Totem atteinte (3× Niveau 1, 1× Niveaux 2 et 3, max 5).",
|
||||
"error_reputation_min": "Réputation déjà au minimum (0).",
|
||||
"error_reputation_max": "Réputation déjà au maximum (10).",
|
||||
"error_evolution_no_adapted": "Il faut strictement plus de Dés du Totem Adapté que Humains.",
|
||||
"learn_level": "Niveau de Groupe",
|
||||
"learn_group_reserve": "Réserve de Groupe",
|
||||
"learn_ability": "Capacité de Groupe",
|
||||
"group_level_hint": "Difficulté = nombre de membres. Handicap = Niveau actuel du Groupe. Max 10.",
|
||||
"group_reserve_hint": "Difficulté = valeur actuelle de la Réserve. Handicap II. Max 10.",
|
||||
"group_ability_hint": "Difficulté = Apprentissage. Handicap = Niveau de la Capacité. Nécessite un Niveau de Groupe ≥ Niveau de la Capacité.",
|
||||
"no_group_ability": "Aucune Capacité de Groupe disponible à développer.",
|
||||
"error_group_level_max": "Niveau de Groupe déjà au maximum (10).",
|
||||
"error_group_reserve_max": "Réserve de Groupe déjà au maximum.",
|
||||
"error_group_level_requirement": "Niveau de Groupe insuffisant pour cette Capacité.",
|
||||
"experience_transfer": "Dés d'Expérience",
|
||||
"experience_pull": "Puiser du Groupe",
|
||||
"experience_give": "Donner 1D au Groupe",
|
||||
"pool_experience": "Expérience",
|
||||
"error_no_group": "Aucun Groupe associé à ce personnage.",
|
||||
"error_no_group_dice": "Réserve de Groupe insuffisante.",
|
||||
"error_select_skill": "Veuillez sélectionner une compétence",
|
||||
"needed": "nécessaire",
|
||||
"cost": "Coût",
|
||||
@@ -259,7 +481,8 @@
|
||||
"totem": "Totem et ajustements",
|
||||
"combat": "Combat et reserves",
|
||||
"equipment": "Matériel",
|
||||
"stories": "Histoire"
|
||||
"stories": "Histoire",
|
||||
"experience": "Expérience"
|
||||
},
|
||||
"ammo": "Munitions",
|
||||
"ammo_sm": "Mun",
|
||||
@@ -277,10 +500,13 @@
|
||||
"boost": "boost",
|
||||
"group_abilities": "Capacités de groupe",
|
||||
"totem_abilities": "Capacités de totem",
|
||||
"creature_capacities": "Capacités de créature",
|
||||
"type_group": "Groupe",
|
||||
"type_character": "Personnage",
|
||||
"type_npc": "PNJ",
|
||||
"type_creature": "Créature",
|
||||
"type_community": "Communauté",
|
||||
"type_vehicle": "Véhicule",
|
||||
"type": "Type",
|
||||
"group_members": "Membres",
|
||||
"encounters": "Rencontres",
|
||||
@@ -301,6 +527,8 @@
|
||||
"morale_low": "Bas",
|
||||
"morale_crisis": "Crise",
|
||||
"objective_placeholder": "Objectif...",
|
||||
"major_objective_placeholder": "Objectif majeur...",
|
||||
"minor_objective_placeholder": "Objectif mineur...",
|
||||
"instincts_placeholder": "Ex: Triompher, relever un défi...",
|
||||
"prohibits_placeholder": "Ex: Fuir, abandonner...",
|
||||
"error_unknown_actor": "Acteur inconnu",
|
||||
@@ -326,7 +554,9 @@
|
||||
"character": "Personnage",
|
||||
"npc": "PNJ",
|
||||
"group": "Groupe",
|
||||
"creature": "Créature"
|
||||
"creature": "Créature",
|
||||
"community": "Communauté",
|
||||
"vehicle": "Véhicule"
|
||||
}
|
||||
},
|
||||
"UI": {
|
||||
@@ -344,6 +574,91 @@
|
||||
"inactive": "Effets inactifs"
|
||||
}
|
||||
},
|
||||
"COMMUNITY": {
|
||||
"tabs": {
|
||||
"identity": "Identité",
|
||||
"domains": "Domaines",
|
||||
"resources": "Ressources"
|
||||
},
|
||||
"identity": "Identité",
|
||||
"name_placeholder": "Nom de la Communauté",
|
||||
"choose_totem": "Choisir le totem",
|
||||
"origin": "Origine",
|
||||
"theme": "Thème",
|
||||
"age_label": "Âge de la Communauté",
|
||||
"age": {
|
||||
"recent": "Quelques jours",
|
||||
"established": "Quelques mois",
|
||||
"old": "Quelques années"
|
||||
},
|
||||
"instincts": "Instincts",
|
||||
"prohibits": "Interdits",
|
||||
"notes": "Notes",
|
||||
"location": "Localisation",
|
||||
"sector": "Secteur",
|
||||
"placeType": "Type de lieu",
|
||||
"landmarks": "Éléments marquants",
|
||||
"territory": "Territoire",
|
||||
"history": "Histoire",
|
||||
"founding": "Fondation",
|
||||
"events": "Événements marquants",
|
||||
"history_relations": "Relations",
|
||||
"objectives": "Objectifs",
|
||||
"objective_major": "Objectif majeur",
|
||||
"objectives_minor": "Objectifs mineurs",
|
||||
"add_objective": "Ajouter un objectif",
|
||||
"size_title": "Taille & Effectif",
|
||||
"size_label": "Taille",
|
||||
"sizeLevels": {
|
||||
"1": "Taille 1",
|
||||
"2": "Taille 2",
|
||||
"3": "Taille 3"
|
||||
},
|
||||
"population": "Effectif",
|
||||
"population_range": "Population recommandée",
|
||||
"figures": "Figures",
|
||||
"companies": "Compagnies",
|
||||
"domains": {
|
||||
"resources": "Ressources",
|
||||
"security": "Sécurité",
|
||||
"dynamism": "Dynamisme",
|
||||
"relations": "Rapports"
|
||||
},
|
||||
"sections": {
|
||||
"vivre": "Vivres",
|
||||
"supplies": "Fournitures",
|
||||
"defense": "Défense",
|
||||
"care": "Soins",
|
||||
"cohesion": "Cohésion",
|
||||
"productivity": "Productivité",
|
||||
"human": "Humain",
|
||||
"ecology": "Écologie"
|
||||
},
|
||||
"states": {
|
||||
"crisis": "Crise",
|
||||
"low": "Bas",
|
||||
"stable": "Stable",
|
||||
"opulent": "Opulent"
|
||||
},
|
||||
"reserves": "Réserves",
|
||||
"group_reserve": "Réserve de Groupe",
|
||||
"material_dice": "Dés de Matériel",
|
||||
"experience_dice": "Dés d'Expérience",
|
||||
"morale": "Moral",
|
||||
"morale_value": "Valeur",
|
||||
"morale_state": "État",
|
||||
"morale_levels": {
|
||||
"high": "Haut",
|
||||
"normal": "Normal",
|
||||
"low": "Bas",
|
||||
"crisis": "Crise"
|
||||
},
|
||||
"morale_blocked": "Les quatre Domaines sont Bas : le Bonus de Moral est inactif.",
|
||||
"habitants_title": "Habitants",
|
||||
"habitants_coming": "La gestion des Figures et des Compagnies arrive dans une prochaine version.",
|
||||
"linked_group": "Groupe rattaché",
|
||||
"profile_hint": "Profil de la Communauté"
|
||||
},
|
||||
"ITEMS": {
|
||||
"item": "Objet",
|
||||
"items": "Objets",
|
||||
@@ -364,6 +679,8 @@
|
||||
"ability": "Capacité",
|
||||
"new_ability": "Nouvelle capacité",
|
||||
"abilities": "Capacités",
|
||||
"creaturecapacity": "Capacité de créature",
|
||||
"new_creaturecapacity": "Nouvelle capacité de créature",
|
||||
"specialties": "Spécialités",
|
||||
"new_specialty": "Nouvelle spécialité",
|
||||
"target": "Cible",
|
||||
@@ -433,6 +750,7 @@
|
||||
"major": "Majeur"
|
||||
},
|
||||
"SIZE_LEVELS": {
|
||||
"tiny": "Insecte",
|
||||
"small": "Petit",
|
||||
"medium": "Moyen",
|
||||
"large": "Grand"
|
||||
@@ -443,6 +761,11 @@
|
||||
"dog": "Chien",
|
||||
"bear": "Ours"
|
||||
},
|
||||
"PACK_LEVELS": {
|
||||
"small": "Petit",
|
||||
"large": "Grand",
|
||||
"giant": "Géant"
|
||||
},
|
||||
"TOTEMS": {
|
||||
"human": {
|
||||
"name": "L'Humain",
|
||||
@@ -596,7 +919,7 @@
|
||||
"name": "Répulsion"
|
||||
},
|
||||
"tracks": {
|
||||
"name": "Pistage"
|
||||
"name": "Traces"
|
||||
},
|
||||
"crafting": {
|
||||
"name": "Artisanat"
|
||||
@@ -617,7 +940,7 @@
|
||||
"name": "Armes à feu"
|
||||
},
|
||||
"archery": {
|
||||
"name": "Tir à l'arc"
|
||||
"name": "Armes de Tir"
|
||||
},
|
||||
"armory": {
|
||||
"name": "Armurerie"
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
input:not([type="radio"]):not([type="checkbox"]),
|
||||
select, textarea {
|
||||
color: @color-text-light-0;
|
||||
color: #333;
|
||||
line-height: 1.2;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
@@ -188,6 +188,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stacked layout for creature sheets (header + tabs + content) ───
|
||||
&.creature .window-content {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
grid-template-columns: 1fr;
|
||||
overflow: hidden;
|
||||
|
||||
> .tab.main {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
> nav.tabs {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
> .tab:not(.main) {
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
display: none;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
|
||||
&.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -304,6 +340,61 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Fiche personnage (ApplicationV2 : l'élément fiche EST le <form>, le
|
||||
// sélecteur "form div.hexa" ci-dessus ne s'y applique donc pas).
|
||||
&.character div.hexa {
|
||||
.hexa-style();
|
||||
.transition();
|
||||
margin: 0.2rem;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.hexa-style(rgba(255, 255, 255, 0.425), rgba(0, 0, 0, 0.288));
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
.hexa-style(rgb(0, 0, 0), rgba(0, 0, 0, 0.288));
|
||||
&:hover { .hexa-style(rgb(43, 43, 43), rgba(0, 0, 0, 0.288)); }
|
||||
}
|
||||
}
|
||||
|
||||
// Fiche de Groupe : mêmes dés de Réserve cliquables (AppV2).
|
||||
&.group div.hexa {
|
||||
.hexa-style();
|
||||
.transition();
|
||||
margin: 0.2rem;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.hexa-style(rgba(255, 255, 255, 0.425), rgba(0, 0, 0, 0.288));
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
.hexa-style(rgb(0, 0, 0), rgba(0, 0, 0, 0.288));
|
||||
&:hover { .hexa-style(rgb(43, 43, 43), rgba(0, 0, 0, 0.288)); }
|
||||
}
|
||||
}
|
||||
|
||||
.sheet-header-toggle {
|
||||
text-align: right;
|
||||
margin-bottom: 0.5rem;
|
||||
@@ -605,6 +696,27 @@
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
}
|
||||
|
||||
.evolution-badge {
|
||||
margin-left: 6px;
|
||||
padding: 1px 6px;
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: @font-size-11;
|
||||
text-transform: uppercase;
|
||||
border-radius: 3px;
|
||||
|
||||
&.adaptation {
|
||||
color: @color-text-dark-header;
|
||||
background: fade(@color-acid-green, 25%);
|
||||
border: 1px solid @color-acid-green;
|
||||
}
|
||||
|
||||
&.mutation {
|
||||
color: @color-text-dark-header;
|
||||
background: fade(@color-hemolymph, 30%);
|
||||
border: 1px solid @color-hemolymph;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── V2 tab navigation — chitin-plate style ─────────────────────────
|
||||
@@ -615,7 +727,7 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: @color-chitin-dark;
|
||||
border-bottom: 2px solid @color-amber;
|
||||
border-bottom: 2px solid @color-hemolymph;
|
||||
min-height: 36px;
|
||||
|
||||
> a {
|
||||
@@ -639,7 +751,7 @@
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(180deg, @color-amber 0%, @color-honeycomb 100%);
|
||||
background: @color-honeycomb;
|
||||
color: @color-chitin-dark;
|
||||
|
||||
&::after {
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
@import "../variables";
|
||||
@import "../utilities";
|
||||
|
||||
.system-vermine2047 .vermine2047.actor.community {
|
||||
.char-header {
|
||||
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
|
||||
max-height: 130px;
|
||||
|
||||
.community-portrait {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.char-name,
|
||||
.char-vermine2047 {
|
||||
border-bottom: none;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
.char-vermine2047 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.profile,
|
||||
.origin,
|
||||
.level,
|
||||
.population {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.level input,
|
||||
.population input {
|
||||
width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.community-section {
|
||||
padding: 8px;
|
||||
border: 1px solid @color-border-dark-4;
|
||||
background: fade(@color-chitin-dark, 20%);
|
||||
|
||||
h3 {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-13;
|
||||
color: @theme-color-dark;
|
||||
margin: 0 0 8px;
|
||||
text-align: left;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.community-linked-group {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
padding: 6px 8px;
|
||||
border: 1px dashed @color-hemolymph;
|
||||
background: fade(@color-hemolymph, 8%);
|
||||
|
||||
.label {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-11;
|
||||
color: @color-text-light-2;
|
||||
}
|
||||
}
|
||||
|
||||
.community-size-info {
|
||||
margin: 6px 0 0;
|
||||
font-size: @font-size-11;
|
||||
color: @color-text-dark-secondary;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.community-warning {
|
||||
margin: 6px 0 0;
|
||||
padding: 4px 8px;
|
||||
font-size: @font-size-11;
|
||||
font-weight: 700;
|
||||
color: @color-hemolymph;
|
||||
background: fade(@color-hemolymph, 12%);
|
||||
border: 1px solid @color-hemolymph;
|
||||
}
|
||||
|
||||
.community-hint {
|
||||
margin: 0;
|
||||
font-size: @font-size-11;
|
||||
font-style: italic;
|
||||
color: @color-text-dark-secondary;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
// Jauges de Domaines
|
||||
.gauge {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 6px;
|
||||
border: 1px solid @color-border-dark-4;
|
||||
border-left: 4px solid @color-border-dark-4;
|
||||
|
||||
label {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-11;
|
||||
color: @color-text-light-2;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
width: 48px;
|
||||
text-align: center;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.gauge-value {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
font-size: @font-size-16;
|
||||
color: @color-text-dark-header;
|
||||
}
|
||||
|
||||
.gauge-state {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: @font-size-11;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&.crisis { border-left-color: @color-hemolymph; .gauge-state { color: @color-hemolymph; } }
|
||||
&.low { border-left-color: @color-honeycomb; .gauge-state { color: @color-honeycomb; } }
|
||||
&.stable { border-left-color: @color-acid-green; .gauge-state { color: @color-acid-green; } }
|
||||
&.opulent { border-left-color: @theme-color-accent; .gauge-state { color: @theme-color-accent; } }
|
||||
}
|
||||
|
||||
.morale-level {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: @font-size-12;
|
||||
font-weight: 700;
|
||||
|
||||
&.high { color: @color-acid-green; }
|
||||
&.normal { color: @color-text-dark-secondary; }
|
||||
&.low { color: @color-honeycomb; }
|
||||
&.crisis { color: @color-hemolymph; }
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
.sheet-header {
|
||||
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
|
||||
padding: 10px;
|
||||
max-height: 110px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.header-fields { flex: 1; }
|
||||
@@ -13,6 +13,10 @@
|
||||
|
||||
.resource {
|
||||
.card-style();
|
||||
|
||||
// Le mixin card-style ajoute un margin-bottom (15px) : inutile dans le
|
||||
// header, on le neutralise pour garder une hauteur compacte.
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.resource-label {
|
||||
@@ -34,13 +38,20 @@
|
||||
|
||||
.charname {
|
||||
margin: 0;
|
||||
height: auto;
|
||||
font-size: @font-size-18;
|
||||
line-height: 1.2;
|
||||
|
||||
span,
|
||||
input {
|
||||
width: 100%;
|
||||
font-size: @font-size-18;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
@@ -116,4 +127,85 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
// ── Cases de blessures (hexagones) ──────────────────────────────────
|
||||
// Le radio est masqué et recouvre l'hexagone : cliquer sur la case la coche.
|
||||
div.hexa {
|
||||
.hexa-style();
|
||||
.transition();
|
||||
margin: 0.2rem;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.hexa-style(rgba(255, 255, 255, 0.425), rgba(0, 0, 0, 0.288));
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
.hexa-style(rgb(0, 0, 0), rgba(0, 0, 0, 0.288));
|
||||
&:hover { .hexa-style(rgb(43, 43, 43), rgba(0, 0, 0, 0.288)); }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Capacités de créature ───────────────────────────────────────────
|
||||
.creature-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
> li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 6px;
|
||||
border-bottom: 1px solid @color-border-dark-3;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
&:hover { box-shadow: 0 0 10px @theme-color-highlight inset; }
|
||||
}
|
||||
|
||||
.item-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.item-roll { cursor: pointer; }
|
||||
}
|
||||
|
||||
.capacity-type {
|
||||
flex: 0 0 auto;
|
||||
font-size: @font-size-12;
|
||||
color: @color-text-light-2;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.item-controls {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
a.item-control {
|
||||
color: inherit;
|
||||
opacity: 0.85;
|
||||
|
||||
&:hover { opacity: 1; }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,13 +15,113 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── Titres de section : uniforme (centré, scotch, majuscules) ───────
|
||||
h4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-14;
|
||||
text-align: center;
|
||||
padding: 2px 8px;
|
||||
margin: 0 0 8px;
|
||||
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
opacity: 0.85;
|
||||
|
||||
&:hover { opacity: 1; }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sous-titres (ex. Objectifs majeurs / mineurs) ───────────────────
|
||||
h5 {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-12;
|
||||
// Brun foncé lisible sur le fond beige de la fiche (le jaune était illisible).
|
||||
color: @color-chitin-dark;
|
||||
margin: 8px 0 4px;
|
||||
text-align: left;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
// ── Liste uniforme (objectifs, capacités, membres, rencontres) ──────
|
||||
.group-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
> li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 6px;
|
||||
border-bottom: 1px solid @color-border-dark-3;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
&:hover { box-shadow: 0 0 10px @theme-color-highlight inset; }
|
||||
}
|
||||
|
||||
> li.group-list-header {
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-12;
|
||||
border-bottom: 2px solid @color-border-dark-3;
|
||||
box-shadow: none;
|
||||
|
||||
> div { text-align: center; }
|
||||
}
|
||||
|
||||
.item-name, .actor-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-controls {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Champs (Origine, Concept, Instincts, Interdits…) ────────────────
|
||||
.group-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
label {
|
||||
flex: 0 0 auto;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
> :last-child {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
input[type="text"] { width: 100%; }
|
||||
}
|
||||
|
||||
// ── Colonnes des tableaux (Rencontres) ──────────────────────────────
|
||||
.gcol-name { flex: 2; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.gcol-profile { flex: 3; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.gcol-totem { flex: 2; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
.reserve-control,
|
||||
.morale-control {
|
||||
.card-style();
|
||||
@@ -39,58 +139,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.actor-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
li.actor {
|
||||
padding: 5px;
|
||||
border-bottom: 1px solid @color-border-dark-3;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 10px @theme-color-highlight inset;
|
||||
}
|
||||
}
|
||||
|
||||
.actor-name, .item-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.objective-item {
|
||||
padding: 5px;
|
||||
border-bottom: 1px solid @color-border-dark-4;
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.totem-display {
|
||||
.card-style();
|
||||
flex: 1;
|
||||
|
||||
span { font-weight: bold; }
|
||||
}
|
||||
|
||||
.identity-field {
|
||||
margin-bottom: 8px;
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
input { width: 100%; }
|
||||
}
|
||||
|
||||
.level input,
|
||||
.reputation input {
|
||||
width: 60px;
|
||||
|
||||
@@ -5,7 +5,25 @@
|
||||
.sheet-header {
|
||||
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
|
||||
padding: 0.5rem;
|
||||
max-height: 150px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.charname {
|
||||
margin: 0;
|
||||
height: auto;
|
||||
font-size: @font-size-18;
|
||||
line-height: 1.2;
|
||||
|
||||
span,
|
||||
input {
|
||||
font-size: @font-size-18;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ability-value,
|
||||
@@ -30,77 +48,81 @@
|
||||
}
|
||||
}
|
||||
|
||||
.skills-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.skill-category {
|
||||
.card-style();
|
||||
padding: 10px;
|
||||
|
||||
h4 {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
font-size: @font-size-14;
|
||||
text-transform: uppercase;
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
|
||||
}
|
||||
}
|
||||
|
||||
.skill-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px;
|
||||
border-bottom: 1px solid @color-border-dark-3;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
|
||||
label {
|
||||
flex: 1;
|
||||
.npc-trait-list {
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid @color-border-dark-3;
|
||||
font-size: @font-size-12;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
|
||||
strong { flex: 1; }
|
||||
}
|
||||
|
||||
.trait-value {
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.npc-roll-btn {
|
||||
color: @color-text-dark-secondary;
|
||||
opacity: 0.75;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 10px @theme-color-highlight inset;
|
||||
opacity: 1;
|
||||
color: @theme-color-dark;
|
||||
text-shadow: 0 0 6px @theme-color-highlight;
|
||||
}
|
||||
}
|
||||
|
||||
.skill-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
.npc-wounds {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.wound-cat {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-weight: bold;
|
||||
|
||||
.wound-label {
|
||||
font-size: @font-size-12;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.wound-boxes {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
|
||||
i {
|
||||
font-size: 8px;
|
||||
color: @color-border-dark-2;
|
||||
}
|
||||
|
||||
.hexa {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.skill-value {
|
||||
min-width: 25px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
.npc-free-skills {
|
||||
margin-top: 15px;
|
||||
.form-group-style();
|
||||
|
||||
.rarity-badge {
|
||||
display: inline-block;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
|
||||
&.rarity-0 { .rarity-badge(@rarity-0-bg, @rarity-0-text); }
|
||||
&.rarity-1 { .rarity-badge(@rarity-1-bg, @rarity-1-text); }
|
||||
&.rarity-2 { .rarity-badge(@rarity-2-bg, @rarity-2-text); }
|
||||
&.rarity-3 { .rarity-badge(@rarity-3-bg, @rarity-3-text); }
|
||||
}
|
||||
|
||||
.skill-category-selector {
|
||||
margin-bottom: 15px;
|
||||
.card-style();
|
||||
label {
|
||||
font-size: @font-size-12;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.wounds-card {
|
||||
@@ -133,39 +155,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ability-section {
|
||||
.card-style();
|
||||
}
|
||||
|
||||
.ability-category-header {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
font-size: @font-size-14;
|
||||
text-transform: uppercase;
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid @color-border-dark-3;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.ability-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 0;
|
||||
|
||||
label {
|
||||
flex: 1;
|
||||
font-size: @font-size-12;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.ability-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
@@ -213,6 +202,17 @@
|
||||
padding: 3px 5px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
font-size: @font-size-12;
|
||||
padding: 3px 5px;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.npc-profile-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.editor {
|
||||
@@ -228,9 +228,25 @@
|
||||
.hexa {
|
||||
.hexa-style();
|
||||
.transition();
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.hexa-style(rgba(255, 255, 255, 0.425), rgba(0, 0, 0, 0.288));
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
.hexa-style(rgb(0, 0, 0), rgba(0, 0, 0, 0.288));
|
||||
&:hover { .hexa-style(rgb(43, 43, 43), rgba(0, 0, 0, 0.288)); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle « Chaman » de l'onglet Totem : ne pas étirer sur toute la largeur.
|
||||
.chaman-toggle {
|
||||
width: fit-content;
|
||||
margin: 0.5rem auto;
|
||||
gap: 0.5rem;
|
||||
|
||||
label {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-12;
|
||||
color: @theme-color-dark;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
div.minor-totems {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -101,5 +124,37 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.totem-influence-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
margin-top: 2px;
|
||||
font-size: 0.75rem;
|
||||
|
||||
.totem-influence-label {
|
||||
color: @color-text-dark-header;
|
||||
|
||||
strong.totem-human { color: @totem-human-color; }
|
||||
strong.totem-adapted { color: @totem-adapted-color; }
|
||||
}
|
||||
|
||||
.totem-manage-btn {
|
||||
flex: none;
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
color: @color-text-dark-header;
|
||||
background: fade(@color-text-light-0, 30%);
|
||||
border: 1px solid @color-text-dark-secondary;
|
||||
border-radius: 3px;
|
||||
.transition();
|
||||
|
||||
&:hover {
|
||||
color: @theme-color-dark;
|
||||
background: @color-hemolymph;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
@import "../variables";
|
||||
@import "../utilities";
|
||||
|
||||
.system-vermine2047 .vermine2047.actor.vehicle {
|
||||
display: grid !important;
|
||||
grid-template-columns: 1fr !important;
|
||||
grid-template-rows: auto 1fr auto !important;
|
||||
|
||||
> header.window-header {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
> section.window-content {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
> div.window-resize-handle {
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
.window-content {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
grid-template-columns: 1fr;
|
||||
overflow: hidden;
|
||||
|
||||
> .tab.main {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
> nav.tabs {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
> .tab:not(.main) {
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
display: none;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
|
||||
&.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sheet-header {
|
||||
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
|
||||
padding: 10px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.header-fields { flex: 1; }
|
||||
|
||||
.resource {
|
||||
.card-style();
|
||||
|
||||
// Le mixin card-style ajoute un margin-bottom (15px) : inutile dans le
|
||||
// header, on le neutralise pour garder une hauteur compacte.
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.resource-label {
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
min-width: 60px;
|
||||
font-size: @font-size-12;
|
||||
}
|
||||
|
||||
.resource-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
select {
|
||||
margin-right: 8px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.charname {
|
||||
margin: 0;
|
||||
height: auto;
|
||||
font-size: @font-size-18;
|
||||
line-height: 1.2;
|
||||
|
||||
span,
|
||||
input {
|
||||
font-size: @font-size-18;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.vehicle-owner {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
padding: 6px 8px;
|
||||
border: 1px dashed @color-hemolymph;
|
||||
background: fade(@color-hemolymph, 8%);
|
||||
|
||||
.label {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-11;
|
||||
color: @color-text-light-2;
|
||||
}
|
||||
}
|
||||
|
||||
.vehicle-section {
|
||||
padding: 8px;
|
||||
border: 1px solid @color-border-dark-4;
|
||||
background: fade(@color-chitin-dark, 20%);
|
||||
margin-bottom: 8px;
|
||||
|
||||
h3 {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-13;
|
||||
color: @theme-color-dark;
|
||||
margin: 0 0 8px;
|
||||
text-align: left;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.resource-label {
|
||||
display: block;
|
||||
margin: 0 0 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: @font-size-11;
|
||||
color: @color-text-dark-secondary;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.maintenance {
|
||||
margin-top: 8px;
|
||||
|
||||
.flexrow label {
|
||||
gap: 4px;
|
||||
margin-right: 12px;
|
||||
font-size: @font-size-12;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
label {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-11;
|
||||
color: @color-text-light-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
@@ -13,28 +13,42 @@ body.system-vermine2047 {
|
||||
}
|
||||
|
||||
// Override Foundry Core default CSS custom properties
|
||||
// for better readability on the dark Vermine2047 theme.
|
||||
// for better readability on the light 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;
|
||||
// Core windows (FilePicker, dialogs, compendium, RollTable, Journal, …) and chat
|
||||
// messages sit on the light box_background texture: force dark readable text.
|
||||
// The dark sidebar keeps Foundry's default light-on-dark text colors.
|
||||
body.system-vermine2047.theme-dark {
|
||||
.window-content,
|
||||
.roll-table-sheet,
|
||||
.journal-sheet,
|
||||
.document-sheet,
|
||||
.settings-sheet {
|
||||
color: @color-text-dark-primary;
|
||||
--color-text-primary: @color-text-dark-primary;
|
||||
--color-text-emphatic: @color-text-dark-header;
|
||||
--color-text-secondary: @color-text-dark-secondary;
|
||||
--color-text-subtle: #666666;
|
||||
--color-text-light-highlight: @color-acid-green;
|
||||
--color-text-light-heading: @color-text-dark-header;
|
||||
--color-text-light-primary: @color-text-dark-secondary;
|
||||
--color-text-dark-primary: @color-text-dark-primary;
|
||||
--color-text-dark-secondary: @color-text-dark-secondary;
|
||||
--color-text-dark-header: @color-text-dark-header;
|
||||
--text-color: @color-text-dark-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
#chat-log .message,
|
||||
.chat-log .message {
|
||||
.message-content {
|
||||
color: @color-text-dark-primary;
|
||||
}
|
||||
|
||||
header.message-header {
|
||||
color: #fff;
|
||||
text-shadow: 0 0 5px #000;
|
||||
}
|
||||
}
|
||||
|
||||
#settings-config button,
|
||||
@@ -45,12 +59,12 @@ body.system-vermine2047.theme-dark {
|
||||
}
|
||||
|
||||
body.system-vermine2047.theme-dark .form-group > label {
|
||||
color: #777777;
|
||||
color: @color-text-dark-secondary;
|
||||
}
|
||||
|
||||
#settings-config p,
|
||||
#settings-config .hint {
|
||||
color: #777777;
|
||||
color: @color-text-dark-secondary;
|
||||
}
|
||||
|
||||
img {
|
||||
@@ -97,6 +111,22 @@ ul.unstyled {
|
||||
}
|
||||
}
|
||||
|
||||
// Les fenêtres de choix (totem, acteur) peuvent dépasser la hauteur fixe :
|
||||
// on autorise le défilement pour accéder à la totalité du contenu.
|
||||
.application#TOTEM_PICKER .window-content,
|
||||
.application#ACTOR_PICKER .window-content,
|
||||
.application#TRAIT_SELECTOR .window-content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
// La fiche de groupe contient beaucoup de zones (membres, objectifs, totem…) :
|
||||
// dès que le contenu dépasse la hauteur de la fenêtre, on autorise le défilement
|
||||
// pour ne pas perdre l'accès aux zones du bas.
|
||||
.application.sheet.vermine2047.group .window-content,
|
||||
.application.sheet.vermine2047.community .window-content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.window-content .row {
|
||||
&.smb { margin-bottom: 0.25rem; }
|
||||
&.mdb { margin-bottom: 0.5rem; }
|
||||
@@ -113,6 +143,29 @@ ul.unstyled {
|
||||
|
||||
.rollable {
|
||||
.transition();
|
||||
|
||||
// Affordance légère au repos : la zone est cliquable.
|
||||
i.fa-dice-d10,
|
||||
i.fa-dice {
|
||||
font-size: 0.8em;
|
||||
color: @color-acid-green;
|
||||
}
|
||||
}
|
||||
|
||||
// Sur la fiche créature, les valeurs lançables (Attaque…) sont fortement
|
||||
// matérialisées : pastille de dé + bordure pointillée.
|
||||
.application.sheet.vermine2047.creature {
|
||||
.rollable {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
color: @theme-color-light;
|
||||
border: 1px dashed fade(@color-acid-green, 45%);
|
||||
background: fade(@color-acid-green, 8%);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
img.profile-img {
|
||||
@@ -420,7 +473,7 @@ body.system-vermine2047 img#logo {
|
||||
}
|
||||
|
||||
.item-name {
|
||||
padding-left: 5px;
|
||||
padding: 0 2px 0 5px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
@@ -608,7 +661,11 @@ ol.chat-log {
|
||||
cursor: pointer;
|
||||
&:hover { transform: translateY(0.5rem); }
|
||||
}
|
||||
&.success { border-bottom-color: @color-acid-green; }
|
||||
&.success {
|
||||
border-bottom-color: @color-acid-green;
|
||||
border-bottom-width: 6px;
|
||||
box-shadow: 0 0 8px fade(@color-acid-green, 55%);
|
||||
}
|
||||
&.adapted::after { content: "adapté"; }
|
||||
&.human::after { content: "humain"; }
|
||||
&.rerolled { transform: translateY(0rem); }
|
||||
@@ -671,7 +728,297 @@ ol.chat-log {
|
||||
}
|
||||
|
||||
#required {
|
||||
color: @color-amber;
|
||||
color: @color-text-light-2;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Verdict ───────────────────────────────────────────────────────
|
||||
.roll-verdict {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin: 6px 0;
|
||||
padding: 6px 12px;
|
||||
border-radius: 3px;
|
||||
font-family: "DistressBlack";
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-16;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
&.success {
|
||||
color: @color-acid-green;
|
||||
background: fade(@color-acid-green, 15%);
|
||||
border: 1px solid @color-acid-green;
|
||||
}
|
||||
|
||||
&.failure {
|
||||
color: @color-hemolymph;
|
||||
background: fade(@color-hemolymph, 15%);
|
||||
border: 1px solid @color-hemolymph;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Contexte du jet ───────────────────────────────────────────────
|
||||
div.roll-context {
|
||||
gap: 6px 12px;
|
||||
margin: 4px 0 6px;
|
||||
padding: 6px 8px;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
|
||||
.context-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
.context-label {
|
||||
font-family: "DistressBlack";
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-11;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.context-value {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: @font-size-12;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.totem-human .context-value { color: @totem-human-color; }
|
||||
&.totem-adapted .context-value { color: @totem-adapted-color; }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Arme du jet ───────────────────────────────────────────────────
|
||||
div.weapon-block {
|
||||
margin: 4px 0 6px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid @color-hemolymph;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
|
||||
.weapon-block-header {
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 2px;
|
||||
|
||||
.weapon-icon {
|
||||
color: @color-hemolymph;
|
||||
}
|
||||
|
||||
.weapon-name {
|
||||
font-family: "DistressBlack";
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-12;
|
||||
font-weight: 900;
|
||||
color: @theme-color-dark;
|
||||
}
|
||||
|
||||
.weapon-targets {
|
||||
margin-left: auto;
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: @font-size-11;
|
||||
color: @theme-color-accent;
|
||||
}
|
||||
}
|
||||
|
||||
.weapon-details {
|
||||
gap: 4px 12px;
|
||||
|
||||
.weapon-detail {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: @font-size-11;
|
||||
|
||||
strong {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Échange de coups ─────────────────────────────────────────────
|
||||
div.exchange-actions {
|
||||
margin: 4px 0 6px;
|
||||
padding: 6px 8px;
|
||||
border: 1px dashed @color-hemolymph;
|
||||
background: fade(@color-hemolymph, 8%);
|
||||
|
||||
.exchange-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
font-family: "DistressBlack";
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-12;
|
||||
font-weight: 900;
|
||||
color: @theme-color-dark;
|
||||
|
||||
i { color: @color-hemolymph; }
|
||||
}
|
||||
|
||||
.exchange-target {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 3px 0;
|
||||
|
||||
.exchange-target-name {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: @font-size-11;
|
||||
color: @theme-color-accent;
|
||||
}
|
||||
|
||||
button.exchange-defend {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: @font-size-11;
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
color: @color-chitin-dark;
|
||||
background: @color-text-light-0;
|
||||
border: 1px solid @color-border-dark-4;
|
||||
border-radius: 3px;
|
||||
.transition();
|
||||
|
||||
&:hover {
|
||||
color: @color-text-light-0;
|
||||
background: @color-hemolymph;
|
||||
border-color: @color-hemolymph;
|
||||
}
|
||||
}
|
||||
|
||||
.exchange-passive {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: @font-size-11;
|
||||
color: @color-text-light-2;
|
||||
opacity: 0.85;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
button.exchange-resolve {
|
||||
margin-top: 4px;
|
||||
font-family: "DistressBlack";
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-11;
|
||||
padding: 3px 12px;
|
||||
cursor: pointer;
|
||||
color: @theme-color-dark;
|
||||
background: fade(@color-hemolymph, 25%);
|
||||
border: 1px solid @color-hemolymph;
|
||||
border-radius: 3px;
|
||||
.transition();
|
||||
|
||||
&:hover {
|
||||
color: @theme-color-dark;
|
||||
background: @color-hemolymph;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.exchange-resolution {
|
||||
margin: 4px 0 6px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid @color-hemolymph;
|
||||
background: fade(@color-hemolymph, 8%);
|
||||
|
||||
.resolution-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
font-family: "DistressBlack";
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-12;
|
||||
font-weight: 900;
|
||||
color: @theme-color-dark;
|
||||
|
||||
i { color: @color-hemolymph; }
|
||||
}
|
||||
|
||||
.resolution-line {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: @font-size-11;
|
||||
padding: 1px 0;
|
||||
|
||||
strong { font-weight: 700; }
|
||||
|
||||
.res-hit { color: @color-hemolymph; font-weight: 700; }
|
||||
.res-dodged,
|
||||
.res-parried { color: @color-acid-green; font-weight: 700; }
|
||||
&.res-wound { color: @color-hemolymph; font-weight: 700; }
|
||||
&.res-noshock,
|
||||
&.res-nodamage { color: @color-acid-green; }
|
||||
}
|
||||
|
||||
button.exchange-apply {
|
||||
margin-top: 6px;
|
||||
font-family: "DistressBlack";
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-11;
|
||||
padding: 3px 12px;
|
||||
cursor: pointer;
|
||||
color: @theme-color-dark;
|
||||
background: fade(@color-hemolymph, 25%);
|
||||
border: 1px solid @color-hemolymph;
|
||||
border-radius: 3px;
|
||||
.transition();
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: @theme-color-dark;
|
||||
background: @color-hemolymph;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: default;
|
||||
color: @color-acid-green;
|
||||
border-color: @color-acid-green;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Composition du jet ────────────────────────────────────────────
|
||||
div.roll-pool {
|
||||
margin: 4px 0;
|
||||
|
||||
h4 {
|
||||
font-family: "DistressBlack";
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-11;
|
||||
margin: 4px 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.pool-breakdown {
|
||||
gap: 4px 10px;
|
||||
|
||||
.pool-item {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: @font-size-11;
|
||||
|
||||
&.pool-total {
|
||||
font-weight: 700;
|
||||
color: @theme-color-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Résumé des dés ────────────────────────────────────────────────
|
||||
div.roll-dice-summary {
|
||||
gap: 6px 12px;
|
||||
justify-content: center;
|
||||
margin: 4px 0 2px;
|
||||
padding: 4px 8px;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
|
||||
.die-summary {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: @font-size-11;
|
||||
|
||||
&.human { color: @totem-human-color; }
|
||||
&.adapted { color: @totem-adapted-color; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -681,11 +1028,122 @@ ol.chat-log {
|
||||
}
|
||||
}
|
||||
|
||||
// Padding supplémentaire pour les fiches groupe, npc et créature
|
||||
// Padding supplémentaire pour les fiches groupe, npc, créature et communauté
|
||||
.system-vermine2047 {
|
||||
.group-content,
|
||||
.npc-content,
|
||||
.creature-content {
|
||||
.creature-content,
|
||||
.community-content {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Onglet Expérience ─────────────────────────────────────────────────
|
||||
.tab.experience {
|
||||
.experience-summary {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.exp-stat {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 8px;
|
||||
border: 1px solid @color-border-dark-4;
|
||||
background: fade(@color-chitin-dark, 30%);
|
||||
|
||||
label {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-11;
|
||||
color: @color-text-light-2;
|
||||
}
|
||||
|
||||
.exp-value {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
font-size: @font-size-24;
|
||||
color: @color-acid-green;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.exp-actions {
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
button {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-12;
|
||||
padding: 5px 14px;
|
||||
cursor: pointer;
|
||||
color: @theme-color-dark;
|
||||
background: fade(@color-hemolymph, 20%);
|
||||
border: 1px solid @color-hemolymph;
|
||||
border-radius: 3px;
|
||||
.transition();
|
||||
|
||||
&:hover {
|
||||
color: @theme-color-dark;
|
||||
background: @color-hemolymph;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.exp-rewards {
|
||||
margin-bottom: 10px;
|
||||
padding: 8px;
|
||||
border: 1px solid @color-border-dark-4;
|
||||
background: fade(@color-chitin-dark, 20%);
|
||||
|
||||
h4 {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-12;
|
||||
color: @theme-color-dark;
|
||||
margin: 0 0 6px;
|
||||
|
||||
.exp-total {
|
||||
color: @color-text-light-2;
|
||||
}
|
||||
}
|
||||
|
||||
.exp-reward-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
font-size: @font-size-12;
|
||||
|
||||
label {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-11;
|
||||
color: @color-text-light-2;
|
||||
}
|
||||
}
|
||||
|
||||
.exp-learned {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
|
||||
span {
|
||||
padding: 2px 8px;
|
||||
font-size: @font-size-11;
|
||||
border: 1px solid @color-border-dark-4;
|
||||
border-radius: 3px;
|
||||
opacity: 0.6;
|
||||
|
||||
&.learned {
|
||||
opacity: 1;
|
||||
color: @color-acid-green;
|
||||
border-color: @color-acid-green;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ iframe {
|
||||
padding: 10px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
color: @color-text-light-0;
|
||||
color: @theme-color-dark;
|
||||
|
||||
.dice-pool {
|
||||
flex: 1;
|
||||
@@ -188,7 +188,7 @@ iframe {
|
||||
|
||||
> label {
|
||||
font-size: @font-size-13;
|
||||
color: @color-amber;
|
||||
color: @color-text-light-2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ iframe {
|
||||
.ability-score {
|
||||
gap: 4px;
|
||||
font-size: @font-size-12;
|
||||
color: @color-text-light-0;
|
||||
color: @color-text-dark-secondary;
|
||||
opacity: 0.85;
|
||||
|
||||
#abilityScoreValue {
|
||||
@@ -273,7 +273,7 @@ iframe {
|
||||
|
||||
label {
|
||||
font-size: @font-size-11;
|
||||
color: @color-amber;
|
||||
color: @color-chitin-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -290,11 +290,11 @@ iframe {
|
||||
font-size: @font-size-12;
|
||||
font-family: "Roboto", sans-serif;
|
||||
text-transform: none;
|
||||
color: @color-text-light-0;
|
||||
color: @color-text-dark-primary;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: @color-amber;
|
||||
color: @color-acid-green;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +317,7 @@ iframe {
|
||||
|
||||
> label.label {
|
||||
font-size: @font-size-11;
|
||||
color: @color-text-light-0;
|
||||
color: @color-text-dark-secondary;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -331,7 +331,7 @@ iframe {
|
||||
font-size: @font-size-12;
|
||||
font-family: "Roboto", sans-serif;
|
||||
text-transform: none;
|
||||
color: @color-text-light-0;
|
||||
color: @color-text-dark-primary;
|
||||
}
|
||||
|
||||
.item-list {
|
||||
@@ -343,11 +343,11 @@ iframe {
|
||||
font-size: @font-size-11;
|
||||
font-family: "Roboto", sans-serif;
|
||||
text-transform: none;
|
||||
color: @color-text-light-0;
|
||||
color: @color-text-dark-primary;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: @color-amber;
|
||||
color: @color-acid-green;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,14 +374,14 @@ iframe {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: @color-text-light-0;
|
||||
color: @color-text-dark-primary;
|
||||
font-size: @font-size-12;
|
||||
font-family: "Roboto", sans-serif;
|
||||
text-transform: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: @color-amber;
|
||||
color: @color-acid-green;
|
||||
}
|
||||
|
||||
small {
|
||||
@@ -392,13 +392,261 @@ iframe {
|
||||
.totem-hint {
|
||||
width: 100%;
|
||||
font-size: @font-size-11;
|
||||
color: @color-text-light-0;
|
||||
opacity: 0.6;
|
||||
color: @color-text-dark-secondary;
|
||||
opacity: 0.8;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Combat dialog (attaque) ──────────────────────────────────────
|
||||
.combat-mode-label {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: @font-size-14;
|
||||
color: @color-hemolymph;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.locked-value {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
padding: 0 8px;
|
||||
font-size: @font-size-12;
|
||||
color: @color-chitin-dark;
|
||||
background: @color-text-light-0;
|
||||
border-radius: 3px;
|
||||
.custom-select-style();
|
||||
}
|
||||
|
||||
.combat-dialog-weapon {
|
||||
border: 1px solid @color-hemolymph;
|
||||
padding: 6px 8px;
|
||||
background: fade(@color-chitin-dark, 40%);
|
||||
|
||||
.weapon-block-header {
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.weapon-icon { color: @color-hemolymph; }
|
||||
|
||||
.weapon-name {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-12;
|
||||
font-weight: 900;
|
||||
color: @theme-color-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.weapon-details {
|
||||
gap: 4px 12px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.weapon-detail {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: @font-size-11;
|
||||
color: @color-text-dark-primary;
|
||||
|
||||
strong { text-transform: uppercase; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.combat-difficulty-options {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
.difficulty-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-family: "Roboto", sans-serif;
|
||||
text-transform: none;
|
||||
font-size: @font-size-12;
|
||||
color: @color-text-dark-primary;
|
||||
cursor: pointer;
|
||||
|
||||
input[type="radio"] { flex: none; }
|
||||
|
||||
&.locked {
|
||||
opacity: 0.8;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.combat-targets {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 4px 2px;
|
||||
|
||||
.label {
|
||||
font-size: @font-size-11;
|
||||
color: @color-text-light-2;
|
||||
}
|
||||
|
||||
.combat-target-tag {
|
||||
padding: 2px 8px;
|
||||
font-size: @font-size-11;
|
||||
font-family: "Roboto", sans-serif;
|
||||
color: @color-text-dark-header;
|
||||
background: fade(@color-hemolymph, 20%);
|
||||
border: 1px solid fade(@color-hemolymph, 50%);
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase d'Expérience ───────────────────────────────────────────
|
||||
.exp-phase-characters {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
|
||||
details > div:not(summary) {
|
||||
padding: 8px;
|
||||
background: fade(@color-chitin-dark, 30%);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dialogue d'apprentissage ─────────────────────────────────────
|
||||
.learning-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid @color-hemolymph;
|
||||
background: fade(@color-hemolymph, 10%);
|
||||
|
||||
.label {
|
||||
font-size: @font-size-12;
|
||||
color: @color-text-light-2;
|
||||
}
|
||||
|
||||
.exp-value {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
font-size: @font-size-20;
|
||||
color: @color-acid-green;
|
||||
}
|
||||
}
|
||||
|
||||
.learning-type {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 6px 2px;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-family: "Roboto", sans-serif;
|
||||
text-transform: none;
|
||||
font-size: @font-size-12;
|
||||
color: @color-text-dark-primary;
|
||||
cursor: pointer;
|
||||
|
||||
input[type="radio"] { flex: none; }
|
||||
|
||||
&:hover { color: @color-acid-green; }
|
||||
}
|
||||
}
|
||||
|
||||
.learning-targets {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
border: 1px solid @color-border-dark-4;
|
||||
background: fade(@color-chitin-dark, 30%);
|
||||
|
||||
.learning-hint {
|
||||
margin: 0;
|
||||
font-size: @font-size-11;
|
||||
font-style: italic;
|
||||
color: @color-text-dark-secondary;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.learning-result {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
padding: 8px;
|
||||
border: 1px solid @color-hemolymph;
|
||||
background: fade(@color-hemolymph, 8%);
|
||||
|
||||
.context-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.context-label {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-11;
|
||||
color: @color-text-light-2;
|
||||
}
|
||||
|
||||
.context-value {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: @font-size-13;
|
||||
font-weight: 700;
|
||||
color: @color-text-dark-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Transfert de Dés d'Expérience ─────────────────────────────────
|
||||
.experience-section {
|
||||
.experience-controls {
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
|
||||
.experience-group {
|
||||
font-size: @font-size-11;
|
||||
color: @color-text-dark-secondary;
|
||||
opacity: 0.8;
|
||||
|
||||
#exp-group {
|
||||
font-weight: 700;
|
||||
color: @color-acid-green;
|
||||
}
|
||||
}
|
||||
|
||||
button[data-action="giveToGroup"] {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-11;
|
||||
padding: 3px 12px;
|
||||
cursor: pointer;
|
||||
color: @theme-color-dark;
|
||||
background: fade(@color-acid-green, 15%);
|
||||
border: 1px solid @color-acid-green;
|
||||
border-radius: 3px;
|
||||
.transition();
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: @theme-color-dark;
|
||||
background: @color-acid-green;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Total dice pool section ─────────────────────────────────────
|
||||
.total-section {
|
||||
align-items: center;
|
||||
@@ -473,3 +721,92 @@ iframe {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Achat d'Adaptation/Mutation ───────────────────────────────────────
|
||||
.application.buy-evolution .bonus-item {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
> label.label {
|
||||
flex: none;
|
||||
font-size: @font-size-12;
|
||||
color: @color-text-light-2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
select {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
flex: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Gestion des Dés de Totems ─────────────────────────────────────────
|
||||
.application.totem-dice .totem-dice-state {
|
||||
gap: 12px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid @color-hemolymph;
|
||||
background: fade(@color-hemolymph, 8%);
|
||||
|
||||
.totem-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.label {
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-12;
|
||||
color: @color-text-light-2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.totem-human .label { color: @color-text-light-2; }
|
||||
&.totem-adapted .label { color: @color-text-light-2; }
|
||||
|
||||
.value {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: @font-size-14;
|
||||
font-weight: 700;
|
||||
color: @color-text-dark-header;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.application.totem-dice .totem-influence {
|
||||
font-size: @font-size-16;
|
||||
}
|
||||
|
||||
.application.totem-dice .totem-actions {
|
||||
gap: 8px;
|
||||
|
||||
button {
|
||||
flex: 1 1 100%;
|
||||
font-family: "DistressBlack", sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: @font-size-12;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
color: @theme-color-dark;
|
||||
background: fade(@color-hemolymph, 15%);
|
||||
border: 1px solid @color-hemolymph;
|
||||
border-radius: 3px;
|
||||
.transition();
|
||||
|
||||
&:hover {
|
||||
color: @theme-color-dark;
|
||||
background: @color-hemolymph;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,11 +66,10 @@
|
||||
|
||||
select {
|
||||
.custom-select-style();
|
||||
color: @color-text-light-1;
|
||||
color: @color-text-dark-primary;
|
||||
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;
|
||||
@@ -98,3 +97,14 @@ ol#chat-log div.item-card header img,
|
||||
ol.chat-log div.item-card header img {
|
||||
max-width: 30%;
|
||||
}
|
||||
|
||||
// Icônes des cartes d'item envoyées au chat (ApplicationV2).
|
||||
// Le template des cartes (templates/item/chatCards/parts/base.hbs) rend
|
||||
// une <img> pleine taille : on la plafonne à 64×64.
|
||||
ol#chat-log .chat-card-img,
|
||||
ol.chat-log .chat-card-img {
|
||||
max-width: 64px;
|
||||
max-height: 64px;
|
||||
object-fit: contain;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
// Niveaux de gris pour le texte clair
|
||||
@color-text-light-0: #fff;
|
||||
@color-text-light-1: #e0f0f0;
|
||||
@color-text-light-2: #c9e0c0;
|
||||
@color-text-light-2: #3f9b55;
|
||||
@color-text-light-3: #90c4a4;
|
||||
@color-text-light-4: #80c08b;
|
||||
@color-text-light-5: #60b06b;
|
||||
@@ -138,8 +138,7 @@
|
||||
@theme-color-highlight: #005a3c;
|
||||
|
||||
// Couleurs insecte / organique
|
||||
@color-acid-green: #6ab04c;
|
||||
@color-amber: #e8b84b;
|
||||
@color-acid-green: #1e7d32;
|
||||
@color-chitin-dark: #2a2520;
|
||||
@color-hemolymph: #8b4513;
|
||||
@color-membrane: rgba(106, 176, 76, 0.1);
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
@import "actor/npc";
|
||||
@import "actor/group";
|
||||
@import "actor/creature";
|
||||
@import "actor/community";
|
||||
@import "actor/vehicle";
|
||||
|
||||
// 6. Styles des items
|
||||
@import "items";
|
||||
|
||||
@@ -4,12 +4,14 @@ export { default as VermineCharacterSheetV2 } from "./character-sheet.mjs"
|
||||
export { default as VermineNpcSheetV2 } from "./npc-sheet.mjs"
|
||||
export { default as VermineGroupSheetV2 } from "./group-sheet.mjs"
|
||||
export { default as VermineCreatureSheetV2 } from "./creature-sheet.mjs"
|
||||
export { default as VermineCommunitySheetV2 } from "./community-sheet.mjs"
|
||||
export { default as VermineVehicleSheetV2 } from "./vehicle-sheet.mjs"
|
||||
export {
|
||||
VermineItemSheetV2,
|
||||
VermineWeaponSheetV2,
|
||||
VermineDefenseSheetV2,
|
||||
VermineVehicleSheetV2,
|
||||
VermineAbilitySheetV2,
|
||||
VermineCreatureCapacitySheetV2,
|
||||
VermineSpecialtySheetV2,
|
||||
VermineBackgroundSheetV2,
|
||||
VermineTraumaSheetV2,
|
||||
|
||||
@@ -30,9 +30,15 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
|
||||
toggleSheet: VermineBaseActorSheet.#onToggleSheet,
|
||||
edit: VermineBaseActorSheet.#onItemEdit,
|
||||
delete: VermineBaseActorSheet.#onItemDelete,
|
||||
toggleEquip: VermineBaseActorSheet.#onToggleEquip,
|
||||
create: VermineBaseActorSheet.#onItemCreate,
|
||||
createVehicle: VermineBaseActorSheet.#onCreateVehicle,
|
||||
editVehicle: VermineBaseActorSheet.#onEditVehicle,
|
||||
deleteVehicle: VermineBaseActorSheet.#onDeleteVehicle,
|
||||
roll: VermineBaseActorSheet.#onRollItem,
|
||||
attack: VermineBaseActorSheet.#onAttack,
|
||||
clickRadio: VermineBaseActorSheet.#onClickRadioHexa,
|
||||
clickWound: VermineBaseActorSheet.#onClickWound,
|
||||
effectControl: VermineBaseActorSheet.#onEffectControl,
|
||||
chooseTotem: VermineBaseActorSheet.#onChooseTotem
|
||||
}
|
||||
@@ -227,17 +233,54 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
|
||||
await this.document.createEmbeddedDocuments("Item", [{ name, type }])
|
||||
}
|
||||
|
||||
// ── Véhicules (acteurs) ──────────────────────────────────────────────
|
||||
|
||||
/** Crée un acteur "vehicle" rattaché à cet acteur (groupe ou personnage). */
|
||||
static async #onCreateVehicle(event, target) {
|
||||
const name = game.i18n.localize("ITEMS.new_vehicle")
|
||||
const vehicle = await Actor.create({
|
||||
name,
|
||||
type: "vehicle",
|
||||
system: { ownerId: this.document.id },
|
||||
// Le véhicule est un bien partagé : reprendre la propriété du propriétaire
|
||||
ownership: foundry.utils.deepClone(this.document.ownership)
|
||||
})
|
||||
vehicle?.sheet.render(true)
|
||||
}
|
||||
|
||||
static async #onEditVehicle(event, target) {
|
||||
const id = target.closest("[data-vehicle-id]")?.dataset?.vehicleId
|
||||
const vehicle = game.actors.get(id)
|
||||
vehicle?.sheet.render(true)
|
||||
}
|
||||
|
||||
static async #onDeleteVehicle(event, target) {
|
||||
const id = target.closest("[data-vehicle-id]")?.dataset?.vehicleId
|
||||
const vehicle = game.actors.get(id)
|
||||
await vehicle?.deleteDialog()
|
||||
}
|
||||
|
||||
static async #onRollItem(event, target) {
|
||||
const id = target.closest("[data-item-id]")?.dataset?.itemId
|
||||
if (!id) return
|
||||
const item = this.document.items.get(id)
|
||||
if (!item) return
|
||||
// Armes : PNJ et créatures → dialogue de combat (valeur d'Attaque) ;
|
||||
// personnage → dialogue de compétence. PNJ/créatures n'ont ni
|
||||
// caractéristiques ni compétences.
|
||||
if (item.type === "weapon" && (this.document.type === 'npc' || this.document.type === 'creature')) {
|
||||
const { default: CombatDialog } = await import("../../system/dialogs/combatDialog.mjs")
|
||||
const dialog = await CombatDialog.create({ actorId: this.document.id, weapon: item })
|
||||
if (dialog) dialog.render(true)
|
||||
return
|
||||
}
|
||||
if (item.type === "weapon" && this.document.system.abilities && this.document.system.skills) {
|
||||
const { default: RollDialog } = await import("../../system/dialogs/rollDialog.mjs")
|
||||
const dialog = await RollDialog.create({
|
||||
actorId: this.document.id,
|
||||
rolltype: "skill",
|
||||
label: item.system.skill
|
||||
label: item.system.skill,
|
||||
weapon: item
|
||||
})
|
||||
if (dialog) dialog.render(true)
|
||||
return
|
||||
@@ -245,9 +288,34 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
|
||||
item?.roll()
|
||||
}
|
||||
|
||||
static async #onAttack(event, target) {
|
||||
const id = target.closest("[data-item-id]")?.dataset?.itemId
|
||||
if (!id) return
|
||||
const item = this.document.items.get(id)
|
||||
if (!item) return
|
||||
const { default: CombatDialog } = await import("../../system/dialogs/combatDialog.mjs")
|
||||
const dialog = await CombatDialog.create({ actorId: this.document.id, weapon: item })
|
||||
if (dialog) dialog.render(true)
|
||||
}
|
||||
|
||||
static async #onToggleEquip(event, target) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const id = target.closest("[data-item-id]")?.dataset?.itemId
|
||||
if (!id) return
|
||||
const item = this.document.items.get(id)
|
||||
if (!item) return
|
||||
await item.update({ "system.equipped": !item.system.equipped })
|
||||
}
|
||||
|
||||
static #onClickRadioHexa(event, target) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
// Évite une erreur de permission pour un utilisateur non éditeur.
|
||||
if (!this.isEditable) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_not_editable"))
|
||||
return
|
||||
}
|
||||
const input = target
|
||||
const update = {}
|
||||
let current = this.document
|
||||
@@ -263,6 +331,36 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
|
||||
this.document.update(update)
|
||||
}
|
||||
|
||||
/**
|
||||
* Coche/décoche les cases de Blessure en respectant l'ordre de gravité
|
||||
* (règles p.39) : on ne peut cocher que la case suivante dans l'ordre
|
||||
* (une blessure s'applique toujours au Seuil égal ou inférieur le plus
|
||||
* élevé), et on ne peut pas en sauter. Décocher une case retire toutes
|
||||
* celles situées après elle.
|
||||
*/
|
||||
static #onClickWound(event, target) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const input = target
|
||||
const box = parseInt(input.value, 10) || 0 // numéro de case (1-based)
|
||||
let current = this.document
|
||||
const propTree = input.name.split(".")
|
||||
for (const prop of propTree) {
|
||||
current = current[prop]
|
||||
}
|
||||
const currentValue = parseInt(current, 10) || 0
|
||||
let next = currentValue
|
||||
if (box === currentValue + 1) {
|
||||
// Cocher la case suivante dans l'ordre.
|
||||
next = box
|
||||
} else if (box <= currentValue) {
|
||||
// Décocher : retire la case cliquée et toutes celles après.
|
||||
next = box - 1
|
||||
}
|
||||
// Si box > currentValue + 1 : on ne permet pas de sauter des cases.
|
||||
this.document.update({ [input.name]: next })
|
||||
}
|
||||
|
||||
static #onEffectControl(event, target) {
|
||||
onManageActiveEffect(event, this.document)
|
||||
}
|
||||
|
||||
@@ -4,21 +4,27 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["character"],
|
||||
position: { width: 860, height: 720 },
|
||||
position: { width: 920, height: 720 },
|
||||
window: { contentClasses: ["character-content"] },
|
||||
actions: {
|
||||
addSpecialty: VermineCharacterSheetV2.#onAddSpecialty
|
||||
addSpecialty: VermineCharacterSheetV2.#onAddSpecialty,
|
||||
openExperiencePhase: VermineCharacterSheetV2.#onOpenExperiencePhase,
|
||||
openLearning: VermineCharacterSheetV2.#onOpenLearning,
|
||||
validatePhase: VermineCharacterSheetV2.#onValidatePhase,
|
||||
buyEvolution: VermineCharacterSheetV2.#onBuyEvolution,
|
||||
openTotemDice: VermineCharacterSheetV2.#onOpenTotemDice
|
||||
}
|
||||
}
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/vermine2047/templates/actor/appv2/character-main.hbs" },
|
||||
main: { template: "systems/vermine2047/templates/actor/appv2/character-main.hbs", scrollable: [""] },
|
||||
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||
abilities: { template: "systems/vermine2047/templates/actor/appv2/character-abilities.hbs" },
|
||||
totem: { template: "systems/vermine2047/templates/actor/appv2/character-totem.hbs" },
|
||||
equipment: { template: "systems/vermine2047/templates/actor/appv2/character-equipment.hbs" },
|
||||
stories: { template: "systems/vermine2047/templates/actor/appv2/character-stories.hbs" },
|
||||
combat: { template: "systems/vermine2047/templates/actor/appv2/character-combat.hbs" }
|
||||
abilities: { template: "systems/vermine2047/templates/actor/appv2/character-abilities.hbs", scrollable: [""] },
|
||||
totem: { template: "systems/vermine2047/templates/actor/appv2/character-totem.hbs", scrollable: [""] },
|
||||
equipment: { template: "systems/vermine2047/templates/actor/appv2/character-equipment.hbs", scrollable: [""] },
|
||||
stories: { template: "systems/vermine2047/templates/actor/appv2/character-stories.hbs", scrollable: [""] },
|
||||
combat: { template: "systems/vermine2047/templates/actor/appv2/character-combat.hbs", scrollable: [""] },
|
||||
experience: { template: "systems/vermine2047/templates/actor/appv2/character-experience.hbs", scrollable: [""] }
|
||||
}
|
||||
|
||||
tabGroups = { sheet: "abilities" }
|
||||
@@ -29,7 +35,8 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
||||
totem: { id: "totem", group: "sheet", icon: "fas fa-star", label: "VERMINE.tabs.totem" },
|
||||
equipment: { id: "equipment", group: "sheet", icon: "fas fa-hammer", label: "VERMINE.tabs.equipment" },
|
||||
stories: { id: "stories", group: "sheet", icon: "fas fa-book-open-reader", label: "VERMINE.tabs.stories" },
|
||||
combat: { id: "combat", group: "sheet", icon: "fas fa-medal", label: "VERMINE.tabs.combat" }
|
||||
combat: { id: "combat", group: "sheet", icon: "fas fa-medal", label: "VERMINE.tabs.combat" },
|
||||
experience: { id: "experience", group: "sheet", icon: "fas fa-graduation-cap", label: "VERMINE.tabs.experience" }
|
||||
}
|
||||
for (const v of Object.values(tabs)) {
|
||||
v.active = this.tabGroups[v.group] === v.id
|
||||
@@ -51,6 +58,7 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
||||
case "main": break
|
||||
case "abilities":
|
||||
context.tab = context.tabs.abilities
|
||||
context.specialties = doc.itemTypes.specialty
|
||||
break
|
||||
case "totem":
|
||||
context.tab = context.tabs.totem
|
||||
@@ -60,22 +68,35 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
||||
context.backgrounds = doc.itemTypes.background
|
||||
context.traumas = doc.itemTypes.trauma
|
||||
context.evolutions = doc.itemTypes.evolution
|
||||
context.rites = doc.itemTypes.rite
|
||||
break
|
||||
case "equipment":
|
||||
context.tab = context.tabs.equipment
|
||||
context.gear = doc.itemTypes.item
|
||||
context.weapons = doc.itemTypes.weapon
|
||||
context.defenses = doc.itemTypes.defense
|
||||
context.vehicles = doc.itemTypes.vehicle
|
||||
context.vehicles = game.actors.filter(a => a.type === "vehicle" && a.system.ownerId === doc.id)
|
||||
break
|
||||
case "stories":
|
||||
context.tab = context.tabs.stories
|
||||
break
|
||||
case "combat":
|
||||
context.tab = context.tabs.combat
|
||||
context.equippedWeapons = doc.itemTypes.weapon.filter(i => i.system.equipped)
|
||||
context.equippedDefenses = doc.itemTypes.defense.filter(i => i.system.equipped)
|
||||
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
|
||||
context.effects = prepareActiveEffectCategories(doc.effects)
|
||||
break
|
||||
case "experience":
|
||||
context.tab = context.tabs.experience
|
||||
context.specialtyCount = doc.itemTypes.specialty.length
|
||||
const ex = doc.system.experience
|
||||
context.experience = ex
|
||||
context.collectiveTotal = (ex.collective.danger + ex.collective.discoveries + ex.collective.duration)
|
||||
context.individualTotal = (ex.individual.implication + ex.individual.influence + ex.individual.interpretation)
|
||||
context.currentPhase = ex.phase
|
||||
context.learnedCurrent = ex.learned.phase === ex.phase
|
||||
break
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -95,4 +116,37 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
||||
if (skillKey) itemData.system = { skill: skillKey }
|
||||
await this.document.createEmbeddedDocuments("Item", [itemData])
|
||||
}
|
||||
|
||||
static async #onOpenExperiencePhase(event, target) {
|
||||
const { default: ExperiencePhaseDialog } = await import("../../system/dialogs/experiencePhaseDialog.mjs")
|
||||
const dialog = await ExperiencePhaseDialog.create()
|
||||
if (dialog) dialog.render(true)
|
||||
}
|
||||
|
||||
static async #onOpenLearning(event, target) {
|
||||
const { default: LearningDialog } = await import("../../system/dialogs/learningDialog.mjs")
|
||||
const dialog = await LearningDialog.create({ actorId: this.document.id })
|
||||
if (dialog) dialog.render(true)
|
||||
}
|
||||
|
||||
/** Valide la phase d'Expérience courante et réinitialise les limites. */
|
||||
static async #onValidatePhase() {
|
||||
const { VermineExperience } = await import("../../system/experience.mjs")
|
||||
await VermineExperience.resetPhaseLimits(this.document)
|
||||
ui.notifications.info(game.i18n.localize("VERMINE.validate_phase_done"))
|
||||
}
|
||||
|
||||
/** Ouvre le dialogue d'achat d'une Adaptation/Mutation (Dés d'Évolution). */
|
||||
static async #onBuyEvolution(event, target) {
|
||||
const { default: BuyEvolutionDialog } = await import("../../system/dialogs/buyEvolutionDialog.mjs")
|
||||
const dialog = await BuyEvolutionDialog.create({ actorId: this.document.id })
|
||||
if (dialog) dialog.render(true)
|
||||
}
|
||||
|
||||
/** Ouvre le dialogue de gestion des Dés de Totems. */
|
||||
static async #onOpenTotemDice(event, target) {
|
||||
const { default: TotemDiceDialog } = await import("../../system/dialogs/totemDiceDialog.mjs")
|
||||
const dialog = await TotemDiceDialog.create({ actorId: this.document.id })
|
||||
if (dialog) dialog.render(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import VermineBaseActorSheet from "./base-actor-sheet.mjs"
|
||||
|
||||
export default class VermineCommunitySheetV2 extends VermineBaseActorSheet {
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["community"],
|
||||
position: { width: 760, height: 640 },
|
||||
window: { contentClasses: ["community-content"] },
|
||||
actions: {
|
||||
addObjective: VermineCommunitySheetV2.#onAddObjective,
|
||||
deleteObjective: VermineCommunitySheetV2.#onDeleteObjective
|
||||
}
|
||||
}
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/vermine2047/templates/actor/appv2/community-main.hbs", scrollable: [""] },
|
||||
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||
identity: { template: "systems/vermine2047/templates/actor/appv2/community-identity.hbs", scrollable: [""] },
|
||||
domains: { template: "systems/vermine2047/templates/actor/appv2/community-domains.hbs", scrollable: [""] },
|
||||
resources: { template: "systems/vermine2047/templates/actor/appv2/community-resources.hbs", scrollable: [""] }
|
||||
}
|
||||
|
||||
tabGroups = { sheet: "identity" }
|
||||
|
||||
#getTabs() {
|
||||
const tabs = {
|
||||
identity: { id: "identity", group: "sheet", icon: "fas fa-info-circle", label: "COMMUNITY.tabs.identity" },
|
||||
domains: { id: "domains", group: "sheet", icon: "fas fa-chart-simple", label: "COMMUNITY.tabs.domains" },
|
||||
resources: { id: "resources", group: "sheet", icon: "fas fa-boxes-stacked", label: "COMMUNITY.tabs.resources" }
|
||||
}
|
||||
for (const v of Object.values(tabs)) {
|
||||
v.active = this.tabGroups[v.group] === v.id
|
||||
v.cssClass = v.active ? "active" : ""
|
||||
}
|
||||
return tabs
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
// Groupe de personnages rattaché
|
||||
context.resolvedGroup = null
|
||||
if (this.document.system.groupId) {
|
||||
const g = game.actors.get(this.document.system.groupId)
|
||||
if (g) context.resolvedGroup = { id: g.id, name: g.name }
|
||||
}
|
||||
// Enrichissement HTML des champs longs (même logique que la fiche PNJ)
|
||||
const enrich = async (path) => {
|
||||
const val = foundry.utils.getProperty(this.document.system, path)
|
||||
return val ? await foundry.applications.ux.TextEditor.implementation.enrichHTML(val, { async: true }) : ""
|
||||
}
|
||||
context.enrichedNotes = await enrich("identity.notes")
|
||||
context.enrichedLandmarks = await enrich("location.landmarks")
|
||||
context.enrichedTerritory = await enrich("location.territory")
|
||||
context.enrichedEvents = await enrich("history.events")
|
||||
context.enrichedRelations = await enrich("history.relations")
|
||||
return context
|
||||
}
|
||||
|
||||
async _preparePartContext(partId, context) {
|
||||
const doc = this.document
|
||||
switch (partId) {
|
||||
case "main": break
|
||||
case "identity":
|
||||
context.tab = context.tabs.identity
|
||||
context.ageOptions = Object.entries(CONFIG.VERMINE.communityAges).map(([key, label]) => ({ key, label: game.i18n.localize(label) }))
|
||||
break
|
||||
case "domains":
|
||||
context.tab = context.tabs.domains
|
||||
context.sizeOptions = CONFIG.VERMINE.communitySizeLevels
|
||||
context.sizeLabel = doc.system.size?.value ? game.i18n.localize(CONFIG.VERMINE.communitySizeLevels[doc.system.size.value]?.label ?? "") : ""
|
||||
context.sizeInfo = doc.system.size?.value ? CONFIG.VERMINE.communitySizeLevels[doc.system.size.value] : null
|
||||
// Préparer les 4 Domaines avec leurs 2 sections (jauges) et leur état
|
||||
context.domains = []
|
||||
for (const [dkey, dcfg] of Object.entries(CONFIG.VERMINE.communityDomains)) {
|
||||
const domain = doc.system.domains?.[dkey]
|
||||
context.domains.push({
|
||||
key: dkey,
|
||||
label: game.i18n.localize(dcfg.label),
|
||||
sections: Object.entries(dcfg.sections).map(([skey, label]) => {
|
||||
const gauge = domain?.[skey] ?? { value: 0 }
|
||||
const state = gauge.state || "crisis"
|
||||
return {
|
||||
domainKey: dkey,
|
||||
key: skey,
|
||||
label: game.i18n.localize(label),
|
||||
gauge,
|
||||
state,
|
||||
stateLabel: game.i18n.localize(CONFIG.VERMINE.communityGaugeStates[state]?.label ?? "COMMUNITY.states.crisis")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
break
|
||||
case "resources":
|
||||
context.tab = context.tabs.resources
|
||||
break
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
changeTab(tab, group, options = {}) {
|
||||
super.changeTab(tab, group, options)
|
||||
if (group === "sheet") {
|
||||
const main = this.element?.querySelector('[data-group="sheet"][data-tab="main"]')
|
||||
if (main) main.classList.add("active")
|
||||
}
|
||||
}
|
||||
|
||||
static #onDeleteObjective(event, target) {
|
||||
const index = parseInt(target.dataset.index)
|
||||
if (isNaN(index)) return
|
||||
const objectives = foundry.utils.duplicate(this.document.system.objectives || { major: "", minor: [] })
|
||||
objectives.minor.splice(index, 1)
|
||||
this.document.update({ "system.objectives": objectives })
|
||||
}
|
||||
|
||||
static #onAddObjective(event, target) {
|
||||
const objectives = foundry.utils.duplicate(this.document.system.objectives || { major: "", minor: [] })
|
||||
objectives.minor.push("")
|
||||
this.document.update({ "system.objectives": objectives })
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,10 @@ import VermineBaseActorSheet from "./base-actor-sheet.mjs"
|
||||
|
||||
export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
|
||||
|
||||
// La fiche créature s'ouvre en mode édition : Gabarit, Taille, Rôle et Meute
|
||||
// se règlent dès l'ouverture (le mode jeu reste accessible via le toggle).
|
||||
_sheetMode = this.constructor.SHEET_MODES.EDIT
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["creature"],
|
||||
position: { width: 700, height: 650 },
|
||||
@@ -9,12 +13,12 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
|
||||
}
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/vermine2047/templates/actor/appv2/creature-main.hbs" },
|
||||
main: { template: "systems/vermine2047/templates/actor/appv2/creature-main.hbs", scrollable: [""] },
|
||||
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||
info: { template: "systems/vermine2047/templates/actor/appv2/creature-info.hbs" },
|
||||
stats: { template: "systems/vermine2047/templates/actor/appv2/creature-stats.hbs" },
|
||||
combat: { template: "systems/vermine2047/templates/actor/appv2/creature-combat.hbs" },
|
||||
effects: { template: "systems/vermine2047/templates/actor/appv2/creature-effects.hbs" }
|
||||
info: { template: "systems/vermine2047/templates/actor/appv2/creature-info.hbs", scrollable: [""] },
|
||||
stats: { template: "systems/vermine2047/templates/actor/appv2/creature-stats.hbs", scrollable: [""] },
|
||||
combat: { template: "systems/vermine2047/templates/actor/appv2/creature-combat.hbs", scrollable: [""] },
|
||||
effects: { template: "systems/vermine2047/templates/actor/appv2/creature-effects.hbs", scrollable: [""] }
|
||||
}
|
||||
|
||||
tabGroups = { sheet: "info" }
|
||||
@@ -55,19 +59,43 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
|
||||
context.roleOptions = CONFIG.VERMINE.creatureRoleLevels
|
||||
context.sizeOptions = CONFIG.VERMINE.creatureSizeLevels
|
||||
context.packOptions = CONFIG.VERMINE.creaturePackLevels
|
||||
context.sizeLabel = doc.system.size?.value !== undefined
|
||||
? game.i18n.localize(CONFIG.VERMINE.creatureSizeLevels[doc.system.size.value]?.label ?? "")
|
||||
: ""
|
||||
context.packLabel = doc.system.pack?.value
|
||||
? game.i18n.localize(CONFIG.VERMINE.creaturePackLevels[doc.system.pack.value]?.label ?? "")
|
||||
: ""
|
||||
break
|
||||
case "info":
|
||||
context.tab = context.tabs.info
|
||||
context.capacities = doc.itemTypes.creaturecapacity
|
||||
break
|
||||
case "stats":
|
||||
context.tab = context.tabs.stats
|
||||
context.patternLabel = doc.system.pattern?.value ? CONFIG.VERMINE.creaturePatternLevels[doc.system.pattern.value]?.label : ""
|
||||
context.sizeLabel = doc.system.size?.value || ""
|
||||
context.roleLabel = doc.system.role?.value ? CONFIG.VERMINE.creatureRoleLevels[doc.system.role.value]?.label : ""
|
||||
context.packLabel = doc.system.pack?.value || game.i18n.localize("VERMINE.none")
|
||||
context.patternLabel = doc.system.pattern?.value ? game.i18n.localize(CONFIG.VERMINE.creaturePatternLevels[doc.system.pattern.value]?.label ?? "") : ""
|
||||
context.sizeLabel = doc.system.size?.value !== undefined ? game.i18n.localize(CONFIG.VERMINE.creatureSizeLevels[doc.system.size.value]?.label ?? "") : ""
|
||||
context.roleLabel = doc.system.role?.value ? game.i18n.localize(CONFIG.VERMINE.creatureRoleLevels[doc.system.role.value]?.label ?? "") : ""
|
||||
context.packLabel = doc.system.pack?.value
|
||||
? `${doc.system.pack.value} (${game.i18n.localize(CONFIG.VERMINE.creaturePackLevels[doc.system.pack.value]?.label ?? "")})`
|
||||
: game.i18n.localize("VERMINE.none")
|
||||
// Contribution cumulée de la Taille (les modificateurs se cumulent : Taille N = somme 1..N)
|
||||
const sizeLevels = CONFIG.VERMINE.creatureSizeLevels || {}
|
||||
const sizeLevel = doc.system.size?.value ?? 0
|
||||
context.sizeCumulative = { attack: 0, vigor: 0, minorWound: 0, majorWound: 0, deadlyWound: 0 }
|
||||
for (let i = 1; i <= sizeLevel; i++) {
|
||||
const cfg = sizeLevels[i] || {}
|
||||
context.sizeCumulative.attack += cfg.attack || 0
|
||||
context.sizeCumulative.vigor += cfg.vigor || 0
|
||||
context.sizeCumulative.minorWound += cfg.minorWound || 0
|
||||
context.sizeCumulative.majorWound += cfg.majorWound || 0
|
||||
context.sizeCumulative.deadlyWound += cfg.deadlyWound || 0
|
||||
}
|
||||
break
|
||||
case "combat":
|
||||
context.tab = context.tabs.combat
|
||||
context.weapons = doc.itemTypes.weapon
|
||||
context.equippedWeapons = doc.itemTypes.weapon.filter(i => i.system.equipped)
|
||||
context.equippedDefenses = doc.itemTypes.defense.filter(i => i.system.equipped)
|
||||
break
|
||||
case "effects":
|
||||
context.tab = context.tabs.effects
|
||||
@@ -77,4 +105,21 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
/** @override - Les créatures n'ont pas de caractéristiques/compétences : on ouvre le dialogue d'attaque (pool calculé) ou de Réaction. */
|
||||
async _onRoll(event) {
|
||||
event.preventDefault()
|
||||
const el = event.currentTarget
|
||||
const type = el.dataset.type
|
||||
if (type === "creature-reaction") {
|
||||
const { default: NpcRollDialog } = await import("../../system/dialogs/npcRollDialog.mjs")
|
||||
const dialog = await NpcRollDialog.create({ actorId: this.document.id, rolltype: "reaction" })
|
||||
if (dialog) dialog.render(true)
|
||||
return
|
||||
}
|
||||
if (type !== "creature-attack") return super._onRoll(event)
|
||||
const { default: CombatDialog } = await import("../../system/dialogs/combatDialog.mjs")
|
||||
const dialog = await CombatDialog.create({ actorId: this.document.id })
|
||||
if (dialog) dialog.render(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,21 +7,26 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
||||
position: { width: 700, height: 600 },
|
||||
window: { contentClasses: ["group-content"] },
|
||||
actions: {
|
||||
chooseTotem: VermineGroupSheetV2.#onChooseTotem,
|
||||
chooseActor: VermineGroupSheetV2.#onChooseActor,
|
||||
deleteMember: VermineGroupSheetV2.#onDeleteMember,
|
||||
deleteEncounter: VermineGroupSheetV2.#onDeleteEncounter,
|
||||
deleteObjective: VermineGroupSheetV2.#onDeleteObjective,
|
||||
addObjective: VermineGroupSheetV2.#onAddObjective
|
||||
addObjective: VermineGroupSheetV2.#onAddObjective,
|
||||
createTotemAbility: VermineGroupSheetV2.#onCreateTotemAbility,
|
||||
openLearning: VermineGroupSheetV2.#onOpenLearning,
|
||||
openExperiencePhase: VermineGroupSheetV2.#onOpenExperiencePhase,
|
||||
validatePhase: VermineGroupSheetV2.#onValidatePhase
|
||||
}
|
||||
}
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/vermine2047/templates/actor/appv2/group-main.hbs" },
|
||||
main: { template: "systems/vermine2047/templates/actor/appv2/group-main.hbs", scrollable: [""] },
|
||||
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||
info: { template: "systems/vermine2047/templates/actor/appv2/group-info.hbs" },
|
||||
gear: { template: "systems/vermine2047/templates/actor/appv2/group-gear.hbs" },
|
||||
road: { template: "systems/vermine2047/templates/actor/appv2/group-road.hbs" },
|
||||
reserve: { template: "systems/vermine2047/templates/actor/appv2/group-reserve.hbs" }
|
||||
info: { template: "systems/vermine2047/templates/actor/appv2/group-info.hbs", scrollable: [""] },
|
||||
gear: { template: "systems/vermine2047/templates/actor/appv2/group-gear.hbs", scrollable: [""] },
|
||||
road: { template: "systems/vermine2047/templates/actor/appv2/group-road.hbs", scrollable: [""] },
|
||||
reserve: { template: "systems/vermine2047/templates/actor/appv2/group-reserve.hbs", scrollable: [""] }
|
||||
}
|
||||
|
||||
tabGroups = { sheet: "info" }
|
||||
@@ -55,9 +60,22 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
||||
if (this.document.system.encounters?.length > 0) {
|
||||
for (const encId of this.document.system.encounters) {
|
||||
const a = game.actors.get(encId)
|
||||
if (a) context.resolvedEncounters[encId] = { name: a.name, id: a.id }
|
||||
if (a) {
|
||||
context.resolvedEncounters[encId] = {
|
||||
name: a.name,
|
||||
id: a.id,
|
||||
type: a.type,
|
||||
profile: a.system?.identity?.profile || "",
|
||||
totem: a.system?.identity?.totem || ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Notes de voyage (riche texte)
|
||||
const roadNotes = this.document.system?.roadNotes
|
||||
context.enrichedRoadNotes = roadNotes
|
||||
? await foundry.applications.ux.TextEditor.implementation.enrichHTML(roadNotes, { async: true })
|
||||
: ""
|
||||
return context
|
||||
}
|
||||
|
||||
@@ -67,21 +85,18 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
||||
case "main": break
|
||||
case "info":
|
||||
context.tab = context.tabs.info
|
||||
context.abilities = doc.itemTypes.ability
|
||||
context.specialties = doc.itemTypes.specialty
|
||||
context.backgrounds = doc.itemTypes.background
|
||||
context.traumas = doc.itemTypes.trauma
|
||||
context.evolutions = doc.itemTypes.evolution
|
||||
context.abilities = doc.itemTypes.ability.filter(i => i.system.type !== "totem")
|
||||
context.totem_abilities = doc.itemTypes.ability.filter(i => i.system.type === "totem")
|
||||
break
|
||||
case "gear":
|
||||
context.tab = context.tabs.gear
|
||||
context.gear = doc.itemTypes.item
|
||||
context.weapons = doc.itemTypes.weapon
|
||||
context.defenses = doc.itemTypes.defense
|
||||
context.vehicles = this.#getOwnedVehicles()
|
||||
break
|
||||
case "road":
|
||||
context.tab = context.tabs.road
|
||||
context.vehicles = doc.itemTypes.vehicle
|
||||
break
|
||||
case "reserve":
|
||||
context.tab = context.tabs.reserve
|
||||
@@ -90,10 +105,60 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
||||
return context
|
||||
}
|
||||
|
||||
/** Véhicules (acteurs) appartenant à ce groupe via system.ownerId. */
|
||||
#getOwnedVehicles() {
|
||||
return game.actors.filter(a => a.type === "vehicle" && a.system.ownerId === this.document.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dépôt d'un acteur Véhicule sur la fiche : lie le véhicule au Groupe
|
||||
* (system.ownerId) au lieu de le dupliquer, puis affiche la liste.
|
||||
* Gère les dépôts depuis un compendium (copie importée dans le monde)
|
||||
* et demande confirmation si le véhicule appartient déjà à un autre Groupe.
|
||||
* @override
|
||||
*/
|
||||
async _onDropActor(event, data) {
|
||||
let actor = data instanceof foundry.abstract.Document ? data : await fromUuid(data.uuid)
|
||||
if (actor?.type !== "vehicle") {
|
||||
return super._onDropActor?.(event, data)
|
||||
}
|
||||
// Véhicule issu d'un compendium : importer une copie dans le monde.
|
||||
if (actor.compendium) {
|
||||
try {
|
||||
const copy = await game.actors.importFromCompendium(actor.compendium, actor._id, { renderSheet: false })
|
||||
if (!copy) return
|
||||
actor = copy
|
||||
} catch {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.vehicle_import_failed"))
|
||||
return
|
||||
}
|
||||
}
|
||||
// Véhicule déjà rattaché à un autre Groupe : confirmation avant transfert.
|
||||
const currentOwner = actor.system?.ownerId
|
||||
if (currentOwner && currentOwner !== this.document.id) {
|
||||
const other = game.actors.get(currentOwner)
|
||||
const content = game.i18n.localize("VERMINE.vehicle_assign_confirm").replace("{group}", other?.name ?? currentOwner)
|
||||
const confirmed = await Dialog.confirm({
|
||||
title: game.i18n.localize("VERMINE.vehicle_assign"),
|
||||
content,
|
||||
defaultYes: false
|
||||
})
|
||||
if (!confirmed) return
|
||||
}
|
||||
try {
|
||||
await actor.update({ "system.ownerId": this.document.id })
|
||||
} catch {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.vehicle_assign_failed"))
|
||||
return
|
||||
}
|
||||
this.render()
|
||||
}
|
||||
|
||||
// Actions : délégation aux applications AppV1 existantes pour TotemPicker/ActorPicker
|
||||
static async #onChooseTotem(event, target) {
|
||||
const { TotemPicker } = await import("../../system/applications.mjs")
|
||||
new TotemPicker(target, this.document).render(true)
|
||||
// Le Totem du Groupe remplit automatiquement Instincts/Interdits.
|
||||
new TotemPicker(target, this.document, { fillInstincts: true }).render(true)
|
||||
}
|
||||
static async #onChooseActor(event, target) {
|
||||
const { ActorPicker } = await import("../../system/applications.mjs")
|
||||
@@ -135,4 +200,28 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
||||
objectives[type].push("")
|
||||
this.document.update({ "system.objectives": objectives })
|
||||
}
|
||||
|
||||
static async #onCreateTotemAbility(event, target) {
|
||||
const name = game.i18n.localize("ITEMS.new_ability")
|
||||
await this.document.createEmbeddedDocuments("Item", [{ name, type: "ability", system: { type: "totem" } }])
|
||||
}
|
||||
|
||||
static async #onOpenExperiencePhase(event, target) {
|
||||
const { default: ExperiencePhaseDialog } = await import("../../system/dialogs/experiencePhaseDialog.mjs")
|
||||
const dialog = await ExperiencePhaseDialog.create()
|
||||
if (dialog) dialog.render(true)
|
||||
}
|
||||
|
||||
static async #onOpenLearning(event, target) {
|
||||
const { default: GroupLearningDialog } = await import("../../system/dialogs/groupLearningDialog.mjs")
|
||||
const dialog = await GroupLearningDialog.create({ actorId: this.document.id })
|
||||
if (dialog) dialog.render(true)
|
||||
}
|
||||
|
||||
/** Valide la phase d'Expérience courante du Groupe et réinitialise les limites. */
|
||||
static async #onValidatePhase() {
|
||||
const { VermineExperience } = await import("../../system/experience.mjs")
|
||||
await VermineExperience.resetPhaseLimits(this.document)
|
||||
ui.notifications.info(game.i18n.localize("VERMINE.validate_phase_done"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,71 +3,71 @@ import { VermineBaseItemSheet } from "./base-item-sheet.mjs"
|
||||
// ── Item générique ────────────────────────────────────────────────────
|
||||
export class VermineItemSheetV2 extends VermineBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = { classes: ["item-gear"], position: { width: 520 } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-item-sheet.hbs" } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-item-sheet.hbs", scrollable: [""] } }
|
||||
}
|
||||
|
||||
// ── Arme ──────────────────────────────────────────────────────────────
|
||||
export class VermineWeaponSheetV2 extends VermineBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = { classes: ["weapon"], position: { width: 520 } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-weapon-sheet.hbs" } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-weapon-sheet.hbs", scrollable: [""] } }
|
||||
}
|
||||
|
||||
// ── Défense ───────────────────────────────────────────────────────────
|
||||
export class VermineDefenseSheetV2 extends VermineBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = { classes: ["defense"], position: { width: 520 } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-defense-sheet.hbs" } }
|
||||
}
|
||||
|
||||
// ── Véhicule ──────────────────────────────────────────────────────────
|
||||
export class VermineVehicleSheetV2 extends VermineBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = { classes: ["vehicle"], position: { width: 520 } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-vehicle-sheet.hbs" } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-defense-sheet.hbs", scrollable: [""] } }
|
||||
}
|
||||
|
||||
// ── Capacité ──────────────────────────────────────────────────────────
|
||||
export class VermineAbilitySheetV2 extends VermineBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = { classes: ["ability"], position: { width: 560 } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-ability-sheet.hbs" } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-ability-sheet.hbs", scrollable: [""] } }
|
||||
}
|
||||
|
||||
// ── Capacité de créature ────────────────────────────────────────────────
|
||||
export class VermineCreatureCapacitySheetV2 extends VermineBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = { classes: ["creaturecapacity"], position: { width: 560 } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-creaturecapacity-sheet.hbs", scrollable: [""] } }
|
||||
}
|
||||
|
||||
// ── Spécialité ────────────────────────────────────────────────────────
|
||||
export class VermineSpecialtySheetV2 extends VermineBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = { classes: ["specialty"], position: { width: 400 } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-specialty-sheet.hbs" } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-specialty-sheet.hbs", scrollable: [""] } }
|
||||
}
|
||||
|
||||
// ── Historique ────────────────────────────────────────────────────────
|
||||
export class VermineBackgroundSheetV2 extends VermineBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = { classes: ["background"], position: { width: 520 } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-background-sheet.hbs" } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-background-sheet.hbs", scrollable: [""] } }
|
||||
}
|
||||
|
||||
// ── Traumatisme ───────────────────────────────────────────────────────
|
||||
export class VermineTraumaSheetV2 extends VermineBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = { classes: ["trauma"], position: { width: 520 } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-trauma-sheet.hbs" } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-trauma-sheet.hbs", scrollable: [""] } }
|
||||
}
|
||||
|
||||
// ── Évolution ─────────────────────────────────────────────────────────
|
||||
export class VermineEvolutionSheetV2 extends VermineBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = { classes: ["evolution"], position: { width: 520 } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-evolution-sheet.hbs" } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-evolution-sheet.hbs", scrollable: [""] } }
|
||||
}
|
||||
|
||||
// ── Rumeur ────────────────────────────────────────────────────────────
|
||||
export class VermineRumorSheetV2 extends VermineBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = { classes: ["rumor"], position: { width: 520 } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rumor-sheet.hbs" } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rumor-sheet.hbs", scrollable: [""] } }
|
||||
}
|
||||
|
||||
// ── Cible ─────────────────────────────────────────────────────────────
|
||||
export class VermineTargetSheetV2 extends VermineBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = { classes: ["target"], position: { width: 520 } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-target-sheet.hbs" } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-target-sheet.hbs", scrollable: [""] } }
|
||||
}
|
||||
|
||||
// ── Rite ──────────────────────────────────────────────────────────────
|
||||
export class VermineRiteSheetV2 extends VermineBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = { classes: ["rite"], position: { width: 520 } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rite-sheet.hbs" } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rite-sheet.hbs", scrollable: [""] } }
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@ import VermineBaseActorSheet from "./base-actor-sheet.mjs"
|
||||
|
||||
export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
|
||||
|
||||
// La fiche PNJ s'ouvre en mode édition : profil et niveaux (Menace,
|
||||
// Expérience, Rôle) se règlent dès l'ouverture (le mode jeu reste accessible
|
||||
// via le toggle).
|
||||
_sheetMode = this.constructor.SHEET_MODES.EDIT
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["npc"],
|
||||
position: { width: 750, height: 680 },
|
||||
@@ -9,22 +14,20 @@ export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
|
||||
}
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/vermine2047/templates/actor/appv2/npc-main.hbs" },
|
||||
main: { template: "systems/vermine2047/templates/actor/appv2/npc-main.hbs", scrollable: [""] },
|
||||
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||
characteristics: { template: "systems/vermine2047/templates/actor/appv2/npc-characteristics.hbs" },
|
||||
skills: { template: "systems/vermine2047/templates/actor/appv2/npc-skills.hbs" },
|
||||
threat: { template: "systems/vermine2047/templates/actor/appv2/npc-threat.hbs" },
|
||||
combat: { template: "systems/vermine2047/templates/actor/appv2/npc-combat.hbs" },
|
||||
notes: { template: "systems/vermine2047/templates/actor/appv2/npc-notes.hbs" }
|
||||
traits: { template: "systems/vermine2047/templates/actor/appv2/npc-traits.hbs", scrollable: [""] },
|
||||
equipment: { template: "systems/vermine2047/templates/actor/appv2/npc-equipment.hbs", scrollable: [""] },
|
||||
combat: { template: "systems/vermine2047/templates/actor/appv2/npc-combat.hbs", scrollable: [""] },
|
||||
notes: { template: "systems/vermine2047/templates/actor/appv2/npc-notes.hbs", scrollable: [""] }
|
||||
}
|
||||
|
||||
tabGroups = { sheet: "characteristics" }
|
||||
tabGroups = { sheet: "traits" }
|
||||
|
||||
#getTabs() {
|
||||
const tabs = {
|
||||
characteristics: { id: "characteristics", group: "sheet", icon: "fas fa-dice", label: "VERMINE.abilities" },
|
||||
skills: { id: "skills", group: "sheet", icon: "fas fa-brain", label: "VERMINE.skills" },
|
||||
threat: { id: "threat", group: "sheet", icon: "fas fa-exclamation-triangle", label: "ADVERSITY.threat" },
|
||||
traits: { id: "traits", group: "sheet", icon: "fas fa-dice", label: "VERMINE.traits" },
|
||||
equipment: { id: "equipment", group: "sheet", icon: "fas fa-hammer", label: "VERMINE.tabs.equipment" },
|
||||
combat: { id: "combat", group: "sheet", icon: "fas fa-sword", label: "VERMINE.combat" },
|
||||
notes: { id: "notes", group: "sheet", icon: "fas fa-sticky-note", label: "IDENTITY.notes" }
|
||||
}
|
||||
@@ -51,17 +54,18 @@ export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
|
||||
context.totemOptions = CONFIG.VERMINE.totems
|
||||
context.originOptions = CONFIG.VERMINE.origins
|
||||
break
|
||||
case "characteristics":
|
||||
context.tab = context.tabs.characteristics
|
||||
case "traits":
|
||||
context.tab = context.tabs.traits
|
||||
break
|
||||
case "skills":
|
||||
context.tab = context.tabs.skills
|
||||
break
|
||||
case "threat":
|
||||
context.tab = context.tabs.threat
|
||||
case "equipment":
|
||||
context.tab = context.tabs.equipment
|
||||
context.weapons = doc.itemTypes.weapon
|
||||
context.defenses = doc.itemTypes.defense
|
||||
break
|
||||
case "combat":
|
||||
context.tab = context.tabs.combat
|
||||
context.equippedWeapons = doc.itemTypes.weapon.filter(i => i.system.equipped)
|
||||
context.equippedDefenses = doc.itemTypes.defense.filter(i => i.system.equipped)
|
||||
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
|
||||
context.effects = prepareActiveEffectCategories(doc.effects)
|
||||
break
|
||||
@@ -71,4 +75,27 @@ export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
/** @override - Le PNJ n'a ni caractéristiques ni compétences : les jets
|
||||
* rapides utilisent la valeur d'Attaque (dialogue de combat) ou les valeurs
|
||||
* dérivées Action / Spécialité / Réaction (dialogue PNJ simplifié). */
|
||||
async _onRoll(event) {
|
||||
event.preventDefault()
|
||||
const el = event.currentTarget
|
||||
const type = el.dataset.type
|
||||
if (!type) return
|
||||
if (type === "attack") {
|
||||
const { default: CombatDialog } = await import("../../system/dialogs/combatDialog.mjs")
|
||||
const dialog = await CombatDialog.create({ actorId: this.document.id })
|
||||
if (dialog) dialog.render(true)
|
||||
return
|
||||
}
|
||||
if (["action", "specialty", "reaction"].includes(type)) {
|
||||
const { default: NpcRollDialog } = await import("../../system/dialogs/npcRollDialog.mjs")
|
||||
const dialog = await NpcRollDialog.create({ actorId: this.document.id, rolltype: type })
|
||||
if (dialog) dialog.render(true)
|
||||
return
|
||||
}
|
||||
return super._onRoll(event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import VermineBaseActorSheet from "./base-actor-sheet.mjs"
|
||||
|
||||
export default class VermineVehicleSheetV2 extends VermineBaseActorSheet {
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["vehicle"],
|
||||
position: { width: 640, height: 580 },
|
||||
window: { contentClasses: ["vehicle-content"] }
|
||||
}
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/vermine2047/templates/actor/appv2/vehicle-main.hbs", scrollable: [""] },
|
||||
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||
specs: { template: "systems/vermine2047/templates/actor/appv2/vehicle-specs.hbs", scrollable: [""] }
|
||||
}
|
||||
|
||||
tabGroups = { sheet: "specs" }
|
||||
|
||||
#getTabs() {
|
||||
const tabs = {
|
||||
specs: { id: "specs", group: "sheet", icon: "fas fa-car-side", label: "VERMINE.vehicle_specs" }
|
||||
}
|
||||
for (const v of Object.values(tabs)) {
|
||||
v.active = this.tabGroups[v.group] === v.id
|
||||
v.cssClass = v.active ? "active" : ""
|
||||
}
|
||||
return tabs
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
// Compétences d'Entretien possibles (mécanique, technologie, artisanat, animalisme…)
|
||||
const maintenanceKeys = ["mecanical", "technology", "crafting", "animalism", "environment", "wildlife", "flora", "repulsion", "toxics"]
|
||||
const current = this.document.system.maintenance || []
|
||||
context.maintenanceSkills = maintenanceKeys.map(key => ({
|
||||
key,
|
||||
label: game.i18n.localize("SKILLS." + key + ".name"),
|
||||
checked: current.includes(key)
|
||||
}))
|
||||
// Propriétaire (groupe ou personnage) résolu
|
||||
context.resolvedOwner = null
|
||||
if (this.document.system.ownerId) {
|
||||
const owner = game.actors.get(this.document.system.ownerId)
|
||||
if (owner) context.resolvedOwner = { id: owner.id, name: owner.name }
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
async _preparePartContext(partId, context) {
|
||||
switch (partId) {
|
||||
case "main": break
|
||||
case "specs":
|
||||
context.tab = context.tabs.specs
|
||||
break
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
changeTab(tab, group, options = {}) {
|
||||
super.changeTab(tab, group, options)
|
||||
if (group === "sheet") {
|
||||
const main = this.element?.querySelector('[data-group="sheet"][data-tab="main"]')
|
||||
if (main) main.classList.add("active")
|
||||
}
|
||||
}
|
||||
|
||||
/** @override - nettoie le tableau d'Entretien (retire les cases non cochées). */
|
||||
_prepareSubmitData(event, form, formData, updateData) {
|
||||
const result = super._prepareSubmitData(event, form, formData, updateData)
|
||||
if (Array.isArray(result?.system?.maintenance)) {
|
||||
result.system.maintenance = result.system.maintenance.filter(v => typeof v === "string" && v.trim() !== "")
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,13 @@ export { default as VermineCharacterData } from "./character.mjs"
|
||||
export { default as VermineNpcData } from "./npc.mjs"
|
||||
export { default as VermineGroupData } from "./group.mjs"
|
||||
export { default as VermineCreatureData } from "./creature.mjs"
|
||||
export { default as VermineCommunityData } from "./community.mjs"
|
||||
export { default as VermineItemData } from "./item.mjs"
|
||||
export { default as VermineWeaponData } from "./weapon.mjs"
|
||||
export { default as VermineDefenseData } from "./defense.mjs"
|
||||
export { default as VermineVehicleData } from "./vehicle.mjs"
|
||||
export { default as VermineAbilityData } from "./ability.mjs"
|
||||
export { default as VermineCreatureCapacityData } from "./creaturecapacity.mjs"
|
||||
export { default as VermineSpecialtyData } from "./specialty.mjs"
|
||||
export { default as VermineBackgroundData } from "./background.mjs"
|
||||
export { default as VermineTraumaData } from "./trauma.mjs"
|
||||
|
||||
@@ -219,7 +219,7 @@ const reqInt = { required: true, nullable: false, integer: true }
|
||||
export function raritySchema() {
|
||||
const fields = foundry.data.fields
|
||||
return new fields.SchemaField({
|
||||
value: new fields.NumberField({ ...reqInt, initial: 3, min: 1, max: 5 }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 3, min: 1, max: 10 }),
|
||||
handicap: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export default class VermineAbilityData extends foundry.abstract.TypeDataModel {
|
||||
totem: new fields.StringField({ required: true, initial: "" }),
|
||||
learn: learnSchema(),
|
||||
level: levelSchema(1, 1, 3),
|
||||
learned: new fields.BooleanField({ required: true, initial: false }),
|
||||
effects: new fields.ArrayField(new fields.StringField({ required: true, initial: "" }), {
|
||||
initial: []
|
||||
})
|
||||
|
||||
@@ -14,7 +14,9 @@ export default class VermineBackgroundData extends foundry.abstract.TypeDataMode
|
||||
const reqInt = { required: true, nullable: false, integer: true }
|
||||
return {
|
||||
...listItemSchema(),
|
||||
cost: new fields.NumberField({ ...reqInt, initial: 1, min: 0 })
|
||||
cost: new fields.NumberField({ ...reqInt, initial: 1, min: 0 }),
|
||||
asterisk: new fields.BooleanField({ required: true, initial: false }),
|
||||
learned: new fields.BooleanField({ required: true, initial: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
|
||||
height: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
|
||||
weight: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
|
||||
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
// Statut de Chaman : débloque l'affichage des Rites chamaniques maîtrisés.
|
||||
chaman: new fields.BooleanField({ required: true, initial: false }),
|
||||
age: new fields.StringField({ required: true, nullable: false, initial: "15" }),
|
||||
ageType: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 2 }),
|
||||
profile: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
@@ -72,8 +74,43 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
|
||||
attributes: new fields.SchemaField({
|
||||
xp: attributeSchema(0, 0, 10),
|
||||
reputation: attributeSchema(0, 0, 10),
|
||||
self_control: attributeSchema(0, 0, 5),
|
||||
effort: attributeSchema(0, 0, 5)
|
||||
self_control: attributeSchema(0, 0, 10),
|
||||
effort: attributeSchema(0, 0, 10)
|
||||
}),
|
||||
|
||||
// Expérience (dés, récompenses, évolution, apprentissage)
|
||||
experience: new fields.SchemaField({
|
||||
dice: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||
phase: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||
collective: new fields.SchemaField({
|
||||
danger: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
|
||||
discoveries: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
|
||||
duration: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 })
|
||||
}),
|
||||
individual: new fields.SchemaField({
|
||||
implication: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
|
||||
influence: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
|
||||
interpretation: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 })
|
||||
}),
|
||||
evolution: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||
traumaReward: new fields.BooleanField({ required: true, initial: false }),
|
||||
// Bonus de Réserve acquis par apprentissage (s'ajoute au max dérivé)
|
||||
reserveBonus: new fields.SchemaField({
|
||||
self_control: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||
effort: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 })
|
||||
}),
|
||||
// Limites d'apprentissage par phase
|
||||
learned: new fields.SchemaField({
|
||||
phase: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||
skill: new fields.BooleanField({ required: true, initial: false }),
|
||||
specialty: new fields.BooleanField({ required: true, initial: false }),
|
||||
reserve: new fields.BooleanField({ required: true, initial: false }),
|
||||
characteristic: new fields.BooleanField({ required: true, initial: false }),
|
||||
evolution: new fields.BooleanField({ required: true, initial: false }),
|
||||
ability: new fields.BooleanField({ required: true, initial: false }),
|
||||
background: new fields.BooleanField({ required: true, initial: false }),
|
||||
reputation: new fields.BooleanField({ required: true, initial: false })
|
||||
})
|
||||
}),
|
||||
|
||||
// Rencontres
|
||||
@@ -127,7 +164,8 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
|
||||
const sum = abilities
|
||||
.filter(a => a.category === "mental" || a.category === "social")
|
||||
.reduce((acc, a) => acc + a.value, 0)
|
||||
this.attributes.self_control.max = sum + modFromAge
|
||||
const bonus = this.experience?.reserveBonus?.self_control || 0
|
||||
this.attributes.self_control.max = Math.min(10, sum + modFromAge + bonus)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,7 +178,8 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
|
||||
const sum = abilities
|
||||
.filter(a => a.category === "physical" || a.category === "manual")
|
||||
.reduce((acc, a) => acc + a.value, 0)
|
||||
this.attributes.effort.max = sum + modFromAge
|
||||
const bonus = this.experience?.reserveBonus?.effort || 0
|
||||
this.attributes.effort.max = Math.min(10, sum + modFromAge + bonus)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* DataModel pour les acteurs de type "community" (Communauté).
|
||||
* La Communauté est l'extension sédentarisée d'un Groupe : elle englobe le
|
||||
* Groupe de personnages, des Figures et des Habitants répartis en Compagnies.
|
||||
* Le socle couvre l'identité, la géographie, l'histoire, les objectifs, la
|
||||
* taille/l'effectif, les quatre Domaines (8 jauges), le Moral et les réserves.
|
||||
*/
|
||||
import {
|
||||
attributeSchema,
|
||||
levelSchema,
|
||||
combatStatusSchema
|
||||
} from "./_shared.mjs"
|
||||
|
||||
export default class VermineCommunityData extends foundry.abstract.TypeDataModel {
|
||||
|
||||
/** @override */
|
||||
static LOCALIZATION_PREFIXES = ["VERMINE.community"]
|
||||
|
||||
/** @override */
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields
|
||||
const reqInt = { required: true, nullable: false, integer: true }
|
||||
const gauge = () => attributeSchema(0, 0, 10)
|
||||
|
||||
return {
|
||||
// Statut de combat (base, peu utilisé pour une Communauté)
|
||||
combatStatus: combatStatusSchema("9"),
|
||||
|
||||
// Identité
|
||||
identity: new fields.SchemaField({
|
||||
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
profile: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
origin: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
theme: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
instincts: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
prohibits: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
// Âge de la Communauté : recent | established | old
|
||||
age: new fields.StringField({ required: true, nullable: false, initial: "recent" }),
|
||||
notes: new fields.HTMLField({ required: true, initial: "", textSearch: true })
|
||||
}),
|
||||
|
||||
// Géographie / localisation
|
||||
location: new fields.SchemaField({
|
||||
sector: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
placeType: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
landmarks: new fields.HTMLField({ required: true, initial: "", textSearch: true }),
|
||||
territory: new fields.HTMLField({ required: true, initial: "", textSearch: true })
|
||||
}),
|
||||
|
||||
// Histoire
|
||||
history: new fields.SchemaField({
|
||||
founding: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
events: new fields.HTMLField({ required: true, initial: "", textSearch: true }),
|
||||
relations: new fields.HTMLField({ required: true, initial: "", textSearch: true })
|
||||
}),
|
||||
|
||||
// Objectifs : un majeur + des mineurs
|
||||
objectives: new fields.SchemaField({
|
||||
major: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
minor: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" }))
|
||||
}),
|
||||
|
||||
// Taille (1-3) et effectif
|
||||
size: attributeSchema(1, 1, 3),
|
||||
population: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
|
||||
// Domaines : 4 Domaines × 2 sections (jauges en Dés)
|
||||
domains: new fields.SchemaField({
|
||||
resources: new fields.SchemaField({
|
||||
vivre: gauge(),
|
||||
supplies: gauge()
|
||||
}),
|
||||
security: new fields.SchemaField({
|
||||
defense: gauge(),
|
||||
care: gauge()
|
||||
}),
|
||||
dynamism: new fields.SchemaField({
|
||||
cohesion: gauge(),
|
||||
productivity: gauge()
|
||||
}),
|
||||
relations: new fields.SchemaField({
|
||||
human: gauge(),
|
||||
ecology: gauge()
|
||||
})
|
||||
}),
|
||||
|
||||
// Moral (comme le Groupe) : value 0-7, level dérivé
|
||||
morale: new fields.SchemaField({
|
||||
level: new fields.StringField({ required: true, nullable: false, initial: "normal" }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 7, min: 0, max: 7 }),
|
||||
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
max: new fields.NumberField({ ...reqInt, initial: 7, min: 0 })
|
||||
}),
|
||||
|
||||
// Réserve de Groupe
|
||||
reserve: attributeSchema(0, 0, 10),
|
||||
|
||||
// Dés collectifs de la Communauté
|
||||
material: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
experience: new fields.SchemaField({
|
||||
dice: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
|
||||
}),
|
||||
|
||||
// Niveau de la Communauté (pour l'expérience des habitants)
|
||||
level: levelSchema(1, 1, 10),
|
||||
|
||||
// Groupe de personnages rattaché (id d'acteur)
|
||||
groupId: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
|
||||
// Habitants (phase 2) : Figures et Compagnies
|
||||
figures: new fields.ArrayField(new fields.SchemaField({
|
||||
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
role: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
specialty: new fields.SchemaField({
|
||||
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
score: new fields.NumberField({ ...reqInt, initial: 3, min: 0, max: 10 })
|
||||
})
|
||||
})),
|
||||
companies: new fields.ArrayField(new fields.SchemaField({
|
||||
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
specialty: new fields.SchemaField({
|
||||
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
score: new fields.NumberField({ ...reqInt, initial: 3, min: 0, max: 10 })
|
||||
}),
|
||||
level: levelSchema(1, 1, 4),
|
||||
members: new fields.NumberField({ ...reqInt, initial: 1, min: 0 }),
|
||||
figureName: new fields.StringField({ required: true, nullable: false, initial: "" })
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
prepareDerivedData() {
|
||||
super.prepareDerivedData()
|
||||
|
||||
// 1. État (Crise/Bas/Stable/Opulent) de chaque jauge de Domaine
|
||||
this._computeGaugeStates()
|
||||
|
||||
// 2. Les quatre Domaines sont-ils Bas ? (Moral inactif)
|
||||
this._computeAllDomainsLow()
|
||||
|
||||
// 3. Mettre à jour le niveau de Moral selon la valeur
|
||||
this._updateMorale()
|
||||
}
|
||||
|
||||
/**
|
||||
* État d'une jauge selon sa valeur en Dés.
|
||||
* Crise 0D / Bas 1-2D / Stable 3-5D / Opulent 6D+.
|
||||
* @param {number} value
|
||||
* @returns {string}
|
||||
*/
|
||||
static gaugeState(value) {
|
||||
const v = parseInt(value, 10) || 0
|
||||
if (v >= 6) return "opulent"
|
||||
if (v >= 3) return "stable"
|
||||
if (v >= 1) return "low"
|
||||
return "crisis"
|
||||
}
|
||||
|
||||
_computeGaugeStates() {
|
||||
for (const domain of Object.values(this.domains)) {
|
||||
for (const gauge of Object.values(domain)) {
|
||||
gauge.state = VermineCommunityData.gaugeState(gauge.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Un Domaine est "Bas" lorsque ses deux sections sont à 2D ou moins.
|
||||
* Si les quatre Domaines sont Bas, le Bonus de Moral est inactif.
|
||||
*/
|
||||
_computeAllDomainsLow() {
|
||||
const domainLow = (domain) => Object.values(domain).every(g => (parseInt(g.value, 10) || 0) <= 2)
|
||||
this.allDomainsLow = Object.values(this.domains).every(domainLow)
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le niveau de Moral en fonction de la valeur de dés.
|
||||
* Règles : 7D = Haut, 6-3D = Normal, 2-1D = Bas, 0D = Crise.
|
||||
*/
|
||||
_updateMorale() {
|
||||
const moraleValue = parseInt(this.morale?.value, 10) || 0
|
||||
if (this.morale.level && this.morale.level !== "normal") return
|
||||
if (moraleValue >= 7) {
|
||||
this.morale.level = "high"
|
||||
} else if (moraleValue >= 3) {
|
||||
this.morale.level = "normal"
|
||||
} else if (moraleValue >= 1) {
|
||||
this.morale.level = "low"
|
||||
} else {
|
||||
this.morale.level = "crisis"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
|
||||
|
||||
// Niveaux de créature (patron, taille, rôle, meute)
|
||||
pattern: attributeSchema(1, 1, 4),
|
||||
size: attributeSchema(1, 1, 3),
|
||||
size: attributeSchema(0, 0, 3),
|
||||
role: attributeSchema(1, 1, 4),
|
||||
pack: attributeSchema(0, 0, 3),
|
||||
|
||||
@@ -90,38 +90,50 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
|
||||
* .creatureRoleLevels, .creaturePackLevels.
|
||||
*
|
||||
* Règles :
|
||||
* - Attaque = pattern.attack + size.attack + pack.attack + role.reaction
|
||||
* - Attaque = pattern.attack + size.attack + pack.attack
|
||||
* - Dégâts = pattern.damage + size.vigor + pack.damage
|
||||
* - Vigueur = size.vigor + pack.damage
|
||||
* - Réaction = role.reaction + role.reaction_bonus
|
||||
*/
|
||||
_calculateCreatureComputedValues() {
|
||||
const patternLevel = this.pattern?.value || 1
|
||||
const sizeLevel = this.size?.value || 1
|
||||
const sizeLevel = this.size?.value || 0
|
||||
const roleLevel = this.role?.value || 1
|
||||
const packLevel = this.pack?.value || 0
|
||||
|
||||
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {}
|
||||
const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {}
|
||||
const roleConfig = CONFIG.VERMINE.creatureRoleLevels[roleLevel] || {}
|
||||
const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {}
|
||||
|
||||
// Attaque : patron + taille + meute + réaction du rôle
|
||||
// Les modificateurs de Taille se cumulent : une créature de Taille N applique
|
||||
// les modificateurs de chaque Taille de 1 à N (Taille 0 = insecte, aucun).
|
||||
const sizeLevels = CONFIG.VERMINE.creatureSizeLevels || {}
|
||||
let sizeAttack = 0
|
||||
let sizeVigor = 0
|
||||
for (let i = 1; i <= sizeLevel; i++) {
|
||||
const cfg = sizeLevels[i] || {}
|
||||
sizeAttack += cfg.attack || 0
|
||||
sizeVigor += cfg.vigor || 0
|
||||
}
|
||||
|
||||
// Attaque : patron + taille + meute.
|
||||
// NB : le Rôle ne donne que la Réaction (dés + réussites automatiques),
|
||||
// il n'ajoute pas de dés d'attaque (regles_creatures.txt).
|
||||
this.computed.attack = (patternConfig.attack || 0)
|
||||
+ (sizeConfig.attack || 0)
|
||||
+ sizeAttack
|
||||
+ (packConfig.attack || 0)
|
||||
+ (roleConfig.reaction || 0)
|
||||
|
||||
// Dégâts : patron + vigueur de taille + meute
|
||||
this.computed.damage = (patternConfig.damage || 0)
|
||||
+ (sizeConfig.vigor || 0)
|
||||
+ sizeVigor
|
||||
+ (packConfig.damage || 0)
|
||||
|
||||
// Vigueur : taille + meute
|
||||
this.computed.vigor = (sizeConfig.vigor || 0) + (packConfig.damage || 0)
|
||||
this.computed.vigor = sizeVigor + (packConfig.damage || 0)
|
||||
|
||||
// Réaction : rôle
|
||||
this.computed.reaction = (roleConfig.reaction || 0) + (roleConfig.reaction_bonus || 0)
|
||||
// Réaction : rôle (le bonus de rôle est compté séparément et ajouté
|
||||
// comme réussites automatiques par le NpcRollDialog — cf. modèle PNJ).
|
||||
this.computed.reaction = roleConfig.reaction || 0
|
||||
this.computed.reactionBonus = roleConfig.reaction_bonus || 0
|
||||
|
||||
// Réserves
|
||||
@@ -136,32 +148,45 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les seuils de blessures à partir du patron, de la taille et de la meute.
|
||||
* Les seuils sont la somme des valeurs correspondantes des trois sources.
|
||||
* Calcule les seuils et les cercles de blessures à partir du patron, de la
|
||||
* taille et de la meute.
|
||||
*
|
||||
* Règles (regles_creatures.txt) :
|
||||
* - Seuil = contribution du Gabarit + contributions de Taille (cumulées).
|
||||
* Le Groupe n'ajoute PAS de seuil (tableau « Groupe » : cercles seuls).
|
||||
* - Cercles = cercles du Gabarit + cercles de Taille (cumulés) + cercles du Groupe.
|
||||
*/
|
||||
_calculateCreatureWoundThresholds() {
|
||||
const patternLevel = this.pattern?.value || 1
|
||||
const sizeLevel = this.size?.value || 1
|
||||
const sizeLevel = this.size?.value || 0
|
||||
const packLevel = this.pack?.value || 0
|
||||
|
||||
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {}
|
||||
const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {}
|
||||
const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {}
|
||||
|
||||
this.minorWound.threshold = (patternConfig.minorWound || 0)
|
||||
+ (sizeConfig.minorWound || 0)
|
||||
+ (packConfig.minorWound || 0)
|
||||
this.majorWound.threshold = (patternConfig.majorWound || 0)
|
||||
+ (sizeConfig.majorWound || 0)
|
||||
+ (packConfig.majorWound || 0)
|
||||
this.deadlyWound.threshold = (patternConfig.deadlyWound || 0)
|
||||
+ (sizeConfig.deadlyWound || 0)
|
||||
+ (packConfig.deadlyWound || 0)
|
||||
// Les modificateurs de Taille se cumulent (Taille N = somme des tailles 1..N).
|
||||
const sizeLevels = CONFIG.VERMINE.creatureSizeLevels || {}
|
||||
const sizeThresholds = { minor: 0, major: 0, deadly: 0 }
|
||||
const sizeWounds = { minor: 0, major: 0, deadly: 0 }
|
||||
for (let i = 1; i <= sizeLevel; i++) {
|
||||
const cfg = sizeLevels[i] || {}
|
||||
sizeThresholds.minor += cfg.minorThreshold || 0
|
||||
sizeThresholds.major += cfg.majorThreshold || 0
|
||||
sizeThresholds.deadly += cfg.deadlyThreshold || 0
|
||||
sizeWounds.minor += cfg.minorWound || 0
|
||||
sizeWounds.major += cfg.majorWound || 0
|
||||
sizeWounds.deadly += cfg.deadlyWound || 0
|
||||
}
|
||||
|
||||
// Max de blessures
|
||||
this.minorWound.max = Math.min(5, this.minorWound.threshold + 2)
|
||||
this.majorWound.max = Math.min(4, this.majorWound.threshold + 1)
|
||||
this.deadlyWound.max = Math.min(2, this.deadlyWound.threshold)
|
||||
// Seuils : gabarit + taille cumulée (pas de contribution du groupe).
|
||||
this.minorWound.threshold = (patternConfig.minorThreshold ?? 0) + sizeThresholds.minor
|
||||
this.majorWound.threshold = (patternConfig.majorThreshold ?? 0) + sizeThresholds.major
|
||||
this.deadlyWound.threshold = (patternConfig.deadlyThreshold ?? 0) + sizeThresholds.deadly
|
||||
|
||||
// Cercles : gabarit + taille cumulée + groupe.
|
||||
this.minorWound.max = (patternConfig.minorWound ?? 0) + sizeWounds.minor + (packConfig.minorWound ?? 0)
|
||||
this.majorWound.max = (patternConfig.majorWound ?? 0) + sizeWounds.major + (packConfig.majorWound ?? 0)
|
||||
this.deadlyWound.max = (patternConfig.deadlyWound ?? 0) + sizeWounds.deadly + (packConfig.deadlyWound ?? 0)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { listItemSchema } from "./_shared.mjs"
|
||||
|
||||
/**
|
||||
* DataModel pour les items de type "creaturecapacity" (capacités de créature).
|
||||
* @augments {foundry.abstract.TypeDataModel}
|
||||
*/
|
||||
export default class VermineCreatureCapacityData extends foundry.abstract.TypeDataModel {
|
||||
/** @override */
|
||||
static LOCALIZATION_PREFIXES = ["VERMINE.item.creaturecapacity"]
|
||||
|
||||
/** @override */
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields
|
||||
return {
|
||||
...listItemSchema(),
|
||||
type: new fields.StringField({ required: true, initial: "base" })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export default class VermineDefenseData extends foundry.abstract.TypeDataModel {
|
||||
return {
|
||||
...baseItemSchema(),
|
||||
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
equipped: new fields.BooleanField({ required: true, initial: false }),
|
||||
specificLevel: new fields.SchemaField({
|
||||
label: new fields.StringField({ required: true, initial: "" }),
|
||||
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
|
||||
|
||||
@@ -13,7 +13,8 @@ export default class VermineEvolutionData extends foundry.abstract.TypeDataModel
|
||||
const fields = foundry.data.fields
|
||||
return {
|
||||
...listItemSchema(),
|
||||
level: levelSchema(1, 1, 4)
|
||||
level: levelSchema(1, 1, 4),
|
||||
mutation: new fields.BooleanField({ required: true, initial: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ export default class VermineGroupData extends foundry.abstract.TypeDataModel {
|
||||
// Équipement
|
||||
equipment: equipmentSchema(),
|
||||
|
||||
// Notes de voyage (lieux, dangers, routes…)
|
||||
roadNotes: new fields.HTMLField({ required: true, initial: "" }),
|
||||
|
||||
// Niveau du groupe (1-10)
|
||||
level: levelSchema(1, 1, 10),
|
||||
|
||||
@@ -75,6 +78,19 @@ export default class VermineGroupData extends foundry.abstract.TypeDataModel {
|
||||
// Membres (IDs d'acteurs)
|
||||
members: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
|
||||
|
||||
// Dés d'Expérience collectifs (réserve de Groupe)
|
||||
experience: new fields.SchemaField({
|
||||
dice: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||
phase: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||
// Limites d'apprentissage par phase
|
||||
learned: new fields.SchemaField({
|
||||
phase: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||
level: new fields.BooleanField({ required: true, initial: false }),
|
||||
reserve: new fields.BooleanField({ required: true, initial: false }),
|
||||
ability: new fields.BooleanField({ required: true, initial: false })
|
||||
})
|
||||
}),
|
||||
|
||||
// Rencontres
|
||||
encounters: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" }))
|
||||
}
|
||||
@@ -113,12 +129,14 @@ export default class VermineGroupData extends foundry.abstract.TypeDataModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule la réserve max en fonction du niveau du groupe.
|
||||
* Règle simplifiée : niveau × 2, plafonné à 10.
|
||||
* Calcule la réserve max selon le nombre de membres (personnages).
|
||||
* Règles p. 68-69 : la Réserve débute à 3D et gagne 1D par personnage,
|
||||
* avec un maximum de 10D.
|
||||
*/
|
||||
_calculateGroupReserve() {
|
||||
const level = this.level?.value || 1
|
||||
this.reserve.max = Math.min(10, level * 2)
|
||||
const members = Array.isArray(this.members) ? this.members : []
|
||||
const characterCount = members.filter((id) => game.actors?.get(id)?.type === "character").length
|
||||
this.reserve.max = Math.min(10, 3 + characterCount)
|
||||
|
||||
if (this.reserve.value > this.reserve.max) {
|
||||
this.reserve.value = this.reserve.max
|
||||
|
||||
@@ -2,17 +2,22 @@
|
||||
* DataModel pour les acteurs de type "npc" (PNJ).
|
||||
* Étend foundry.abstract.TypeDataModel.
|
||||
*
|
||||
* Note : le champ libre de compétences (texte descriptif) est nommé "freeSkills"
|
||||
* pour éviter le conflit avec le SchemaField "skills" qui contient les 30 compétences.
|
||||
* Conforme aux règles des PNJ (regles/regles_pnj.txt) : un PNJ n'a ni
|
||||
* caractéristiques chiffrées, ni réserves détaillées, ni liste de compétences.
|
||||
* Il est défini uniquement par son Profil (identité) et trois critères :
|
||||
* Menace, Expérience et Rôle. Toutes les valeurs utiles en jeu sont dérivées
|
||||
* des tableaux de référence (CONFIG.VERMINE.npc*Levels) et exposées dans
|
||||
* "system.computed".
|
||||
*
|
||||
* Note : le champ libre de description des Spécialités est nommé "freeSkills"
|
||||
* pour éviter le conflit avec le champ "skills" que le template.json historique
|
||||
* définissait comme texte libre pour les PNJ.
|
||||
*/
|
||||
import {
|
||||
woundSchema,
|
||||
combatStatusSchema,
|
||||
equipmentSchema,
|
||||
attributeSchema,
|
||||
abilitiesSchema,
|
||||
skillCategoriesSchema,
|
||||
skillsSchema
|
||||
attributeSchema
|
||||
} from "./_shared.mjs"
|
||||
|
||||
export default class VermineNpcData extends foundry.abstract.TypeDataModel {
|
||||
@@ -23,8 +28,7 @@ export default class VermineNpcData extends foundry.abstract.TypeDataModel {
|
||||
/**
|
||||
* Migration des données avant traitement par le schéma.
|
||||
* Avant DataModel, template.json définissait "skills" comme un champ texte libre
|
||||
* pour les PNJ. Le DataModel réserve "skills" pour les 30 compétences individuelles
|
||||
* (SchemaField) et utilise "freeSkills" pour le texte libre.
|
||||
* pour les PNJ. Le DataModel utilise "freeSkills" pour cette description.
|
||||
* @param {Object} source Données brutes avant validation du schéma
|
||||
* @returns {Object} Données migrées
|
||||
*/
|
||||
@@ -48,9 +52,10 @@ export default class VermineNpcData extends foundry.abstract.TypeDataModel {
|
||||
// Statut de combat (base, difficulté par défaut 9 pour PNJ)
|
||||
combatStatus: combatStatusSchema("9"),
|
||||
|
||||
// Identité
|
||||
// Identité (Profil du PNJ)
|
||||
identity: new fields.SchemaField({
|
||||
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
age: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
profile: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
origin: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
@@ -58,30 +63,29 @@ export default class VermineNpcData extends foundry.abstract.TypeDataModel {
|
||||
notes: new fields.HTMLField({ required: true, initial: "" })
|
||||
}),
|
||||
|
||||
// Attributs (XP, réputation, sang-froid, effort)
|
||||
attributes: new fields.SchemaField({
|
||||
xp: attributeSchema(0, 0, 10),
|
||||
reputation: attributeSchema(0, 0, 10),
|
||||
self_control: attributeSchema(0, 0, 5),
|
||||
effort: attributeSchema(0, 0, 5)
|
||||
}),
|
||||
|
||||
// Niveaux PNJ (menace, expérience, rôle)
|
||||
// Critères de définition du PNJ (Menace, Expérience, Rôle)
|
||||
threat: attributeSchema(1, 1, 4),
|
||||
experience: attributeSchema(1, 1, 4),
|
||||
role: attributeSchema(1, 1, 4),
|
||||
|
||||
// Compétences (les 30 compétences individuelles)
|
||||
skills: skillsSchema(),
|
||||
|
||||
// Description libre des compétences (champ texte PNJ)
|
||||
// Description libre des Spécialités (champ texte PNJ)
|
||||
freeSkills: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
|
||||
// Catégories de compétences
|
||||
skill_categories: skillCategoriesSchema(),
|
||||
|
||||
// Caractéristiques (8)
|
||||
abilities: abilitiesSchema(),
|
||||
// Valeurs calculées (dérivées des niveaux de menace / expérience / rôle)
|
||||
computed: new fields.SchemaField({
|
||||
attack: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
|
||||
vigor: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
|
||||
action: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 3 }),
|
||||
specialties: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 4 }),
|
||||
rerolls: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
|
||||
contact: new fields.StringField({ required: true, nullable: false, initial: "7" }),
|
||||
reaction: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 3 }),
|
||||
reactionBonus: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
|
||||
pools: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
|
||||
gear: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 9 }),
|
||||
gearHindrance: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
|
||||
protection: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1 })
|
||||
}),
|
||||
|
||||
// Équipement
|
||||
equipment: equipmentSchema()
|
||||
@@ -92,58 +96,67 @@ export default class VermineNpcData extends foundry.abstract.TypeDataModel {
|
||||
prepareDerivedData() {
|
||||
super.prepareDerivedData()
|
||||
|
||||
// 1. Calculer les seuils de blessures selon le niveau de menace
|
||||
// 1. Calculer les valeurs dérivées selon les niveaux de menace/expérience/rôle
|
||||
this._setNpcComputedValues()
|
||||
|
||||
// 2. Calculer les seuils de blessures selon le niveau de menace
|
||||
this._setNpcWoundThresholds()
|
||||
|
||||
// 2. Calculer les réserves selon le niveau de rôle
|
||||
this._setNpcAttributes()
|
||||
|
||||
// 3. Définir les libellés des caractéristiques
|
||||
this._setAbilityLabels()
|
||||
|
||||
// 4. Mettre à jour le statut de combat
|
||||
// 3. Mettre à jour le statut de combat
|
||||
this._updateCombatStatus()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les seuils de blessures à partir du niveau de menace.
|
||||
* Utilise CONFIG.VERMINE.npcThreatLevels.
|
||||
* Calcule les valeurs dérivées à partir des niveaux de Menace, Expérience
|
||||
* et Rôle. Utilise CONFIG.VERMINE.npcThreatLevels, .npcExperienceLevels
|
||||
* et .npcRoleLevels.
|
||||
*/
|
||||
_setNpcComputedValues() {
|
||||
const threatLevel = this.threat?.value || 1
|
||||
const expLevel = this.experience?.value || 1
|
||||
const roleLevel = this.role?.value || 1
|
||||
|
||||
const threatConfig = CONFIG.VERMINE.npcThreatLevels[threatLevel] || {}
|
||||
const expConfig = CONFIG.VERMINE.npcExperienceLevels[expLevel] || {}
|
||||
const roleConfig = CONFIG.VERMINE.npcRoleLevels[roleLevel] || {}
|
||||
|
||||
// Menace
|
||||
this.computed.attack = threatConfig.attack || 0
|
||||
this.computed.vigor = threatConfig.vigor || 0
|
||||
|
||||
// Expérience
|
||||
this.computed.action = expConfig.action || 3
|
||||
this.computed.specialties = expConfig.specialties || 4
|
||||
this.computed.rerolls = expConfig.rerolls || 0
|
||||
this.computed.contact = expConfig.contact || "7"
|
||||
|
||||
// Rôle
|
||||
this.computed.reaction = roleConfig.reaction || 0
|
||||
this.computed.reactionBonus = roleConfig.reaction_bonus || 0
|
||||
this.computed.pools = roleConfig.pools || 0
|
||||
this.computed.gear = roleConfig.gear || 9
|
||||
this.computed.gearHindrance = roleConfig.gear_hindrance || 0
|
||||
this.computed.protection = roleConfig.protection || 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les seuils et le nombre de boîtes de blessures à partir du
|
||||
* niveau de menace (tableau "Niveau de Menace"). Utilise
|
||||
* CONFIG.VERMINE.npcThreatLevels.
|
||||
*/
|
||||
_setNpcWoundThresholds() {
|
||||
const health = this.abilities?.health?.value || 1
|
||||
const threatLevel = this.threat?.value || 1
|
||||
const threatConfig = CONFIG.VERMINE.npcThreatLevels[threatLevel] || {}
|
||||
|
||||
this.minorWound.threshold = health
|
||||
this.majorWound.threshold = health + 3
|
||||
this.deadlyWound.threshold = Math.min(health + 7, 10)
|
||||
this.minorWound.threshold = threatConfig.minorThreshold ?? 1
|
||||
this.majorWound.threshold = threatConfig.majorThreshold ?? 4
|
||||
this.deadlyWound.threshold = threatConfig.deadlyThreshold ?? 6
|
||||
|
||||
this.minorWound.max = threatConfig.minorWound || 1
|
||||
this.majorWound.max = threatConfig.majorWound || 1
|
||||
this.deadlyWound.max = threatConfig.deadlyWound || 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit les attributs dérivés (effort, sang-froid) selon le niveau de rôle.
|
||||
* Utilise CONFIG.VERMINE.npcRoleLevels.
|
||||
*/
|
||||
_setNpcAttributes() {
|
||||
const roleLevel = this.role?.value || 1
|
||||
const roleConfig = CONFIG.VERMINE.npcRoleLevels[roleLevel] || {}
|
||||
|
||||
this.attributes.effort.max = roleConfig.pools || 0
|
||||
this.attributes.self_control.max = roleConfig.reaction_bonus || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit les libellés localisés des caractéristiques.
|
||||
*/
|
||||
_setAbilityLabels() {
|
||||
for (const [k, v] of Object.entries(this.abilities)) {
|
||||
v.label = game.i18n.localize(CONFIG.VERMINE.abilities[k]) ?? k
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le label du statut de combat en fonction de la difficulté.
|
||||
*/
|
||||
|
||||
@@ -10,9 +10,11 @@ export default class VermineSpecialtyData extends foundry.abstract.TypeDataModel
|
||||
/** @override */
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields
|
||||
const reqInt = { required: true, nullable: false, integer: true }
|
||||
return {
|
||||
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
skill: new fields.StringField({ required: true, nullable: false, initial: "" })
|
||||
skill: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
level: new fields.NumberField({ ...reqInt, initial: 1, min: 0, max: 5 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,67 @@
|
||||
import { baseItemSchema } from "./_shared.mjs"
|
||||
|
||||
/**
|
||||
* DataModel pour les items de type "vehicle" (véhicules).
|
||||
* @augments {foundry.abstract.TypeDataModel}
|
||||
* DataModel pour les acteurs de type "vehicle" (véhicules et montures).
|
||||
* Un véhicule est désormais un acteur (et non un objet) : il possède sa propre
|
||||
* fiche, peut être posé sur la scène comme un jeton et appartient à un groupe
|
||||
* ou un personnage via `ownerId`.
|
||||
*
|
||||
* Caractéristiques (regles_vehicules.txt) : Rareté, Fiabilité (véhicules),
|
||||
* Réserve d'Effort (montures), Distance, Vitesse (croisière/pointe) et Entretien.
|
||||
* Pas de statistiques de combat (les règles ne définissent pas de points de vie).
|
||||
*/
|
||||
import { raritySchema } from "./_shared.mjs"
|
||||
|
||||
export default class VermineVehicleData extends foundry.abstract.TypeDataModel {
|
||||
|
||||
/** @override */
|
||||
static LOCALIZATION_PREFIXES = ["VERMINE.item.vehicle"]
|
||||
static LOCALIZATION_PREFIXES = ["VERMINE.vehicle"]
|
||||
|
||||
/** @override */
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields
|
||||
const reqInt = { required: true, nullable: false, integer: true }
|
||||
return {
|
||||
...baseItemSchema(),
|
||||
mobility: new fields.NumberField({ ...reqInt, initial: 3, min: 0 })
|
||||
// Identité
|
||||
identity: new fields.SchemaField({
|
||||
profile: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
origin: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
theme: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
notes: new fields.HTMLField({ required: true, initial: "", textSearch: true })
|
||||
}),
|
||||
|
||||
// Rareté (valeur 1-10 + handicap)
|
||||
rarity: raritySchema(),
|
||||
|
||||
// Fiabilité étendue (1-10) pour les véhicules
|
||||
reliability: new fields.NumberField({ ...reqInt, initial: 5, min: 1, max: 10 }),
|
||||
|
||||
// Type : "vehicle" (véhicule) ou "mount" (monture)
|
||||
kind: new fields.StringField({ required: true, nullable: false, initial: "vehicle" }),
|
||||
|
||||
// Distance : km parcourus avec un "plein"
|
||||
distance: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
|
||||
// Vitesse : croisière / pointe (durée de la pointe)
|
||||
speed: new fields.SchemaField({
|
||||
cruise: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
top: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
topDuration: new fields.StringField({ required: true, nullable: false, initial: "10 min" })
|
||||
}),
|
||||
|
||||
// Entretien : compétences nécessaires (clés de CONFIG.VERMINE.skills)
|
||||
maintenance: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
|
||||
|
||||
// Réserve d'Effort (montures)
|
||||
effortReserve: new fields.SchemaField({
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
max: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
|
||||
}),
|
||||
|
||||
// Mobilité
|
||||
mobility: new fields.NumberField({ ...reqInt, initial: 3, min: 0 }),
|
||||
|
||||
// Propriétaire (id d'acteur : groupe ou personnage)
|
||||
ownerId: new fields.StringField({ required: true, nullable: false, initial: "" })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export default class VermineWeaponData extends foundry.abstract.TypeDataModel {
|
||||
return {
|
||||
...baseItemSchema(),
|
||||
skill: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
equipped: new fields.BooleanField({ required: true, initial: false }),
|
||||
min_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
max_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
damage: new fields.SchemaField({
|
||||
|
||||
@@ -2,10 +2,11 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api
|
||||
|
||||
export class TotemPicker extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
|
||||
constructor(linkEl, actor) {
|
||||
constructor(linkEl, actor, options = {}) {
|
||||
super();
|
||||
this.linkEl = linkEl;
|
||||
this.actor = actor;
|
||||
this.fillInstincts = options.fillInstincts === true;
|
||||
}
|
||||
|
||||
static get DEFAULT_OPTIONS() {
|
||||
@@ -32,7 +33,17 @@ export class TotemPicker extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
el.addEventListener('click', event => {
|
||||
const link = event.target.closest('a');
|
||||
if (!link?.dataset.totem) return;
|
||||
this.actor.update({ 'system.identity.totem': link.dataset.totem });
|
||||
const update = { 'system.identity.totem': link.dataset.totem };
|
||||
// Remplit automatiquement Instincts/Interdits depuis la config du totem
|
||||
// (utilisé pour le Totem du Groupe).
|
||||
if (this.fillInstincts && this.actor.system?.identity) {
|
||||
const totemKey = link.dataset.totem;
|
||||
const instincts = game.i18n.localize(`TOTEMS.${totemKey}.instincts`);
|
||||
const bans = game.i18n.localize(`TOTEMS.${totemKey}.bans`);
|
||||
if (!instincts.startsWith("TOTEMS.")) update['system.identity.instincts'] = instincts;
|
||||
if (!bans.startsWith("TOTEMS.")) update['system.identity.prohibits'] = bans;
|
||||
}
|
||||
this.actor.update(update);
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,10 +99,10 @@ VERMINE.totemDomains = {
|
||||
* NPC Threat Levels configuration
|
||||
*/
|
||||
VERMINE.npcThreatLevels = {
|
||||
1: { "label": "THREAT_LEVELS.minor", "attack": 3, "vigor": 1, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
|
||||
2: { "label": "THREAT_LEVELS.serious", "attack": 4, "vigor": 2, "minorWound": 2, "majorWound": 1, "deadlyWound": 1 },
|
||||
3: { "label": "THREAT_LEVELS.major", "attack": 5, "vigor": 3, "minorWound": 2, "majorWound": 1, "deadlyWound": 1 },
|
||||
4: { "label": "THREAT_LEVELS.deadly", "attack": 6, "vigor": 4, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }
|
||||
1: { "label": "THREAT_LEVELS.minor", "attack": 3, "vigor": 1, "minorThreshold": 1, "majorThreshold": 4, "deadlyThreshold": 6, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
|
||||
2: { "label": "THREAT_LEVELS.serious", "attack": 4, "vigor": 2, "minorThreshold": 2, "majorThreshold": 5, "deadlyThreshold": 8, "minorWound": 2, "majorWound": 1, "deadlyWound": 1 },
|
||||
3: { "label": "THREAT_LEVELS.major", "attack": 5, "vigor": 3, "minorThreshold": 2, "majorThreshold": 5, "deadlyThreshold": 9, "minorWound": 2, "majorWound": 2, "deadlyWound": 1 },
|
||||
4: { "label": "THREAT_LEVELS.deadly", "attack": 6, "vigor": 4, "minorThreshold": 3, "majorThreshold": 6, "deadlyThreshold": 9, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,29 +129,30 @@ VERMINE.npcRoleLevels = {
|
||||
* Creature Pattern Levels configuration
|
||||
*/
|
||||
VERMINE.creaturePatternLevels = {
|
||||
1: { "label": "PATTERN_LEVELS.insect", "attack": 2, "damage": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
|
||||
2: { "label": "PATTERN_LEVELS.rat", "attack": 3, "damage": 1, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
|
||||
3: { "label": "PATTERN_LEVELS.dog", "attack": 4, "damage": 3, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
|
||||
4: { "label": "PATTERN_LEVELS.bear", "attack": 6, "damage": 6, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }
|
||||
1: { "label": "PATTERN_LEVELS.insect", "attack": 2, "damage": 0, "minorThreshold": 0, "majorThreshold": 0, "deadlyThreshold": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
|
||||
2: { "label": "PATTERN_LEVELS.rat", "attack": 3, "damage": 1, "minorThreshold": 0, "majorThreshold": 1, "deadlyThreshold": 3, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
|
||||
3: { "label": "PATTERN_LEVELS.dog", "attack": 4, "damage": 3, "minorThreshold": 1, "majorThreshold": 3, "deadlyThreshold": 5, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
|
||||
4: { "label": "PATTERN_LEVELS.bear", "attack": 6, "damage": 6, "minorThreshold": 3, "majorThreshold": 5, "deadlyThreshold": 7, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creature Size Levels configuration
|
||||
*/
|
||||
VERMINE.creatureSizeLevels = {
|
||||
1: { "label": "SIZE_LEVELS.small", "attack": 2, "vigor": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
|
||||
2: { "label": "SIZE_LEVELS.medium", "attack": 3, "vigor": 2, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
|
||||
3: { "label": "SIZE_LEVELS.large", "attack": 4, "vigor": 3, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 }
|
||||
0: { "label": "SIZE_LEVELS.tiny", "attack": 0, "vigor": 0, "minorThreshold": 0, "majorThreshold": 0, "deadlyThreshold": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 },
|
||||
1: { "label": "SIZE_LEVELS.small", "attack": 1, "vigor": 1, "minorThreshold": 0, "majorThreshold": 1, "deadlyThreshold": 1, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
|
||||
2: { "label": "SIZE_LEVELS.medium", "attack": 2, "vigor": 2, "minorThreshold": 2, "majorThreshold": 2, "deadlyThreshold": 2, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
|
||||
3: { "label": "SIZE_LEVELS.large", "attack": 3, "vigor": 3, "minorThreshold": 3, "majorThreshold": 3, "deadlyThreshold": 3, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creature Pack Levels configuration
|
||||
*/
|
||||
VERMINE.creaturePackLevels = {
|
||||
0: { "attack": 0, "damage": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 },
|
||||
1: { "attack": 1, "damage": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
|
||||
2: { "attack": 2, "damage": 2, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 },
|
||||
3: { "attack": 5, "damage": 5, "minorWound": 3, "majorWound": 3, "deadlyWound": 3 }
|
||||
0: { "label": "VERMINE.none", "attack": 0, "damage": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 },
|
||||
1: { "label": "PACK_LEVELS.small", "attack": 1, "damage": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
|
||||
2: { "label": "PACK_LEVELS.large", "attack": 2, "damage": 2, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 },
|
||||
3: { "label": "PACK_LEVELS.giant", "attack": 5, "damage": 5, "minorWound": 3, "majorWound": 3, "deadlyWound": 3 }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,6 +180,25 @@ VERMINE.abilityCategories = {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Types de Capacités de créature (mode d'activation/disponibilité).
|
||||
* Correspond aux Modes de jeu + une catégorie "base".
|
||||
*/
|
||||
VERMINE.creatureCapacityTypes = {
|
||||
"base": {
|
||||
"label": "CREATURE_CAPACITY_TYPES.base"
|
||||
},
|
||||
"survival": {
|
||||
"label": "CREATURE_CAPACITY_TYPES.survival"
|
||||
},
|
||||
"nightmare": {
|
||||
"label": "CREATURE_CAPACITY_TYPES.nightmare"
|
||||
},
|
||||
"apocalypse": {
|
||||
"label": "CREATURE_CAPACITY_TYPES.apocalypse"
|
||||
}
|
||||
}
|
||||
|
||||
VERMINE.abilities = {
|
||||
"vigor": "ABILITIES.vigor.name",
|
||||
"health": "ABILITIES.health.name",
|
||||
@@ -268,6 +288,23 @@ VERMINE.totems = {
|
||||
"loner": "TOTEMS.loner.name",
|
||||
"adapted": "TOTEMS.adapted.name"
|
||||
}
|
||||
/**
|
||||
* Influence des Dés de Totems (règles p. 121).
|
||||
* Selon le Totem dominant (strictement plus de Dés Humains ou Adaptés),
|
||||
* le personnage subit un Bonus/Malus de 1D sur les Domaines de Compétences.
|
||||
* Neutre : aucun modificateur.
|
||||
*/
|
||||
VERMINE.totemInfluence = {
|
||||
"human": {
|
||||
"bonus": ["man", "tool", "world"],
|
||||
"malus": ["animal", "survival", "weapon"]
|
||||
},
|
||||
"adapted": {
|
||||
"bonus": ["animal", "survival", "weapon"],
|
||||
"malus": ["man", "tool", "world"]
|
||||
}
|
||||
}
|
||||
|
||||
VERMINE.totem_opposites = {
|
||||
"human": "TOTEMS.adapted.name",
|
||||
"predator": "TOTEMS.scavenger.name",
|
||||
@@ -468,3 +505,54 @@ VERMINE.totemEffects = {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Communauté — Domaines et sections (jauges).
|
||||
*/
|
||||
VERMINE.communityDomains = {
|
||||
resources: {
|
||||
label: "COMMUNITY.domains.resources",
|
||||
sections: { vivre: "COMMUNITY.sections.vivre", supplies: "COMMUNITY.sections.supplies" }
|
||||
},
|
||||
security: {
|
||||
label: "COMMUNITY.domains.security",
|
||||
sections: { defense: "COMMUNITY.sections.defense", care: "COMMUNITY.sections.care" }
|
||||
},
|
||||
dynamism: {
|
||||
label: "COMMUNITY.domains.dynamism",
|
||||
sections: { cohesion: "COMMUNITY.sections.cohesion", productivity: "COMMUNITY.sections.productivity" }
|
||||
},
|
||||
relations: {
|
||||
label: "COMMUNITY.domains.relations",
|
||||
sections: { human: "COMMUNITY.sections.human", ecology: "COMMUNITY.sections.ecology" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Communauté — Tailles (effectif, Figures, Compagnies recommandés).
|
||||
*/
|
||||
VERMINE.communitySizeLevels = {
|
||||
1: { label: "COMMUNITY.sizeLevels.1", population: "7-50", figures: 3, companies: 5 },
|
||||
2: { label: "COMMUNITY.sizeLevels.2", population: "51-75", figures: 5, companies: 7 },
|
||||
3: { label: "COMMUNITY.sizeLevels.3", population: "100+", figures: 7, companies: 9 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Communauté — États d'une jauge de Domaine.
|
||||
* Crise 0D / Bas 1-2D / Stable 3-5D / Opulent 6D+.
|
||||
*/
|
||||
VERMINE.communityGaugeStates = {
|
||||
crisis: { label: "COMMUNITY.states.crisis" },
|
||||
low: { label: "COMMUNITY.states.low" },
|
||||
stable: { label: "COMMUNITY.states.stable" },
|
||||
opulent: { label: "COMMUNITY.states.opulent" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Communauté — Âge (notion de temps).
|
||||
*/
|
||||
VERMINE.communityAges = {
|
||||
recent: "COMMUNITY.age.recent",
|
||||
established: "COMMUNITY.age.established",
|
||||
old: "COMMUNITY.age.old"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { VermineExperience } from "../experience.mjs";
|
||||
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
/**
|
||||
* Dialogue d'achat d'une Adaptation/Mutation avec les Dés d'Évolution
|
||||
* accumulés. Vérifie le coût (niveau 1-4), la limite du Mode de jeu et les
|
||||
* restrictions sur les Mutations ; en cas de succès, crée l'item et efface
|
||||
* les Dés d'Évolution accumulés.
|
||||
*/
|
||||
export default class BuyEvolutionDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
||||
|
||||
#actor;
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["vermine-roll", "buy-evolution"],
|
||||
tag: "form",
|
||||
window: {
|
||||
icon: "fas fa-cart-shopping",
|
||||
resizable: false
|
||||
},
|
||||
position: {
|
||||
width: 480,
|
||||
height: 460
|
||||
},
|
||||
actions: {
|
||||
buy: BuyEvolutionDialog.#onBuy,
|
||||
cancel: BuyEvolutionDialog.#onCancel
|
||||
}
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/vermine2047/templates/dialogs/buy-evolution-dialog.hbs", scrollable: [""] }
|
||||
};
|
||||
|
||||
static async create({ actorId }) {
|
||||
const actor = await game.actors.get(actorId);
|
||||
if (!actor) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
|
||||
return null;
|
||||
}
|
||||
return new BuyEvolutionDialog({ actor });
|
||||
}
|
||||
|
||||
constructor(options = {}) {
|
||||
super(options);
|
||||
this.#actor = options.actor;
|
||||
}
|
||||
|
||||
get title() {
|
||||
return game.i18n.localize("VERMINE.buy_evolution");
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const actor = this.#actor;
|
||||
const mode = VermineExperience.gameMode();
|
||||
const modeLabels = {
|
||||
1: game.i18n.localize("VERMINE.mode_survie"),
|
||||
2: game.i18n.localize("VERMINE.mode_cauchemar"),
|
||||
3: game.i18n.localize("VERMINE.mode_apocalypse")
|
||||
};
|
||||
const levels = [];
|
||||
for (let value = 1; value <= 4; value++) {
|
||||
levels.push({ value, cost: value });
|
||||
}
|
||||
return {
|
||||
evolutionDice: actor.system?.experience?.evolution || 0,
|
||||
defaultName: game.i18n.localize("ITEMS.new_evolution"),
|
||||
levels,
|
||||
totalLevels: VermineExperience.evolutionTotalLevels(actor),
|
||||
modeMax: VermineExperience.evolutionMaxForMode(mode),
|
||||
modeLabel: modeLabels[mode] || ""
|
||||
};
|
||||
}
|
||||
|
||||
static async #onCancel(event, target) {
|
||||
this.close();
|
||||
}
|
||||
|
||||
static async #onBuy(event, target) {
|
||||
const actor = this.#actor;
|
||||
const name = this.element.querySelector("#evolution-name")?.value?.trim() || "";
|
||||
const cost = parseInt(this.element.querySelector("#evolution-level")?.value, 10) || 0;
|
||||
const mutation = this.element.querySelector("#evolution-mutation")?.checked || false;
|
||||
|
||||
if (!name) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_evolution_no_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
const error = VermineExperience.validateEvolutionPurchase(actor, cost, mutation);
|
||||
if (error) {
|
||||
ui.notifications.warn(game.i18n.localize(error));
|
||||
return;
|
||||
}
|
||||
|
||||
const created = await actor.createEmbeddedDocuments("Item", [{
|
||||
name,
|
||||
type: "evolution",
|
||||
system: {
|
||||
level: { value: cost },
|
||||
mutation
|
||||
}
|
||||
}]);
|
||||
// N'efface les Dés d'Évolution que si l'item a bien été créé.
|
||||
if (!created || created.length === 0) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_evolution_create"));
|
||||
return;
|
||||
}
|
||||
await actor.update({ "system.experience.evolution": 0 });
|
||||
|
||||
ui.notifications.info(game.i18n.localize("VERMINE.buy_evolution_done"));
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
import { VermineUtils } from "../roll.mjs";
|
||||
import { VermineExchange } from "../exchange.mjs";
|
||||
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
/**
|
||||
* Dialogue d'attaque pour les échanges de coups.
|
||||
* Prépare le pool, la difficulté (contact / distance) et le contexte
|
||||
* d'échange (cibles, dégâts de base) selon le type d'acteur :
|
||||
* - Personnage : capacité + compétence de l'arme.
|
||||
* - PNJ : pool d'attaque du niveau de menace + réserves de rôle.
|
||||
* - Créature : pool d'attaque calculé, difficulté du statut de combat.
|
||||
*/
|
||||
export default class CombatDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["vermine-roll"],
|
||||
tag: "form",
|
||||
window: {
|
||||
icon: "fas fa-crosshairs",
|
||||
resizable: false
|
||||
},
|
||||
position: {
|
||||
width: 520,
|
||||
height: 600
|
||||
},
|
||||
actions: {
|
||||
roll: CombatDialog.#onRoll,
|
||||
cancel: CombatDialog.#onCancel
|
||||
}
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/vermine2047/templates/dialogs/combat-dialog.hbs" }
|
||||
};
|
||||
|
||||
static async create({ actorId, weapon = null }) {
|
||||
const actor = await game.actors.get(actorId);
|
||||
if (!actor) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
|
||||
return null;
|
||||
}
|
||||
const mode = actor.type === "creature" ? "contact" : VermineExchange.modeForWeapon(weapon);
|
||||
return new CombatDialog({ actor, weapon, mode });
|
||||
}
|
||||
|
||||
constructor(options = {}) {
|
||||
super(options);
|
||||
this.actor = options.actor;
|
||||
this.weapon = options.weapon ?? null;
|
||||
this.mode = options.mode ?? "contact";
|
||||
this.abilityKey = VermineExchange.attackAbility(this.mode);
|
||||
this.skillKey = this.weapon?.system?.skill || null;
|
||||
this.skillValue = this.skillKey ? (this.actor.system?.skills?.[this.skillKey]?.value || 0) : 0;
|
||||
this.skillPool = this.skillValue ? (CONFIG.VERMINE.SkillLevels?.[this.skillValue]?.dicePool || 0) : 0;
|
||||
this.skillReroll = this.skillValue ? (CONFIG.VERMINE.SkillLevels?.[this.skillValue]?.reroll || 0) : 0;
|
||||
this.basePool = VermineExchange.baseAttackPool(this.actor, this.weapon, this.mode);
|
||||
this.baseDamage = VermineExchange.baseDamage(this.actor, this.weapon);
|
||||
}
|
||||
|
||||
get title() {
|
||||
return game.i18n.localize("VERMINE.attack");
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const actor = this.actor;
|
||||
const isCharacter = actor.type === "character";
|
||||
const isNpc = actor.type === "npc";
|
||||
const isCreature = actor.type === "creature";
|
||||
|
||||
const targets = [...game.user.targets].map(t => ({
|
||||
id: t.actor?.id ?? null,
|
||||
uuid: t.actor?.uuid ?? null,
|
||||
name: t.name,
|
||||
canDefend: t.actor?.type !== "creature"
|
||||
}));
|
||||
|
||||
let difficultyOptions = [];
|
||||
let lockedDifficulty = null;
|
||||
let defaultDifficulty = null;
|
||||
|
||||
if (isCreature) {
|
||||
const d = parseInt(actor.system?.combatStatus?.difficulty, 10) || 9;
|
||||
lockedDifficulty = d;
|
||||
difficultyOptions = [{ difficulty: d, label: "", locked: true }];
|
||||
defaultDifficulty = d;
|
||||
} else if (this.mode === "ranged") {
|
||||
difficultyOptions = VermineExchange.rangeDifficultyOptions().map(o => ({
|
||||
difficulty: o.difficulty,
|
||||
label: game.i18n.localize(o.label),
|
||||
rangeKey: o.key
|
||||
}));
|
||||
defaultDifficulty = difficultyOptions[0]?.difficulty ?? 7;
|
||||
} else if (isNpc) {
|
||||
difficultyOptions = VermineExchange.npcDifficultyOptions(actor).map(d => ({
|
||||
difficulty: d,
|
||||
label: String(d)
|
||||
}));
|
||||
defaultDifficulty = difficultyOptions[0]?.difficulty ?? 7;
|
||||
} else {
|
||||
difficultyOptions = VermineExchange.contactDifficultyOptions(this.skillValue).map(d => ({
|
||||
difficulty: d,
|
||||
label: String(d)
|
||||
}));
|
||||
defaultDifficulty = difficultyOptions[0]?.difficulty ?? 7;
|
||||
}
|
||||
|
||||
// Cible de type créature : défense passive, la difficulté d'attaque est
|
||||
// forcée au statut de combat de la créature (toute réussite ≥1 touche).
|
||||
const creatureTargets = targets.filter(t => t.id && !t.canDefend);
|
||||
const creatureLocked = !isCreature && creatureTargets.length && creatureTargets.length === targets.length;
|
||||
if (creatureLocked) {
|
||||
const ref = creatureTargets[0];
|
||||
const creature = ref.id ? game.actors.get(ref.id) : null;
|
||||
const actor = creature ?? (ref.uuid ? await fromUuid(ref.uuid) : null);
|
||||
const d = parseInt(actor?.system?.combatStatus?.difficulty, 10) || 7;
|
||||
lockedDifficulty = d;
|
||||
difficultyOptions = [{ difficulty: d, label: "", locked: true }];
|
||||
defaultDifficulty = d;
|
||||
}
|
||||
|
||||
const threat = actor.system?.threat?.value || 1;
|
||||
const role = actor.system?.role?.value || 1;
|
||||
|
||||
return {
|
||||
actor,
|
||||
system: actor.system,
|
||||
config: CONFIG.VERMINE,
|
||||
actorType: actor.type,
|
||||
mode: this.mode,
|
||||
weapon: this.weapon,
|
||||
abilityKey: this.abilityKey,
|
||||
abilityValue: actor.system?.abilities?.[this.abilityKey]?.value || 0,
|
||||
skillKey: this.skillKey,
|
||||
skillValue: this.skillValue,
|
||||
basePool: this.basePool,
|
||||
baseDamage: this.baseDamage,
|
||||
isCharacter,
|
||||
isNpc,
|
||||
isCreature,
|
||||
creatureLocked,
|
||||
npcThreatPool: isNpc ? (CONFIG.VERMINE.npcThreatLevels?.[threat]?.attack || 0) : 0,
|
||||
npcPoolMax: isNpc ? (CONFIG.VERMINE.npcRoleLevels?.[role]?.pools || 0) : 0,
|
||||
difficultyOptions,
|
||||
lockedDifficulty,
|
||||
defaultDifficulty,
|
||||
hasAmmo: this.mode === "ranged" && (this.weapon?.system?.ammo ?? 0) > 0,
|
||||
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
|
||||
availableItems: actor.items.filter(i => i.type === "item"),
|
||||
targets
|
||||
};
|
||||
}
|
||||
|
||||
async _onRender(context, options) {
|
||||
this.element.dataset.actorId = this.actor.id;
|
||||
this.element.dataset.weaponId = this.weapon?.id ?? "";
|
||||
this.element.dataset.mode = this.mode;
|
||||
|
||||
for (const inp of this.element.querySelectorAll("[data-roll]")) {
|
||||
inp.addEventListener("change", this.#onInputChange.bind(this));
|
||||
}
|
||||
|
||||
const selfControl = this.element.querySelector("#self_control");
|
||||
if (selfControl) {
|
||||
selfControl.addEventListener("change", this.#onChangeSelfControl.bind(this));
|
||||
}
|
||||
|
||||
this.element.querySelector("#second-action")?.addEventListener("change", () => this.#updateUI());
|
||||
this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI());
|
||||
this.element.querySelector("#human-totem")?.addEventListener("change", () => this.#updateUI());
|
||||
this.element.querySelector("#adapted-totem")?.addEventListener("change", () => this.#updateUI());
|
||||
this.element.querySelector("#pools-spent")?.addEventListener("change", () => this.#updateUI());
|
||||
|
||||
const ability = this.element.querySelector("#ability");
|
||||
if (ability && this.actor.type === "character") {
|
||||
const sc = this.element.querySelector("#self_control");
|
||||
if (sc) sc.max = ability.value;
|
||||
}
|
||||
|
||||
this.#updateUI();
|
||||
}
|
||||
|
||||
// ── Getters ──────────────────────────────────────────────────────────
|
||||
|
||||
get #el() { return this.element; }
|
||||
|
||||
#getDifficultyBase() {
|
||||
const el = this.#el.querySelector('input[name="combat-difficulty"]:checked');
|
||||
return parseInt(el?.value, 10) || 7;
|
||||
}
|
||||
|
||||
#getSecondAction() {
|
||||
return this.#el.querySelector("#second-action")?.checked ?? false;
|
||||
}
|
||||
|
||||
#getHandicap() {
|
||||
const sel = this.#el.querySelector("#handicap");
|
||||
return Math.max(0, (parseInt(sel?.value, 10) || 1) - 1);
|
||||
}
|
||||
|
||||
#getSelfCtrl() {
|
||||
return parseInt(this.#el.querySelector("#self_control")?.value, 10) || 0;
|
||||
}
|
||||
|
||||
#getPoolsSpent() {
|
||||
return parseInt(this.#el.querySelector("#pools-spent")?.value, 10) || 0;
|
||||
}
|
||||
|
||||
#getUseAmmo() {
|
||||
return this.#el.querySelector("#use-ammo")?.checked ?? false;
|
||||
}
|
||||
|
||||
#getTotems() {
|
||||
return {
|
||||
human: this.#el.querySelector("#human-totem")?.checked ?? false,
|
||||
adapted: this.#el.querySelector("#adapted-totem")?.checked ?? false
|
||||
};
|
||||
}
|
||||
|
||||
#getKeepTotem() {
|
||||
return this.#el.querySelector("#keep-totem-select")?.value ?? null;
|
||||
}
|
||||
|
||||
getDifficultySelect() {
|
||||
return this.#getDifficultyBase() + (this.#getSecondAction() ? 2 : 0);
|
||||
}
|
||||
|
||||
getSkillCategory() {
|
||||
if (this.actor.type !== "character" || !this.skillKey) return null;
|
||||
return this.actor.system.skills[this.skillKey]?.category ?? null;
|
||||
}
|
||||
|
||||
getSkillLevel() {
|
||||
return this.actor.type === "character" && this.skillValue ? this.skillValue : null;
|
||||
}
|
||||
|
||||
getReroll() {
|
||||
if (this.actor.type === "npc") {
|
||||
const exp = this.actor.system?.experience?.value || 1;
|
||||
return CONFIG.VERMINE.npcExperienceLevels?.[exp]?.rerolls || 0;
|
||||
}
|
||||
if (this.actor.type === "creature") return 0;
|
||||
return this.skillReroll;
|
||||
}
|
||||
|
||||
getMaxEffort() {
|
||||
if (this.actor.type === "character") return this.actor.system?.abilities?.[this.abilityKey]?.value || 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
getPoolBreakdown() {
|
||||
const ability = this.actor.type === "character"
|
||||
? (this.actor.system?.abilities?.[this.abilityKey]?.value || 0)
|
||||
: 0;
|
||||
const skill = this.actor.type === "character" ? this.skillPool : 0;
|
||||
const selfControl = this.#getSelfCtrl();
|
||||
const pools = this.actor.type === "npc" ? this.#getPoolsSpent() : 0;
|
||||
const specialty = this.#hasSpecialtySelected() ? 1 : 0;
|
||||
const help = this.#el.querySelector("#helped")?.checked ? 1 : 0;
|
||||
const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked");
|
||||
const tooling = toolsChecked && toolsChecked.value !== "0" ? 1 : 0;
|
||||
const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
|
||||
const malus = VermineExchange.woundMalus(this.actor);
|
||||
const total = Math.max(0, this.basePool + selfControl + pools + specialty + help + tooling + group - malus);
|
||||
return { ability, skill, selfControl, pools, specialty, help, tooling, group, malus, total };
|
||||
}
|
||||
|
||||
getDicePool() {
|
||||
return this.getPoolBreakdown().total;
|
||||
}
|
||||
|
||||
#hasSpecialtySelected() {
|
||||
const checked = this.#el.querySelector("input[name='usingSpecialization']:checked");
|
||||
return Boolean(checked && checked.value !== "aucune");
|
||||
}
|
||||
|
||||
getSpecialtyName() {
|
||||
const checked = this.#el.querySelector("input[name='usingSpecialization']:checked");
|
||||
return checked && checked.value !== "aucune" ? checked.value : null;
|
||||
}
|
||||
|
||||
getLabel() {
|
||||
const base = this.mode === "ranged"
|
||||
? game.i18n.localize("VERMINE.attack_ranged")
|
||||
: game.i18n.localize("VERMINE.attack_contact");
|
||||
return this.weapon ? `${base} (${this.weapon.name})` : base;
|
||||
}
|
||||
|
||||
// ── UI ───────────────────────────────────────────────────────────────
|
||||
|
||||
#updateUI() {
|
||||
const total = this.getDicePool();
|
||||
const totalEl = this.#el.querySelector("#dice-pool-total");
|
||||
if (totalEl) totalEl.textContent = `${total}D`;
|
||||
|
||||
const finalEls = this.#el.querySelectorAll("#final-difficulty");
|
||||
for (const el of finalEls) el.textContent = this.getDifficultySelect();
|
||||
|
||||
const b = this.getPoolBreakdown();
|
||||
const bonusEl = this.#el.querySelector("#total-bonus");
|
||||
if (bonusEl) bonusEl.textContent = b.selfControl + b.pools + b.specialty + b.help + b.tooling + b.group - b.malus;
|
||||
|
||||
const handSel = this.#el.querySelector("#handicap");
|
||||
const handEl = this.#el.querySelector("#current-handicap");
|
||||
if (handSel && handEl) {
|
||||
handEl.textContent = handSel.options[handSel.selectedIndex]?.text ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
#onInputChange() {
|
||||
this.#updateUI();
|
||||
}
|
||||
|
||||
#onChangeSelfControl(ev) {
|
||||
const valEl = this.#el.querySelector("#self_control_value");
|
||||
if (valEl) valEl.textContent = ev.currentTarget.value;
|
||||
this.#updateUI();
|
||||
}
|
||||
|
||||
static async #onCancel(event, target) {
|
||||
this.close();
|
||||
}
|
||||
|
||||
static async #onRoll(event, target) {
|
||||
const selfCtrl = this.getPoolBreakdown().selfControl;
|
||||
if (selfCtrl > 0) {
|
||||
const current = this.actor?.system?.attributes?.self_control?.value ?? 0;
|
||||
if (current < selfCtrl) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_not_enough_self_control"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const finalDifficulty = this.getDifficultySelect();
|
||||
|
||||
// Consommation de munitions (armes à distance)
|
||||
if (this.mode === "ranged" && this.#getUseAmmo() && this.weapon) {
|
||||
const ammo = this.weapon.system.ammo || 0;
|
||||
if (ammo > 0) {
|
||||
await this.weapon.update({ "system.ammo": ammo - 1 });
|
||||
}
|
||||
}
|
||||
|
||||
if (selfCtrl > 0) {
|
||||
const newVal = this.actor.system.attributes.self_control.value - selfCtrl;
|
||||
await this.actor.update({ "system.attributes.self_control.value": newVal });
|
||||
}
|
||||
|
||||
const targets = [...game.user.targets].map(t => ({
|
||||
id: t.actor?.id ?? null,
|
||||
uuid: t.actor?.uuid ?? null,
|
||||
name: t.name,
|
||||
canDefend: t.actor?.type !== "creature"
|
||||
}));
|
||||
|
||||
const attack = {
|
||||
id: foundry.utils.randomID(),
|
||||
attackerId: this.actor.id,
|
||||
attackerName: this.actor.name,
|
||||
mode: this.mode,
|
||||
difficulty: finalDifficulty,
|
||||
range: this.mode === "ranged" ? this.#getRangeKey() : null,
|
||||
secondAction: this.#getSecondAction(),
|
||||
baseDamage: this.baseDamage,
|
||||
targets,
|
||||
weapon: this.weapon
|
||||
? { name: this.weapon.name, img: this.weapon.img, damage: foundry.utils.duplicate(this.weapon.system.damage) }
|
||||
: null
|
||||
};
|
||||
|
||||
const rollParams = {
|
||||
actor: this.actor,
|
||||
NoD: this.getDicePool(),
|
||||
Reroll: this.getReroll(),
|
||||
difficulty: finalDifficulty,
|
||||
handicap: this.#getHandicap(),
|
||||
rollLabel: this.getLabel(),
|
||||
totems: this.#getTotems(),
|
||||
self_control: selfCtrl,
|
||||
max_effort: this.getMaxEffort(),
|
||||
keepTotem: this.#getKeepTotem(),
|
||||
skillCategory: this.getSkillCategory(),
|
||||
skillLevel: this.getSkillLevel(),
|
||||
hasSpecialty: this.#hasSpecialtySelected(),
|
||||
poolBreakdown: this.getPoolBreakdown(),
|
||||
specialtyName: this.getSpecialtyName(),
|
||||
weapon: this.weapon?.toObject() ?? null,
|
||||
targets: targets.map(t => t.name),
|
||||
attack,
|
||||
messageFlags: { "vermine-exchange": attack }
|
||||
};
|
||||
|
||||
// Fermeture immédiate du dialogue : le jet (et l'animation 3D) continue
|
||||
// en arrière-plan sans bloquer la fermeture.
|
||||
this.close();
|
||||
|
||||
await VermineUtils.roll(rollParams);
|
||||
}
|
||||
|
||||
#getRangeKey() {
|
||||
const el = this.#el.querySelector('input[name="combat-difficulty"]:checked');
|
||||
return el?.dataset?.rangeKey ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
/**
|
||||
* Dialogue du meneur pour ouvrir une phase d'Expérience.
|
||||
* Permet d'attribuer les Dés d'Expérience collectifs (Danger, Découvertes,
|
||||
* Durée) au Groupe et aux personnages, ainsi que les Dés individuels
|
||||
* (Implication, Influence, Interprétation), les récompenses d'Objectifs et
|
||||
* la récompense de Traumatisme choisi.
|
||||
*/
|
||||
export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["vermine-roll"],
|
||||
tag: "form",
|
||||
window: {
|
||||
icon: "fas fa-star",
|
||||
resizable: true
|
||||
},
|
||||
position: {
|
||||
width: 620,
|
||||
height: 700
|
||||
},
|
||||
actions: {
|
||||
apply: ExperiencePhaseDialog.#onApply,
|
||||
cancel: ExperiencePhaseDialog.#onCancel
|
||||
}
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/vermine2047/templates/dialogs/experience-phase-dialog.hbs", scrollable: [""] }
|
||||
};
|
||||
|
||||
static async create() {
|
||||
return new ExperiencePhaseDialog();
|
||||
}
|
||||
|
||||
get title() {
|
||||
return game.i18n.localize("VERMINE.experience_phase");
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const characters = game.actors.filter(a => a.type === "character");
|
||||
const groups = game.actors.filter(a => a.type === "group");
|
||||
// Associe chaque personnage à son premier groupe (même logique que RollDialog.#findGroup)
|
||||
const memberOf = new Map();
|
||||
for (const g of groups) {
|
||||
for (const id of g.system?.members || []) {
|
||||
if (!memberOf.has(id)) memberOf.set(id, g.id);
|
||||
}
|
||||
}
|
||||
return {
|
||||
characters: characters.map(a => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
dice: a.system?.experience?.dice || 0,
|
||||
groupId: memberOf.get(a.id) || ""
|
||||
})),
|
||||
groups: groups.map(a => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
dice: a.system?.experience?.dice || 0
|
||||
})),
|
||||
rewards: [
|
||||
{ value: 0, label: game.i18n.localize("VERMINE.reward_anecdotic") },
|
||||
{ value: 1, label: game.i18n.localize("VERMINE.reward_minor") },
|
||||
{ value: 2, label: game.i18n.localize("VERMINE.reward_major") },
|
||||
{ value: 3, label: game.i18n.localize("VERMINE.reward_exceptionnal") }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
async _onRender(context, options) {
|
||||
this.element.querySelectorAll("[data-roll]").forEach(inp => inp.addEventListener("change", () => this.#updateUI()));
|
||||
this.element.querySelector("#group-select")?.addEventListener("change", () => this.#updateGroupFilter());
|
||||
this.#updateUI();
|
||||
this.#updateGroupFilter();
|
||||
}
|
||||
|
||||
#updateUI() {
|
||||
const danger = parseInt(this.element.querySelector("#collective-danger")?.value, 10) || 0;
|
||||
const discoveries = parseInt(this.element.querySelector("#collective-discoveries")?.value, 10) || 0;
|
||||
const duration = parseInt(this.element.querySelector("#collective-duration")?.value, 10) || 0;
|
||||
const el = this.element.querySelector("#collective-total");
|
||||
if (el) el.textContent = `${danger + discoveries + duration}D`;
|
||||
}
|
||||
|
||||
/** Filtre les personnages selon le groupe sélectionné (vide = tous). */
|
||||
#updateGroupFilter() {
|
||||
const groupId = this.element.querySelector("#group-select")?.value || "";
|
||||
for (const row of this.element.querySelectorAll("[data-character-id]")) {
|
||||
const show = !groupId || row.dataset.groupId === groupId;
|
||||
row.hidden = !show;
|
||||
}
|
||||
}
|
||||
|
||||
static async #onCancel(event, target) {
|
||||
this.close();
|
||||
}
|
||||
|
||||
static async #onApply(event, target) {
|
||||
const collective = (parseInt(this.element.querySelector("#collective-danger")?.value, 10) || 0)
|
||||
+ (parseInt(this.element.querySelector("#collective-discoveries")?.value, 10) || 0)
|
||||
+ (parseInt(this.element.querySelector("#collective-duration")?.value, 10) || 0);
|
||||
|
||||
const groupId = this.element.querySelector("#group-select")?.value || null;
|
||||
if (groupId) {
|
||||
const group = game.actors.get(groupId);
|
||||
if (group && collective > 0) {
|
||||
const current = group.system?.experience?.dice || 0;
|
||||
const phase = (group.system?.experience?.phase || 0) + 1;
|
||||
await group.update({
|
||||
"system.experience.dice": current + collective,
|
||||
"system.experience.phase": phase,
|
||||
"system.experience.learned.phase": phase,
|
||||
"system.experience.learned.level": false,
|
||||
"system.experience.learned.reserve": false,
|
||||
"system.experience.learned.ability": false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const row of this.element.querySelectorAll("[data-character-id]")) {
|
||||
// Quand un groupe précis est choisi, ne récompenser que ses membres.
|
||||
if (groupId && row.dataset.groupId !== groupId) continue;
|
||||
const id = row.dataset.characterId;
|
||||
const actor = game.actors.get(id);
|
||||
if (!actor) continue;
|
||||
const implication = parseInt(row.querySelector(`[name="implication-${id}"]`)?.value, 10) || 0;
|
||||
const influence = parseInt(row.querySelector(`[name="influence-${id}"]`)?.value, 10) || 0;
|
||||
const interpretation = parseInt(row.querySelector(`[name="interpretation-${id}"]`)?.value, 10) || 0;
|
||||
const objectiveMinor = row.querySelector(`[name="objective-minor-${id}"]`)?.checked ? 3 : 0;
|
||||
const objectiveMajor = row.querySelector(`[name="objective-major-${id}"]`)?.checked ? 5 : 0;
|
||||
const trauma = row.querySelector(`[name="trauma-${id}"]`)?.checked ? 5 : 0;
|
||||
|
||||
const individual = implication + influence + interpretation;
|
||||
const bonus = objectiveMinor + objectiveMajor + trauma;
|
||||
const gained = collective + individual + bonus;
|
||||
|
||||
const ex = actor.system?.experience;
|
||||
const nextPhase = (ex?.phase || 0) + 1;
|
||||
const update = {
|
||||
"system.experience.phase": nextPhase,
|
||||
"system.experience.dice": (ex?.dice || 0) + gained,
|
||||
"system.experience.collective.danger": parseInt(this.element.querySelector("#collective-danger")?.value, 10) || 0,
|
||||
"system.experience.collective.discoveries": parseInt(this.element.querySelector("#collective-discoveries")?.value, 10) || 0,
|
||||
"system.experience.collective.duration": parseInt(this.element.querySelector("#collective-duration")?.value, 10) || 0,
|
||||
"system.experience.individual.implication": implication,
|
||||
"system.experience.individual.influence": influence,
|
||||
"system.experience.individual.interpretation": interpretation,
|
||||
"system.experience.traumaReward": trauma > 0,
|
||||
"system.experience.learned.phase": nextPhase,
|
||||
"system.experience.learned.skill": false,
|
||||
"system.experience.learned.specialty": false,
|
||||
"system.experience.learned.reserve": false,
|
||||
"system.experience.learned.characteristic": false,
|
||||
"system.experience.learned.evolution": false,
|
||||
"system.experience.learned.ability": false,
|
||||
"system.experience.learned.background": false,
|
||||
"system.experience.learned.reputation": false
|
||||
};
|
||||
await actor.update(update);
|
||||
results.push({ name: actor.name, gained });
|
||||
}
|
||||
|
||||
const lines = [`${game.i18n.localize("VERMINE.experience_phase")} +1`];
|
||||
if (groupId) {
|
||||
const group = game.actors.get(groupId);
|
||||
lines.push(`${group?.name ?? ""} (${game.i18n.localize("VERMINE.collective")}): +${collective}D`);
|
||||
}
|
||||
for (const r of results) {
|
||||
lines.push(`${r.name}: +${r.gained}D`);
|
||||
}
|
||||
await ChatMessage.create({
|
||||
user: game.user?._id,
|
||||
speaker: ChatMessage.getSpeaker(),
|
||||
content: `<div class="vermine-roll-message"><h3>${game.i18n.localize("VERMINE.experience_rewards")}</h3><div class="roll-context flexrow flex-wrap">${lines.map(l => `<span class="context-item">${l}</span>`).join("")}</div></div>`
|
||||
});
|
||||
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import { VermineUtils } from '../roll.mjs';
|
||||
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
/**
|
||||
* Dialogue d'apprentissage de Groupe (phase d'Expérience).
|
||||
* Permet de dépenser des Dés d'Expérience du Groupe pour tenter d'améliorer
|
||||
* le Niveau de Groupe, la Réserve de Groupe ou de développer une Capacité
|
||||
* collective. La progression est appliquée automatiquement en cas de réussite.
|
||||
*/
|
||||
export default class GroupLearningDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
||||
#actor;
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ['vermine-roll'],
|
||||
tag: 'form',
|
||||
window: {
|
||||
icon: 'fas fa-graduation-cap',
|
||||
resizable: false,
|
||||
},
|
||||
position: {
|
||||
width: 560,
|
||||
height: 560,
|
||||
},
|
||||
actions: {
|
||||
roll: GroupLearningDialog.#onRoll,
|
||||
cancel: GroupLearningDialog.#onCancel,
|
||||
},
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
main: { template: 'systems/vermine2047/templates/dialogs/group-learning-dialog.hbs', scrollable: [''] },
|
||||
};
|
||||
|
||||
static async create({ actorId }) {
|
||||
const actor = await game.actors.get(actorId);
|
||||
if (!actor) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'));
|
||||
return null;
|
||||
}
|
||||
return new GroupLearningDialog({ actor });
|
||||
}
|
||||
|
||||
constructor(options = {}) {
|
||||
super(options);
|
||||
this.#actor = options.actor;
|
||||
}
|
||||
|
||||
get title() {
|
||||
return game.i18n.localize('VERMINE.open_learning');
|
||||
}
|
||||
|
||||
_prepareContext() {
|
||||
const actor = this.#actor;
|
||||
return {
|
||||
actor,
|
||||
system: actor.system,
|
||||
dice: actor.system?.experience?.dice || 0,
|
||||
abilities: actor.itemTypes.ability
|
||||
.filter((i) => i.system.type !== 'totem' && !i.system.learned)
|
||||
.map((a) => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
threshold: a.system?.learn?.threshold ?? 5,
|
||||
level: a.system?.level?.value ?? 1,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
_onRender() {
|
||||
this.element.dataset.actorId = this.#actor.id;
|
||||
for (const inp of this.element.querySelectorAll('[data-roll]')) {
|
||||
inp.addEventListener('change', () => this.#updateUI());
|
||||
}
|
||||
this.element.querySelector('#learning-dice')?.addEventListener('input', () => this.#updateUI());
|
||||
this.#updateUI();
|
||||
}
|
||||
|
||||
get #el() { return this.element; }
|
||||
|
||||
#getType() {
|
||||
return this.#el.querySelector('input[name="learning-type"]:checked')?.value ?? 'level';
|
||||
}
|
||||
|
||||
#getAbilityId() {
|
||||
return this.#el.querySelector('#learning-ability')?.value ?? '';
|
||||
}
|
||||
|
||||
#getDiceSpent() {
|
||||
return Math.max(1, parseInt(this.#el.querySelector('#learning-dice')?.value, 10) || 1);
|
||||
}
|
||||
|
||||
#compute() {
|
||||
const actor = this.#actor;
|
||||
const type = this.#getType();
|
||||
const key = type === 'ability' ? this.#getAbilityId() : '';
|
||||
let difficulty = 7;
|
||||
let handicap = 0;
|
||||
let targetLevel = null;
|
||||
let targetLabel = '';
|
||||
|
||||
if (type === 'level') {
|
||||
const current = actor.system?.level?.value || 1;
|
||||
difficulty = actor.system?.members?.length || 0;
|
||||
handicap = current;
|
||||
targetLevel = current + 1;
|
||||
} else if (type === 'reserve') {
|
||||
const current = actor.system?.reserve?.value || 0;
|
||||
difficulty = current;
|
||||
handicap = 2;
|
||||
targetLevel = current + 1;
|
||||
} else {
|
||||
const ability = key ? actor.items.get(key) : null;
|
||||
if (ability) {
|
||||
difficulty = ability.system?.learn?.threshold ?? 5;
|
||||
handicap = ability.system?.level?.value ?? 1;
|
||||
targetLabel = ability.name;
|
||||
}
|
||||
}
|
||||
|
||||
const labels = {
|
||||
level: 'VERMINE.learn_level',
|
||||
reserve: 'VERMINE.learn_group_reserve',
|
||||
ability: 'VERMINE.learn_ability',
|
||||
};
|
||||
return {
|
||||
type,
|
||||
key,
|
||||
targetLevel,
|
||||
difficulty,
|
||||
handicap,
|
||||
poolModifier: 0,
|
||||
label: game.i18n.localize(labels[type] || labels.level),
|
||||
targetLabel,
|
||||
};
|
||||
}
|
||||
|
||||
#updateUI() {
|
||||
const type = this.#getType();
|
||||
const show = (id, visible) => {
|
||||
const el = this.#el.querySelector(id);
|
||||
if (el) el.style.display = visible ? '' : 'none';
|
||||
};
|
||||
show('#learning-level-section', type === 'level');
|
||||
show('#learning-reserve-section', type === 'reserve');
|
||||
show('#learning-ability-section', type === 'ability');
|
||||
|
||||
const c = this.#compute();
|
||||
const set = (sel, txt) => {
|
||||
const el = this.#el.querySelector(sel);
|
||||
if (el) el.textContent = txt;
|
||||
};
|
||||
set('#diff-value', c.difficulty);
|
||||
set('#handicap-value', c.handicap);
|
||||
set('#required-value', 1 + c.handicap);
|
||||
const pool = Math.max(0, this.#getDiceSpent() + c.poolModifier);
|
||||
set('#pool-value', `${pool}D${c.poolModifier ? ` (${c.poolModifier >= 0 ? '+' : ''}${c.poolModifier}D)` : ''}`);
|
||||
const poolInput = this.#el.querySelector('#learning-dice');
|
||||
if (poolInput) poolInput.max = this.#actor.system?.experience?.dice || 0;
|
||||
}
|
||||
|
||||
static #onCancel() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
/** Renvoie la clé i18n d'erreur si le jet est impossible, sinon null. */
|
||||
#getError(c) {
|
||||
const actor = this.#actor;
|
||||
const ex = actor.system?.experience;
|
||||
// Limite : un seul Niveau, une seule Réserve et une seule Capacité par phase.
|
||||
if (ex?.learned?.phase === ex?.phase && ex.learned[c.type]) {
|
||||
return 'VERMINE.error_learning_limit';
|
||||
}
|
||||
if (c.type === 'level' && c.targetLevel !== null && (actor.system?.level?.value || 0) >= 10) {
|
||||
return 'VERMINE.error_group_level_max';
|
||||
}
|
||||
if (c.type === 'reserve' && c.targetLevel !== null &&
|
||||
(actor.system?.reserve?.value || 0) >= (actor.system?.reserve?.max || 10)) {
|
||||
return 'VERMINE.error_group_reserve_max';
|
||||
}
|
||||
if (c.type === 'ability') {
|
||||
if (!c.key) {
|
||||
return 'VERMINE.no_group_ability';
|
||||
}
|
||||
const ability = c.key ? actor.items.get(c.key) : null;
|
||||
const requiredLevel = ability?.system?.level?.value ?? 1;
|
||||
if (ability && (actor.system?.level?.value || 1) < requiredLevel) {
|
||||
return 'VERMINE.error_group_level_requirement';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static async #onRoll() {
|
||||
const actor = this.#actor;
|
||||
const ex = actor.system?.experience;
|
||||
const spent = Math.min(this.#getDiceSpent(), ex?.dice || 0);
|
||||
if (spent < 1) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_experience_dice'));
|
||||
return;
|
||||
}
|
||||
|
||||
const c = this.#compute();
|
||||
const errorKey = this.#getError(c);
|
||||
if (errorKey) {
|
||||
ui.notifications.warn(game.i18n.localize(errorKey));
|
||||
return;
|
||||
}
|
||||
|
||||
const pool = Math.max(0, spent + c.poolModifier);
|
||||
const rollLabel = `${game.i18n.localize('VERMINE.open_learning')} — ${c.label}${c.targetLabel ? ` (${c.targetLabel})` : ''}`;
|
||||
|
||||
const roll = await VermineUtils.roll({
|
||||
actor,
|
||||
NoD: pool,
|
||||
Reroll: 0,
|
||||
difficulty: c.difficulty,
|
||||
handicap: c.handicap,
|
||||
rollLabel,
|
||||
poolBreakdown: { ability: 0, skill: 0, selfControl: 0, specialty: 0, help: 0, tooling: 0, group: 0, total: pool },
|
||||
messageFlags: { 'vermine-learning': { type: c.type, key: c.key, spent, success: false } },
|
||||
});
|
||||
|
||||
const required = 1 + c.handicap;
|
||||
if ((roll._total ?? 0) >= required) {
|
||||
await this.#applySuccess(c);
|
||||
}
|
||||
// Les Dés d'Expérience sont perdus que le jet soit réussi ou raté.
|
||||
await actor.update({ "system.experience.dice": Math.max(0, (actor.system.experience.dice || 0) - spent) });
|
||||
this.close();
|
||||
}
|
||||
|
||||
async #applySuccess(c) {
|
||||
const actor = this.#actor;
|
||||
const updates = {};
|
||||
if (c.type === 'level') {
|
||||
const current = actor.system.level.value || 1;
|
||||
updates['system.level.value'] = Math.min(10, current + 1);
|
||||
updates['system.experience.learned.level'] = true;
|
||||
} else if (c.type === 'reserve') {
|
||||
const current = actor.system.reserve.value || 0;
|
||||
updates['system.reserve.value'] = Math.min(actor.system.reserve.max || 10, current + 1);
|
||||
updates['system.experience.learned.reserve'] = true;
|
||||
} else {
|
||||
if (c.key) {
|
||||
await actor.updateEmbeddedDocuments('Item', [{ _id: c.key, 'system.learned': true }]);
|
||||
}
|
||||
updates['system.experience.learned.ability'] = true;
|
||||
}
|
||||
updates['system.experience.learned.phase'] = actor.system.experience.phase;
|
||||
await actor.update(updates);
|
||||
ui.notifications.info(game.i18n.localize('VERMINE.learning_success'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,544 @@
|
||||
import { VermineUtils } from "../roll.mjs";
|
||||
import { VermineExperience } from "../experience.mjs";
|
||||
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
/**
|
||||
* Dialogue d'apprentissage (phase d'Expérience).
|
||||
* Permet de dépenser des Dés d'Expérience pour tenter de développer une
|
||||
* Compétence, une Spécialité, une Caractéristique, une Réserve ou un
|
||||
* Dé d'Évolution. La progression est appliquée automatiquement en cas de
|
||||
* réussite.
|
||||
*/
|
||||
export default class LearningDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
||||
|
||||
#actor;
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["vermine-roll"],
|
||||
tag: "form",
|
||||
window: {
|
||||
icon: "fas fa-graduation-cap",
|
||||
resizable: false
|
||||
},
|
||||
position: {
|
||||
width: 560,
|
||||
height: 640
|
||||
},
|
||||
actions: {
|
||||
roll: LearningDialog.#onRoll,
|
||||
cancel: LearningDialog.#onCancel
|
||||
}
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/vermine2047/templates/dialogs/learning-dialog.hbs", scrollable: [""] }
|
||||
};
|
||||
|
||||
static async create({ actorId }) {
|
||||
const actor = await game.actors.get(actorId);
|
||||
if (!actor) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
|
||||
return null;
|
||||
}
|
||||
return new LearningDialog({ actor });
|
||||
}
|
||||
|
||||
constructor(options = {}) {
|
||||
super(options);
|
||||
this.#actor = options.actor;
|
||||
}
|
||||
|
||||
get title() {
|
||||
return game.i18n.localize("VERMINE.open_learning");
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const actor = this.#actor;
|
||||
const skills = Object.entries(CONFIG.VERMINE.skills).map(([key, cfg]) => ({
|
||||
key,
|
||||
name: game.i18n.localize("SKILLS." + key + ".name"),
|
||||
category: cfg.category,
|
||||
value: actor.system?.skills?.[key]?.value || 0,
|
||||
target: Math.min(5, (actor.system?.skills?.[key]?.value || 0) + 1),
|
||||
rarity: actor.system?.skills?.[key]?.rarity || 0,
|
||||
isDomain: VermineExperience.isPreferredDomain(actor, key)
|
||||
}));
|
||||
const totem = actor.system?.identity?.totem || "";
|
||||
const gameMode = this.#getGameMode();
|
||||
const groupLevel = this.#getGroupLevel();
|
||||
const totemAbilities = (actor.itemTypes?.ability || [])
|
||||
.filter(i =>
|
||||
i.system?.type === "character"
|
||||
&& !i.system?.learned
|
||||
&& i.system?.totem === totem
|
||||
&& (i.system?.level?.value || 0) <= gameMode
|
||||
&& groupLevel >= (i.system?.level?.value || 0)
|
||||
)
|
||||
.map(a => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
level: a.system.level.value,
|
||||
threshold: a.system.learn.threshold,
|
||||
hindrance: a.system.learn.hindrance
|
||||
}));
|
||||
const backgrounds = (actor.itemTypes?.background || [])
|
||||
.filter(i => !i.system?.learned)
|
||||
.map(b => ({ id: b.id, name: b.name, asterisk: b.system?.asterisk || false }));
|
||||
const human = actor.system?.adaptation?.totems?.human?.value || 0;
|
||||
const adapted = actor.system?.adaptation?.totems?.adapted?.value || 0;
|
||||
return {
|
||||
actor,
|
||||
system: actor.system,
|
||||
config: CONFIG.VERMINE,
|
||||
dice: actor.system?.experience?.dice || 0,
|
||||
ageType: actor.system?.identity?.ageType || 2,
|
||||
abilities: Object.entries(CONFIG.VERMINE.abilities).map(([key, label]) => ({
|
||||
key,
|
||||
label: game.i18n.localize(label),
|
||||
value: actor.system?.abilities?.[key]?.value || 0
|
||||
})),
|
||||
reserves: [
|
||||
{ key: "self_control", label: game.i18n.localize("VERMINE.self_control"), value: actor.system?.attributes?.self_control?.value || 0, max: actor.system?.attributes?.self_control?.max || 0 },
|
||||
{ key: "effort", label: game.i18n.localize("VERMINE.effort"), value: actor.system?.attributes?.effort?.value || 0, max: actor.system?.attributes?.effort?.max || 0 }
|
||||
],
|
||||
specialties: actor.itemTypes?.specialty || [],
|
||||
skills,
|
||||
specialtyCount: (actor.itemTypes?.specialty || []).length,
|
||||
totem,
|
||||
gameMode,
|
||||
groupLevel,
|
||||
totemAbilities,
|
||||
backgrounds,
|
||||
reputation: actor.system?.attributes?.reputation?.value ?? 0,
|
||||
human,
|
||||
adapted,
|
||||
canUseAdaptedDie: VermineExperience.canUseAdaptedDie(human, adapted)
|
||||
};
|
||||
}
|
||||
|
||||
#getGameMode() {
|
||||
return parseInt(game.settings.get("vermine2047", "game-mode") || "1", 10) || 1;
|
||||
}
|
||||
|
||||
#getGroupLevel() {
|
||||
const groups = game.actors.filter(a => a.type === "group" && (a.system?.members || []).includes(this.#actor.id));
|
||||
return Math.max(0, ...groups.map(g => g.system?.level?.value || 0));
|
||||
}
|
||||
|
||||
#canUseAdaptedDie() {
|
||||
const a = this.#actor.system?.adaptation?.totems;
|
||||
return VermineExperience.canUseAdaptedDie(a?.human?.value || 0, a?.adapted?.value || 0);
|
||||
}
|
||||
|
||||
#totemCapacityCounts() {
|
||||
// Ne comptabilise que les Capacités du Totem courant du personnage
|
||||
// (les Capacités liées à un autre Totem ne bloquent pas les limites).
|
||||
const totem = this.#actor.system?.identity?.totem;
|
||||
const items = (this.#actor.itemTypes?.ability || []).filter(i =>
|
||||
i.system?.type === "character" && i.system?.learned && i.system?.totem === totem);
|
||||
const byLevel = { 1: 0, 2: 0, 3: 0 };
|
||||
for (const i of items) {
|
||||
const level = i.system?.level?.value || 0;
|
||||
byLevel[level] = (byLevel[level] || 0) + 1;
|
||||
}
|
||||
return { total: items.length, byLevel };
|
||||
}
|
||||
|
||||
async _onRender() {
|
||||
this.element.dataset.actorId = this.#actor.id;
|
||||
for (const inp of this.element.querySelectorAll("[data-roll]")) {
|
||||
inp.addEventListener("change", () => this.#updateUI());
|
||||
}
|
||||
this.element.querySelector("#learning-dice")?.addEventListener("input", () => this.#updateUI());
|
||||
this.#updateUI();
|
||||
}
|
||||
|
||||
get #el() { return this.element; }
|
||||
|
||||
#getType() {
|
||||
return this.#el.querySelector('input[name="learning-type"]:checked')?.value ?? "skill";
|
||||
}
|
||||
|
||||
#getTargetKey() {
|
||||
const type = this.#getType();
|
||||
if (type === "characteristic") return this.#el.querySelector("#learning-ability")?.value ?? "";
|
||||
if (type === "reserve") return this.#el.querySelector("#learning-reserve")?.value ?? "";
|
||||
if (type === "totem") return this.#el.querySelector("#learning-totem")?.value ?? "";
|
||||
if (type === "background") return this.#el.querySelector("#learning-background")?.value ?? "";
|
||||
return this.#el.querySelector("#learning-target")?.value ?? "";
|
||||
}
|
||||
|
||||
#getReputationDirection() {
|
||||
return this.#el.querySelector("#learning-reputation-direction")?.value ?? "reduce";
|
||||
}
|
||||
|
||||
#isAdaptedDieChecked() {
|
||||
return Boolean(this.#el.querySelector("#learning-adapted-die")?.checked);
|
||||
}
|
||||
|
||||
#getDiceSpent() {
|
||||
return Math.max(1, parseInt(this.#el.querySelector("#learning-dice")?.value, 10) || 1);
|
||||
}
|
||||
|
||||
#getSpecialtyName() {
|
||||
// Le dialogue peut avoir été fermé pendant le jet (this.#el null).
|
||||
const el = this.#el?.querySelector("#specialty-name");
|
||||
return el?.value?.trim() || game.i18n.localize("ITEMS.new_specialty");
|
||||
}
|
||||
|
||||
#compute() {
|
||||
const actor = this.#actor;
|
||||
const type = this.#getType();
|
||||
const key = this.#getTargetKey();
|
||||
let label = "";
|
||||
let targetLevel = null;
|
||||
|
||||
if (type === "skill") {
|
||||
const current = key ? actor.system?.skills?.[key]?.value || 0 : 0;
|
||||
targetLevel = Math.min(5, current + 1);
|
||||
const rarity = key ? actor.system?.skills?.[key]?.rarity || 0 : 0;
|
||||
label = game.i18n.localize(VermineExperience.skillLevelLabelKey(targetLevel));
|
||||
const diff = VermineExperience.skillLearningDifficulty(targetLevel);
|
||||
const domain = key ? VermineExperience.isPreferredDomain(actor, key) : false;
|
||||
return {
|
||||
type,
|
||||
key,
|
||||
targetLevel,
|
||||
difficulty: domain ? Math.max(3, diff - 2) : diff,
|
||||
handicap: VermineExperience.skillLearningHandicap(targetLevel, rarity),
|
||||
poolModifier: 0,
|
||||
label,
|
||||
targetLabel: key ? game.i18n.localize("SKILLS." + key + ".name") : ""
|
||||
};
|
||||
}
|
||||
if (type === "specialty") {
|
||||
const current = key ? actor.system?.skills?.[key]?.value || 0 : 0;
|
||||
const domain = key ? VermineExperience.isPreferredDomain(actor, key) : false;
|
||||
label = game.i18n.localize("VERMINE.specialty");
|
||||
return {
|
||||
type,
|
||||
key,
|
||||
targetLevel: current,
|
||||
difficulty: VermineExperience.specialtyLearningDifficulty(current, domain),
|
||||
handicap: VermineExperience.specialtyLearningHandicap(actor.itemTypes?.specialty?.length || 0),
|
||||
poolModifier: 0,
|
||||
label,
|
||||
targetLabel: key ? game.i18n.localize("SKILLS." + key + ".name") : ""
|
||||
};
|
||||
}
|
||||
if (type === "characteristic") {
|
||||
const current = key ? actor.system?.abilities?.[key]?.value || 0 : 0;
|
||||
label = game.i18n.localize("VERMINE.characteristic");
|
||||
return {
|
||||
type,
|
||||
key,
|
||||
targetLevel: current,
|
||||
difficulty: VermineExperience.characteristicLearningDifficulty(),
|
||||
handicap: current,
|
||||
poolModifier: 0,
|
||||
label,
|
||||
targetLabel: key ? game.i18n.localize(CONFIG.VERMINE.abilities[key]) : ""
|
||||
};
|
||||
}
|
||||
if (type === "reserve") {
|
||||
const current = key ? actor.system?.attributes?.[key]?.value || 0 : 0;
|
||||
label = game.i18n.localize("VERMINE.reserve");
|
||||
return {
|
||||
type,
|
||||
key,
|
||||
targetLevel: current,
|
||||
difficulty: VermineExperience.reserveLearningDifficulty(current),
|
||||
handicap: VermineExperience.ageHandicap(actor.system?.identity?.ageType || 2),
|
||||
poolModifier: 0,
|
||||
adaptedDie: false,
|
||||
label,
|
||||
targetLabel: key ? game.i18n.localize(key === "self_control" ? "VERMINE.self_control" : "VERMINE.effort") : ""
|
||||
};
|
||||
}
|
||||
if (type === "totem") {
|
||||
const item = actor.itemTypes?.ability?.find(i => i.id === key);
|
||||
const data = item?.system;
|
||||
label = game.i18n.localize("VERMINE.learn_totem_ability");
|
||||
return {
|
||||
type,
|
||||
key,
|
||||
targetLevel: data?.level?.value || 0,
|
||||
difficulty: VermineExperience.totemAbilityLearningDifficulty(data),
|
||||
handicap: VermineExperience.totemAbilityLearningHandicap(data),
|
||||
poolModifier: 0,
|
||||
adaptedDie: false,
|
||||
label,
|
||||
targetLabel: item?.name || ""
|
||||
};
|
||||
}
|
||||
if (type === "background") {
|
||||
const item = actor.itemTypes?.background?.find(i => i.id === key);
|
||||
const data = item?.system;
|
||||
label = game.i18n.localize("VERMINE.learn_background");
|
||||
return {
|
||||
type,
|
||||
key,
|
||||
targetLevel: 0,
|
||||
difficulty: VermineExperience.backgroundLearningDifficulty(),
|
||||
handicap: VermineExperience.backgroundLearningHandicap(data?.asterisk),
|
||||
poolModifier: 0,
|
||||
adaptedDie: false,
|
||||
label,
|
||||
targetLabel: item?.name || ""
|
||||
};
|
||||
}
|
||||
if (type === "reputation") {
|
||||
const current = actor.system?.attributes?.reputation?.value ?? 0;
|
||||
const direction = this.#getReputationDirection();
|
||||
if (direction === "increase") {
|
||||
return {
|
||||
type,
|
||||
key: "increase",
|
||||
targetLevel: current,
|
||||
difficulty: VermineExperience.reputationIncreaseDifficulty(current),
|
||||
handicap: 0,
|
||||
poolModifier: 0,
|
||||
adaptedDie: false,
|
||||
label: game.i18n.localize("VERMINE.learn_reputation"),
|
||||
targetLabel: game.i18n.localize("VERMINE.reputation_increase")
|
||||
};
|
||||
}
|
||||
return {
|
||||
type,
|
||||
key: "reduce",
|
||||
targetLevel: Math.max(0, current - 1),
|
||||
difficulty: VermineExperience.reputationReduceDifficulty(current),
|
||||
handicap: 0,
|
||||
poolModifier: 0,
|
||||
adaptedDie: false,
|
||||
label: game.i18n.localize("VERMINE.learn_reputation"),
|
||||
targetLabel: game.i18n.localize("VERMINE.reputation_reduce")
|
||||
};
|
||||
}
|
||||
// evolution
|
||||
const human = actor.system?.adaptation?.totems?.human?.value || 0;
|
||||
const adapted = actor.system?.adaptation?.totems?.adapted?.value || 0;
|
||||
label = game.i18n.localize("VERMINE.evolution_dice");
|
||||
return {
|
||||
type,
|
||||
key,
|
||||
targetLevel: actor.system?.experience?.evolution || 0,
|
||||
difficulty: VermineExperience.evolutionDifficulty(actor.system?.identity?.ageType || 2),
|
||||
handicap: actor.system?.experience?.evolution || 0,
|
||||
poolModifier: VermineExperience.evolutionTotemModifier(human, adapted),
|
||||
adaptedDie: this.#canUseAdaptedDie() && this.#isAdaptedDieChecked(),
|
||||
label,
|
||||
targetLabel: ""
|
||||
};
|
||||
}
|
||||
|
||||
#updateUI() {
|
||||
const type = this.#getType();
|
||||
const show = (id, visible) => {
|
||||
const el = this.#el.querySelector(id);
|
||||
if (el) el.style.display = visible ? "" : "none";
|
||||
};
|
||||
show("#learning-skill-section", type === "skill" || type === "specialty");
|
||||
show("#learning-specialty-section", type === "specialty");
|
||||
show("#learning-ability-section", type === "characteristic");
|
||||
show("#learning-reserve-section", type === "reserve");
|
||||
show("#learning-evolution-section", type === "evolution");
|
||||
show("#learning-totem-section", type === "totem");
|
||||
show("#learning-background-section", type === "background");
|
||||
show("#learning-reputation-section", type === "reputation");
|
||||
show("#learning-adapted-die-row", type === "evolution" && this.#canUseAdaptedDie());
|
||||
|
||||
// Pour une Spécialité, seule une compétence au moins Confirmée (≥2) est valable.
|
||||
const skillSelect = this.#el.querySelector("#learning-target");
|
||||
if (skillSelect) {
|
||||
for (const opt of skillSelect.options) {
|
||||
const value = parseInt(opt.dataset.value, 10) || 0;
|
||||
opt.hidden = (type === "specialty") ? value < 2 : false;
|
||||
}
|
||||
}
|
||||
|
||||
const c = this.#compute();
|
||||
const set = (sel, txt) => { const el = this.#el.querySelector(sel); if (el) el.textContent = txt; };
|
||||
set("#diff-value", c.difficulty);
|
||||
set("#handicap-value", c.handicap);
|
||||
set("#required-value", 1 + c.handicap);
|
||||
const pool = Math.max(0, this.#getDiceSpent() + c.poolModifier);
|
||||
let poolText = `${pool}D${c.poolModifier ? ` (${c.poolModifier >= 0 ? "+" : ""}${c.poolModifier}D)` : ""}`;
|
||||
if (c.adaptedDie) poolText += " — " + game.i18n.localize("VERMINE.adapted_die_x2");
|
||||
set("#pool-value", poolText);
|
||||
if (type === "reputation") {
|
||||
const current = this.#actor.system?.attributes?.reputation?.value ?? 0;
|
||||
const direction = this.#getReputationDirection();
|
||||
set("#reputation-target", direction === "increase"
|
||||
? `${current} → ${Math.min(10, current + 1)}+`
|
||||
: `${current} → ${Math.max(0, current - 1)}`);
|
||||
}
|
||||
const poolInput = this.#el.querySelector("#learning-dice");
|
||||
if (poolInput) poolInput.max = this.#actor.system?.experience?.dice || 0;
|
||||
}
|
||||
|
||||
static async #onCancel() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
static async #onRoll() {
|
||||
const actor = this.#actor;
|
||||
const ex = actor.system?.experience;
|
||||
const dice = ex?.dice || 0;
|
||||
const spent = Math.min(this.#getDiceSpent(), dice);
|
||||
if (spent < 1) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_experience_dice"));
|
||||
return;
|
||||
}
|
||||
|
||||
const c = this.#compute();
|
||||
|
||||
// Limite : une seule compétence, spécialité, réserve, caractéristique,
|
||||
// un seul Dé d'Évolution, Capacité de Totem, Historique ou jet de
|
||||
// Réputation par phase.
|
||||
if (ex?.learned?.phase === ex?.phase) {
|
||||
const flagMap = { skill: "skill", specialty: "specialty", reserve: "reserve", characteristic: "characteristic", evolution: "evolution", totem: "ability", background: "background", reputation: "reputation" };
|
||||
if (ex.learned[flagMap[c.type]]) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_learning_limit"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (c.type === "skill" && c.targetLevel !== null && (actor.system?.skills?.[c.key]?.value || 0) >= 5) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_skill_max"));
|
||||
return;
|
||||
}
|
||||
if (c.type === "characteristic" && c.targetLevel !== null && (actor.system?.abilities?.[c.key]?.value || 0) >= 3) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_characteristic_max"));
|
||||
return;
|
||||
}
|
||||
if (c.type === "specialty" && c.targetLevel !== null && c.targetLevel < 2) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_specialty_requirement"));
|
||||
return;
|
||||
}
|
||||
if (c.type === "reserve" && c.targetLevel !== null && (actor.system?.attributes?.[c.key]?.max || 0) >= 10) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_reserve_max"));
|
||||
return;
|
||||
}
|
||||
if (c.type === "totem") {
|
||||
const item = actor.itemTypes?.ability?.find(i => i.id === c.key);
|
||||
if (!item || item.system?.learned) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.no_totem_ability"));
|
||||
return;
|
||||
}
|
||||
const level = item.system?.level?.value || 0;
|
||||
if (level > this.#getGameMode()) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_mode"));
|
||||
return;
|
||||
}
|
||||
if (item.system?.totem !== actor.system?.identity?.totem) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_totem"));
|
||||
return;
|
||||
}
|
||||
if (this.#getGroupLevel() < level) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_group_level"));
|
||||
return;
|
||||
}
|
||||
const counts = this.#totemCapacityCounts();
|
||||
if (level === 1 && (counts.byLevel[1] || 0) >= 3) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_limit"));
|
||||
return;
|
||||
}
|
||||
if (level >= 2 && (counts.byLevel[level] || 0) >= 1) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_limit"));
|
||||
return;
|
||||
}
|
||||
if (counts.total >= 5) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_limit"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (c.type === "background") {
|
||||
const item = actor.itemTypes?.background?.find(i => i.id === c.key);
|
||||
if (!item || item.system?.learned) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.no_background"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (c.type === "reputation") {
|
||||
const current = actor.system?.attributes?.reputation?.value ?? 0;
|
||||
if (c.key === "reduce" && current <= 0) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_reputation_min"));
|
||||
return;
|
||||
}
|
||||
if (c.key === "increase" && current >= 10) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_reputation_max"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (c.type === "evolution" && c.adaptedDie && !this.#canUseAdaptedDie()) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_evolution_no_adapted"));
|
||||
return;
|
||||
}
|
||||
|
||||
const pool = Math.max(0, spent + c.poolModifier);
|
||||
const rollLabel = `${game.i18n.localize("VERMINE.open_learning")} — ${c.label}${c.targetLabel ? " (" + c.targetLabel + ")" : ""}`;
|
||||
|
||||
const roll = await VermineUtils.roll({
|
||||
actor,
|
||||
NoD: pool,
|
||||
Reroll: 0,
|
||||
difficulty: c.difficulty,
|
||||
handicap: c.handicap,
|
||||
totems: c.adaptedDie ? { adapted: true } : { human: false, adapted: false },
|
||||
rollLabel,
|
||||
poolBreakdown: { ability: 0, skill: 0, selfControl: 0, specialty: 0, help: 0, tooling: 0, group: 0, total: pool },
|
||||
messageFlags: { "vermine-learning": { type: c.type, key: c.key, spent, success: false } }
|
||||
});
|
||||
|
||||
const required = 1 + c.handicap;
|
||||
const specialtyName = this.#getSpecialtyName();
|
||||
if ((roll._total ?? 0) >= required) {
|
||||
await this.#applySuccess(c, specialtyName, roll._total ?? 0);
|
||||
}
|
||||
// Les Dés d'Expérience sont perdus que le jet soit réussi ou raté.
|
||||
await actor.update({ "system.experience.dice": Math.max(0, (actor.system.experience.dice || 0) - spent) });
|
||||
this.close();
|
||||
}
|
||||
|
||||
async #applySuccess(c, specialtyName, successes = 0) {
|
||||
const actor = this.#actor;
|
||||
const updates = {};
|
||||
if (c.type === "skill") {
|
||||
const current = actor.system.skills[c.key].value || 0;
|
||||
updates["system.skills." + c.key + ".value"] = Math.min(5, current + 1);
|
||||
updates["system.experience.learned.skill"] = true;
|
||||
} else if (c.type === "specialty") {
|
||||
await actor.createEmbeddedDocuments("Item", [{ name: specialtyName, type: "specialty", system: { skill: c.key } }]);
|
||||
updates["system.experience.learned.specialty"] = true;
|
||||
} else if (c.type === "characteristic") {
|
||||
const current = actor.system.abilities[c.key].value || 0;
|
||||
updates["system.abilities." + c.key + ".value"] = Math.min(3, current + 1);
|
||||
updates["system.experience.learned.characteristic"] = true;
|
||||
} else if (c.type === "reserve") {
|
||||
const bonus = actor.system.experience.reserveBonus?.[c.key] || 0;
|
||||
updates["system.experience.reserveBonus." + c.key] = bonus + 1;
|
||||
updates["system.experience.learned.reserve"] = true;
|
||||
} else if (c.type === "evolution") {
|
||||
updates["system.experience.evolution"] = (actor.system.experience.evolution || 0) + 1;
|
||||
updates["system.experience.learned.evolution"] = true;
|
||||
} else if (c.type === "totem") {
|
||||
updates["system.experience.learned.ability"] = true;
|
||||
await actor.updateEmbeddedDocuments("Item", [{ _id: c.key, "system.learned": true }]);
|
||||
} else if (c.type === "background") {
|
||||
updates["system.experience.learned.background"] = true;
|
||||
await actor.updateEmbeddedDocuments("Item", [{ _id: c.key, "system.learned": true }]);
|
||||
} else if (c.type === "reputation") {
|
||||
const current = actor.system?.attributes?.reputation?.value ?? 0;
|
||||
if (c.key === "increase") {
|
||||
updates["system.attributes.reputation.value"] = Math.min(10, current + successes);
|
||||
} else {
|
||||
updates["system.attributes.reputation.value"] = Math.max(0, current - 1);
|
||||
}
|
||||
updates["system.experience.learned.reputation"] = true;
|
||||
}
|
||||
updates["system.experience.learned.phase"] = actor.system.experience.phase;
|
||||
await actor.update(updates);
|
||||
ui.notifications.info(game.i18n.localize("VERMINE.learning_success"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import { VermineUtils } from "../roll.mjs"
|
||||
import { VermineExchange } from "../exchange.mjs"
|
||||
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api
|
||||
|
||||
/**
|
||||
* Dialogue de jet simplifié pour les PNJ.
|
||||
*
|
||||
* Un PNJ n'a ni caractéristiques ni compétences : toutes ses actions sont
|
||||
* résolues par une valeur unique dérivée de son Profil (Menace / Expérience /
|
||||
* Rôle), exposée dans system.computed. Ce dialogue fixe le pool selon le type
|
||||
* de jet (Action, Spécialité, Attaque ou Réaction) et n'offre que la difficulté,
|
||||
* le handicap et les bonus simples.
|
||||
*
|
||||
* Utilisé par :
|
||||
* - la fiche PNJ (boutons de jet rapide Action / Spécialité / Réaction) ;
|
||||
* - la défense des PNJ dans les échanges de coups (mode verrouillé).
|
||||
*/
|
||||
export default class NpcRollDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
||||
|
||||
#actor
|
||||
|
||||
get title() {
|
||||
return game.i18n.localize("VERMINE.roll")
|
||||
}
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["vermine-roll"],
|
||||
tag: "form",
|
||||
window: {
|
||||
icon: "fas fa-dice-d10",
|
||||
resizable: false
|
||||
},
|
||||
position: {
|
||||
width: 520,
|
||||
height: 600
|
||||
},
|
||||
actions: {
|
||||
roll: NpcRollDialog.#onRoll,
|
||||
cancel: NpcRollDialog.#onCancel
|
||||
}
|
||||
}
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/vermine2047/templates/dialogs/npc-roll-dialog.hbs" }
|
||||
}
|
||||
|
||||
static async create(data = {}) {
|
||||
const actor = data.actor instanceof Actor ? data.actor : await game.actors.get(data.actorId)
|
||||
if (!actor) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"))
|
||||
return null
|
||||
}
|
||||
return new NpcRollDialog({
|
||||
actor,
|
||||
rolltype: data.rolltype ?? "action",
|
||||
locks: data.locks ?? null,
|
||||
defense: data.defense ?? null
|
||||
})
|
||||
}
|
||||
|
||||
constructor(options = {}) {
|
||||
super(options)
|
||||
this.#actor = options.actor
|
||||
this.rolltype = options.rolltype ?? "action"
|
||||
this.locks = options.locks ?? null
|
||||
this.defense = options.defense ?? null
|
||||
}
|
||||
|
||||
// ── Getters ──────────────────────────────────────────────────────────
|
||||
|
||||
get computed() { return this.#actor.system?.computed || {} }
|
||||
|
||||
/** Pool de dés selon le type de jet. */
|
||||
get pool() {
|
||||
const c = this.computed
|
||||
switch (this.rolltype) {
|
||||
case "specialty": return c.specialties || 0
|
||||
case "attack": return c.attack || 0
|
||||
case "reaction": return c.reaction || 0
|
||||
default: return c.action || 3
|
||||
}
|
||||
}
|
||||
|
||||
/** Réussites automatiques (bonus de Réaction du rôle). */
|
||||
get freeSuccesses() {
|
||||
return this.rolltype === "reaction" ? (this.computed.reactionBonus || 0) : 0
|
||||
}
|
||||
|
||||
/** Relances : utilisables uniquement sur les attaques ou les Spécialités. */
|
||||
get rerolls() {
|
||||
if (this.rolltype !== "specialty" && this.rolltype !== "attack") return 0
|
||||
return this.computed.rerolls || 0
|
||||
}
|
||||
|
||||
get label() {
|
||||
const keys = {
|
||||
action: "ADVERSITY.action",
|
||||
specialty: "ADVERSITY.specialties",
|
||||
attack: "ADVERSITY.attack",
|
||||
reaction: "ADVERSITY.reaction"
|
||||
}
|
||||
return game.i18n.localize(keys[this.rolltype] || "VERMINE.roll")
|
||||
}
|
||||
|
||||
// ── Rendu ────────────────────────────────────────────────────────────
|
||||
|
||||
async _prepareContext() {
|
||||
const difficultyOptions = []
|
||||
for (let d = 3; d <= 10; d++) {
|
||||
difficultyOptions.push({ difficulty: d, label: String(d) })
|
||||
}
|
||||
let lockedDifficulty = null
|
||||
let defaultDifficulty = 7
|
||||
if (this.locks?.difficulty !== undefined && this.locks?.difficulty !== null) {
|
||||
lockedDifficulty = parseInt(this.locks.difficulty, 10)
|
||||
defaultDifficulty = lockedDifficulty
|
||||
}
|
||||
return {
|
||||
actor: this.#actor,
|
||||
system: this.#actor.system,
|
||||
config: CONFIG.VERMINE,
|
||||
rollLabel: this.label,
|
||||
rolltype: this.rolltype,
|
||||
pool: this.pool,
|
||||
freeSuccesses: this.freeSuccesses,
|
||||
rerolls: this.rerolls,
|
||||
isDefense: Boolean(this.defense),
|
||||
lockedDifficulty,
|
||||
difficultyOptions,
|
||||
defaultDifficulty,
|
||||
speakerId: this.#actor.id,
|
||||
availableItems: this.#actor.items.filter(i => i.type === "item")
|
||||
}
|
||||
}
|
||||
|
||||
async _onRender(context, options) {
|
||||
this.element.dataset.actorId = this.#actor.id
|
||||
for (const inp of this.element.querySelectorAll("[data-roll]")) {
|
||||
inp.addEventListener("change", this.#onInputChange.bind(this))
|
||||
}
|
||||
this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI())
|
||||
this.#updateUI()
|
||||
}
|
||||
|
||||
get #el() { return this.element }
|
||||
|
||||
#getHandicap() {
|
||||
const sel = this.#el.querySelector("#handicap")
|
||||
return Math.max(0, (parseInt(sel?.value, 10) || 1) - 1)
|
||||
}
|
||||
|
||||
#getDifficulty() {
|
||||
const sel = this.#el.querySelector("#difficulty")
|
||||
return parseInt(sel?.value, 10) || 7
|
||||
}
|
||||
|
||||
#getHelped() { return this.#el.querySelector("#helped")?.checked ? 1 : 0 }
|
||||
|
||||
#getGroup() { return parseInt(this.#el.querySelector("#group")?.value, 10) || 0 }
|
||||
|
||||
getPoolBreakdown() {
|
||||
const help = this.#getHelped()
|
||||
const group = this.#getGroup()
|
||||
const malus = VermineExchange.woundMalus(this.#actor)
|
||||
return { pool: this.pool, help, group, malus, total: Math.max(0, this.pool + help + group - malus) }
|
||||
}
|
||||
|
||||
#updateUI() {
|
||||
const b = this.getPoolBreakdown()
|
||||
const totalEl = this.#el.querySelector("#dice-pool-total")
|
||||
if (totalEl) totalEl.textContent = `${b.total}D`
|
||||
const bonusEl = this.#el.querySelector("#total-bonus")
|
||||
if (bonusEl) bonusEl.textContent = b.help + b.group - b.malus
|
||||
const freeEl = this.#el.querySelector("#free-successes")
|
||||
if (freeEl) freeEl.textContent = String(this.freeSuccesses)
|
||||
}
|
||||
|
||||
#onInputChange() {
|
||||
this.#updateUI()
|
||||
}
|
||||
|
||||
// ── Actions ──────────────────────────────────────────────────────────
|
||||
|
||||
static async #onCancel(event, target) {
|
||||
this.close()
|
||||
}
|
||||
|
||||
static async #onRoll(event, target) {
|
||||
const label = this.defense
|
||||
? (this.defense.type === "parade"
|
||||
? game.i18n.localize("VERMINE.defense_parade")
|
||||
: game.i18n.localize("VERMINE.defense_esquive"))
|
||||
: this.label
|
||||
|
||||
const rollParams = {
|
||||
actor: this.#actor,
|
||||
NoD: this.getPoolBreakdown().total,
|
||||
Reroll: this.rerolls,
|
||||
difficulty: this.#getDifficulty(),
|
||||
handicap: this.#getHandicap(),
|
||||
rollLabel: label,
|
||||
bonusSuccesses: this.freeSuccesses,
|
||||
poolBreakdown: this.getPoolBreakdown(),
|
||||
messageFlags: this.defense ? { "vermine-defense": this.defense } : null
|
||||
}
|
||||
|
||||
// Fermeture immédiate du dialogue : le jet (et l'animation 3D) continue
|
||||
// en arrière-plan sans bloquer la fermeture.
|
||||
this.close()
|
||||
|
||||
await VermineUtils.roll(rollParams)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { VermineUtils } from "../roll.mjs";
|
||||
import { VermineExchange } from "../exchange.mjs";
|
||||
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
@@ -23,7 +24,8 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
},
|
||||
actions: {
|
||||
roll: RollDialog.#onRoll,
|
||||
cancel: RollDialog.#onCancel
|
||||
cancel: RollDialog.#onCancel,
|
||||
giveToGroup: RollDialog.#onGiveToGroup
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,6 +34,9 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
};
|
||||
|
||||
static async create(data = {}) {
|
||||
if (data.actor instanceof Actor) {
|
||||
return new RollDialog({ actor: data.actor, label: data.label, rolltype: data.rolltype, weapon: data.weapon, locks: data.locks, defense: data.defense });
|
||||
}
|
||||
const actorId = data.actorId ?? game.user.character?.id ?? canvas.tokens.controlled[0]?.actor?.id;
|
||||
if (!actorId || typeof actorId !== "string") {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
|
||||
@@ -42,7 +47,7 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
|
||||
return null;
|
||||
}
|
||||
return new RollDialog({ actor, label: data.label, rolltype: data.rolltype });
|
||||
return new RollDialog({ actor, label: data.label, rolltype: data.rolltype, weapon: data.weapon, locks: data.locks, defense: data.defense });
|
||||
}
|
||||
|
||||
constructor(options = {}) {
|
||||
@@ -50,10 +55,16 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
this.#actor = options.actor;
|
||||
this.label = options.label ?? null;
|
||||
this.rolltype = options.rolltype ?? null;
|
||||
this.weapon = options.weapon ?? null;
|
||||
this.locks = options.locks ?? null;
|
||||
this.defense = options.defense ?? null;
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const actor = this.#actor;
|
||||
const isCharacter = actor.type === "character";
|
||||
const group = isCharacter ? RollDialog.#findGroup(actor) : null;
|
||||
const groupPool = group?.system?.experience?.dice || 0;
|
||||
return {
|
||||
actor,
|
||||
system: actor.system,
|
||||
@@ -66,13 +77,28 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
help: false,
|
||||
specialty: false,
|
||||
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
|
||||
availableItems: actor.items.filter(i => i.type === "item")
|
||||
availableItems: actor.items.filter(i => i.type === "item"),
|
||||
locks: this.locks,
|
||||
experience: {
|
||||
hasExperience: isCharacter,
|
||||
individual: isCharacter ? actor.system?.experience?.dice || 0 : 0,
|
||||
groupPool,
|
||||
pullOptions: [0, 1, 2].filter(v => v <= Math.min(2, groupPool)),
|
||||
canPull: Boolean(group) && (game.user.isGM || group.isOwner),
|
||||
canGive: isCharacter && (game.user.isGM || actor.isOwner)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async _onRender(context, options) {
|
||||
this.element.dataset.actorId = this.#actor.id;
|
||||
|
||||
// Mode verrouillé (défense en échange de coups) : présélectionne et bloque
|
||||
// caractéristique + difficulté, présélectionne la compétence.
|
||||
if (this.locks) {
|
||||
this.#applyLocks(this.locks);
|
||||
}
|
||||
|
||||
for (const inp of this.element.querySelectorAll("[data-roll]")) {
|
||||
inp.addEventListener("change", this.#onInputChange.bind(this));
|
||||
}
|
||||
@@ -95,6 +121,7 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
this.element.querySelector("#adapted-totem")?.addEventListener("change", () => this.#updateUI());
|
||||
|
||||
this.#displaySpecialties();
|
||||
this.#refreshExperienceUI();
|
||||
this.#updateUI();
|
||||
|
||||
if (ability?.value !== "0") {
|
||||
@@ -111,19 +138,45 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
#getDifficulty() { return this.#el.querySelector("#difficulty"); }
|
||||
#getHandicap() { return this.#el.querySelector("#handicap"); }
|
||||
#getSelfCtrl() { return this.#el.querySelector("#self_control"); }
|
||||
#getExperiencePull() { return parseInt(this.#el.querySelector("#experience-pull")?.value, 10) || 0; }
|
||||
|
||||
getDicePool() {
|
||||
/**
|
||||
* Trouve le premier Groupe dont le personnage est membre.
|
||||
* @param {Actor} actor
|
||||
* @returns {Actor|null}
|
||||
*/
|
||||
static #findGroup(actor) {
|
||||
return game.actors.filter(a => a.type === "group" && (a.system?.members ?? []).includes(actor.id))[0] ?? null;
|
||||
}
|
||||
|
||||
/** Décomposition du pool de dés pour affichage dans le message. */
|
||||
getPoolBreakdown() {
|
||||
const abil = this.#getAbility();
|
||||
const abilVal = parseInt(abil?.options[abil?.selectedIndex]?.value, 10) || 0;
|
||||
const ability = parseInt(abil?.options[abil?.selectedIndex]?.value, 10) || 0;
|
||||
const skill = this.#getSkill();
|
||||
const skillPool = parseInt(skill?.options[skill?.selectedIndex]?.dataset?.pool, 10) || 0;
|
||||
const sc = parseInt(this.#getSelfCtrl()?.value, 10) || 0;
|
||||
const specChecked = this.#el.querySelector("#usingSpecialization")?.checked;
|
||||
const selfControl = parseInt(this.#getSelfCtrl()?.value, 10) || 0;
|
||||
const specChecked = this.hasSpecialtySelected();
|
||||
const helped = this.#el.querySelector("#helped")?.checked;
|
||||
const tools = this.#el.querySelector("input[name='usingTools']:checked")?.value !== "0";
|
||||
const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked");
|
||||
const tools = toolsChecked && toolsChecked.value !== "0";
|
||||
const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
|
||||
const bonuses = (specChecked ? 1 : 0) + (helped ? 1 : 0) + (tools ? 1 : 0) + group;
|
||||
return (abilVal + sc + skillPool + bonuses) || 0;
|
||||
const experience = this.#getExperiencePull();
|
||||
const specialty = specChecked ? 1 : 0;
|
||||
const help = helped ? 1 : 0;
|
||||
const tooling = tools ? 1 : 0;
|
||||
const malus = VermineExchange.woundMalus(this.#actor);
|
||||
const total = Math.max(0, ability + skillPool + selfControl + specialty + help + tooling + group + experience - malus);
|
||||
return { ability, skill: skillPool, selfControl, specialty, help, tooling, group, experience, malus, total };
|
||||
}
|
||||
|
||||
getDicePool() {
|
||||
return this.getPoolBreakdown().total;
|
||||
}
|
||||
|
||||
getSpecialtyName() {
|
||||
const checked = this.#el.querySelector("input[name='usingSpecialization']:checked");
|
||||
return checked && checked.value !== "aucune" ? checked.value : null;
|
||||
}
|
||||
|
||||
getDifficultySelect() {
|
||||
@@ -200,6 +253,40 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
|
||||
// ── UI ───────────────────────────────────────────────────────────────
|
||||
|
||||
#applyLocks(locks) {
|
||||
const ability = this.#getAbility();
|
||||
if (ability && locks.ability) {
|
||||
const opt = [...ability.options].find(o => o.dataset.label === locks.ability);
|
||||
if (opt) {
|
||||
opt.selected = true;
|
||||
ability.disabled = true;
|
||||
const scoreEl = this.#el.querySelector("#abilityScore");
|
||||
if (scoreEl) scoreEl.value = opt.value;
|
||||
const sc = this.#getSelfCtrl();
|
||||
if (sc) sc.max = opt.value;
|
||||
}
|
||||
}
|
||||
const skill = this.#getSkill();
|
||||
if (skill && locks.skill) {
|
||||
const opt = [...skill.options].find(o => o.dataset.label === locks.skill);
|
||||
if (opt) opt.selected = true;
|
||||
}
|
||||
const difficulty = this.#getDifficulty();
|
||||
if (difficulty && locks.difficulty !== undefined && locks.difficulty !== null) {
|
||||
const locked = parseInt(locks.difficulty, 10);
|
||||
let opt = [...difficulty.options].find(o => parseInt(o.value, 10) === locked);
|
||||
if (!opt) {
|
||||
// Difficulté hors des options (ex: 9 + 2) : ajoute une option dédiée.
|
||||
opt = document.createElement("option");
|
||||
opt.value = String(locked);
|
||||
opt.textContent = `${game.i18n.localize("VERMINE.difficulty")} (${locked})`;
|
||||
difficulty.appendChild(opt);
|
||||
}
|
||||
opt.selected = true;
|
||||
difficulty.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
#displaySpecialties() {
|
||||
for (const el of this.#el.querySelectorAll("[data-spec-skill]")) {
|
||||
el.style.display = "inline";
|
||||
@@ -207,14 +294,32 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
}
|
||||
|
||||
#calculateBonusCount() {
|
||||
let b = 0;
|
||||
if (this.#el.querySelector("#helped")?.checked) b += 1;
|
||||
b += parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
|
||||
b += parseInt(this.#getSelfCtrl()?.value, 10) || 0;
|
||||
const tools = this.#el.querySelector("input[name='usingTools']:checked");
|
||||
if (tools && tools.value !== "0") b += 1;
|
||||
if (this.hasSpecialtySelected()) b += 1;
|
||||
return b;
|
||||
const b = this.getPoolBreakdown();
|
||||
return b.specialty + b.help + b.tooling + b.group + b.selfControl + b.experience - b.malus;
|
||||
}
|
||||
|
||||
#refreshExperienceUI() {
|
||||
const actor = this.#actor;
|
||||
const group = RollDialog.#findGroup(actor);
|
||||
const individual = actor.system?.experience?.dice || 0;
|
||||
const groupPool = group?.system?.experience?.dice || 0;
|
||||
const indEl = this.#el.querySelector("#exp-individual");
|
||||
if (indEl) indEl.textContent = individual;
|
||||
const grpEl = this.#el.querySelector("#exp-group");
|
||||
if (grpEl) grpEl.textContent = groupPool;
|
||||
const pull = this.#el.querySelector("#experience-pull");
|
||||
if (pull) {
|
||||
const max = Math.min(2, groupPool);
|
||||
const current = Math.min(parseInt(pull.value, 10) || 0, max);
|
||||
pull.innerHTML = "";
|
||||
for (let i = 0; i <= max; i++) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = String(i);
|
||||
opt.textContent = `${i}D`;
|
||||
if (i === current) opt.selected = true;
|
||||
pull.appendChild(opt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#updateUI() {
|
||||
@@ -281,6 +386,29 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
this.close();
|
||||
}
|
||||
|
||||
static async #onGiveToGroup(event, target) {
|
||||
const actor = this.#actor;
|
||||
if (actor.type !== "character") return;
|
||||
const individual = actor.system?.experience?.dice || 0;
|
||||
if (individual < 1) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_experience_dice"));
|
||||
return;
|
||||
}
|
||||
if (!game.user.isGM && !actor.isOwner) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_permission"));
|
||||
return;
|
||||
}
|
||||
const group = RollDialog.#findGroup(actor);
|
||||
if (!group) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_group"));
|
||||
return;
|
||||
}
|
||||
const groupPool = group.system?.experience?.dice || 0;
|
||||
await actor.update({ "system.experience.dice": individual - 1 });
|
||||
await group.update({ "system.experience.dice": groupPool + 1 });
|
||||
this.#refreshExperienceUI();
|
||||
}
|
||||
|
||||
static async #onRoll(event, target) {
|
||||
const selfCtrl = this.getSelfControl();
|
||||
if (selfCtrl > 0) {
|
||||
@@ -302,7 +430,27 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
await this.#actor.update({ "system.attributes.self_control.value": newVal });
|
||||
}
|
||||
|
||||
await VermineUtils.roll({
|
||||
// Transfert d'Expérience : puiser dans la Réserve de Groupe (1-2D).
|
||||
const pulled = this.#getExperiencePull();
|
||||
if (pulled > 0) {
|
||||
const group = RollDialog.#findGroup(this.#actor);
|
||||
if (!group) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_group"));
|
||||
return;
|
||||
}
|
||||
if (!game.user.isGM && !group.isOwner) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_permission"));
|
||||
return;
|
||||
}
|
||||
const groupPool = group.system?.experience?.dice || 0;
|
||||
if (groupPool < pulled) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_group_dice"));
|
||||
return;
|
||||
}
|
||||
await group.update({ "system.experience.dice": groupPool - pulled });
|
||||
}
|
||||
|
||||
const rollParams = {
|
||||
actor: this.#actor,
|
||||
NoD: this.getDicePool(),
|
||||
Reroll: this.getReroll(),
|
||||
@@ -315,10 +463,19 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
keepTotem: this.getKeepTotem(),
|
||||
skillCategory: this.getSkillCategory(),
|
||||
skillLevel: this.getSkillLevel(),
|
||||
hasSpecialty: this.hasSpecialtySelected()
|
||||
});
|
||||
hasSpecialty: this.hasSpecialtySelected(),
|
||||
poolBreakdown: this.getPoolBreakdown(),
|
||||
specialtyName: this.getSpecialtyName(),
|
||||
weapon: this.weapon?.toObject() ?? null,
|
||||
targets: [...game.user.targets].map(t => t.name),
|
||||
messageFlags: this.defense ? { "vermine-defense": this.defense } : null
|
||||
};
|
||||
|
||||
// Fermeture immédiate du dialogue : le jet (et l'animation 3D) continue
|
||||
// en arrière-plan sans bloquer la fermeture.
|
||||
this.close();
|
||||
|
||||
await VermineUtils.roll(rollParams);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { VermineTotemDice } from "../totem-dice.mjs";
|
||||
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
/**
|
||||
* Dialogue de gestion des Dés de Totems (Humain / Adapté).
|
||||
* Permet d'appliquer les gains et pertes liés aux Instincts et Interdits
|
||||
* (1D ou 2D selon la gravité de l'acte), en respectant les limites
|
||||
* (3 Dés du même Totem, 5 Dés au maximum) et les règles d'échange.
|
||||
*/
|
||||
export default class TotemDiceDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
||||
|
||||
#actor;
|
||||
#serious = false;
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["vermine-roll", "totem-dice"],
|
||||
tag: "form",
|
||||
window: {
|
||||
icon: "fas fa-dice-d10",
|
||||
resizable: false
|
||||
},
|
||||
position: {
|
||||
width: 480,
|
||||
height: 420
|
||||
},
|
||||
actions: {
|
||||
gainHuman: TotemDiceDialog.#onGainHuman,
|
||||
gainAdapted: TotemDiceDialog.#onGainAdapted,
|
||||
loseHuman: TotemDiceDialog.#onLoseHuman,
|
||||
close: TotemDiceDialog.#onClose
|
||||
}
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/vermine2047/templates/dialogs/totem-dice-dialog.hbs", scrollable: [""] }
|
||||
};
|
||||
|
||||
static async create({ actorId }) {
|
||||
const actor = await game.actors.get(actorId);
|
||||
if (!actor) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
|
||||
return null;
|
||||
}
|
||||
return new TotemDiceDialog({ actor });
|
||||
}
|
||||
|
||||
constructor(options = {}) {
|
||||
super(options);
|
||||
this.#actor = options.actor;
|
||||
}
|
||||
|
||||
get title() {
|
||||
return game.i18n.localize("VERMINE.totem_dice");
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const actor = this.#actor;
|
||||
const influence = VermineTotemDice.getInfluence(actor);
|
||||
const influenceLabels = {
|
||||
human: game.i18n.localize("TOTEMS.human.name"),
|
||||
adapted: game.i18n.localize("TOTEMS.adapted.name"),
|
||||
neutral: game.i18n.localize("VERMINE.totem_neutral")
|
||||
};
|
||||
return {
|
||||
humanValue: actor.system?.adaptation?.totems?.human?.value ?? 0,
|
||||
humanMax: actor.system?.adaptation?.totems?.human?.max ?? 3,
|
||||
adaptedValue: actor.system?.adaptation?.totems?.adapted?.value ?? 0,
|
||||
adaptedMax: actor.system?.adaptation?.totems?.adapted?.max ?? 3,
|
||||
influenceLabel: influenceLabels[influence] || "",
|
||||
serious: this.#serious
|
||||
};
|
||||
}
|
||||
|
||||
_onRender(context, options) {
|
||||
super._onRender(context, options);
|
||||
const checkbox = this.element.querySelector("#totem-serious");
|
||||
if (checkbox) {
|
||||
checkbox.checked = this.#serious;
|
||||
checkbox.addEventListener("change", () => { this.#serious = checkbox.checked; });
|
||||
}
|
||||
}
|
||||
|
||||
#getCount() {
|
||||
return this.element.querySelector("#totem-serious")?.checked ? 2 : 1;
|
||||
}
|
||||
|
||||
#notify(result) {
|
||||
const key = result.totem === "human" ? "TOTEMS.human.name" : "TOTEMS.adapted.name";
|
||||
const totemName = game.i18n.localize(key);
|
||||
const parts = [];
|
||||
if (result.gained) parts.push(game.i18n.localize("VERMINE.totem_gained").replace("{totem}", totemName));
|
||||
if (result.exchanged) parts.push(game.i18n.localize("VERMINE.totem_exchanged").replace("{totem}", totemName));
|
||||
if (result.replaced) parts.push(game.i18n.localize("VERMINE.totem_replaced").replace("{totem}", totemName));
|
||||
if (result.lost) parts.push(game.i18n.localize("VERMINE.totem_lost"));
|
||||
if (result.gainedAdapted) parts.push(game.i18n.localize("VERMINE.totem_gained_adapted"));
|
||||
if (result.refused) parts.push(game.i18n.localize("VERMINE.totem_limit_reached"));
|
||||
const text = parts.join(" ");
|
||||
if (result.refused && !result.gained && !result.lost && !result.exchanged && !result.replaced) {
|
||||
ui.notifications.warn(text || game.i18n.localize("VERMINE.totem_limit_reached"));
|
||||
} else if (text) {
|
||||
ui.notifications.info(text);
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
||||
static async #onGainHuman() {
|
||||
const result = await VermineTotemDice.gainDie(this.#actor, "human", this.#getCount());
|
||||
this.#notify(result);
|
||||
}
|
||||
|
||||
static async #onGainAdapted() {
|
||||
const result = await VermineTotemDice.gainDie(this.#actor, "adapted", this.#getCount());
|
||||
this.#notify(result);
|
||||
}
|
||||
|
||||
static async #onLoseHuman() {
|
||||
const result = await VermineTotemDice.loseDie(this.#actor, "human", this.#getCount());
|
||||
this.#notify(result);
|
||||
}
|
||||
|
||||
static #onClose() {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,549 @@
|
||||
/**
|
||||
* Moteur de résolution des échanges de coups.
|
||||
*
|
||||
* Gère l'attaque (contact / distance), la défense (esquive / parade),
|
||||
* le calcul des dégâts, la déduction des protections et l'application
|
||||
* des blessures pour les personnages, PNJ et créatures.
|
||||
*
|
||||
* Le flux est assisté : les cartes de chat calculent et affichent tout,
|
||||
* un bouton "Appliquer" coche la blessure sur l'acteur cible.
|
||||
*
|
||||
* Liens entre messages (flags "world") :
|
||||
* - "vermine-exchange" sur le message d'attaque
|
||||
* - "vermine-defense" sur le message de défense
|
||||
*/
|
||||
|
||||
const FLAG_EXCHANGE = 'vermine-exchange'
|
||||
const FLAG_DEFENSE = 'vermine-defense'
|
||||
|
||||
export class VermineExchange {
|
||||
|
||||
// ── Tables de difficulté ────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Capacité d'attaque selon le mode.
|
||||
* @param {string} mode 'contact' ou 'ranged'
|
||||
* @returns {string} clé de capacité (vigor / precision)
|
||||
*/
|
||||
static attackAbility(mode) {
|
||||
return mode === 'ranged' ? 'precision' : 'vigor'
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficultés d'attaque au contact selon le niveau de compétence (0-5).
|
||||
* Incompétent/Débutant = 7, Confirmé = 5/7, Expert = 5/7/9,
|
||||
* Maître/Légende = 3/5/7/9.
|
||||
* @param {number} skillLevel valeur de compétence (0-5)
|
||||
* @returns {number[]}
|
||||
*/
|
||||
static contactDifficultyOptions(skillLevel) {
|
||||
switch (parseInt(skillLevel, 10) || 0) {
|
||||
case 2: return [5, 7]
|
||||
case 3: return [5, 7, 9]
|
||||
case 4:
|
||||
case 5: return [3, 5, 7, 9]
|
||||
default: return [7]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficultés d'attaque à distance par portée.
|
||||
* @returns {{key: string, label: string, difficulty: number}[]}
|
||||
*/
|
||||
static rangeDifficultyOptions() {
|
||||
return [
|
||||
{ key: 'short', label: 'VERMINE.range_short', difficulty: 5 },
|
||||
{ key: 'mid', label: 'VERMINE.range_mid', difficulty: 7 },
|
||||
{ key: 'long', label: 'VERMINE.range_long', difficulty: 9 }
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficultés proposées au PNJ selon son niveau d'expérience
|
||||
* (champ "contact" : "7", "5 ou 7", "5,7 ou 9", "3,5,7 ou 9").
|
||||
* @param {Actor} actor
|
||||
* @returns {number[]}
|
||||
*/
|
||||
static npcDifficultyOptions(actor) {
|
||||
const level = actor.system?.experience?.value || 1
|
||||
const cfg = CONFIG.VERMINE.npcExperienceLevels?.[level] || {}
|
||||
const raw = cfg.contact || '7'
|
||||
return raw.split(',').flatMap(part => part.trim().split(/\s+ou\s+/).map(Number))
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode d'attaque déduit de la compétence de l'arme.
|
||||
* @param {Item|null} weapon
|
||||
* @returns {string} 'contact' ou 'ranged'
|
||||
*/
|
||||
static modeForWeapon(weapon) {
|
||||
const skill = weapon?.system?.skill || ''
|
||||
return ['firearms', 'archery', 'throwing'].includes(skill) ? 'ranged' : 'contact'
|
||||
}
|
||||
|
||||
// ── Pool et dégâts ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Pool d'attaque de base (sans les bonus optionnels du dialogue).
|
||||
* Personnage : caractéristique + pool de compétence.
|
||||
* PNJ : pool d'attaque du niveau de menace.
|
||||
* Créature : pool d'attaque calculé.
|
||||
* @param {Actor} actor L'attaquant
|
||||
* @param {Item|null} weapon
|
||||
* @param {string} mode
|
||||
* @returns {number}
|
||||
*/
|
||||
static baseAttackPool(actor, weapon, mode) {
|
||||
const type = actor.type
|
||||
if (type === 'creature') {
|
||||
return actor.system?.computed?.attack || 0
|
||||
}
|
||||
if (type === 'npc') {
|
||||
const threat = actor.system?.threat?.value || 1
|
||||
return CONFIG.VERMINE.npcThreatLevels?.[threat]?.attack || 0
|
||||
}
|
||||
const abilityKey = this.attackAbility(mode)
|
||||
const ability = actor.system?.abilities?.[abilityKey]?.value || 0
|
||||
const skillKey = weapon?.system?.skill
|
||||
const skillValue = skillKey ? actor.system?.skills?.[skillKey]?.value || 0 : 0
|
||||
const skillPool = skillValue ? CONFIG.VERMINE.SkillLevels?.[skillValue]?.dicePool || 0 : 0
|
||||
return ability + skillPool
|
||||
}
|
||||
|
||||
/**
|
||||
* Dégâts de base de l'arme (hors réussites de l'attaque).
|
||||
* Le trait "Vigueur+" ajoute la vigueur de l'attaquant.
|
||||
* PNJ sans arme et créatures utilisent leurs dégâts propres. Une créature
|
||||
* équipée d'une arme (morsure, griffes…) utilise les dégâts de celle-ci.
|
||||
* @param {Actor} actor L'attaquant
|
||||
* @param {Item|null} weapon
|
||||
* @returns {number}
|
||||
*/
|
||||
static baseDamage(actor, weapon) {
|
||||
const type = actor.type
|
||||
if (type === 'creature') {
|
||||
if (weapon?.system?.damage) {
|
||||
let base = weapon.system.damage.value || 0
|
||||
if (weapon.system.damage.addVigor) base += actor.system?.computed?.vigor || 0
|
||||
return base
|
||||
}
|
||||
return actor.system?.computed?.damage || 0
|
||||
}
|
||||
if (weapon?.system?.damage) {
|
||||
let base = weapon.system.damage.value || 0
|
||||
if (weapon.system.damage.addVigor) base += this.#vigor(actor)
|
||||
return base
|
||||
}
|
||||
return this.#vigor(actor)
|
||||
}
|
||||
|
||||
static #vigor(actor) {
|
||||
if (actor.type === 'npc') {
|
||||
const threat = actor.system?.threat?.value || 1
|
||||
return CONFIG.VERMINE.npcThreatLevels?.[threat]?.vigor || 0
|
||||
}
|
||||
if (actor.type === 'creature') return actor.system?.computed?.vigor || 0
|
||||
return actor.system?.abilities?.vigor?.value || 0
|
||||
}
|
||||
|
||||
// ── Protection et blessures ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Protection du défenseur.
|
||||
* Parade : seule la protection utilisée compte.
|
||||
* Sinon : somme des protections équipées (créature : protection de rôle).
|
||||
* @param {Actor} defender
|
||||
* @param {number|null} parryLevel Protection utilisée lors d'une parade
|
||||
* @returns {number}
|
||||
*/
|
||||
static protectionTotal(defender, parryLevel = null) {
|
||||
if (parryLevel !== null) return Math.max(0, parryLevel)
|
||||
if (defender.type === 'creature') {
|
||||
return defender.system?.computed?.protection || 0
|
||||
}
|
||||
const defenses = defender.itemTypes?.defense?.filter(d => d.system.equipped) || []
|
||||
return defenses.reduce((sum, d) => sum + (d.system.level || 0), 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Catégorie de blessure : le seuil le plus élevé inférieur ou égal
|
||||
* aux dégâts nets subis.
|
||||
* @param {number} netDamage
|
||||
* @param {Actor} defender
|
||||
* @returns {string|null} 'minorWound', 'majorWound', 'deadlyWound' ou null
|
||||
*/
|
||||
static highestWound(netDamage, defender) {
|
||||
const sys = defender.system
|
||||
const deadly = sys?.deadlyWound?.threshold ?? Number.POSITIVE_INFINITY
|
||||
const major = sys?.majorWound?.threshold ?? Number.POSITIVE_INFINITY
|
||||
const minor = sys?.minorWound?.threshold ?? Number.POSITIVE_INFINITY
|
||||
if (netDamage >= deadly) return 'deadlyWound'
|
||||
if (netDamage >= major) return 'majorWound'
|
||||
if (netDamage >= minor) return 'minorWound'
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Clé i18n du libellé d'une catégorie de blessure.
|
||||
* @param {string|null} category
|
||||
* @returns {string}
|
||||
*/
|
||||
static woundLabelKey(category) {
|
||||
if (category === 'deadlyWound') return 'VERMINE.wounds.deadly_wounds'
|
||||
if (category === 'majorWound') return 'VERMINE.wounds.heavy_wounds'
|
||||
if (category === 'minorWound') return 'VERMINE.wounds.light_wounds'
|
||||
return 'VERMINE.no_wound'
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie que l'utilisateur peut appliquer une blessure sur l'acteur.
|
||||
* @param {Actor} actor
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static canApplyWound(actor) {
|
||||
return Boolean(actor) && (game.user.isGM || actor.isOwner)
|
||||
}
|
||||
|
||||
/** Ordre de gravité des blessures (croissant). */
|
||||
static WOUND_ORDER = ['minorWound', 'majorWound', 'deadlyWound']
|
||||
|
||||
/** Malus de la pire blessure (règles p.42) : -1D Légère, -2D Grave, -3D Mortelle. */
|
||||
static WOUND_MALUS = { minorWound: 1, majorWound: 2, deadlyWound: 3 }
|
||||
|
||||
/**
|
||||
* Malus de blessure applicable à toutes les actions : seul le Malus de la
|
||||
* Blessure la plus grave compte (non cumulatif).
|
||||
* @param {Actor} actor
|
||||
* @returns {number} 0, 1, 2 ou 3
|
||||
*/
|
||||
static woundMalus(actor) {
|
||||
if (!actor?.system) return 0
|
||||
if (actor.system.deadlyWound?.value > 0) return 3
|
||||
if (actor.system.majorWound?.value > 0) return 2
|
||||
if (actor.system.minorWound?.value > 0) return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique +1 cercle de blessure, en montant en gravité si les cercles du
|
||||
* niveau correspondant sont déjà pleins (règles p.42 : si tous les cercles
|
||||
* d'une Blessure sont cochés, la suivante est de gravité supérieure).
|
||||
* @param {Actor} actor
|
||||
* @param {string} woundCategory 'minorWound' | 'majorWound' | 'deadlyWound'
|
||||
* @returns {Promise<Object|boolean|null>}
|
||||
* - false si refusé (permissions) ou null si invalide
|
||||
* - { category, value, escalated, death } si appliqué (death si tout est plein)
|
||||
*/
|
||||
static async applyWound(actor, woundCategory) {
|
||||
if (!actor || !woundCategory) return null
|
||||
if (!this.canApplyWound(actor)) return false
|
||||
const startIdx = this.WOUND_ORDER.indexOf(woundCategory)
|
||||
if (startIdx === -1) return null
|
||||
for (let i = startIdx; i < this.WOUND_ORDER.length; i++) {
|
||||
const cat = this.WOUND_ORDER[i]
|
||||
const sys = actor.system?.[cat]
|
||||
const max = sys?.max ?? Number.POSITIVE_INFINITY
|
||||
const value = sys?.value || 0
|
||||
if (value < max) {
|
||||
await actor.update({ [`system.${cat}.value`]: value + 1 })
|
||||
return { category: cat, value: value + 1, escalated: cat !== woundCategory, death: false }
|
||||
}
|
||||
}
|
||||
return { category: woundCategory, value: null, escalated: false, death: true }
|
||||
}
|
||||
|
||||
// ── Résolution ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Résout l'échange pour une cible donnée (défense la plus récente liée
|
||||
* à cette cible, ou abstention si aucune défense).
|
||||
* @param {ChatMessage} attackMessage
|
||||
* @param {string|null} targetId id d'acteur de la cible (défaut : première cible)
|
||||
* @returns {Promise<Object|null>} résultat de la résolution
|
||||
*/
|
||||
static async resolve(attackMessage, targetId = null) {
|
||||
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||
if (!exchange) return null
|
||||
|
||||
const target = (targetId && exchange.targets?.find(t => t.id === targetId)) || exchange.targets?.[0] || null
|
||||
if (!target) return null
|
||||
|
||||
const defs = this.#defenseMessagesFor(exchange.id, target.id)
|
||||
const defenderMessage = defs[defs.length - 1]
|
||||
const defense = defenderMessage ? defenderMessage.getFlag('world', FLAG_DEFENSE) : null
|
||||
|
||||
const attackerSuccesses = this.#messageTotal(attackMessage)
|
||||
const defenderSuccesses = defense ? this.#messageTotal(defenderMessage) : 0
|
||||
const hit = attackerSuccesses > defenderSuccesses
|
||||
|
||||
// Le flag de défense porte actorId/actorUuid (clés différentes de celles
|
||||
// des cibles) : on normalise la référence avant résolution de l'acteur.
|
||||
const defenderRef = defense
|
||||
? { id: defense.actorId ?? null, uuid: defense.actorUuid ?? null, name: defense.actorName ?? '' }
|
||||
: target
|
||||
const defender = await this.#resolveDefender(defenderRef)
|
||||
|
||||
const damage = (exchange.baseDamage || 0) + attackerSuccesses
|
||||
const parryLevel = defense?.type === 'parade' ? (defense.parryLevel ?? 0) : null
|
||||
const protection = hit && parryLevel !== null
|
||||
? parryLevel
|
||||
: (defender ? this.protectionTotal(defender, hit ? null : parryLevel) : 0)
|
||||
const netDamage = hit ? Math.max(0, damage - protection) : 0
|
||||
const woundCategory = hit && defender ? this.highestWound(netDamage, defender) : null
|
||||
|
||||
return {
|
||||
exchangeId: exchange.id,
|
||||
defenderId: defenderRef.id ?? null,
|
||||
defenderUuid: defenderRef.uuid ?? null,
|
||||
defenderName: defense?.actorName || target.name || '',
|
||||
defenseType: defense?.type || null,
|
||||
attackerSuccesses,
|
||||
defenderSuccesses,
|
||||
hit,
|
||||
damage,
|
||||
protection,
|
||||
netDamage,
|
||||
woundCategory,
|
||||
woundThreshold: woundCategory && defender ? defender.system[woundCategory].threshold : null,
|
||||
canApply: Boolean(defender) && this.canApplyWound(defender)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rend le bloc de résolution (HTML) pour insertion dans le message.
|
||||
* @param {Object} r Résultat de VermineExchange.resolve
|
||||
* @returns {string}
|
||||
*/
|
||||
static renderResolution(r) {
|
||||
const t = (key) => game.i18n.localize(key)
|
||||
const verdict = !r.hit
|
||||
? (r.defenseType === 'parade'
|
||||
? `<span class="res-parried">${t('VERMINE.missed_parade')}</span>`
|
||||
: `<span class="res-dodged">${t('VERMINE.missed_esquive')}</span>`)
|
||||
: (r.defenseType === 'parade'
|
||||
? `<span class="res-hit">${t('VERMINE.hit_parade')}</span>`
|
||||
: `<span class="res-hit">${t('VERMINE.hit')}</span>`)
|
||||
|
||||
const lines = [
|
||||
`<div class="resolution-line">${t('VERMINE.attacker_successes')}: ${r.attackerSuccesses} · ${t('VERMINE.defender_successes')}: ${r.defenderSuccesses} → ${verdict}</div>`
|
||||
]
|
||||
if (r.hit) {
|
||||
lines.push(`<div class="resolution-line">${t('VERMINE.raw_damage')}: <strong>${r.damage}</strong>${r.protection ? ` − ${t('ADVERSITY.protection')}: ${r.protection}` : ''} = ${t('VERMINE.net_damage')}: <strong>${r.netDamage}</strong></div>`)
|
||||
if (r.woundCategory) {
|
||||
lines.push(`<div class="resolution-line res-wound">${t('VERMINE.wound')}: <strong>${t(this.woundLabelKey(r.woundCategory))}</strong> (${t('VERMINE.wounds.threshold')} ${r.woundThreshold}D)</div>`)
|
||||
} else {
|
||||
lines.push(`<div class="resolution-line res-noshock">${t('VERMINE.no_wound')}</div>`)
|
||||
}
|
||||
} else {
|
||||
lines.push(`<div class="resolution-line res-nodamage">${t('VERMINE.no_damage')}</div>`)
|
||||
}
|
||||
|
||||
const apply = (r.hit && r.woundCategory && r.canApply)
|
||||
? `<button type="button" class="exchange-apply" data-action="exchange-apply" data-defender-id="${r.defenderId}" data-defender-uuid="${r.defenderUuid}" data-wound="${r.woundCategory}"><i class="fas fa-briefcase-medical"></i> ${t('VERMINE.apply_wound')}</button>`
|
||||
: ''
|
||||
|
||||
return `<div class="exchange-resolution">
|
||||
<div class="resolution-header"><i class="fas fa-exchange-alt"></i> ${t('VERMINE.resolve_result')}</div>
|
||||
${lines.join('')}
|
||||
${apply}
|
||||
</div>`
|
||||
}
|
||||
|
||||
// ── Liaison chat ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Lie les interactions d'échange de coups sur un message de chat.
|
||||
* @param {HTMLElement} html élément du message
|
||||
* @param {ChatMessage} message
|
||||
*/
|
||||
static bindChat(html, message) {
|
||||
const exchange = message.getFlag('world', FLAG_EXCHANGE)
|
||||
if (exchange) {
|
||||
html.querySelectorAll('.exchange-defend').forEach(btn => {
|
||||
btn.addEventListener('click', ev => this.#onDefense(ev, message))
|
||||
})
|
||||
html.querySelectorAll('.exchange-resolve').forEach(btn => {
|
||||
btn.addEventListener('click', ev => this.#onResolve(ev, message))
|
||||
})
|
||||
}
|
||||
html.querySelectorAll('[data-action="exchange-apply"]').forEach(btn => {
|
||||
btn.addEventListener('click', ev => this.#onApply(ev, message))
|
||||
})
|
||||
}
|
||||
|
||||
static async #onDefense(ev, attackMessage) {
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
const btn = ev.currentTarget
|
||||
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||
if (!exchange) return
|
||||
const type = btn.dataset.type
|
||||
const actor = await this.#resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
|
||||
if (!actor) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
|
||||
return
|
||||
}
|
||||
if (!game.user.isGM && !actor.isOwner) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
|
||||
return
|
||||
}
|
||||
if (actor.type === 'creature') {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_creature_no_defense'))
|
||||
return
|
||||
}
|
||||
const defense = {
|
||||
exchangeId: exchange.id,
|
||||
type,
|
||||
actorId: actor.id,
|
||||
actorUuid: actor.uuid,
|
||||
actorName: actor.name,
|
||||
difficulty: exchange.difficulty,
|
||||
parryLevel: type === 'parade' ? this.#parryLevel(actor) : null
|
||||
}
|
||||
// PNJ : pas de caractéristiques/compétences — la défense utilise la valeur
|
||||
// d'Action (Expérience), difficulté verrouillée.
|
||||
if (actor.type === 'npc') {
|
||||
const { default: NpcRollDialog } = await import('./dialogs/npcRollDialog.mjs')
|
||||
const dialog = await NpcRollDialog.create({
|
||||
actor,
|
||||
rolltype: 'action',
|
||||
locks: { difficulty: exchange.difficulty },
|
||||
defense
|
||||
})
|
||||
if (dialog) dialog.render(true)
|
||||
return
|
||||
}
|
||||
const { default: RollDialog } = await import('./dialogs/rollDialog.mjs')
|
||||
const locks = this.#defenseLocks(actor, exchange, type)
|
||||
const dialog = await RollDialog.create({
|
||||
actor,
|
||||
rolltype: 'skill',
|
||||
label: locks.skill,
|
||||
locks,
|
||||
defense
|
||||
})
|
||||
if (dialog) dialog.render(true)
|
||||
}
|
||||
|
||||
static #defenseLocks(actor, exchange, type) {
|
||||
if (type === 'parade') {
|
||||
return { ability: 'vigor', skill: this.#parrySkill(actor), difficulty: exchange.difficulty }
|
||||
}
|
||||
return {
|
||||
ability: 'reflexes',
|
||||
skill: exchange.mode === 'ranged' ? 'alertness' : 'close',
|
||||
difficulty: exchange.difficulty
|
||||
}
|
||||
}
|
||||
|
||||
static #parrySkill(actor) {
|
||||
const hasMelee = actor.itemTypes?.weapon?.some(w => w.system.equipped && w.system.skill === 'melee')
|
||||
return hasMelee ? 'melee' : 'close'
|
||||
}
|
||||
|
||||
static #parryLevel(actor) {
|
||||
const defenses = actor.itemTypes?.defense?.filter(d => d.system.equipped) || []
|
||||
return defenses.reduce((best, d) => Math.max(best, d.system.level || 0), 0)
|
||||
}
|
||||
|
||||
static async #onResolve(ev, attackMessage) {
|
||||
ev.preventDefault()
|
||||
const btn = ev.currentTarget
|
||||
if (!game.user.isGM && !attackMessage.isOwner) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
|
||||
return
|
||||
}
|
||||
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||
if (!exchange) return
|
||||
if (!exchange.targets?.length) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_target'))
|
||||
return
|
||||
}
|
||||
const wrapper = btn.closest('.exchange-actions')
|
||||
const container = wrapper?.parentElement
|
||||
if (!container) return
|
||||
container.querySelectorAll('.exchange-resolution').forEach(el => el.remove())
|
||||
|
||||
const targets = exchange.targets?.length ? exchange.targets : [null]
|
||||
for (const target of targets) {
|
||||
const result = await this.resolve(attackMessage, target?.id ?? null)
|
||||
if (!result) continue
|
||||
const block = document.createElement('div')
|
||||
block.innerHTML = this.renderResolution(result)
|
||||
const node = block.firstElementChild
|
||||
container.appendChild(node)
|
||||
this.bindChat(node, attackMessage)
|
||||
}
|
||||
|
||||
const msgEl = btn.closest('.vermine-roll-message')
|
||||
if (msgEl) await attackMessage.update({ content: msgEl.outerHTML })
|
||||
}
|
||||
|
||||
static async #onApply(ev, attackMessage) {
|
||||
ev.preventDefault()
|
||||
const btn = ev.currentTarget
|
||||
const defender = await this.#resolveDefender({ id: btn.dataset.defenderId, uuid: btn.dataset.defenderUuid })
|
||||
const wound = btn.dataset.wound
|
||||
if (!defender || !wound) return
|
||||
if (!this.canApplyWound(defender)) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
|
||||
return
|
||||
}
|
||||
const result = await this.applyWound(defender, wound)
|
||||
if (result === false) return
|
||||
btn.disabled = true
|
||||
let label = game.i18n.localize('VERMINE.wound_applied')
|
||||
if (result?.death) {
|
||||
label = game.i18n.localize('VERMINE.wound_death')
|
||||
} else if (result?.escalated) {
|
||||
const from = game.i18n.localize(this.woundLabelKey(wound))
|
||||
const to = game.i18n.localize(this.woundLabelKey(result.category))
|
||||
label = game.i18n.format('VERMINE.wound_escalated', { from, to })
|
||||
}
|
||||
btn.innerHTML = `<i class="fas fa-check"></i> ${label}`
|
||||
// Persiste l'état du bouton seulement si l'utilisateur peut modifier le message
|
||||
if (game.user.isGM || attackMessage.isOwner) {
|
||||
const msgEl = btn.closest('.vermine-roll-message')
|
||||
if (msgEl) await attackMessage.update({ content: msgEl.outerHTML })
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers internes ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Résout un acteur depuis son id d'acteur monde (acteurs liés) ou son
|
||||
* uuid (tokens non liés via fromUuid).
|
||||
* @param {{id: string|null, uuid: string|null}} ref
|
||||
* @returns {Promise<Actor|null>}
|
||||
*/
|
||||
static async #resolveDefender({ id = null, uuid = null }) {
|
||||
if (id) {
|
||||
const actor = game.actors.get(id)
|
||||
if (actor) return actor
|
||||
}
|
||||
if (uuid) {
|
||||
const doc = await fromUuid(uuid)
|
||||
if (!doc) return null
|
||||
if (doc.documentType === 'Actor') return doc
|
||||
if (doc.documentType === 'Token') return doc.actor ?? null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
static #defenseMessagesFor(exchangeId, targetId = null) {
|
||||
return game.messages.contents.filter(m => {
|
||||
const f = m.getFlag('world', FLAG_DEFENSE)
|
||||
if (!f || f.exchangeId !== exchangeId) return false
|
||||
if (targetId) return f.actorId === targetId
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
static #messageTotal(message) {
|
||||
const el = document.createElement('div')
|
||||
el.innerHTML = message.content || ''
|
||||
const totalEl = el.querySelector('#total')
|
||||
const val = parseInt(totalEl?.innerText, 10)
|
||||
return Number.isNaN(val) ? 0 : val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* Règles d'apprentissage (phase d'Expérience).
|
||||
* Helpers purs de difficultés, handicaps et calculs de jets d'Expérience.
|
||||
*/
|
||||
|
||||
export class VermineExperience {
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet d'apprentissage de Compétence selon le niveau visé.
|
||||
* @param {number} targetLevel niveau visé (1-5)
|
||||
* @returns {number}
|
||||
*/
|
||||
static skillLearningDifficulty(targetLevel) {
|
||||
switch (parseInt(targetLevel, 10)) {
|
||||
case 1: return 5 // Débutant
|
||||
case 2: return 7 // Confirmé
|
||||
case 3: return 9 // Expert
|
||||
case 4: return 10 // Maître
|
||||
case 5: return 10 // Légende
|
||||
default: return 7
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handicap d'un jet d'apprentissage de Compétence.
|
||||
* Rareté (1-3) + handicap (I) supplémentaire pour le niveau Légende.
|
||||
* @param {number} targetLevel niveau visé (1-5)
|
||||
* @param {number} rarity rareté de la compétence (0-3)
|
||||
* @returns {number}
|
||||
*/
|
||||
static skillLearningHandicap(targetLevel, rarity) {
|
||||
let handicap = parseInt(rarity, 10) || 0
|
||||
if (parseInt(targetLevel, 10) === 5) handicap += 1 // Légende : (I)
|
||||
return handicap
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet d'apprentissage de Spécialité selon le niveau actuel
|
||||
* de la compétence associée. Domaine de prédilection : -2.
|
||||
* @param {number} skillLevel niveau actuel de la compétence (2-5)
|
||||
* @param {boolean} isDomain la compétence est dans le domaine de prédilection
|
||||
* @returns {number}
|
||||
*/
|
||||
static specialtyLearningDifficulty(skillLevel, isDomain) {
|
||||
let difficulty
|
||||
switch (parseInt(skillLevel, 10)) {
|
||||
case 2: difficulty = 9 // Confirmé
|
||||
break
|
||||
case 3: difficulty = 7 // Expert
|
||||
break
|
||||
case 4: difficulty = 5 // Maître
|
||||
break
|
||||
case 5: difficulty = 5 // Légende (relance automatique)
|
||||
break
|
||||
default: difficulty = 9
|
||||
}
|
||||
return isDomain ? difficulty - 2 : difficulty
|
||||
}
|
||||
|
||||
/**
|
||||
* Handicap d'un jet d'apprentissage de Spécialité : nombre de spécialités
|
||||
* déjà possédées (requiert au moins Confirmé dans la compétence associée).
|
||||
* @param {number} specialtyCount
|
||||
* @returns {number}
|
||||
*/
|
||||
static specialtyLearningHandicap(specialtyCount) {
|
||||
return Math.max(0, parseInt(specialtyCount, 10) || 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet d'apprentissage de Caractéristique.
|
||||
* @returns {number}
|
||||
*/
|
||||
static characteristicLearningDifficulty() {
|
||||
return 9
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet d'apprentissage de Réserve : valeur actuelle.
|
||||
* @param {number} currentValue
|
||||
* @returns {number}
|
||||
*/
|
||||
static reserveLearningDifficulty(currentValue) {
|
||||
return Math.max(0, parseInt(currentValue, 10) || 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handicap d'âge pour l'apprentissage des Réserves et autres jets liés à l'âge.
|
||||
* @param {number} ageType 1=Jeune, 2=Adulte, 3=Ancien
|
||||
* @returns {number}
|
||||
*/
|
||||
static ageHandicap(ageType) {
|
||||
switch (parseInt(ageType, 10)) {
|
||||
case 1: return 1 // Jeune (I)
|
||||
case 3: return 3 // Ancien (III)
|
||||
default: return 2 // Adulte (II)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet d'acquisition d'un Dé d'Évolution selon l'âge.
|
||||
* @param {number} ageType 1=Jeune, 2=Adulte, 3=Ancien
|
||||
* @returns {number}
|
||||
*/
|
||||
static evolutionDifficulty(ageType) {
|
||||
switch (parseInt(ageType, 10)) {
|
||||
case 1: return 5
|
||||
case 3: return 9
|
||||
default: return 7
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bonus/Malus de Totem sur le jet de Dé d'Évolution.
|
||||
* Plus de Dés Adaptés que Humains : +1D. Inversement : -1D.
|
||||
* @param {number} human human totem dice
|
||||
* @param {number} adapted adapted totem dice
|
||||
* @returns {number}
|
||||
*/
|
||||
static evolutionTotemModifier(human, adapted) {
|
||||
if (adapted > human) return 1
|
||||
if (human > adapted) return -1
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Un Dé de Totem Adapté peut remplacer un Dé d'Expérience (compte ×2)
|
||||
* uniquement si le personnage possède strictement plus de Dés Adaptés
|
||||
* que Humains.
|
||||
* @param {number} human human totem dice
|
||||
* @param {number} adapted adapted totem dice
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static canUseAdaptedDie(human, adapted) {
|
||||
return adapted > human
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet d'apprentissage de Capacité de Totem : valeur
|
||||
* d'Apprentissage de la capacité.
|
||||
* @param {object} abilityData system data de l'item ability
|
||||
* @returns {number}
|
||||
*/
|
||||
static totemAbilityLearningDifficulty(abilityData) {
|
||||
return parseInt(abilityData?.learn?.threshold, 10) || 5
|
||||
}
|
||||
|
||||
/**
|
||||
* Handicap d'un jet d'apprentissage de Capacité de Totem : valeur
|
||||
* d'Apprentissage de la capacité.
|
||||
* @param {object} abilityData system data de l'item ability
|
||||
* @returns {number}
|
||||
*/
|
||||
static totemAbilityLearningHandicap(abilityData) {
|
||||
return parseInt(abilityData?.learn?.hindrance, 10) || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet d'obtention d'un Élément d'Historique.
|
||||
* @returns {number}
|
||||
*/
|
||||
static backgroundLearningDifficulty() {
|
||||
return 9
|
||||
}
|
||||
|
||||
/**
|
||||
* Handicap d'un jet d'obtention d'un Élément d'Historique : (I) si
|
||||
* l'Historique est listé avec un astérisque.
|
||||
* @param {boolean} asterisk
|
||||
* @returns {number}
|
||||
*/
|
||||
static backgroundLearningHandicap(asterisk) {
|
||||
return asterisk ? 1 : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet visant à réduire le score de Réputation (devenir
|
||||
* plus célèbre) : 10 moins le score que le personnage souhaite atteindre.
|
||||
* @param {number} current réputation actuelle
|
||||
* @returns {number}
|
||||
*/
|
||||
static reputationReduceDifficulty(current) {
|
||||
return 10 - Math.max(0, (parseInt(current, 10) || 0) - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet visant à augmenter le score de Réputation (se faire
|
||||
* oublier) : égale à la Réputation actuelle.
|
||||
* @param {number} current réputation actuelle
|
||||
* @returns {number}
|
||||
*/
|
||||
static reputationIncreaseDifficulty(current) {
|
||||
return parseInt(current, 10) || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Clé i18n du libellé du niveau de compétence visé.
|
||||
* @param {number} level
|
||||
* @returns {string}
|
||||
*/
|
||||
static skillLevelLabelKey(level) {
|
||||
return CONFIG.VERMINE.SkillLevels?.[level]?.label ?? "SKILL_LEVELS.incompetent"
|
||||
}
|
||||
|
||||
/**
|
||||
* Détermine si une compétence appartient au domaine de prédilection.
|
||||
* @param {Actor} actor
|
||||
* @param {string} skillKey
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isPreferredDomain(actor, skillKey) {
|
||||
const category = actor.system?.skills?.[skillKey]?.category
|
||||
const preferred = actor.system?.skill_categories?.preferred
|
||||
return Boolean(category && preferred && category === preferred)
|
||||
}
|
||||
|
||||
/**
|
||||
* Réinitialise les limites d'apprentissage (learned.*) de la phase
|
||||
* courante, pour un personnage ou un Groupe, sans toucher au compteur
|
||||
* de phase (avancé uniquement par le dialogue d'attribution du MJ).
|
||||
* @param {foundry.abstract.Document} actor personnage ou Groupe
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async resetPhaseLimits(actor) {
|
||||
const ex = actor.system?.experience
|
||||
if (!ex) return
|
||||
const update = {}
|
||||
if (ex.learned && typeof ex.learned === "object") {
|
||||
for (const key of Object.keys(ex.learned)) {
|
||||
if (key === "phase") continue
|
||||
update[`system.experience.learned.${key}`] = false
|
||||
}
|
||||
}
|
||||
if (Object.keys(update).length) await actor.update(update)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode de jeu actif (1=Survie, 2=Cauchemar, 3=Apocalypse).
|
||||
* @returns {number}
|
||||
*/
|
||||
static gameMode() {
|
||||
return parseInt(game.settings.get("vermine2047", "game-mode"), 10) || 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Total de Dés d'Évolution maximum selon le Mode de jeu.
|
||||
* @param {number} mode 1=Survie, 2=Cauchemar, 3=Apocalypse
|
||||
* @returns {number}
|
||||
*/
|
||||
static evolutionMaxForMode(mode) {
|
||||
switch (parseInt(mode, 10)) {
|
||||
case 2: return 6
|
||||
case 3: return 7
|
||||
default: return 5
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Somme des niveaux des Adaptations/Mutations déjà possédées.
|
||||
* @param {foundry.abstract.Document} actor personnage
|
||||
* @returns {number}
|
||||
*/
|
||||
static evolutionTotalLevels(actor) {
|
||||
return (actor?.itemTypes?.evolution || []).reduce((sum, item) => sum + (item.system?.level?.value || 0), 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide l'achat d'une Adaptation/Mutation contre les Dés d'Évolution
|
||||
* accumulés. Retourne une clé i18n d'erreur, ou null si l'achat est permis.
|
||||
* Règles p. 116-117 : coût = niveau ; total limité par le Mode ; Mutations
|
||||
* 3D/4D interdites en Mode Survie ; une seule Mutation à 4D.
|
||||
* @param {foundry.abstract.Document} actor personnage
|
||||
* @param {number} cost niveau de l'Adaptation/Mutation (1-4)
|
||||
* @param {boolean} mutation vrai si c'est une Mutation
|
||||
* @returns {string|null}
|
||||
*/
|
||||
static validateEvolutionPurchase(actor, cost, mutation) {
|
||||
const level = parseInt(cost, 10) || 0
|
||||
const accumulated = actor.system?.experience?.evolution || 0
|
||||
const mode = this.gameMode()
|
||||
if (accumulated < level) return "VERMINE.error_evolution_not_enough_dice"
|
||||
if (level < 1 || level > 4) return "VERMINE.error_evolution_invalid_level"
|
||||
const total = this.evolutionTotalLevels(actor) + level
|
||||
if (total > this.evolutionMaxForMode(mode)) return "VERMINE.error_evolution_mode_limit"
|
||||
if (mutation && level >= 3 && mode < 2) return "VERMINE.error_evolution_mutation_mode"
|
||||
if (mutation && level === 4) {
|
||||
const hasFourDMutation = (actor?.itemTypes?.evolution || []).some(
|
||||
(item) => item.system?.mutation && (item.system?.level?.value || 0) === 4
|
||||
)
|
||||
if (hasFourDMutation) return "VERMINE.error_evolution_mutation_4d"
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export const preloadHandlebarsTemplates = async function () {
|
||||
"systems/vermine2047/templates/actor/parts/actor-items.hbs",
|
||||
"systems/vermine2047/templates/actor/parts/actor-weapons.hbs",
|
||||
"systems/vermine2047/templates/actor/parts/actor-defenses.hbs",
|
||||
"systems/vermine2047/templates/actor/parts/actor-equipped-gear.hbs",
|
||||
|
||||
// Character partials.
|
||||
"systems/vermine2047/templates/actor/character/character-id.hbs",
|
||||
@@ -32,6 +33,7 @@ export const preloadHandlebarsTemplates = async function () {
|
||||
// npc partials
|
||||
"systems/vermine2047/templates/actor/npc/npc-combat.hbs",
|
||||
"systems/vermine2047/templates/actor/parts/npc-skill-category.hbs",
|
||||
"systems/vermine2047/templates/actor/parts/npc-skill-item.hbs",
|
||||
|
||||
// creature partials
|
||||
"systems/vermine2047/templates/actor/creature/creature-combat.hbs",
|
||||
@@ -96,8 +98,24 @@ export const registerHandlebarsHelpers = function () {
|
||||
});
|
||||
|
||||
// search translation with variables
|
||||
Handlebars.registerHelper('smarttl', function (arrayLabel, objectLabel, options) {
|
||||
return game.i18n.localize(arrayLabel + "." + objectLabel + ".name");
|
||||
Handlebars.registerHelper('smarttl', function (arrayLabel, objectLabel, key) {
|
||||
if (!key || typeof key !== 'string') key = 'name';
|
||||
return game.i18n.localize(arrayLabel + "." + objectLabel + "." + key);
|
||||
});
|
||||
|
||||
// Return an object of skills ordered alphabetically by their localized
|
||||
// display name (easier to find a skill quickly). Use with `{{#each}}`.
|
||||
Handlebars.registerHelper('sortedSkills', function (skills) {
|
||||
if (!skills || typeof skills !== 'object') return skills;
|
||||
const sorted = {};
|
||||
Object.keys(skills)
|
||||
.sort((a, b) => {
|
||||
const na = game.i18n.localize(`SKILLS.${a}.name`).toLocaleLowerCase();
|
||||
const nb = game.i18n.localize(`SKILLS.${b}.name`).toLocaleLowerCase();
|
||||
return na.localeCompare(nb, game.i18n.lang);
|
||||
})
|
||||
.forEach(k => { sorted[k] = skills[k]; });
|
||||
return sorted;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('smarttlk', function (arrayLabel, objectLabel, key) {
|
||||
@@ -347,6 +365,20 @@ export const registerHandlebarsHelpers = function () {
|
||||
Handlebars.registerHelper('ifincludes', function (arg1, arg2, options) {
|
||||
return (arg1.includes(arg2)) ? options.fn(this) : options.inverse(this);
|
||||
});
|
||||
Handlebars.registerHelper('ifStartsWith', function (arg1, arg2, options) {
|
||||
return (typeof arg1 === 'string' && arg1.startsWith(arg2)) ? options.fn(this) : options.inverse(this);
|
||||
});
|
||||
// Chiffre → chiffre romain (handicaps, entraves). 0 → chaîne vide.
|
||||
Handlebars.registerHelper('roman', function (n) {
|
||||
const table = [[10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']]
|
||||
let num = Number(n)
|
||||
if (!Number.isFinite(num) || num <= 0) return ''
|
||||
let out = ''
|
||||
for (const [v, s] of table) {
|
||||
while (num >= v) { out += s; num -= v }
|
||||
}
|
||||
return out
|
||||
});
|
||||
|
||||
|
||||
//math operations
|
||||
|
||||
@@ -41,6 +41,10 @@ export const registerHooks = function () {
|
||||
});
|
||||
|
||||
Hooks.on('renderChatMessageHTML', async (message, html, data) => {
|
||||
// Échanges de coups : liaisons ouvertes à tous (propriétaires de cibles inclus)
|
||||
const { VermineExchange } = await import('./exchange.mjs');
|
||||
VermineExchange.bindChat(html, message);
|
||||
|
||||
let rerollTitle = html.querySelector(".reroll-fromroll h4");
|
||||
if (rerollTitle) {
|
||||
rerollTitle.addEventListener("click", () => { html.querySelector(".reroll").classList.toggle('visible') })
|
||||
@@ -127,6 +131,15 @@ export const registerHooks = function () {
|
||||
}
|
||||
});
|
||||
|
||||
// Un Groupe démarre avec une Réserve à son maximum (3D + 1D par membre).
|
||||
Hooks.on("createActor", async function (actor) {
|
||||
if (actor.type !== "group") return;
|
||||
const reserve = actor.system?.reserve;
|
||||
if (reserve && reserve.value === 0 && reserve.max > 0) {
|
||||
await actor.update({ "system.reserve.value": reserve.max });
|
||||
}
|
||||
});
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Combat Hooks */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { VermineTotemDice } from "./totem-dice.mjs";
|
||||
|
||||
export class VermineUtils {
|
||||
/**
|
||||
* Rolls dice with Vermine2047-specific rules.
|
||||
@@ -29,7 +31,14 @@ export class VermineUtils {
|
||||
keepTotem = null,
|
||||
skillLevel = null,
|
||||
hasSpecialty = false,
|
||||
handicap = 0
|
||||
handicap = 0,
|
||||
bonusSuccesses = 0,
|
||||
poolBreakdown = null,
|
||||
specialtyName = null,
|
||||
weapon = null,
|
||||
targets = [],
|
||||
attack = null,
|
||||
messageFlags = null
|
||||
}) {
|
||||
// Validate inputs
|
||||
if (!actor) {
|
||||
@@ -42,12 +51,12 @@ export class VermineUtils {
|
||||
// Declare variables
|
||||
let formula = "";
|
||||
let modFormula = null;
|
||||
let totemBonus = { human: 0, adapted: 0 };
|
||||
|
||||
// Calculate domain bonuses for totems
|
||||
if (skillCategory) {
|
||||
totemBonus = this._calculateTotemDomainBonuses(skillCategory, actor);
|
||||
}
|
||||
// Influence des Dés de Totems : Bonus/Malus de 1D selon le Totem dominant
|
||||
// et le Domaine de la Compétence (règles p. 121). S'applique même sans
|
||||
// utiliser un Dé de Totem.
|
||||
const influenceMod = this._calculateTotemInfluence(skillCategory, actor);
|
||||
NoD += influenceMod;
|
||||
|
||||
// Apply automatic successes from skill mastery
|
||||
let adjustedDifficulty = difficulty;
|
||||
@@ -58,11 +67,6 @@ export class VermineUtils {
|
||||
const humanDifficulty = adjustedDifficulty;
|
||||
const humanFormula = `(1D10cs>=${humanDifficulty}[human_${safeUserName}]*2)`;
|
||||
|
||||
// Apply domain bonus/malus
|
||||
if (totemBonus.human !== 0) {
|
||||
NoD += totemBonus.human;
|
||||
}
|
||||
|
||||
modFormula = humanFormula;
|
||||
}
|
||||
|
||||
@@ -72,11 +76,6 @@ export class VermineUtils {
|
||||
const adaptedDifficulty = adjustedDifficulty;
|
||||
const adaptedFormula = `(1D10cs>=${adaptedDifficulty}[adapted_${safeUserName}]*2)`;
|
||||
|
||||
// Apply domain bonus/malus
|
||||
if (totemBonus.adapted !== 0) {
|
||||
NoD += totemBonus.adapted;
|
||||
}
|
||||
|
||||
// Build combined formula
|
||||
if (modFormula !== null) {
|
||||
modFormula = `${modFormula}+${adaptedFormula}`;
|
||||
@@ -105,6 +104,22 @@ export class VermineUtils {
|
||||
// Build final formula
|
||||
formula = modFormula !== null ? `${baseFormula}+${modFormula}` : baseFormula;
|
||||
|
||||
// Reconcile the breakdown displayed in chat with the dice actually rolled.
|
||||
// L'influence s'applique au pool ; les Dés de Totem remplacent (n'ajoutent
|
||||
// pas) des dés du pool de base.
|
||||
if (poolBreakdown) {
|
||||
// Count totem dice present in the final modifier formula.
|
||||
let totemDice = 0;
|
||||
if (modFormula) {
|
||||
if (modFormula.includes("human_")) totemDice += 1;
|
||||
if (modFormula.includes("adapted_")) totemDice += 1;
|
||||
}
|
||||
poolBreakdown.totemDice = totemDice;
|
||||
poolBreakdown.domainBonus = influenceMod;
|
||||
// Total = nombre réel de dés lancés (NoD réguliers + dés de totem).
|
||||
poolBreakdown.total = NoD + totemDice;
|
||||
}
|
||||
|
||||
// Create the roll
|
||||
const roll = new Roll(formula, actor.getRollData());
|
||||
|
||||
@@ -117,12 +132,12 @@ export class VermineUtils {
|
||||
skillCategory: skillCategory,
|
||||
skillLevel: skillLevel,
|
||||
hasSpecialty: hasSpecialty,
|
||||
totemBonuses: { ...totemBonus },
|
||||
totemInfluence: influenceMod,
|
||||
baseNoD: NoD,
|
||||
rerolls: Reroll,
|
||||
selfControl: self_control
|
||||
selfControl: self_control,
|
||||
bonusSuccesses: bonusSuccesses
|
||||
};
|
||||
|
||||
// Evaluate the roll
|
||||
await roll.evaluate();
|
||||
|
||||
@@ -143,7 +158,14 @@ export class VermineUtils {
|
||||
keepTotem,
|
||||
skillLevel,
|
||||
hasSpecialty,
|
||||
handicap
|
||||
handicap,
|
||||
bonusSuccesses,
|
||||
poolBreakdown,
|
||||
specialtyName,
|
||||
weapon,
|
||||
targets,
|
||||
attack,
|
||||
messageFlags
|
||||
});
|
||||
|
||||
return roll;
|
||||
@@ -153,56 +175,21 @@ export class VermineUtils {
|
||||
* Calculates domain bonuses/penalties for totems.
|
||||
* @param {string} skillCategory - The skill category
|
||||
* @param {Actor} actor - The actor
|
||||
* @returns {Object} Bonuses for each totem {human: number, adapted: number}
|
||||
* @returns {number} Modificateur d'influence (-1, 0 ou +1)
|
||||
*/
|
||||
static _calculateTotemDomainBonuses(skillCategory, actor) {
|
||||
const bonuses = { human: 0, adapted: 0 };
|
||||
|
||||
// Validate inputs
|
||||
if (!CONFIG.VERMINE?.totemDomains || !actor?.system?.identity?.totem) {
|
||||
return bonuses;
|
||||
}
|
||||
|
||||
const actorTotem = actor.system.identity.totem;
|
||||
|
||||
// Check if actor's totem exists in configuration
|
||||
if (!CONFIG.VERMINE.totemDomains[actorTotem]) {
|
||||
return bonuses;
|
||||
}
|
||||
|
||||
const totemConfig = CONFIG.VERMINE.totemDomains[actorTotem];
|
||||
|
||||
if (!totemConfig?.domains) {
|
||||
return bonuses;
|
||||
}
|
||||
|
||||
// Get actor's preferred skill category
|
||||
const preferredCategory = actor.system.skill_categories?.preferred;
|
||||
|
||||
// Bonus for actor's totem if preferred category is in its domains
|
||||
if (preferredCategory && totemConfig.domains.includes(preferredCategory)) {
|
||||
bonuses[actorTotem] = totemConfig.bonus || 1;
|
||||
}
|
||||
|
||||
// Penalty for opposite totem if preferred category is in its domains
|
||||
const oppositeTotem = CONFIG.VERMINE.totem_opposites?.[actorTotem];
|
||||
if (oppositeTotem && CONFIG.VERMINE.totemDomains[oppositeTotem]) {
|
||||
const oppositeConfig = CONFIG.VERMINE.totemDomains[oppositeTotem];
|
||||
if (preferredCategory && oppositeConfig?.domains?.includes(preferredCategory)) {
|
||||
bonuses[oppositeTotem] = -(oppositeConfig.bonus || 1);
|
||||
}
|
||||
}
|
||||
|
||||
return bonuses;
|
||||
static _calculateTotemInfluence(skillCategory, actor) {
|
||||
// Les Dés de Totems n'existent que sur les personnages.
|
||||
if (actor?.type !== "character" || !skillCategory) return 0;
|
||||
return VermineTotemDice.influenceModifier(actor, skillCategory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles reroll events on dice in chat messages.
|
||||
* @param {Object} message - The chat message containing the reroll event
|
||||
* @param {Object} ev - The reroll event
|
||||
* @param {HTMLElement} target - The clicked die element
|
||||
* @returns {Promise<boolean>} Whether the reroll was successful
|
||||
*/
|
||||
static async onReroll(message, ev) {
|
||||
static async onReroll(message, target) {
|
||||
// Verify user permissions
|
||||
const msgUserId = message.user?.id ?? message.user;
|
||||
if (msgUserId !== game.user?.id && !game.user?.isGM) {
|
||||
@@ -211,7 +198,7 @@ export class VermineUtils {
|
||||
}
|
||||
|
||||
// Get reroll count
|
||||
const rollMessage = ev.currentTarget.closest('div.vermine-roll-message');
|
||||
const rollMessage = target.closest('div.vermine-roll-message');
|
||||
if (!rollMessage) {
|
||||
return false;
|
||||
}
|
||||
@@ -221,22 +208,22 @@ export class VermineUtils {
|
||||
// Check if rerolls are available
|
||||
if (!rerollCount || parseInt(rerollCount, 10) < 1) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_rerolls_left'));
|
||||
const rerollables = ev.currentTarget.closest('ul')?.querySelectorAll('.rerollable');
|
||||
const rerollables = target.closest('ul')?.querySelectorAll('.rerollable');
|
||||
if (rerollables) {
|
||||
rerollables.forEach(el => el.classList.remove('rerollable'));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ev.currentTarget.classList.add('rerolled');
|
||||
target.classList.add('rerolled');
|
||||
|
||||
// Set reroll flag
|
||||
await message.setFlag("world", "reroll", true);
|
||||
|
||||
// Get difficulty and dice type
|
||||
const ulElement = ev.currentTarget.closest('ul');
|
||||
const ulElement = target.closest('ul');
|
||||
const difficulty = ulElement?.dataset.difficulty ?? 7;
|
||||
let diceType = ev.currentTarget.dataset.diceType;
|
||||
let diceType = target.dataset.diceType;
|
||||
|
||||
// Sanitize user name
|
||||
const safeUserName = (game.user?.name ?? "user").replace(/[^a-zA-Z0-9_]/g, '_');
|
||||
@@ -265,30 +252,71 @@ export class VermineUtils {
|
||||
|
||||
// Update die display
|
||||
const result = reroll.dice[0]?.results[0]?.result ?? 0;
|
||||
const dieSpan = ev.currentTarget.querySelector('span');
|
||||
const dieSpan = target.querySelector('span');
|
||||
if (dieSpan) {
|
||||
dieSpan.innerText = result;
|
||||
}
|
||||
// Update the face used for the CSS background image.
|
||||
target.dataset.result = result;
|
||||
|
||||
// Update total if successful
|
||||
// Set/clear the success class from the rerolled die (a previous success
|
||||
// must be removed if the new result fails).
|
||||
const success = reroll.dice[0]?.results[0]?.success;
|
||||
if (success) {
|
||||
ev.currentTarget.classList.add('success');
|
||||
const totalElement = rollMessage.querySelector('#total');
|
||||
if (totalElement) {
|
||||
const currentTotal = parseInt(totalElement.innerText, 10) || 0;
|
||||
totalElement.innerText = currentTotal + reroll.total;
|
||||
}
|
||||
target.classList.toggle('success', !!success);
|
||||
target.classList.remove("rerollable");
|
||||
|
||||
// Recompute the total from the live dice in the DOM. This avoids the
|
||||
// cumulative-increment drift of the old logic (which never removed a
|
||||
// removed success from #total). Totem dice (human/adapted) count double.
|
||||
const totalEl = rollMessage.querySelector('#total');
|
||||
if (totalEl) {
|
||||
let total = 0;
|
||||
rollMessage.querySelectorAll('li.die.success').forEach(die => {
|
||||
const isTotem = die.classList.contains('human') || die.classList.contains('adapted');
|
||||
total += isTotem ? 2 : 1;
|
||||
});
|
||||
// Réussites automatiques (ex. bonus de Réaction) : ajoutées au total.
|
||||
const freeSuccesses = parseInt(rollMessage.dataset?.freeSuccesses ?? '0', 10) || 0;
|
||||
total += freeSuccesses;
|
||||
totalEl.innerText = total;
|
||||
}
|
||||
|
||||
// Update message content
|
||||
ev.currentTarget.classList.remove("rerollable");
|
||||
|
||||
const messageContent = ev.currentTarget.closest('div.message-content');
|
||||
const messageContent = target.closest('div.message-content');
|
||||
if (messageContent) {
|
||||
const newRerollCount = parseInt(rerollCount, 10) - 1;
|
||||
rollMessage.querySelector('#allowed_reroll').innerText = newRerollCount;
|
||||
|
||||
|
||||
// Recompute the verdict banner from the recomputed total.
|
||||
const verdictEl = rollMessage.querySelector('.roll-verdict');
|
||||
const requiredEl = rollMessage.querySelector('#required');
|
||||
if (verdictEl && totalEl && requiredEl) {
|
||||
const total = parseInt(totalEl.innerText, 10) || 0;
|
||||
const required = parseInt(requiredEl.innerText, 10) || 1;
|
||||
const won = total >= required;
|
||||
verdictEl.classList.toggle('success', won);
|
||||
verdictEl.classList.toggle('failure', !won);
|
||||
const label = won
|
||||
? game.i18n.localize('VERMINE.roll_success')
|
||||
: game.i18n.localize('VERMINE.roll_failure');
|
||||
verdictEl.innerHTML = `${won ? '<i class="fas fa-check-circle"></i>' : '<i class="fas fa-times-circle"></i>'}<span>${label}</span>`;
|
||||
}
|
||||
|
||||
// Recompute the dice summary from the current die list.
|
||||
const summaryEl = rollMessage.querySelector('.roll-dice-summary');
|
||||
if (summaryEl) {
|
||||
const stats = { regular: { count: 0, success: 0 }, human: { count: 0, success: 0 }, adapted: { count: 0, success: 0 } };
|
||||
rollMessage.querySelectorAll('li.die').forEach(die => {
|
||||
const type = die.classList.contains('human') ? 'human' : die.classList.contains('adapted') ? 'adapted' : 'regular';
|
||||
stats[type].count += 1;
|
||||
if (die.classList.contains('success')) stats[type].success += 1;
|
||||
});
|
||||
const mk = (key, count, s) => count ? `<span class="die-summary ${key}">${count} ${game.i18n.localize(`VERMINE.die_${key}`)} · ${s} ${game.i18n.localize('VERMINE.die_successes')}</span>` : '';
|
||||
summaryEl.innerHTML = mk('regular', stats.regular.count, stats.regular.success)
|
||||
+ mk('human', stats.human.count, stats.human.success)
|
||||
+ mk('adapted', stats.adapted.count, stats.adapted.success);
|
||||
}
|
||||
|
||||
await message.update({
|
||||
content: messageContent.outerHTML
|
||||
});
|
||||
@@ -314,10 +342,11 @@ export class VermineUtils {
|
||||
html.querySelectorAll('.rerollable').forEach(el => {
|
||||
el.addEventListener('click', async (ev) => {
|
||||
ev.preventDefault();
|
||||
const msgId = ev.currentTarget.closest("li.message")?.dataset?.messageId;
|
||||
const dieEl = ev.currentTarget;
|
||||
const msgId = dieEl.closest("li.message")?.dataset?.messageId;
|
||||
if (msgId) {
|
||||
const message = await game.messages.get(msgId);
|
||||
await VermineUtils.onReroll(message, ev);
|
||||
await VermineUtils.onReroll(message, dieEl);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -336,6 +365,7 @@ export class VermineUtils {
|
||||
// Add click event for granting rerolls
|
||||
html.querySelectorAll("button.grant-reroll").forEach(el => {
|
||||
el.addEventListener('click', async (ev) => {
|
||||
const btn = ev.currentTarget;
|
||||
const grantedRerollElement = html.querySelector('#granted-reroll');
|
||||
const allowedRerollElement = html.querySelector("#allowed_reroll");
|
||||
|
||||
@@ -343,13 +373,13 @@ export class VermineUtils {
|
||||
allowedRerollElement.innerText = grantedRerollElement.innerText;
|
||||
}
|
||||
|
||||
const mesEl = ev.currentTarget.closest('[data-message-id]');
|
||||
const mesEl = btn.closest('[data-message-id]');
|
||||
const messageId = mesEl?.dataset?.messageId;
|
||||
|
||||
if (messageId) {
|
||||
ev.currentTarget.closest('.reroll-from-effort').style.display = "none";
|
||||
btn.closest('.reroll-from-effort').style.display = "none";
|
||||
|
||||
const rollMessage = ev.currentTarget.closest(".vermine-roll-message");
|
||||
const rollMessage = btn.closest(".vermine-roll-message");
|
||||
if (rollMessage) {
|
||||
const content = rollMessage.outerHTML;
|
||||
const message = await game.messages.get(messageId);
|
||||
@@ -401,12 +431,38 @@ export class VermineUtils {
|
||||
* @returns {Promise<ChatMessage>} The created chat message
|
||||
*/
|
||||
static async diplayChatRoll(roll, param) {
|
||||
const content = await foundry.applications.handlebars.renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param });
|
||||
// Aggregate dice stats per type (regular / human / adapted)
|
||||
const diceStats = { regular: { count: 0, success: 0 }, human: { count: 0, success: 0 }, adapted: { count: 0, success: 0 } };
|
||||
for (const die of roll.dice ?? []) {
|
||||
// Flavors look like "<type>_<user>"; classify from the leading token.
|
||||
const flavor = die.options?.flavor ?? "";
|
||||
const prefix = flavor.split("_")[0];
|
||||
const type = prefix === "human" ? "human" : prefix === "adapted" ? "adapted" : "regular";
|
||||
for (const r of die.results ?? []) {
|
||||
diceStats[type].count += 1;
|
||||
if (r.success) diceStats[type].success += 1;
|
||||
}
|
||||
}
|
||||
// Verdict
|
||||
const required = 1 + (param.handicap ?? 0);
|
||||
const bonusSuccesses = param.bonusSuccesses ?? 0;
|
||||
const total = (roll._total ?? 0) + bonusSuccesses;
|
||||
const verdict = {
|
||||
success: total >= required,
|
||||
total,
|
||||
required,
|
||||
bonusSuccesses
|
||||
};
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param, diceStats, verdict });
|
||||
const chatData = {
|
||||
user: game.user?._id,
|
||||
speaker: ChatMessage.getSpeaker(),
|
||||
content: content
|
||||
};
|
||||
if (param.messageFlags) {
|
||||
chatData.flags = { world: param.messageFlags };
|
||||
}
|
||||
const msg = await ChatMessage.create(chatData);
|
||||
return msg;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* VermineTotemDice - Gestion des Dés de Totems (Humain / Adapté).
|
||||
*
|
||||
* Règles p. 121 :
|
||||
* - Influence : si un joueur possède strictement plus de Dés d'un Totem que
|
||||
* de l'autre, ce Totem prend l'ascendant (+1D/-1D sur certains Domaines).
|
||||
* - Gains et pertes via les Instincts et les Interdits (1D ou 2D selon la
|
||||
* gravité), avec des limites (3 Dés du même Totem, 5 Dés au maximum).
|
||||
*/
|
||||
export class VermineTotemDice {
|
||||
|
||||
/**
|
||||
* Totem dominant d'un personnage.
|
||||
* @param {foundry.abstract.Document} actor
|
||||
* @returns {'human'|'adapted'|'neutral'}
|
||||
*/
|
||||
static getInfluence(actor) {
|
||||
const human = actor.system?.adaptation?.totems?.human?.value || 0;
|
||||
const adapted = actor.system?.adaptation?.totems?.adapted?.value || 0;
|
||||
if (human > adapted) return "human";
|
||||
if (adapted > human) return "adapted";
|
||||
return "neutral";
|
||||
}
|
||||
|
||||
/**
|
||||
* Modificateur (Bonus/Malus) d'influence pour une catégorie de compétence.
|
||||
* @param {foundry.abstract.Document} actor
|
||||
* @param {string} skillCategory catégorie (man, animal, tool, weapon, survival, world)
|
||||
* @returns {number} -1, 0 ou +1
|
||||
*/
|
||||
static influenceModifier(actor, skillCategory) {
|
||||
const influence = this.getInfluence(actor);
|
||||
if (influence === "neutral" || !skillCategory) return 0;
|
||||
const cfg = CONFIG.VERMINE?.totemInfluence?.[influence];
|
||||
if (!cfg) return 0;
|
||||
if ((cfg.bonus || []).includes(skillCategory)) return 1;
|
||||
if ((cfg.malus || []).includes(skillCategory)) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique un gain d'un Dé de Totem selon les règles d'échange et de limites.
|
||||
* Retourne un objet décrivant ce qui s'est passé (pour notification).
|
||||
* @param {foundry.abstract.Document} actor
|
||||
* @param {'human'|'adapted'} totem
|
||||
* @param {number} [count=1] 1 ou 2 dés (acte grave)
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
static async gainDie(actor, totem, count = 1) {
|
||||
const human = actor.system.adaptation.totems.human;
|
||||
const adapted = actor.system.adaptation.totems.adapted;
|
||||
const actions = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const step = this._gainOneDie(human.value, adapted.value, totem);
|
||||
if (step.unchanged) {
|
||||
actions.push({ ok: false, reason: "max" });
|
||||
break;
|
||||
}
|
||||
human.value = step.human;
|
||||
adapted.value = step.adapted;
|
||||
actions.push({ ok: true, exchanged: step.exchanged, replaced: step.replaced });
|
||||
}
|
||||
|
||||
await actor.update({
|
||||
"system.adaptation.totems.human.value": human.value,
|
||||
"system.adaptation.totems.adapted.value": adapted.value
|
||||
});
|
||||
|
||||
return this._summarize(actions, totem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique une perte d'un Dé de Totem.
|
||||
* Règle : seul l'Humain a des Interdits. Briser un Interdit Humain fait
|
||||
* perdre 1D Humain ; si le personnage n'en possède pas, il gagne 1D Adapté.
|
||||
* @param {foundry.abstract.Document} actor
|
||||
* @param {'human'} totem
|
||||
* @param {number} [count=1] 1 ou 2 dés (acte grave)
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
static async loseDie(actor, totem, count = 1) {
|
||||
const human = actor.system.adaptation.totems.human;
|
||||
const adapted = actor.system.adaptation.totems.adapted;
|
||||
const actions = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (human.value > 0) {
|
||||
human.value -= 1;
|
||||
actions.push({ ok: true, lost: true });
|
||||
} else if (adapted.value < adapted.max && (human.value + adapted.value) < 5) {
|
||||
// Pas de Dé Humain : gagne un Dé Adapté à la place (limites appliquées).
|
||||
adapted.value += 1;
|
||||
actions.push({ ok: true, gainedAdapted: true });
|
||||
} else {
|
||||
actions.push({ ok: false, reason: "max" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await actor.update({
|
||||
"system.adaptation.totems.human.value": human.value,
|
||||
"system.adaptation.totems.adapted.value": adapted.value
|
||||
});
|
||||
|
||||
return this._summarize(actions, totem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute un Dé du Totem donné en respectant les limites et les échanges.
|
||||
* Règles :
|
||||
* - 3 Dés max du même Totem, 5 Dés max au total.
|
||||
* - Si le joueur gagne un Dé qu'il ne peut conserver, il peut supprimer un
|
||||
* Dé du Totem opposé à la place ; sinon le gain est annulé.
|
||||
* - Gagner un 4e Dé Adapté est impossible, mais supprime un Dé Humain.
|
||||
* @param {number} human
|
||||
* @param {number} adapted
|
||||
* @param {'human'|'adapted'} totem
|
||||
* @returns {Object}
|
||||
*/
|
||||
static _gainOneDie(human, adapted, totem) {
|
||||
const limitTotem = 3;
|
||||
const opposite = totem === "human" ? "adapted" : "human";
|
||||
const total = human + adapted;
|
||||
const current = totem === "human" ? human : adapted;
|
||||
const oppositeVal = totem === "human" ? adapted : human;
|
||||
|
||||
// Le Totem cible est déjà au maximum (3) : impossible d'empiler.
|
||||
if (current >= limitTotem) {
|
||||
if (oppositeVal > 0) {
|
||||
// Supprime un Dé du Totem opposé à la place (le gain n'augmente pas).
|
||||
const result = { human, adapted, unchanged: false, replaced: true };
|
||||
result[opposite] -= 1;
|
||||
return result;
|
||||
}
|
||||
return { human, adapted, unchanged: true };
|
||||
}
|
||||
|
||||
// Limite totale atteinte (5) : peut remplacer un Dé opposé.
|
||||
if (total >= 5) {
|
||||
if (oppositeVal > 0) {
|
||||
const result = { human, adapted, unchanged: false, exchanged: true };
|
||||
result[totem] += 1;
|
||||
result[opposite] -= 1;
|
||||
return result;
|
||||
}
|
||||
return { human, adapted, unchanged: true };
|
||||
}
|
||||
|
||||
// Gain normal.
|
||||
const result = { human, adapted, unchanged: false };
|
||||
result[totem] += 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Résume les actions appliquées en un libellé localisé.
|
||||
* @param {Array} actions
|
||||
* @param {'human'|'adapted'} totem
|
||||
* @returns {Object}
|
||||
*/
|
||||
static _summarize(actions, totem) {
|
||||
const gained = actions.filter(a => a.ok && !a.exchanged && !a.replaced && !a.lost && !a.gainedAdapted).length;
|
||||
const exchanged = actions.filter(a => a.exchanged).length;
|
||||
const replaced = actions.filter(a => a.replaced).length;
|
||||
const lost = actions.filter(a => a.lost).length;
|
||||
const gainedAdapted = actions.filter(a => a.gainedAdapted).length;
|
||||
const refused = actions.some(a => !a.ok);
|
||||
return { totem, gained, exchanged, replaced, lost, gainedAdapted, refused };
|
||||
}
|
||||
}
|
||||
@@ -37,12 +37,23 @@ Hooks.once('init', async function () {
|
||||
CONFIG.Actor.dataModels.npc = models.VermineNpcData;
|
||||
CONFIG.Actor.dataModels.group = models.VermineGroupData;
|
||||
CONFIG.Actor.dataModels.creature = models.VermineCreatureData;
|
||||
CONFIG.Actor.dataModels.community = models.VermineCommunityData;
|
||||
CONFIG.Actor.dataModels.vehicle = models.VermineVehicleData;
|
||||
|
||||
// Libellés des types "community"/"vehicle" (TYPES.Actor.* du core ne les connaît pas)
|
||||
CONFIG.Actor.typeLabels ??= {};
|
||||
CONFIG.Actor.typeLabels.community = "VERMINE.type_community";
|
||||
CONFIG.Actor.typeLabels.vehicle = "VERMINE.type_vehicle";
|
||||
Hooks.once('i18nInit', () => {
|
||||
CONFIG.Actor.typeLabels.community = "VERMINE.type_community";
|
||||
CONFIG.Actor.typeLabels.vehicle = "VERMINE.type_vehicle";
|
||||
});
|
||||
|
||||
CONFIG.Item.dataModels.item = models.VermineItemData;
|
||||
CONFIG.Item.dataModels.weapon = models.VermineWeaponData;
|
||||
CONFIG.Item.dataModels.defense = models.VermineDefenseData;
|
||||
CONFIG.Item.dataModels.vehicle = models.VermineVehicleData;
|
||||
CONFIG.Item.dataModels.ability = models.VermineAbilityData;
|
||||
CONFIG.Item.dataModels.creaturecapacity = models.VermineCreatureCapacityData;
|
||||
CONFIG.Item.dataModels.specialty = models.VermineSpecialtyData;
|
||||
CONFIG.Item.dataModels.background = models.VermineBackgroundData;
|
||||
CONFIG.Item.dataModels.trauma = models.VermineTraumaData;
|
||||
@@ -72,7 +83,7 @@ Hooks.once('init', async function () {
|
||||
// Register sheet application classes (ApplicationV2)
|
||||
// Unregister core sheets
|
||||
foundry.documents.collections.Actors.unregisterSheet("core", foundry.applications.sheets.ActorSheetV2, {
|
||||
types: ["character", "npc", "group", "creature"]
|
||||
types: ["character", "npc", "group", "creature", "community", "vehicle"]
|
||||
})
|
||||
foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1?.sheets?.ItemSheet)
|
||||
|
||||
@@ -89,6 +100,12 @@ Hooks.once('init', async function () {
|
||||
foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineGroupSheetV2, {
|
||||
types: ["group"], makeDefault: true, label: "VERMINE.Sheet.group"
|
||||
})
|
||||
foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineCommunitySheetV2, {
|
||||
types: ["community"], makeDefault: true, label: "VERMINE.Sheet.community"
|
||||
})
|
||||
foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineVehicleSheetV2, {
|
||||
types: ["vehicle"], makeDefault: true, label: "VERMINE.Sheet.vehicle"
|
||||
})
|
||||
|
||||
// Item sheets — un par type
|
||||
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineItemSheetV2, {
|
||||
@@ -100,12 +117,12 @@ Hooks.once('init', async function () {
|
||||
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineDefenseSheetV2, {
|
||||
types: ["defense"], makeDefault: true
|
||||
})
|
||||
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineVehicleSheetV2, {
|
||||
types: ["vehicle"], makeDefault: true
|
||||
})
|
||||
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineAbilitySheetV2, {
|
||||
types: ["ability"], makeDefault: true
|
||||
})
|
||||
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineCreatureCapacitySheetV2, {
|
||||
types: ["creaturecapacity"], makeDefault: true
|
||||
})
|
||||
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineSpecialtySheetV2, {
|
||||
types: ["specialty"], makeDefault: true
|
||||
})
|
||||
@@ -178,9 +195,8 @@ Hooks.once('init', async function () {
|
||||
"systems/vermine2047/templates/actor/appv2/character-stories.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/character-combat.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/npc-main.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/npc-characteristics.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/npc-skills.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/npc-threat.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/npc-traits.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/npc-equipment.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/npc-combat.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/npc-notes.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/group-main.hbs",
|
||||
@@ -192,7 +208,13 @@ Hooks.once('init', async function () {
|
||||
"systems/vermine2047/templates/actor/appv2/creature-info.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/creature-stats.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/creature-combat.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/creature-effects.hbs"
|
||||
"systems/vermine2047/templates/actor/appv2/creature-effects.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/community-main.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/community-identity.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/community-domains.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/community-resources.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/vehicle-main.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/vehicle-specs.hbs"
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 241 KiB |
|
Before Width: | Height: | Size: 329 KiB |
@@ -33,15 +33,17 @@
|
||||
"Actor": {
|
||||
"character": { "htmlFields": ["equipment.description", "identity.biography", "identity.relations"] },
|
||||
"npc": { "htmlFields": ["equipment.description", "identity.notes"] },
|
||||
"group": { "htmlFields": ["equipment.description", "identity.notes"] },
|
||||
"creature": { "htmlFields": ["equipment.description", "identity.notes", "identity.biography"] }
|
||||
"group": { "htmlFields": ["equipment.description", "identity.notes", "roadNotes"] },
|
||||
"creature": { "htmlFields": ["equipment.description", "identity.notes", "identity.biography"] },
|
||||
"community": { "htmlFields": ["identity.notes", "location.landmarks", "location.territory", "history.events", "history.relations"] },
|
||||
"vehicle": { "htmlFields": ["identity.notes"] }
|
||||
},
|
||||
"Item": {
|
||||
"item": { "htmlFields": ["description"] },
|
||||
"weapon": { "htmlFields": ["description"] },
|
||||
"defense": { "htmlFields": ["description"] },
|
||||
"vehicle": { "htmlFields": ["description"] },
|
||||
"ability": { "htmlFields": ["description"] },
|
||||
"creaturecapacity": { "htmlFields": ["description"] },
|
||||
"specialty": {},
|
||||
"background": { "htmlFields": ["description"] },
|
||||
"trauma": { "htmlFields": ["description"] },
|
||||
|
||||
@@ -221,6 +221,7 @@
|
||||
{{> "systems/vermine2047/templates/actor/parts/npc-skill-category.hbs"
|
||||
categoryKey=key
|
||||
categoryLabel=(concat "VERMINE.skill_category." key)
|
||||
skills=@root.system.skills
|
||||
}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="tab abilities sheet-part" data-group="sheet" data-tab="abilities">
|
||||
<div class="tab abilities sheet-part {{#if @root.tabs.abilities.active}}active{{/if}}" data-group="sheet" data-tab="abilities">
|
||||
{{!-- Character --}}
|
||||
<h3>{{ localize 'VERMINE.abilities' }}</h3>
|
||||
<div class="grid grid-4col">
|
||||
@@ -55,7 +55,7 @@
|
||||
>
|
||||
{{/if}}
|
||||
</h4>
|
||||
{{#each @root.system.skills as |skill skey|}}
|
||||
{{#each (sortedSkills @root.system.skills) as |skill skey|}}
|
||||
|
||||
{{#if (eq skill.category sckey) }}
|
||||
<div class="ability flexrow ">
|
||||
@@ -79,12 +79,10 @@
|
||||
data-action="addSpecialty"
|
||||
></i>
|
||||
{{/ifgt}}
|
||||
{{#each skill.specialties as |spe ind|}}
|
||||
{{#each @root.specialties as |spe|}}
|
||||
{{#ife spe.system.skill skey}}
|
||||
<i
|
||||
class="specialty"
|
||||
data-tooltip="spécialité utilisable"
|
||||
>{{spe.name}}</i>
|
||||
<span class="specialty" data-tooltip="spécialité utilisable"
|
||||
>{{spe.name}}<sup>{{spe.system.level}}</sup></span>
|
||||
{{/ife}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="tab combat sheet-part" data-group="sheet" data-tab="combat">
|
||||
<div class="tab combat sheet-part {{#if @root.tabs.combat.active}}active{{/if}}" data-group="sheet" data-tab="combat">
|
||||
<div class="align-center">
|
||||
<div class="shadow">
|
||||
<h4>situation de combat par défaut</h4>
|
||||
@@ -20,6 +20,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{> "systems/vermine2047/templates/actor/parts/actor-equipped-gear.hbs"}}
|
||||
<div class="flexrow">
|
||||
<div class="self-control-container flexcol align-center">
|
||||
<h4 class="align-center">
|
||||
@@ -42,7 +43,6 @@
|
||||
unavailable
|
||||
{{/ifgt}}">
|
||||
{{#iflteq @row @root.system.attributes.self_control.max }}
|
||||
{{#if @root.isEditMode}}
|
||||
<input
|
||||
type="radio"
|
||||
data-dtype="Number"
|
||||
@@ -56,7 +56,6 @@
|
||||
|
||||
"
|
||||
/>
|
||||
{{/if}}
|
||||
{{/iflteq}}
|
||||
|
||||
</div>
|
||||
@@ -88,7 +87,6 @@
|
||||
unavailable
|
||||
{{/ifgt}}">
|
||||
{{#iflteq @row @root.system.attributes.effort.max }}
|
||||
{{#if @root.isEditMode}}
|
||||
<input
|
||||
type="radio"
|
||||
data-dtype="Number"
|
||||
@@ -102,7 +100,6 @@
|
||||
|
||||
"
|
||||
/>
|
||||
{{/if}}
|
||||
{{/iflteq}}
|
||||
|
||||
</div>
|
||||
@@ -124,14 +121,13 @@
|
||||
<div class="hexa ability{{#iflteq @minorwoundmax @root.system.minorWound.value }}
|
||||
checked
|
||||
{{/iflteq}}">
|
||||
{{#if @root.isEditMode}}
|
||||
<input
|
||||
type="radio"
|
||||
data-dtype="Number"
|
||||
name="system.minorWound.value"
|
||||
value="{{@minorwoundmax}}"
|
||||
data-wound="minorWound"
|
||||
data-action="clickRadio"
|
||||
data-action="clickWound"
|
||||
class="
|
||||
{{#iflteq @minorwoundmax @root.system.minorWound.value }}
|
||||
checked
|
||||
@@ -141,7 +137,6 @@
|
||||
checked="true"
|
||||
{{/ife}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/repeat}}
|
||||
</div>
|
||||
@@ -155,14 +150,13 @@
|
||||
<div class="hexa ability{{#iflteq @majourwoundmax @root.system.majorWound.value }}
|
||||
checked
|
||||
{{/iflteq}}">
|
||||
{{#if @root.isEditMode}}
|
||||
<input
|
||||
type="radio"
|
||||
data-dtype="Number"
|
||||
name="system.majorWound.value"
|
||||
value="{{@majourwoundmax}}"
|
||||
data-wound="majorWound"
|
||||
data-action="clickRadio"
|
||||
data-action="clickWound"
|
||||
class="
|
||||
{{#iflteq @majourwoundmax @root.system.majorWound.value }}
|
||||
checked
|
||||
@@ -173,7 +167,6 @@
|
||||
checked="true"
|
||||
{{/ife}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
{{/repeat}}
|
||||
@@ -189,14 +182,13 @@
|
||||
<div class="hexa ability{{#iflteq @deadlywoundmax @root.system.deadlyWound.value }}
|
||||
checked
|
||||
{{/iflteq}}">
|
||||
{{#if @root.isEditMode}}
|
||||
<input
|
||||
type="radio"
|
||||
data-dtype="Number"
|
||||
name="system.deadlyWound.value"
|
||||
value="{{@deadlywoundmax}}"
|
||||
data-wound="deadlyWound"
|
||||
data-action="clickRadio"
|
||||
data-action="clickWound"
|
||||
class=" {{#iflteq @deadlywoundmax @root.system.deadlyWound.value }}
|
||||
checked
|
||||
{{/iflteq}}
|
||||
@@ -206,7 +198,6 @@
|
||||
checked="true"
|
||||
{{/ife}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/repeat}}
|
||||
|
||||
|
||||
@@ -1,90 +1,4 @@
|
||||
<div class="tab equipment sheet-part" data-group="sheet" data-tab="equipment">
|
||||
<div class="grid grid-2col">
|
||||
<div>
|
||||
<ol class="items-list">
|
||||
<li class="item flexrow items-header">
|
||||
<div
|
||||
class="item-name"
|
||||
style="flex:4;"
|
||||
>{{ localize
|
||||
'IDENTITY.name'}}</div>
|
||||
<div class="item-quantity">{{ localize 'VERMINE.qty'}}</div>
|
||||
<div class="item-weight">{{ localize 'VERMINE.weight'}}</div>
|
||||
<div class="item-controls">
|
||||
{{#if isEditMode}}
|
||||
<a
|
||||
class="item-control item-create"
|
||||
title="Create item"
|
||||
data-action="create"
|
||||
data-type="item"
|
||||
><i class="fas fa-plus"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{#each gear as |item id|}}
|
||||
<li
|
||||
class="item flexrow flex-group-center"
|
||||
data-item-id="{{item._id}}"
|
||||
>
|
||||
<div
|
||||
class="item-name"
|
||||
style="flex:4;"
|
||||
>
|
||||
<div class="item-image">
|
||||
<a
|
||||
class="item-control item-edit"
|
||||
data-action="edit"
|
||||
data-item-id="{{item._id}}"
|
||||
data-roll-type="item"
|
||||
><img
|
||||
src="{{item.img}}"
|
||||
title="{{item.name}}"
|
||||
width="24"
|
||||
height="24"
|
||||
/></a>
|
||||
</div>
|
||||
<a
|
||||
class="item-control item-edit"
|
||||
data-action="edit"
|
||||
data-item-id="{{item._id}}"
|
||||
data-tooltip="Edit Item"
|
||||
>{{item.name}} </a>
|
||||
|
||||
{{#if item.system.damages.value}}
|
||||
/
|
||||
<span data-tooltip="{{item.damagedLabel}}">{{{item.damagedIcon}}}</span>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">
|
||||
{{item.system.quantity}}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">
|
||||
{{item.system.weight}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="item-controls">
|
||||
{{#if @root.isEditMode}}
|
||||
<a
|
||||
class="item-control item-delete"
|
||||
data-action="delete"
|
||||
data-item-id="{{item._id}}"
|
||||
title="Delete Item"
|
||||
><i class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
<div>
|
||||
<h4>{{ localize 'IDENTITY.notes'}}</h4>
|
||||
{{formInput systemFields.equipment.fields.description enriched=enrichedEquipmentNotes value=system.equipment.description name="system.equipment.description" toggled=true}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab equipment sheet-part {{#if @root.tabs.equipment.active}}active{{/if}}" data-group="sheet" data-tab="equipment">
|
||||
<div class="weapons">
|
||||
<h4>{{localize 'ITEMS.weapons'}}</h4>
|
||||
{{> "systems/vermine2047/templates/actor/parts/actor-weapons.hbs"}}
|
||||
@@ -93,4 +7,128 @@
|
||||
<h4>{{localize 'ITEMS.defenses'}}</h4>
|
||||
{{> "systems/vermine2047/templates/actor/parts/actor-defenses.hbs"}}
|
||||
</div>
|
||||
<div class="objects">
|
||||
<h4>{{localize 'ITEMS.items'}}</h4>
|
||||
<ol class="items-list">
|
||||
<li class="item flexrow items-header">
|
||||
<div
|
||||
class="item-name"
|
||||
style="flex:4;"
|
||||
>{{ localize
|
||||
'IDENTITY.name'}}</div>
|
||||
<div class="item-quantity">{{ localize 'VERMINE.qty'}}</div>
|
||||
<div class="item-weight">{{ localize 'VERMINE.weight'}}</div>
|
||||
<div class="item-controls">
|
||||
<a
|
||||
class="item-control item-create"
|
||||
title="Create item"
|
||||
data-action="create"
|
||||
data-type="item"
|
||||
><i class="fas fa-plus"></i></a>
|
||||
</div>
|
||||
</li>
|
||||
{{#each gear as |item id|}}
|
||||
<li
|
||||
class="item flexrow flex-group-center"
|
||||
data-item-id="{{item._id}}"
|
||||
>
|
||||
<div
|
||||
class="item-name"
|
||||
style="flex:4;"
|
||||
>
|
||||
<div class="item-image">
|
||||
<a
|
||||
class="item-control item-edit"
|
||||
data-action="edit"
|
||||
data-item-id="{{item._id}}"
|
||||
data-roll-type="item"
|
||||
><img
|
||||
src="{{item.img}}"
|
||||
title="{{item.name}}"
|
||||
width="24"
|
||||
height="24"
|
||||
/></a>
|
||||
</div>
|
||||
<a
|
||||
class="item-control item-edit"
|
||||
data-action="edit"
|
||||
data-item-id="{{item._id}}"
|
||||
data-tooltip="Edit Item"
|
||||
>{{item.name}} </a>
|
||||
|
||||
{{#if item.system.damages.value}}
|
||||
/
|
||||
<span data-tooltip="{{item.damagedLabel}}">{{{item.damagedIcon}}}</span>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">
|
||||
{{item.system.quantity}}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">
|
||||
{{item.system.weight}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="item-controls">
|
||||
{{#if @root.isEditMode}}
|
||||
<a
|
||||
class="item-control item-delete"
|
||||
data-action="delete"
|
||||
data-item-id="{{item._id}}"
|
||||
title="Delete Item"
|
||||
><i class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
<div class="vehicles">
|
||||
<h4>{{localize 'ITEMS.vehicles'}}</h4>
|
||||
<ol class="items-list">
|
||||
<li class="item flexrow items-header">
|
||||
<div class="item-name" style="flex:4;">{{ localize 'IDENTITY.name'}}</div>
|
||||
<div class="item-mobility">{{ localize 'VERMINE.mobility'}}</div>
|
||||
<div class="item-rarity">{{ localize 'VERMINE.rarity'}}</div>
|
||||
<div class="item-reliability">{{ localize 'VERMINE.reliability'}}</div>
|
||||
<div class="item-controls">
|
||||
{{#if isEditMode}}
|
||||
<a class="item-control item-create" data-action="createVehicle" title="Create vehicle"><i class="fas fa-plus"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{#each vehicles as |vehicle|}}
|
||||
<li class="item flexrow flex-group-center" data-vehicle-id="{{vehicle.id}}">
|
||||
<div class="item-name" style="flex:4;">
|
||||
<div class="item-image">
|
||||
<a class="item-control item-edit" data-action="editVehicle" data-vehicle-id="{{vehicle.id}}"><img
|
||||
src="{{vehicle.img}}" title="{{vehicle.name}}" width="24"
|
||||
height="24" /></a>
|
||||
</div>
|
||||
<a class="item-control item-edit" data-action="editVehicle" data-vehicle-id="{{vehicle.id}}" title="Edit Vehicle">{{vehicle.name}}</a>
|
||||
</div>
|
||||
<div><a class="item-control item-edit" data-action="editVehicle" data-vehicle-id="{{vehicle.id}}" title="Edit Vehicle">{{vehicle.system.mobility}}</a></div>
|
||||
<div><a class="item-control item-edit" data-action="editVehicle" data-vehicle-id="{{vehicle.id}}" title="Edit Vehicle">{{vehicle.system.rarity.value}}</a></div>
|
||||
<div><a class="item-control item-edit" data-action="editVehicle" data-vehicle-id="{{vehicle.id}}" title="Edit Vehicle">{{vehicle.system.reliability}}</a></div>
|
||||
<div class="item-controls">
|
||||
{{#if @root.isEditMode}}
|
||||
<a class="item-control item-delete" data-action="deleteVehicle" data-vehicle-id="{{vehicle.id}}" title="Delete Vehicle"><i
|
||||
class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="item flexrow flex-group-center">
|
||||
<span class="empty">{{ localize 'VERMINE.no_vehicles' }}</span>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
<div class="equipment-notes">
|
||||
<h4>{{ localize 'IDENTITY.notes'}}</h4>
|
||||
{{formInput systemFields.equipment.fields.description enriched=enrichedEquipmentNotes value=system.equipment.description name="system.equipment.description" toggled=true}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<div class="tab experience sheet-part {{#if @root.tabs.experience.active}}active{{/if}}" data-group="sheet" data-tab="experience">
|
||||
<h3>{{ localize 'VERMINE.experience' }}</h3>
|
||||
|
||||
<div class="experience-summary">
|
||||
<div class="exp-stat">
|
||||
<label>{{ localize 'VERMINE.experience_dice' }}</label>
|
||||
<span class="exp-value">{{ system.experience.dice }}D</span>
|
||||
</div>
|
||||
<div class="exp-stat">
|
||||
<label>{{ localize 'VERMINE.experience_phase' }}</label>
|
||||
<span class="exp-value">{{ system.experience.phase }}</span>
|
||||
</div>
|
||||
<div class="exp-stat">
|
||||
<label>{{ localize 'VERMINE.evolution_dice' }}</label>
|
||||
<span class="exp-value">{{ system.experience.evolution }}D</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flexrow exp-actions">
|
||||
{{#if isGM}}
|
||||
<button type="button" data-action="openExperiencePhase">
|
||||
<i class="fas fa-star"></i> {{ localize 'VERMINE.open_experience_phase' }}
|
||||
</button>
|
||||
{{/if}}
|
||||
<button type="button" data-action="openLearning">
|
||||
<i class="fas fa-graduation-cap"></i> {{ localize 'VERMINE.open_learning' }}
|
||||
</button>
|
||||
<button type="button" data-action="validatePhase">
|
||||
<i class="fas fa-check"></i> {{ localize 'VERMINE.validate_phase' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section class="exp-rewards">
|
||||
<h4>{{ localize 'VERMINE.collective_rewards' }} <span class="exp-total">({{ collectiveTotal }}D)</span></h4>
|
||||
<div class="exp-reward-row">
|
||||
<label>{{ localize 'VERMINE.danger' }}</label>
|
||||
<span>{{ system.experience.collective.danger }}D</span>
|
||||
<label>{{ localize 'VERMINE.discoveries' }}</label>
|
||||
<span>{{ system.experience.collective.discoveries }}D</span>
|
||||
<label>{{ localize 'VERMINE.duration' }}</label>
|
||||
<span>{{ system.experience.collective.duration }}D</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="exp-rewards">
|
||||
<h4>{{ localize 'VERMINE.individual_rewards' }} <span class="exp-total">({{ individualTotal }}D)</span></h4>
|
||||
<div class="exp-reward-row">
|
||||
<label>{{ localize 'VERMINE.implication' }}</label>
|
||||
<span>{{ system.experience.individual.implication }}D</span>
|
||||
<label>{{ localize 'VERMINE.influence' }}</label>
|
||||
<span>{{ system.experience.individual.influence }}D</span>
|
||||
<label>{{ localize 'VERMINE.interpretation' }}</label>
|
||||
<span>{{ system.experience.individual.interpretation }}D</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="exp-rewards">
|
||||
<h4>{{ localize 'VERMINE.learning_limits' }}</h4>
|
||||
<div class="exp-learned">
|
||||
<span class="{{#if system.experience.learned.skill}}learned{{/if}}">{{ localize 'VERMINE.learned_skill' }}</span>
|
||||
<span class="{{#if system.experience.learned.specialty}}learned{{/if}}">{{ localize 'VERMINE.learned_specialty' }}</span>
|
||||
<span class="{{#if system.experience.learned.reserve}}learned{{/if}}">{{ localize 'VERMINE.learned_reserve' }}</span>
|
||||
<span class="{{#if system.experience.learned.characteristic}}learned{{/if}}">{{ localize 'VERMINE.learned_characteristic' }}</span>
|
||||
<span class="{{#if system.experience.learned.evolution}}learned{{/if}}">{{ localize 'VERMINE.learned_evolution' }}</span>
|
||||
<span class="{{#if system.experience.learned.ability}}learned{{/if}}">{{ localize 'VERMINE.learned_ability' }}</span>
|
||||
<span class="{{#if system.experience.learned.background}}learned{{/if}}">{{ localize 'VERMINE.learned_background' }}</span>
|
||||
<span class="{{#if system.experience.learned.reputation}}learned{{/if}}">{{ localize 'VERMINE.learned_reputation' }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="exp-rewards">
|
||||
<h4>{{ localize 'VERMINE.trauma_reward' }}</h4>
|
||||
<div class="exp-learned">
|
||||
<span class="{{#if system.experience.traumaReward}}learned{{/if}}">{{ localize 'VERMINE.trauma_reward_5' }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -47,14 +47,6 @@
|
||||
{{else}}
|
||||
<span>{{system.attributes.reputation.value}}</span>
|
||||
{{/if}}
|
||||
|
||||
<label>{{ localize 'VERMINE.experience' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input name="system.attributes.xp.value" data-type="Number" type="number"
|
||||
value="{{system.attributes.xp.value}}" />
|
||||
{{else}}
|
||||
<span>{{system.attributes.xp.value}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
||||
</header>
|
||||
|
||||
@@ -74,6 +74,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Influence + gestion des Dés de Totems --}}
|
||||
<div class="totem-influence-row">
|
||||
<span class="totem-influence-label">
|
||||
{{ localize "VERMINE.influence" }}:
|
||||
{{#ifgt system.adaptation.totems.human.value system.adaptation.totems.adapted.value}}
|
||||
<strong class="totem-human">{{ localize "TOTEMS.human.name" }}</strong>
|
||||
{{else ifgt system.adaptation.totems.adapted.value system.adaptation.totems.human.value}}
|
||||
<strong class="totem-adapted">{{ localize "TOTEMS.adapted.name" }}</strong>
|
||||
{{else}}
|
||||
<strong>{{ localize "VERMINE.totem_neutral" }}</strong>
|
||||
{{/ifgt}}
|
||||
</span>
|
||||
<button type="button" class="totem-manage-btn" data-action="openTotemDice"
|
||||
title="{{ localize 'VERMINE.totem_manage' }}"><i class="fas fa-sliders"></i></button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<ul class="padding-with-frieze unstyled paper">
|
||||
{{#if system.identity.totem }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="tab stories sheet-part" data-group="sheet" data-tab="stories">
|
||||
<div class="tab stories sheet-part {{#if @root.tabs.stories.active}}active{{/if}}" data-group="sheet" data-tab="stories">
|
||||
<section class="flexrow flex-group-left flex-align-left gap-md">
|
||||
<div>
|
||||
<h4>{{ localize 'IDENTITY.theme'}}</h4>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="tab totem sheet-part" data-group="sheet" data-tab="totem">
|
||||
<div class="tab totem sheet-part {{#if @root.tabs.totem.active}}active{{/if}}" data-group="sheet" data-tab="totem">
|
||||
{{#if system.identity.totem}}
|
||||
<div class="totem-details">
|
||||
<img
|
||||
@@ -13,10 +13,10 @@
|
||||
<p style="font-size:80%;">{{smarttl "TOTEMS" system.identity.totem "description"}}</p>
|
||||
<div class="grid grid-2col">
|
||||
<div class>
|
||||
<div class="item-name"><strong>Principes</strong><br />{{smarttl "TOTEMS" system.identity.totem "instincts"}}</div>
|
||||
<div class="item-name"><strong>{{ localize 'IDENTITY.instincts' }}</strong><br />{{smarttl "TOTEMS" system.identity.totem "instincts"}}</div>
|
||||
</div>
|
||||
<div class>
|
||||
<div class="item-name"><strong>Interdits</strong><br />{{smarttl "TOTEMS" system.identity.totem "bans"}}</div>
|
||||
<div class="item-name"><strong>{{ localize 'IDENTITY.prohibits' }}</strong><br />{{smarttl "TOTEMS" system.identity.totem "bans"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -28,6 +28,11 @@
|
||||
|
||||
{{/if}}
|
||||
|
||||
<div class="chaman-toggle flexrow flex-group-center">
|
||||
<label for="identity-chaman">{{ localize 'VERMINE.chaman' }}</label>
|
||||
<input type="checkbox" id="identity-chaman" name="system.identity.chaman" data-dtype="Boolean" {{#if system.identity.chaman}}checked{{/if}} />
|
||||
</div>
|
||||
|
||||
{{!-- Abstract Items --}}
|
||||
<div class="grid grid-2col">
|
||||
<div>
|
||||
@@ -54,7 +59,7 @@
|
||||
data-tooltip="niveau">{{item.system.level.value}}</span>
|
||||
<span
|
||||
data-tooltip="apprentissage">
|
||||
{{item.system.learn.threshold}}/{{item.system.learn.hindrance}}
|
||||
{{item.system.learn.threshold}}{{#ifgt item.system.learn.hindrance 0}}/{{ roman item.system.learn.hindrance }}{{/ifgt}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="item-controls flexrow">
|
||||
@@ -92,7 +97,6 @@
|
||||
data-action="edit"
|
||||
data-item-id="{{item._id}}"
|
||||
title="Edit Item">{{item.name}}</a>
|
||||
<span data-tooltip="Compétence">{{item.system.skill}}</span>
|
||||
</div>
|
||||
<div class="item-controls flexrow">
|
||||
{{#if item.system.description}}
|
||||
@@ -187,13 +191,54 @@
|
||||
{{#if isEditMode}}
|
||||
<a
|
||||
class="item-control item-create"
|
||||
title="Create item"
|
||||
data-action="create"
|
||||
data-type="evolution"><i class="fas fa-plus"></i></a>
|
||||
title="Buy evolution"
|
||||
data-action="buyEvolution"><i class="fas fa-cart-shopping"></i></a>
|
||||
{{/if}}
|
||||
</h4>
|
||||
<ol class="list-item">
|
||||
{{#each evolutions as |item id|}}
|
||||
<li class="item flexrow" data-item-id="{{item._id}}">
|
||||
<div class="item-name" style="flex:4;">
|
||||
<a
|
||||
class="item-control item-edit"
|
||||
data-action="edit"
|
||||
data-item-id="{{item._id}}"
|
||||
title="Edit Item">{{item.name}}</a>
|
||||
{{#if item.system.mutation}}
|
||||
<span class="evolution-badge mutation">{{ localize 'VERMINE.evolution_mutation' }}</span>
|
||||
{{else}}
|
||||
<span class="evolution-badge adaptation">{{ localize 'VERMINE.evolution_adaptation' }}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="item-controls flexrow">
|
||||
{{#if item.system.description}}
|
||||
<i
|
||||
class="fas fa-circle-question"
|
||||
data-tooltip="{{item.system.description}}"></i>
|
||||
{{/if}}
|
||||
{{#if @root.isEditMode}}
|
||||
<a class="item-control item-delete" data-action="delete" data-item-id="{{item._id}}" title="Delete Item"><i
|
||||
class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
{{#if system.identity.chaman}}
|
||||
<div>
|
||||
<h4>
|
||||
<span>{{localize "ITEMS.rites"}}</span>
|
||||
{{#if isEditMode}}
|
||||
<a
|
||||
class="item-control item-create"
|
||||
title="Create item"
|
||||
data-action="create"
|
||||
data-type="rite"><i class="fas fa-plus"></i></a>
|
||||
{{/if}}
|
||||
</h4>
|
||||
<ol class="list-item">
|
||||
{{#each rites as |item id|}}
|
||||
<li class="item flexrow" data-item-id="{{item._id}}">
|
||||
<div class="item-name" style="flex:4;">
|
||||
<a
|
||||
@@ -217,5 +262,6 @@
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<div class="tab domains sheet-part {{#if @root.tabs.domains.active}}active{{/if}}" data-group="sheet" data-tab="domains">
|
||||
<section class="community-section">
|
||||
<h3>{{ localize 'COMMUNITY.size_title' }}</h3>
|
||||
<div class="flexrow community-size">
|
||||
<div class="form-group" style="flex: 1;">
|
||||
<label for="system.size.value">{{ localize 'COMMUNITY.size_label' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<select name="system.size.value" data-dtype="Number">
|
||||
{{#each sizeOptions as |cfg level|}}
|
||||
<option value="{{ level }}" {{#ife @root.system.size.value level}}selected{{/ife}}>{{ localize cfg.label }}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
{{else}}
|
||||
<span>{{ sizeLabel }}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="form-group" style="flex: 1;">
|
||||
<label for="system.population">{{ localize 'COMMUNITY.population' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="number" name="system.population" value="{{system.population}}" data-dtype="Number" min="0" />
|
||||
{{else}}
|
||||
<span>{{system.population}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{#if sizeInfo}}
|
||||
<p class="community-size-info">
|
||||
{{ localize 'COMMUNITY.population_range' }}: <strong>{{ sizeInfo.population }}</strong> ·
|
||||
{{ localize 'COMMUNITY.figures' }}: <strong>{{ sizeInfo.figures }}+</strong> ·
|
||||
{{ localize 'COMMUNITY.companies' }}: <strong>{{ sizeInfo.companies }}+</strong>
|
||||
</p>
|
||||
{{/if}}
|
||||
</section>
|
||||
|
||||
{{#each domains}}
|
||||
<section class="community-section">
|
||||
<h3>{{ this.label }}</h3>
|
||||
<div class="grid grid-2col">
|
||||
{{#each this.sections}}
|
||||
<div class="gauge {{ this.state }}">
|
||||
<label for="system.domains.{{ this.domainKey }}.{{ this.key }}.value">{{ this.label }}</label>
|
||||
{{#if @root.isEditMode}}
|
||||
<input type="number" name="system.domains.{{ this.domainKey }}.{{ this.key }}.value"
|
||||
value="{{ this.gauge.value }}" data-dtype="Number" min="0" />
|
||||
{{else}}
|
||||
<span class="gauge-value">{{ this.gauge.value }}D</span>
|
||||
{{/if}}
|
||||
<span class="gauge-state {{ this.state }}">{{ this.stateLabel }}</span>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</section>
|
||||
{{/each}}
|
||||
</div>
|
||||
@@ -0,0 +1,129 @@
|
||||
<div class="tab identity sheet-part {{#if @root.tabs.identity.active}}active{{/if}}" data-group="sheet" data-tab="identity">
|
||||
<section class="community-section">
|
||||
<h3>{{ localize 'COMMUNITY.identity' }}</h3>
|
||||
<div class="grid grid-2col">
|
||||
<div class="form-group">
|
||||
<label for="system.identity.theme">{{ localize 'COMMUNITY.theme' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.identity.theme" value="{{system.identity.theme}}" data-dtype="String" />
|
||||
{{else}}
|
||||
<span>{{system.identity.theme}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="system.identity.age">{{ localize 'COMMUNITY.age_label' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<select name="system.identity.age" data-dtype="String">
|
||||
{{#each ageOptions}}
|
||||
<option value="{{ this.key }}" {{#ife @root.system.identity.age this.key}}selected{{/ife}}>{{ this.label }}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
{{else}}
|
||||
<span>{{#each ageOptions}}{{#ife @root.system.identity.age this.key}}{{ this.label }}{{/ife}}{{/each}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ localize 'COMMUNITY.instincts' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.identity.instincts" value="{{system.identity.instincts}}" data-dtype="String" />
|
||||
{{else}}
|
||||
<span>{{system.identity.instincts}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ localize 'COMMUNITY.prohibits' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.identity.prohibits" value="{{system.identity.prohibits}}" data-dtype="String" />
|
||||
{{else}}
|
||||
<span>{{system.identity.prohibits}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="system.identity.notes">{{ localize 'COMMUNITY.notes' }}</label>
|
||||
{{formInput systemFields.identity.fields.notes enriched=enrichedNotes value=system.identity.notes name="system.identity.notes" toggled=true}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="community-section">
|
||||
<h3>{{ localize 'COMMUNITY.location' }}</h3>
|
||||
<div class="grid grid-2col">
|
||||
<div class="form-group">
|
||||
<label for="system.location.sector">{{ localize 'COMMUNITY.sector' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.location.sector" value="{{system.location.sector}}" data-dtype="String" />
|
||||
{{else}}
|
||||
<span>{{system.location.sector}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="system.location.placeType">{{ localize 'COMMUNITY.placeType' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.location.placeType" value="{{system.location.placeType}}" data-dtype="String" />
|
||||
{{else}}
|
||||
<span>{{system.location.placeType}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="system.location.landmarks">{{ localize 'COMMUNITY.landmarks' }}</label>
|
||||
{{formInput systemFields.location.fields.landmarks enriched=enrichedLandmarks value=system.location.landmarks name="system.location.landmarks" toggled=true}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="system.location.territory">{{ localize 'COMMUNITY.territory' }}</label>
|
||||
{{formInput systemFields.location.fields.territory enriched=enrichedTerritory value=system.location.territory name="system.location.territory" toggled=true}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="community-section">
|
||||
<h3>{{ localize 'COMMUNITY.history' }}</h3>
|
||||
<div class="form-group">
|
||||
<label for="system.history.founding">{{ localize 'COMMUNITY.founding' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.history.founding" value="{{system.history.founding}}" data-dtype="String" />
|
||||
{{else}}
|
||||
<span>{{system.history.founding}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="system.history.events">{{ localize 'COMMUNITY.events' }}</label>
|
||||
{{formInput systemFields.history.fields.events enriched=enrichedEvents value=system.history.events name="system.history.events" toggled=true}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="system.history.relations">{{ localize 'COMMUNITY.history_relations' }}</label>
|
||||
{{formInput systemFields.history.fields.relations enriched=enrichedRelations value=system.history.relations name="system.history.relations" toggled=true}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="community-section">
|
||||
<h3>{{ localize 'COMMUNITY.objectives' }}</h3>
|
||||
<div class="form-group">
|
||||
<label for="system.objectives.major">{{ localize 'COMMUNITY.objective_major' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.objectives.major" value="{{system.objectives.major}}" data-dtype="String" />
|
||||
{{else}}
|
||||
<span>{{system.objectives.major}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ localize 'COMMUNITY.objectives_minor' }}</label>
|
||||
<ol class="items-list">
|
||||
{{#each system.objectives.minor as |obj index|}}
|
||||
<li class="item flexrow" data-index="{{index}}">
|
||||
{{#if @root.isEditMode}}
|
||||
<input type="text" name="system.objectives.minor.{{index}}" value="{{obj}}" data-dtype="String" style="flex: 1;" />
|
||||
<a data-action="deleteObjective" data-index="{{index}}" title="{{ localize 'VERMINE.delete' }}"><i class="fas fa-trash"></i></a>
|
||||
{{else}}
|
||||
<span style="flex: 1;">{{obj}}</span>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/each}}
|
||||
{{#if isEditMode}}
|
||||
<li class="item flexrow">
|
||||
<a data-action="addObjective" title="{{ localize 'VERMINE.add' }}"><i class="fas fa-plus"></i> {{ localize 'COMMUNITY.add_objective' }}</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,71 @@
|
||||
<div class="tab main sheet-part" data-group="sheet" data-tab="main">
|
||||
{{!-- Toggle Edit/Play --}}
|
||||
<div class="sheet-header-toggle">
|
||||
{{#if isEditMode}}
|
||||
<button type="button" data-action="toggleSheet">{{localize "VERMINE.playMode"}}</button>
|
||||
{{else}}
|
||||
<button type="button" data-action="toggleSheet">{{localize "VERMINE.editMode"}}</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{!-- HEADER --}}
|
||||
<header class="char-header grid grid-12col w-full">
|
||||
<div class="grid-span-2 community-portrait">
|
||||
<img class="profile-img" src="{{actor.img}}" data-action="editImage" data-edit="img" width="110"
|
||||
height="110" title="{{actor.name}}" />
|
||||
</div>
|
||||
|
||||
<section class="char-details grid-span-6">
|
||||
<h1 class="char-name flexrow flex-group-left w-full">
|
||||
<label>{{ localize 'IDENTITY.name' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input name="name" type="text" value="{{actor.name}}" placeholder="{{ localize 'COMMUNITY.name_placeholder' }}" />
|
||||
{{else}}
|
||||
<span>{{actor.name}}</span>
|
||||
{{/if}}
|
||||
</h1>
|
||||
<div class="profile flexrow flex-group-center">
|
||||
<label>{{ localize 'IDENTITY.profile' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input name="system.identity.profile" type="text" value="{{system.identity.profile}}" data-dtype="String" />
|
||||
{{else}}
|
||||
<span>{{#if system.identity.profile}}{{system.identity.profile}}{{else}}{{ localize 'COMMUNITY.profile_hint' }}{{/if}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="origin flexrow flex-group-center">
|
||||
<label>{{ localize 'COMMUNITY.origin' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input name="system.identity.origin" type="text" value="{{system.identity.origin}}" data-dtype="String" />
|
||||
{{else}}
|
||||
<span>{{system.identity.origin}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="char-level grid-span-4">
|
||||
<h1 class="char-vermine2047 flexrow flex-group-left w-full">
|
||||
{{#if (eq system.identity.totem "")}}
|
||||
<a data-action="chooseTotem" class="chooseTotem">{{ localize 'COMMUNITY.choose_totem' }}…</a>
|
||||
{{ else }}
|
||||
<a data-action="chooseTotem" class="chooseTotem">{{ smarttl "TOTEMS" system.identity.totem }}</a>
|
||||
{{/if}}
|
||||
</h1>
|
||||
<div class="level flexrow flex-group-center">
|
||||
<label>{{ localize 'VERMINE.level' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input name="system.level.value" type="number" value="{{system.level.value}}" data-dtype="Number" min="{{system.level.min}}" max="{{system.level.max}}" />
|
||||
{{else}}
|
||||
<span>{{system.level.value}}</span>
|
||||
{{/if}}
|
||||
<span>/ {{system.level.max}}</span>
|
||||
</div>
|
||||
</section>
|
||||
</header>
|
||||
|
||||
{{#if resolvedGroup}}
|
||||
<div class="community-linked-group">
|
||||
<span class="label">{{ localize 'COMMUNITY.linked_group' }}:</span>
|
||||
<span class="value">{{ resolvedGroup.name }}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -0,0 +1,63 @@
|
||||
<div class="tab resources sheet-part {{#if @root.tabs.resources.active}}active{{/if}}" data-group="sheet" data-tab="resources">
|
||||
<section class="community-section">
|
||||
<h3>{{ localize 'COMMUNITY.reserves' }}</h3>
|
||||
<div class="grid grid-3col">
|
||||
<div class="form-group">
|
||||
<label for="system.reserve.value">{{ localize 'COMMUNITY.group_reserve' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="number" name="system.reserve.value" value="{{system.reserve.value}}" data-dtype="Number" min="0" max="{{system.reserve.max}}" />
|
||||
{{else}}
|
||||
<span>{{system.reserve.value}} / {{system.reserve.max}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="system.material">{{ localize 'COMMUNITY.material_dice' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="number" name="system.material" value="{{system.material}}" data-dtype="Number" min="0" />
|
||||
{{else}}
|
||||
<span>{{system.material}}D</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="system.experience.dice">{{ localize 'COMMUNITY.experience_dice' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="number" name="system.experience.dice" value="{{system.experience.dice}}" data-dtype="Number" min="0" />
|
||||
{{else}}
|
||||
<span>{{system.experience.dice}}D</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="community-section">
|
||||
<h3>{{ localize 'COMMUNITY.morale' }}</h3>
|
||||
<div class="flexrow">
|
||||
<div class="form-group" style="flex: 1;">
|
||||
<label for="system.morale.value">{{ localize 'COMMUNITY.morale_value' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="number" name="system.morale.value" value="{{system.morale.value}}" data-dtype="Number" min="0" max="7" />
|
||||
{{else}}
|
||||
<span>{{system.morale.value}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="form-group" style="flex: 1;">
|
||||
<label>{{ localize 'COMMUNITY.morale_state' }}</label>
|
||||
<span class="morale-level {{ system.morale.level }}">{{ localize (concat 'COMMUNITY.morale_levels.' system.morale.level) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{#if system.allDomainsLow}}
|
||||
<p class="community-warning">{{ localize 'COMMUNITY.morale_blocked' }}</p>
|
||||
{{/if}}
|
||||
</section>
|
||||
|
||||
<section class="community-section">
|
||||
<h3>{{ localize 'COMMUNITY.habitants_title' }}</h3>
|
||||
<p class="community-hint">{{ localize 'COMMUNITY.habitants_coming' }}</p>
|
||||
{{#if resolvedGroup}}
|
||||
<p class="community-linked-group">
|
||||
<span class="label">{{ localize 'COMMUNITY.linked_group' }}:</span>
|
||||
<span class="value">{{ resolvedGroup.name }}</span>
|
||||
</p>
|
||||
{{/if}}
|
||||
</section>
|
||||
</div>
|
||||
@@ -1,4 +1,6 @@
|
||||
<div class="tab combat sheet-part" data-group="sheet" data-tab="combat">
|
||||
<div class="tab combat sheet-part {{#if @root.tabs.combat.active}}active{{/if}}" data-group="sheet" data-tab="combat">
|
||||
{{> "systems/vermine2047/templates/actor/parts/actor-equipped-gear.hbs"}}
|
||||
|
||||
<div class="grid grid-2col">
|
||||
{{!-- Computed Values --}}
|
||||
<div>
|
||||
@@ -37,36 +39,54 @@
|
||||
<ul class="unstyled">
|
||||
<li class="row mdb">
|
||||
{{ localize 'VERMINE.wounds.light' }} ({{ system.minorWound.threshold }}D)
|
||||
{{#repeat system.minorWound.max }}
|
||||
<input type="radio" data-dtype="Number" name="system.minorWound.value"
|
||||
value="{{this}}" data-action="clickRadio" data-wound="minorWound"
|
||||
{{#ife @root.system.minorWound.value this }}checked="checked"{{/ife}}
|
||||
class="{{#iflteq this @root.system.minorWound.value }}checked{{/iflteq}}"
|
||||
/>
|
||||
{{/repeat}}
|
||||
<div class="flexrow align-center">
|
||||
{{#repeat system.minorWound.max 1 "minorwoundmax"}}
|
||||
<div class="hexa ability{{#iflteq @minorwoundmax @root.system.minorWound.value }} checked{{/iflteq}}">
|
||||
<input type="radio" data-dtype="Number" name="system.minorWound.value"
|
||||
value="{{@minorwoundmax}}" data-action="clickWound" data-wound="minorWound"
|
||||
class="{{#iflteq @minorwoundmax @root.system.minorWound.value }}checked{{/iflteq}}"
|
||||
{{#ife @minorwoundmax @root.system.minorWound.value }}checked="checked"{{/ife}}
|
||||
/>
|
||||
</div>
|
||||
{{/repeat}}
|
||||
</div>
|
||||
</li>
|
||||
<li class="row mdb">
|
||||
{{ localize 'VERMINE.wounds.heavy' }} ({{ system.majorWound.threshold }}D)
|
||||
{{#repeat system.majorWound.max }}
|
||||
<input type="radio" name="system.majorWound.value" value="{{this}}" data-action="clickRadio" data-wound="majorWound"
|
||||
{{#ife @root.system.majorWound.value this }}checked="checked"{{/ife}}
|
||||
class="{{#iflteq this @root.system.majorWound.value }}checked{{/iflteq}}"
|
||||
/>
|
||||
{{/repeat}}
|
||||
<div class="flexrow align-center">
|
||||
{{#repeat system.majorWound.max 1 "majorwoundmax"}}
|
||||
<div class="hexa ability{{#iflteq @majorwoundmax @root.system.majorWound.value }} checked{{/iflteq}}">
|
||||
<input type="radio" name="system.majorWound.value" value="{{@majorwoundmax}}" data-action="clickWound" data-wound="majorWound"
|
||||
class="{{#iflteq @majorwoundmax @root.system.majorWound.value }}checked{{/iflteq}}"
|
||||
{{#ife @majorwoundmax @root.system.majorWound.value }}checked="checked"{{/ife}}
|
||||
/>
|
||||
</div>
|
||||
{{/repeat}}
|
||||
</div>
|
||||
</li>
|
||||
<li class="row mdb">
|
||||
{{ localize 'VERMINE.wounds.deadly' }} ({{ system.deadlyWound.threshold }}D)
|
||||
{{#repeat system.deadlyWound.max }}
|
||||
<input type="radio" name="system.deadlyWound.value" value="{{this}}" data-action="clickRadio" data-wound="deadlyWound"
|
||||
{{#ife @root.system.deadlyWound.value this }}checked="checked"{{/ife}}
|
||||
class="{{#iflteq this @root.system.deadlyWound.value }}checked{{/iflteq}}"
|
||||
/>
|
||||
{{/repeat}}
|
||||
<div class="flexrow align-center">
|
||||
{{#repeat system.deadlyWound.max 1 "deadlywoundmax"}}
|
||||
<div class="hexa ability{{#iflteq @deadlywoundmax @root.system.deadlyWound.value }} checked{{/iflteq}}">
|
||||
<input type="radio" name="system.deadlyWound.value" value="{{@deadlywoundmax}}" data-action="clickWound" data-wound="deadlyWound"
|
||||
class="{{#iflteq @deadlywoundmax @root.system.deadlyWound.value }}checked{{/iflteq}}"
|
||||
{{#ife @deadlywoundmax @root.system.deadlyWound.value }}checked="checked"{{/ife}}
|
||||
/>
|
||||
</div>
|
||||
{{/repeat}}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Armes --}}
|
||||
<div class="grid-span-2 weapons">
|
||||
<h4 class="item-name flexrow">{{ localize 'ITEMS.weapons' }}</h4>
|
||||
{{> "systems/vermine2047/templates/actor/parts/actor-weapons.hbs"}}
|
||||
</div>
|
||||
|
||||
{{!-- Equipment --}}
|
||||
<div class="grid-span-2">
|
||||
<h4>{{ localize 'VERMINE.gear' }}</h4>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="tab effects sheet-part" data-group="sheet" data-tab="effects">
|
||||
<div class="tab effects sheet-part {{#if @root.tabs.effects.active}}active{{/if}}" data-group="sheet" data-tab="effects">
|
||||
<h4 class="item-name effect-name flexrow">{{ localize "UI.effects.name" }}</h4>
|
||||
<ol class="items-list effects-list">
|
||||
{{#each effects as |section sid|}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="tab info sheet-part" data-group="sheet" data-tab="info">
|
||||
<div class="tab info sheet-part {{#if @root.tabs.info.active}}active{{/if}}" data-group="sheet" data-tab="info">
|
||||
<div class="grid grid-2col">
|
||||
<div>
|
||||
<h4>{{ localize 'IDENTITY.profile' }}</h4>
|
||||
@@ -38,6 +38,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Capacités de créature --}}
|
||||
<div class="grid-span-2">
|
||||
<h4>{{ localize 'VERMINE.creature_capacities' }}{{#if isEditMode}}<a class="item-control item-create" data-action="create" data-type="creaturecapacity" title="{{ localize 'ITEMS.new_creaturecapacity' }}"><i class="fas fa-plus"></i></a>{{/if}}</h4>
|
||||
<ol class="creature-list">
|
||||
{{#each capacities as |item id|}}
|
||||
<li class="item" data-item-id="{{item._id}}">
|
||||
<div class="item-name">
|
||||
<a class="item-control item-roll" data-action="roll" data-item-id="{{item._id}}" title="{{ localize 'VERMINE.roll' }}">{{item.name}}</a>
|
||||
<span class="capacity-type">{{ smarttl "CREATURE_CAPACITY_TYPES" item.system.type }}</span>
|
||||
</div>
|
||||
<div class="item-controls">
|
||||
{{#if @root.isEditMode}}
|
||||
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="{{ localize 'UI.effect_edit' }}"><i class="fas fa-edit"></i></a>
|
||||
<a class="item-control item-delete" data-action="delete" data-item-id="{{item._id}}" title="{{ localize 'UI.effect_delete' }}"><i class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="grid-span-2">
|
||||
<fieldset>
|
||||
<legend><h4>{{ localize 'IDENTITY.notes' }}</h4></legend>
|
||||
|
||||
@@ -61,12 +61,12 @@
|
||||
<select name="system.size.value" id="system.size.value" data-dtype="Number">
|
||||
{{#each sizeOptions}}
|
||||
<option value="{{@key}}" {{#ife @key @root.system.size.value}}selected{{/ife}}>
|
||||
{{@key}}
|
||||
{{@key}} ({{localize label}})
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
{{else}}
|
||||
<span>{{system.size.value}}</span>
|
||||
<span>{{#if (ne system.size.value "0")}}{{system.size.value}} ({{localize sizeLabel}}){{else}}{{localize 'VERMINE.none'}}{{/if}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,17 +75,17 @@
|
||||
<div class="resource-content">
|
||||
{{#if isEditMode}}
|
||||
<select name="system.pack.value" id="system.pack.value" data-dtype="Number">
|
||||
<option value="0">{{localize 'VERMINE.none'}}</option>
|
||||
<option value="0" {{#ife "0" @root.system.pack.value}}selected{{/ife}}>{{ localize 'VERMINE.none' }}</option>
|
||||
{{#each packOptions}}
|
||||
{{#if @key}}
|
||||
{{#if (ne @key "0")}}
|
||||
<option value="{{@key}}" {{#ife @key @root.system.pack.value}}selected{{/ife}}>
|
||||
{{@key}}
|
||||
{{@key}} ({{localize label}})
|
||||
</option>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</select>
|
||||
{{else}}
|
||||
<span>{{system.pack.value}}</span>
|
||||
<span>{{#if system.pack.value}}{{system.pack.value}} ({{localize packLabel}}){{else}}{{ localize 'VERMINE.none' }}{{/if}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
<div class="tab stats sheet-part" data-group="sheet" data-tab="stats">
|
||||
<div class="tab stats sheet-part {{#if @root.tabs.stats.active}}active{{/if}}" data-group="sheet" data-tab="stats">
|
||||
<h3>{{ localize 'VERMINE.base_values' }}</h3>
|
||||
|
||||
{{!-- Total attaque (lancable) --}}
|
||||
<div class="mdb align-center">
|
||||
<h4>{{ localize 'VERMINE.total_attack' }}</h4>
|
||||
<a class="rollable" data-type="creature-attack" data-label="attack" title="{{ localize 'VERMINE.roll' }}">{{ system.computed.attack }}<i class="fas fa-dice-d10"></i></a>
|
||||
</div>
|
||||
|
||||
{{!-- Réaction (lancable) --}}
|
||||
<div class="mdb align-center">
|
||||
<h4>{{ localize 'ADVERSITY.reaction' }}</h4>
|
||||
<a class="rollable" data-type="creature-reaction" data-label="reaction" title="{{ localize 'VERMINE.roll' }}">{{ system.computed.reaction }}{{#if system.computed.reactionBonus}}+{{system.computed.reactionBonus}}{{/if}}<i class="fas fa-dice-d10"></i></a>
|
||||
</div>
|
||||
|
||||
<section class="grid grid-3col gap-md">
|
||||
<div class="mdb">
|
||||
<h4 class="align-center">{{ localize 'ADVERSITY.pattern' }}: {{ patternLabel }}</h4>
|
||||
@@ -16,12 +29,12 @@
|
||||
<div class="mdb">
|
||||
<h4 class="align-center">{{ localize 'ADVERSITY.size' }}: {{ sizeLabel }}</h4>
|
||||
<ul class="unstyled">
|
||||
<li>{{ localize 'ADVERSITY.attack' }}: {{ creatureSizeLevel "attack" system.size.value }}</li>
|
||||
<li>{{ localize 'ADVERSITY.vigor' }}: {{ creatureSizeLevel "vigor" system.size.value }}</li>
|
||||
<li>{{ localize 'ADVERSITY.attack' }}: {{ sizeCumulative.attack }}</li>
|
||||
<li>{{ localize 'ADVERSITY.vigor' }}: {{ sizeCumulative.vigor }}</li>
|
||||
<li>{{ localize 'ADVERSITY.wounds' }}:
|
||||
{{ creatureSizeLevel "minorWound" system.size.value }}/
|
||||
{{ creatureSizeLevel "majorWound" system.size.value }}/
|
||||
{{ creatureSizeLevel "deadlyWound" system.size.value }}
|
||||
{{ sizeCumulative.minorWound }}/
|
||||
{{ sizeCumulative.majorWound }}/
|
||||
{{ sizeCumulative.deadlyWound }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1,45 +1,50 @@
|
||||
<div class="tab gear sheet-part" data-group="sheet" data-tab="gear">
|
||||
<div class="grid grid-2col">
|
||||
<div>
|
||||
<ol class="items-list">
|
||||
<li class="item flexrow items-header">
|
||||
<div class="item-name" style="flex:4;">{{ localize 'IDENTITY.name'}}</div>
|
||||
<div class="item-quantity">{{ localize 'VERMINE.qty'}}</div>
|
||||
<div class="item-weight">{{ localize 'VERMINE.weight'}}</div>
|
||||
<div class="tab gear sheet-part {{#if @root.tabs.gear.active}}active{{/if}}" data-group="sheet" data-tab="gear">
|
||||
<div class="weapons">
|
||||
<h4>{{localize 'ITEMS.weapons'}}</h4>
|
||||
{{> "systems/vermine2047/templates/actor/parts/actor-weapons.hbs"}}
|
||||
</div>
|
||||
<div class="protections">
|
||||
<h4>{{localize 'ITEMS.defenses'}}</h4>
|
||||
{{> "systems/vermine2047/templates/actor/parts/actor-defenses.hbs"}}
|
||||
</div>
|
||||
<div class="objects">
|
||||
<h4>{{localize 'ITEMS.items'}}</h4>
|
||||
<ol class="items-list">
|
||||
<li class="item flexrow items-header">
|
||||
<div class="item-name" style="flex:4;">{{ localize 'IDENTITY.name'}}</div>
|
||||
<div class="item-quantity">{{ localize 'VERMINE.qty'}}</div>
|
||||
<div class="item-weight">{{ localize 'VERMINE.weight'}}</div>
|
||||
<div class="item-controls">
|
||||
{{#if isEditMode}}
|
||||
<a class="item-control item-create" data-action="create" data-type="item" title="Create item"><i class="fas fa-plus"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{#each gear as |item id|}}
|
||||
<li class="item flexrow flex-group-center" data-item-id="{{item._id}}">
|
||||
<div class="item-name" style="flex:4;">
|
||||
<div class="item-image">
|
||||
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" data-roll-type="item"><img src="{{item.img}}" title="{{item.name}}" width="24" height="24"/></a>
|
||||
</div>
|
||||
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">{{item.name}}</a>
|
||||
</div>
|
||||
<div><a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">{{item.system.quantity}}</a></div>
|
||||
<div><a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">{{item.system.weight}}</a></div>
|
||||
<div class="item-controls">
|
||||
{{#if isEditMode}}
|
||||
<a class="item-control item-create" data-action="create" data-type="item" title="Create item"><i class="fas fa-plus"></i></a>
|
||||
{{#if @root.isEditMode}}
|
||||
<a class="item-control item-delete" data-action="delete" data-item-id="{{item._id}}" title="Delete Item"><i class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{#each gear as |item id|}}
|
||||
<li class="item flexrow flex-group-center" data-item-id="{{item._id}}">
|
||||
<div class="item-name" style="flex:4;">
|
||||
<div class="item-image">
|
||||
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" data-roll-type="item"><img src="{{item.img}}" title="{{item.name}}" width="24" height="24"/></a>
|
||||
</div>
|
||||
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">{{item.name}}</a>
|
||||
</div>
|
||||
<div><a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">{{item.system.quantity}}</a></div>
|
||||
<div><a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">{{item.system.weight}}</a></div>
|
||||
<div class="item-controls">
|
||||
{{#if @root.isEditMode}}
|
||||
<a class="item-control item-delete" data-action="delete" data-item-id="{{item._id}}" title="Delete Item"><i class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
<div>
|
||||
<h4>{{ localize 'IDENTITY.notes'}}</h4>
|
||||
{{editor system.equipment.description target="system.equipment.description" button=true owner=owner editable=editable}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
<div class="vehicles">
|
||||
<h4>{{localize 'ITEMS.vehicles'}}</h4>
|
||||
{{> "systems/vermine2047/templates/actor/group/group-vehicles.hbs"}}
|
||||
</div>
|
||||
<div class="equipment-notes">
|
||||
<h4>{{ localize 'IDENTITY.notes'}}</h4>
|
||||
{{formInput systemFields.equipment.fields.description enriched=enrichedEquipmentNotes value=system.equipment.description name="system.equipment.description" toggled=true}}
|
||||
</div>
|
||||
<h4>{{localize 'ITEMS.weapons'}}</h4>
|
||||
{{> "systems/vermine2047/templates/actor/parts/actor-weapons.hbs"}}
|
||||
<h4>{{localize 'ITEMS.defenses'}}</h4>
|
||||
{{> "systems/vermine2047/templates/actor/parts/actor-defenses.hbs"}}
|
||||
<h4>{{localize 'ITEMS.vehicles'}}</h4>
|
||||
{{> "systems/vermine2047/templates/actor/group/group-vehicles.hbs"}}
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<div class="tab info sheet-part" data-group="sheet" data-tab="info">
|
||||
<div class="tab info sheet-part {{#if @root.tabs.info.active}}active{{/if}}" data-group="sheet" data-tab="info">
|
||||
<div class="grid grid-2col">
|
||||
{{!-- Group Identity --}}
|
||||
|
||||
{{!-- Origine / Concept --}}
|
||||
<div>
|
||||
<h4>{{ localize 'IDENTITY.origin' }}</h4>
|
||||
<div class="flexrow flex-group-center">
|
||||
<div class="group-field">
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.identity.origin" value="{{system.identity.origin}}"
|
||||
placeholder="Origine..." style="flex: 1;" />
|
||||
<input type="text" name="system.identity.origin" value="{{system.identity.origin}}" placeholder="Origine..." />
|
||||
{{else}}
|
||||
<span>{{system.identity.origin}}</span>
|
||||
{{/if}}
|
||||
@@ -16,40 +14,126 @@
|
||||
|
||||
<div>
|
||||
<h4>{{ localize 'IDENTITY.theme' }}</h4>
|
||||
<div class="flexrow flex-group-center">
|
||||
<div class="group-field">
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.identity.theme" value="{{system.identity.theme}}"
|
||||
placeholder="Concept..." style="flex: 1;" />
|
||||
<input type="text" name="system.identity.theme" value="{{system.identity.theme}}" placeholder="Concept..." />
|
||||
{{else}}
|
||||
<span>{{system.identity.theme}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Group Notes --}}
|
||||
{{!-- Notes --}}
|
||||
<div class="grid-span-2">
|
||||
<fieldset>
|
||||
<legend><h4>{{ localize 'IDENTITY.notes' }}</h4></legend>
|
||||
{{formInput systemFields.identity.fields.notes enriched=enrichedNotes value=system.identity.notes name="system.identity.notes" toggled=true}}
|
||||
</fieldset>
|
||||
<h4>{{ localize 'IDENTITY.notes' }}</h4>
|
||||
{{formInput systemFields.identity.fields.notes enriched=enrichedNotes value=system.identity.notes name="system.identity.notes" toggled=true}}
|
||||
</div>
|
||||
|
||||
{{!-- Group Abilities (from items) --}}
|
||||
{{!-- Objectifs --}}
|
||||
<div class="grid-span-2">
|
||||
<h4>
|
||||
<span>{{ localize 'VERMINE.abilities' }}</span>
|
||||
{{#if isEditMode}}
|
||||
<a class="item-control item-create" data-action="create" data-type="ability" title="{{ localize 'ITEMS.new_ability' }}"><i class="fas fa-plus"></i></a>
|
||||
<h4>{{ localize 'VERMINE.objectives' }}</h4>
|
||||
|
||||
<h5>{{ localize 'VERMINE.major_objectives' }}</h5>
|
||||
<ol class="group-list">
|
||||
{{#each system.objectives.major as |objective id|}}
|
||||
<li class="item" data-index="{{id}}">
|
||||
<div class="item-name">
|
||||
{{#if @root.isEditMode}}
|
||||
<input type="text" name="system.objectives.major.{{id}}" value="{{objective}}"
|
||||
placeholder="{{ localize 'VERMINE.major_objective_placeholder' }}" />
|
||||
{{else}}
|
||||
<span>{{objective}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="item-controls">
|
||||
{{#if @root.isEditMode}}
|
||||
<a class="item-control objective-delete" data-action="deleteObjective" title="{{ localize 'UI.effect_delete' }}"
|
||||
data-type="major" data-index="{{id}}"><i class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
{{#if isEditMode}}
|
||||
<a class="item-control item-create" data-action="addObjective" title="{{ localize 'UI.add' }}"
|
||||
data-type="major_objective"><i class="fas fa-plus"></i> {{ localize 'ITEMS.new_objective' }}</a>
|
||||
{{/if}}
|
||||
|
||||
<h5>{{ localize 'VERMINE.minor_objectives' }}</h5>
|
||||
<ol class="group-list">
|
||||
{{#each system.objectives.minor as |objective id|}}
|
||||
<li class="item" data-index="{{id}}">
|
||||
<div class="item-name">
|
||||
{{#if @root.isEditMode}}
|
||||
<input type="text" name="system.objectives.minor.{{id}}" value="{{objective}}"
|
||||
placeholder="{{ localize 'VERMINE.minor_objective_placeholder' }}" />
|
||||
{{else}}
|
||||
<span>{{objective}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="item-controls">
|
||||
{{#if @root.isEditMode}}
|
||||
<a class="item-control objective-delete" data-action="deleteObjective" title="{{ localize 'UI.effect_delete' }}"
|
||||
data-type="minor" data-index="{{id}}"><i class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
{{#if isEditMode}}
|
||||
<a class="item-control item-create" data-action="addObjective" title="{{ localize 'UI.add' }}"
|
||||
data-type="minor_objective"><i class="fas fa-plus"></i> {{ localize 'ITEMS.new_objective' }}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{!-- Totem --}}
|
||||
<div class="grid-span-2">
|
||||
<h4>{{ localize 'IDENTITY.totem' }}</h4>
|
||||
<div class="group-field">
|
||||
{{#if (eq system.identity.totem "")}}
|
||||
<a data-action="chooseTotem" class="chooseTotem">{{ localize 'VERMINE.totem_picker' }}</a>
|
||||
{{else}}
|
||||
<div class="totem-display flexrow">
|
||||
<span>{{ smarttl "TOTEMS" system.identity.totem }}</span>
|
||||
<a data-action="chooseTotem" class="chooseTotem">{{ localize 'UI.effect_edit' }}</a>
|
||||
</div>
|
||||
<div class="totem-base grid grid-2col">
|
||||
<div class="item-name"><strong>{{ localize 'IDENTITY.instincts' }}</strong><br />{{ smarttl "TOTEMS" system.identity.totem "instincts" }}</div>
|
||||
<div class="item-name"><strong>{{ localize 'IDENTITY.prohibits' }}</strong><br />{{ smarttl "TOTEMS" system.identity.totem "bans" }}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</h4>
|
||||
<ol class="list-item">
|
||||
</div>
|
||||
<div class="group-field">
|
||||
<label>{{ localize 'IDENTITY.instincts' }}:</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.identity.instincts" value="{{system.identity.instincts}}"
|
||||
placeholder="{{ localize 'VERMINE.instincts_placeholder' }}" />
|
||||
{{else}}
|
||||
<span>{{system.identity.instincts}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="group-field">
|
||||
<label>{{ localize 'IDENTITY.prohibits' }}:</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.identity.prohibits" value="{{system.identity.prohibits}}"
|
||||
placeholder="{{ localize 'VERMINE.prohibits_placeholder' }}" />
|
||||
{{else}}
|
||||
<span>{{system.identity.prohibits}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Capacités de groupe --}}
|
||||
<div class="grid-span-2">
|
||||
<h4>{{ localize 'VERMINE.group_abilities' }}{{#if isEditMode}}<a class="item-control item-create" data-action="create" data-type="ability" title="{{ localize 'ITEMS.new_ability' }}"><i class="fas fa-plus"></i></a>{{/if}}</h4>
|
||||
<ol class="group-list">
|
||||
{{#each abilities as |item id|}}
|
||||
{{#if (ne item.type 'totem')}}
|
||||
<li class="item flexrow" data-item-id="{{item._id}}">
|
||||
<div class="item-name" style="flex:4;">
|
||||
{{#if (ne item.system.type 'totem')}}
|
||||
<li class="item" data-item-id="{{item._id}}">
|
||||
<div class="item-name">
|
||||
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="{{ localize 'UI.effect_edit' }}">{{item.name}}</a>
|
||||
</div>
|
||||
<div class="item-controls flexrow">
|
||||
<div class="item-controls">
|
||||
{{#if @root.isEditMode}}
|
||||
<a class="item-control item-delete" data-action="delete" data-item-id="{{item._id}}" title="{{ localize 'UI.effect_delete' }}"><i class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
@@ -60,93 +144,18 @@
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{{!-- Specialties --}}
|
||||
<div>
|
||||
<h4>
|
||||
<span>{{ localize 'ITEMS.specialties' }}</span>
|
||||
{{#if isEditMode}}
|
||||
<a class="item-control item-create" data-action="create" data-type="specialty" title="{{ localize 'ITEMS.new_specialty' }}"><i class="fas fa-plus"></i></a>
|
||||
{{/if}}
|
||||
</h4>
|
||||
<ol class="list-item">
|
||||
{{#each specialties as |item id|}}
|
||||
<li class="item flexrow" data-item-id="{{item._id}}">
|
||||
<div class="item-name" style="flex:4;">
|
||||
{{!-- Capacités de totem --}}
|
||||
<div class="grid-span-2">
|
||||
<h4>{{ localize 'VERMINE.totem_abilities' }}{{#if isEditMode}}<a class="item-control item-create" data-action="createTotemAbility" title="{{ localize 'ITEMS.new_ability' }}"><i class="fas fa-plus"></i></a>{{/if}}</h4>
|
||||
<ol class="group-list">
|
||||
{{#each totem_abilities as |item id|}}
|
||||
<li class="item" data-item-id="{{item._id}}">
|
||||
<div class="item-name">
|
||||
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="{{ localize 'UI.effect_edit' }}">{{item.name}}</a>
|
||||
<span class="hexa" title="{{ localize 'VERMINE.level' }}">{{item.system.level.value}}</span>
|
||||
<span title="{{ localize 'VERMINE.learning' }}">{{item.system.learn.threshold}}{{#ifgt item.system.learn.hindrance 0}}/{{ roman item.system.learn.hindrance }}{{/ifgt}}</span>
|
||||
</div>
|
||||
<div class="item-controls flexrow">
|
||||
{{#if @root.isEditMode}}
|
||||
<a class="item-control item-delete" data-action="delete" data-item-id="{{item._id}}" title="{{ localize 'UI.effect_delete' }}"><i class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{{!-- Backgrounds --}}
|
||||
<div>
|
||||
<h4>
|
||||
<span>{{ localize 'ITEMS.backgrounds' }}</span>
|
||||
{{#if isEditMode}}
|
||||
<a class="item-control item-create" data-action="create" data-type="background" title="{{ localize 'ITEMS.new_background' }}"><i class="fas fa-plus"></i></a>
|
||||
{{/if}}
|
||||
</h4>
|
||||
<ol class="list-item">
|
||||
{{#each backgrounds as |item id|}}
|
||||
<li class="item flexrow" data-item-id="{{item._id}}">
|
||||
<div class="item-name" style="flex:4;">
|
||||
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="{{ localize 'UI.effect_edit' }}">{{item.name}}</a>
|
||||
</div>
|
||||
<div class="item-controls flexrow">
|
||||
{{#if @root.isEditMode}}
|
||||
<a class="item-control item-delete" data-action="delete" data-item-id="{{item._id}}" title="{{ localize 'UI.effect_delete' }}"><i class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{{!-- Traumas --}}
|
||||
<div>
|
||||
<h4>
|
||||
<span>{{ localize 'ITEMS.traumas' }}</span>
|
||||
{{#if isEditMode}}
|
||||
<a class="item-control item-create" data-action="create" data-type="trauma" title="{{ localize 'ITEMS.new_trauma' }}"><i class="fas fa-plus"></i></a>
|
||||
{{/if}}
|
||||
</h4>
|
||||
<ol class="list-item">
|
||||
{{#each traumas as |item id|}}
|
||||
<li class="item flexrow" data-item-id="{{item._id}}">
|
||||
<div class="item-name" style="flex:4;">
|
||||
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="{{ localize 'UI.effect_edit' }}">{{item.name}}</a>
|
||||
</div>
|
||||
<div class="item-controls flexrow">
|
||||
{{#if @root.isEditMode}}
|
||||
<a class="item-control item-delete" data-action="delete" data-item-id="{{item._id}}" title="{{ localize 'UI.effect_delete' }}"><i class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{{!-- Evolutions --}}
|
||||
<div>
|
||||
<h4>
|
||||
<span>{{ localize 'ITEMS.evolutions' }}</span>
|
||||
{{#if isEditMode}}
|
||||
<a class="item-control item-create" data-action="create" data-type="evolution" title="{{ localize 'ITEMS.new_evolution' }}"><i class="fas fa-plus"></i></a>
|
||||
{{/if}}
|
||||
</h4>
|
||||
<ol class="list-item">
|
||||
{{#each evolutions as |item id|}}
|
||||
<li class="item flexrow" data-item-id="{{item._id}}">
|
||||
<div class="item-name" style="flex:4;">
|
||||
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="{{ localize 'UI.effect_edit' }}">{{item.name}}</a>
|
||||
</div>
|
||||
<div class="item-controls flexrow">
|
||||
<div class="item-controls">
|
||||
{{#if @root.isEditMode}}
|
||||
<a class="item-control item-delete" data-action="delete" data-item-id="{{item._id}}" title="{{ localize 'UI.effect_delete' }}"><i class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<div class="tab reserve sheet-part" data-group="sheet" data-tab="reserve">
|
||||
<div class="tab reserve sheet-part {{#if @root.tabs.reserve.active}}active{{/if}}" data-group="sheet" data-tab="reserve">
|
||||
<div class="grid grid-2col">
|
||||
{{!-- Group Reserve and Morale --}}
|
||||
{{!-- Réserve et Moral --}}
|
||||
<div class="grid-span-2">
|
||||
<h4 class="align-center">{{ localize 'VERMINE.reserve' }}</h4>
|
||||
<h4>{{ localize 'VERMINE.reserve' }}</h4>
|
||||
<div class="reserve-control flexrow flex-group-center">
|
||||
<label>{{ localize 'VERMINE.pool' }}:</label>
|
||||
{{#if isEditMode}}
|
||||
@@ -13,6 +13,13 @@
|
||||
{{/if}}
|
||||
<span>/ {{system.reserve.max}}D</span>
|
||||
</div>
|
||||
<div class="reserve-dice flexrow">
|
||||
{{#repeat system.reserve.max 1 "d"}}
|
||||
<div class="hexa {{#iflteq @d @root.system.reserve.value}}checked{{/iflteq}}">
|
||||
<input type="radio" data-dtype="Number" name="system.reserve.value" value="{{@d}}" data-action="clickRadio" />
|
||||
</div>
|
||||
{{/repeat}}
|
||||
</div>
|
||||
<div class="morale-control flexrow flex-group-center">
|
||||
<label>{{ localize 'VERMINE.morale' }}:</label>
|
||||
{{#if isEditMode}}
|
||||
@@ -33,29 +40,38 @@
|
||||
{{/if}}
|
||||
<span>/ {{system.morale.max}}D</span>
|
||||
</div>
|
||||
<div class="experience-control flexrow flex-group-center">
|
||||
<label>{{ localize 'VERMINE.experience_dice' }}:</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="number" name="system.experience.dice" value="{{system.experience.dice}}"
|
||||
data-dtype="Number" min="0" />
|
||||
{{else}}
|
||||
<span>{{system.experience.dice}}</span>
|
||||
{{/if}}
|
||||
<span>D</span>
|
||||
</div>
|
||||
<div class="group-experience-actions flexrow flex-group-center">
|
||||
<button type="button" data-action="openLearning"><i class="fas fa-graduation-cap"></i> {{ localize 'VERMINE.open_learning' }}</button>
|
||||
<button type="button" data-action="openExperiencePhase"><i class="fas fa-star"></i> {{ localize 'VERMINE.experience_phase' }}</button>
|
||||
<button type="button" data-action="validatePhase"><i class="fas fa-check"></i> {{ localize 'VERMINE.validate_phase' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Group Members --}}
|
||||
<div>
|
||||
<h4 class="align-center">
|
||||
<span>{{ localize 'VERMINE.group_members' }}</span>
|
||||
{{#if isEditMode}}
|
||||
<a class="item-control chooseActor" data-action="chooseActor" data-list="members" title="{{ localize 'UI.add' }}"
|
||||
data-type="members"><i class="fas fa-plus"></i></a>
|
||||
{{/if}}
|
||||
</h4>
|
||||
<ol class="actor-list">
|
||||
{{!-- Membres --}}
|
||||
<div class="grid-span-2">
|
||||
<h4>{{ localize 'VERMINE.group_members' }}{{#if isEditMode}}<a class="item-control chooseActor" data-action="chooseActor" data-list="members" title="{{ localize 'UI.add' }}" data-type="members"><i class="fas fa-plus"></i></a>{{/if}}</h4>
|
||||
<ol class="group-list">
|
||||
{{#each system.members as |memberId|}}
|
||||
{{#with (lookup ../resolvedMembers memberId) as |member|}}
|
||||
<li class="actor flexrow" data-actor-id="{{memberId}}">
|
||||
<div class="actor-name" style="flex:4;">
|
||||
<li class="actor" data-actor-id="{{memberId}}">
|
||||
<div class="actor-name">
|
||||
{{#if member}}
|
||||
<a class="item-control">{{member.name}}</a>
|
||||
{{else}}
|
||||
<a class="item-control" style="color: red;">ID: {{memberId}}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="item-controls flexrow">
|
||||
<div class="item-controls">
|
||||
{{#if @root.isEditMode}}
|
||||
<a class="item-control member-delete" data-action="deleteMember" data-actor-id="{{memberId}}" title="{{ localize 'UI.effect_delete' }}"><i class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
@@ -65,156 +81,5 @@
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{{!-- Group Encounters --}}
|
||||
<div>
|
||||
<h4 class="align-center">
|
||||
<span>{{ localize 'VERMINE.encounters' }}</span>
|
||||
{{#if isEditMode}}
|
||||
<a class="item-control chooseActor" data-action="chooseActor" data-list="encounters" title="{{ localize 'UI.add' }}"
|
||||
data-type="encounters"><i class="fas fa-plus"></i></a>
|
||||
{{/if}}
|
||||
</h4>
|
||||
<ol class="actor-list">
|
||||
{{#each system.encounters as |encounterId|}}
|
||||
{{#with (lookup ../resolvedEncounters encounterId) as |encounter|}}
|
||||
<li class="actor flexrow" data-actor-id="{{encounterId}}">
|
||||
<div class="item-name" style="flex:4;">
|
||||
{{#if encounter}}
|
||||
<a class="item-control">{{encounter.name}}</a>
|
||||
{{else}}
|
||||
<a class="item-control" style="color: red;">ID: {{encounterId}}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="item-controls flexrow">
|
||||
{{#if @root.isEditMode}}
|
||||
<a class="item-control encounter-delete" data-action="deleteEncounter" data-actor-id="{{encounterId}}" title="{{ localize 'UI.effect_delete' }}"><i class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{/with}}
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{{!-- Group Objectives --}}
|
||||
<div class="grid-span-2">
|
||||
<h4 class="align-center">{{ localize 'VERMINE.objectives' }}</h4>
|
||||
|
||||
<div>
|
||||
<h5>{{ localize 'VERMINE.major_objectives' }}</h5>
|
||||
<ol class="list-item">
|
||||
{{#each system.objectives.major as |objective id|}}
|
||||
<li class="item flexrow" data-index="{{id}}">
|
||||
<div class="item-name" style="flex:4;">
|
||||
{{#if @root.isEditMode}}
|
||||
<input type="text" name="system.objectives.major.{{id}}" value="{{objective}}"
|
||||
placeholder="Objectif majeur..." />
|
||||
{{else}}
|
||||
<span>{{objective}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="item-controls flexrow">
|
||||
{{#if @root.isEditMode}}
|
||||
<a class="item-control objective-delete" data-action="deleteObjective" title="{{ localize 'UI.effect_delete' }}"
|
||||
data-type="major" data-index="{{id}}"><i class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
{{#if isEditMode}}
|
||||
<a class="item-control item-create" data-action="addObjective" title="{{ localize 'UI.add' }}"
|
||||
data-type="major_objective"><i class="fas fa-plus"></i> {{ localize 'ITEMS.new_objective' }}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5>{{ localize 'VERMINE.minor_objectives' }}</h5>
|
||||
<ol class="list-item">
|
||||
{{#each system.objectives.minor as |objective id|}}
|
||||
<li class="item flexrow" data-index="{{id}}">
|
||||
<div class="item-name" style="flex:4;">
|
||||
{{#if @root.isEditMode}}
|
||||
<input type="text" name="system.objectives.minor.{{id}}" value="{{objective}}"
|
||||
placeholder="Objectif mineur..." />
|
||||
{{else}}
|
||||
<span>{{objective}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="item-controls flexrow">
|
||||
{{#if @root.isEditMode}}
|
||||
<a class="item-control objective-delete" data-action="deleteObjective" title="{{ localize 'UI.effect_delete' }}"
|
||||
data-type="minor" data-index="{{id}}"><i class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
{{#if isEditMode}}
|
||||
<a class="item-control item-create" data-action="addObjective" title="{{ localize 'UI.add' }}"
|
||||
data-type="minor_objective"><i class="fas fa-plus"></i> {{ localize 'ITEMS.new_objective' }}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Group Abilities --}}
|
||||
<div class="grid-span-2">
|
||||
<h4 class="align-center">
|
||||
<span>{{ localize 'VERMINE.group_abilities' }}</span>
|
||||
{{#if isEditMode}}
|
||||
<a class="item-control item-create" data-action="create" data-type="ability" title="{{ localize 'ITEMS.new_ability' }}"><i class="fas fa-plus"></i></a>
|
||||
{{/if}}
|
||||
</h4>
|
||||
<ol class="list-item">
|
||||
{{#each abilities as |item id|}}
|
||||
{{#if (ne item.type 'totem')}}
|
||||
<li class="item flexrow" data-item-id="{{item._id}}">
|
||||
<div class="item-name" style="flex:4;">
|
||||
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="{{ localize 'UI.effect_edit' }}">{{item.name}}</a>
|
||||
</div>
|
||||
<div class="item-controls flexrow">
|
||||
{{#if @root.isEditMode}}
|
||||
<a class="item-control item-delete" data-action="delete" data-item-id="{{item._id}}" title="{{ localize 'UI.effect_delete' }}"><i class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{{!-- Totem Section --}}
|
||||
<div class="grid-span-2">
|
||||
<h4 class="align-center">{{ localize 'IDENTITY.totem' }}</h4>
|
||||
<div class="flexrow flex-group-center">
|
||||
<label>{{ localize 'IDENTITY.instincts' }}:</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.identity.instincts" value="{{system.identity.instincts}}"
|
||||
placeholder="Ex: Triompher, relever un défi..." />
|
||||
{{else}}
|
||||
<span>{{system.identity.instincts}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="flexrow flex-group-center">
|
||||
<label>{{ localize 'IDENTITY.prohibits' }}:</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.identity.prohibits" value="{{system.identity.prohibits}}"
|
||||
placeholder="Ex: Fuir, abandonner..." />
|
||||
{{else}}
|
||||
<span>{{system.identity.prohibits}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="flexrow flex-group-center">
|
||||
{{#if (eq system.identity.totem "")}}
|
||||
<a data-action="chooseTotem" class="chooseTotem">{{ localize 'VERMINE.totem_picker' }}</a>
|
||||
{{else}}
|
||||
<div class="totem-display flexrow">
|
||||
<span style="flex: 1;">{{ smarttl "TOTEMS" system.identity.totem }}</span>
|
||||
<a data-action="chooseTotem" class="chooseTotem">{{ localize 'UI.effect_edit' }}</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,37 +1,49 @@
|
||||
<div class="tab road sheet-part" data-group="sheet" data-tab="road">
|
||||
<ol class="items-list">
|
||||
<li class="item flexrow items-header">
|
||||
<div class="item-name" style="flex:4;">{{ localize 'IDENTITY.name'}}</div>
|
||||
<div class="item-mobility">{{ localize 'VERMINE.mobility'}}</div>
|
||||
<div class="item-rarity">{{ localize 'VERMINE.rarity'}}</div>
|
||||
<div class="item-reliability">{{ localize 'VERMINE.reliability'}}</div>
|
||||
<div class="item-controls">
|
||||
{{#if isEditMode}}
|
||||
<a class="item-control item-create" data-action="create" title="Create item"
|
||||
data-type="vehicle"><i class="fas fa-plus"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{#each vehicles as |item id|}}
|
||||
<li class="item flexrow flex-group-center" data-item-id="{{item._id}}">
|
||||
<div class="item-name" style="flex:4;">
|
||||
<div class="item-image">
|
||||
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" data-roll-type="item"><img
|
||||
src="{{item.img}}" title="{{item.name}}" width="24"
|
||||
height="24" /></a>
|
||||
</div>
|
||||
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">{{item.name}}</a>
|
||||
</div>
|
||||
<div><a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">{{item.system.mobility}}</a></div>
|
||||
<div><a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">{{item.system.rarity.value}}</a></div>
|
||||
<div><a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">{{item.system.reliability}}</a></div>
|
||||
<div class="item-controls">
|
||||
{{#if @root.isEditMode}}
|
||||
<a class="item-control item-delete" data-action="delete" data-item-id="{{item._id}}" title="Delete Item"><i
|
||||
class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
<div class="tab road sheet-part {{#if @root.tabs.road.active}}active{{/if}}" data-group="sheet" data-tab="road">
|
||||
<div class="grid grid-1col">
|
||||
{{!-- Rencontres --}}
|
||||
<div>
|
||||
<h4>{{ localize 'VERMINE.encounters' }}{{#if isEditMode}}<a class="item-control chooseActor" data-action="chooseActor" data-list="encounters" title="{{ localize 'UI.add' }}" data-type="encounters"><i class="fas fa-plus"></i></a>{{/if}}</h4>
|
||||
<ol class="group-list">
|
||||
<li class="group-list-header">
|
||||
<div class="gcol-name">{{ localize 'IDENTITY.name'}}</div>
|
||||
<div class="gcol-profile">{{ localize 'IDENTITY.profile'}}</div>
|
||||
<div class="gcol-totem">{{ localize 'IDENTITY.totem'}}</div>
|
||||
<div class="item-controls"></div>
|
||||
</li>
|
||||
{{#each system.encounters as |encounterId|}}
|
||||
{{#with (lookup ../resolvedEncounters encounterId) as |encounter|}}
|
||||
<li class="actor" data-actor-id="{{encounterId}}">
|
||||
{{#if encounter}}
|
||||
<div class="gcol-name">
|
||||
<a class="item-control">{{encounter.name}}</a>
|
||||
<span class="encounter-type">({{encounter.type}})</span>
|
||||
</div>
|
||||
<div class="gcol-profile">{{encounter.profile}}</div>
|
||||
<div class="gcol-totem">{{#if encounter.totem}}{{ smarttl "TOTEMS" encounter.totem }}{{/if}}</div>
|
||||
{{else}}
|
||||
<div class="gcol-name"><a class="item-control" style="color: red;">ID: {{encounterId}}</a></div>
|
||||
<div class="gcol-profile"></div>
|
||||
<div class="gcol-totem"></div>
|
||||
{{/if}}
|
||||
<div class="item-controls">
|
||||
{{#if @root.isEditMode}}
|
||||
<a class="item-control encounter-delete" data-action="deleteEncounter" data-actor-id="{{encounterId}}" title="{{ localize 'UI.effect_delete' }}"><i class="fas fa-trash"></i></a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{/with}}
|
||||
{{else}}
|
||||
<li class="item">
|
||||
<span class="item-name">{{ localize 'VERMINE.no_encounters' }}</span>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{{!-- Notes de voyage --}}
|
||||
<div>
|
||||
<h4>{{ localize 'VERMINE.travel_notes' }}</h4>
|
||||
{{formInput systemFields.roadNotes enriched=enrichedRoadNotes value=system.roadNotes name="system.roadNotes" toggled=true}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
<div class="tab characteristics sheet-part" data-group="sheet" data-tab="characteristics">
|
||||
<h3>{{ localize 'VERMINE.abilities' }}</h3>
|
||||
|
||||
{{!-- Caracteristiques physiques --}}
|
||||
<div class="ability-section">
|
||||
<h4 class="ability-category-header">{{ localize 'VERMINE.ability_category.physical' }}</h4>
|
||||
<div class="grid grid-2col">
|
||||
<div class="ability-item">
|
||||
<label for="system.abilities.vigor.value">{{ localize 'ABILITIES.vigor.name' }}</label>
|
||||
<div class="ability-control">
|
||||
{{#if isEditMode}}
|
||||
<input type="range" name="system.abilities.vigor.value" value="{{system.abilities.vigor.value}}" min="{{system.abilities.vigor.min}}" max="{{system.abilities.vigor.max}}" data-dtype="Number"/>
|
||||
{{/if}}
|
||||
<span class="ability-value">{{system.abilities.vigor.value}}D</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ability-item">
|
||||
<label for="system.abilities.health.value">{{ localize 'ABILITIES.health.name' }}</label>
|
||||
<div class="ability-control">
|
||||
{{#if isEditMode}}
|
||||
<input type="range" name="system.abilities.health.value" value="{{system.abilities.health.value}}" min="{{system.abilities.health.min}}" max="{{system.abilities.health.max}}" data-dtype="Number"/>
|
||||
{{/if}}
|
||||
<span class="ability-value">{{system.abilities.health.value}}D</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Caracteristiques manuelles --}}
|
||||
<div class="ability-section">
|
||||
<h4 class="ability-category-header">{{ localize 'VERMINE.ability_category.manual' }}</h4>
|
||||
<div class="grid grid-2col">
|
||||
<div class="ability-item">
|
||||
<label for="system.abilities.precision.value">{{ localize 'ABILITIES.precision.name' }}</label>
|
||||
<div class="ability-control">
|
||||
{{#if isEditMode}}
|
||||
<input type="range" name="system.abilities.precision.value" value="{{system.abilities.precision.value}}" min="{{system.abilities.precision.min}}" max="{{system.abilities.precision.max}}" data-dtype="Number"/>
|
||||
{{/if}}
|
||||
<span class="ability-value">{{system.abilities.precision.value}}D</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ability-item">
|
||||
<label for="system.abilities.reflexes.value">{{ localize 'ABILITIES.reflexes.name' }}</label>
|
||||
<div class="ability-control">
|
||||
{{#if isEditMode}}
|
||||
<input type="range" name="system.abilities.reflexes.value" value="{{system.abilities.reflexes.value}}" min="{{system.abilities.reflexes.min}}" max="{{system.abilities.reflexes.max}}" data-dtype="Number"/>
|
||||
{{/if}}
|
||||
<span class="ability-value">{{system.abilities.reflexes.value}}D</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Caracteristiques mentales --}}
|
||||
<div class="ability-section">
|
||||
<h4 class="ability-category-header">{{ localize 'VERMINE.ability_category.mental' }}</h4>
|
||||
<div class="grid grid-2col">
|
||||
<div class="ability-item">
|
||||
<label for="system.abilities.knowledge.value">{{ localize 'ABILITIES.knowledge.name' }}</label>
|
||||
<div class="ability-control">
|
||||
{{#if isEditMode}}
|
||||
<input type="range" name="system.abilities.knowledge.value" value="{{system.abilities.knowledge.value}}" min="{{system.abilities.knowledge.min}}" max="{{system.abilities.knowledge.max}}" data-dtype="Number"/>
|
||||
{{/if}}
|
||||
<span class="ability-value">{{system.abilities.knowledge.value}}D</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ability-item">
|
||||
<label for="system.abilities.perception.value">{{ localize 'ABILITIES.perception.name' }}</label>
|
||||
<div class="ability-control">
|
||||
{{#if isEditMode}}
|
||||
<input type="range" name="system.abilities.perception.value" value="{{system.abilities.perception.value}}" min="{{system.abilities.perception.min}}" max="{{system.abilities.perception.max}}" data-dtype="Number"/>
|
||||
{{/if}}
|
||||
<span class="ability-value">{{system.abilities.perception.value}}D</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Caracteristiques sociales --}}
|
||||
<div class="ability-section">
|
||||
<h4 class="ability-category-header">{{ localize 'VERMINE.ability_category.social' }}</h4>
|
||||
<div class="grid grid-2col">
|
||||
<div class="ability-item">
|
||||
<label for="system.abilities.will.value">{{ localize 'ABILITIES.will.name' }}</label>
|
||||
<div class="ability-control">
|
||||
{{#if isEditMode}}
|
||||
<input type="range" name="system.abilities.will.value" value="{{system.abilities.will.value}}" min="{{system.abilities.will.min}}" max="{{system.abilities.will.max}}" data-dtype="Number"/>
|
||||
{{/if}}
|
||||
<span class="ability-value">{{system.abilities.will.value}}D</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ability-item">
|
||||
<label for="system.abilities.empathy.value">{{ localize 'ABILITIES.empathy.name' }}</label>
|
||||
<div class="ability-control">
|
||||
{{#if isEditMode}}
|
||||
<input type="range" name="system.abilities.empathy.value" value="{{system.abilities.empathy.value}}" min="{{system.abilities.empathy.min}}" max="{{system.abilities.empathy.max}}" data-dtype="Number"/>
|
||||
{{/if}}
|
||||
<span class="ability-value">{{system.abilities.empathy.value}}D</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Reserves --}}
|
||||
<div class="ability-section">
|
||||
<h4 class="ability-category-header">{{ localize 'VERMINE.reserves' }}</h4>
|
||||
<div class="grid grid-2col">
|
||||
<div class="ability-item">
|
||||
<span>{{ localize 'VERMINE.effort' }} : {{system.attributes.effort.value}}/{{system.attributes.effort.max}}D</span>
|
||||
</div>
|
||||
<div class="ability-item">
|
||||
<span>{{ localize 'VERMINE.self_control' }} : {{system.attributes.self_control.value}}/{{system.attributes.self_control.max}}D</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,57 +1,45 @@
|
||||
<div class="tab combat sheet-part" data-group="sheet" data-tab="combat">
|
||||
<div class="grid grid-2col">
|
||||
<div>
|
||||
<h4 class="align-center">{{ localize "VERMINE.self_control"}}</h4>
|
||||
<p class="align-center">
|
||||
{{#if isEditMode}}
|
||||
<input type="number" name="system.attributes.self_control.value"
|
||||
value="{{ system.attributes.self_control.value }}" data-dtype="Number"
|
||||
min="{{ system.attributes.self_control.min }}"
|
||||
max="{{ system.attributes.self_control.max }}" />
|
||||
{{/if}}
|
||||
{{ system.attributes.self_control.value }} / {{
|
||||
system.attributes.self_control.max }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="align-center">{{ localize "VERMINE.effort"}}</h4>
|
||||
<p class="align-center">
|
||||
{{#if isEditMode}}
|
||||
<input type="number" name="system.attributes.effort.value"
|
||||
value="{{ system.attributes.effort.value }}" data-dtype="Number"
|
||||
min="{{ system.attributes.effort.min }}"
|
||||
max="{{ system.attributes.effort.max }}" />
|
||||
{{/if}}
|
||||
{{ system.attributes.effort.value }} / {{
|
||||
system.attributes.effort.max }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="tab combat sheet-part {{#if @root.tabs.combat.active}}active{{/if}}" data-group="sheet" data-tab="combat">
|
||||
{{> "systems/vermine2047/templates/actor/parts/actor-equipped-gear.hbs"}}
|
||||
<h4 class="item-name effect-name flexrow">{{ localize "VERMINE.wounds.name"}}</h4>
|
||||
<div class="npc-wounds flexrow">
|
||||
<span class="wound-cat">
|
||||
<span class="wound-label">{{ localize 'VERMINE.wounds.light'}} ({{ system.minorWound.threshold }})</span>
|
||||
<span class="wound-boxes">
|
||||
{{#repeat system.minorWound.max 1 "minorwoundmax"}}
|
||||
<div class="hexa ability{{#iflteq @minorwoundmax @root.system.minorWound.value }} checked{{/iflteq}}">
|
||||
<input type="radio" data-dtype="Number" name="system.minorWound.value"
|
||||
value="{{@minorwoundmax}}" data-action="clickWound" data-wound="minorWound"
|
||||
class="{{#iflteq @minorwoundmax @root.system.minorWound.value }}checked{{/iflteq}}"
|
||||
{{#ife @minorwoundmax @root.system.minorWound.value }}checked="checked"{{/ife}} />
|
||||
</div>
|
||||
{{/repeat}}
|
||||
</span>
|
||||
</span>
|
||||
<span class="wound-cat">
|
||||
<span class="wound-label">{{ localize 'VERMINE.wounds.heavy'}} ({{ system.majorWound.threshold }})</span>
|
||||
<span class="wound-boxes">
|
||||
{{#repeat system.majorWound.max 1 "majorwoundmax"}}
|
||||
<div class="hexa ability{{#iflteq @majorwoundmax @root.system.majorWound.value }} checked{{/iflteq}}">
|
||||
<input type="radio" name="system.majorWound.value" value="{{@majorwoundmax}}" data-action="clickWound" data-wound="majorWound"
|
||||
class="{{#iflteq @majorwoundmax @root.system.majorWound.value }}checked{{/iflteq}}"
|
||||
{{#ife @majorwoundmax @root.system.majorWound.value }}checked="checked"{{/ife}} />
|
||||
</div>
|
||||
{{/repeat}}
|
||||
</span>
|
||||
</span>
|
||||
<span class="wound-cat">
|
||||
<span class="wound-label">{{ localize 'VERMINE.wounds.deadly'}} ({{ system.deadlyWound.threshold }})</span>
|
||||
<span class="wound-boxes">
|
||||
{{#repeat system.deadlyWound.max 1 "deadlywoundmax"}}
|
||||
<div class="hexa ability{{#iflteq @deadlywoundmax @root.system.deadlyWound.value }} checked{{/iflteq}}">
|
||||
<input type="radio" name="system.deadlyWound.value" value="{{@deadlywoundmax}}" data-action="clickWound" data-wound="deadlyWound"
|
||||
class="{{#iflteq @deadlywoundmax @root.system.deadlyWound.value }}checked{{/iflteq}}"
|
||||
{{#ife @deadlywoundmax @root.system.deadlyWound.value }}checked="checked"{{/ife}} />
|
||||
</div>
|
||||
{{/repeat}}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<h4 class="item-name effect-name flexrow">{{ localize
|
||||
"VERMINE.wounds.name"}}</h4>
|
||||
<ul class="unstyled">
|
||||
<li class="row mdb">{{ localize 'VERMINE.wounds.light'}} ({{
|
||||
system.minorWound.threshold }})
|
||||
{{#repeat system.minorWound.max }}
|
||||
<input type="radio" data-dtype="Number" name="system.minorWound.value"
|
||||
value="{{this}}" data-action="clickRadio" data-wound="minorWound"
|
||||
{{#ife @root.system.minorWound.value this }}checked="checked"{{/ife}} />
|
||||
{{/repeat}}</li>
|
||||
<li class="row mdb">{{ localize 'VERMINE.wounds.heavy'}} ({{
|
||||
system.majorWound.threshold }})
|
||||
{{#repeat system.majorWound.max }}
|
||||
<input type="radio" name="system.majorWound.value" value="{{this}}" data-action="clickRadio" data-wound="majorWound"
|
||||
{{#ife @root.system.majorWound.value this }}checked="checked"{{/ife}} />
|
||||
{{/repeat}}
|
||||
</li>
|
||||
<li class="row mdb">{{ localize 'VERMINE.wounds.deadly'}} ({{
|
||||
system.deadlyWound.threshold }})
|
||||
{{#repeat system.deadlyWound.max }}
|
||||
<input type="radio" name="system.deadlyWound.value" value="{{this}}" data-action="clickRadio" data-wound="deadlyWound"
|
||||
{{#ife @root.system.deadlyWound.value this }}checked="checked"{{/ife}} />
|
||||
{{/repeat}}
|
||||
</li>
|
||||
</ul>
|
||||
<h4 class="item-name effect-name flexrow">{{ localize "UI.effects.name"}}</h4>
|
||||
<ol class="items-list effects-list">
|
||||
{{#each effects as |section sid|}}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="tab equipment sheet-part {{#if @root.tabs.equipment.active}}active{{/if}}" data-group="sheet" data-tab="equipment">
|
||||
<div class="weapons">
|
||||
<h4>{{localize 'ITEMS.weapons'}}</h4>
|
||||
{{> "systems/vermine2047/templates/actor/parts/actor-weapons.hbs"}}
|
||||
</div>
|
||||
<div class="protections">
|
||||
<h4>{{localize 'ITEMS.defenses'}}</h4>
|
||||
{{> "systems/vermine2047/templates/actor/parts/actor-defenses.hbs"}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -20,7 +20,7 @@
|
||||
{{/if}}
|
||||
</h1>
|
||||
|
||||
{{!-- Niveaux de Menace, Experience, Role --}}
|
||||
{{!-- Critères : Menace, Expérience, Rôle --}}
|
||||
<div class="resources grid grid-3col">
|
||||
<div class="resource flex-group-center">
|
||||
<label for="system.threat.value" class="resource-label">{{ localize 'ADVERSITY.threat' }}</label>
|
||||
@@ -29,12 +29,12 @@
|
||||
<select name="system.threat.value" id="system.threat.value" data-dtype="Number">
|
||||
{{#each npcThreatOptions}}
|
||||
<option value="{{@key}}" {{#ife @key @root.system.threat.value}}selected{{/ife}}>
|
||||
{{localize label}} ({{@key}})
|
||||
{{localize label}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
{{else}}
|
||||
<span>{{system.threat.value}}</span>
|
||||
<span>{{ system.threat.value }}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,12 +46,12 @@
|
||||
<select name="system.experience.value" id="system.experience.value" data-dtype="Number">
|
||||
{{#each npcExperienceOptions}}
|
||||
<option value="{{@key}}" {{#ife @key @root.system.experience.value}}selected{{/ife}}>
|
||||
{{localize label}} ({{@key}})
|
||||
{{localize label}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
{{else}}
|
||||
<span>{{system.experience.value}}</span>
|
||||
<span>{{ system.experience.value }}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,19 +63,39 @@
|
||||
<select name="system.role.value" id="system.role.value" data-dtype="Number">
|
||||
{{#each npcRoleOptions}}
|
||||
<option value="{{@key}}" {{#ife @key @root.system.role.value}}selected{{/ife}}>
|
||||
{{localize label}} ({{@key}})
|
||||
{{localize label}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
{{else}}
|
||||
<span>{{system.role.value}}</span>
|
||||
<span>{{ system.role.value }}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Totem et Origine --}}
|
||||
<div class="resources grid grid-2col">
|
||||
{{!-- Profil : âge, activité, totem, origine --}}
|
||||
<div class="resources grid grid-4col npc-profile-grid">
|
||||
<div class="resource flex-group-center">
|
||||
<label for="system.identity.age" class="resource-label">{{ localize 'IDENTITY.age' }}</label>
|
||||
<div class="resource-content">
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.identity.age" id="system.identity.age" value="{{system.identity.age}}" data-dtype="String"/>
|
||||
{{else}}
|
||||
<span>{{system.identity.age}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource flex-group-center">
|
||||
<label for="system.identity.profile" class="resource-label">{{ localize 'IDENTITY.profile' }}</label>
|
||||
<div class="resource-content">
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.identity.profile" id="system.identity.profile" value="{{system.identity.profile}}" data-dtype="String" placeholder="{{ localize 'PROFILE' }}"/>
|
||||
{{else}}
|
||||
<span>{{system.identity.profile}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource flex-group-center">
|
||||
<label for="system.identity.totem" class="resource-label">{{ localize 'TOTEM' }}</label>
|
||||
<div class="resource-content">
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
<div class="tab notes sheet-part" data-group="sheet" data-tab="notes">
|
||||
<div class="tab notes sheet-part {{#if @root.tabs.notes.active}}active{{/if}}" data-group="sheet" data-tab="notes">
|
||||
<h3>{{ localize 'IDENTITY.notes' }}</h3>
|
||||
<div class="grid grid-1col">
|
||||
<div class="form-group">
|
||||
<label for="system.identity.profile">{{ localize 'IDENTITY.profile' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.identity.profile" value="{{system.identity.profile}}" data-dtype="String" placeholder="{{ localize 'PROFILE' }}"/>
|
||||
{{else}}
|
||||
<span>{{system.identity.profile}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="system.identity.theme">{{ localize 'IDENTITY.theme' }}</label>
|
||||
{{#if isEditMode}}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<div class="tab skills sheet-part" data-group="sheet" data-tab="skills">
|
||||
<h3>{{ localize 'VERMINE.skills' }}</h3>
|
||||
|
||||
{{!-- Category Selector --}}
|
||||
<div class="skill-category-selector">
|
||||
<label>{{ localize 'VERMINE.preferred_category' }}:</label>
|
||||
{{#if isEditMode}}
|
||||
<select name="system.skill_categories.preferred" data-dtype="String">
|
||||
<option value="">{{ localize 'NONE' }}</option>
|
||||
{{#each system.skill_categories as |category key|}}
|
||||
{{#ife key "preferred"}}{{else}}
|
||||
<option value="{{key}}" {{#ife key ../system.skill_categories.preferred}}selected{{/ife}}>
|
||||
{{localize category.label}}
|
||||
</option>
|
||||
{{/ife}}
|
||||
{{/each}}
|
||||
</select>
|
||||
{{else}}
|
||||
<span>{{system.skill_categories.preferred}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{!-- Skills Grid by Category --}}
|
||||
<div class="skills-container">
|
||||
{{#each config.skillCategories as |category key|}}
|
||||
{{#ife key "preferred"}}{{else}}
|
||||
{{> "systems/vermine2047/templates/actor/parts/npc-skill-category.hbs"
|
||||
categoryKey=key
|
||||
categoryLabel=(concat "VERMINE.skill_category." key)
|
||||
}}
|
||||
{{/ife}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
{{!-- Free Skills text field --}}
|
||||
<div class="form-group">
|
||||
<label for="system.freeSkills">{{ localize 'ADVERSITY.skills' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.freeSkills" value="{{system.freeSkills}}" data-dtype="String" placeholder="{{ localize 'ADVERSITY.skills_placeholder' }}"/>
|
||||
{{else}}
|
||||
<span>{{system.freeSkills}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,75 +0,0 @@
|
||||
<div class="tab threat sheet-part" data-group="sheet" data-tab="threat">
|
||||
<h3>{{ localize 'ADVERSITY.threat_details' }}</h3>
|
||||
<div class="grid grid-3col">
|
||||
{{!-- Menace --}}
|
||||
<div class="card npc-card">
|
||||
<h4 class="align-center"><i class="fas fa-exclamation-triangle"></i> {{ localize 'ADVERSITY.threat' }}</h4>
|
||||
<ul class="unstyled">
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.attack' }}:</strong>
|
||||
{{ npcThreatLevel "attack" system.threat.value }}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.vigor' }}:</strong>
|
||||
{{ npcThreatLevel "vigor" system.threat.value }}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.wounds' }}:</strong>
|
||||
{{ npcThreatLevel "minorWound" system.threat.value }} /
|
||||
{{ npcThreatLevel "majorWound" system.threat.value }} /
|
||||
{{ npcThreatLevel "deadlyWound" system.threat.value }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{!-- Experience --}}
|
||||
<div class="card npc-card">
|
||||
<h4 class="align-center"><i class="fas fa-star"></i> {{ localize 'ADVERSITY.experience' }}</h4>
|
||||
<ul class="unstyled">
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.action' }}:</strong>
|
||||
{{ npcExperienceLevel "action" system.experience.value }}D
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.specialties' }}:</strong>
|
||||
{{ npcExperienceLevel "specialties" system.experience.value }}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.rerolls' }}:</strong>
|
||||
{{ npcExperienceLevel "rerolls" system.experience.value }}D
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.contact' }}:</strong>
|
||||
{{ npcExperienceLevel "contact" system.experience.value }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{!-- Role --}}
|
||||
<div class="card npc-card">
|
||||
<h4 class="align-center"><i class="fas fa-users"></i> {{ localize 'ADVERSITY.role' }}</h4>
|
||||
<ul class="unstyled">
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.reaction' }}:</strong>
|
||||
{{ npcRoleLevel "reaction" system.role.value }} + {{ npcRoleLevel "reaction_bonus" system.role.value }}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.pools' }}:</strong>
|
||||
{{ npcRoleLevel "pools" system.role.value }}D
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.gear' }}:</strong>
|
||||
{{ npcRoleLevel "gear" system.role.value }}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.gear_hindrance' }}:</strong>
|
||||
{{ npcRoleLevel "gear_hindrance" system.role.value }}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.protection' }}:</strong>
|
||||
{{ npcRoleLevel "protection" system.role.value }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,86 @@
|
||||
<div class="tab traits sheet-part {{#if @root.tabs.traits.active}}active{{/if}}" data-group="sheet" data-tab="traits">
|
||||
<h3>{{ localize 'VERMINE.traits' }}</h3>
|
||||
<div class="grid grid-3col">
|
||||
|
||||
{{!-- Menace --}}
|
||||
<div class="card npc-card">
|
||||
<h4 class="align-center"><i class="fas fa-exclamation-triangle"></i> {{ localize 'ADVERSITY.threat' }}</h4>
|
||||
<ul class="unstyled npc-trait-list">
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.attack' }}:</strong>
|
||||
<span class="trait-value">{{ system.computed.attack }}D</span>
|
||||
<a class="rollable npc-roll-btn" data-type="attack" title="{{ localize 'VERMINE.roll' }}"><i class="fas fa-dice-d10"></i></a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.vigor' }}:</strong>
|
||||
<span class="trait-value">{{ system.computed.vigor }}D</span>
|
||||
</li>
|
||||
<li class="npc-wounds">
|
||||
<strong>{{ localize 'ADVERSITY.wounds' }}:</strong>
|
||||
<span class="wound-cat"><span class="wound-boxes">{{#repeat system.minorWound.max 1 "woundindex"}}<i class="fas fa-circle"></i>{{/repeat}}</span> {{ system.minorWound.threshold }}</span>
|
||||
<span class="wound-cat"><span class="wound-boxes">{{#repeat system.majorWound.max 1 "woundindex"}}<i class="fas fa-circle"></i>{{/repeat}}</span> {{ system.majorWound.threshold }}</span>
|
||||
<span class="wound-cat"><span class="wound-boxes">{{#repeat system.deadlyWound.max 1 "woundindex"}}<i class="fas fa-circle"></i>{{/repeat}}</span> {{ system.deadlyWound.threshold }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{!-- Expérience --}}
|
||||
<div class="card npc-card">
|
||||
<h4 class="align-center"><i class="fas fa-star"></i> {{ localize 'ADVERSITY.experience' }}</h4>
|
||||
<ul class="unstyled npc-trait-list">
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.action' }}:</strong>
|
||||
<span class="trait-value">{{ system.computed.action }}D</span>
|
||||
<a class="rollable npc-roll-btn" data-type="action" title="{{ localize 'VERMINE.roll' }}"><i class="fas fa-dice-d10"></i></a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.specialties' }}:</strong>
|
||||
<span class="trait-value">{{ system.computed.specialties }}D</span>
|
||||
<a class="rollable npc-roll-btn" data-type="specialty" title="{{ localize 'VERMINE.roll' }}"><i class="fas fa-dice-d10"></i></a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.rerolls' }}:</strong>
|
||||
<span class="trait-value">{{ system.computed.rerolls }}D</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.contact' }}:</strong>
|
||||
<span class="trait-value">{{ system.computed.contact }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{!-- Rôle --}}
|
||||
<div class="card npc-card">
|
||||
<h4 class="align-center"><i class="fas fa-users"></i> {{ localize 'ADVERSITY.role' }}</h4>
|
||||
<ul class="unstyled npc-trait-list">
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.reaction' }}:</strong>
|
||||
<span class="trait-value">{{ system.computed.reaction }}D{{#if system.computed.reactionBonus}} +{{ system.computed.reactionBonus }}{{/if}}</span>
|
||||
<a class="rollable npc-roll-btn" data-type="reaction" title="{{ localize 'VERMINE.roll' }}"><i class="fas fa-dice-d10"></i></a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.pools' }}:</strong>
|
||||
<span class="trait-value">{{ system.computed.pools }}D</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.gear' }}:</strong>
|
||||
<span class="trait-value">{{ system.computed.gear }}{{#if system.computed.gearHindrance}} ({{ system.computed.gearHindrance }}){{/if}}</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ localize 'ADVERSITY.protection' }}:</strong>
|
||||
<span class="trait-value">{{ system.computed.protection }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Spécialités (description libre) --}}
|
||||
<div class="form-group npc-free-skills">
|
||||
<label for="system.freeSkills">{{ localize 'ADVERSITY.skills' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.freeSkills" id="system.freeSkills" value="{{system.freeSkills}}" data-dtype="String" placeholder="{{ localize 'ADVERSITY.skills_placeholder' }}"/>
|
||||
{{else}}
|
||||
<span>{{system.freeSkills}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,94 @@
|
||||
<div class="tab main sheet-part" data-group="sheet" data-tab="main">
|
||||
{{!-- Toggle Edit/Play --}}
|
||||
<div class="sheet-header-toggle">
|
||||
{{#if isEditMode}}
|
||||
<button type="button" data-action="toggleSheet">{{localize "VERMINE.playMode"}}</button>
|
||||
{{else}}
|
||||
<button type="button" data-action="toggleSheet">{{localize "VERMINE.editMode"}}</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{!-- HEADER --}}
|
||||
<header class="sheet-header">
|
||||
<img class="profile-img" src="{{actor.img}}" data-action="editImage" data-edit="img" title="{{actor.name}}"
|
||||
height="100" width="100" />
|
||||
<div class="header-fields">
|
||||
<div class="resources grid grid-4col">
|
||||
<h1 class="charname grid-span-2">
|
||||
{{#if isEditMode}}
|
||||
<input name="name" type="text" value="{{actor.name}}" placeholder="{{ localize 'VERMINE.vehicle' }}" />
|
||||
{{else}}
|
||||
<span>{{actor.name}}</span>
|
||||
{{/if}}
|
||||
</h1>
|
||||
<div class="resource flex-group-center grid-span-1">
|
||||
<label for="system.kind" class="resource-label">{{ localize 'VERMINE.kind' }}</label>
|
||||
<div class="resource-content">
|
||||
{{#if isEditMode}}
|
||||
<select name="system.kind" id="system.kind" data-dtype="String">
|
||||
<option value="vehicle" {{#ife system.kind "vehicle"}}selected{{/ife}}>{{ localize "VERMINE.kind_vehicle" }}</option>
|
||||
<option value="mount" {{#ife system.kind "mount"}}selected{{/ife}}>{{ localize "VERMINE.kind_mount" }}</option>
|
||||
</select>
|
||||
{{else}}
|
||||
<span>{{#ife system.kind "mount"}}{{ localize "VERMINE.kind_mount" }}{{else}}{{ localize "VERMINE.kind_vehicle" }}{{/ife}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource flex-group-center grid-span-1">
|
||||
<label for="system.reliability" class="resource-label">{{ localize 'VERMINE.reliability' }}</label>
|
||||
<div class="resource-content">
|
||||
{{#if isEditMode}}
|
||||
<input type="number" name="system.reliability" id="system.reliability" value="{{system.reliability}}"
|
||||
data-dtype="Number" min="1" max="10" />
|
||||
{{else}}
|
||||
<span>{{system.reliability}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resources grid grid-4col">
|
||||
<div class="resource flex-group-center grid-span-1">
|
||||
<label for="system.rarity.value" class="resource-label">{{ localize 'VERMINE.rarity' }}</label>
|
||||
<div class="resource-content">
|
||||
{{#if isEditMode}}
|
||||
<input type="number" name="system.rarity.value" id="system.rarity.value" value="{{system.rarity.value}}"
|
||||
data-dtype="Number" min="1" max="10" />
|
||||
{{else}}
|
||||
<span>{{system.rarity.value}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource flex-group-center grid-span-1">
|
||||
<label for="system.mobility" class="resource-label">{{ localize 'VERMINE.mobility' }}</label>
|
||||
<div class="resource-content">
|
||||
{{#if isEditMode}}
|
||||
<input type="number" name="system.mobility" id="system.mobility" value="{{system.mobility}}"
|
||||
data-dtype="Number" min="0" />
|
||||
{{else}}
|
||||
<span>{{system.mobility}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource flex-group-center grid-span-2">
|
||||
<label for="system.distance" class="resource-label">{{ localize 'VERMINE.distance' }}</label>
|
||||
<div class="resource-content">
|
||||
{{#if isEditMode}}
|
||||
<input type="number" name="system.distance" id="system.distance" value="{{system.distance}}"
|
||||
data-dtype="Number" min="0" />
|
||||
{{else}}
|
||||
<span>{{system.distance}}</span>
|
||||
{{/if}}
|
||||
<span> km</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{#if resolvedOwner}}
|
||||
<div class="vehicle-owner">
|
||||
<span class="label">{{ localize 'VERMINE.owner' }}:</span>
|
||||
<span class="value">{{ resolvedOwner.name }}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -0,0 +1,83 @@
|
||||
<div class="tab specs sheet-part {{#if @root.tabs.specs.active}}active{{/if}}" data-group="sheet" data-tab="specs">
|
||||
<section class="vehicle-section">
|
||||
<h3>{{ localize 'VERMINE.vehicle_specs' }}</h3>
|
||||
<div class="grid grid-2col">
|
||||
<div class="resource align-center flexcol">
|
||||
<label class="resource-label">{{ localize 'VERMINE.distance' }}</label>
|
||||
<div class="flexrow align-center">
|
||||
<div class="hexa"><input type="number" name="system.distance" value="{{system.distance}}" data-dtype="Number" /></div>
|
||||
<span class="unit">km</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource align-center flexcol">
|
||||
<label class="resource-label">{{ localize 'VERMINE.speed' }}</label>
|
||||
<div class="flexrow align-center">
|
||||
<div class="hexa"><input type="number" name="system.speed.cruise" value="{{system.speed.cruise}}" data-dtype="Number" /></div>
|
||||
<span>/</span>
|
||||
<div class="hexa"><input type="number" name="system.speed.top" value="{{system.speed.top}}" data-dtype="Number" /></div>
|
||||
</div>
|
||||
<input type="text" name="system.speed.topDuration" value="{{system.speed.topDuration}}"
|
||||
placeholder="{{ localize 'VERMINE.speed_duration_ph' }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-2col">
|
||||
<div class="resource align-center flexcol">
|
||||
<label class="resource-label">{{ localize 'VERMINE.effort_reserve' }}</label>
|
||||
<div class="flexrow align-center">
|
||||
<div class="hexa"><input type="number" name="system.effortReserve.value" value="{{system.effortReserve.value}}" data-dtype="Number" /></div>
|
||||
<span>/</span>
|
||||
<div class="hexa"><input type="number" name="system.effortReserve.max" value="{{system.effortReserve.max}}" data-dtype="Number" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource align-center flexcol">
|
||||
<label class="resource-label">{{ localize 'VERMINE.mobility' }}</label>
|
||||
<div class="hexa"><input type="number" name="system.mobility" value="{{system.mobility}}" data-dtype="Number" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="maintenance">
|
||||
<label class="resource-label">{{ localize 'VERMINE.maintenance' }}</label>
|
||||
<div class="flexrow flex-wrap">
|
||||
{{#each maintenanceSkills}}
|
||||
<label class="flexrow align-center">
|
||||
<input type="checkbox" name="system.maintenance" value="{{ this.key }}" {{#if this.checked}}checked{{/if}} />
|
||||
<span>{{ this.label }}</span>
|
||||
</label>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="vehicle-section">
|
||||
<h3>{{ localize 'IDENTITY.profile' }}</h3>
|
||||
<div class="grid grid-2col">
|
||||
<div class="form-group">
|
||||
<label for="system.identity.profile">{{ localize 'IDENTITY.profile' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.identity.profile" value="{{system.identity.profile}}" data-dtype="String" />
|
||||
{{else}}
|
||||
<span>{{system.identity.profile}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="system.identity.origin">{{ localize 'IDENTITY.origin' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.identity.origin" value="{{system.identity.origin}}" data-dtype="String" />
|
||||
{{else}}
|
||||
<span>{{system.identity.origin}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="system.identity.theme">{{ localize 'IDENTITY.theme' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="text" name="system.identity.theme" value="{{system.identity.theme}}" data-dtype="String" />
|
||||
{{else}}
|
||||
<span>{{system.identity.theme}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="system.identity.notes">{{ localize 'IDENTITY.notes' }}</label>
|
||||
{{formInput systemFields.identity.fields.notes enriched=enrichedNotes value=system.identity.notes name="system.identity.notes" toggled=true}}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||