Fix apv2, WIP

This commit is contained in:
2026-06-06 10:21:24 +02:00
parent 6cec1da910
commit 9b77a0c552
130 changed files with 12850 additions and 2830 deletions
+69
View File
@@ -0,0 +1,69 @@
# Makefile pour Vermine2047
# Ce fichier fournit des commandes courantes pour le développement
# Utilise uniquement LESS comme préprocesseur CSS
.PHONY: help install build build-less build-dev build-css watch clean lint
# Couleurs pour l'affichage
GREEN := \033[0;32m
YELLOW := \033[1;33m
NC := \033[0m # No Color
help: ## Affiche cette aide
@echo "Commandes disponibles pour Vermine2047:"
@echo ""
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[0;32m%-20s\033[0m %s\n", $$1, $$2}'
@echo ""
install: ## Installe les dépendances npm
@echo "$(YELLOW)Installation des dépendances npm...$(NC)"
npm install
@echo "$(GREEN)✓ Dépendances installées$(NC)"
build: build-less ## Compile LESS → vermine2047.min.css
build-less: ## Compile le LESS en CSS minifié (vermine2047.min.css)
@echo "$(YELLOW)Compilation du LESS...$(NC)"
npm run build:less
@echo "$(GREEN)✓ LESS compilé$(NC)"
build-dev: ## Compile le LESS en CSS non minifié (vermine2047.dev.css)
@echo "$(YELLOW)Compilation du LESS (mode dev)...$(NC)"
npm run build:less:dev
@echo "$(GREEN)✓ LESS compilé en mode dev$(NC)"
build-css: build-less build-dev ## Compile tout le CSS (minifié + dev)
watch: ## Lance le mode watch (recompilation automatique)
@echo "$(YELLOW)Lancement du mode watch...$(NC)"
npm run watch
clean: clean-css ## Nettoie les fichiers CSS générés
clean-css: ## Supprime les fichiers CSS compilés (garde vermine2047.css original)
@echo "$(YELLOW)Nettoyage des fichiers CSS...$(NC)"
npm run clean:css
@echo "$(GREEN)✓ Fichiers CSS nettoyés$(NC)"
rebuild: clean build ## Reconstruit tout le CSS
@echo "$(YELLOW)Reconstruction complète du CSS...$(NC)"
npm run rebuild:css
@echo "$(GREEN)✓ CSS reconstruit$(NC)"
lint: lint-less ## Lance le linting du code LESS
lint-less: ## Vérifie la qualité du code LESS
@echo "$(YELLOW)Linting du code LESS...$(NC)"
npm run lint:less
@echo "$(GREEN)✓ Linting terminé$(NC)"
# Commandes utilitaires
launch-foundry: ## Lance FoundryVTT
npm run launch_Foundry12
push-yaml: ## Push LDB vers YAML
node ./tools/pushLDBtoYAML.mjs
pull-yaml: ## Pull YAML vers LDB
node ./tools/pullYAMLtoLDB.mjs
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 776 KiB

+680
View File
@@ -0,0 +1,680 @@
uid=78_0 RootWebArea "Foundry Virtual Tabletop" url="https://localhost:31000/game"
uid=78_1 ignored
uid=78_2 generic
uid=78_3 list
uid=78_4 generic
uid=78_5 generic
uid=78_6 generic
uid=78_7 generic
uid=78_8 generic
uid=78_9 generic
uid=78_10 list
uid=78_11 listitem level="1"
uid=78_12 tab "Outils de token" selectable
uid=78_13 listitem level="1"
uid=78_14 tab "Outils de tuile" selectable
uid=78_15 listitem level="1"
uid=78_16 tab "Outils de dessin" selectable
uid=78_17 listitem level="1"
uid=78_18 tab "Outils de mur" selectable
uid=78_19 listitem level="1"
uid=78_20 tab "Outils de lumière" selectable
uid=78_21 listitem level="1"
uid=78_22 tab "Outils de son dambiance" selectable
uid=78_23 listitem level="1"
uid=78_24 tab "Outils de région" selectable
uid=78_25 listitem level="1"
uid=78_26 tab "Notes" selectable
uid=78_27 list
uid=78_28 listitem level="1"
uid=78_29 button "Sélection de tokens" pressed
uid=78_30 listitem level="1"
uid=78_31 button "Sélection de cibles"
uid=78_32 listitem level="1"
uid=78_33 button "Règle"
uid=78_34 listitem level="1"
uid=78_35 button "Déplacement sans contrainte"
uid=78_36 generic
uid=78_37 generic
uid=78_38 list
uid=78_39 listitem level="1"
uid=78_40 ignored
uid=78_41 StaticText "Gamemaster [MJ]"
uid=78_42 InlineTextBox "Gamemaster [MJ]"
uid=78_43 generic
uid=78_44 generic
uid=78_45 LabelText
uid=78_46 StaticText "Latence"
uid=78_42 InlineTextBox "Latence"
uid=78_47 StaticText " "
uid=78_42 InlineTextBox " "
uid=78_48 StaticText "1ms"
uid=78_42 InlineTextBox "1ms"
uid=78_49 generic
uid=78_50 LabelText
uid=78_51 StaticText "IPS"
uid=78_42 InlineTextBox "IPS"
uid=78_52 StaticText " "
uid=78_42 InlineTextBox " "
uid=78_53 StaticText "--"
uid=78_42 InlineTextBox "--"
uid=78_54 button
uid=78_55 generic
uid=78_56 navigation
uid=78_57 list
uid=78_58 generic
uid=78_59 sectionheader
uid=78_60 generic
uid=78_61 generic
uid=78_62 LabelText
uid=78_63 LabelText
uid=78_64 sectionfooter
uid=78_65 generic roledescription="Barre de raccourcis"
uid=78_66 generic
uid=78_67 button "Couper le son"
uid=78_68 button "Menu principal"
uid=78_69 list
uid=78_70 button "Emplacement vide"
uid=78_71 ignored
uid=78_72 StaticText "1"
uid=78_42 InlineTextBox "1"
uid=78_73 button "Emplacement vide"
uid=78_74 ignored
uid=78_75 StaticText "2"
uid=78_42 InlineTextBox "2"
uid=78_76 button "Emplacement vide"
uid=78_77 ignored
uid=78_78 StaticText "3"
uid=78_42 InlineTextBox "3"
uid=78_79 button "Emplacement vide"
uid=78_80 ignored
uid=78_81 StaticText "4"
uid=78_42 InlineTextBox "4"
uid=78_82 button "Emplacement vide"
uid=78_83 ignored
uid=78_84 StaticText "5"
uid=78_42 InlineTextBox "5"
uid=78_85 button "Emplacement vide"
uid=78_86 ignored
uid=78_87 StaticText "6"
uid=78_42 InlineTextBox "6"
uid=78_88 button "Emplacement vide"
uid=78_89 ignored
uid=78_90 StaticText "7"
uid=78_42 InlineTextBox "7"
uid=78_91 button "Emplacement vide"
uid=78_92 ignored
uid=78_93 StaticText "8"
uid=78_42 InlineTextBox "8"
uid=78_94 button "Emplacement vide"
uid=78_95 ignored
uid=78_96 StaticText "9"
uid=78_42 InlineTextBox "9"
uid=78_97 button "Emplacement vide"
uid=78_98 ignored
uid=78_99 StaticText "0"
uid=78_42 InlineTextBox "0"
uid=78_100 generic
uid=78_101 navigation
uid=78_102 button "Page suivante"
uid=78_103 ignored
uid=78_104 StaticText "1"
uid=78_42 InlineTextBox "1"
uid=78_105 button "Page précédente"
uid=78_106 ignored
uid=78_107 button "Verrouiller la barre de raccourcis"
uid=78_108 button "Effacer la barre de raccourcis"
uid=78_109 generic
uid=78_110 generic
uid=78_111 generic
uid=78_112 ignored
uid=78_113 list
uid=78_114 generic "Tchat"
uid=78_115 generic
uid=78_116 generic value="
"
uid=78_117 paragraph
uid=78_118 LineBreak "
"
uid=78_42 InlineTextBox "
"
uid=78_119 ignored
uid=78_42 generic
uid=78_42 StaticText "Entrer un message"
uid=78_42 InlineTextBox "Entrer un message"
uid=78_120 generic
uid=78_121 generic
uid=78_122 button "Public en tant qu'utilisateur" pressed
uid=78_123 button "Privé pour les maîtres de jeu"
uid=78_124 button "Aveugle pour les maîtres de jeu"
uid=78_125 button "Seulement pour soi-même"
uid=78_126 button "Public en tant que personnage"
uid=78_127 generic
uid=78_128 tablist orientation="horizontal"
uid=78_129 list
uid=78_130 listitem level="1"
uid=78_131 tab "Messages du tchat" selectable
uid=78_132 listitem level="1"
uid=78_133 tab "Rencontres de combat" selectable
uid=78_134 listitem level="1"
uid=78_135 tab "Scènes" selectable
uid=78_136 listitem level="1"
uid=78_137 tab "Objets plaçables" selectable
uid=78_138 listitem level="1"
uid=78_139 tab "Acteurs" selectable
uid=78_140 listitem level="1"
uid=78_141 tab "Objets" selectable
uid=78_142 listitem level="1"
uid=78_143 tab "Journaux" selectable
uid=78_144 listitem level="1"
uid=78_145 tab "Tables aléatoires" selectable
uid=78_146 listitem level="1"
uid=78_147 tab "Jeux de cartes" selectable
uid=78_148 listitem level="1"
uid=78_149 tab "Macros" selectable
uid=78_150 listitem level="1"
uid=78_151 tab "Playlists" selectable
uid=78_152 listitem level="1"
uid=78_153 tab "Compendiums" selectable
uid=78_154 listitem level="1"
uid=78_155 tab "Paramètres" selectable
uid=78_156 listitem level="1"
uid=78_157 button "Réduire"
uid=78_158 generic
uid=78_159 ignored
uid=78_160 ignored
uid=78_161 ignored
uid=78_162 ignored
uid=78_163 generic
uid=78_164 sectionheader
uid=78_165 ignored
uid=78_166 button "Créer un acteur"
uid=78_167 ignored
uid=78_168 ignored
uid=78_169 StaticText "Créer un acteur"
uid=78_42 InlineTextBox "Créer un acteur"
uid=78_170 button "Créer un dossier"
uid=78_171 ignored
uid=78_172 ignored
uid=78_173 StaticText "Créer un dossier"
uid=78_42 InlineTextBox "Créer un dossier"
uid=78_174 search
uid=78_175 button "Recherche par nom uniquement"
uid=78_176 searchbox "Chercher dans les Acteurs"
uid=78_177 ignored
uid=78_178 ignored
uid=78_179 generic
uid=78_180 button "Trier par ordre alphabétique"
uid=78_181 button "Réduire tous les dossiers"
uid=78_182 list
uid=78_183 listitem level="1"
uid=78_184 image "Acteur" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/creature.webp"
uid=78_185 generic
uid=78_186 StaticText "Acteur"
uid=78_42 InlineTextBox "Acteur"
uid=78_187 listitem level="1"
uid=78_188 image "Acteur (2)" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/group.webp"
uid=78_189 generic
uid=78_190 StaticText "Acteur (2)"
uid=78_42 InlineTextBox "Acteur (2)"
uid=78_191 listitem level="1"
uid=78_192 image "Acteur (3)" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/character.webp"
uid=78_193 generic
uid=78_194 StaticText "Acteur (3)"
uid=78_42 InlineTextBox "Acteur (3)"
uid=78_195 sectionfooter
uid=78_196 ignored
uid=78_197 ignored
uid=78_198 ignored
uid=78_199 ignored
uid=78_200 ignored
uid=78_201 ignored
uid=78_202 ignored
uid=78_203 ignored
uid=78_204 ignored
uid=78_205 ignored
uid=78_206 ignored
uid=78_207 ignored
uid=78_208 ignored
uid=78_209 ignored
uid=78_210 ignored
uid=78_211 ignored
uid=78_212 ignored
uid=78_213 ignored
uid=78_214 ignored
uid=78_215 ignored
uid=78_216 ignored
uid=78_217 ignored
uid=78_218 ignored
uid=78_219 figure
uid=78_220 image url="https://localhost:31000/ui/pause.svg"
uid=78_221 Figcaption
uid=78_222 StaticText "JEU EN PAUSE"
uid=78_42 InlineTextBox "JEU EN PAUSE"
uid=78_223 form
uid=78_224 banner
uid=78_225 heading "Personnage: Acteur (3)" level="1"
uid=78_226 StaticText "Personnage: Acteur (3)"
uid=78_42 InlineTextBox "Personnage: Acteur (3)"
uid=78_227 button "Basculer les contrôles"
uid=78_228 button "Copier l'UUID du document"
uid=78_229 button "Fermer la fenêtre"
uid=78_230 generic
uid=78_231 ignored
uid=78_232 ignored
uid=78_233 button "MODE ÉDITION"
uid=78_234 StaticText "MODE ÉDITION"
uid=78_42 InlineTextBox "MODE ÉDITION"
uid=78_235 image "logo Vermine" url="https://localhost:31000/systems/vermine2047/assets/images/ui/logo.webp"
uid=78_236 ignored
uid=78_237 image "Acteur (3)" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/character.webp"
uid=78_238 generic
uid=78_239 heading "ADAPTATION" level="3"
uid=78_240 StaticText "ADAPTATION"
uid=78_42 InlineTextBox "ADAPTATION"
uid=78_241 heading "L'Humain" level="5"
uid=78_242 StaticText "L'Humain"
uid=78_42 InlineTextBox "L'Humain"
uid=78_243 image url="https://localhost:31000/systems/vermine2047/assets/images/ui/totems/human.webp"
uid=78_244 heading "L'adapté" level="5"
uid=78_245 StaticText "L'adapté"
uid=78_42 InlineTextBox "L'adapté"
uid=78_246 image url="https://localhost:31000/systems/vermine2047/assets/images/ui/totems/adapted.webp"
uid=78_247 ignored
uid=78_248 ignored
uid=78_249 ignored
uid=78_250 generic
uid=78_251 generic
uid=78_252 ignored
uid=78_253 ignored
uid=78_254 ignored
uid=78_255 ignored
uid=78_256 generic
uid=78_257 generic
uid=78_258 ignored
uid=78_259 ignored
uid=78_260 list
uid=78_261 listitem level="1"
uid=78_262 ignored
uid=78_263 heading "LA HORDE" level="4"
uid=78_264 StaticText "LA HORDE"
uid=78_42 InlineTextBox "LA HORDE"
uid=78_265 image "La Horde" url="https://localhost:31000/systems/vermine2047/assets/images/ui/totems/horde.webp"
uid=78_266 navigation roledescription="Navigation dans l’onglet des feuilles de personnages"
uid=78_267 generic
uid=78_268 ignored
uid=78_269 StaticText "Caractéristiques et compétences"
uid=78_42 InlineTextBox "Caractéristiques "
uid=78_42 InlineTextBox "et compétences"
uid=78_270 generic
uid=78_271 ignored
uid=78_272 StaticText "Totem et ajustements"
uid=78_42 InlineTextBox "Totem et "
uid=78_42 InlineTextBox "ajustements"
uid=78_273 generic
uid=78_274 ignored
uid=78_275 StaticText "Matériel"
uid=78_42 InlineTextBox "Matériel"
uid=78_276 generic
uid=78_277 ignored
uid=78_278 StaticText "Histoire"
uid=78_42 InlineTextBox "Histoire"
uid=78_279 generic
uid=78_280 ignored
uid=78_281 StaticText "Combat et reserves"
uid=78_42 InlineTextBox "Combat "
uid=78_42 InlineTextBox "et "
uid=78_42 InlineTextBox "reserves"
uid=78_282 generic
uid=78_283 sectionheader
uid=78_284 generic
uid=78_285 heading "Nom Acteur (3)" level="1"
uid=78_286 LabelText
uid=78_287 StaticText "Nom"
uid=78_42 InlineTextBox "Nom"
uid=78_288 ignored
uid=78_289 StaticText "Acteur (3)"
uid=78_42 InlineTextBox "Acteur (3)"
uid=78_290 ignored
uid=78_291 LabelText
uid=78_292 StaticText "Profil"
uid=78_42 InlineTextBox "Profil"
uid=78_293 ignored
uid=78_294 ignored
uid=78_295 LabelText
uid=78_296 StaticText "Age"
uid=78_42 InlineTextBox "Age"
uid=78_297 ignored
uid=78_298 ignored
uid=78_299 StaticText "15"
uid=78_42 InlineTextBox "15"
uid=78_300 generic
uid=78_301 StaticText "(Jeune)"
uid=78_42 InlineTextBox "(Jeune)"
uid=78_302 generic
uid=78_303 heading "Totem La Horde" level="1"
uid=78_304 LabelText
uid=78_305 StaticText "Totem"
uid=78_42 InlineTextBox "Totem"
uid=78_306 generic
uid=78_307 StaticText "La Horde"
uid=78_42 InlineTextBox "La Horde"
uid=78_308 ignored
uid=78_309 LabelText
uid=78_310 StaticText "Réputation"
uid=78_42 InlineTextBox "Réputation"
uid=78_311 ignored
uid=78_312 StaticText "0"
uid=78_42 InlineTextBox "0"
uid=78_313 LabelText
uid=78_314 StaticText "Expérience"
uid=78_42 InlineTextBox "Expérience"
uid=78_315 ignored
uid=78_316 StaticText "0"
uid=78_42 InlineTextBox "0"
uid=78_317 heading "CARACTÉRISTIQUES" level="3"
uid=78_318 StaticText "CARACTÉRISTIQUES"
uid=78_42 InlineTextBox "CARACTÉRISTIQUES"
uid=78_319 ignored
uid=78_320 ignored
uid=78_321 heading "PHYSIQUE" level="4"
uid=78_322 StaticText "PHYSIQUE"
uid=78_42 InlineTextBox "PHYSIQUE"
uid=78_323 generic
uid=78_324 LabelText
uid=78_325 StaticText "Vigueur"
uid=78_42 InlineTextBox "Vigueur"
uid=78_326 generic
uid=78_327 LabelText
uid=78_328 StaticText "Santé"
uid=78_42 InlineTextBox "Santé"
uid=78_329 ignored
uid=78_330 heading "MANUEL" level="4"
uid=78_331 StaticText "MANUEL"
uid=78_42 InlineTextBox "MANUEL"
uid=78_332 generic
uid=78_333 LabelText
uid=78_334 StaticText "Précision"
uid=78_42 InlineTextBox "Précision"
uid=78_335 generic
uid=78_336 LabelText
uid=78_337 StaticText "Réflexes"
uid=78_42 InlineTextBox "Réflexes"
uid=78_338 ignored
uid=78_339 heading "MENTAL" level="4"
uid=78_340 StaticText "MENTAL"
uid=78_42 InlineTextBox "MENTAL"
uid=78_341 generic
uid=78_342 LabelText
uid=78_343 StaticText "Savoir"
uid=78_42 InlineTextBox "Savoir"
uid=78_344 generic
uid=78_345 LabelText
uid=78_346 StaticText "Perception"
uid=78_42 InlineTextBox "Perception"
uid=78_347 ignored
uid=78_348 heading "SOCIAL" level="4"
uid=78_349 StaticText "SOCIAL"
uid=78_42 InlineTextBox "SOCIAL"
uid=78_350 generic
uid=78_351 LabelText
uid=78_352 StaticText "Volonté"
uid=78_42 InlineTextBox "Volonté"
uid=78_353 generic
uid=78_354 LabelText
uid=78_355 StaticText "Empathie"
uid=78_42 InlineTextBox "Empathie"
uid=78_356 heading "COMPÉTENCES" level="3"
uid=78_357 StaticText "COMPÉTENCES"
uid=78_42 InlineTextBox "COMPÉTENCES"
uid=78_358 ignored
uid=78_359 ignored
uid=78_360 heading "L'HOMME" level="4"
uid=78_361 StaticText "L'HOMME"
uid=78_42 InlineTextBox "L'HOMME"
uid=78_362 generic
uid=78_363 LabelText
uid=78_364 StaticText "Arts "
uid=78_42 InlineTextBox "Arts "
uid=78_365 superscript
uid=78_366 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_367 generic
uid=78_368 LabelText
uid=78_369 StaticText "Civilisation "
uid=78_42 InlineTextBox "Civilisation "
uid=78_370 superscript
uid=78_371 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_372 generic
uid=78_373 LabelText
uid=78_374 StaticText "Psychologie "
uid=78_42 InlineTextBox "Psychologie "
uid=78_375 superscript
uid=78_376 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_377 generic
uid=78_378 LabelText
uid=78_379 StaticText "Rumeurs"
uid=78_42 InlineTextBox "Rumeurs"
uid=78_380 generic
uid=78_381 LabelText
uid=78_382 StaticText "Soins "
uid=78_42 InlineTextBox "Soins "
uid=78_383 superscript
uid=78_384 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_385 ignored
uid=78_386 heading "L'ANIMAL" level="4"
uid=78_387 StaticText "L'ANIMAL"
uid=78_42 InlineTextBox "L'ANIMAL"
uid=78_388 generic
uid=78_389 LabelText
uid=78_390 StaticText "Animalisme "
uid=78_42 InlineTextBox "Animalisme "
uid=78_391 superscript
uid=78_392 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_393 generic
uid=78_394 LabelText
uid=78_395 StaticText "Dissection "
uid=78_42 InlineTextBox "Dissection "
uid=78_396 superscript
uid=78_397 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_398 generic
uid=78_399 LabelText
uid=78_400 StaticText "Faune "
uid=78_42 InlineTextBox "Faune "
uid=78_401 superscript
uid=78_402 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_403 generic
uid=78_404 LabelText
uid=78_405 StaticText "Répulsion"
uid=78_42 InlineTextBox "Répulsion"
uid=78_406 generic
uid=78_407 LabelText
uid=78_408 StaticText "Pistage"
uid=78_42 InlineTextBox "Pistage"
uid=78_409 ignored
uid=78_410 heading "LA MACHINE" level="4"
uid=78_411 StaticText "LA MACHINE"
uid=78_42 InlineTextBox "LA MACHINE"
uid=78_412 generic
uid=78_413 LabelText
uid=78_414 StaticText "Artisanat "
uid=78_42 InlineTextBox "Artisanat "
uid=78_415 superscript
uid=78_416 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_417 generic
uid=78_418 LabelText
uid=78_419 StaticText "Bricolage"
uid=78_42 InlineTextBox "Bricolage"
uid=78_420 generic
uid=78_421 LabelText
uid=78_422 StaticText "Mécanique "
uid=78_42 InlineTextBox "Mécanique "
uid=78_423 superscript
uid=78_424 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_425 generic
uid=78_426 LabelText
uid=78_427 StaticText "Pilotage "
uid=78_42 InlineTextBox "Pilotage "
uid=78_428 superscript
uid=78_429 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_430 generic
uid=78_431 LabelText
uid=78_432 StaticText "Technologie "
uid=78_42 InlineTextBox "Technologie "
uid=78_433 superscript
uid=78_434 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_435 ignored
uid=78_436 heading "L'ARME" level="4"
uid=78_437 StaticText "L'ARME"
uid=78_42 InlineTextBox "L'ARME"
uid=78_438 generic
uid=78_439 LabelText
uid=78_440 StaticText "Armes à feu "
uid=78_42 InlineTextBox "Armes à feu "
uid=78_441 superscript
uid=78_442 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_443 generic
uid=78_444 LabelText
uid=78_445 StaticText "Tir à l'arc"
uid=78_42 InlineTextBox "Tir à l'arc"
uid=78_446 generic
uid=78_447 LabelText
uid=78_448 StaticText "Armurerie "
uid=78_42 InlineTextBox "Armurerie "
uid=78_449 superscript
uid=78_450 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_451 generic
uid=78_452 LabelText
uid=78_453 StaticText "Lancer"
uid=78_42 InlineTextBox "Lancer"
uid=78_454 generic
uid=78_455 LabelText
uid=78_456 StaticText "Mêlée"
uid=78_42 InlineTextBox "Mêlée"
uid=78_457 ignored
uid=78_458 heading "LA SURVIE" level="4"
uid=78_459 StaticText "LA SURVIE"
uid=78_42 InlineTextBox "LA SURVIE"
uid=78_460 generic
uid=78_461 LabelText
uid=78_462 StaticText "Vigilance"
uid=78_42 InlineTextBox "Vigilance"
uid=78_463 generic
uid=78_464 LabelText
uid=78_465 StaticText "Athlétisme"
uid=78_42 InlineTextBox "Athlétisme"
uid=78_466 generic
uid=78_467 LabelText
uid=78_468 StaticText "Alimentation"
uid=78_42 InlineTextBox "Alimentation"
uid=78_469 generic
uid=78_470 LabelText
uid=78_471 StaticText "Discrétion"
uid=78_42 InlineTextBox "Discrétion"
uid=78_472 generic
uid=78_473 LabelText
uid=78_474 StaticText "Corps-à-corps"
uid=78_42 InlineTextBox "Corps-à-corps"
uid=78_475 ignored
uid=78_476 heading "LA TERRE" level="4"
uid=78_477 StaticText "LA TERRE"
uid=78_42 InlineTextBox "LA TERRE"
uid=78_478 generic
uid=78_479 LabelText
uid=78_480 StaticText "Environnement "
uid=78_42 InlineTextBox "Environnement "
uid=78_481 superscript
uid=78_482 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_483 generic
uid=78_484 LabelText
uid=78_485 StaticText "Flore "
uid=78_42 InlineTextBox "Flore "
uid=78_486 superscript
uid=78_487 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_488 generic
uid=78_489 LabelText
uid=78_490 StaticText "Route"
uid=78_42 InlineTextBox "Route"
uid=78_491 generic
uid=78_492 LabelText
uid=78_493 StaticText "Toxiques "
uid=78_42 InlineTextBox "Toxiques "
uid=78_494 superscript
uid=78_495 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_496 generic
uid=78_497 LabelText
uid=78_498 StaticText "Vestiges "
uid=78_42 InlineTextBox "Vestiges "
uid=78_499 superscript
uid=78_500 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_501 ignored
uid=78_502 ignored
uid=78_503 ignored
uid=78_504 ignored
uid=78_505 ignored
uid=78_506 ignored
uid=78_507 ignored
uid=78_508 ignored
uid=78_509 ignored
uid=78_510 ignored
uid=78_511 ignored
uid=78_512 ignored
uid=78_513 ignored
uid=78_514 ignored
uid=78_515 ignored
uid=78_516 ignored
uid=78_517 ignored
uid=78_518 ignored
uid=78_519 ignored
uid=78_520 ignored
uid=78_521 ignored
uid=78_522 ignored
uid=78_523 ignored
uid=78_524 ignored
uid=78_525 ignored
uid=78_526 ignored
uid=78_527 ignored
uid=78_528 ignored
uid=78_529 ignored
uid=78_530 ignored
uid=78_531 ignored
uid=78_532 ignored
uid=78_533 ignored
uid=78_534 ignored
uid=78_535 ignored
uid=78_536 ignored
uid=78_537 ignored
uid=78_538 ignored
uid=78_539 ignored
uid=78_540 ignored
uid=78_541 ignored
uid=78_542 ignored
uid=78_543 ignored
uid=78_544 ignored
uid=78_545 ignored
uid=78_546 ignored
uid=78_547 ignored
uid=78_548 ignored
uid=78_549 ignored
uid=78_550 ignored
uid=78_551 ignored
uid=78_552 ignored
uid=78_553 ignored
uid=78_554 ignored
uid=78_555 generic
-583
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
vermine2047.min.css
+63 -20
View File
@@ -1,40 +1,83 @@
'use strict'; 'use strict';
const gulp = require('gulp'); const gulp = require('gulp');
const sass = require('gulp-sass')(require('sass')); const less = require('gulp-less');
const autoprefixer = require('gulp-autoprefixer');
const cleanCSS = require('gulp-clean-css');
const rename = require('gulp-rename');
var browserSync = require('browser-sync').create(); var browserSync = require('browser-sync').create();
function buildStyles() { // ============================================
return gulp.src('./scss/vermine2047.scss') // LESS Tasks
.pipe(sass({ outputStyle: 'compressed' }).on('error', sass.logError)) // ============================================
function buildLess() {
return gulp.src('./less/vermine2047.less')
.pipe(less().on('error', function(err) {
console.error('LESS compilation error:', err.message);
this.emit('end');
}))
.pipe(autoprefixer({
overrideBrowserslist: ['> 1%', 'last 2 versions', 'Firefox ESR'],
cascade: false
}))
.pipe(cleanCSS({ compatibility: 'ie11' }))
.pipe(rename({ suffix: '.min' }))
.pipe(gulp.dest('./css')) .pipe(gulp.dest('./css'))
.pipe(browserSync.stream()); .pipe(browserSync.stream());
}
// Build LESS without minification (for debugging)
function buildLessDev() {
return gulp.src('./less/vermine2047.less')
.pipe(less().on('error', function(err) {
console.error('LESS compilation error:', err.message);
this.emit('end');
}))
.pipe(autoprefixer({
overrideBrowserslist: ['> 1%', 'last 2 versions', 'Firefox ESR'],
cascade: false
}))
.pipe(rename({ suffix: '.dev' }))
.pipe(gulp.dest('./css'))
.pipe(browserSync.stream());
}
function watchLess() {
gulp.watch(['./less/**/*.less', './less/*.less'], buildLess);
}
};
function reloadTemplatesHTML() { function reloadTemplatesHTML() {
return browserSync.reload("templates/**/*.html");
return browserSync.reload("templates/**/*.html")
} }
function reloadTemplatesHBS() { function reloadTemplatesHBS() {
return browserSync.reload("templates/**/*.hbs");
return browserSync.reload("templates/**/*.hbs")
} }
exports.buildStyles = buildStyles;
exports.watch = function () {
browserSync.init(
{
server: false,
proxy: {
target: "https://localhost:30000/",
ws: true,
}
// ============================================
// Exports
// ============================================
exports.buildLess = buildLess;
exports.buildLessDev = buildLessDev;
exports.buildCSS = gulp.series(buildLess);
exports.buildAllCSS = gulp.series(buildLess, buildLessDev);
exports.watch = function () {
browserSync.init({
server: false,
proxy: {
target: "https://localhost:30000/",
ws: true,
} }
); });
// Watch templates
gulp.watch("./templates/**/*.html").on('change', reloadTemplatesHTML); gulp.watch("./templates/**/*.html").on('change', reloadTemplatesHTML);
gulp.watch("./templates/**/*.hbs").on('change', reloadTemplatesHBS); gulp.watch("./templates/**/*.hbs").on('change', reloadTemplatesHBS);
gulp.watch(['./scss/**/*.scss', './scss/*.scss'], buildStyles);
// Watch LESS files
gulp.watch(['./less/**/*.less', './less/*.less'], buildLess);
}; };
exports.default = buildLess;
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 834 KiB

+158 -6
View File
@@ -13,6 +13,22 @@
}, },
"VERMINE.WorldSettings.GameMode.Name": "Game Mode Selection", "VERMINE.WorldSettings.GameMode.Name": "Game Mode Selection",
"VERMINE.WorldSettings.GameMode.Hint": "Just like some video games offer different modes, Vermine 2047 lets players choose their Game Mode and set the degree of realism, supernatural and dangerosity of the universe themselves.", "VERMINE.WorldSettings.GameMode.Hint": "Just like some video games offer different modes, Vermine 2047 lets players choose their Game Mode and set the degree of realism, supernatural and dangerosity of the universe themselves.",
"TYPES.Item.item": "Item",
"TYPES.Item.weapon": "Weapon",
"TYPES.Item.defense": "Defense",
"TYPES.Item.vehicle": "Vehicle",
"TYPES.Item.ability": "Ability",
"TYPES.Item.specialty": "Specialty",
"TYPES.Item.background": "Background",
"TYPES.Item.trauma": "Trauma",
"TYPES.Item.evolution": "Adaptation",
"TYPES.Item.rumor": "Rumor",
"TYPES.Item.target": "Target",
"TYPES.Item.rite": "Rite",
"TYPES.Actor.character": "Character",
"TYPES.Actor.npc": "NPC",
"TYPES.Actor.group": "Group",
"TYPES.Actor.creature": "Creature",
"GAME_MODES": { "GAME_MODES": {
"heroic": { "heroic": {
"name": "Heroic" "name": "Heroic"
@@ -22,7 +38,10 @@
}, },
"legendary": { "legendary": {
"name": "Legendary" "name": "Legendary"
} },
"survival": "Survival",
"nightmare": "Nightmare",
"apocalypse": "Apocalypse"
}, },
"ROLLS": { "ROLLS": {
"tool": "Dice Roller", "tool": "Dice Roller",
@@ -86,7 +105,8 @@
"relations": "Relations", "relations": "Relations",
"morale": "Morale", "morale": "Morale",
"morale_level": "Morale level", "morale_level": "Morale level",
"notes": "Notes" "notes": "Notes",
"biography": "Biography"
}, },
"ADVERSITY": { "ADVERSITY": {
"threat": "Threat", "threat": "Threat",
@@ -108,7 +128,11 @@
"skills": "Skills", "skills": "Skills",
"pattern": "Pattern", "pattern": "Pattern",
"size": "Size", "size": "Size",
"pack": "Group" "pack": "Group",
"gear_hindrance": "Gear Hindrance",
"skills_placeholder": "Skills Placeholder",
"threat_details": "Threat Details",
"role_details": "Role Details"
}, },
"VERMINE": { "VERMINE": {
"name": "Vermine", "name": "Vermine",
@@ -180,7 +204,106 @@
"encounters": "Encounters", "encounters": "Encounters",
"road": "The Road", "road": "The Road",
"totem_picker": "Totem picker", "totem_picker": "Totem picker",
"actor_picker": "Actor picker" "actor_picker": "Actor picker",
"ability_category": {
"physical": "Physical",
"manual": "Manual",
"mental": "Mental",
"social": "Social"
},
"skill_category": {
"man": "Man",
"animal": "Animal",
"tool": "Tool",
"weapon": "Weapon",
"survival": "Survival",
"world": "World"
},
"CharacterNamePlaceholder": "Character Name",
"none": "None",
"bonuses": "Bonuses",
"choose_ability": "Choose Ability",
"choose_skill": "Choose Skill",
"grant_reroll": "Grant Reroll",
"handicap": "Handicap",
"keep_totem": "Keep Totem",
"morale_crisis": "Morale Crisis",
"morale_high": "High Morale",
"morale_low": "Low Morale",
"morale_normal": "Normal Morale",
"rerolls_possible": "Rerolls Possible",
"reserve": "Reserve",
"score": "Score",
"success_count": "Success Count",
"success_required": "Success Required",
"test_of": "Test of",
"total": "Total",
"totem_dice": "Totem Dice",
"totem_hint": "Totem Hint",
"FightTool": "Fight Tool",
"RollTool": "Roll Tool",
"Roll4Fight": "Roll for Fight",
"Selected": "Selected",
"PurposeTrait": "Purpose Trait",
"SpleenTrait": "Spleen Trait",
"ConfrontationHint": "Confrontation Hint",
"Achievement": "Achievement",
"Conservation": "Conservation",
"error_cannot_reroll": "Cannot reroll",
"error_no_rerolls_left": "No rerolls left",
"error_no_actor_selected": "No actor selected",
"error_not_enough_self_control": "Not enough self control",
"error_select_ability": "Select an ability",
"tabs": {
"abilities": "Abilities",
"combat": "Combat",
"equipment": "Equipment",
"stories": "Stories",
"totem": "Totem"
},
"base_values": "Base Values",
"computed_values": "Computed Values",
"cost": "Cost",
"creature": "Creature",
"effect": "Effect",
"error_select_skill": "Select a skill",
"error_unknown_actor": "Unknown actor",
"instincts": "Instincts",
"instincts_placeholder": "eg: Aggressive, Defensive",
"learn": "Learn",
"major_objectives": "Major Objectives",
"minor_objectives": "Minor Objectives",
"morale": "Morale",
"needed": "Needed",
"objective_placeholder": "eg: Survive, Explore",
"objectives": "Objectives",
"pack": "Pack",
"pattern": "Pattern",
"preferred_category": "Preferred Category",
"prohibits": "Prohibits",
"prohibits_placeholder": "eg: Fire, Water",
"rarity_0": "Common",
"rarity_1": "Rare",
"rarity_2": "Very Rare",
"reserves": "Reserves",
"ritual": "Ritual",
"sexes": {
"female": "Female",
"male": "Male"
},
"size": "Size",
"total_attack": "Total Attack",
"total_damage": "Total Damage",
"trance": "Trance",
"vote_reserve": "Vote Reserve",
"wound_thresholds": "Wound Thresholds",
"types": {
"shield": "Shield"
},
"roll": "Roll",
"cancel": "Cancel",
"playMode": "Play Mode",
"editMode": "Edit Mode"
}, },
"UI": { "UI": {
"add": "Add", "add": "Add",
@@ -195,9 +318,15 @@
"temporary": "Temporary effects", "temporary": "Temporary effects",
"passive": "Passive effects", "passive": "Passive effects",
"inactive": "Inactive effects" "inactive": "Inactive effects"
} },
"effects.inactive": "Inactive",
"effects.passive": "Passive",
"effects.temporary": "Temporary"
}, },
"ITEMS": { "ITEMS": {
"item": "Item",
"items": "Items",
"new_item": "New item",
"defense": "Protection", "defense": "Protection",
"defenses": "Protections", "defenses": "Protections",
"new_defense": "New protection", "new_defense": "New protection",
@@ -216,6 +345,12 @@
"abilities": "Abilities", "abilities": "Abilities",
"specialties": "Specialties", "specialties": "Specialties",
"new_specialty": "New specialty", "new_specialty": "New specialty",
"target": "Target",
"targets": "Targets",
"new_target": "New target",
"rite": "Rite",
"rites": "Rites",
"new_rite": "New rite",
"evolution": "Adaptation", "evolution": "Adaptation",
"new_evolution": "New adaptation", "new_evolution": "New adaptation",
"evolutions": "Adaptations", "evolutions": "Adaptations",
@@ -227,7 +362,8 @@
"rituel": "Ritual", "rituel": "Ritual",
"transe": "Trance", "transe": "Trance",
"effects": "Effects", "effects": "Effects",
"details": "Details" "details": "Details",
"shield": "Shield"
}, },
"ABILITIES": { "ABILITIES": {
"vigor": { "vigor": {
@@ -498,10 +634,26 @@
}, },
"ruins": { "ruins": {
"name": "Ruins" "name": "Ruins"
},
"athletics": {
"name": "Athletics"
},
"fauna": {
"name": "Fauna"
} }
}, },
"SEXES": { "SEXES": {
"male": "Masculine", "male": "Masculine",
"female": "Feminine" "female": "Feminine"
},
"COMBAT": {
"Encounter": "Encounter",
"None": "None",
"Round": "Round"
},
"SIZE_LEVELS": {
"small": "Small",
"medium": "Medium",
"large": "Large"
} }
} }
+91 -69
View File
@@ -1,4 +1,26 @@
{ {
"NAME": "Nom",
"NONE": "Aucun",
"ORIGIN": "Origine",
"PROFILE": "Profil",
"THEME": "Concept",
"TOTEM": "Totem",
"TYPES.Item.item": "Objet",
"TYPES.Item.weapon": "Arme",
"TYPES.Item.defense": "Protection",
"TYPES.Item.vehicle": "Véhicule",
"TYPES.Item.ability": "Capacité",
"TYPES.Item.specialty": "Spécialité",
"TYPES.Item.background": "Historique",
"TYPES.Item.trauma": "Traumatisme",
"TYPES.Item.evolution": "Adaptation",
"TYPES.Item.rumor": "Rumeur",
"TYPES.Item.target": "Cible",
"TYPES.Item.rite": "Rite",
"TYPES.Actor.character": "Personnage",
"TYPES.Actor.npc": "PNJ",
"TYPES.Actor.group": "Groupe",
"TYPES.Actor.creature": "Créature",
"SETTINGS": { "SETTINGS": {
"world": { "world": {
"game_mode": { "game_mode": {
@@ -132,9 +154,13 @@
"preferred_category": "Catégorie préférée", "preferred_category": "Catégorie préférée",
"wounds": { "wounds": {
"name": "Blessures", "name": "Blessures",
"light": "Légères", "threshold": "Seuil",
"heavy": "Graves", "light": "Légère",
"deadly": "Mortelles" "heavy": "Grave",
"deadly": "Mortelle",
"light_wounds": "Blessure légère",
"heavy_wounds": "Blessure grave",
"deadly_wounds": "Blessure mortelle"
}, },
"ability_category": { "ability_category": {
"physical": "Physiques", "physical": "Physiques",
@@ -153,6 +179,7 @@
"rarity_0": "Commun", "rarity_0": "Commun",
"rarity_1": "Peu commun", "rarity_1": "Peu commun",
"rarity_2": "Rare", "rarity_2": "Rare",
"rarity_3": "Très rare",
"test_of": "test de", "test_of": "test de",
"rerolls_possible": "relances possibles", "rerolls_possible": "relances possibles",
"grant_reroll": "accorder des relances", "grant_reroll": "accorder des relances",
@@ -162,7 +189,7 @@
"error_cannot_reroll": "Vous ne pouvez pas relancer un dés sur ce jet", "error_cannot_reroll": "Vous ne pouvez pas relancer un dés sur ce jet",
"error_no_rerolls_left": "Plus de relances possibles", "error_no_rerolls_left": "Plus de relances possibles",
"error_select_ability": "Veuillez sélectionner une caractéristique", "error_select_ability": "Veuillez sélectionner une caractéristique",
"error_not_enough_self_control": "Pas assez de points de sang-froid", "error_not_enough_self_control": "Vous n'avez pas assez de Sang-Froid",
"FightTool": "Outil de combat", "FightTool": "Outil de combat",
"Roll4Fight": "Lancer pour le combat", "Roll4Fight": "Lancer pour le combat",
"Selected": "Sélectionné", "Selected": "Sélectionné",
@@ -174,7 +201,9 @@
"group": "Groupe", "group": "Groupe",
"abilities": "Caractéristiques", "abilities": "Caractéristiques",
"ability": "Caractéristique", "ability": "Caractéristique",
"skills": "Compétences",
"skills_title": "Compétences", "skills_title": "Compétences",
"skill": "Compétence",
"skill_title": "Compétence", "skill_title": "Compétence",
"skill_mastery": "Maîtrise", "skill_mastery": "Maîtrise",
"bonus": "Bonus", "bonus": "Bonus",
@@ -182,6 +211,8 @@
"reroll": "Relance", "reroll": "Relance",
"equipment": "Equipement", "equipment": "Equipement",
"specialty": "Spécialité", "specialty": "Spécialité",
"specificLevel": "Niveau spécifique",
"competence": "Compétence",
"technique": "Technique", "technique": "Technique",
"techniques": "Techniques", "techniques": "Techniques",
"difficulty": "Difficulté", "difficulty": "Difficulté",
@@ -200,16 +231,6 @@
"rarity_sm": "Rar.", "rarity_sm": "Rar.",
"reliability": "Fiabilité", "reliability": "Fiabilité",
"reliability_sm": "Fiab.", "reliability_sm": "Fiab.",
"wounds": {
"name": "Blessures",
"threshold": "Seuil",
"light": "Légère",
"heavy": "Grave",
"deadly": "Mortelle",
"light_wounds": "Blessure légère",
"heavy_wounds": "Blessure grave",
"deadly_wounds": "Blessure mortelle"
},
"choose_ability": "Choisissez une caractéristique", "choose_ability": "Choisissez une caractéristique",
"choose_skill": "Choisissez une compétence", "choose_skill": "Choisissez une compétence",
"none": "Aucun", "none": "Aucun",
@@ -220,8 +241,6 @@
"totem_dice": "Dés de totem", "totem_dice": "Dés de totem",
"keep_totem": "Garder le totem", "keep_totem": "Garder le totem",
"totem_hint": "Cochez pour utiliser les dés de totem (double réussite possible)", "totem_hint": "Cochez pour utiliser les dés de totem (double réussite possible)",
"error_not_enough_self_control": "Vous n'avez pas assez de Sang-Froid",
"error_select_ability": "Veuillez sélectionner une caractéristique",
"error_select_skill": "Veuillez sélectionner une compétence", "error_select_skill": "Veuillez sélectionner une compétence",
"needed": "nécessaire", "needed": "nécessaire",
"cost": "Coût", "cost": "Coût",
@@ -243,11 +262,15 @@
"ammo_sm": "Mun", "ammo_sm": "Mun",
"trait": "Trait", "trait": "Trait",
"traits": "Traits", "traits": "Traits",
"traits_selector": "Sélecteur de traits",
"clew": "Indice", "clew": "Indice",
"combat": "Combat", "combat": "Combat",
"stories": "Histoires", "stories": "Histoires",
"gear": "Matériel", "gear": "Matériel",
"information": "Informations", "information": "Informations",
"editMode": "Mode édition",
"playMode": "Mode jeu",
"modes": "Modes de jeu",
"boost": "boost", "boost": "boost",
"group_abilities": "Capacités de groupe", "group_abilities": "Capacités de groupe",
"totem_abilities": "Capacités de totem", "totem_abilities": "Capacités de totem",
@@ -268,7 +291,6 @@
"minor_objectives": "Objectifs mineurs", "minor_objectives": "Objectifs mineurs",
"instincts": "Instincts", "instincts": "Instincts",
"prohibits": "Interdictions", "prohibits": "Interdictions",
"group_abilities": "Capacités de groupe",
"vote_reserve": "Vote pour utiliser la réserve", "vote_reserve": "Vote pour utiliser la réserve",
"morale_high": "Haut", "morale_high": "Haut",
"morale_normal": "Normal", "morale_normal": "Normal",
@@ -286,7 +308,15 @@
"base_values": "Valeurs de base", "base_values": "Valeurs de base",
"total_attack": "Attaque totale", "total_attack": "Attaque totale",
"total_damage": "Dégâts totaux", "total_damage": "Dégâts totaux",
"wound_thresholds": "Seuils de blessures" "wound_thresholds": "Seuils de blessures",
"CharacterNamePlaceholder": "Nom du personnage",
"sexes": {
"female": "Féminin",
"male": "Masculin"
},
"RollTool": "Outil de lancer de dés",
"roll": "Lancer",
"cancel": "Annuler"
}, },
"UI": { "UI": {
"add": "Ajouter", "add": "Ajouter",
@@ -304,6 +334,9 @@
} }
}, },
"ITEMS": { "ITEMS": {
"item": "Objet",
"items": "Objets",
"new_item": "Nouvel objet",
"defense": "Protection", "defense": "Protection",
"defenses": "Protections", "defenses": "Protections",
"new_defense": "Nouvelle protection", "new_defense": "Nouvelle protection",
@@ -322,6 +355,12 @@
"abilities": "Capacités", "abilities": "Capacités",
"specialties": "Spécialités", "specialties": "Spécialités",
"new_specialty": "Nouvelle spécialité", "new_specialty": "Nouvelle spécialité",
"target": "Cible",
"targets": "Cibles",
"new_target": "Nouvelle cible",
"rite": "Rite",
"rites": "Rites",
"new_rite": "Nouveau rite",
"shield": "Bouclier", "shield": "Bouclier",
"evolution": "Adaptation", "evolution": "Adaptation",
"new_evolution": "Nouvelle adaptation", "new_evolution": "Nouvelle adaptation",
@@ -522,7 +561,7 @@
"name": "Arts" "name": "Arts"
}, },
"civilization": { "civilization": {
"name": "Civilisations" "name": "Civilisation"
}, },
"psychology": { "psychology": {
"name": "Psychologie" "name": "Psychologie"
@@ -539,14 +578,14 @@
"dissection": { "dissection": {
"name": "Dissection" "name": "Dissection"
}, },
"wildlife": { "fauna": {
"name": "Faune" "name": "Faune"
}, },
"repulsion": { "repulsion": {
"name": "Répulsion" "name": "Répulsion"
}, },
"tracks": { "tracks": {
"name": "Traces" "name": "Pistage"
}, },
"crafting": { "crafting": {
"name": "Artisanat" "name": "Artisanat"
@@ -566,42 +605,39 @@
"firearms": { "firearms": {
"name": "Armes à feu" "name": "Armes à feu"
}, },
"archery": {
"name": "Tir à l'arc"
},
"armory": { "armory": {
"name": "Armurerie" "name": "Armurerie"
}, },
"shield": {
"name": "Bouclier"
},
"close": {
"name": "Corps-à-corps"
},
"archery": {
"name": "Armes de tir"
},
"throwing": { "throwing": {
"name": "Lancer" "name": "Lancer"
}, },
"melee": { "melee": {
"name": "Mêlée" "name": "Mêlée"
}, },
"atletics": {
"name": "Atlétisme"
},
"stealth": {
"name": "Discrétion"
},
"alertness": { "alertness": {
"name": "Vigilance" "name": "Vigilance"
}, },
"flora": { "athletics": {
"name": "Flore" "name": "Athlétisme"
}, },
"food": { "food": {
"name": "Alimentation" "name": "Alimentation"
}, },
"stealth": {
"name": "Discrétion"
},
"close": {
"name": "Corps-à-corps"
},
"environment": { "environment": {
"name": "Environnement" "name": "Environnement"
}, },
"flora": {
"name": "Flore"
},
"road": { "road": {
"name": "Route" "name": "Route"
}, },
@@ -610,42 +646,28 @@
}, },
"ruins": { "ruins": {
"name": "Vestiges" "name": "Vestiges"
},
"shield": {
"name": "Bouclier"
},
"wildlife": {
"name": "Faune"
},
"atletics": {
"name": "Athlétisme"
} }
}, },
"SEXES": { "SEXES": {
"male": "Masculin", "male": "Masculin",
"female": "Féminin" "female": "Féminin"
}, },
"SKILLS": { "COMBAT": {
"arts": "Arts", "Begin": "Commencer",
"civilization": "Civilisation", "Encounter": "Rencontre",
"psychology": "Psychologie", "End": "Terminer",
"rumors": "Rumeurs", "None": "Aucun",
"healing": "Soins", "NotStarted": "Non commencé",
"animalism": "Animalisme", "Round": "Tour",
"dissection": "Dissection", "TurnEnd": "Fin de tour"
"fauna": "Faune",
"repulsion": "Répulsion",
"tracks": "Pistage",
"crafting": "Artisanat",
"diy": "Bricolage",
"mecanical": "Mécanique",
"piloting": "Pilotage",
"technology": "Technologie",
"firearms": "Armes à feu",
"archery": "Tir à l'arc",
"armory": "Armurerie",
"throwing": "Lancer",
"melee": "Mêlée",
"alertness": "Vigilance",
"athletics": "Athlétisme",
"food": "Alimentation",
"stealth": "Discrétion",
"close": "Corps-à-corps",
"environment": "Environnement",
"flora": "Flore",
"road": "Route",
"toxics": "Toxiques",
"ruins": "Vestiges"
} }
} }
+641
View File
@@ -0,0 +1,641 @@
@import "totem";
.system-vermine2047 .vermine2047.actor {
// ── Fix contrast: dark text on light parchment bg ───────────────────
.window-content,
& {
color: @theme-color-dark;
label, span, p, li, a:not(.active) {
color: @theme-color-dark;
}
h4, h5 {
color: @color-text-dark-header;
}
h3 {
color: @theme-color-light;
}
input:not([type="radio"]):not([type="checkbox"]),
select, textarea {
color: @color-text-light-0;
line-height: 1.2;
padding: 1px 4px;
}
.char-header {
h1 label {
font-size: 0.9rem;
}
.char-name-value {
font-size: 1.5rem;
}
input:not([type="radio"]):not([type="checkbox"]) {
font-size: 1rem;
}
}
.ability input.hexa {
font-size: 0.8rem;
}
.resource-label {
color: @color-text-dark-secondary;
}
.ability label {
color: @color-text-dark-header;
}
.char-header {
color: @theme-color-dark;
label {
color: @color-text-dark-header;
}
}
.hexa {
color: @color-text-dark-primary;
}
.sheet-tabs a {
color: @color-text-dark-inactive;
&.active {
color: @theme-color-dark;
}
}
}
// ── Two-column layout for character sheets ──────────────────────────
&.character {
display: grid !important;
grid-template-columns: 1fr !important;
grid-template-rows: auto 1fr auto !important;
> header.window-header {
grid-column: 1;
grid-row: 1;
}
> section.window-content {
grid-column: 1;
grid-row: 2;
}
> div.window-resize-handle {
grid-column: 1;
grid-row: 3;
}
}
&.character .window-content {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: auto 1fr;
overflow: hidden;
> .tab.main {
grid-column: 1;
grid-row: 1 / -1;
display: flex !important;
flex-direction: column;
overflow-y: auto;
padding: 8px;
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
> nav.tabs {
grid-column: 2;
grid-row: 1;
}
> .tab:not(.main) {
grid-column: 2;
grid-row: 2;
display: none;
overflow-y: auto;
padding: 8px;
&.active {
display: flex;
flex-direction: column;
}
}
}
// ── Stacked layout for NPC sheets (header + tabs + content) ────────
&.npc {
display: grid !important;
grid-template-columns: 1fr !important;
grid-template-rows: auto 1fr auto !important;
> header.window-header {
grid-column: 1;
grid-row: 1;
}
> section.window-content {
grid-column: 1;
grid-row: 2;
}
> div.window-resize-handle {
grid-column: 1;
grid-row: 3;
}
}
&.npc .window-content {
display: grid;
grid-template-rows: auto auto 1fr;
grid-template-columns: 1fr;
overflow: hidden;
> .tab.main {
grid-column: 1;
grid-row: 1;
display: flex !important;
flex-direction: column;
overflow-y: auto;
padding: 8px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
> nav.tabs {
grid-column: 1;
grid-row: 2;
}
> .tab:not(.main) {
grid-column: 1;
grid-row: 3;
display: none;
overflow-y: auto;
padding: 8px;
&.active {
display: flex;
flex-direction: column;
}
}
}
form {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
> .tab {
flex: 1;
overflow-y: auto;
padding: 4px;
}
.charname input {
color: @theme-color-dark;
font-family: "DistressBlack", sans-serif;
font-size: 30px;
font-style: normal;
}
div.hexa {
.hexa-style();
.transition();
margin: 0.2rem;
&:hover {
.hexa-style(rgba(255, 255, 255, 0.425), rgba(0, 0, 0, 0.288));
}
input {
opacity: 1;
min-width: 100%;
min-height: 100%;
opacity: 0;
}
&.checked {
.hexa-style(rgb(0, 0, 0), rgba(0, 0, 0, 0.288));
&:hover { .hexa-style(rgb(43, 43, 43), rgba(0, 0, 0, 0.288)); }
}
&.unavailable {
background: radial-gradient(circle, rgba(66, 15, 15, 0.664) 0%, rgba(131, 70, 70, 0.432) 100%);
pointer-events: none;
}
}
input[type="text"],
input[type="number"] {
width: calc(100% - 2px);
height: calc(100% - 2px);
background: none;
padding: 0;
margin: 1px 0;
color: #333;
border: 1px solid rgba(0, 0, 0, 0);
&.hexa { .hexa-style(); }
}
input[disabled],
select[disabled] {
cursor: not-allowed;
}
input[type="text"]:hover:not(:disabled),
input[type="text"]:focus,
select:hover:not(:disabled),
select:focus,
input[type="number"]:hover:not(:disabled),
input[type="number"]:focus,
textarea:hover:not(:disabled),
textarea:focus {
box-shadow: 0 0 10px @theme-color-highlight inset;
}
select {
font-size: 0.6rem;
border: none;
appearance: none;
min-width: fit-content;
max-width: fit-content;
padding: 0 0.2rem;
margin: 0 0.2rem;
cursor: help;
}
label {
display: block;
}
.mce-panel span {
display: inherit;
}
.rollable:hover,
a:hover {
color: #000;
text-shadow: 0 5px 5px @theme-color-accent;
cursor: pointer;
}
.chooseTotem {
font-family: "DistressBlack", sans-serif;
font-size: 0.9rem;
cursor: pointer;
&:hover {
text-shadow: 0 0 10px @theme-color-highlight;
}
}
}
.sheet-header-toggle {
text-align: right;
margin-bottom: 0.5rem;
button {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
padding: 0.2rem 1rem;
cursor: pointer;
.background-image(url("@{ui-path}/scotch.webp"), no-repeat, cover, 50% 0%);
border: none;
box-shadow: 0px 0px 3px rgba(31, 26, 26, 0.979) inset;
color: @theme-color-dark;
&:hover {
box-shadow: 0 0 10px @theme-color-highlight inset;
}
}
}
.image-wrapper {
text-align: center;
margin-bottom: 1rem;
img {
width: 80%;
height: auto;
}
}
.padding-with-frieze {
margin-left: 18% !important;
margin-right: 10% !important;
li {
max-width: 100%;
}
.major-totem {
position: relative;
h4 {
position: absolute;
transform: rotate(-8deg);
opacity: 1;
transition: 0.2s;
}
}
.paper {
margin-top: 1rem;
height: 350px;
}
.second-paper {
margin-top: 4rem;
height: 150px;
}
}
.char-header {
font-family: "DistressBlack", sans-serif;
section {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
}
h1.char-name,
.char-vermine2047 {
border-bottom: none;
line-height: 2rem;
}
.char-vermine2047 {
font-size: 1.5rem;
}
}
h3 {
font-family: "DistressBlack", sans-serif;
text-align: center;
text-transform: uppercase;
color: @theme-color-light;
font-size: 1.7rem;
border-bottom: none;
margin: 0;
}
h4 {
font-family: "DistressBlack", sans-serif;
font-size: 1.4em;
text-transform: uppercase;
margin: 0 0 0.2rem;
&.characteristics {
font-size: 1.25rem;
margin-top: 0.1rem;
}
.tab.totem &,
.tab.equipment &,
.tab.stories & {
margin-top: 0.875rem;
}
}
nav.tabs[data-group="sheet"] {
display: inline-flex;
justify-content: space-around;
align-items: center;
height: 54px;
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, auto, right top);
background-size: 100% 100%;
width: 100%;
position: relative;
padding-right: 4rem;
font-size: 1.4rem;
.item {
height: 2.4rem;
display: inline-flex;
align-items: center;
gap: 0.3rem;
z-index: 1;
transition: all 0.1s ease-out;
color: #606060;
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.404);
&:hover,
&.active {
color: #000;
text-shadow: 0 5px 5px @theme-color-accent;
cursor: pointer;
}
&:hover {
text-shadow: 0 5px 5px rgba(30, 82, 37, 0.6039215686);
}
}
}
.ability {
padding-right: 0.6rem;
font-size: 0.8rem;
border-bottom: 1px solid rgba(170, 170, 152, 0.664);
box-shadow: 0px 0px 15px rgba(128, 128, 128, 0) inset;
transition: 0.2s;
position: relative;
flex-wrap: nowrap;
min-width: min-content;
container-type: inline-size;
container-name: ability-row;
&:hover {
box-shadow: 0px 0px 15px gray inset;
}
label {
min-width: 40%;
flex: 1.3;
}
span {
max-width: fit-content;
margin: 0 1rem;
flex: 0.5;
}
div.specialties {
position: absolute;
bottom: -0.2rem;
font-size: 0.7rem;
}
.skill-dots {
height: 100%;
align-self: center;
flex: 1.5;
min-width: fit-content;
> div {
max-width: 0.7rem;
height: 0.7rem;
aspect-ratio: 1/1;
border-radius: 50%;
font-weight: 700;
text-align: center;
padding-bottom: 0.2rem;
font-style: oblique;
align-self: flex-start;
&.dice-pool-dot {
.background-image(radial-gradient(circle, @dice-pool-color 25%, rgb(0, 0, 0) 100%), @dice-pool-color);
max-width: 0.7rem;
aspect-ratio: 1/1;
border-radius: 50%;
}
&.dice-reroll-dot {
background: radial-gradient(circle, @dice-reroll-color 25%, rgb(0, 0, 0) 100%);
}
}
}
}
.skill-category {
padding: 0.3rem;
&.preferred {
box-shadow: 0px 0px 30px rgba(0, 128, 0, 0.306) inset;
h4, label {
text-shadow: 0px 0px 5px rgba(0, 128, 0, 0.411);
}
}
}
#edit {
background-color: #000;
color: #fff;
width: 100%;
}
.reserve-grid {
line-height: 0.5rem;
transform-origin: 0% 50%;
max-width: fit-content;
align-items: center;
display: flex;
flex-direction: column;
div.flexrow,
input,
.hexa {
margin: 0;
padding: 0;
min-width: 1rem;
min-height: 1rem;
}
> .flexrow {
position: relative;
max-width: fit-content;
justify-content: center;
}
}
.items-list {
.item-name .item-image {
flex: 0 0 30px;
height: 30px;
background-size: 30px;
border: none;
margin-right: 5px;
}
}
.items-list .item.flexrow > div:not(.item-name):not(.item-controls) {
text-align: center;
}
.list-item {
list-style: none;
margin: 0;
padding: 0;
.item {
align-items: center;
padding: 2px;
border-bottom: 1px solid rgba(170, 170, 152, 0.664);
&:last-child { border-bottom: none; }
}
}
// ── V2 tab navigation — chitin-plate style ─────────────────────────
nav.sheet-tabs[data-application-part="tabs"] {
display: flex;
align-items: stretch;
gap: 0;
margin: 0;
padding: 0;
background: @color-chitin-dark;
border-bottom: 2px solid @color-amber;
min-height: 36px;
> a {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 6px;
padding: 6px 16px;
color: @color-text-light-0;
opacity: 1;
transition: background 0.15s;
position: relative;
border-right: 1px solid fade(@color-membrane, 12%);
&:hover {
background: fade(@color-honeycomb, 10%);
text-shadow: none;
}
&.active {
background: linear-gradient(180deg, @color-amber 0%, @color-honeycomb 100%);
color: @color-chitin-dark;
&::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background: @color-acid-green;
}
}
> i {
font-size: @font-size-14;
width: 16px;
text-align: center;
}
> span, > i {
color: inherit;
}
}
}
}
@container ability-row (max-width: 240px) {
.skill-dots,
span.hexa {
display: none;
}
}
+119
View File
@@ -0,0 +1,119 @@
@import "../variables";
@import "../utilities";
.system-vermine2047 .vermine2047.actor.creature {
.sheet-header {
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
padding: 10px;
max-height: 110px;
}
.header-fields { flex: 1; }
.resources { margin-bottom: 10px; }
.resource {
.card-style();
}
.resource-label {
font-weight: bold;
margin-right: 8px;
min-width: 60px;
font-size: @font-size-12;
}
.resource-content {
display: flex;
align-items: center;
select {
margin-right: 8px;
min-width: 80px;
}
}
.charname {
margin: 0;
input {
width: 100%;
font-size: @font-size-18;
font-weight: bold;
text-align: center;
}
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.stat {
.card-style();
display: flex;
justify-content: space-between;
label { font-weight: bold; }
span {
font-weight: bold;
color: @theme-color-light;
}
}
.mdb {
&:not(.row) {
.card-style();
}
h4:first-child {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-14;
margin-top: 0;
border-bottom: 1px solid @color-border-dark-3;
padding-bottom: 5px;
text-align: center;
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
}
ul.unstyled {
margin: 0;
padding: 0;
list-style: none;
}
li {
padding: 3px 0;
border-bottom: 1px solid @color-border-dark-3;
&:last-child { border-bottom: none; }
}
}
input[type="text"] {
width: 100%;
padding: 5px;
margin-bottom: 8px;
}
.grid-3col {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
label {
display: flex;
align-items: center;
gap: 5px;
padding: 5px;
}
}
.row.mdb {
display: flex;
align-items: center;
gap: 8px;
}
}
+99
View File
@@ -0,0 +1,99 @@
@import "../variables";
@import "../utilities";
.system-vermine2047 .vermine2047.actor.group {
.char-header {
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
max-height: 110px;
}
.char-details {
h1.char-name input,
.char-vermine2047 a.chooseTotem {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
}
}
h4 {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-14;
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
}
.reserve-control,
.morale-control {
.card-style();
label {
font-weight: bold;
margin-right: 8px;
min-width: 80px;
}
input, select {
margin-right: 8px;
min-width: 50px;
text-align: center;
}
}
.actor-list {
list-style: none;
padding: 0;
margin: 0;
li.actor {
padding: 5px;
border-bottom: 1px solid @color-border-dark-3;
&:last-child { border-bottom: none; }
&:hover {
box-shadow: 0 0 10px @theme-color-highlight inset;
}
}
.actor-name, .item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.objective-item {
padding: 5px;
border-bottom: 1px solid @color-border-dark-4;
input {
flex: 1;
width: 100%;
}
}
.totem-display {
.card-style();
span { font-weight: bold; }
}
.identity-field {
margin-bottom: 8px;
label {
font-weight: bold;
display: block;
margin-bottom: 3px;
}
input { width: 100%; }
}
.level input,
.reputation input {
width: 60px;
text-align: center;
}
}
+231
View File
@@ -0,0 +1,231 @@
@import "../variables";
@import "../utilities";
.system-vermine2047 .vermine2047.actor.npc {
.sheet-header {
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
padding: 0.5rem;
max-height: 110px;
}
.ability-value,
.resource-value {
min-width: 30px;
text-align: center;
font-weight: bold;
margin-left: 8px;
}
.card, .npc-card {
.card-style();
h4 {
.card-title();
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-14;
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
i { margin-right: 5px; }
}
}
.skills-container {
display: flex;
flex-direction: column;
gap: 15px;
}
.skill-category {
.card-style();
padding: 10px;
h4 {
font-family: "DistressBlack", sans-serif;
font-size: @font-size-14;
text-transform: uppercase;
margin-top: 0;
margin-bottom: 8px;
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
}
}
.skill-item {
display: flex;
align-items: center;
gap: 8px;
padding: 5px;
border-bottom: 1px solid @color-border-dark-3;
&:last-child { border-bottom: none; }
label {
flex: 1;
font-size: @font-size-12;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&:hover {
box-shadow: 0 0 10px @theme-color-highlight inset;
}
}
.skill-control {
display: flex;
align-items: center;
gap: 5px;
}
.skill-value {
min-width: 25px;
text-align: center;
font-weight: bold;
}
.rarity-badge {
display: inline-block;
padding: 1px 4px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
margin-left: 5px;
&.rarity-0 { .rarity-badge(@rarity-0-bg, @rarity-0-text); }
&.rarity-1 { .rarity-badge(@rarity-1-bg, @rarity-1-text); }
&.rarity-2 { .rarity-badge(@rarity-2-bg, @rarity-2-text); }
&.rarity-3 { .rarity-badge(@rarity-3-bg, @rarity-3-text); }
}
.skill-category-selector {
margin-bottom: 15px;
.card-style();
}
.wounds-card {
margin-top: 15px;
.card-style();
}
.wound-item {
display: flex;
flex-direction: column;
gap: 5px;
label {
font-size: @font-size-12;
font-weight: bold;
text-align: center;
}
}
.wound-control {
display: flex;
align-items: center;
gap: 8px;
input { flex: 1; }
span {
font-size: @font-size-12;
min-width: 30px;
text-align: center;
}
}
.ability-section {
.card-style();
}
.ability-category-header {
font-family: "DistressBlack", sans-serif;
font-size: @font-size-14;
text-transform: uppercase;
margin-top: 0;
margin-bottom: 8px;
border-bottom: 1px solid @color-border-dark-3;
padding-bottom: 5px;
}
.ability-item {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 0;
label {
flex: 1;
font-size: @font-size-12;
white-space: nowrap;
}
}
.ability-control {
display: flex;
align-items: center;
gap: 8px;
}
.tab {
padding: 10px;
overflow-y: auto;
}
.form-group {
.form-group-style();
}
.header-fields {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
}
.resources {
display: grid;
gap: 8px;
}
.resource {
.card-style();
label { font-weight: bold; }
}
.resource-label {
font-size: @font-size-12;
font-weight: bold;
white-space: nowrap;
}
.resource-content {
display: flex;
align-items: center;
select {
font-size: @font-size-12;
padding: 3px 5px;
min-width: 100px;
}
}
.editor {
min-height: 100px;
margin-bottom: 10px;
.editor-content {
min-height: 80px;
font-size: @font-size-12;
}
}
.hexa {
.hexa-style();
.transition();
&:hover {
.hexa-style(rgba(255, 255, 255, 0.425), rgba(0, 0, 0, 0.288));
}
}
}
+93
View File
@@ -0,0 +1,93 @@
@import "../variables";
@import "../utilities";
.system-vermine2047 .vermine2047.actor {
.totem-details {
position: relative;
img.img-totem {
transform-origin: 50% 50%;
filter: grayscale(1);
opacity: 0.15;
position: absolute;
width: 30%;
height: auto;
pointer-events: none;
aspect-ratio: 1/1;
left: 35%;
}
}
div.minor-totems {
position: relative;
background-color: rgba(146, 156, 111, 0.5215686275);
h5 {
position: absolute;
top: 0;
img {
max-width: 2rem;
position: absolute;
bottom: -2rem;
}
&.human, &.adapted {
.transition(0.3s);
}
&.human img.img-totem,
&.adapted img.img-totem {
filter: drop-shadow(0px 0px 20px rgb(0, 0, 0));
}
&.human.major,
&.adapted.major {
transform: scale(1.1);
}
&.human.major img,
&.adapted.major img {
filter: drop-shadow(0px 0px 10px red);
}
}
.totem-dice {
.human-dice,
.adapted-dice {
display: flex;
flex-direction: row;
margin-left: 2rem;
}
.human-dice i,
.adapted-dice i {
padding-top: 0.5rem;
color: @totem-human-color;
}
.adapted-dice {
justify-content: flex-end;
margin-left: 0;
margin-right: 2rem;
transform: rotate(180deg);
}
.adapted-dice i {
transform: rotate(180deg);
padding-top: 0.5rem;
color: @totem-adapted-color;
}
}
.human {
left: 0;
img { left: 0; }
}
.adapted {
right: 0;
img { right: 0; }
}
}
}
+650
View File
@@ -0,0 +1,650 @@
@font-face {
font-family: "DistressBlack";
src: url("@{fonts-path}/dcc_sharp_distress_black_by_dccanim.otf");
}
* {
box-sizing: border-box;
}
body.system-vermine2047 {
font-family: "Roboto", sans-serif;
color: @theme-color-secondary;
}
img {
border: none;
}
ul.unstyled {
list-style-type: none;
padding: 0;
margin: 0;
li {
padding: 0;
margin: 0;
}
}
.w-full {
width: 100%;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.window-app {
font-family: "Roboto", sans-serif;
.shadow(0px, 0px, 30px, rgba(106, 176, 76, 0.25));
}
.system-vermine2047 {
.window-content,
form.application.sheet.vermine2047 {
.background-image(url("@{ui-path}/box_background.webp"), repeat);
padding: 10px;
overflow-y: hidden;
}
.dialog .window-content {
.background-image(url("@{ui-path}/fond_chat_box.webp"), repeat);
padding: 0.5rem;
overflow-y: hidden;
}
}
.window-content .row {
&.smb { margin-bottom: 0.25rem; }
&.mdb { margin-bottom: 0.5rem; }
&.lgb { margin-bottom: 1rem; }
}
.rollable:hover,
.rollable:focus {
color: #000;
text-shadow: 0 0 10px @color-acid-green;
cursor: pointer;
.transition();
}
.rollable {
.transition();
}
img.profile-img {
filter: drop-shadow(0px 0px 20px rgb(110, 133, 27));
height: auto;
width: 100%;
max-width: 10rem;
}
body.system-vermine2047 img#logo {
content: url("@{assets-path}/images/ui/logo_vermine_foundry.webp");
height: auto;
}
#chat-log,
.chat-log {
.message {
.background-image(url("@{ui-path}/box_background.webp"), repeat);
}
}
.grid {
display: grid;
grid-column: span 2 / span 2;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
margin: 5px 0;
padding: 0;
}
.grid-2col { .grid(); }
.grid-3col { grid-column: span 3; grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4col { grid-column: span 4; grid-template-columns: repeat(4, minmax(0, 1fr)); }
.grid-5col { grid-column: span 5; grid-template-columns: repeat(5, minmax(0, 1fr)); }
.grid-6col { grid-column: span 6; grid-template-columns: repeat(6, minmax(0, 1fr)); }
.grid-7col { grid-column: span 7; grid-template-columns: repeat(7, minmax(0, 1fr)); }
.grid-8col { grid-column: span 8; grid-template-columns: repeat(8, minmax(0, 1fr)); }
.grid-9col { grid-column: span 9; grid-template-columns: repeat(9, minmax(0, 1fr)); }
.grid-10col { grid-column: span 10; grid-template-columns: repeat(10, minmax(0, 1fr)); }
.grid-11col { grid-column: span 11; grid-template-columns: repeat(11, minmax(0, 1fr)); }
.grid-12col { grid-column: span 12; grid-template-columns: repeat(12, minmax(0, 1fr)); }
.grid-start-2 { grid-column-start: 2; }
.grid-start-3 { grid-column-start: 3; }
.grid-start-4 { grid-column-start: 4; }
.grid-start-5 { grid-column-start: 5; }
.grid-start-6 { grid-column-start: 6; }
.grid-start-7 { grid-column-start: 7; }
.grid-start-8 { grid-column-start: 8; }
.grid-start-9 { grid-column-start: 9; }
.grid-start-10 { grid-column-start: 10; }
.grid-start-11 { grid-column-start: 11; }
.grid-start-12 { grid-column-start: 12; }
.grid-span-2 { grid-column-end: span 2; }
.grid-span-3 { grid-column-end: span 3; }
.grid-span-4 { grid-column-end: span 4; }
.grid-span-5 { grid-column-end: span 5; }
.grid-span-6 { grid-column-end: span 6; }
.grid-span-7 { grid-column-end: span 7; }
.grid-span-8 { grid-column-end: span 8; }
.grid-span-9 { grid-column-end: span 9; }
.grid-span-10 { grid-column-end: span 10; }
.grid-span-11 { grid-column-end: span 11; }
.grid-span-12 { grid-column-end: span 12; }
.flex-group-center,
.flex-group-left,
.flex-group-right {
justify-content: center;
align-items: center;
text-align: center;
}
.flex-group-left {
justify-content: flex-start;
text-align: left;
}
.flex-group-right {
justify-content: flex-end;
text-align: right;
}
.flex-align-left { align-items: flex-start; }
.flex-align-right { align-items: flex-end; }
.gap-xs { gap: 2px; }
.gap-sm { gap: 4px; }
.gap-md { gap: 8px; }
.gap-lg { gap: 16px; }
.flexshrink { flex: 0; }
.flex-between { justify-content: space-between; }
.flexlarge { flex: 2; }
.align-left {
justify-content: flex-start;
text-align: left;
}
.align-right {
justify-content: flex-end;
text-align: right;
}
.align-center {
justify-content: center;
text-align: center;
}
::-webkit-scrollbar-thumb {
outline: none;
border-radius: 3px;
background: #577822;
border: 1px solid var(--color-border-highlight);
}
::-webkit-scrollbar {
width: 3px;
height: 3px;
}
.system-vermine2047 {
.item-form {
font-family: "Roboto", sans-serif;
}
.sheet-header {
flex: 0 auto;
overflow: hidden;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
margin-bottom: 10px;
.profile-img {
flex: 0 0 100px;
height: 100px;
margin-right: 10px;
}
.header-fields {
flex: 1;
}
h1.charname {
height: 50px;
padding: 0px;
margin: 5px 0;
border-bottom: 0;
input {
width: 100%;
height: 100%;
margin: 0;
}
}
}
.sheet-tabs {
flex: 0;
}
.sheet-body .tab,
.editor {
height: 100%;
width: 100%;
}
.editor {
min-height: 75px;
margin-bottom: 1rem;
min-width: 100%;
.editor-content {
min-width: 100%;
min-height: 3rem;
}
}
.editor:hover .editor-edit {
display: block;
}
.tox {
min-height: 25vh;
.tox-editor-container {
background: #fff;
}
.tox-edit-area {
padding: 0 8px;
}
}
.resource-label {
font-weight: bold;
}
.items-header {
height: 28px;
margin: 2px 0;
padding: 0;
align-items: center;
background: rgba(0, 0, 0, 0.05);
border: 2px groove #eeede0;
font-weight: bold;
> * {
font-size: 14px;
text-align: center;
}
.item-name {
font-weight: bold;
padding-left: 5px;
text-align: left;
display: flex;
}
}
.items-list {
list-style: none;
margin: 0;
padding: 0;
overflow-y: auto;
scrollbar-width: thin;
color: #444;
.item-list {
list-style: none;
margin: 0;
padding: 0;
}
.item-name {
flex: 2;
margin: 0;
overflow: hidden;
font-size: 13px;
text-align: left;
align-items: center;
display: flex;
h3, h4 {
margin: 0;
white-space: nowrap;
overflow-x: hidden;
}
}
.item-controls {
display: flex;
flex: 0;
justify-content: flex-end;
a {
font-size: 12px;
text-align: center;
margin: 0 6px;
}
}
.item {
align-items: center;
padding: 0 2px;
border-bottom: 1px solid #c9c7b8;
&:last-child {
border-bottom: none;
}
.item-name {
color: @theme-color-dark;
.item-image {
flex: 0 0 30px;
height: 30px;
background-size: 30px;
border: none;
margin-right: 5px;
}
}
.item-prop {
text-align: center;
border-left: 1px solid #c9c7b8;
border-right: 1px solid #c9c7b8;
font-size: 12px;
}
.items-header {
height: 28px;
margin: 2px 0;
padding: 0;
align-items: center;
background: rgba(0, 0, 0, 0.05);
border: 2px groove #eeede0;
font-weight: bold;
> * {
font-size: 12px;
text-align: center;
}
.item-name {
padding-left: 5px;
text-align: left;
}
}
}
.effects .item {
.effect-source,
.effect-duration,
.effect-controls {
text-align: center;
border-left: 1px solid #c9c7b8;
border-right: 1px solid #c9c7b8;
font-size: 12px;
}
.effect-controls {
border: none;
}
}
}
.item-formula {
flex: 0 0 200px;
padding: 0 8px;
}
}
span.game-mode {
font-family: "DistressBlack", sans-serif;
position: absolute;
margin-left: auto;
color: rgba(0, 0, 0, 0);
top: 1rem;
z-index: 900;
width: 55%;
text-align: center;
text-transform: uppercase;
font-weight: 900;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.767) 0%,
rgba(0, 0, 0, 0.61) 17%,
rgba(0, 0, 0, 0.548) 19%,
rgba(222, 255, 221, 0.575) 24%,
rgba(255, 255, 255, 0.637) 43%,
rgba(0, 0, 0, 0.486) 47%,
rgba(254, 255, 254, 0.466) 50%,
rgba(0, 0, 0, 0.699) 63%,
rgba(134, 160, 137, 0.479) 64%,
rgba(213, 248, 210, 0.493) 100%
);
background-clip: text;
}
span.game-mode#game-mode-1 { color: @game-mode-1-color; }
span.game-mode#game-mode-2 { color: @game-mode-2-color; }
span.game-mode#game-mode-3 { color: @game-mode-3-color; }
.shadow {
.shadow();
}
ol#chat-log,
ol.chat-log {
header.message-header {
background-color: #000;
padding: 0 1rem;
}
.vermine-roll-message {
overflow: hidden;
padding: 4px 10px 10px;
position: relative;
> h3 {
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-14;
margin: 6px 0 8px;
padding: 4px 10px;
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
box-shadow: 0px 0px 3px rgba(31, 26, 26, 0.3) inset;
font-weight: 900;
border-bottom: none;
}
> .flexrow:not(.roll-total):not(.roll-results) {
align-items: center;
gap: 6px;
margin: 4px 0;
h4 {
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-12;
margin: 0;
border-bottom: none;
font-weight: 900;
}
span {
font-family: "Roboto", sans-serif;
font-size: @font-size-12;
}
}
// ── Reroll section ──────────────────────────────────────────────
.reroll-fromroll {
> h4 {
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-12;
margin: 6px 0 4px;
border-bottom: none;
font-weight: 900;
#allowed_reroll {
font-size: @font-size-12;
}
}
}
div.reroll {
.transition(0.3s);
max-height: 1px;
overflow: hidden;
justify-content: flex-end;
align-items: center;
gap: 8px;
button {
text-transform: uppercase;
font-family: "DistressBlack";
font-size: @font-size-12;
padding: 2px 12px;
max-width: fit-content;
box-shadow: 0px 0px 2px #000;
.background-image(url("@{ui-path}/scotch.webp"), no-repeat, cover, 50% 0%);
}
&.visible { max-height: 15rem; }
}
// ── Dice results ────────────────────────────────────────────────
ul.roll-results {
list-style: none;
padding: 6px 0;
justify-content: center;
gap: 6px;
flex-wrap: wrap;
li.die {
position: relative;
width: 3rem;
height: 3rem;
flex: none;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
.transition();
border-bottom: 4px solid @color-hemolymph;
border-radius: 50%;
&:after {
content: "";
position: absolute;
top: -1rem;
text-wrap: nowrap;
color: #fff;
font-weight: 100;
font-size: smaller;
text-align: center;
opacity: 0;
text-shadow: 0px 0px 5px #000;
}
&:hover::after {
opacity: 1;
color: #fff;
}
&.human { border-top: 4px solid @totem-human-color; }
&.adapted { border-top: 4px solid @totem-adapted-color; }
&.rerollable {
cursor: pointer;
&:hover { transform: translateY(0.5rem); }
}
&.success { border-bottom-color: @color-acid-green; }
&.adapted::after { content: "adapté"; }
&.human::after { content: "humain"; }
&.rerolled { transform: translateY(0rem); }
span {
display: none;
}
&:not([data-result]) {
background-image: url("@{dice-path}/d10c-1.webp");
opacity: 0.5;
filter: grayscale(1);
}
}
// Per-face dice background images
.generate-dice-faces(@i: 1) when (@i =< 10) {
li.die[data-result="@{i}"] {
background-image: url("@{dice-path}/d10c-@{i}.webp");
}
.generate-dice-faces(@i + 1);
}
.generate-dice-faces();
}
// ── Total section ───────────────────────────────────────────────
div.roll-total {
margin-top: 8px;
padding: 8px 16px;
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
box-shadow: 0px 0px 3px rgba(31, 26, 26, 0.3) inset;
border-radius: 2px;
gap: 16px;
justify-content: center;
> div {
display: flex;
align-items: center;
gap: 6px;
}
h4 {
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-12;
margin: 0;
border-bottom: none;
font-weight: 900;
text-align: center;
}
span {
font-family: "DistressBlack";
font-size: @font-size-20;
}
#total {
color: @color-acid-green;
text-shadow: 0 0 4px fade(@color-acid-green, 40%);
}
#required {
color: @color-amber;
}
}
}
div.item-card header img {
max-width: 30%;
}
}
// Padding supplémentaire pour les fiches groupe, npc et créature
.system-vermine2047 {
.group-content,
.npc-content,
.creature-content {
padding: 15px;
}
}
+475
View File
@@ -0,0 +1,475 @@
@import "variables";
@import "utilities";
.window-app.vermineDialog {
max-width: 50vw;
height: fit-content;
.window-content {
.background-image(url("@{ui-path}/box_background.webp"), repeat);
color: #000;
}
details > summary::after {
content: "▶️";
position: relative;
right: 40%;
}
details[open] > summary::after {
content: "🔽";
}
.grid {
justify-content: space-around;
.shadow(0px, 1px, 10px, rgba(0, 0, 0, 0.555));
align-items: center;
padding: 0.5rem 0.2rem;
> * { margin: 0 0.3rem; }
}
label {
font-family: "DistressBlack", sans-serif;
font-size: larger;
}
select {
max-width: fit-content;
.custom-select-style();
option { max-width: fit-content; }
}
.dialog-buttons {
display: flex;
justify-content: space-around;
flex-direction: row;
button {
display: block;
flex: 0.3;
color: @theme-color-dark;
}
}
}
input[type="range"] { .custom-input-style(); }
input[type="checkbox"],
input[type="radio"] { .custom-checkbox-radio(); }
input[type="radio"] {
width: 1rem;
height: 1rem;
&:after {
width: 0.8rem;
background-size: 100% 100%;
top: 5%;
left: 5%;
width: 90%;
height: 90%;
background-size: 30% 30%;
background-position: center;
}
&:not([disabled]):hover::after { background-size: 90% 90%; }
&:checked::after {
content: "";
background-size: 70% 70%;
top: 5%;
left: 5%;
position: relative;
background-color: rgba(26, 1, 1, 0);
}
}
.app.vermine2047.trait-selector {
.form-group {
.shadow(0, 0, 30px, gray);
padding: 0.3rem 0.5rem;
border: 3px solid #8e9010;
&:has(input[type="checkbox"]:checked) {
border: 3px solid green;
}
label {
display: inline-flex;
align-items: center;
justify-content: space-around;
width: 100%;
text-align: center;
border-bottom: 2px solid #000;
}
}
}
.app .actor.choose {
div.actor {
position: relative;
img {
border-radius: 50%;
.shadow(0px, 0px, 8px, #000);
}
span.actor-name {
position: absolute;
text-align: center;
background-color: rgba(255, 255, 255, 0.562);
border: 5px;
width: 100%;
padding: 0 1rem;
border-radius: 5px;
.shadow(0px, 0px, 8px, #000);
}
}
}
iframe {
min-height: 500px;
.tabs.moods-headings {
max-width: 1px;
}
}
// ── Roll Dialog V2 ─────────────────────────────────────────────────────
.application.vermine-roll {
.window-content {
overflow-y: auto;
}
.roll-dialog-content {
display: flex;
flex-direction: column;
gap: 6px;
padding: 10px;
height: 100%;
overflow-y: auto;
color: @color-text-light-0;
.dice-pool {
flex: 1;
display: flex;
flex-direction: column;
gap: inherit;
}
label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-12;
}
input, select, textarea {
color: @color-chitin-dark;
background: @color-text-light-0;
}
select {
.custom-select-style();
width: 100%;
}
input[type="range"] { .custom-input-style(); }
input[type="checkbox"],
input[type="radio"] { .custom-checkbox-radio(); }
// ── Main roll section: ability + skill side-by-side ─────────────
.main-roll-section {
gap: 10px;
> .flexcol {
flex: 1;
gap: 4px;
min-width: 0;
> label {
font-size: @font-size-13;
color: @color-amber;
white-space: nowrap;
}
select {
min-height: 28px;
width: 100%;
}
}
.ability-score {
gap: 4px;
font-size: @font-size-12;
color: @color-text-light-0;
opacity: 0.85;
#abilityScoreValue {
font-weight: bold;
color: @color-acid-green;
}
}
}
// ── Collapsible sections (specialties, difficulty, bonuses) ─────
details {
margin-top: 2px;
summary {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-12;
padding: 6px 10px;
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
box-shadow: 0px 0px 3px rgba(31, 26, 26, 0.5) inset;
cursor: pointer;
color: @theme-color-dark;
display: flex;
align-items: center;
gap: 8px;
&::after {
content: "▶";
margin-left: auto;
font-size: @font-size-11;
}
&:hover {
box-shadow: 0 0 10px @theme-color-highlight inset;
}
span.label {
font-family: "DistressBlack", sans-serif;
font-size: @font-size-12;
}
.current-values,
.current-specialty,
.bonus-count {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
text-transform: none;
opacity: 0.8;
}
}
&[open] > summary::after {
content: "▼";
}
> div:not(summary) {
padding: 8px;
background: fade(@color-chitin-dark, 40%);
}
}
// ── Difficulty section ──────────────────────────────────────────
.difficulty-controls {
gap: 12px;
> .flexcol {
flex: 1;
gap: 4px;
label {
font-size: @font-size-11;
color: @color-amber;
}
}
}
// ── Specialties section ─────────────────────────────────────────
.specialty-options {
gap: 8px;
flex-wrap: wrap;
label {
display: flex;
align-items: center;
gap: 4px;
font-size: @font-size-12;
font-family: "Roboto", sans-serif;
text-transform: none;
color: @color-text-light-0;
cursor: pointer;
&:hover {
color: @color-amber;
}
}
input[type="radio"] {
width: 14px;
height: 14px;
flex: none;
}
}
// ── Bonuses grid ────────────────────────────────────────────────
.bonus-grid {
margin: 0;
gap: 8px;
.bonus-item {
align-items: center;
gap: 6px;
padding: 4px 0;
> label.label {
font-size: @font-size-11;
color: @color-text-light-0;
white-space: nowrap;
}
input[type="number"] {
width: 40px;
text-align: center;
padding: 2px;
}
input[type="checkbox"] + label {
font-size: @font-size-12;
font-family: "Roboto", sans-serif;
text-transform: none;
color: @color-text-light-0;
}
.item-list {
gap: 4px;
label {
align-items: center;
gap: 4px;
font-size: @font-size-11;
font-family: "Roboto", sans-serif;
text-transform: none;
color: @color-text-light-0;
cursor: pointer;
&:hover {
color: @color-amber;
}
}
}
&.full-width {
grid-column: 1 / -1;
}
}
// Totems sub-section
.totems-section {
flex-wrap: wrap;
gap: 6px;
> label.label {
width: 100%;
}
.totem-options {
gap: 12px;
}
.totem-option {
display: flex;
align-items: center;
gap: 4px;
color: @color-text-light-0;
font-size: @font-size-12;
font-family: "Roboto", sans-serif;
text-transform: none;
cursor: pointer;
&:hover {
color: @color-amber;
}
small {
opacity: 0.7;
}
}
.totem-hint {
width: 100%;
font-size: @font-size-11;
color: @color-text-light-0;
opacity: 0.6;
font-style: italic;
}
}
}
// ── Total dice pool section ─────────────────────────────────────
.total-section {
align-items: center;
padding: 10px;
border-top: 1px solid @color-border-dark-4;
margin-top: 4px;
gap: 10px;
background: fade(@color-chitin-dark, 50%);
.label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-16;
color: @color-text-light-0;
}
.total-value {
font-family: "DistressBlack", sans-serif;
font-size: @font-size-28;
color: @color-acid-green;
text-shadow: 0 0 8px fade(@color-acid-green, 40%);
}
.totem-selector {
font-size: @font-size-11;
color: @color-text-light-0;
opacity: 0.7;
margin-left: auto;
select {
font-size: @font-size-11;
padding: 1px 4px;
width: auto;
display: inline;
}
}
}
}
// ── Footer buttons ───────────────────────────────────────────────
.sheet-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 8px 10px;
margin-top: auto;
border-top: 1px solid @color-border-dark-4;
background: fade(@color-chitin-dark, 60%);
button {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-13;
padding: 6px 20px;
border: none;
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
box-shadow: 0px 0px 3px rgba(31, 26, 26, 0.979) inset;
cursor: pointer;
color: @theme-color-dark;
.transition();
display: flex;
align-items: center;
gap: 6px;
&:hover {
box-shadow: 0 0 10px @theme-color-highlight inset;
}
i {
font-size: @font-size-14;
}
}
}
}
+84
View File
@@ -0,0 +1,84 @@
@import "variables";
@import "utilities";
.system-vermine2047 .item-formula {
flex: 0 0 200px;
padding: 0 8px;
}
.system-vermine2047 .vermine2047.item .window-content {
padding: 0 1rem;
.flexrow { align-items: center; }
header, h1, h2, h3, h4, h5 {
.background-image(url("@{ui-path}/scotch.webp"), no-repeat, cover, 50% 50%);
text-transform: uppercase;
font-family: "DistressBlack";
margin-top: 1rem;
border-bottom: none;
}
h2, h3, h4 { text-align: center; }
h5 { margin-bottom: 0; }
.resource-label {
font-size: 0.75rem;
color: @color-text-light-2;
text-shadow: 0 0 3px rgba(0, 0, 0, 0.9);
}
.resource {
border: none;
border-left: 1px solid gray;
padding: 0.2rem 1rem;
text-align: center;
.flexrow {
min-width: 5rem;
box-shadow: none;
}
}
.damages-row {
margin: 0;
.radios {
margin: 0;
padding: 0.5rem;
}
}
.damage-pannes,
.damage-state,
.damage-effect {
text-align: center;
font-family: "DistressBlack";
}
select {
background: rgba(0, 0, 0, 0.4);
color: @color-text-light-1;
font-family: "DistressBlack", sans-serif;
font-size: 0.875rem;
text-align: center;
border: none;
box-shadow: 0px 0px 3px rgba(31, 26, 26, 0.979) inset;
text-shadow: 0 0 3px rgba(0, 0, 0, 0.9);
}
option {
background: #1a1a1a;
color: @color-text-light-1;
}
.traits {
.shadow();
h3, h4 { margin: 0; }
}
}
ol#chat-log div.item-card header img,
ol.chat-log div.item-card header img {
max-width: 30%;
}
+23
View File
@@ -0,0 +1,23 @@
.sans-font {
font-family: "DistressBlack", sans-serif;
}
.chat-message .message-header {
line-height: 20px;
color: white;
text-shadow: 0px 0px 5px black;
background: #1918135e;
}
.flex-center {
align-items: center;
justify-content: center;
}
.flex-around {
justify-content: space-around;
}
.flexsmall {
flex: 0.5;
}
+257
View File
@@ -0,0 +1,257 @@
// ============================================
// Utilitaires LESS pour Vermine2047
// Mixins, fonctions et classes utilitaires
// ============================================
// Mixin pour les ombres standard
.shadow() {
box-shadow: 0 2rem @theme-color-shadow;
margin: 2rem 0;
}
.shadow(@color) {
box-shadow: 0 2rem @color;
margin: 2rem 0;
}
.shadow(@color, @blur) {
box-shadow: 0 @blur @color;
margin: 2rem 0;
}
.shadow(@h-offset, @v-offset, @blur, @color) {
box-shadow: @h-offset @v-offset @blur @color;
margin: 2rem 0;
}
// Mixin pour le style des hexagones
.hexa-style(@bg-color: rgba(255, 255, 255, 0.425), @bg-color-end: rgba(0, 0, 0, 0.288)) {
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
background: radial-gradient(circle, @bg-color 0%, @bg-color-end 100%);
max-height: 1.5rem;
max-width: 1.5rem;
min-width: 1.5rem;
aspect-ratio: 1/1;
color: #000;
vertical-align: center;
text-align: center;
}
// Mixin pour les boutons rollable
.rollable-style() {
&:hover,
&:focus {
color: #000;
text-shadow: 0 0 10px red;
cursor: pointer;
}
}
// Mixin pour les inputs de type hexa
.input-hexa-style() {
.hexa-style();
input {
width: 1rem;
}
input[type="radio"] {
opacity: 0;
}
input[type="radio"]::after,
input[type="radio"]::before {
display: none;
}
}
// Mixin pour les classes flex
.flex-container(@direction: row, @wrap: nowrap, @justify: flex-start, @align: stretch) {
display: flex;
flex-direction: @direction;
flex-wrap: @wrap;
justify-content: @justify;
align-items: @align;
}
// Mixin pour le style des cartes
.card-style(@bg-color: rgba(0, 0, 0, 0.1), @border-color: #444) {
border: 1px solid @border-color;
border-radius: 4px;
padding: 10px;
margin-bottom: 15px;
background: @bg-color;
}
// Mixin pour les titres de carte
.card-title(@border-color: #666) {
margin-top: 0;
border-bottom: 1px solid @border-color;
padding-bottom: 5px;
text-align: center;
}
// Mixin pour les éléments de liste avec bordure
.list-item-with-border(@border-color: #333) {
padding: 5px;
border-bottom: 1px solid @border-color;
&:last-child {
border-bottom: none;
}
}
// Mixin pour les groupes de formulaires
.form-group-style(@margin-bottom: 10px) {
margin-bottom: @margin-bottom;
label {
display: block;
margin-bottom: 3px;
font-weight: bold;
font-size: 12px;
}
input,
select {
width: 100%;
font-size: 12px;
}
}
// Mixin pour les ressources
.resource-style(@bg-color: rgba(0, 0, 0, 0.05)) {
padding: 5px;
background: @bg-color;
border-radius: 4px;
margin: 0 5px;
label {
font-weight: bold;
margin-right: 8px;
min-width: 60px;
font-size: 12px;
}
.resource-content {
display: flex;
align-items: center;
}
}
// Mixin pour les badges de rareté
.rarity-badge(@bg-color, @text-color) {
display: inline-block;
padding: 1px 4px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
margin-left: 5px;
background: @bg-color;
color: @text-color;
}
// Mixin pour le fond avec image
.background-image(@url, @repeat: no-repeat, @size: auto, @position: center) {
background: @url @repeat @position / @size;
}
// Mixin pour les transitions
.transition(@property: all, @duration: 0.2s, @timing: ease-out) {
transition: @property @duration @timing;
}
// Mixin pour les styles de l'éditeur TinyMCE
.tiny-mce-style() {
.tox {
min-height: 25vh;
.tox-editor-container {
background: #fff;
}
.tox-edit-area {
padding: 0 8px;
}
}
}
// Mixin pour les styles des inputs personnalisés
.custom-input-style() {
appearance: none;
background: rgba(0, 0, 0, 0);
cursor: pointer;
width: 100%;
&::-webkit-slider-runnable-track {
.background-image(url("@{ui-path}/scotch.webp"), no-repeat, auto, center);
background-size: 100% auto;
height: 0.4rem;
border: none;
box-shadow: 0px 0px 13px rgba(31, 26, 26, 0.979) inset;
}
&::-webkit-slider-thumb {
appearance: none;
margin-top: -0.3rem;
height: 1rem;
width: 1rem;
border: none;
border-radius: 50%;
.background-image(url("@{totems-path}/human.webp"), no-repeat, cover);
filter: contrast(2);
box-shadow: 0px 0px 10px #000;
&:focus {
box-shadow: 0px 0px 10px #ff0;
}
}
}
// Mixin pour les checkbox et radio boutons personnalisés
.custom-checkbox-radio() {
-webkit-appearance: none;
appearance: none;
background: rgba(0, 0, 0, 0);
box-shadow: 0px 0px 3px #85854e;
cursor: pointer;
width: 1.5rem;
height: 1rem;
border-radius: 0.4rem;
.transition();
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
box-shadow: 0px 0px 6px #000 inset;
background-color: rgba(61, 11, 11, 0.658);
&[disabled=true] {
filter: grayscale(1);
}
&:after {
content: " ";
.background-image(url("@{totems-path}/human.webp"), no-repeat, auto, 50% 150%);
position: relative;
top: 10%;
left: 0%;
width: 100%;
height: 80%;
display: block;
border-radius: 0%;
padding: 0;
.transition();
}
&:checked {
background-color: rgba(26, 107, 12, 0.658);
&:after {
font-weight: 900;
background-color: rgba(26, 1, 1, 0);
left: 50%;
}
}
}
// Mixin pour les selects personnalisés
.custom-select-style() {
border: none;
.background-image(url("@{ui-path}/scotch.webp"), no-repeat, auto, 100% 100%);
box-shadow: 0px 0px 3px rgba(31, 26, 26, 0.979) inset;
&[disabled] {
color: #000;
text-shadow: 0px 0px 15px #000;
}
option {
appearance: none;
border: none;
.background-image(url("@{ui-path}/scotch.webp"), no-repeat, auto, 100% 100%);
}
}
+175
View File
@@ -0,0 +1,175 @@
// ============================================
// Variables LESS pour Vermine2047
// Converti depuis les variables CSS du fichier vermine2047.css
// ============================================
// Couleurs de texte - clair
@color-text-light-highlight: #96d696;
@color-text-light-heading: #9fd8a8;
@color-text-light-primary: #a4b5b3;
// Couleurs de texte - foncé
@color-text-dark-primary: #131919;
@color-text-dark-secondary: #444b4a;
@color-text-dark-header: #1d2223;
@color-text-dark-inactive: #71797a;
// Couleurs de texte - autres
@color-text-hyperlink: #5aaf0a;
// Niveaux de gris pour le texte clair
@color-text-light-0: #fff;
@color-text-light-1: #e0f0f0;
@color-text-light-2: #c9e0c0;
@color-text-light-3: #90c4a4;
@color-text-light-4: #80c08b;
@color-text-light-5: #60b06b;
@color-text-light-6: #40a05d;
@color-text-light-7: #208028;
// Niveaux de gris pour le texte foncé
@color-text-dark-1: #111;
@color-text-dark-2: #222;
@color-text-dark-3: #444;
@color-text-dark-4: #555;
@color-text-dark-5: #666;
@color-text-dark-6: #777;
// Couleurs de bordure - clair
@color-border-light-1: #b0d9b0;
@color-border-light-2: #80c0c0;
// Couleurs de bordure - foncé
@color-border-dark-1: #131919;
@color-border-dark-2: #1d2223;
@color-border-dark-3: #2d3333;
@color-border-dark-4: #3d4444;
@color-border-dark-5: #668888;
// Couleurs d'ombre
@color-shadow-primary: #7bb60d;
@color-shadow-highlight: #85cc01d0;
@color-shadow-dark: #000;
// Couleurs de soulignement
@color-underline-inactive: #71797a;
@color-underline-active: #1a1944;
@color-underline-header: #228247;
// Couleurs de bordure - autres
@color-border-light-highlight: #b0d9b0;
@color-border-light-primary: #a4b5b3;
@color-border-light-secondary: #9fc7d8;
@color-border-light-tertiary: #71797a;
@color-border-dark: #000;
@color-border-dark-primary: #131919;
@color-border-dark-secondary: #1d2223;
@color-border-dark-tertiary: #444b4a;
@color-border-highlight: #85c019;
@color-border-highlight-alt: #70c008;
// Couleurs de fond
@color-bg-btn-minor-inactive: #9fc7d8;
@color-bg-btn-minor-active: #a4b5b3;
@color-bg-option: #ccdada;
// Autres couleurs
@color-checkbox-checked: #666;
@color-ownership-none: #00ff55;
@color-ownership-observer: #71797a;
@color-ownership-owner: #a4b5b3;
// Niveaux de log
@color-level-info: #b95c87;
@color-level-warning: #04b184;
@color-level-error: #03750;
@color-level-success: #3c266c;
// Z-index
@z-index-canvas: 0;
@z-index-app: 30;
@z-index-ui: 60;
@z-index-window: 100;
@z-index-tooltip: 9999;
// Dimensions
@sidebar-width: 300px;
@sidebar-header-height: 32px;
@sidebar-item-height: 48px;
@hotbar-height: 52px;
@hotbar-width: 578px;
@macro-size: 50px;
@players-width: 200px;
@form-field-height: 26px;
// Polices
@font-mono: monospace;
// Tailles de police
@font-size-11: 0.6875rem;
@font-size-12: 0.75rem;
@font-size-13: 0.8125rem;
@font-size-14: 0.875rem;
@font-size-16: 1rem;
@font-size-18: 1.125rem;
@font-size-20: 1.25rem;
@font-size-24: 1.5rem;
@font-size-28: 1.75rem;
@font-size-32: 2rem;
@font-size-48: 3rem;
// Hauteurs de ligne
@line-height-12: 0.75rem;
@line-height-16: 1rem;
@line-height-20: 1.25rem;
@line-height-30: 1.875rem;
// ============================================
// Variables spécifiques Vermine2047
// ============================================
// Couleurs thématiques
@theme-color-primary: #7e7544;
@theme-color-secondary: #dfdfdf;
@theme-color-accent: #1fa832;
@theme-color-dark: #191813;
@theme-color-light: #4e564c;
@theme-color-shadow: rgba(0, 0, 0, 0.7098039216);
@theme-color-highlight: #005a3c;
// Couleurs insecte / organique
@color-acid-green: #6ab04c;
@color-amber: #e8b84b;
@color-chitin-dark: #2a2520;
@color-hemolymph: #8b4513;
@color-membrane: rgba(106, 176, 76, 0.1);
@color-honeycomb: #c9a84c;
// Couleurs pour les dés et totems
@dice-pool-color: rgb(94, 90, 77);
@dice-reroll-color: rgb(187, 182, 165);
@totem-human-color: #064930;
@totem-adapted-color: #553402;
// Couleurs pour les niveaux de rareté
@rarity-0-bg: #444;
@rarity-0-text: #aaa;
@rarity-1-bg: #5a7a5a;
@rarity-1-text: #fff;
@rarity-2-bg: #7a9a7a;
@rarity-2-text: #000;
@rarity-3-bg: #9a5a9a;
@rarity-3-text: #fff;
// Couleurs pour les modes de jeu
@game-mode-1-color: rgba(235, 218, 143, 0.8);
@game-mode-2-color: #83f883;
@game-mode-3-color: rgba(245, 124, 124, 0.8);
// Chemins des assets
@assets-path: "/systems/vermine2047/assets";
@images-path: "@{assets-path}/images";
@fonts-path: "@{assets-path}/fonts";
@ui-path: "@{images-path}/ui";
@totems-path: "@{images-path}/ui/totems";
@dice-path: "@{assets-path}/dice";
+37
View File
@@ -0,0 +1,37 @@
// ============================================
// Fichier principal LESS pour Vermine2047
// Ce fichier importe tous les modules LESS
// ============================================
// 1. Import de police Google (doit être en premier car c'est du CSS pur)
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap");
// 2. Variables et utilitaires (doivent être importés avant tout le reste)
@import "variables";
@import "utilities";
// 3. Styles de base
@import "base";
// 4. Legacy SCSS converti en LESS
@import "legacy";
// 5. Styles des acteurs
@import "actor/actor";
@import "actor/totem";
@import "actor/npc";
@import "actor/group";
@import "actor/creature";
// 6. Styles des items
@import "items";
// 7. Styles des dialogs
@import "dialogs";
// ============================================
// Notes:
// - L'ordre des imports est important
// - Les variables doivent être définies avant d'être utilisées
// - Les mixins doivent être définis avant d'être appelés
// ============================================
+20
View File
@@ -0,0 +1,20 @@
export { VermineBaseActorSheet } from "./base-actor-sheet.mjs"
export { VermineBaseItemSheet } from "./base-item-sheet.mjs"
export { VermineCharacterSheetV2 } from "./character-sheet.mjs"
export { VermineNpcSheetV2 } from "./npc-sheet.mjs"
export { VermineGroupSheetV2 } from "./group-sheet.mjs"
export { VermineCreatureSheetV2 } from "./creature-sheet.mjs"
export {
VermineItemSheetV2,
VermineWeaponSheetV2,
VermineDefenseSheetV2,
VermineVehicleSheetV2,
VermineAbilitySheetV2,
VermineSpecialtySheetV2,
VermineBackgroundSheetV2,
VermineTraumaSheetV2,
VermineEvolutionSheetV2,
VermineRumorSheetV2,
VermineTargetSheetV2,
VermineRiteSheetV2
} from "./item-sheets.mjs"
@@ -0,0 +1,207 @@
import { onManageActiveEffect } from "../../system/effects.mjs"
const { HandlebarsApplicationMixin } = foundry.applications.api
/**
* Fiche de base pour tous les acteurs Vermine 2047 (ApplicationV2).
* Remplace VermineActorSheet (AppV1).
*/
export class VermineBaseActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) {
// ── Mode édition / jeu ──────────────────────────────────────────────
static SHEET_MODES = { EDIT: 0, PLAY: 1 }
_sheetMode = this.constructor.SHEET_MODES.PLAY
get isPlayMode() { return this._sheetMode === this.constructor.SHEET_MODES.PLAY }
get isEditMode() { return this._sheetMode === this.constructor.SHEET_MODES.EDIT }
// ── Options par défaut ──────────────────────────────────────────────
static DEFAULT_OPTIONS = {
classes: ["vermine2047", "actor"],
position: { width: 800, height: "auto" },
form: { submitOnChange: true },
window: { resizable: true },
dragDrop: [{ dragSelector: ".item", dropSelector: null }],
actions: {
editImage: VermineBaseActorSheet.#onEditImage,
toggleSheet: VermineBaseActorSheet.#onToggleSheet,
edit: VermineBaseActorSheet.#onItemEdit,
delete: VermineBaseActorSheet.#onItemDelete,
create: VermineBaseActorSheet.#onItemCreate,
roll: VermineBaseActorSheet.#onRollItem,
clickRadio: VermineBaseActorSheet.#onClickRadioHexa,
effectControl: VermineBaseActorSheet.#onEffectControl,
chooseTotem: VermineBaseActorSheet.#onChooseTotem
}
}
// ── Drag & Drop ─────────────────────────────────────────────────────
#dragDrop
constructor(options = {}) {
super(options)
this.#dragDrop = this.#createDragDropHandlers()
}
#createDragDropHandlers() {
return this.options.dragDrop.map(d => {
d.permissions = {
dragstart: this._canDragStart.bind(this),
drop: this._canDragDrop.bind(this)
}
d.callbacks = {
dragover: this._onDragOver.bind(this),
drop: this._onDrop.bind(this)
}
return new foundry.applications.ux.DragDrop.implementation(d)
})
}
_canDragStart() { return this.isEditable }
_canDragDrop() { return this.isEditable }
// ── Contexte commun ─────────────────────────────────────────────────
async _prepareContext() {
return {
fields: this.document.schema.fields,
systemFields: this.document.system.schema.fields,
actor: this.document,
system: this.document.system,
source: this.document.toObject(),
config: CONFIG.VERMINE,
rollData: this.document.getRollData(),
isGM: game.user.isGM,
isEditMode: this.isEditMode,
isPlayMode: this.isPlayMode,
isEditable: this.isEditable
}
}
// ── Rendu ───────────────────────────────────────────────────────────
async _onRoll(event) {
event.preventDefault()
const el = event.currentTarget
const type = el.dataset.type
const label = el.dataset.label
if (!type || !label) return
const { default: RollDialog } = await import("../../system/dialogs/rollDialog.mjs")
const dialog = await RollDialog.create({
actorId: this.document.id,
rolltype: type,
label
})
if (dialog) dialog.render(true)
}
// ── Actions ─────────────────────────────────────────────────────────
_onRender(context, options) {
super._onRender(context, options)
this.#dragDrop.forEach(d => d.bind(this.element))
this.element.querySelectorAll(".rollable").forEach(el => {
el.addEventListener("click", this._onRoll.bind(this))
})
// Auto-fill empty number inputs on change to prevent validation errors
this.element.addEventListener("change", e => {
const input = e.target
if (input?.type === "number" && !input.value && input.name && input !== document.activeElement) {
input.value = "0"
}
}, { capture: true })
}
/** @override */
async _onDropItem(item) {
const doc = item instanceof foundry.abstract.Document ? item : await fromUuid(item.uuid)
if (!doc) return
const itemData = doc.toObject()
await this.document.createEmbeddedDocuments("Item", [itemData], { renderSheet: false })
}
static #onToggleSheet() {
const modes = this.constructor.SHEET_MODES
this._sheetMode = this.isEditMode ? modes.PLAY : modes.EDIT
this.render()
}
static async #onEditImage(event, target) {
const attr = target.dataset.edit ?? "img"
const current = foundry.utils.getProperty(this.document, attr)
const fp = new FilePicker({
current,
type: "image",
callback: (path) => this.document.update({ [attr]: path }),
top: this.position.top + 40,
left: this.position.left + 10
})
return fp.browse()
}
static async #onItemEdit(event, target) {
const id = target.closest("[data-item-id]")?.dataset?.itemId
const uuid = target.closest("[data-item-uuid]")?.dataset?.itemUuid
let item
if (uuid) item = await fromUuid(uuid)
if (!item) item = this.document.items.get(id)
item?.sheet.render(true)
}
static async #onItemDelete(event, target) {
const itemUuid = target.closest("[data-item-uuid]")?.dataset?.itemUuid
if (itemUuid) {
const item = await fromUuid(itemUuid)
await item?.deleteDialog()
return
}
const id = target.closest("[data-item-id]")?.dataset?.itemId
const item = this.document.items.get(id)
await item?.deleteDialog()
}
static async #onItemCreate(event, target) {
const type = target.dataset.type
if (!type) return
const name = game.i18n.localize("ITEMS.new_" + type)
await this.document.createEmbeddedDocuments("Item", [{ name, type }])
}
static async #onRollItem(event, target) {
const id = target.closest("[data-item-id]")?.dataset?.itemId
if (!id) return
const item = this.document.items.get(id)
item?.roll()
}
static #onClickRadioHexa(event, target) {
event.preventDefault()
event.stopPropagation()
const input = target
const update = {}
let current = this.document
const propTree = input.name.split(".")
for (const prop of propTree) {
current = current[prop]
}
if (current != input.value) {
update[input.name] = parseInt(input.value)
} else {
update[input.name] = parseInt(input.value) - 1
}
this.document.update(update)
}
static #onEffectControl(event, target) {
onManageActiveEffect(event, this.document)
}
static async #onChooseTotem(event, target) {
const { TotemPicker } = await import("../../system/applications.mjs")
new TotemPicker(target, this.document).render(true)
}
}
@@ -0,0 +1,114 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
/**
* Fiche de base pour tous les items Vermine 2047 (ApplicationV2).
*/
export class VermineBaseItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) {
// ── Mode édition ────────────────────────────────────────────────────
static SHEET_MODES = { EDIT: 0, PLAY: 1 }
_sheetMode = this.constructor.SHEET_MODES.PLAY
get isPlayMode() { return this._sheetMode === this.constructor.SHEET_MODES.PLAY }
get isEditMode() { return this._sheetMode === this.constructor.SHEET_MODES.EDIT }
// ── Options par défaut ──────────────────────────────────────────────
static DEFAULT_OPTIONS = {
classes: ["vermine2047", "item"],
position: { width: 560, height: "auto" },
form: { submitOnChange: true },
window: { resizable: true },
actions: {
editImage: VermineBaseItemSheet.#onEditImage,
toggleSheet: VermineBaseItemSheet.#onToggleSheet,
clickDamage: VermineBaseItemSheet.#onClickDamage,
openTraits: VermineBaseItemSheet.#onOpenTraits
}
}
// ── Drag & Drop ─────────────────────────────────────────────────────
#dragDrop
constructor(options = {}) {
super(options)
this.#dragDrop = this.#createDragDropHandlers()
}
#createDragDropHandlers() {
if (!this.options.dragDrop) return []
return this.options.dragDrop.map(d => {
d.permissions = {
dragstart: this._canDragStart.bind(this),
drop: this._canDragDrop.bind(this)
}
d.callbacks = {
dragover: this._onDragOver.bind(this),
drop: this._onDrop.bind(this)
}
return new foundry.applications.ux.DragDrop.implementation(d)
})
}
_canDragStart() { return this.isEditable }
_canDragDrop() { return this.isEditable }
// ── Contexte commun ─────────────────────────────────────────────────
async _prepareContext() {
return {
fields: this.document.schema.fields,
systemFields: this.document.system.schema.fields,
item: this.document,
system: this.document.system,
source: this.document.toObject(),
config: CONFIG.VERMINE,
isEditMode: this.isEditMode,
isPlayMode: this.isPlayMode,
isEditable: this.isEditable
}
}
// ── Rendu ───────────────────────────────────────────────────────────
_onRender(context, options) {
this.#dragDrop.forEach(d => d.bind(this.element))
}
// ── Actions ─────────────────────────────────────────────────────────
static #onToggleSheet() {
const modes = this.constructor.SHEET_MODES
this._sheetMode = this.isEditMode ? modes.PLAY : modes.EDIT
this.render()
}
static async #onEditImage(event, target) {
const attr = target.dataset.edit ?? "img"
const current = foundry.utils.getProperty(this.document, attr)
const fp = new FilePicker({
current,
type: "image",
callback: (path) => this.document.update({ [attr]: path }),
top: this.position.top + 40,
left: this.position.left + 10
})
return fp.browse()
}
static #onClickDamage(event, target) {
// Les radios de dégâts sont 1-based dans le template (value="{{@index}}" avec index 1..max)
// mais le stockage est 0-based. On soustrait 1 avant de sauvegarder.
const prop = target.name
const value = parseInt(target.value) - 1
this.document.update({ [prop]: value })
}
static async #onOpenTraits(event, target) {
const { TraitSelector } = await import("../../system/applications.mjs")
new TraitSelector(this.document).render(true)
}
}
@@ -0,0 +1,98 @@
import { VermineBaseActorSheet } from "./base-actor-sheet.mjs"
export class VermineCharacterSheetV2 extends VermineBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ["character"],
position: { width: 860, height: 720 },
window: { contentClasses: ["character-content"] },
actions: {
addSpecialty: VermineCharacterSheetV2.#onAddSpecialty
}
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/character-main.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
abilities: { template: "systems/vermine2047/templates/actor/appv2/character-abilities.hbs" },
totem: { template: "systems/vermine2047/templates/actor/appv2/character-totem.hbs" },
equipment: { template: "systems/vermine2047/templates/actor/appv2/character-equipment.hbs" },
stories: { template: "systems/vermine2047/templates/actor/appv2/character-stories.hbs" },
combat: { template: "systems/vermine2047/templates/actor/appv2/character-combat.hbs" }
}
tabGroups = { sheet: "abilities" }
#getTabs() {
const tabs = {
abilities: { id: "abilities", group: "sheet", icon: "fas fa-address-card", label: "VERMINE.tabs.abilities" },
totem: { id: "totem", group: "sheet", icon: "fas fa-star", label: "VERMINE.tabs.totem" },
equipment: { id: "equipment", group: "sheet", icon: "fas fa-hammer", label: "VERMINE.tabs.equipment" },
stories: { id: "stories", group: "sheet", icon: "fas fa-book-open-reader", label: "VERMINE.tabs.stories" },
combat: { id: "combat", group: "sheet", icon: "fas fa-medal", label: "VERMINE.tabs.combat" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
async _preparePartContext(partId, context) {
const doc = this.document
context.systemFields = doc.system.schema.fields
switch (partId) {
case "main": break
case "abilities":
context.tab = context.tabs.abilities
break
case "totem":
context.tab = context.tabs.totem
context.abilities = doc.itemTypes.ability.filter(i => i.system.type !== "totem")
context.totem_abilities = doc.itemTypes.ability.filter(i => i.system.type === "totem")
context.specialties = doc.itemTypes.specialty
context.backgrounds = doc.itemTypes.background
context.traumas = doc.itemTypes.trauma
context.evolutions = doc.itemTypes.evolution
break
case "equipment":
context.tab = context.tabs.equipment
context.gear = doc.itemTypes.item
context.weapons = doc.itemTypes.weapon
context.defenses = doc.itemTypes.defense
context.vehicles = doc.itemTypes.vehicle
break
case "stories":
context.tab = context.tabs.stories
break
case "combat":
context.tab = context.tabs.combat
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
context.effects = prepareActiveEffectCategories(doc.effects)
break
}
return context
}
changeTab(tab, group, options = {}) {
super.changeTab(tab, group, options)
if (group === "sheet") {
const main = this.element?.querySelector('[data-group="sheet"][data-tab="main"]')
if (main) main.classList.add("active")
}
}
static async #onAddSpecialty(event, target) {
const skillKey = target.dataset.skill
const name = game.i18n.localize("ITEMS.new_specialty")
const itemData = { name, type: "specialty" }
if (skillKey) itemData.system = { skill: skillKey }
await this.document.createEmbeddedDocuments("Item", [itemData])
}
}
@@ -0,0 +1,67 @@
import { VermineBaseActorSheet } from "./base-actor-sheet.mjs"
export class VermineCreatureSheetV2 extends VermineBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ["creature"],
position: { width: 700, height: 650 },
window: { contentClasses: ["creature-content"] }
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/creature-main.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
info: { template: "systems/vermine2047/templates/actor/appv2/creature-info.hbs" },
stats: { template: "systems/vermine2047/templates/actor/appv2/creature-stats.hbs" },
combat: { template: "systems/vermine2047/templates/actor/appv2/creature-combat.hbs" },
effects: { template: "systems/vermine2047/templates/actor/appv2/creature-effects.hbs" }
}
tabGroups = { sheet: "info" }
#getTabs() {
const tabs = {
info: { id: "info", group: "sheet", icon: "fas fa-info-circle", label: "VERMINE.information" },
stats: { id: "stats", group: "sheet", icon: "fas fa-chart-bar", label: "VERMINE.stats" },
combat: { id: "combat", group: "sheet", icon: "fas fa-sword", label: "VERMINE.combat" },
effects: { id: "effects", group: "sheet", icon: "fas fa-magic", label: "UI.effects.name" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main": break
case "info":
context.tab = context.tabs.info
break
case "stats":
context.tab = context.tabs.stats
context.patternLabel = doc.system.pattern?.value ? CONFIG.VERMINE.creaturePatternLevels[doc.system.pattern.value]?.label : ""
context.sizeLabel = doc.system.size?.value || ""
context.roleLabel = doc.system.role?.value ? CONFIG.VERMINE.creatureRoleLevels[doc.system.role.value]?.label : ""
context.packLabel = doc.system.pack?.value || game.i18n.localize("VERMINE.none")
break
case "combat":
context.tab = context.tabs.combat
break
case "effects":
context.tab = context.tabs.effects
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
context.effects = prepareActiveEffectCategories(doc.effects)
break
}
return context
}
}
+139
View File
@@ -0,0 +1,139 @@
import { VermineBaseActorSheet } from "./base-actor-sheet.mjs"
export class VermineGroupSheetV2 extends VermineBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ["group"],
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
}
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/group-main.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
info: { template: "systems/vermine2047/templates/actor/appv2/group-info.hbs" },
gear: { template: "systems/vermine2047/templates/actor/appv2/group-gear.hbs" },
road: { template: "systems/vermine2047/templates/actor/appv2/group-road.hbs" },
reserve: { template: "systems/vermine2047/templates/actor/appv2/group-reserve.hbs" }
}
tabGroups = { sheet: "info" }
#getTabs() {
const tabs = {
info: { id: "info", group: "sheet", icon: "fas fa-star", label: "VERMINE.information" },
gear: { id: "gear", group: "sheet", icon: "fas fa-gear", label: "VERMINE.gear" },
road: { id: "road", group: "sheet", icon: "fas fa-route", label: "VERMINE.road" },
reserve: { id: "reserve", group: "sheet", icon: "fas fa-users", label: "VERMINE.reserve" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
// Résoudre les IDs des membres/encounters en données acteur
context.resolvedMembers = {}
if (this.document.system.members?.length > 0) {
for (const memberId of this.document.system.members) {
const a = game.actors.get(memberId)
if (a) context.resolvedMembers[memberId] = { name: a.name, id: a.id }
}
}
context.resolvedEncounters = {}
if (this.document.system.encounters?.length > 0) {
for (const encId of this.document.system.encounters) {
const a = game.actors.get(encId)
if (a) context.resolvedEncounters[encId] = { name: a.name, id: a.id }
}
}
return context
}
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main": break
case "info":
context.tab = context.tabs.info
context.abilities = doc.itemTypes.ability
context.specialties = doc.itemTypes.specialty
context.backgrounds = doc.itemTypes.background
context.traumas = doc.itemTypes.trauma
context.evolutions = doc.itemTypes.evolution
break
case "gear":
context.tab = context.tabs.gear
context.gear = doc.itemTypes.item
context.weapons = doc.itemTypes.weapon
context.defenses = doc.itemTypes.defense
break
case "road":
context.tab = context.tabs.road
context.vehicles = doc.itemTypes.vehicle
break
case "reserve":
context.tab = context.tabs.reserve
break
}
return context
}
// Actions : délégation aux applications AppV1 existantes pour TotemPicker/ActorPicker
static async #onChooseTotem(event, target) {
const { TotemPicker } = await import("../../system/applications.mjs")
new TotemPicker(target, this.document).render(true)
}
static async #onChooseActor(event, target) {
const { ActorPicker } = await import("../../system/applications.mjs")
new ActorPicker(target, this.document).render(true)
}
static #onDeleteMember(event, target) {
const li = target.closest("li.actor")
if (!li) return
const actorId = li.dataset.actorId
const idx = this.document.system.members.indexOf(actorId)
if (idx !== -1) {
const members = [...this.document.system.members]
members.splice(idx, 1)
this.document.update({ "system.members": members })
}
}
static #onDeleteEncounter(event, target) {
const li = target.closest("li.actor")
if (!li) return
const actorId = li.dataset.actorId
const idx = this.document.system.encounters.indexOf(actorId)
if (idx !== -1) {
const encounters = [...this.document.system.encounters]
encounters.splice(idx, 1)
this.document.update({ "system.encounters": encounters })
}
}
static #onDeleteObjective(event, target) {
const type = target.dataset.type
const index = parseInt(target.dataset.index)
if (isNaN(index)) return
const objectives = foundry.utils.duplicate(this.document.system.objectives || { major: [], minor: [] })
objectives[type].splice(index, 1)
this.document.update({ "system.objectives": objectives })
}
static #onAddObjective(event, target) {
const type = target.dataset.type === "major_objective" ? "major" : "minor"
const objectives = foundry.utils.duplicate(this.document.system.objectives || { major: [], minor: [] })
objectives[type].push("")
this.document.update({ "system.objectives": objectives })
}
}
@@ -0,0 +1,73 @@
import { VermineBaseItemSheet } from "./base-item-sheet.mjs"
// ── Item générique ────────────────────────────────────────────────────
export class VermineItemSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["item-gear"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-item-sheet.hbs" } }
}
// ── Arme ──────────────────────────────────────────────────────────────
export class VermineWeaponSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["weapon"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-weapon-sheet.hbs" } }
}
// ── Défense ───────────────────────────────────────────────────────────
export class VermineDefenseSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["defense"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-defense-sheet.hbs" } }
}
// ── Véhicule ──────────────────────────────────────────────────────────
export class VermineVehicleSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["vehicle"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-vehicle-sheet.hbs" } }
}
// ── Capacité ──────────────────────────────────────────────────────────
export class VermineAbilitySheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["ability"], position: { width: 560 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-ability-sheet.hbs" } }
}
// ── Spécialité ────────────────────────────────────────────────────────
export class VermineSpecialtySheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["specialty"], position: { width: 400 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-specialty-sheet.hbs" } }
}
// ── Historique ────────────────────────────────────────────────────────
export class VermineBackgroundSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["background"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-background-sheet.hbs" } }
}
// ── Traumatisme ───────────────────────────────────────────────────────
export class VermineTraumaSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["trauma"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-trauma-sheet.hbs" } }
}
// ── Évolution ─────────────────────────────────────────────────────────
export class VermineEvolutionSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["evolution"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-evolution-sheet.hbs" } }
}
// ── Rumeur ────────────────────────────────────────────────────────────
export class VermineRumorSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["rumor"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rumor-sheet.hbs" } }
}
// ── Cible ─────────────────────────────────────────────────────────────
export class VermineTargetSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["target"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-target-sheet.hbs" } }
}
// ── Rite ──────────────────────────────────────────────────────────────
export class VermineRiteSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["rite"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rite-sheet.hbs" } }
}
+76
View File
@@ -0,0 +1,76 @@
import { VermineBaseActorSheet } from "./base-actor-sheet.mjs"
export class VermineNpcSheetV2 extends VermineBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ["npc"],
position: { width: 750, height: 680 },
window: { contentClasses: ["npc-content"] }
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/npc-main.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
characteristics: { template: "systems/vermine2047/templates/actor/appv2/npc-characteristics.hbs" },
skills: { template: "systems/vermine2047/templates/actor/appv2/npc-skills.hbs" },
threat: { template: "systems/vermine2047/templates/actor/appv2/npc-threat.hbs" },
combat: { template: "systems/vermine2047/templates/actor/appv2/npc-combat.hbs" },
notes: { template: "systems/vermine2047/templates/actor/appv2/npc-notes.hbs" }
}
tabGroups = { sheet: "characteristics" }
#getTabs() {
const tabs = {
characteristics: { id: "characteristics", group: "sheet", icon: "fas fa-dice", label: "VERMINE.abilities" },
skills: { id: "skills", group: "sheet", icon: "fas fa-brain", label: "VERMINE.skills" },
threat: { id: "threat", group: "sheet", icon: "fas fa-exclamation-triangle", label: "ADVERSITY.threat" },
combat: { id: "combat", group: "sheet", icon: "fas fa-sword", label: "VERMINE.combat" },
notes: { id: "notes", group: "sheet", icon: "fas fa-sticky-note", label: "IDENTITY.notes" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
changeTab(tab, group, options = {}) {
super.changeTab(tab, group, options)
if (group === "sheet") {
const main = this.element?.querySelector('[data-group="sheet"][data-tab="main"]')
if (main) main.classList.add("active")
}
}
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main": break
case "characteristics":
context.tab = context.tabs.characteristics
break
case "skills":
context.tab = context.tabs.skills
break
case "threat":
context.tab = context.tabs.threat
break
case "combat":
context.tab = context.tabs.combat
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
context.effects = prepareActiveEffectCategories(doc.effects)
break
case "notes":
context.tab = context.tabs.notes
break
}
return context
}
}
+7
View File
@@ -0,0 +1,7 @@
/**
* Module de ré-export des classes de documents
* Compatible avec Foundry V2
*/
export { default as VermineActor } from "./actor.mjs";
export { default as VermineItem } from "./item.mjs";
+1 -38
View File
@@ -3,44 +3,7 @@
* Extend the base Actor document by defining a custom roll data structure which is ideal for the Simple system. * Extend the base Actor document by defining a custom roll data structure which is ideal for the Simple system.
* @extends {Actor} * @extends {Actor}
*/ */
export class VermineActor extends Actor { export default class VermineActor extends Actor {
/** @override */
prepareBaseData() {
// Data modifications in this step occur before processing embedded
// documents or derived data.
// Initialize wound data to prevent undefined errors with active effects
if (!this.system.minorWound) this.system.minorWound = { value: 0, min: 0, max: 5, threshold: 1 };
if (!this.system.majorWound) this.system.majorWound = { value: 0, min: 0, max: 4, threshold: 4 };
if (!this.system.deadlyWound) this.system.deadlyWound = { value: 0, min: 0, max: 2, threshold: 8 };
// Initialize combatStatus to prevent errors
if (!this.system.combatStatus) {
this.system.combatStatus = { difficulty: "9", label: "Passif" };
}
if (this.type == 'character') {
}
}
/** @override */
prepareEmbeddedDocuments() {
// Check if effects are already being applied in this preparation cycle
// In Foundry V11+, the parent prepareEmbeddedDocuments() calls applyActiveEffects()
// If this is called multiple times in the same cycle, we get the "phase already completed" error
const phase = this.effects?.applicationPhase;
// If we're already in the middle of applying effects (initial or final phase),
// don't call super as it would try to apply effects again
if (phase === "initial" || phase === "final") {
return;
}
// If effects haven't been applied yet, proceed normally
super.prepareEmbeddedDocuments();
}
/** /**
* @override * @override
+6 -13
View File
@@ -2,7 +2,7 @@
* Extend the basic Item with some very simple modifications. * Extend the basic Item with some very simple modifications.
* @extends {Item} * @extends {Item}
*/ */
export class VermineItem extends Item { export default class VermineItem extends Item {
/** /**
* Augment the basic Item data model with additional dynamic data. * Augment the basic Item data model with additional dynamic data.
*/ */
@@ -15,8 +15,7 @@ export class VermineItem extends Item {
const actorType = (this.actor !== null) ? this.actor.type : 'character'; const actorType = (this.actor !== null) ? this.actor.type : 'character';
const itemType = this.type; const itemType = this.type;
// Vérifie si une méthode spécifique au type existe
// Vérifie si une méthode spécifique au type existe// preparedData specifique au type
if (typeof this[`prepare${itemType.charAt(0).toUpperCase() + itemType.slice(1)}Data`] === 'function') { if (typeof this[`prepare${itemType.charAt(0).toUpperCase() + itemType.slice(1)}Data`] === 'function') {
this[`prepare${itemType.charAt(0).toUpperCase() + itemType.slice(1)}Data`](); this[`prepare${itemType.charAt(0).toUpperCase() + itemType.slice(1)}Data`]();
} }
@@ -26,15 +25,14 @@ export class VermineItem extends Item {
this.damagedLabel = this.system.damages.state[parseInt(this.system.damages?.value) - 1]; this.damagedLabel = this.system.damages.state[parseInt(this.system.damages?.value) - 1];
switch (this.damagedLabel) { switch (this.damagedLabel) {
case "endommagé": case "endommagé":
this.damagedIcon = '<i class="fas fa-exclamation-circle" style:"color="yellow"></i>'; this.damagedIcon = '<i class="fas fa-exclamation-circle" style="color=yellow"></i>';
break; break;
case "défectueux": case "défectueux":
this.damagedIcon = '<i class="fas fa-exclamation-triangle" style:"color="orange"></i>'; this.damagedIcon = '<i class="fas fa-exclamation-triangle" style="color=orange"></i>';
break; break;
case "hors d'usage": case "hors d'usage":
this.damagedIcon = '<i class="fas fa-star-exclamation" style:"color="red"></i>'; this.damagedIcon = '<i class="fas fa-star-exclamation" style="color=red"></i>';
break; break;
} }
} }
} }
@@ -44,11 +42,9 @@ export class VermineItem extends Item {
const actorType = (this.actor !== null) ? this.actor.type : 'character'; const actorType = (this.actor !== null) ? this.actor.type : 'character';
if (this.system.type == "") { if (this.system.type == "") {
// console.log('je suis une capacité, avec pour sous-type', this.system.type, actorType);
this.system.type = actorType; this.system.type = actorType;
} }
if (this.system.totem == "" && this.actor !== null && this.actor.system.identity.totem != "") { if (this.system.totem == "" && this.actor !== null && this.actor.system.identity.totem != "") {
// console.log('je suis une capacité, avec pour sous-type', this.system.type, actorType);
this.system.totem = this.actor.system.identity.totem; this.system.totem = this.actor.system.identity.totem;
} }
} }
@@ -79,16 +75,13 @@ export class VermineItem extends Item {
const rollMode = game.settings.get('core', 'rollMode'); const rollMode = game.settings.get('core', 'rollMode');
const label = `[${item.type}] ${item.name}`; const label = `[${item.type}] ${item.name}`;
// If there's no roll data, send a chat message.
let mess = { let mess = {
speaker: speaker, speaker: speaker,
rollMode: rollMode, rollMode: rollMode,
flavor: label, flavor: label,
}; };
mess.content = await renderTemplate(`systems/vermine2047/templates/item/chatCards/${this.type}.hbs`, { item: this, message: mess }) ?? null; mess.content = await foundry.applications.handlebars.renderTemplate(`systems/vermine2047/templates/item/chatCards/${this.type}.hbs`, { item: this, message: mess }) ?? null;
ChatMessage.create(mess) ChatMessage.create(mess)
} }
} }
+16
View File
@@ -0,0 +1,16 @@
export { default as VermineCharacterData } from "./character.mjs"
export { default as VermineNpcData } from "./npc.mjs"
export { default as VermineGroupData } from "./group.mjs"
export { default as VermineCreatureData } from "./creature.mjs"
export { default as VermineItemData } from "./item.mjs"
export { default as VermineWeaponData } from "./weapon.mjs"
export { default as VermineDefenseData } from "./defense.mjs"
export { default as VermineVehicleData } from "./vehicle.mjs"
export { default as VermineAbilityData } from "./ability.mjs"
export { default as VermineSpecialtyData } from "./specialty.mjs"
export { default as VermineBackgroundData } from "./background.mjs"
export { default as VermineTraumaData } from "./trauma.mjs"
export { default as VermineEvolutionData } from "./evolution.mjs"
export { default as VermineRumorData } from "./rumor.mjs"
export { default as VermineTargetData } from "./target.mjs"
export { default as VermineRiteData } from "./rite.mjs"
+285
View File
@@ -0,0 +1,285 @@
/**
* Schémas partagés pour les DataModels Vermine 2047.
* Fonctions factory retournant des objets SchemaField réutilisables.
*/
/**
* Retourne un schema pour une blessure (minor/major/deadly)
* @param {number} defaultThreshold
* @param {number} defaultMax
* @returns {Object}
*/
export function woundSchema(defaultThreshold = 1, defaultMax = 5) {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
threshold: new fields.NumberField({ ...reqInt, initial: defaultThreshold, min: 0 }),
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: defaultMax, min: 0 })
}
}
/**
* Schema des 3 types de blessures présents sur tous les acteurs.
*/
export function woundsSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
minorWound: new fields.SchemaField(woundSchema(1, 5)),
majorWound: new fields.SchemaField(woundSchema(4, 4)),
deadlyWound: new fields.SchemaField(woundSchema(8, 2))
})
}
/**
* Statut de combat (offensif/actif/passif).
*/
export function combatStatusSchema(defaultDifficulty = "7") {
const fields = foundry.data.fields
return new fields.SchemaField({
label: new fields.StringField({ required: true, nullable: false, initial: "" }),
difficulty: new fields.StringField({ required: true, nullable: false, initial: defaultDifficulty })
})
}
/**
* Description d'équipement.
*/
export function equipmentSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
description: new fields.HTMLField({ required: true, initial: "", textSearch: true })
})
}
/**
* Attribut avec value/min/max.
* @param {number} defaultVal
* @param {number} defaultMin
* @param {number} defaultMax
*/
export function attributeSchema(defaultVal = 0, defaultMin = 0, defaultMax = 10) {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: defaultVal, min: defaultMin }),
min: new fields.NumberField({ ...reqInt, initial: defaultMin, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: defaultMax, min: 0 })
})
}
/**
* Caractéristique (capacité) avec catégorie.
* @param {string} category - physical, manual, mental, social
*/
export function abilityField(category) {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 1, min: 0, max: 5 }),
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: 5, min: 0 }),
category: new fields.StringField({ required: true, nullable: false, initial: category })
})
}
/**
* Les 8 caractéristiques communes à character et npc.
*/
export function abilitiesSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
vigor: abilityField("physical"),
health: abilityField("physical"),
precision: abilityField("manual"),
reflexes: abilityField("manual"),
knowledge: abilityField("mental"),
perception: abilityField("mental"),
will: abilityField("social"),
empathy: abilityField("social")
})
}
/**
* Une compétence individuelle.
* @param {string} category
* @param {number} rarity - 0, 1, ou 2
*/
export function skillField(category, rarity = 0) {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 5 }),
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: 5, min: 0 }),
category: new fields.StringField({ required: true, nullable: false, initial: category }),
rarity: new fields.NumberField({ ...reqInt, initial: rarity, min: 0, max: 2 })
})
}
/**
* Les 30 compétences (character et npc).
*/
export function skillsSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
// man
arts: skillField("man", 1),
civilization: skillField("man", 2),
psychology: skillField("man", 1),
rumors: skillField("man", 0),
healing: skillField("man", 1),
// animal
animalism: skillField("animal", 1),
dissection: skillField("animal", 2),
wildlife: skillField("animal", 1),
repulsion: skillField("animal", 0),
tracks: skillField("animal", 0),
// tool
crafting: skillField("tool", 2),
diy: skillField("tool", 0),
mecanical: skillField("tool", 2),
piloting: skillField("tool", 1),
technology: skillField("tool", 2),
// weapon
firearms: skillField("weapon", 2),
archery: skillField("weapon", 0),
armory: skillField("weapon", 2),
throwing: skillField("weapon", 0),
melee: skillField("weapon", 0),
// survival
alertness: skillField("survival", 0),
atletics: skillField("survival", 0),
food: skillField("survival", 0),
stealth: skillField("survival", 0),
close: skillField("survival", 0),
// world
environment: skillField("world", 1),
flora: skillField("world", 1),
road: skillField("world", 0),
toxics: skillField("world", 2),
ruins: skillField("world", 1)
})
}
/**
* Catégories de compétences avec domaine de prédilection.
*/
export function skillCategoriesSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
preferred: new fields.StringField({ required: true, nullable: false, initial: "" }),
man: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "VERMINE.skill_category.man" }),
preferred: new fields.BooleanField({ required: true, initial: false })
}),
animal: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "VERMINE.skill_category.animal" }),
preferred: new fields.BooleanField({ required: true, initial: false })
}),
tool: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "VERMINE.skill_category.tool" }),
preferred: new fields.BooleanField({ required: true, initial: false })
}),
weapon: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "VERMINE.skill_category.weapon" }),
preferred: new fields.BooleanField({ required: true, initial: false })
}),
survival: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "VERMINE.skill_category.survival" }),
preferred: new fields.BooleanField({ required: true, initial: false })
}),
world: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "VERMINE.skill_category.world" }),
preferred: new fields.BooleanField({ required: true, initial: false })
})
})
}
// ── Item shared schemas ──────────────────────────────────────────────────
const reqInt = { required: true, nullable: false, integer: true }
/**
* Rareté avec handicap.
*/
export function raritySchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 3, min: 1, max: 5 }),
handicap: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
})
}
/**
* Dégâts des items (hors arme).
*/
export function itemDamagesSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 5 }),
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: 5, min: 0 }),
pannes: new fields.ArrayField(new fields.StringField({ required: true, initial: "" }), {
initial: ["mineure", "mineure", "grave", "grave", "critique"]
}),
state: new fields.ArrayField(new fields.StringField({ required: true, initial: "" }), {
initial: ["endommagé", "endommagé", "défectueux", "défectueux", "hors d'usage"]
}),
effect: new fields.ArrayField(new fields.StringField({ required: true, initial: "" }), {
initial: ["bonus annulé", "bonus annulé", "malus 1D", "malus 1D", "inutilisable"]
})
})
}
/**
* Base commune à tous les items (template "base" dans l'ancien template.json).
*/
export function baseItemSchema() {
const fields = foundry.data.fields
return {
description: new fields.HTMLField({ required: true, initial: "", textSearch: true }),
rarity: raritySchema(),
reliability: new fields.NumberField({ ...reqInt, initial: 3, min: 1, max: 5 }),
handicap: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
quantity: new fields.NumberField({ ...reqInt, initial: 1, min: 1 }),
weight: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
traits: new fields.ObjectField({ required: true, initial: {} }),
damages: itemDamagesSchema()
}
}
/**
* Template "list" pour les items abstraits (ability, background, trauma, evolution, rumor, target).
* Version légère avec seulement description.
*/
export function listItemSchema() {
const fields = foundry.data.fields
return {
description: new fields.HTMLField({ required: true, initial: "", textSearch: true })
}
}
/**
* Schéma d'apprentissage pour les abilities.
*/
export function learnSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
threshold: new fields.NumberField({ ...reqInt, initial: 5, min: 0 }),
hindrance: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
})
}
/**
* Niveau générique (value/min/max).
*/
export function levelSchema(defaultVal = 1, defaultMin = 1, defaultMax = 5) {
const fields = foundry.data.fields
return new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: defaultVal, min: defaultMin }),
min: new fields.NumberField({ ...reqInt, initial: defaultMin, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: defaultMax, min: 0 })
})
}
+25
View File
@@ -0,0 +1,25 @@
import { listItemSchema, learnSchema, levelSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "ability" (capacités, pouvoirs).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineAbilityData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.ability"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...listItemSchema(),
type: new fields.StringField({ required: true, initial: "" }),
totem: new fields.StringField({ required: true, initial: "" }),
learn: learnSchema(),
level: levelSchema(1, 1, 3),
effects: new fields.ArrayField(new fields.StringField({ required: true, initial: "" }), {
initial: []
})
}
}
}
+20
View File
@@ -0,0 +1,20 @@
import { listItemSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "background" (historiques, origines).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineBackgroundData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.background"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
...listItemSchema(),
cost: new fields.NumberField({ ...reqInt, initial: 1, min: 0 })
}
}
}
+215
View File
@@ -0,0 +1,215 @@
/**
* DataModel pour les acteurs de type "character" (personnage).
* Étend foundry.abstract.TypeDataModel.
*/
import {
woundSchema,
combatStatusSchema,
equipmentSchema,
attributeSchema,
abilitiesSchema,
skillCategoriesSchema,
skillsSchema
} from "./_shared.mjs"
export default class VermineCharacterData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.character"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
// Blessures (base)
minorWound: new fields.SchemaField(woundSchema(1, 5)),
majorWound: new fields.SchemaField(woundSchema(4, 4)),
deadlyWound: new fields.SchemaField(woundSchema(8, 2)),
// Statut de combat (base)
combatStatus: combatStatusSchema("7"),
// Adaptation (totems humain/adapté)
adaptation: new fields.SchemaField({
value: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1, min: 0, max: 5 }),
min: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
max: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 5, min: 0 }),
totems: new fields.SchemaField({
human: new fields.SchemaField({
value: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1, min: 0, max: 3 }),
min: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
max: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 3, min: 0 })
}),
adapted: new fields.SchemaField({
value: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1, min: 0, max: 3 }),
min: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
max: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 3, min: 0 })
})
})
}),
// Identité
identity: new fields.SchemaField({
height: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
weight: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
age: new fields.StringField({ required: true, nullable: false, initial: "15" }),
ageType: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 2 }),
profile: new fields.StringField({ required: true, nullable: false, initial: "" }),
theme: new fields.StringField({ required: true, nullable: false, initial: "" }),
instincts: new fields.StringField({ required: true, nullable: false, initial: "" }),
prohibits: new fields.StringField({ required: true, nullable: false, initial: "" }),
objectives: new fields.StringField({ required: true, nullable: false, initial: "" }),
relations: new fields.StringField({ required: true, nullable: false, initial: "" }),
biography: new fields.StringField({ required: true, nullable: false, initial: "" })
}),
// Équipement
equipment: equipmentSchema(),
// Attributs (XP, réputation, sang-froid, effort)
attributes: new fields.SchemaField({
xp: attributeSchema(0, 0, 10),
reputation: attributeSchema(0, 0, 10),
self_control: attributeSchema(0, 0, 5),
effort: attributeSchema(0, 0, 5)
}),
// Rencontres
encounters: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
// Caractéristiques (8)
abilities: abilitiesSchema(),
// Catégories de compétences
skill_categories: skillCategoriesSchema(),
// Compétences (30)
skills: skillsSchema()
}
}
/** @override */
prepareDerivedData() {
super.prepareDerivedData()
// 1. Déterminer la tranche d'âge
this._setAgeType()
// 2. Calculer les modificateurs de caractéristiques
this._setAbilityModifiers()
// 3. Calculer les réserves (sang-froid et effort)
this._setSelfControlMax()
this._setEffortMax()
// 4. Calculer les seuils de blessures
this._setWoundThresholds()
// 5. Mettre à jour le statut de combat
this._updateCombatStatus()
}
/**
* Détermine la tranche d'âge (1=jeune, 2=adulte, 3=vieux)
* à partir de l'âge et de la config VERMINE.AgeTypes.
*/
_setAgeType() {
const age = this.identity.age
const ageTypes = CONFIG.VERMINE.AgeTypes
for (const [type, cfg] of Object.entries(ageTypes)) {
if (age >= parseInt(cfg.beginning, 10)) {
this.identity.ageType = parseInt(type, 10)
}
}
}
/**
* Calcule les modificateurs de caractéristiques (règle d20).
*/
_setAbilityModifiers() {
for (const ability of Object.values(this.abilities)) {
ability.mod = Math.floor((ability.value - 10) / 2)
}
}
/**
* Calcule le max de sang-froid :
* somme des caractéristiques mentales + sociales + modificateur d'âge.
*/
_setSelfControlMax() {
const abilities = Object.values(this.abilities)
const modFromAge = this._getModFromAgeSelfControl()
const sum = abilities
.filter(a => a.category === "mental" || a.category === "social")
.reduce((acc, a) => acc + a.value, 0)
this.attributes.self_control.max = sum + modFromAge
}
/**
* Calcule le max d'effort :
* somme des caractéristiques physiques + manuelles + modificateur d'âge.
*/
_setEffortMax() {
const abilities = Object.values(this.abilities)
const modFromAge = this._getModFromAgeEffort()
const sum = abilities
.filter(a => a.category === "physical" || a.category === "manual")
.reduce((acc, a) => acc + a.value, 0)
this.attributes.effort.max = sum + modFromAge
}
/**
* Calcule les seuils de blessures à partir de la Santé.
*/
_setWoundThresholds() {
const health = this.abilities.health.value
const ageMods = this._getModFromAgeWounds()
this.minorWound.threshold = health
this.majorWound.threshold = health + 3
this.deadlyWound.threshold = (health + 7 < 11) ? health + 7 : 10
this.minorWound.max = 4 + ageMods.l
this.majorWound.max = 3 + ageMods.h
this.deadlyWound.max = 2 + ageMods.d
}
/**
* Met à jour le label du statut de combat en fonction de la difficulté.
*/
_updateCombatStatus() {
const difficulty = parseInt(this.combatStatus.difficulty) || 9
let newLabel = "Passif"
switch (difficulty) {
case 5: newLabel = "Offensif"; break
case 7: newLabel = "Actif"; break
case 9: newLabel = "Passif"; break
}
if (this.combatStatus.label !== newLabel) {
this.combatStatus.label = newLabel
}
}
// ── Modificateurs liés à l'âge ──────────────────────────────────────
/** @returns {number} Modificateur de sang-froid selon l'âge. */
_getModFromAgeSelfControl() {
return this.identity.ageType === 1 ? -1 : 0
}
/** @returns {number} Modificateur d'effort selon l'âge. */
_getModFromAgeEffort() {
if (this.identity.ageType === 1) return -1
if (this.identity.ageType === 3) return -2
return 0
}
/** @returns {{l: number, h: number, d: number}} Modificateurs de blessures selon l'âge. */
_getModFromAgeWounds() {
if (this.identity.ageType === 1) return { l: 0, h: 0, d: -1 }
if (this.identity.ageType === 3) return { l: -1, h: -1, d: -1 }
return { l: 0, h: 0, d: 0 }
}
}
+182
View File
@@ -0,0 +1,182 @@
/**
* DataModel pour les acteurs de type "creature" (créature).
* Étend foundry.abstract.TypeDataModel.
*/
import {
woundSchema,
combatStatusSchema,
equipmentSchema,
attributeSchema
} from "./_shared.mjs"
export default class VermineCreatureData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.creature"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
// Blessures (base)
minorWound: new fields.SchemaField(woundSchema(1, 5)),
majorWound: new fields.SchemaField(woundSchema(4, 4)),
deadlyWound: new fields.SchemaField(woundSchema(8, 2)),
// Statut de combat (base)
combatStatus: combatStatusSchema(),
// Identité
identity: new fields.SchemaField({
profile: new fields.StringField({ required: true, nullable: false, initial: "" }),
origin: new fields.StringField({ required: true, nullable: false, initial: "" }),
theme: new fields.StringField({ required: true, nullable: false, initial: "" }),
notes: new fields.StringField({ required: true, nullable: false, initial: "" }),
biography: new fields.StringField({ required: true, nullable: false, initial: "" })
}),
// Compétences (description libre)
skills: new fields.StringField({ required: true, nullable: false, initial: "" }),
// Modes de jeu actifs
modes: new fields.SchemaField({
survival: new fields.BooleanField({ required: true, initial: true }),
nightmare: new fields.BooleanField({ required: true, initial: true }),
apocalypse: new fields.BooleanField({ required: true, initial: false })
}),
// Niveaux de créature (patron, taille, rôle, meute)
pattern: attributeSchema(1, 1, 4),
size: attributeSchema(1, 1, 3),
role: attributeSchema(1, 1, 4),
pack: attributeSchema(0, 0, 3),
// Valeurs calculées (dérivées de pattern/size/role/pack)
computed: new fields.SchemaField({
attack: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
damage: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
vigor: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
reaction: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
reactionBonus: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
pools: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
gear: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 9 }),
gearHindrance: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
protection: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1 })
}),
// Équipement
equipment: equipmentSchema()
}
}
/** @override */
prepareDerivedData() {
super.prepareDerivedData()
// 1. Calculer les valeurs dérivées (attaque, dégâts, vigueur, etc.)
this._calculateCreatureComputedValues()
// 2. Calculer les seuils de blessures
this._calculateCreatureWoundThresholds()
// 3. Mettre à jour le statut de combat
this._updateCombatStatus()
}
/**
* Calcule les valeurs dérivées à partir des niveaux de patron, taille, rôle et meute.
* Utilise les configs CONFIG.VERMINE.creaturePatternLevels, .creatureSizeLevels,
* .creatureRoleLevels, .creaturePackLevels.
*
* Règles :
* - Attaque = pattern.attack + size.attack + pack.attack + role.reaction
* - Dégâts = pattern.damage + size.vigor + pack.damage
* - Vigueur = size.vigor + pack.damage
* - Réaction = role.reaction + role.reaction_bonus
*/
_calculateCreatureComputedValues() {
const patternLevel = this.pattern?.value || 1
const sizeLevel = this.size?.value || 1
const roleLevel = this.role?.value || 1
const packLevel = this.pack?.value || 0
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {}
const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {}
const roleConfig = CONFIG.VERMINE.creatureRoleLevels[roleLevel] || {}
const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {}
// Attaque : patron + taille + meute + réaction du rôle
this.computed.attack = (patternConfig.attack || 0)
+ (sizeConfig.attack || 0)
+ (packConfig.attack || 0)
+ (roleConfig.reaction || 0)
// Dégâts : patron + vigueur de taille + meute
this.computed.damage = (patternConfig.damage || 0)
+ (sizeConfig.vigor || 0)
+ (packConfig.damage || 0)
// Vigueur : taille + meute
this.computed.vigor = (sizeConfig.vigor || 0) + (packConfig.damage || 0)
// Réaction : rôle
this.computed.reaction = (roleConfig.reaction || 0) + (roleConfig.reaction_bonus || 0)
this.computed.reactionBonus = roleConfig.reaction_bonus || 0
// Réserves
this.computed.pools = roleConfig.pools || 0
// Équipement et handicap
this.computed.gear = roleConfig.gear || 9
this.computed.gearHindrance = roleConfig.gear_hindrance || 0
// Protection
this.computed.protection = roleConfig.protection || 1
}
/**
* Calcule les seuils de blessures à partir du patron, de la taille et de la meute.
* Les seuils sont la somme des valeurs correspondantes des trois sources.
*/
_calculateCreatureWoundThresholds() {
const patternLevel = this.pattern?.value || 1
const sizeLevel = this.size?.value || 1
const packLevel = this.pack?.value || 0
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {}
const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {}
const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {}
this.minorWound.threshold = (patternConfig.minorWound || 0)
+ (sizeConfig.minorWound || 0)
+ (packConfig.minorWound || 0)
this.majorWound.threshold = (patternConfig.majorWound || 0)
+ (sizeConfig.majorWound || 0)
+ (packConfig.majorWound || 0)
this.deadlyWound.threshold = (patternConfig.deadlyWound || 0)
+ (sizeConfig.deadlyWound || 0)
+ (packConfig.deadlyWound || 0)
// Max de blessures
this.minorWound.max = Math.min(5, this.minorWound.threshold + 2)
this.majorWound.max = Math.min(4, this.majorWound.threshold + 1)
this.deadlyWound.max = Math.min(2, this.deadlyWound.threshold)
}
/**
* Met à jour le label du statut de combat en fonction de la difficulté.
*/
_updateCombatStatus() {
const difficulty = parseInt(this.combatStatus.difficulty) || 9
let newLabel = "Passif"
switch (difficulty) {
case 5: newLabel = "Offensif"; break
case 7: newLabel = "Actif"; break
case 9: newLabel = "Passif"; break
}
if (this.combatStatus.label !== newLabel) {
this.combatStatus.label = newLabel
}
}
}
+26
View File
@@ -0,0 +1,26 @@
import { baseItemSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "defense" (protections, armures, boucliers).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineDefenseData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.defense"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
...baseItemSchema(),
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
specificLevel: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "" }),
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
}),
mobility: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
isShield: new fields.BooleanField({ required: true, initial: false })
}
}
}
+19
View File
@@ -0,0 +1,19 @@
import { listItemSchema, levelSchema } from './_shared.mjs'
/**
* DataModel pour les items de type "evolution" (évolutions du personnage).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineEvolutionData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.evolution"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...listItemSchema(),
level: levelSchema(1, 1, 4)
}
}
}
+164
View File
@@ -0,0 +1,164 @@
/**
* DataModel pour les acteurs de type "group" (groupe).
* Étend foundry.abstract.TypeDataModel.
*/
import {
woundSchema,
combatStatusSchema,
equipmentSchema,
attributeSchema,
levelSchema
} from "./_shared.mjs"
export default class VermineGroupData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.group"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
// Blessures (base)
minorWound: new fields.SchemaField(woundSchema(1, 5)),
majorWound: new fields.SchemaField(woundSchema(4, 4)),
deadlyWound: new fields.SchemaField(woundSchema(8, 2)),
// Statut de combat (base)
combatStatus: combatStatusSchema(),
// Identité du groupe
identity: new fields.SchemaField({
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
profile: new fields.StringField({ required: true, nullable: false, initial: "" }),
origin: new fields.StringField({ required: true, nullable: false, initial: "" }),
theme: new fields.StringField({ required: true, nullable: false, initial: "" }),
instincts: new fields.StringField({ required: true, nullable: false, initial: "" }),
prohibits: new fields.StringField({ required: true, nullable: false, initial: "" }),
notes: new fields.StringField({ required: true, nullable: false, initial: "" })
}),
// Équipement
equipment: equipmentSchema(),
// Niveau du groupe (1-10)
level: levelSchema(1, 1, 10),
// Réputation
reputation: new fields.SchemaField({
value: new fields.NumberField({ required: true, nullable: true, integer: true, initial: 10, min: 2 }),
min: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 2, min: 0 }),
max: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 10, min: 0 })
}),
// Moral
morale: new fields.SchemaField({
level: new fields.StringField({ required: true, nullable: false, initial: "high" }),
value: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 7, min: 0, max: 7 }),
min: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
max: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 7, min: 0 })
}),
// Réserve
reserve: attributeSchema(0, 0, 10),
// Objectifs (majeurs et mineurs)
objectives: new fields.SchemaField({
major: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
minor: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" }))
}),
// Capacités de groupe
groupAbilities: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
// Membres (IDs d'acteurs)
members: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
// Rencontres
encounters: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" }))
}
}
/** @override */
prepareDerivedData() {
super.prepareDerivedData()
// 1. Initialiser les données de groupe si absentes
this._initGroupData()
// 2. Calculer la réserve max selon le niveau
this._calculateGroupReserve()
// 3. Mettre à jour le moral selon la valeur de dés
this._updateGroupMorale()
// 4. Mettre à jour le statut de combat
this._updateCombatStatus()
}
/**
* Initialise les champs optionnels du groupe s'ils ne sont pas présents.
*/
_initGroupData() {
if (!this.objectives) {
this.objectives = { major: [], minor: [] }
}
if (!this.groupAbilities) {
this.groupAbilities = []
}
if (!this.reserve) {
this.reserve = { value: 0, min: 0, max: 10 }
}
}
/**
* Calcule la réserve max en fonction du niveau du groupe.
* Règle simplifiée : niveau × 2, plafonné à 10.
*/
_calculateGroupReserve() {
const level = this.level?.value || 1
this.reserve.max = Math.min(10, level * 2)
if (this.reserve.value > this.reserve.max) {
this.reserve.value = this.reserve.max
}
}
/**
* Met à jour le niveau de moral en fonction de la valeur de dés.
* Règles : 7D+ = Haut, 6-3D = Normal, 2-1D = Bas, 0D = Crise.
*/
_updateGroupMorale() {
const moraleValue = this.morale?.value || 0
// Ne pas écraser un niveau explicitement défini (sauf "high" qui est la valeur par défaut)
if (this.morale.level && this.morale.level !== "high") return
if (moraleValue >= 7) {
this.morale.level = "high"
} else if (moraleValue >= 3) {
this.morale.level = "normal"
} else if (moraleValue >= 1) {
this.morale.level = "low"
} else {
this.morale.level = "crisis"
}
}
/**
* Met à jour le label du statut de combat en fonction de la difficulté.
*/
_updateCombatStatus() {
const difficulty = parseInt(this.combatStatus.difficulty) || 9
let newLabel = "Passif"
switch (difficulty) {
case 5: newLabel = "Offensif"; break
case 7: newLabel = "Actif"; break
case 9: newLabel = "Passif"; break
}
if (this.combatStatus.label !== newLabel) {
this.combatStatus.label = newLabel
}
}
}
+23
View File
@@ -0,0 +1,23 @@
/**
* DataModel pour les items de type "item" (équipement générique).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineItemData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.item"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...baseItemSchema(),
needSkill: new fields.SchemaField({
value: new fields.BooleanField({ required: true, initial: false }),
skill: new fields.StringField({ required: true, initial: "" })
})
}
}
}
// Import partagé — après la déclaration de classe car defineSchema est statique
import { baseItemSchema } from './_shared.mjs'
+162
View File
@@ -0,0 +1,162 @@
/**
* DataModel pour les acteurs de type "npc" (PNJ).
* Étend foundry.abstract.TypeDataModel.
*
* Note : le champ libre de compétences (texte descriptif) est nommé "freeSkills"
* pour éviter le conflit avec le SchemaField "skills" qui contient les 30 compétences.
*/
import {
woundSchema,
combatStatusSchema,
equipmentSchema,
attributeSchema,
abilitiesSchema,
skillCategoriesSchema,
skillsSchema
} from "./_shared.mjs"
export default class VermineNpcData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.npc"]
/**
* Migration des données avant traitement par le schéma.
* Avant DataModel, template.json définissait "skills" comme un champ texte libre
* pour les PNJ. Le DataModel réserve "skills" pour les 30 compétences individuelles
* (SchemaField) et utilise "freeSkills" pour le texte libre.
* @param {Object} source Données brutes avant validation du schéma
* @returns {Object} Données migrées
*/
static migrateData(source) {
if (typeof source.skills === "string") {
source.freeSkills = source.skills
}
return super.migrateData(source)
}
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
// Blessures (base)
minorWound: new fields.SchemaField(woundSchema(1, 5)),
majorWound: new fields.SchemaField(woundSchema(4, 4)),
deadlyWound: new fields.SchemaField(woundSchema(8, 2)),
// Statut de combat (base, difficulté par défaut 9 pour PNJ)
combatStatus: combatStatusSchema("9"),
// Identité
identity: new fields.SchemaField({
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
profile: new fields.StringField({ required: true, nullable: false, initial: "" }),
origin: new fields.StringField({ required: true, nullable: false, initial: "" }),
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
theme: new fields.StringField({ required: true, nullable: false, initial: "" }),
notes: new fields.StringField({ required: true, nullable: false, initial: "" })
}),
// Attributs (XP, réputation, sang-froid, effort)
attributes: new fields.SchemaField({
xp: attributeSchema(0, 0, 10),
reputation: attributeSchema(0, 0, 10),
self_control: attributeSchema(0, 0, 5),
effort: attributeSchema(0, 0, 5)
}),
// Niveaux PNJ (menace, expérience, rôle)
threat: attributeSchema(1, 1, 4),
experience: attributeSchema(1, 1, 4),
role: attributeSchema(1, 1, 4),
// Compétences (les 30 compétences individuelles)
skills: skillsSchema(),
// Description libre des compétences (champ texte PNJ)
freeSkills: new fields.StringField({ required: true, nullable: false, initial: "" }),
// Catégories de compétences
skill_categories: skillCategoriesSchema(),
// Caractéristiques (8)
abilities: abilitiesSchema(),
// Équipement
equipment: equipmentSchema()
}
}
/** @override */
prepareDerivedData() {
super.prepareDerivedData()
// 1. Calculer les seuils de blessures selon le niveau de menace
this._setNpcWoundThresholds()
// 2. Calculer les réserves selon le niveau de rôle
this._setNpcAttributes()
// 3. Définir les libellés des caractéristiques
this._setAbilityLabels()
// 4. Mettre à jour le statut de combat
this._updateCombatStatus()
}
/**
* Calcule les seuils de blessures à partir du niveau de menace.
* Utilise CONFIG.VERMINE.npcThreatLevels.
*/
_setNpcWoundThresholds() {
const health = this.abilities?.health?.value || 1
const threatLevel = this.threat?.value || 1
const threatConfig = CONFIG.VERMINE.npcThreatLevels[threatLevel] || {}
this.minorWound.threshold = threatConfig.minorWound || health
this.majorWound.threshold = threatConfig.majorWound || (health + 3)
this.deadlyWound.threshold = threatConfig.deadlyWound || (health + 7 < 11 ? health + 7 : 10)
this.minorWound.max = threatConfig.minorWound || 4
this.majorWound.max = threatConfig.majorWound || 3
this.deadlyWound.max = threatConfig.deadlyWound || 2
}
/**
* Définit les attributs dérivés (effort, sang-froid) selon le niveau de rôle.
* Utilise CONFIG.VERMINE.npcRoleLevels.
*/
_setNpcAttributes() {
const roleLevel = this.role?.value || 1
const roleConfig = CONFIG.VERMINE.npcRoleLevels[roleLevel] || {}
this.attributes.effort.max = roleConfig.pools || 0
this.attributes.self_control.max = roleConfig.reaction_bonus || 0
}
/**
* Définit les libellés localisés des caractéristiques.
*/
_setAbilityLabels() {
for (const [k, v] of Object.entries(this.abilities)) {
v.label = game.i18n.localize(CONFIG.VERMINE.abilities[k]) ?? k
}
}
/**
* Met à jour le label du statut de combat en fonction de la difficulté.
*/
_updateCombatStatus() {
const difficulty = parseInt(this.combatStatus.difficulty) || 9
let newLabel = "Passif"
switch (difficulty) {
case 5: newLabel = "Offensif"; break
case 7: newLabel = "Actif"; break
case 9: newLabel = "Passif"; break
}
if (this.combatStatus.label !== newLabel) {
this.combatStatus.label = newLabel
}
}
}
+22
View File
@@ -0,0 +1,22 @@
import { baseItemSchema } from './_shared.mjs'
/**
* DataModel pour les items de type "rite" (rites, rituels).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineRiteData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.rite"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...baseItemSchema(),
rituel: new fields.StringField({ required: true, initial: "" }),
transe: new fields.StringField({ required: true, initial: "" }),
ability: new fields.StringField({ required: true, initial: "" }),
effect: new fields.StringField({ required: true, initial: "" })
}
}
}
+18
View File
@@ -0,0 +1,18 @@
import { listItemSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "rumor" (rumeurs).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineRumorData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.rumor"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...listItemSchema()
}
}
}
+18
View File
@@ -0,0 +1,18 @@
/**
* DataModel pour les items de type "specialty" (spécialités de compétence).
* Modèle minimal sans base partagée.
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineSpecialtyData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.specialty"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
skill: new fields.StringField({ required: true, nullable: false, initial: "" })
}
}
}
+19
View File
@@ -0,0 +1,19 @@
import { listItemSchema } from './_shared.mjs'
/**
* DataModel pour les items de type "target" (cibles, objectifs).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineTargetData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.target"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...listItemSchema(),
level: new fields.StringField({ required: true, initial: "minor" })
}
}
}
+19
View File
@@ -0,0 +1,19 @@
import { listItemSchema } from './_shared.mjs'
/**
* DataModel pour les items de type "trauma" (traumatismes, séquelles).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineTraumaData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.trauma"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...listItemSchema(),
type: new fields.StringField({ required: true, initial: "" })
}
}
}
+20
View File
@@ -0,0 +1,20 @@
import { baseItemSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "vehicle" (véhicules).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineVehicleData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.vehicle"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
...baseItemSchema(),
mobility: new fields.NumberField({ ...reqInt, initial: 3, min: 0 })
}
}
}
+27
View File
@@ -0,0 +1,27 @@
import { baseItemSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "weapon" (armes).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineWeaponData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.weapon"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
...baseItemSchema(),
min_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
damage: new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
type: new fields.StringField({ required: true, initial: "" }),
addVigor: new fields.BooleanField({ required: true, initial: false })
}),
ammo: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
}
}
}
-157
View File
@@ -1,157 +0,0 @@
import { onManageActiveEffect, prepareActiveEffectCategories } from "../system/effects.mjs";
import { preloadHandlebarsTemplates } from "../system/handlebars-manager.mjs";
/**
* Extend the basic ActorSheet with some very simple modifications
* @extends {ActorSheet}
*/
export class VermineActorSheet extends ActorSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
/*classes: ["vermine2047", "sheet", "actor"],
template: "systems/vermine2047/templates/actor/actor-sheet.hbs",
height: 800,
width: 690,
resizable: false,
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "features" }]*/
});
}
/** @override */
get template() {
return `systems/vermine2047/templates/actor/actor-${this.actor.type}-sheet.hbs`;
}
/* -------------------------------------------- */
/** @override */
getData() {
// Retrieve the data structure from the base sheet. You can inspect or log
// the context variable to see the structure, but some key properties for
// sheets are the actor object, the data object, whether or not it's
// editable, the items array, and the effects array.
const context = super.getData();
// Use a safe clone of the actor data for further operations.
const actorData = this.actor.toObject(false);
// Add the actor's data to context.data for easier access, as well as flags.
context.system = actorData.system;
context.flags = actorData.flags;
//add system config for convenience use
context.config = CONFIG.VERMINE;
// Add roll data for TinyMCE editors.
context.rollData = context.actor.getRollData();
// Prepare active effects
context.effects = prepareActiveEffectCategories(this.actor.effects);
return context;
}
/** @override */
activateListeners(html) {
super.activateListeners(html);
// Render the item sheet for viewing/editing prior to the editable check.
html.find('.item-edit').click(ev => {
const li = $(ev.currentTarget).parents(".item");
const item = this.actor.items.get(li.data("itemId"));
item.sheet.render(true);
});
// -------------------------------------------------------------
// Everything below here is only needed if the sheet is editable
if (!this.isEditable) return;
// Add Inventory Item
html.find('.item-create').click(this._onItemCreate.bind(this));
// Delete Inventory Item
html.find('.item-delete').click(ev => {
const li = $(ev.currentTarget).parents(".item");
const item = this.actor.items.get(li.data("itemId"));
item.delete();
li.slideUp(200, () => this.render(false));
});
html.find(".item-roll").click(ev => {
this._onRollItem(ev)
})
// Active Effect management
html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.actor));
// Drag events for macros.
if (this.actor.isOwner) {
let handler = ev => this._onDragStart(ev);
html.find('li.item').each((i, li) => {
if (li.classList.contains("inventory-header")) return;
li.setAttribute("draggable", true);
li.addEventListener("dragstart", handler, false);
});
}
//click on wound radio
html.find('.hexa [type="radio"]').click(ev => {
ev.preventDefault();
ev.stopPropagation();
return this._onClickRadioHexa(ev)
})
}
async _onRollItem(ev) {
const li = $(ev.currentTarget).parents(".item");
const item = this.actor.items.get(li.data("itemId"));
item.roll();
}
_onClickRadioHexa(ev) {
let input = ev.currentTarget;
console.log(input.value, input.name);
let update = {};
update[input.name] = 0
let propTree = input.name.split('.')
let current = this.actor;
for (let prop of propTree) {
current = current[prop]
}
if (current != input.value) {
update[input.name] = parseInt(input.value)
} else {
update[input.name] = parseInt(input.value) - 1;
}
this.actor.update(update)
}
async _onItemCreate(event) {
event.preventDefault();
const header = event.currentTarget;
// Get the type of item to create.
const type = header.dataset.type;
// Grab any data associated with this control.
const data = foundry.utils.duplicate(header.dataset);
// Initialize a default name.
// const name = `New ${type.capitalize()}`;
const name = game.i18n.localize('ITEMS.new_' + type);
// Prepare the item object.
const itemData = {
name: name,
type: type,
system: data
};
// Remove the type from the dataset since it's in the itemData.type prop.
delete itemData.system["type"];
// Finally, create the item!
return await Item.create(itemData, { parent: this.actor });
}
}
-228
View File
@@ -1,228 +0,0 @@
import { onManageActiveEffect, prepareActiveEffectCategories } from "../system/effects.mjs";
import { VermineActorSheet } from "./actor-sheet.mjs";
import RollDialog from "../system/dialogs/rollDialog.mjs";
import { TotemPicker } from "../system/applications.mjs";
/**
* Extend the basic ActorSheet with some very simple modifications
* @extends {VermineActorSheet}
*/
export class VermineCharacterSheet extends VermineActorSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["vermine2047", "sheet", "character", "actor"],
template: "systems/vermine2047/templates/actor/actor-sheet.hbs",
width: "fit-content",
height: "fit-content",
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "features" }]
});
}
/** @override */
get template() {
return `systems/vermine2047/templates/actor/actor-character-sheet.hbs`;
}
/* -------------------------------------------- */
/** @override */
getData() {
// Retrieve the data structure from the base sheet. You can inspect or log
// the context variable to see the structure, but some key properties for
// sheets are the actor object, the data object, whether or not it's
// editable, the items array, and the effects array.
const context = super.getData();
// Use a safe clone of the actor data for further operations.
const actorData = this.actor.toObject(false);
// Add the actor's data to context.data for easier access, as well as flags.
context.system = actorData.system;
context.flags = actorData.flags;
context.config = CONFIG.VERMINE;
// Prepare character data and items.
if (actorData.type == 'character') {
this._prepareItems(context);
this._prepareCharacterData(context);
}
// Prepare NPC data and items.
if (actorData.type == 'npc') {
this._prepareItems(context);
}
// Add roll data for TinyMCE editors.
context.rollData = context.actor.getRollData();
// Prepare active effects
context.effects = prepareActiveEffectCategories(this.actor.effects);
return context;
}
/**
* Organize and classify Items for Character sheets.
*
* @param {Object} actorData The actor to prepare.
*
* @return {undefined}
*/
_prepareCharacterData(context) {
// Handle ability scores.
for (let [k, v] of Object.entries(context.system.abilities)) {
v.label = game.i18n.localize(context.system.abilities[k].label) ?? k;
}
for (let [k, v] of Object.entries(context.system.skills)) {
if (v.value >= 2) {
let spe = this.actor.items.filter(it => it.type == "specialty").filter(spec => spec.system.skill == k);
v.specialties = spe
}
}
}
/**
* Organize and classify Items for Character sheets.
*
* @param {Object} actorData The actor to prepare.
*
* @return {undefined}
*/
_prepareItems(context) {
context.gear = this.actor.itemTypes['item'];
context.weapons = this.actor.itemTypes['weapon'];
context.defenses = this.actor.itemTypes['defense'];
context.traits = this.actor.itemTypes['trait'];
context.specialties = this.actor.itemTypes['specialty'];
context.abilities = this.actor.itemTypes['ability'];
context.evolutions = this.actor.itemTypes['evolution'];
context.traumas = this.actor.itemTypes['trauma'];
context.backgrounds = this.actor.itemTypes['background'];
context.rumors = this.actor.itemTypes['rumor'];
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
//desactiver les inputs si mode jeu
if (!this.actor.flags.world?.editMode) {
this.disableInputs(html)
}
// Choose Totem
html.find('.chooseTotem').click(this._onTotemButton.bind(this));
//activer lest jets
html.find('.ability .rollable').click(this._onRoll.bind(this));
//gérer les dés totems
html.find('[data-totem-name]').click(this._onClickTotemDice.bind(this));
//creation de specialités
html.find('i.add-specialty').click(this.addSpecialty.bind(this))
}
//mode jeu/edit en mode jeu on bloque les selects et input
disableInputs(html) {
for (let input of html.find('input')) {
//préserver le toggle mode jeu/ mode edit
if (input.name != "flags.world.editMode") {
input.setAttribute('disabled', true)
}
}
for (let select of html.find('select')) {
select.setAttribute('disabled', true)
}
}
async addSpecialty(ev) {
let skillName = ev.target.closest('.ability').querySelector('label').dataset.label;
let itemData = {
name: `spécialité, ${skillName}`,
type: 'specialty',
system: {
skill: skillName
}
}
let spec = await this.actor.createEmbeddedDocuments("Item", [itemData]);
spec[0].sheet.render(true)
}
async _onClickTotemDice(ev) {
let el = ev.currentTarget;
let totem = el.dataset.totemName;
let value = parseInt(el.dataset.totemValue) || 0;
let oldValue = this.actor.system.adaptation.totems[totem].value;
if (value === oldValue) { value-- };
let updates = {};
updates[`system.adaptation.totems.${totem}.value`] = value;
//verifier le max des dés totems
let sum = value;
switch (totem) {
case "human":
sum += this.actor.system.adaptation.totems.adapted.value;
break;
case "adapted":
sum += this.actor.system.adaptation.totems.human.value;
break;
}
if (sum > 5) { return ui.notifications.warn("pas plus de 5 dés totems") }
await this.actor.update(updates);
}
/**
* Handle clickable rolls.
* @param {Event} event The originating click event
* @private
*/
async _onRoll(event) {
event.preventDefault();
const element = event.currentTarget;
const dataset = element.dataset;
console.log("Ceci est un jet d'un personnage joueur", this.actor);
// Handle item rolls.
if (dataset.rollType) {
if (dataset.rollType == 'item') {
const itemId = element.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemId);
if (item) return item.roll();
}
}
// Handle rolls that supply the formula directly.
if (dataset.label) {
dataset.rollType = dataset.type;
let data = {
actorId: this.object.id,
rollType: dataset.rollType,
labelKey: dataset.label,
label: game.i18n.localize(dataset.label)
};
let dial = await RollDialog.create(data);
console.log("from sheet", data, this)
return dial.render(true)
}
}
/**
* Handle totem pick
* @param {Event} event The originating click event
* @private
*/
_onTotemButton(event) {
event.preventDefault();
const el = event.currentTarget;
// const dataset = el.dataset;
const totemPicker = new TotemPicker(el, this.actor);
totemPicker.render(true);
}
}
-167
View File
@@ -1,167 +0,0 @@
import { onManageActiveEffect, prepareActiveEffectCategories } from "../system/effects.mjs";
import { VermineActorSheet } from "./actor-sheet.mjs";
/**
* Extend the basic ActorSheet with some very simple modifications
* @extends {ActorSheet}
*/
export class VermineCreatureSheet extends VermineActorSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["vermine2047", "sheet", "actor", "creature"],
template: "systems/vermine2047/templates/actor/actor-sheet.hbs",
width: 650,
height: 600,
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description" }]
});
}
/** @override */
get template() {
return `systems/vermine2047/templates/actor/actor-${this.actor.type}-sheet.hbs`;
}
/* -------------------------------------------- */
/** @override */
getData() {
// Retrieve the data structure from the base sheet. You can inspect or log
// the context variable to see the structure, but some key properties for
// sheets are the actor object, the data object, whether or not it's
// editable, the items array, and the effects array.
const context = super.getData();
// Use a safe clone of the actor data for further operations.
const actorData = this.actor.toObject(false);
// Add the actor's data to context.data for easier access, as well as flags.
context.system = actorData.system;
context.flags = actorData.flags;
context.config = CONFIG.VERMINE;
// Prepare character data and items.
if (actorData.type == 'character') {
this._prepareItems(context);
this._prepareCharacterData(context);
}
// Prepare NPC data and items.
if (actorData.type == 'npc') {
this._prepareItems(context);
}
// Prepare Creature data and items.
if (actorData.type == 'creature') {
this._prepareItems(context);
this._prepareCreatureData(context);
}
// Add roll data for TinyMCE editors.
context.rollData = context.actor.getRollData();
// Prepare active effects
context.effects = prepareActiveEffectCategories(this.actor.effects);
return context;
}
/**
* Organize and classify Items for Character sheets.
*
* @param {Object} actorData The actor to prepare.
*
* @return {undefined}
*/
_prepareItems(context) {
context.gear = this.actor.itemTypes['item'];
context.traits = this.actor.itemTypes['trait'];
}
/**
* Prepare Character type specific data.
*
* @param {Object} actorData The actor to prepare.
*
* @return {undefined}
*/
_prepareCharacterData(context) {
// Handle ability scores.
for (let [k, v] of Object.entries(context.system.abilities)) {
v.label = game.i18n.localize(context.system.abilities[k].label) ?? k;
}
}
/**
* Prepare Creature type specific data for the sheet.
*
* @param {Object} context The context data to prepare.
* @return {undefined}
*/
_prepareCreatureData(context) {
if (this.actor.type !== 'creature') return;
// Add computed values to context
context.computed = context.system.computed || {};
// Get labels for pattern, size, role
const patternLevel = context.system.pattern?.value || 1;
const sizeLevel = context.system.size?.value || 1;
const roleLevel = context.system.role?.value || 1;
const packLevel = context.system.pack?.value || 0;
// Add pattern label
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel];
if (patternConfig) {
context.patternLabel = game.i18n.localize(patternConfig.label);
}
// Add size label (using numeric for now)
context.sizeLabel = sizeLevel;
// Add role label
const roleConfig = CONFIG.VERMINE.creatureRoleLevels[roleLevel];
if (roleConfig) {
context.roleLabel = game.i18n.localize(roleConfig.label);
}
// Add pack label
context.packLabel = packLevel > 0 ? packLevel : game.i18n.localize('VERMINE.none');
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
html.find('.item-create').click(this._onItemCreate.bind(this));
}
async _onItemCreate(event) {
event.preventDefault();
const header = event.currentTarget;
// Get the type of item to create.
const type = header.dataset.type;
// Grab any data associated with this control.
const data = foundry.utils.duplicate(header.dataset);
// Initialize a default name.
// const name = `New ${type.capitalize()}`;
const name = game.i18n.localize('ITEMS.new_' + type);
console.log('onItemCreate child', data.type, this.actor.type);
// Prepare the item object.
const itemData = {
name: name,
type: type,
system: data
};
// Remove the type from the dataset since it's in the itemData.type prop.
delete itemData.system["type"];
// Finally, create the item!
return await Item.create(itemData, { parent: this.actor });
}
}
-167
View File
@@ -1,167 +0,0 @@
import { onManageActiveEffect, prepareActiveEffectCategories } from "../system/effects.mjs";
import { VermineActorSheet } from "./actor-sheet.mjs";
/**
* Extend the basic ActorSheet with some very simple modifications
* @extends {ActorSheet}
*/
export class VermineCreatureSheet extends VermineActorSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["vermine2047", "sheet", "actor", "creature"],
template: "systems/vermine2047/templates/actor/actor-sheet.hbs",
width: 650,
height: 600,
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description" }]
});
}
/** @override */
get template() {
return `systems/vermine2047/templates/actor/actor-${this.actor.type}-sheet.hbs`;
}
/* -------------------------------------------- */
/** @override */
getData() {
// Retrieve the data structure from the base sheet. You can inspect or log
// the context variable to see the structure, but some key properties for
// sheets are the actor object, the data object, whether or not it's
// editable, the items array, and the effects array.
const context = super.getData();
// Use a safe clone of the actor data for further operations.
const actorData = this.actor.toObject(false);
// Add the actor's data to context.data for easier access, as well as flags.
context.system = actorData.system;
context.flags = actorData.flags;
context.config = CONFIG.VERMINE;
// Prepare character data and items.
if (actorData.type == 'character') {
this._prepareItems(context);
this._prepareCharacterData(context);
}
// Prepare NPC data and items.
if (actorData.type == 'npc') {
this._prepareItems(context);
}
// Prepare Creature data and items.
if (actorData.type == 'creature') {
this._prepareItems(context);
this._prepareCreatureData(context);
}
// Add roll data for TinyMCE editors.
context.rollData = context.actor.getRollData();
// Prepare active effects
context.effects = prepareActiveEffectCategories(this.actor.effects);
return context;
/**
* Prepare Creature type specific data for the sheet.
*
* @param {Object} context The context data to prepare.
* @return {undefined}
*/
_prepareCreatureData(context) {
if (this.actor.type !== 'creature') return;
// Add computed values to context
context.computed = context.system.computed || {};
// Get labels for pattern, size, role
const patternLevel = context.system.pattern?.value || 1;
const sizeLevel = context.system.size?.value || 1;
const roleLevel = context.system.role?.value || 1;
const packLevel = context.system.pack?.value || 0;
// Add pattern label
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel];
if (patternConfig) {
context.patternLabel = game.i18n.localize(patternConfig.label);
}
// Add size label (using numeric for now)
context.sizeLabel = sizeLevel;
// Add role label
const roleConfig = CONFIG.VERMINE.creatureRoleLevels[roleLevel];
if (roleConfig) {
context.roleLabel = game.i18n.localize(roleConfig.label);
}
// Add pack label
context.packLabel = packLevel > 0 ? packLevel : game.i18n.localize('VERMINE.none');
}}
/**
* Organize and classify Items for Character sheets.
*
* @param {Object} actorData The actor to prepare.
*
* @return {undefined}
*/
_prepareCharacterData(context) {
// Handle ability scores.
for (let [k, v] of Object.entries(context.system.abilities)) {
v.label = game.i18n.localize(context.system.abilities[k].label) ?? k;
}
}
/**
* Organize and classify Items for Character sheets.
*
* @param {Object} actorData The actor to prepare.
*
* @return {undefined}
*/
_prepareItems(context) {
context.gear = this.actor.itemTypes['item'];
context.traits = this.actor.itemTypes['trait'];
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
html.find('.item-create').click(this._onItemCreate.bind(this));
}
async _onItemCreate(event) {
event.preventDefault();
const header = event.currentTarget;
// Get the type of item to create.
const type = header.dataset.type;
// Grab any data associated with this control.
const data = duplicate(header.dataset);
// Initialize a default name.
// const name = `New ${type.capitalize()}`;
const name = game.i18n.localize('ITEMS.new_' + type);
console.log('onItemCreate child', data.type, this.actor.type);
// Prepare the item object.
const itemData = {
name: name,
type: type,
system: data
};
// Remove the type from the dataset since it's in the itemData.type prop.
delete itemData.system["type"];
// Finally, create the item!
return await Item.create(itemData, { parent: this.actor });
}
}
-80
View File
@@ -1,80 +0,0 @@
import { TraitSelector } from "../system/applications.mjs";
/**
* Extend the basic ItemSheet with some very simple modifications
* @extends {ItemSheet}
*/
export class VermineItemSheet extends ItemSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["vermine2047", "sheet", "item"],
width: 450,
height: "max-content",
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description" }]
});
}
/** @override */
get template() {
const path = "systems/vermine2047/templates/item";
return `${path}/item-${this.item.type}-sheet.hbs`;
}
/* -------------------------------------------- */
/** @override */
getData() {
// Retrieve base data structure.
const context = super.getData();
// Use a safe clone of the item data for further operations.
const itemData = context.item;
// Retrieve the roll data for TinyMCE editors.
context.rollData = {};
let actor = this.object?.parent ?? null;
if (actor) {
context.rollData = actor.getRollData();
}
// Add the actor's data to context.data for easier access, as well as flags.
context.system = itemData.system;
context.flags = itemData.flags;
context.config = CONFIG.VERMINE;
return context;
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
// Everything below here is only needed if the sheet is editable
if (!this.isEditable) return;
//click on wound radio
html.find('.damages-row [type="radio"]').click(ev => {
this._onClickDamage(ev)
})
html.find('.traits-selector').click(ev => {
this.openTraitSelector(ev)
})
}
async _onClickDamage(ev) {
if (!ev.currentTarget.checked) { return }
let prop = ev.currentTarget.name;
let update = {};
update[prop] = ev.currentTarget.value - 1
this.item.update(update)
}
async openTraitSelector(ev) {
let selector = new TraitSelector(this.item);
selector.render(true)
}
}
-275
View File
@@ -1,275 +0,0 @@
import { onManageActiveEffect, prepareActiveEffectCategories } from "../system/effects.mjs";
import { VermineActorSheet } from "./actor-sheet.mjs";
import { TotemPicker, ActorPicker } from "../system/applications.mjs";
/**
* Extend the basic ActorSheet with some very simple modifications
* @extends {VermineActorSheet}
*/
export class VermineGroupSheet extends VermineActorSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["vermine2047", "sheet", "actor", "group"],
template: "systems/vermine2047/templates/actor/actor-sheet.hbs",
width: 700,
height: 600,
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description" }]
});
}
/** @override */
get template() {
return `systems/vermine2047/templates/actor/actor-${this.actor.type}-sheet.hbs`;
}
/* -------------------------------------------- */
/** @override */
getData() {
// Retrieve the data structure from the base sheet. You can inspect or log
// the context variable to see the structure, but some key properties for
// sheets are the actor object, the data object, whether or not it's
// editable, the items array, and the effects array.
const context = super.getData();
// Use a safe clone of the actor data for further operations.
const actorData = this.actor.toObject(false);
// Add the actor's data to context.data for easier access, as well as flags.
context.system = actorData.system;
context.flags = actorData.flags;
context.config = CONFIG.VERMINE;
// Prepare character data and items.
if (actorData.type == 'character') {
this._prepareItems(context);
this._prepareCharacterData(context);
}
// Prepare NPC data and items.
if (actorData.type == 'npc') {
this._prepareItems(context);
}
if (actorData.type == 'group') {
this._prepareItems(context);
this._prepareGroupData(context);
}
// Add roll data for TinyMCE editors.
context.rollData = this.actor.getRollData();
// Prepare active effects
context.effects = prepareActiveEffectCategories(this.actor.effects);
return context;
}
/**
* Organize and classify Items for Character sheets.
*
* @param {Object} actorData The actor to prepare.
*
* @return {undefined}
*/
_prepareCharacterData(context) {
// Handle ability scores.
for (let [k, v] of Object.entries(context.system.abilities)) {
v.label = game.i18n.localize(context.system.abilities[k].label) ?? k;
}
}
/**
* Prepare Group type specific data.
* Resolves member and encounter actor IDs to actual actor data.
*
* @param {Object} context The context data to prepare.
* @return {undefined}
*/
_prepareGroupData(context) {
if (this.actor.type !== 'group') return;
// Resolve member IDs to actor data
context.resolvedMembers = {};
if (context.system.members && context.system.members.length > 0) {
context.system.members.forEach(memberId => {
const actor = game.actors.get(memberId);
if (actor) {
context.resolvedMembers[memberId] = {
name: actor.name,
id: actor.id
};
}
});
}
// Resolve encounter IDs to actor data
context.resolvedEncounters = {};
if (context.system.encounters && context.system.encounters.length > 0) {
context.system.encounters.forEach(encounterId => {
const actor = game.actors.get(encounterId);
if (actor) {
context.resolvedEncounters[encounterId] = {
name: actor.name,
id: actor.id
};
}
});
}
// Set morale level based on dice value (rules: p. 68-69)
this._updateMoraleLevel(context);
}
/**
* Update morale level based on dice value.
* Rules: 7D+ = Haut, 6-3D = Normal, 2D- = Bas, 0D = Crise
*
* @param {Object} context The context data.
* @return {undefined}
*/
_updateMoraleLevel(context) {
const moraleValue = context.system.morale.value || 0;
// If level is already set, keep it
if (context.system.morale.level) return;
// Determine morale level based on dice value
if (moraleValue >= 7) {
context.system.morale.level = "high";
} else if (moraleValue >= 3) {
context.system.morale.level = "normal";
} else if (moraleValue >= 1) {
context.system.morale.level = "low";
} else {
context.system.morale.level = "crisis";
}
}
/**
* Organize and classify Items for Character sheets.
*
* @param {Object} actorData The actor to prepare.
*
* @return {undefined}
*/
_prepareItems(context) {
context.specialties = this.actor.itemTypes['specialty'];
context.backgrounds = this.actor.itemTypes['background'];
context.evolutions = this.actor.itemTypes['evolution'];
context.traumas = this.actor.itemTypes['trauma'];
context.gear = this.actor.itemTypes['item'];
context.weapons = this.actor.itemTypes['weapon'];
context.defenses = this.actor.itemTypes['defense'];
context.vehicles = this.actor.itemTypes['vehicle'];
context.totem_abilities = this.actor.itemTypes['ability'].filter(i => i.system.type === 'totem');
context.abilities = this.actor.itemTypes['ability'].filter(i => i.system.type !== 'totem');
context.members = this.actor.system.members;
context.encounters = this.actor.system.encounters;
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
// Choose Totem
html.find('.chooseTotem').click(this._onTotemButton.bind(this));
// Choose Members / Encounters
html.find('.chooseActor').click(this._onRoadButton.bind(this));
html.find('.member-delete').click(ev => {
const li = $(ev.currentTarget).parents("li.actor");
const actorId = li.data("actor-id");
const actorIdIndex = this.actor.system.members.indexOf(actorId);
if (actorIdIndex !== -1) {
this.actor.system.members.splice(actorIdIndex, 1);
}
this.actor.update({ "system.members": this.actor.system.members });
this.render(true);
});
html.find('.encounter-delete').click(ev => {
const li = $(ev.currentTarget).parents("li.actor");
const actorId = li.data("actor-id");
const actorIdIndex = this.actor.system.encounters.indexOf(actorId);
if (actorIdIndex !== -1) {
this.actor.system.encounters.splice(actorIdIndex, 1);
}
this.actor.update({ "system.encounters": this.actor.system.encounters });
this.render(true);
});
// Handle objective deletion
html.find('.objective-delete').click(ev => {
ev.preventDefault();
const btn = $(ev.currentTarget);
const type = btn.data("type"); // 'major' or 'minor'
const index = parseInt(btn.data("index"));
if (!isNaN(index)) {
const objectives = foundry.utils.duplicate(this.actor.system.objectives || { major: [], minor: [] });
objectives[type].splice(index, 1);
this.actor.update({ "system.objectives": objectives });
}
});
// Handle adding new objectives
html.find('.item-create[data-type="major_objective"], .item-create[data-type="minor_objective"]').click(ev => {
ev.preventDefault();
const btn = $(ev.currentTarget);
const type = btn.data("type") === "major_objective" ? "major" : "minor";
const objectives = foundry.utils.duplicate(this.actor.system.objectives || { major: [], minor: [] });
objectives[type].push("");
this.actor.update({ "system.objectives": objectives });
});
// Handle morale level change
html.find('select[name="system.morale.level"]').change(ev => {
const select = $(ev.currentTarget);
const level = select.val();
this.actor.update({ "system.morale.level": level });
});
}
/**
* Handle totem pick
* @param {Event} event The originating click event
* @private
*/
_onTotemButton(event) {
event.preventDefault();
const el = event.currentTarget;
// const dataset = el.dataset;
const totemPicker = new TotemPicker(el, this.actor);
totemPicker.render(true);
}
/**
* Handle actor pick
* @param {Event} event The originating click event
* @private
*/
_onRoadButton(event) {
event.preventDefault();
const el = event.currentTarget;
// const dataset = el.dataset;
const actorPicker = new ActorPicker(el, this.actor);
actorPicker.render(true);
}
}
-154
View File
@@ -1,154 +0,0 @@
import { onManageActiveEffect, prepareActiveEffectCategories } from "../system/effects.mjs";
import { VermineActorSheet } from "./actor-sheet.mjs";
import { TotemPicker } from "../system/applications.mjs";
/**
* Extend the basic ActorSheet for NPC type
* @extends {VermineActorSheet}
*/
export class VermineNpcSheet extends VermineActorSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["vermine2047", "sheet", "actor", "npc"],
template: "systems/vermine2047/templates/actor/actor-sheet.hbs",
width: 600,
height: 700,
tabs: [
{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "characteristics" }
]
});
}
/** @override */
get template() {
return `systems/vermine2047/templates/actor/actor-${this.actor.type}-sheet.hbs`;
}
/* -------------------------------------------- */
/** @override */
getData() {
// Retrieve the data structure from the base sheet.
const context = super.getData();
// Use a safe clone of the actor data for further operations.
const actorData = this.actor.toObject(false);
// Add the actor's data to context.data for easier access, as well as flags.
context.system = actorData.system;
context.flags = actorData.flags;
context.config = CONFIG.VERMINE;
// Prepare items for all actor types
this._prepareItems(context);
// Prepare NPC-specific data
if (actorData.type === 'npc') {
this._prepareNpcData(context);
}
// Add roll data for TinyMCE editors
context.rollData = this.actor.getRollData();
// Prepare active effects
context.effects = prepareActiveEffectCategories(this.actor.effects);
return context;
}
/**
* Prepare NPC specific data
*/
_prepareNpcData(context) {
// Calculate derived values from threat, experience, and role
const threat = CONFIG.VERMINE.npcThreatLevels[context.system.threat.value];
const experience = CONFIG.VERMINE.npcExperienceLevels[context.system.experience.value];
const role = CONFIG.VERMINE.npcRoleLevels[context.system.role.value];
// Add calculated values to context for easier access
context.threatData = threat;
context.experienceData = experience;
context.roleData = role;
// Set wound thresholds based on threat level
if (threat) {
context.system.minorWound.threshold = threat.minorWound || context.system.minorWound.threshold;
context.system.majorWound.threshold = threat.majorWound || context.system.majorWound.threshold;
context.system.deadlyWound.threshold = threat.deadlyWound || context.system.deadlyWound.threshold;
// Set max wounds
context.system.minorWound.max = threat.minorWound || context.system.minorWound.max;
context.system.majorWound.max = threat.majorWound || context.system.majorWound.max;
context.system.deadlyWound.max = threat.deadlyWound || context.system.deadlyWound.max;
}
// Set reserve max values based on role
if (role) {
context.system.attributes.effort.max = role.pools || context.system.attributes.effort.max;
context.system.attributes.self_control.max = role.reaction_bonus || context.system.attributes.self_control.max;
}
// Prepare abilities with labels
for (let [k, v] of Object.entries(context.system.abilities)) {
v.label = game.i18n.localize(CONFIG.VERMINE.abilities[k]) ?? k;
}
// Prepare skills with localized names
for (let [k, v] of Object.entries(context.system.skills)) {
const skillKey = `VERMINE.skill.${k}`;
v.name = game.i18n.localize(skillKey);
if (v.name === skillKey) {
// Fallback to key if no translation
v.name = k.charAt(0).toUpperCase() + k.slice(1);
}
}
// Prepare skill categories
for (let [k, v] of Object.entries(context.system.skill_categories)) {
if (k !== 'preferred') {
v.label = game.i18n.localize(v.label) ?? k;
}
}
}
/**
* Organize and classify Items for NPC sheets.
*
* @param {Object} context - The context to prepare.
*/
_prepareItems(context) {
context.gear = this.actor.itemTypes['item'];
context.weapons = this.actor.itemTypes['weapon'];
context.defenses = this.actor.itemTypes['defense'];
context.vehicles = this.actor.itemTypes['vehicle'];
context.abilities = this.actor.itemTypes['ability'];
context.specialties = this.actor.itemTypes['specialty'];
context.backgrounds = this.actor.itemTypes['background'];
context.traumas = this.actor.itemTypes['trauma'];
context.evolutions = this.actor.itemTypes['evolution'];
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
// Choose Totem
html.find('.chooseTotem').click(this._onTotemButton.bind(this));
}
/**
* Handle totem pick
* @param {Event} event - The originating click event
* @private
*/
_onTotemButton(event) {
event.preventDefault();
const el = event.currentTarget;
const totemPicker = new TotemPicker(el, this.actor);
totemPicker.render(true);
}
}
+1 -1
View File
@@ -251,7 +251,7 @@ VERMINE.skillCategories = {
} }
} }
VERMINE.sexes = { "male": "VERMINE.sexes.male", "female": "VERMINE.sexes.female" }; VERMINE.sexes = { "male": "SEXES.male", "female": "SEXES.female" };
VERMINE.totems = { VERMINE.totems = {
"human": "TOTEMS.human.name", "human": "TOTEMS.human.name",
+298 -567
View File
@@ -1,596 +1,327 @@
import { VermineUtils } from "../roll.mjs"; import { VermineUtils } from "../roll.mjs";
/** const { HandlebarsApplicationMixin } = foundry.applications.api;
* Dialog for rolling dice in Vermine2047.
* Handles dice pool calculation, modifiers, and roll execution.
*/
export default class RollDialog extends Dialog {
/** export default class RollDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
* Creates a new RollDialog instance.
* @param {Object} data - The data for the dialog #actor;
* @param {HTMLElement} html - The HTML content of the dialog
* @param {Object} options - The options for the dialog get title() {
* @param {Function} [close] - The callback function for closing the dialog return game.i18n.localize("VERMINE.roll");
*/ }
constructor(data, html, options, close = undefined) {
const conf = { static DEFAULT_OPTIONS = {
title: "jet de dés", classes: ["vermine-roll"],
content: html, tag: "form",
buttons: { window: {
roll: { icon: "fas fa-dice-d10",
icon: '<i class="fas fa-check"></i>', resizable: false
label: "Lancer !", },
callback: () => this._onRoll() position: {
}, width: 520,
cancel: { height: 600
icon: '<i class="fas fa-times"></i>', },
label: "Annuler", actions: {
callback: () => this.close() roll: RollDialog.#onRoll,
} cancel: RollDialog.#onCancel
},
close: close
};
super({ ...conf, ...data }, options);
// Store reference to close callback
this._closeCallback = close;
} }
};
/** static PARTS = {
* Creates a new RollDialog instance. main: { template: "systems/vermine2047/templates/dialogs/roll-dialog.hbs" }
* @param {Object} [data] - The data for the dialog };
* @param {string} [data.label] - Roll label
* @param {string} [data.rolltype] - Roll type
* @param {number} [data.NoD=1] - Number of dice
* @param {boolean} [data.Reroll=false] - Allow rerolls
* @param {string} [data.actorId] - Actor ID for the roll
* @returns {Promise<RollDialog|null>} The RollDialog instance or null if creation failed
*/
static async create(data = {
label: null,
rolltype: null,
NoD: 1,
Reroll: false,
actorId: game.user.character?.id ?? canvas.tokens.controlled[0]?.actor?.id
}) {
// Validate actorId
const actorId = data.actorId;
if (!actorId || typeof actorId !== 'string') {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'));
return null;
}
// Retrieve the actor data based on the actorId static async create(data = {}) {
data.actor = await game.actors.get(actorId); const actorId = data.actorId ?? game.user.character?.id ?? canvas.tokens.controlled[0]?.actor?.id;
if (!data.actor) { if (!actorId || typeof actorId !== "string") {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected')); ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
return null; return null;
}
data.availableSpecialties = data.actor.items.filter(item => item.type === "specialty");
data.availableItems = data.actor.items.filter(item => item.type === "item");
data.config = CONFIG.VERMINE;
// Define options for the dialog
const options = {
classes: ["vermineDialog"],
width: "fit-content",
height: 'fit-content',
zIndex: 99999
};
// Render the HTML template for the dialog
const html = await renderTemplate('systems/vermine2047/templates/dialogs/roll-dialog.hbs', data);
// Return a new RollDialog instance with the provided data, HTML, and options
return new RollDialog(data, html, options);
} }
const actor = await game.actors.get(actorId);
/** if (!actor) {
* Retrieves the default options for the RollDialog. ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
*/ return null;
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
focus: true,
classes: ["dialog vermine-roll"],
});
} }
/** return new RollDialog({ actor, label: data.label, rolltype: data.rolltype });
* Retrieves the data for the dialog. }
* @returns {Object} The context data for the dialog
*/
getData() {
// Get the context data from the superclass
const context = super.getData();
context.data = this.data;
context.config = CONFIG.VERMINE;
return context; constructor(options = {}) {
} super(options);
this.#actor = options.actor;
/** this.label = options.label ?? null;
* Prepares items for display. this.rolltype = options.rolltype ?? null;
* @returns {Array} Filtered list of items }
*/
prepareItems() {
return this.data.actor.items.filter(it => it.type === "item");
}
/**
* Prepares specialties for display.
* @returns {Array} Filtered list of specialties
*/
prepareSpecialties() {
return this.data.actor.items.filter(it => it.type === "specialty");
}
/**
* Activates event listeners for the dialog.
* @param {HTMLElement} html - The HTML element of the dialog.
*/
async activateListeners(html) {
// Activate event listeners from the superclass
super.activateListeners(html);
// Initialize UI elements
this._html = html;
// Retrieve roll data and set up event listeners
await this.getRollData();
// Set up event listeners for all roll-related inputs
const rollInputs = html.find('[data-roll]');
for (const inp of rollInputs) {
inp.addEventListener('change', this._onRollInputChange.bind(this));
}
this.displaySpecialties();
const selectAbil = html.find('#ability')[0];
// Set the maximum value for self control based on ability value
html.find("#self_control")[0].max = selectAbil.value;
selectAbil.addEventListener('change', this._onChangeAbility.bind(this));
const selfControl = html.find('#self_control')[0];
// Add event listener for self control changes
selfControl.addEventListener('change', this._onChangeSelfControl.bind(this));
// Set up difficulty change listener
html.find('#difficulty')[0].addEventListener('change', this._onDifficultyChange.bind(this));
// Set up handicap change listener
html.find('#handicap')[0].addEventListener('change', this._onHandicapChange.bind(this));
// Set up totem checkbox listeners
html.find('#human-totem')[0]?.addEventListener('change', this._onTotemChange.bind(this));
html.find('#adapted-totem')[0]?.addEventListener('change', this._onTotemChange.bind(this));
// Initial update of all UI elements
this._updateUI();
async _prepareContext() {
const actor = this.#actor;
return {
actor,
system: actor.system,
config: CONFIG.VERMINE,
label: this.label,
rollType: this.rolltype,
labelKey: this.label,
speakerId: actor.id,
ability: null,
help: false,
specialty: false,
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
availableItems: actor.items.filter(i => i.type === "item")
}; };
}
/** async _onRender(context, options) {
* Retrieves the roll data for the dialog. this.element.dataset.actorId = this.#actor.id;
* @param {Event} _ev - The event triggering the roll data retrieval (unused).
*/ for (const inp of this.element.querySelectorAll("[data-roll]")) {
getRollData(_ev) { inp.addEventListener("change", this.#onInputChange.bind(this));
// Calculate and store the roll data }
this.rollData = {
actor: this.data.actor, const ability = this.element.querySelector("#ability");
NoD: this.getDicePool(), if (ability) {
Reroll: this.getReroll(), ability.addEventListener("change", this.#onChangeAbility.bind(this));
difficulty: this.getDifficulty(), const selfControl = this.element.querySelector("#self_control");
handicap: this.getHandicap(), if (selfControl) selfControl.max = ability.value;
rollType: this.getRollType(), }
rollLabel: this.getLabel(),
totems: this.getTotems(), const selfControl = this.element.querySelector("#self_control");
self_control: this.getSelfControl(), if (selfControl) {
max_effort: this.getMaxEffort(), selfControl.addEventListener("change", this.#onChangeSelfControl.bind(this));
keepTotem: this.getKeepTotem(), }
skillCategory: this.getSkillCategory()
}; this.element.querySelector("#difficulty")?.addEventListener("change", () => this.#updateUI());
this.displaySpecialties(); this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI());
this._updateUI(); this.element.querySelector("#human-totem")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#adapted-totem")?.addEventListener("change", () => this.#updateUI());
this.#displaySpecialties();
this.#updateUI();
if (ability?.value !== "0") {
this.element.querySelector("#self_control")?.dispatchEvent(new Event("change"));
}
}
// ── Getters ──────────────────────────────────────────────────────────
get #el() { return this.element; }
#getAbility() { return this.#el.querySelector("#ability"); }
#getSkill() { return this.#el.querySelector("#skill"); }
#getDifficulty() { return this.#el.querySelector("#difficulty"); }
#getHandicap() { return this.#el.querySelector("#handicap"); }
#getSelfCtrl() { return this.#el.querySelector("#self_control"); }
getDicePool() {
const abil = this.#getAbility();
const abilVal = 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 helped = this.#el.querySelector("#helped")?.checked;
const tools = this.#el.querySelector("input[name='usingTools']:checked")?.value !== "0";
const bonuses = (specChecked ? 1 : 0) + (helped ? 1 : 0) + (tools ? 1 : 0);
return (abilVal + sc + skillPool + bonuses) || 0;
}
getDifficultySelect() {
const sel = this.#getDifficulty();
const idx = sel?.selectedIndex ?? 0;
return parseInt(sel?.options[idx]?.value, 10) || 7;
}
getReroll() {
const sel = this.#getSkill();
const idx = sel?.selectedIndex ?? 0;
return parseInt(sel?.options[idx]?.dataset?.reroll, 10) || 0;
}
getHandicapSelect() {
const sel = this.#getHandicap();
return parseInt(sel?.value, 10) || 1;
}
getSkillCategory() {
const sel = this.#getSkill();
const idx = sel?.selectedIndex ?? 0;
return sel?.options[idx]?.dataset?.category ?? null;
}
getSkillLevel() {
const sel = this.#getSkill();
const idx = sel?.selectedIndex ?? 0;
const val = sel?.options[idx]?.value;
return val ? parseInt(val, 10) : null;
}
hasSpecialtySelected() {
const checked = this.#el.querySelector("input[name='usingSpecialization']:checked");
return checked && checked.value !== "aucune";
}
getRollType() {
const sel = this.#getSkill();
return sel?.value ? "skill" : "ability";
}
getLabel() {
const type = this.getRollType();
if (type === "skill") {
const sel = this.#getSkill();
const idx = sel?.selectedIndex ?? 0;
return sel?.options[idx]?.dataset?.label ?? "";
}
const sel = this.#getAbility();
const idx = sel?.selectedIndex ?? 0;
return sel?.options[idx]?.dataset?.label ?? "";
}
getSelfControl() {
return parseInt(this.#getSelfCtrl()?.value, 10) || 0;
}
getMaxEffort() {
const sel = this.#getAbility();
return parseInt(sel?.value, 10) || 0;
}
getTotems() {
return {
human: this.#el.querySelector("#human-totem")?.checked ?? false,
adapted: this.#el.querySelector("#adapted-totem")?.checked ?? false
}; };
}
/** getKeepTotem() {
* Gets the selected skill category return this.#el.querySelector("#keep-totem-select")?.value ?? null;
* @returns {string|null} - The skill category }
*/
getSkillCategory() { // ── UI ───────────────────────────────────────────────────────────────
const html = this.element[0];
const skillSelect = html.querySelector('#skill'); #displaySpecialties() {
if (skillSelect && skillSelect.selectedIndex > 0) { for (const el of this.#el.querySelectorAll("[data-spec-skill]")) {
const selectedOption = skillSelect.options[skillSelect.selectedIndex]; el.style.display = "inline";
return selectedOption.dataset.category || null; }
} }
return null;
#calculateBonusCount() {
let b = 0;
if (this.#el.querySelector("#helped")?.checked) b += 1;
b += parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
b += parseInt(this.#getSelfCtrl()?.value, 10) || 0;
const tools = this.#el.querySelector("input[name='usingTools']:checked");
if (tools && tools.value !== "0") b += 1;
const human = this.#el.querySelector("#human-totem");
if (human?.checked) b += parseInt(this.#actor?.system?.adaptation?.totems?.human?.value, 10) || 0;
const adapted = this.#el.querySelector("#adapted-totem");
if (adapted?.checked) b += parseInt(this.#actor?.system?.adaptation?.totems?.adapted?.value, 10) || 0;
if (this.hasSpecialtySelected()) b += 1;
return b;
}
#updateUI() {
const total = this.getDicePool();
const totalEl = this.#el.querySelector("#dice-pool-total");
if (totalEl) totalEl.textContent = `${total}D`;
const bonusEl = this.#el.querySelector("#total-bonus");
if (bonusEl) bonusEl.textContent = this.#calculateBonusCount();
const diffSel = this.#getDifficulty();
const diffEl = this.#el.querySelector("#current-difficulty");
if (diffEl && diffSel) {
const idx = diffSel.selectedIndex;
const val = diffSel.options[idx].value;
const lbl = diffSel.options[idx].text.split(" ")[0];
diffEl.textContent = `${lbl} (${val})`;
} }
/** const handSel = this.#getHandicap();
* Gets the selected skill level const handEl = this.#el.querySelector("#current-handicap");
* @returns {number|null} - The skill level if (handEl && handSel) {
*/ handEl.textContent = handSel.options[handSel.selectedIndex].text;
getSkillLevel() {
const html = this.element[0];
const skillSelect = html.querySelector('#skill');
if (skillSelect && skillSelect.selectedIndex > 0) {
const selectedOption = skillSelect.options[skillSelect.selectedIndex];
return parseInt(selectedOption.value) || null;
}
return null;
} }
/** const abilSel = this.#getAbility();
* Checks if a specialty is selected const abilValEl = this.#el.querySelector("#abilityScoreValue");
* @returns {boolean} - True if a specialty is selected if (abilSel && abilValEl) {
*/ const idx = abilSel.selectedIndex;
hasSpecialtySelected() { abilValEl.textContent = idx > 0 ? abilSel.options[idx].value : "0";
const html = this.element[0];
const specialtyRadio = html.querySelector('input[name="usingSpecialization"]:checked');
return specialtyRadio && specialtyRadio.value !== 'aucune';
} }
/** const specChecked = this.#el.querySelector("input[name='usingSpecialization']:checked");
* Handles changes to roll inputs and updates UI. const specEl = this.#el.querySelector(".current-specialty");
* @param {Event} ev - The change event. if (specEl && specChecked) {
*/ specEl.textContent = specChecked.value === "aucune"
_onRollInputChange(ev) { ? game.i18n.localize("VERMINE.none")
this.getRollData(ev); : specChecked.value;
}
}
// ── Event handlers ───────────────────────────────────────────────────
#onInputChange() {
this.#updateUI();
}
#onChangeAbility(ev) {
const sel = ev.currentTarget;
const score = sel.options[sel.selectedIndex]?.value ?? "0";
const scoreEl = this.#el.querySelector("#abilityScore");
if (scoreEl) scoreEl.value = score;
const sc = this.#getSelfCtrl();
if (sc) sc.max = score;
this.#updateUI();
}
#onChangeSelfControl(ev) {
const valEl = this.#el.querySelector("#self_control_value");
if (valEl) valEl.textContent = ev.currentTarget.value;
}
static async #onCancel(event, target) {
this.close();
}
static async #onRoll(event, target) {
const selfCtrl = this.getSelfControl();
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 abilityVal = this.#el.querySelector('[name="ability"]')?.value;
* Updates all UI elements based on current roll data if (!abilityVal || abilityVal === "0") {
*/ ui.notifications.warn(game.i18n.localize("VERMINE.error_select_ability"));
_updateUI() { return;
if (!this._html) return;
const html = this._html[0];
// Update total dice pool display
const totalDice = this.getDicePool();
const totalEl = html.querySelector('#dice-pool-total');
if (totalEl) {
totalEl.textContent = `${totalDice}D`;
}
// Update bonus count
const bonusCount = this._calculateBonusCount();
const bonusEl = html.querySelector('#total-bonus');
if (bonusEl) {
bonusEl.textContent = bonusCount;
}
// Update difficulty display
const difficultyEl = html.querySelector('#current-difficulty');
const difficultySelect = html.querySelector('#difficulty');
if (difficultyEl && difficultySelect) {
const selectedIndex = difficultySelect.selectedIndex;
const diffValue = parseInt(difficultySelect.options[selectedIndex].value);
const diffLabel = difficultySelect.options[selectedIndex].text.split(' ')[0];
difficultyEl.textContent = `${diffLabel} (${diffValue})`;
}
// Update handicap display
const handicapEl = html.querySelector('#current-handicap');
const handicapSelect = html.querySelector('#handicap');
if (handicapEl && handicapSelect) {
const selectedIndex = handicapSelect.selectedIndex;
handicapEl.textContent = handicapSelect.options[selectedIndex].text;
}
// Update ability score display
const abilSelect = html.querySelector('#ability');
const abilScoreEl = html.querySelector('#abilityScoreValue');
if (abilSelect && abilScoreEl) {
const selectedIndex = abilSelect.selectedIndex;
if (selectedIndex > 0) {
abilScoreEl.textContent = abilSelect.options[selectedIndex].value;
} else {
abilScoreEl.textContent = '0';
}
}
// Update specialty display
const specialtyRadios = html.querySelectorAll('input[name="usingSpecialization"]:checked');
const currentSpecEl = html.querySelector('.current-specialty');
if (currentSpecEl && specialtyRadios.length > 0) {
const checkedRadio = specialtyRadios[0];
currentSpecEl.textContent = checkedRadio.value === 'aucune' ? game.i18n.localize('VERMINE.none') : checkedRadio.value;
}
} }
/** if (selfCtrl > 0) {
* Calculates the bonus count for display. const newVal = this.#actor.system.attributes.self_control.value - selfCtrl;
* @returns {number} Total bonus dice. await this.#actor.update({ "system.attributes.self_control.value": newVal });
*/
_calculateBonusCount() {
let bonus = 0;
// Help bonus
if (this._html?.find('#helped')[0]?.checked) {
bonus += 1;
}
// Group bonus
const groupValue = parseInt(this._html?.find('#group')[0]?.value, 10) || 0;
bonus += groupValue;
// Self control bonus
const selfControlValue = parseInt(this._html?.find('#self_control')[0]?.value, 10) || 0;
bonus += selfControlValue;
// Tools bonus
const toolsChecked = this._html?.find('input[name="usingTools"]:checked')[0]?.value !== '0';
if (toolsChecked) {
bonus += 1;
}
// Totems bonus
if (this._html?.find('#human-totem')[0]?.checked) {
bonus += parseInt(this.data.actor?.system?.adaptation?.totems?.human?.value, 10) || 0;
}
if (this._html?.find('#adapted-totem')[0]?.checked) {
bonus += parseInt(this.data.actor?.system?.adaptation?.totems?.adapted?.value, 10) || 0;
}
// Specialty bonus
const specialtyChecked = this._html?.find('input[name="usingSpecialization"]:checked')[0]?.value !== 'aucune';
if (specialtyChecked) {
bonus += 1;
}
return bonus;
} }
/** await VermineUtils.roll({
* Handles difficulty change actor: this.#actor,
* @param {Event} ev - The change event NoD: this.getDicePool(),
*/ Reroll: this.getReroll(),
_onDifficultyChange(ev) { difficulty: this.getDifficultySelect(),
this._updateUI(); handicap: this.getHandicapSelect(),
} 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()
});
/** this.close();
* Handles handicap change }
* @param {Event} ev - The change event
*/
_onHandicapChange(ev) {
this._updateUI();
}
/**
* Handles totem checkbox change
* @param {Event} ev - The change event
*/
_onTotemChange(ev) {
this._updateUI();
}
/**
* Gets the selected totem to keep (for dual totem rolls)
* @returns {string|null} - The totem to keep ('human', 'adapted', or null)
*/
getKeepTotem() {
const keepTotemSelect = this._html?.find('#keep-totem-select')[0];
if (keepTotemSelect) {
return keepTotemSelect.value;
}
// Default to null (both totems used)
return null;
}
/**
* Handles the change in self control value.
* @param {Event} ev - The event triggering the change in self control value.
*/
_onChangeSelfControl(ev) {
const html = this.element[0];
const selfControlValueElement = html.querySelector('#self_control_value');
if (selfControlValueElement) {
selfControlValueElement.innerText = ev.currentTarget.value;
}
}
/**
* Retrieves the handicap value from the HTML element.
* @returns {number} The handicap value.
*/
getHandicap() {
const html = this.element[0];
const handicapValue = html.querySelector('#handicap')?.value ?? '1';
return parseInt(handicapValue, 10);
}
/**
* Gets the roll type (ability or skill).
* @returns {string} The roll type: 'skill' or 'ability'.
*/
getRollType() {
const html = this.element[0];
return html.querySelector('select#skill')?.value ? "skill" : "ability";
}
/**
* Gets the label for the roll.
* @returns {string} The roll label.
*/
getLabel() {
const html = this.element[0];
const rollType = this.getRollType();
if (rollType === "skill") {
const skillSelect = html.querySelector('select#skill');
const selectedIndex = skillSelect?.selectedIndex ?? 0;
return skillSelect?.options[selectedIndex]?.dataset?.label ?? "";
}
const abilitySelect = html.querySelector('select#ability');
const selectedIndex = abilitySelect?.selectedIndex ?? 0;
return abilitySelect?.options[selectedIndex]?.dataset?.label ?? "";
}
/**
* Displays specialties related to the selected skill.
*/
displaySpecialties() {
const specialties = this.element[0]?.querySelectorAll('[data-spec-skill]');
if (specialties) {
specialties.forEach(specEl => {
specEl.style.display = "inline";
});
}
}
/**
* Retrieves the self control value from the HTML element.
* @returns {number} The self control value.
*/
getSelfControl() {
const html = this.element[0];
const selfControlValue = html.querySelector('#self_control')?.value ?? '0';
return parseInt(selfControlValue, 10);
}
/**
* Retrieves the maximum effort value from the HTML element.
* @returns {number} The maximum effort value.
*/
getMaxEffort() {
const html = this.element[0];
const abilityValue = html.querySelector('#ability')?.value ?? '0';
return parseInt(abilityValue, 10);
}
/**
* Retrieves the selected totems from the HTML element.
* @returns {Object} An object containing the selected totems {human: boolean, adapted: boolean}.
*/
getTotems() {
const html = this.element[0];
return {
human: html.querySelector('#human-totem')?.checked ?? false,
adapted: html.querySelector('#adapted-totem')?.checked ?? false
};
}
/**
* Handles the change in ability value.
* @param {Event} ev - The event triggering the change in ability value.
*/
_onChangeAbility(ev) {
const html = this.element[0];
const abilitySelect = html.querySelector('#ability');
const selectedIndex = abilitySelect?.selectedIndex ?? 0;
const score = abilitySelect?.options[selectedIndex]?.value ?? '0';
const scoreElement = html.querySelector('#abilityScore');
if (scoreElement) {
scoreElement.value = score;
}
const selfControlElement = html.querySelector('#self_control');
if (selfControlElement) {
selfControlElement.max = score;
}
}
/**
* Retrieves the total dice pool based on various factors.
* @returns {number} The total dice pool value.
*/
getDicePool() {
// Retrieve the HTML element
const html = this.element[0];
// Safely get ability value
const abilitySelect = html.querySelector('#ability');
const abilValue = abilitySelect?.options[abilitySelect?.selectedIndex]?.value ?? 0;
// Safely get skill value and pool
const skillSelect = html.querySelector('#skill');
const skillOption = skillSelect?.options[skillSelect?.selectedIndex];
const skillValue = skillOption?.dataset?.pool ?? 0;
// Get the self control value
const selfControl = html.querySelector('#self_control')?.value ?? 0;
// Calculate bonuses based on certain conditions
const bonuses =
(html.querySelector('#usingSpecialization')?.checked ? 1 : 0) +
(html.querySelector('#helped')?.checked ? 1 : 0) +
(html.querySelector('#usingTools')?.checked ? 1 : 0);
// Calculate the total dice pool
const total = parseInt(abilValue, 10) + parseInt(selfControl, 10) + parseInt(skillValue, 10) + bonuses;
return total || 0;
}
/**
* Retrieves the reroll value based on selected skill.
* @returns {number} The reroll value.
*/
getReroll() {
const html = this.element[0];
const skillSelect = html.querySelector('#skill');
const selectedIndex = skillSelect?.selectedIndex ?? 0;
const rerollValue = skillSelect?.options[selectedIndex]?.dataset?.reroll ?? '0';
return parseInt(rerollValue, 10) || 0;
}
/**
* Retrieves the difficulty value based on selected option.
* @returns {number} The difficulty value.
*/
getDifficulty() {
const html = this.element[0];
const difficultySelect = html.querySelector('#difficulty');
const selectedIndex = difficultySelect?.selectedIndex ?? 0;
const diffValue = difficultySelect?.options[selectedIndex]?.value ?? '0';
return parseInt(diffValue, 10) || 0;
}
/**
* Performs a dice roll based on the roll data and handles self control checks.
* @returns {Promise<Roll|false>} A promise that resolves with the Roll result or false if cancelled.
*/
async _onRoll() {
// Check if self control is required for the roll
if (this.rollData.self_control > 0) {
// Check if the actor has enough self control
const currentSelfControl = this.rollData.actor?.system?.attributes?.self_control?.value ?? 0;
if (currentSelfControl < this.rollData.self_control) {
// Display a warning message if self control is insufficient
ui.notifications.warn(game.i18n.localize('VERMINE.error_not_enough_self_control'));
// Re-render the dialog
this.render(true);
return false; // Exit the function if self control is insufficient
}
}
const caracName = this.element[0]?.querySelector('[name="ability"]')?.value;
if (caracName === "0" || caracName === undefined) {
// Display a warning message if no ability selected
ui.notifications.warn(game.i18n.localize('VERMINE.error_select_ability'));
// Re-render the dialog
this.render(true);
return false; // Exit the function if no ability
}
// Deduct self control points if necessary
if (this.rollData.self_control > 0) {
const newSelfControl = this.rollData.actor.system.attributes.self_control.value - this.rollData.self_control;
// Update the actor's self control value
await this.rollData.actor.update({
"system.attributes.self_control.value": newSelfControl
});
}
// Perform the dice roll using VermineUtils
return VermineUtils.roll({
...this.rollData,
skillLevel: this.getSkillLevel(),
hasSpecialty: this.hasSpecialtySelected()
});
}
} }
+2 -2
View File
@@ -291,7 +291,7 @@ export class VermineFight {
console.log(data); console.log(data);
// render template // render template
let html = await renderTemplate(data._template, data); let html = await foundry.applications.handlebars.renderTemplate(data._template, data);
let ui = new Dialog({ let ui = new Dialog({
title: game.i18n.localize("VERMINE.FightTool"), title: game.i18n.localize("VERMINE.FightTool"),
@@ -415,7 +415,7 @@ export class VermineCombat extends Combat {
} }
} }
export class VermineCombatTracker extends CombatTracker { export class VermineCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
get template() { get template() {
return "systems/vermine2047/templates/combat-tracker.hbs"; return "systems/vermine2047/templates/combat-tracker.hbs";
+2 -1
View File
@@ -4,7 +4,7 @@
* @return {Promise} * @return {Promise}
*/ */
export const preloadHandlebarsTemplates = async function () { export const preloadHandlebarsTemplates = async function () {
return loadTemplates([ return foundry.applications.handlebars.loadTemplates([
// Actor partials. // Actor partials.
@@ -31,6 +31,7 @@ export const preloadHandlebarsTemplates = async function () {
// npc partials // npc partials
"systems/vermine2047/templates/actor/npc/npc-combat.hbs", "systems/vermine2047/templates/actor/npc/npc-combat.hbs",
"systems/vermine2047/templates/actor/parts/npc-skill-category.hbs",
// creature partials // creature partials
"systems/vermine2047/templates/actor/creature/creature-combat.hbs", "systems/vermine2047/templates/actor/creature/creature-combat.hbs",
+5 -5
View File
@@ -38,16 +38,16 @@ export const registerHooks = function () {
}); });
Hooks.on('renderChatMessage', async (message, html, data) => { Hooks.on('renderChatMessageHTML', async (message, html, data) => {
let rerollTitle = html[0].querySelector(".reroll-fromroll h4"); let rerollTitle = html.querySelector(".reroll-fromroll h4");
if (rerollTitle) { if (rerollTitle) {
rerollTitle.addEventListener("click", () => { html[0].querySelector(".reroll").classList.toggle('visible') }) rerollTitle.addEventListener("click", () => { html.querySelector(".reroll").classList.toggle('visible') })
} }
if (message.author?._id != game.user._id || !game.user.isGM) { if (message.author?._id != game.user._id || !game.user.isGM) {
// désactiver les inputs pour les joueurs non-auteurs du message // désactiver les inputs pour les joueurs non-auteurs du message
html[0].querySelectorAll("input").forEach(inp => inp.disabled = true); html.querySelectorAll("input").forEach(inp => inp.disabled = true);
//cacher le boutton reroll //cacher le boutton reroll
html[0].querySelectorAll("div.reroll-from-effort").forEach(el => el.style.display = "none") html.querySelectorAll("div.reroll-from-effort").forEach(el => el.style.display = "none")
return return
} }
await VermineUtils.chatListenners(html) await VermineUtils.chatListenners(html)
+1 -1
View File
@@ -468,7 +468,7 @@ export class VermineUtils {
* @returns {Promise<ChatMessage>} The created chat message * @returns {Promise<ChatMessage>} The created chat message
*/ */
static async diplayChatRoll(roll, param) { static async diplayChatRoll(roll, param) {
const content = await renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param }); const content = await foundry.applications.handlebars.renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param });
const chatData = { const chatData = {
user: game.user?._id, user: game.user?._id,
speaker: ChatMessage.getSpeaker(), speaker: ChatMessage.getSpeaker(),
+1 -1
View File
@@ -48,7 +48,7 @@ class CreateActorDialog extends FormApplication {
}); });
} }
} }
class VermineTour extends Tour { class VermineTour extends foundry.nue.Tour {
/** @override */ /** @override */
async _preStep() { async _preStep() {
var _a2, _b, _c, _d, _e; var _a2, _b, _c, _d, _e;
+127 -51
View File
@@ -3,19 +3,17 @@ import { registerSettings } from "./system/settings.mjs";
import { GroupLink } from "./system/group-link.mjs"; import { GroupLink } from "./system/group-link.mjs";
// Import document classes. // Import document classes.
import { VermineActor } from "./documents/actor.mjs"; import * as documents from "./documents/_module.mjs";
import { VermineCharacterSheet } from "./sheets/character-sheet.mjs";
import { VermineNpcSheet } from "./sheets/npc-sheet.mjs";
import { VermineGroupSheet } from "./sheets/npc-group.mjs";
import { VermineCreatureSheet } from "./sheets/creature-sheet.mjs";
import { VermineItem } from "./documents/item.mjs";
import { VermineItemSheet } from "./sheets/item-sheet.mjs";
import { VermineUtils } from "./system/roll.mjs"; import { VermineUtils } from "./system/roll.mjs";
import { VermineCombat, VermineCombatant, VermineCombatTracker } from "./system/fight.mjs"; import { VermineCombat, VermineCombatant, VermineCombatTracker } from "./system/fight.mjs";
// Import DataModels
import * as models from "./models/_module.mjs"
// Import ApplicationV2 sheets
import * as sheets from "./applications/sheets/_module.mjs"
// Import helper/utility classes and constants. // Import helper/utility classes and constants.
import { preloadHandlebarsTemplates, registerHandlebarsHelpers } from "./system/handlebars-manager.mjs"; import { preloadHandlebarsTemplates, registerHandlebarsHelpers } from "./system/handlebars-manager.mjs";
import { VERMINE } from "./system/config.mjs"; import { VERMINE } from "./system/config.mjs";
@@ -26,51 +24,109 @@ import { VERMINE } from "./system/config.mjs";
Hooks.once('init', async function () { Hooks.once('init', async function () {
// Add utility classes to the global game object so that they're more easily // System stylesheet is automatically loaded by Foundry from system.json
// accessible in global contexts. // No need to manually inject it - this was causing MIME type issues
game.vermine2047 = { // If you need to ensure fresh CSS, use cache-busting in the filename or system.json version
VermineActor,
VermineItem,
VermineUtils,
VermineCombat,
GroupLink
};
// Register GroupLink hooks for automatic synchronization // Register GroupLink hooks for automatic synchronization
GroupLink.registerHooks(); GroupLink.registerHooks();
// Define custom Document classes // Register ALL DataModels FIRST - this is crucial for Foundry V2
CONFIG.Actor.documentClass = VermineActor; // Use individual assignments like Celestopol for compatibility
CONFIG.Item.documentClass = VermineItem; CONFIG.Actor.dataModels.character = models.VermineCharacterData;
CONFIG.Actor.dataModels.npc = models.VermineNpcData;
CONFIG.Actor.dataModels.group = models.VermineGroupData;
CONFIG.Actor.dataModels.creature = models.VermineCreatureData;
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.specialty = models.VermineSpecialtyData;
CONFIG.Item.dataModels.background = models.VermineBackgroundData;
CONFIG.Item.dataModels.trauma = models.VermineTraumaData;
CONFIG.Item.dataModels.evolution = models.VermineEvolutionData;
CONFIG.Item.dataModels.rumor = models.VermineRumorData;
CONFIG.Item.dataModels.target = models.VermineTargetData;
CONFIG.Item.dataModels.rite = models.VermineRiteData;
// Define custom Document classes AFTER ALL DataModels are registered
CONFIG.Actor.documentClass = documents.VermineActor;
CONFIG.Item.documentClass = documents.VermineItem;
// Add utility classes to the global game object so that they're more easily
// accessible in global contexts.
// Note: Do NOT expose Document classes here as it can cause issues with DataModel initialization
game.vermine2047 = {
VermineUtils,
VermineCombat,
GroupLink
};
CONFIG.ui.combat = VermineCombatTracker; CONFIG.ui.combat = VermineCombatTracker;
CONFIG.Combatant.documentClass = VermineCombatant; CONFIG.Combatant.documentClass = VermineCombatant;
CONFIG.Combat.documentClass = VermineCombat; CONFIG.Combat.documentClass = VermineCombat;
// Register sheet application classes // Register sheet application classes (ApplicationV2)
Actors.unregisterSheet("core", ActorSheet); // Unregister core sheets
Actors.registerSheet('vermine2047', VermineCharacterSheet, { foundry.applications.sheets.ActorSheetV2?.unregisterSheet?.("core", "Actor", {
types: ['character'], types: ["character", "npc", "group", "creature"]
makeDefault: true, })
}); foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1?.sheets?.ItemSheet)
Actors.registerSheet('vermine2047', VermineNpcSheet, { // Actor sheets
types: ['npc'], foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineCharacterSheetV2, {
makeDefault: true, types: ["character"], makeDefault: true, label: "VERMINE.Sheet.character"
}); })
foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineNpcSheetV2, {
types: ["npc"], makeDefault: true, label: "VERMINE.Sheet.npc"
})
foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineCreatureSheetV2, {
types: ["creature"], makeDefault: true, label: "VERMINE.Sheet.creature"
})
foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineGroupSheetV2, {
types: ["group"], makeDefault: true, label: "VERMINE.Sheet.group"
})
Actors.registerSheet('vermine2047', VermineCreatureSheet, { // Item sheets — un par type
types: ['creature'], foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineItemSheetV2, {
makeDefault: true, types: ["item"], makeDefault: true
}); })
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineWeaponSheetV2, {
Actors.registerSheet('vermine2047', VermineGroupSheet, { types: ["weapon"], makeDefault: true
types: ['group'], })
makeDefault: true, foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineDefenseSheetV2, {
}); types: ["defense"], makeDefault: true
Items.unregisterSheet("core", ItemSheet); })
Items.registerSheet("vermine2047", VermineItemSheet, { 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.VermineSpecialtySheetV2, {
types: ["specialty"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineBackgroundSheetV2, {
types: ["background"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineTraumaSheetV2, {
types: ["trauma"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineEvolutionSheetV2, {
types: ["evolution"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineRumorSheetV2, {
types: ["rumor"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineTargetSheetV2, {
types: ["target"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineRiteSheetV2, {
types: ["rite"], makeDefault: true
})
registerHandlebarsHelpers(); // Register Handlebars helpers registerHandlebarsHelpers(); // Register Handlebars helpers
registerHooks(); // register Hooks registerHooks(); // register Hooks
@@ -79,13 +135,8 @@ Hooks.once('init', async function () {
// Add custom constants for configuration. // Add custom constants for configuration.
CONFIG.VERMINE = VERMINE; CONFIG.VERMINE = VERMINE;
// Set up model templates - must be done after system templates are loaded // Les DataModels sont déjà enregistrés dans CONFIG.Actor.dataModels et
if (game.system?.template?.Actor && game.system?.template?.Item) { // CONFIG.Item.dataModels. On expose leurs définitions pour compatibilité.
CONFIG.VERMINE.model = {
Actor: game.system.template.Actor,
Item: game.system.template.Item
};
}
/** /**
* Set an initiative formula for the system * Set an initiative formula for the system
@@ -116,8 +167,33 @@ Hooks.once('init', async function () {
document.querySelector('#ui-left').prepend(el); document.querySelector('#ui-left').prepend(el);
// Preload Handlebars templates. // Preload templates (ApplicationV2 + legacy)
return preloadHandlebarsTemplates(); await preloadHandlebarsTemplates();
await foundry.applications.handlebars.loadTemplates([
"systems/vermine2047/templates/actor/appv2/character-header.hbs",
"systems/vermine2047/templates/actor/appv2/character-main.hbs",
"systems/vermine2047/templates/actor/appv2/character-abilities.hbs",
"systems/vermine2047/templates/actor/appv2/character-totem.hbs",
"systems/vermine2047/templates/actor/appv2/character-equipment.hbs",
"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-combat.hbs",
"systems/vermine2047/templates/actor/appv2/npc-notes.hbs",
"systems/vermine2047/templates/actor/appv2/group-main.hbs",
"systems/vermine2047/templates/actor/appv2/group-info.hbs",
"systems/vermine2047/templates/actor/appv2/group-gear.hbs",
"systems/vermine2047/templates/actor/appv2/group-road.hbs",
"systems/vermine2047/templates/actor/appv2/group-reserve.hbs",
"systems/vermine2047/templates/actor/appv2/creature-main.hbs",
"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"
]);
}); });
+3883 -106
View File
File diff suppressed because it is too large Load Diff
+20 -4
View File
@@ -7,18 +7,34 @@
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"launch_Foundry12": "sudo node /home/rwan/foundry/foundry_software/FoundryVTT-12.331/resources/app/main.js", "launch_Foundry12": "sudo node /home/rwan/foundry/foundry_software/FoundryVTT-12.331/resources/app/main.js",
"watch": "gulp watch", "watch": "gulp watch",
"buildStyle": "gulp buildStyles", "build:less": "gulp buildLess",
"build:less:dev": "gulp buildLessDev",
"build:css": "gulp buildCSS",
"build:all-css": "gulp buildAllCSS",
"build": "npm run build:less",
"dev": "npm run launch_Foundry12 & xdg-open http://localhost:30000/", "dev": "npm run launch_Foundry12 & xdg-open http://localhost:30000/",
"clean:css": "rm -f css/vermine2047.*.css",
"rebuild:less": "npm run clean:css && npm run build:less",
"rebuild:css": "npm run clean:css && npm run build:all-css",
"pushLDBtoYAML": "node ./tools/pushLDBtoYAML.mjs", "pushLDBtoYAML": "node ./tools/pushLDBtoYAML.mjs",
"pullYAMLtoLDB": "node ./tools/pullYAMLtoLDB.mjs" "pullYAMLtoLDB": "node ./tools/pullYAMLtoLDB.mjs",
"lint:less": "lesshint less/**/*.less"
}, },
"author": "Rwan", "author": "Rwan",
"devDependencies": { "devDependencies": {
"@foundryvtt/foundryvtt-cli": "^1.0.2", "@foundryvtt/foundryvtt-cli": "^1.0.2",
"@typescript-eslint/eslint-plugin": "^8.60.1",
"@typescript-eslint/parser": "^8.60.1",
"@typhonjs-fvtt/eslint-config-foundry.js": "^0.8.0", "@typhonjs-fvtt/eslint-config-foundry.js": "^0.8.0",
"browser-sync": "^3.0.3", "browser-sync": "^3.0.3",
"eslint": "^8.57.1",
"eslint-plugin-html": "^8.1.4",
"eslint-plugin-import": "^2.32.0",
"gulp": "^5.0.0", "gulp": "^5.0.0",
"gulp-sass": "^5.1.0", "gulp-autoprefixer": "^8.0.0",
"sass": "^1.55.0" "gulp-clean-css": "^4.3.0",
"gulp-less": "^5.0.0",
"gulp-rename": "^2.0.0",
"less": "^4.2.0"
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

+31 -7
View File
@@ -4,11 +4,13 @@
"description": "The Vermine 2047 system for FoundryVTT!", "description": "The Vermine 2047 system for FoundryVTT!",
"version": "0.1.13", "version": "0.1.13",
"compatibility": { "compatibility": {
"minimum": "11", "minimum": "14",
"verified": "14.0", "verified": "14"
"maximum": "14"
}, },
"authors": [ "authors": [
{
"name": "LeRatierBretonnien/Uberwald"
},
{ {
"name": "François-Xavier Guillois" "name": "François-Xavier Guillois"
}, },
@@ -25,8 +27,30 @@
"module/vermine2047.mjs" "module/vermine2047.mjs"
], ],
"styles": [ "styles": [
"css/vermine2047.css" "css/vermine2047.min.css"
], ],
"documentTypes": {
"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"] }
},
"Item": {
"item": { "htmlFields": ["description"] },
"weapon": { "htmlFields": ["description"] },
"defense": { "htmlFields": ["description"] },
"vehicle": { "htmlFields": ["description"] },
"ability": { "htmlFields": ["description"] },
"specialty": {},
"background": { "htmlFields": ["description"] },
"trauma": { "htmlFields": ["description"] },
"evolution": { "htmlFields": ["description"] },
"rumor": { "htmlFields": ["description"] },
"target": { "htmlFields": ["description"] },
"rite": { "htmlFields": ["description"] }
}
},
"languages": [ "languages": [
{ {
"lang": "en", "lang": "en",
@@ -144,9 +168,9 @@
}, },
"primaryTokenAttribute": "health", "primaryTokenAttribute": "health",
"secondaryTokenAttribute": "power", "secondaryTokenAttribute": "power",
"url": "https://github.com/rwanoux/vermine2047/tree/main", "url": "https://www.uberwald.me/gitea/uberwald/vermine2047",
"manifest": "https://raw.githubusercontent.com/rwanoux/vermine2047/refs/heads/main/system.json", "manifest": "https://www.uberwald.me/gitea/uberwald/vermine2047/raw/refs/heads/main/system.json",
"download": "https://github.com/rwanoux/vermine2047/archive/refs/heads/main.zip", "download": "https://www.uberwald.me/gitea/uberwald/vermine2047/archive/refs/heads/main.zip",
"license": "LICENSE.txt", "license": "LICENSE.txt",
"changelog": "CHANGELOG.md", "changelog": "CHANGELOG.md",
"flags": { "flags": {

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