Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7de32431b6 | ||
|
|
099ce03774 | ||
|
|
344123b304 | ||
|
|
9b5c7834c0 | ||
|
|
86fc41accd | ||
|
|
a3cb1b0329 | ||
|
|
d63eeda299 | ||
|
|
be43913ae3 | ||
|
|
f9ef1d48a4 | ||
|
|
5be1365038 | ||
|
|
89413c17a4 | ||
|
|
15f8e927f0 | ||
|
|
7382723e75 | ||
|
|
3a84ef09f6 | ||
|
|
edd1819c86 | ||
|
|
9ab2c6b01a | ||
|
|
0ef0de45ff |
@@ -61,3 +61,16 @@ make build / watch / lint # Makefile wrappers for above
|
||||
- **No tests**: `npm test` is a stub (exits 1). No test framework installed.
|
||||
- **No CI**: `.github/` is gitignored; no workflows configured.
|
||||
- **HotReload**: `system.json` flags hotReload for css, scss, hbs, json extensions.
|
||||
|
||||
|
||||
## Constraints and remarks
|
||||
|
||||
Foundryv14 only : foundryvtt.com/api/
|
||||
|
||||
## Tests
|
||||
|
||||
With chrome-devtools on port 9222
|
||||
|
||||
World is called "Vermine", thru https://localhost:31000, with Gamemaster user logged in (no password needed)
|
||||
|
||||
Always try to re-use existing chrome-devtools session.
|
||||
|
||||
+36
-27
@@ -1,13 +1,15 @@
|
||||
{
|
||||
"NAME": "Name",
|
||||
"NONE": "None",
|
||||
"ORIGIN": "Origin",
|
||||
"PROFILE": "Profile",
|
||||
"THEME": "Concept",
|
||||
"TOTEM": "Totem",
|
||||
"SETTINGS": {
|
||||
"world": {
|
||||
"game_mode": {
|
||||
"name": "Game Mode Selection",
|
||||
"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."
|
||||
},
|
||||
"first_run": {
|
||||
"name": "First Run Display",
|
||||
"hint": "To help beginners get started with the Vermine 2047 system."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -63,23 +65,7 @@
|
||||
"roll_dice": "Roll the dice"
|
||||
},
|
||||
"TOURS": {
|
||||
"ChatMessage": "<h3>Welcome to Vermine!</h3><p>If you're using this system for the first time, our shaman will guide you to your initiation rite</p> <button id=\"vermine-tour-chat-button\"> <i class=\"fas fa-person-hiking\"></i> I'm going but I'm scared </button>",
|
||||
"Welcome": {
|
||||
"Title": "Welcome to Vermine",
|
||||
"Description": "Overview of the Vermine system",
|
||||
"WelcomeTitle": "Welcome",
|
||||
"WelcomeContent": "<p>Welcome to Vermine, survivor! Take a tour of our action zone to find your bearings, even if it's your first day among us.</p><p>At the top of the right column, you have everything you're allowed to do here: talk, see other tronches of your commu, manage your matos and that of your potes, study the maps, read the journals of the clamsés, go get us some food with your surin. Let's get into the thick of it.</p>",
|
||||
"ActorTabTitle": "Populate your universe",
|
||||
"ActorTabContent": "<p>Well installed, survivor? Let's not turn around the pot, if you want to stay with us, you'll have to tell us who you are, where you come from, and who you trust. Once you're ready for that, click on the \"Create an actor\" button</p>",
|
||||
"CharacterCreateTitle": "Create a group",
|
||||
"CharacterCreateContent": "<p>This button creates a group file, because that's where we start, in this trou! Your tronche will come later, when you create your character file. As you progress, you might want to add some info on what you see around the camp.</p>",
|
||||
"CompendiumTitle": "Maps",
|
||||
"CompendiumContent": "<p>You will probably need a map of the Iron Lands or a sector of the Forge. You can create your own scene for that, or you can use one of ours. Open one of these compendiums and drag a map into your scene tab (the tab that looks like a folded map).</p>",
|
||||
"OracleToolTitle": "Totem",
|
||||
"OracleToolContent": "<p>No game of Vermine or Starforged would be complete without oracles. This button will open a window with all the oracles of the chosen game. The oracles are also available from the character sheet.</p>",
|
||||
"ToursTitle": "Next steps",
|
||||
"ToursContent": "<p>You're not doing too badly! I'll leave you there, but if you have any questions, you can come find me. If it's about the mechanics, sign up for our group of speech, over there <a href=\"https://discord.gg/FqGHYvXg\">Totem Foundry Discord</a>. Otherwise, you can join the central place over here <a href=\"https://discord.gg/qejqmSxr\">Vermine Discord</a></p><p>Enjoy!</p>"
|
||||
}
|
||||
"ChatMessage": "<h3>Welcome to Vermine 2047!</h3><hr><p class=\"vermine-credits\"><strong>Vermine 2047</strong> — a game by Julien Blondel<br>© 2023 Studio Agate — TOTEM System — © 2023 Julien Blondel — All rights reserved.<br>Foundry VTT system developed by <a href=\"https://www.uberwald.me\">LeRatierBretonnier</a>, based on an original work by Rwanoux.</p>"
|
||||
},
|
||||
"DIFFICULTY_LEVELS": {
|
||||
"obvious": "Obvious",
|
||||
@@ -137,6 +123,7 @@
|
||||
"rerolls": "Rerolls",
|
||||
"contact": "Contact",
|
||||
"reaction": "Reaction",
|
||||
"reaction_bonus": "Reaction bonus",
|
||||
"pools": "Reserves",
|
||||
"gear": "Gear",
|
||||
"protection": "Protection",
|
||||
@@ -197,13 +184,19 @@
|
||||
"effort": "Effort",
|
||||
"equip": "Equip",
|
||||
"unequip": "Unequip",
|
||||
"group": "Group",
|
||||
"abilities": "Characteristics",
|
||||
"ability": "Characteristic",
|
||||
"skills": "Skills",
|
||||
"skill": "Skill",
|
||||
"skills_title": "Skills",
|
||||
"skill_title": "Skill",
|
||||
"skill_mastery": "Mastery",
|
||||
"specificLevel": "Specific level",
|
||||
"competence": "Skill",
|
||||
"modes": "Game modes",
|
||||
"traits_selector": "Trait selector",
|
||||
"bonus": "Bonus",
|
||||
"bonus_supplementary": "Supplementary bonus",
|
||||
"penalty": "Penalty",
|
||||
"reroll": "Reroll",
|
||||
"equipment": "Equipment",
|
||||
@@ -212,6 +205,9 @@
|
||||
"techniques": "Techniques",
|
||||
"difficulty": "Difficulty",
|
||||
"help": "Help",
|
||||
"add": "Add",
|
||||
"delete": "Delete",
|
||||
"learning": "Learning",
|
||||
"tooling": "Equipment",
|
||||
"quantity": "Quantity",
|
||||
"qty": "Qty",
|
||||
@@ -333,6 +329,9 @@
|
||||
"CharacterNamePlaceholder": "Character Name",
|
||||
"ProfilePlaceholder": "Profile",
|
||||
"none": "None",
|
||||
"all_characters": "All characters",
|
||||
"no_group": "No group",
|
||||
"include_in_phase": "Include in this phase",
|
||||
"bonuses": "Bonuses",
|
||||
"choose_ability": "Choose Ability",
|
||||
"choose_skill": "Choose Skill",
|
||||
@@ -359,7 +358,6 @@
|
||||
"pool_specialty": "Specialty",
|
||||
"pool_help": "Help",
|
||||
"pool_tooling": "Equipment",
|
||||
"pool_group": "Group",
|
||||
"pool_selfControl": "Self-Control",
|
||||
"pool_pools": "Pools",
|
||||
"pool_total": "Total",
|
||||
@@ -373,6 +371,9 @@
|
||||
"die_adapted": "adapted",
|
||||
"die_successes": "successes",
|
||||
"total": "Total",
|
||||
"stat": "Stat",
|
||||
"adjustment": "Adjustment",
|
||||
"adjustments": "Adjustments",
|
||||
"totem_dice": "Totem Dice",
|
||||
"totem_hint": "Check to use totem dice (double success possible)",
|
||||
"totem_neutral": "Neutral",
|
||||
@@ -469,7 +470,7 @@
|
||||
"mode_survie": "Survival",
|
||||
"mode_cauchemar": "Nightmare",
|
||||
"mode_apocalypse": "Apocalypse",
|
||||
"evolution_buy_hint": "The purchase consumes and erases all accumulated Evolution dice. Limits: Survival 5D, Nightmare 6D, Apocalypse 7D total. Level 3/4 Mutations are forbidden in Survival; only one level 4 Mutation.",
|
||||
"evolution_buy_hint": "The purchase consumes a number of Evolution dice equal to the chosen level (the remainder is kept). Limits: Survival 5D, Nightmare 6D, Apocalypse 7D total. Level 3/4 Mutations are forbidden in Survival; only one level 4 Mutation.",
|
||||
"buy_evolution_done": "Adaptation/Mutation acquired. Evolution dice erased.",
|
||||
"error_evolution_no_name": "Provide a name for the Adaptation/Mutation.",
|
||||
"error_evolution_invalid_level": "Invalid level (1 to 4).",
|
||||
@@ -514,6 +515,8 @@
|
||||
"characteristic_hint": "Difficulty 9 + Handicap = current value. Max 3 (except Superhuman).",
|
||||
"reserve_hint": "Difficulty = current value + age Handicap. Max 10.",
|
||||
"evolution_hint": "Difficulty by age + Handicap = accumulated Evolution dice. Adapted totem +1D, Human -1D.",
|
||||
"socle_competences_hint": "Socle de compétences atout: +{level} free Experience die (skill already possessed).",
|
||||
"socle_competences_message": "Socle de compétences: +{level} free Experience die. Reminder: a Group companion who learns or develops with him a skill he possesses also gains 1 Experience die.",
|
||||
"totem_ability_hint": "Prerequisites: same Totem as the character, Group level ≥ Capacity level, game mode compatible (Survival N1, Nightmare N2, Apocalypse N3).",
|
||||
"background_hint": "Difficulty 9. Handicap (I) if the Background is marked with an asterisk (*). GM authorization required.",
|
||||
"reputation_hint": "Reduce = more famous (Difficulty 10 − target score, 1 pt max per phase). Increase = fade into obscurity (requires a major Objective, Difficulty = current Reputation, +1 per Success).",
|
||||
@@ -593,8 +596,9 @@
|
||||
"prohibits": "Prohibits",
|
||||
"prohibits_placeholder": "eg: Fire, Water",
|
||||
"rarity_0": "Common",
|
||||
"rarity_1": "Rare",
|
||||
"rarity_2": "Very Rare",
|
||||
"rarity_1": "Uncommon",
|
||||
"rarity_2": "Rare",
|
||||
"rarity_3": "Very rare",
|
||||
"reserves": "Reserves",
|
||||
"ritual": "Ritual",
|
||||
"sexes": {
|
||||
@@ -1050,9 +1054,14 @@
|
||||
"female": "Feminine"
|
||||
},
|
||||
"COMBAT": {
|
||||
"Begin": "Start Combat",
|
||||
"Delete": "Delete",
|
||||
"Encounter": "Encounter",
|
||||
"End": "End Combat",
|
||||
"None": "None",
|
||||
"Round": "Round"
|
||||
"NotStarted": "Not Started",
|
||||
"Round": "Round",
|
||||
"TurnEnd": "End Turn"
|
||||
},
|
||||
"SIZE_LEVELS": {
|
||||
"tiny": "Insect",
|
||||
|
||||
+16
-24
@@ -27,10 +27,6 @@
|
||||
"game_mode": {
|
||||
"name": "Choix du mode de jeu",
|
||||
"hint": "À l’image de certains jeux vidéo proposant différents, Vermine 2047 permet aux joueurs de choisir leur Mode de jeu et de fixer eux-mêmes le degré de réalisme, de surnaturel et de dangerosité de l’univers."
|
||||
},
|
||||
"first_run": {
|
||||
"name": "Affichage du tour d'accueil",
|
||||
"hint": "Pour aider les débutantEs à bien commencer avec le système Vermine 2047."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -69,23 +65,7 @@
|
||||
"roll_dice": "Jeter les dés"
|
||||
},
|
||||
"TOURS": {
|
||||
"ChatMessage": "<h3>Bienvenue dans Vermine!</h3><p>Si vous utilisez ce système pour la première fois, notre shaman va vous guider vers votre rite d'initiation</p> <button id=\"vermine-tour-chat-button\"> <i class=\"fas fa-person-hiking\"></i> J'y vais mais j'ai peur </button>",
|
||||
"Welcome": {
|
||||
"Title": "Bienvenue dans Vermine",
|
||||
"Description": "Aperçu du système Vermine",
|
||||
"WelcomeTitle": "Bienvenue",
|
||||
"WelcomeContent": "<p>Bienvenue dans Vermine, survivantE ! Faire le tour de notre zone d'action te permettre de retrouver tes marques, même si c'est ton premier jour parmi nous.</p><p>En haut de la colonne de droite, tu as tout ce que tu as le droit de faire chez nous : parler, voir les autres tronches de ta commu, gérer ton matos et celui de tes potes, étudier les cartes, lire les journaux des clamsés, aller nous chercher de la bouffe à coups de surin. Entrons dans le vif du sujet.</p>",
|
||||
"ActorTabTitle": "Peupler son univers",
|
||||
"ActorTabContent": "<p>Bien installéE, survivantE ? Tournons pas au tour du pot, si tu veux rester avec nous, va falloir nous dire qui tu es, d'où tu viens, et à qui tu fais confiance. Une fois que t'es prêtE pour ca, clique donc sur le bouton \"Créer un acteur\"</p>",
|
||||
"CharacterCreateTitle": "Créer un groupe",
|
||||
"CharacterCreateContent": "<p>Ce bouton crée une fiche de groupe, parce que c'est par là qu'on commence, dans ce trou ! Ta tronche viendra ensuite, quand tu créeras ta fiche de personnage. Au fur et à mesure que tu progresses, tu voudras peut-être rajouter des infos sur ce que tu vois trainer autour du camp.</p>",
|
||||
"CompendiumTitle": "Cartes",
|
||||
"CompendiumContent": "<p>Vous aurez probablement besoin d'une carte des Terres de Fer ou d'un secteur de la Forge. Vous pouvez créer votre propre scène pour cela, ou vous pouvez utiliser l'une des nôtres. Ouvrez l'un de ces compendiums et faites glisser une carte dans votre zone de scènes (l'onglet qui ressemble à une carte pliée).</p>",
|
||||
"OracleToolTitle": "Totem",
|
||||
"OracleToolContent": "<p>Aucune partie d'Vermine ou Starforged ne serait complète sans oracles. Ce bouton fera apparaître une fenêtre avec tous les oracles du jeu choisi. Les oracles sont également disponibles depuis la fiche de personnage.</p>",
|
||||
"ToursTitle": "Prochaines étapes",
|
||||
"ToursContent": "<p>Tu t'en sors pas si mal ! Moi je vais te laisser là, mais si tu as d'autres questions, tu peux venir me trouver. Si ca concerne la mécanique, inscris-toi dans notre groupe de parole, par là <a href=\"https://discord.gg/FqGHYvXg\">Totem Foundry Discord</a>. Sinon, tu peux rejoindre la place centrale par ici <a href=\"https://discord.gg/qejqmSxr\">Vermine Discord</a></p><p>Enjoy!</p>"
|
||||
}
|
||||
"ChatMessage": "<h3>Bienvenue dans Vermine 2047 !</h3><hr><p class=\"vermine-credits\"><strong>Vermine 2047</strong> — un jeu de Julien Blondel<br>© 2023 Studio Agate — TOTEM System — © 2023 Julien Blondel — Tous droits réservés.<br>Système Foundry VTT développé par <a href=\"https://www.uberwald.me\">LeRatierBretonnier</a>, sur un travail original de Rwanoux.</p>"
|
||||
},
|
||||
"DIFFICULTY_LEVELS": {
|
||||
"obvious": "Evidente",
|
||||
@@ -143,6 +123,7 @@
|
||||
"rerolls": "Relances",
|
||||
"contact": "Contact",
|
||||
"reaction": "Réaction",
|
||||
"reaction_bonus": "Bonus de Réaction",
|
||||
"pools": "Réserves",
|
||||
"gear": "Matériel",
|
||||
"gear_hindrance": "Handicap Matériel",
|
||||
@@ -248,7 +229,6 @@
|
||||
"pool_specialty": "Spécialité",
|
||||
"pool_help": "Entraide",
|
||||
"pool_tooling": "Équipement",
|
||||
"pool_group": "Groupe",
|
||||
"pool_selfControl": "Sang-Froid",
|
||||
"pool_pools": "Réserves",
|
||||
"pool_total": "Total",
|
||||
@@ -274,7 +254,6 @@
|
||||
"ConfrontationHint": "Résolvez la confrontation en sélectionnant des dés",
|
||||
"PurposeTrait": "Trait de but",
|
||||
"SpleenTrait": "Trait de rate",
|
||||
"group": "Groupe",
|
||||
"abilities": "Caractéristiques",
|
||||
"ability": "Caractéristique",
|
||||
"skills": "Compétences",
|
||||
@@ -283,6 +262,7 @@
|
||||
"skill_title": "Compétence",
|
||||
"skill_mastery": "Maîtrise",
|
||||
"bonus": "Bonus",
|
||||
"bonus_supplementary": "Bonus supplémentaire",
|
||||
"penalty": "Malus",
|
||||
"reroll": "Relance",
|
||||
"equipment": "Equipement",
|
||||
@@ -293,6 +273,9 @@
|
||||
"techniques": "Techniques",
|
||||
"difficulty": "Difficulté",
|
||||
"help": "Entraide",
|
||||
"add": "Ajouter",
|
||||
"delete": "Supprimer",
|
||||
"learning": "Apprentissage",
|
||||
"tooling": "Equipement",
|
||||
"quantity": "Quantité",
|
||||
"qty": "Qté",
|
||||
@@ -365,7 +348,13 @@
|
||||
"choose_ability": "Choisissez une caractéristique",
|
||||
"choose_skill": "Choisissez une compétence",
|
||||
"none": "Aucun",
|
||||
"all_characters": "Tous les personnages",
|
||||
"no_group": "Sans groupe",
|
||||
"include_in_phase": "Inclure dans cette phase",
|
||||
"total": "Total",
|
||||
"stat": "Statistique",
|
||||
"adjustment": "Ajustement",
|
||||
"adjustments": "Ajustements",
|
||||
"bonuses": "Bonus",
|
||||
"handicap": "Handicap",
|
||||
"score": "Score",
|
||||
@@ -452,7 +441,7 @@
|
||||
"mode_survie": "Survie",
|
||||
"mode_cauchemar": "Cauchemar",
|
||||
"mode_apocalypse": "Apocalypse",
|
||||
"evolution_buy_hint": "L'achat consomme et efface tous les Dés d'Évolution accumulés. Limites : Survie 5D, Cauchemar 6D, Apocalypse 7D au total. Mutations 3D/4D interdites en Mode Survie ; une seule Mutation à 4D.",
|
||||
"evolution_buy_hint": "L'achat consomme un nombre de Dés d'Évolution égal au niveau choisi (le reste est conservé). Limites : Survie 5D, Cauchemar 6D, Apocalypse 7D au total. Mutations 3D/4D interdites en Mode Survie ; une seule Mutation à 4D.",
|
||||
"buy_evolution_done": "Adaptation/Mutation acquise. Dés d'Évolution effacés.",
|
||||
"error_evolution_no_name": "Indiquez un nom pour l'Adaptation/Mutation.",
|
||||
"error_evolution_invalid_level": "Niveau invalide (1 à 4).",
|
||||
@@ -497,6 +486,8 @@
|
||||
"characteristic_hint": "Difficulté 9 + Handicap = valeur actuelle. Max 3 (Surhumain excepté).",
|
||||
"reserve_hint": "Difficulté = valeur actuelle + Handicap d'âge. Max 10.",
|
||||
"evolution_hint": "Difficulté selon l'âge + Handicap = Dés d'Évolution accumulés. Totem Adapté +1D, Humain -1D.",
|
||||
"socle_competences_hint": "Atout Socle de compétences : +{level}D d'Expérience gratuit (compétence déjà possédée).",
|
||||
"socle_competences_message": "Socle de compétences : +{level}D d'Expérience gratuit. Rappel : un camarade du Groupe qui apprend ou développe avec lui une Compétence qu'il possède gagne aussi 1D d'Expérience.",
|
||||
"totem_ability_hint": "Prérequis : même Totem que le personnage, Niveau de Groupe ≥ Niveau de la Capacité, Mode de jeu compatible (Survie N1, Cauchemar N2, Apocalypse N3).",
|
||||
"background_hint": "Difficulté 9. Handicap (I) si l'Historique est marqué d'un astérisque (*). Autorisation du MJ requise.",
|
||||
"reputation_hint": "Réduire = plus célèbre (Difficulté 10 − score visé, 1 pt max/phase). Augmenter = se faire oublier (exige un Objectif majeur, Difficulté = Réputation actuelle, +1 par Réussite).",
|
||||
@@ -1068,6 +1059,7 @@
|
||||
},
|
||||
"COMBAT": {
|
||||
"Begin": "Commencer",
|
||||
"Delete": "Supprimer",
|
||||
"Encounter": "Rencontre",
|
||||
"End": "Terminer",
|
||||
"None": "Aucun",
|
||||
|
||||
@@ -208,4 +208,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── Ajustements manuels des valeurs calculées ────────────────────────
|
||||
.creature-adjustments {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 4px;
|
||||
|
||||
th, td {
|
||||
padding: 2px 8px;
|
||||
border-bottom: 1px solid @color-border-dark-3;
|
||||
font-size: @font-size-12;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th { text-transform: uppercase; }
|
||||
|
||||
td:nth-child(2) {
|
||||
width: 80px;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
td:nth-child(3) { text-align: center; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -627,6 +627,25 @@ ol.chat-log {
|
||||
&.visible { max-height: 15rem; }
|
||||
}
|
||||
|
||||
.reroll-from-effort {
|
||||
h4.flexcol { gap: 2px; }
|
||||
.effort-pool {
|
||||
font-size: @font-size-12;
|
||||
opacity: 0.8;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mentions de bienvenue / crédits ──────────────────────────────
|
||||
.vermine-credits {
|
||||
font-size: @font-size-12;
|
||||
opacity: 0.8;
|
||||
line-height: 1.4;
|
||||
margin: 4px 0 0;
|
||||
|
||||
a { color: inherit; }
|
||||
}
|
||||
|
||||
// ── Dice results ────────────────────────────────────────────────
|
||||
ul.roll-results {
|
||||
list-style: none;
|
||||
|
||||
@@ -124,13 +124,13 @@ export class VermineBaseItemSheet extends HandlebarsApplicationMixin(foundry.app
|
||||
}
|
||||
|
||||
static async #onReliabilityTest() {
|
||||
const { ReliabilityDialog } = await import("../../system/dialogs/reliabilityDialog.mjs")
|
||||
const { default: ReliabilityDialog } = await import("../../system/dialogs/reliabilityDialog.mjs")
|
||||
const dialog = await ReliabilityDialog.create({ item: this.document, mode: "test" })
|
||||
await dialog.render(true)
|
||||
}
|
||||
|
||||
static async #onReliabilityRepair() {
|
||||
const { ReliabilityDialog } = await import("../../system/dialogs/reliabilityDialog.mjs")
|
||||
const { default: ReliabilityDialog } = await import("../../system/dialogs/reliabilityDialog.mjs")
|
||||
const dialog = await ReliabilityDialog.create({ item: this.document, mode: "repair" })
|
||||
await dialog.render(true)
|
||||
}
|
||||
|
||||
@@ -65,6 +65,22 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
|
||||
protection: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1 })
|
||||
}),
|
||||
|
||||
// Ajustements manuels des valeurs calculées : permettent les disparités
|
||||
// entre créatures de même Gabarit/Taille (ex. un sanglier avec une
|
||||
// Protection supérieure à celle d'un chien, un loup plus vigoureux).
|
||||
// Base dérivée + ajustement = valeur finale (jamais négative).
|
||||
adjustments: 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: 0 }),
|
||||
gearHindrance: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
|
||||
protection: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 })
|
||||
}),
|
||||
|
||||
// Équipement
|
||||
equipment: equipmentSchema()
|
||||
}
|
||||
@@ -145,6 +161,18 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
|
||||
|
||||
// Protection
|
||||
this.computed.protection = roleConfig.protection || 1
|
||||
|
||||
// Ajustements manuels (base + modificateur), plancher à 0.
|
||||
const adj = this.adjustments ?? {}
|
||||
this.computed.attack = Math.max(0, this.computed.attack + (adj.attack ?? 0))
|
||||
this.computed.damage = Math.max(0, this.computed.damage + (adj.damage ?? 0))
|
||||
this.computed.vigor = Math.max(0, this.computed.vigor + (adj.vigor ?? 0))
|
||||
this.computed.reaction = Math.max(0, this.computed.reaction + (adj.reaction ?? 0))
|
||||
this.computed.reactionBonus = Math.max(0, this.computed.reactionBonus + (adj.reactionBonus ?? 0))
|
||||
this.computed.pools = Math.max(0, this.computed.pools + (adj.pools ?? 0))
|
||||
this.computed.gear = Math.max(0, this.computed.gear + (adj.gear ?? 0))
|
||||
this.computed.gearHindrance = Math.max(0, this.computed.gearHindrance + (adj.gearHindrance ?? 0))
|
||||
this.computed.protection = Math.max(0, this.computed.protection + (adj.protection ?? 0))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -295,13 +295,15 @@ VERMINE.totems = {
|
||||
* Neutre : aucun modificateur.
|
||||
*/
|
||||
VERMINE.totemInfluence = {
|
||||
// Règles p.63 : l'Humain est à l'aise avec L'Homme, La Machine et Les
|
||||
// Armes (+1D), mal à l'aise avec L'Animal, La Survie et La Terre (-1D).
|
||||
"human": {
|
||||
"bonus": ["man", "tool", "world"],
|
||||
"malus": ["animal", "survival", "weapon"]
|
||||
"bonus": ["man", "tool", "weapon"],
|
||||
"malus": ["animal", "survival", "world"]
|
||||
},
|
||||
"adapted": {
|
||||
"bonus": ["animal", "survival", "weapon"],
|
||||
"malus": ["man", "tool", "world"]
|
||||
"bonus": ["animal", "survival", "world"],
|
||||
"malus": ["man", "tool", "weapon"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
/**
|
||||
* Dialogue d'achat d'une Adaptation/Mutation avec les Dés d'Évolution
|
||||
* accumulés. Vérifie le coût (niveau 1-4), la limite du Mode de jeu et les
|
||||
* restrictions sur les Mutations ; en cas de succès, crée l'item et efface
|
||||
* les Dés d'Évolution accumulés.
|
||||
* restrictions sur les Mutations ; en cas de succès, crée l'item et consomme
|
||||
* un nombre de Dés d'Évolution égal au niveau choisi.
|
||||
*/
|
||||
export default class BuyEvolutionDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
||||
|
||||
@@ -61,7 +61,7 @@ export default class BuyEvolutionDialog extends HandlebarsApplicationMixin(found
|
||||
};
|
||||
const levels = [];
|
||||
for (let value = 1; value <= 4; value++) {
|
||||
levels.push({ value, cost: value });
|
||||
levels.push({ value });
|
||||
}
|
||||
return {
|
||||
evolutionDice: actor.system?.experience?.evolution || 0,
|
||||
@@ -73,6 +73,18 @@ export default class BuyEvolutionDialog extends HandlebarsApplicationMixin(found
|
||||
};
|
||||
}
|
||||
|
||||
async _onRender() {
|
||||
this.element.querySelectorAll("[data-roll]").forEach(inp => inp.addEventListener("change", () => this.#updateUI()));
|
||||
this.#updateUI();
|
||||
}
|
||||
|
||||
/** Met à jour le total des niveaux « après achat » selon le niveau sélectionné. */
|
||||
#updateUI() {
|
||||
const cost = parseInt(this.element.querySelector("#evolution-level")?.value, 10) || 0;
|
||||
const el = this.element.querySelector("#evolution-total-value");
|
||||
if (el) el.textContent = `${VermineExperience.evolutionTotalLevels(this.#actor) + cost}D`;
|
||||
}
|
||||
|
||||
static async #onCancel(event, target) {
|
||||
this.close();
|
||||
}
|
||||
@@ -102,12 +114,12 @@ export default class BuyEvolutionDialog extends HandlebarsApplicationMixin(found
|
||||
mutation
|
||||
}
|
||||
}]);
|
||||
// N'efface les Dés d'Évolution que si l'item a bien été créé.
|
||||
// Ne consomme que le coût du niveau choisi (Règles p. 116-117 : coût = niveau).
|
||||
if (!created || created.length === 0) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_evolution_create"));
|
||||
return;
|
||||
}
|
||||
await actor.update({ "system.experience.evolution": 0 });
|
||||
await actor.update({ "system.experience.evolution": Math.max(0, (actor.system?.experience?.evolution || 0) - cost) });
|
||||
|
||||
ui.notifications.info(game.i18n.localize("VERMINE.buy_evolution_done"));
|
||||
this.close();
|
||||
|
||||
@@ -119,9 +119,10 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
||||
const creatureLocked = !isCreature && creatureTargets.length && creatureTargets.length === targets.length;
|
||||
if (creatureLocked) {
|
||||
const ref = creatureTargets[0];
|
||||
const creature = ref.id ? game.actors.get(ref.id) : null;
|
||||
const actor = creature ?? (ref.uuid ? await fromUuid(ref.uuid) : null);
|
||||
const d = parseInt(actor?.system?.combatStatus?.difficulty, 10) || 7;
|
||||
// uuid d'abord : un token non lié ne doit pas résoudre vers l'acteur
|
||||
// monde dont il est issu (même principe que VermineExchange).
|
||||
const creature = await VermineExchange.resolveDefender({ id: ref.id, uuid: ref.uuid });
|
||||
const d = parseInt(creature?.system?.combatStatus?.difficulty, 10) || 7;
|
||||
lockedDifficulty = d;
|
||||
difficultyOptions = [{ difficulty: d, label: "", locked: true }];
|
||||
defaultDifficulty = d;
|
||||
@@ -153,7 +154,8 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
||||
lockedDifficulty,
|
||||
defaultDifficulty,
|
||||
hasAmmo: this.mode === "ranged" && (this.weapon?.system?.ammo ?? 0) > 0,
|
||||
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
|
||||
// Specialties are only usable when they match the attack skill.
|
||||
availableSpecialties: actor.items.filter(i => i.type === "specialty" && i.system?.skill === this.skillKey),
|
||||
availableItems: actor.items.filter(i => i.type === "item"),
|
||||
targets,
|
||||
lourdMalus: VermineTraits.lourdMalus(actor),
|
||||
@@ -266,8 +268,10 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
||||
}
|
||||
|
||||
getMaxEffort() {
|
||||
if (this.actor.type === "character") return this.actor.system?.abilities?.[this.abilityKey]?.value || 0;
|
||||
return 0;
|
||||
if (this.actor.type !== "character") return 0;
|
||||
const ability = this.actor.system?.abilities?.[this.abilityKey]?.value || 0;
|
||||
const effort = this.actor.system?.attributes?.effort?.value || 0;
|
||||
return Math.min(ability, effort);
|
||||
}
|
||||
|
||||
getPoolBreakdown() {
|
||||
@@ -330,6 +334,14 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
||||
if (handSel && handEl) {
|
||||
handEl.textContent = handSel.options[handSel.selectedIndex]?.text ?? "";
|
||||
}
|
||||
|
||||
const specChecked = this.#el.querySelector("input[name='usingSpecialization']:checked");
|
||||
const specEl = this.#el.querySelector(".current-specialty");
|
||||
if (specEl && specChecked) {
|
||||
specEl.textContent = specChecked.value === "aucune"
|
||||
? game.i18n.localize("VERMINE.none")
|
||||
: specChecked.value;
|
||||
}
|
||||
}
|
||||
|
||||
#onInputChange() {
|
||||
|
||||
@@ -48,13 +48,18 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
|
||||
if (!memberOf.has(id)) memberOf.set(id, g.id);
|
||||
}
|
||||
}
|
||||
const groupById = new Map(groups.map(g => [g.id, g]));
|
||||
return {
|
||||
characters: characters.map(a => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
dice: a.system?.experience?.dice || 0,
|
||||
groupId: memberOf.get(a.id) || ""
|
||||
})),
|
||||
characters: characters.map(a => {
|
||||
const groupId = memberOf.get(a.id) || "";
|
||||
return {
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
dice: a.system?.experience?.dice || 0,
|
||||
groupId,
|
||||
groupName: groupId ? groupById.get(groupId)?.name : ""
|
||||
};
|
||||
}),
|
||||
groups: groups.map(a => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
@@ -72,6 +77,9 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
|
||||
async _onRender(context, options) {
|
||||
this.element.querySelectorAll("[data-roll]").forEach(inp => inp.addEventListener("change", () => this.#updateUI()));
|
||||
this.element.querySelector("#group-select")?.addEventListener("change", () => this.#updateGroupFilter());
|
||||
this.element.querySelectorAll("[name^='include-']").forEach(cb =>
|
||||
cb.addEventListener("click", event => event.stopPropagation())
|
||||
);
|
||||
this.#updateUI();
|
||||
this.#updateGroupFilter();
|
||||
}
|
||||
@@ -84,11 +92,16 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
|
||||
if (el) el.textContent = `${danger + discoveries + duration}D`;
|
||||
}
|
||||
|
||||
/** Filtre les personnages selon le groupe sélectionné (vide = tous). */
|
||||
/** Filtre les personnages selon le groupe sélectionné (vide = tous, "none" = sans groupe). */
|
||||
#updateGroupFilter() {
|
||||
const groupId = this.element.querySelector("#group-select")?.value || "";
|
||||
for (const row of this.element.querySelectorAll("[data-character-id]")) {
|
||||
const show = !groupId || row.dataset.groupId === groupId;
|
||||
const rowGroupId = row.dataset.groupId || "";
|
||||
const show = groupId === ""
|
||||
? true
|
||||
: groupId === "none"
|
||||
? rowGroupId === ""
|
||||
: rowGroupId === groupId;
|
||||
row.hidden = !show;
|
||||
}
|
||||
}
|
||||
@@ -102,8 +115,8 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
|
||||
+ (parseInt(this.element.querySelector("#collective-discoveries")?.value, 10) || 0)
|
||||
+ (parseInt(this.element.querySelector("#collective-duration")?.value, 10) || 0);
|
||||
|
||||
const groupId = this.element.querySelector("#group-select")?.value || null;
|
||||
if (groupId) {
|
||||
const groupId = this.element.querySelector("#group-select")?.value || "";
|
||||
if (groupId && groupId !== "none") {
|
||||
const group = game.actors.get(groupId);
|
||||
if (group && collective > 0) {
|
||||
const current = group.system?.experience?.dice || 0;
|
||||
@@ -121,8 +134,11 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
|
||||
|
||||
const results = [];
|
||||
for (const row of this.element.querySelectorAll("[data-character-id]")) {
|
||||
// Quand un groupe précis est choisi, ne récompenser que ses membres.
|
||||
if (groupId && row.dataset.groupId !== groupId) continue;
|
||||
// Filtre par groupe sélectionné (vide = tous, "none" = sans groupe)
|
||||
const rowGroupId = row.dataset.groupId || "";
|
||||
if (groupId === "none" ? rowGroupId !== "" : groupId !== "" && rowGroupId !== groupId) continue;
|
||||
// Case « Inclure » décochée → personnage exclu de cette phase
|
||||
if (!row.querySelector(`[name="include-${row.dataset.characterId}"]`)?.checked) continue;
|
||||
const id = row.dataset.characterId;
|
||||
const actor = game.actors.get(id);
|
||||
if (!actor) continue;
|
||||
@@ -164,7 +180,7 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
|
||||
}
|
||||
|
||||
const lines = [`${game.i18n.localize("VERMINE.experience_phase")} +1`];
|
||||
if (groupId) {
|
||||
if (groupId && groupId !== "none") {
|
||||
const group = game.actors.get(groupId);
|
||||
lines.push(`${group?.name ?? ""} (${game.i18n.localize("VERMINE.collective")}): +${collective}D`);
|
||||
}
|
||||
|
||||
@@ -145,6 +145,20 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
return { total: items.length, byLevel };
|
||||
}
|
||||
|
||||
/**
|
||||
* Bonus de l'atout "Socle de compétences" (règles p. 112).
|
||||
* Rend le niveau (1-3) de l'atout possédé par le personnage, ou 0.
|
||||
* @returns {number}
|
||||
*/
|
||||
#socleCompetencesBonus() {
|
||||
const item = (this.#actor.itemTypes?.ability || []).find(i =>
|
||||
i.system?.type === "character"
|
||||
&& (i.name || "").trim().toLowerCase() === "socle de compétences".toLowerCase()
|
||||
);
|
||||
if (!item) return 0;
|
||||
return Math.min(3, Math.max(1, parseInt(item.system?.level?.value, 10) || 1));
|
||||
}
|
||||
|
||||
async _onRender() {
|
||||
this.element.dataset.actorId = this.#actor.id;
|
||||
for (const inp of this.element.querySelectorAll("[data-roll]")) {
|
||||
@@ -201,13 +215,16 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
label = game.i18n.localize(VermineExperience.skillLevelLabelKey(targetLevel));
|
||||
const diff = VermineExperience.skillLearningDifficulty(targetLevel);
|
||||
const domain = key ? VermineExperience.isPreferredDomain(actor, key) : false;
|
||||
// Atout "Socle de compétences" : 1D gratuit si la Compétence est déjà possédée.
|
||||
const socleBonus = current >= 1 ? this.#socleCompetencesBonus() : 0;
|
||||
return {
|
||||
type,
|
||||
key,
|
||||
targetLevel,
|
||||
difficulty: domain ? Math.max(3, diff - 2) : diff,
|
||||
handicap: VermineExperience.skillLearningHandicap(targetLevel, rarity),
|
||||
poolModifier: 0,
|
||||
poolModifier: socleBonus,
|
||||
socleBonus,
|
||||
label,
|
||||
targetLabel: key ? game.i18n.localize("SKILLS." + key + ".name") : ""
|
||||
};
|
||||
@@ -363,6 +380,15 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
set("#diff-value", c.difficulty);
|
||||
set("#handicap-value", c.handicap);
|
||||
set("#required-value", 1 + c.handicap);
|
||||
const socleHint = this.#el.querySelector("#socle-bonus-hint");
|
||||
if (socleHint) {
|
||||
if (type === "skill" && c.socleBonus > 0) {
|
||||
socleHint.style.display = "";
|
||||
socleHint.textContent = game.i18n.format("VERMINE.socle_competences_hint", { level: c.socleBonus });
|
||||
} else {
|
||||
socleHint.style.display = "none";
|
||||
}
|
||||
}
|
||||
const pool = Math.max(0, this.#getDiceSpent() + c.poolModifier);
|
||||
let poolText = `${pool}D${c.poolModifier ? ` (${c.poolModifier >= 0 ? "+" : ""}${c.poolModifier}D)` : ""}`;
|
||||
if (c.adaptedDie) poolText += " — " + game.i18n.localize("VERMINE.adapted_die_x2");
|
||||
@@ -496,6 +522,14 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
if ((roll._total ?? 0) >= required) {
|
||||
await this.#applySuccess(c, specialtyName, roll._total ?? 0);
|
||||
}
|
||||
// Atout "Socle de compétences" : rappeler le bonus conféré aux camarades
|
||||
// du Groupe qui apprennent une Compétence possédée par le personnage.
|
||||
if (c.socleBonus > 0) {
|
||||
await ChatMessage.create({
|
||||
content: game.i18n.format("VERMINE.socle_competences_message", { level: c.socleBonus }),
|
||||
speaker: ChatMessage.getSpeaker({ actor })
|
||||
});
|
||||
}
|
||||
// Les Dés d'Expérience sont perdus que le jet soit réussi ou raté.
|
||||
await actor.update({ "system.experience.dice": Math.max(0, (actor.system.experience.dice || 0) - spent) });
|
||||
this.close();
|
||||
|
||||
@@ -288,7 +288,9 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
|
||||
getMaxEffort() {
|
||||
const sel = this.#getAbility();
|
||||
return parseInt(sel?.value, 10) || 0;
|
||||
const ability = parseInt(sel?.value, 10) || 0;
|
||||
const effort = this.#actor?.system?.attributes?.effort?.value || 0;
|
||||
return Math.min(ability, effort);
|
||||
}
|
||||
|
||||
getTotems() {
|
||||
|
||||
+52
-23
@@ -258,17 +258,19 @@ export class VermineExchange {
|
||||
* Résout l'échange pour une cible donnée (défense la plus récente liée
|
||||
* à cette cible, ou abstention si aucune défense).
|
||||
* @param {ChatMessage} attackMessage
|
||||
* @param {string|null} targetId id d'acteur de la cible (défaut : première cible)
|
||||
* @param {Object|string|null} targetRef cible de l'attaque (ou son uuid),
|
||||
* défaut : première cible — le matching se fait par uuid d'abord car des
|
||||
* PNJ mineurs (tokens non liés) issus de la même fiche partagent le même id
|
||||
* @returns {Promise<Object|null>} résultat de la résolution
|
||||
*/
|
||||
static async resolve(attackMessage, targetId = null) {
|
||||
static async resolve(attackMessage, targetRef = null) {
|
||||
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||
if (!exchange) return null
|
||||
|
||||
const target = (targetId && exchange.targets?.find(t => t.id === targetId)) || exchange.targets?.[0] || null
|
||||
const target = this.#matchTarget(exchange.targets || [], targetRef)
|
||||
if (!target) return null
|
||||
|
||||
const defs = this.#defenseMessagesFor(exchange.id, target.id)
|
||||
const defs = this.#defenseMessagesFor(exchange.id, target)
|
||||
const defenderMessage = defs[defs.length - 1]
|
||||
const defense = defenderMessage ? defenderMessage.getFlag('world', FLAG_DEFENSE) : null
|
||||
|
||||
@@ -281,7 +283,7 @@ export class VermineExchange {
|
||||
const defenderRef = defense
|
||||
? { id: defense.actorId ?? null, uuid: defense.actorUuid ?? null, name: defense.actorName ?? '' }
|
||||
: target
|
||||
const defender = await this.#resolveDefender(defenderRef)
|
||||
const defender = await this.resolveDefender(defenderRef)
|
||||
|
||||
const damage = (exchange.baseDamage || 0) + attackerSuccesses
|
||||
const parryLevel = defense?.type === 'parade' ? (defense.parryLevel ?? 0) : null
|
||||
@@ -384,7 +386,7 @@ export class VermineExchange {
|
||||
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||
if (!exchange) return
|
||||
const type = btn.dataset.type
|
||||
const actor = await this.#resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
|
||||
const actor = await this.resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
|
||||
if (!actor) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
|
||||
return
|
||||
@@ -474,7 +476,7 @@ export class VermineExchange {
|
||||
|
||||
const targets = exchange.targets?.length ? exchange.targets : [null]
|
||||
for (const target of targets) {
|
||||
const result = await this.resolve(attackMessage, target?.id ?? null)
|
||||
const result = await this.resolve(attackMessage, target)
|
||||
if (!result) continue
|
||||
const block = document.createElement('div')
|
||||
block.innerHTML = this.renderResolution(result)
|
||||
@@ -491,7 +493,7 @@ export class VermineExchange {
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
const btn = ev.currentTarget
|
||||
const actor = await this.#resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
|
||||
const actor = await this.resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
|
||||
if (!actor) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
|
||||
return
|
||||
@@ -541,9 +543,9 @@ export class VermineExchange {
|
||||
}
|
||||
let applied = 0
|
||||
for (const target of exchange.targets) {
|
||||
const r = await this.resolve(attackMessage, target?.id ?? null)
|
||||
const r = await this.resolve(attackMessage, target)
|
||||
if (!r?.hit || !r.woundCategory) continue
|
||||
const defender = await this.#resolveDefender({ id: r.defenderId, uuid: r.defenderUuid })
|
||||
const defender = await this.resolveDefender({ id: r.defenderId, uuid: r.defenderUuid })
|
||||
if (!defender || !this.canApplyWound(defender)) continue
|
||||
const result = await this.applyWound(defender, r.woundCategory)
|
||||
if (result && result !== false) applied += 1
|
||||
@@ -559,7 +561,7 @@ export class VermineExchange {
|
||||
static async #onApply(ev, attackMessage) {
|
||||
ev.preventDefault()
|
||||
const btn = ev.currentTarget
|
||||
const defender = await this.#resolveDefender({ id: btn.dataset.defenderId, uuid: btn.dataset.defenderUuid })
|
||||
const defender = await this.resolveDefender({ id: btn.dataset.defenderId, uuid: btn.dataset.defenderUuid })
|
||||
const wound = btn.dataset.wound
|
||||
if (!defender || !wound) return
|
||||
if (!this.canApplyWound(defender)) {
|
||||
@@ -588,31 +590,58 @@ export class VermineExchange {
|
||||
// ── Helpers internes ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Résout un acteur depuis son id d'acteur monde (acteurs liés) ou son
|
||||
* uuid (tokens non liés via fromUuid).
|
||||
* Résout l'acteur d'une cible depuis sa référence (id d'acteur monde ou
|
||||
* uuid). Le uuid est privilégié : pour un token non lié, `token.actor.id`
|
||||
* expose encore l'id de l'acteur monde dont le token est issu, et résoudre
|
||||
* par id blesserait alors tous les tokens liés à cette fiche (PNJ mineurs).
|
||||
* Le uuid d'un acteur synthétique ("Scene.x.Token.y.Actor.z") n'est pas
|
||||
* résolvable de façon fiable par fromUuid (document non indexé quand la
|
||||
* résolution est asynchrone) : on extrait donc directement le token.
|
||||
* @param {{id: string|null, uuid: string|null}} ref
|
||||
* @returns {Promise<Actor|null>}
|
||||
*/
|
||||
static async #resolveDefender({ id = null, uuid = null }) {
|
||||
static async resolveDefender({ id = null, uuid = null }) {
|
||||
if (uuid) {
|
||||
const parts = uuid.split('.')
|
||||
if (parts[0] === 'Scene' && parts[2] === 'Token') {
|
||||
const token = game.scenes.get(parts[1])?.tokens.get(parts[3])
|
||||
return token?.actor ?? null
|
||||
}
|
||||
if (parts[0] === 'Actor') {
|
||||
return game.actors.get(parts[1]) ?? null
|
||||
}
|
||||
}
|
||||
if (id) {
|
||||
const actor = game.actors.get(id)
|
||||
if (actor) return actor
|
||||
}
|
||||
if (uuid) {
|
||||
const doc = await fromUuid(uuid)
|
||||
if (!doc) return null
|
||||
if (doc.documentType === 'Actor') return doc
|
||||
if (doc.documentType === 'Token') return doc.actor ?? null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
static #defenseMessagesFor(exchangeId, targetId = null) {
|
||||
/**
|
||||
* Retrouve la cible d'un échange par uuid d'abord, puis par id. Le uuid est
|
||||
* fiable pour distinguer des PNJ mineurs (tokens non liés) issus de la même
|
||||
* fiche, qui partagent le même id d'acteur monde.
|
||||
* @param {Object[]} targets cibles du flag d'échange
|
||||
* @param {Object|string|null} ref référence de la cible (ou son uuid)
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
static #matchTarget(targets, ref) {
|
||||
if (!ref) return targets[0] ?? null
|
||||
const uuid = typeof ref === 'string' ? ref : (ref.uuid ?? null)
|
||||
const id = typeof ref === 'string' ? null : (ref.id ?? null)
|
||||
if (uuid) return targets.find(t => t.uuid === uuid) ?? null
|
||||
if (id) return targets.find(t => t.id === id) ?? null
|
||||
return targets[0] ?? null
|
||||
}
|
||||
|
||||
static #defenseMessagesFor(exchangeId, target = null) {
|
||||
return game.messages.contents.filter(m => {
|
||||
const f = m.getFlag('world', FLAG_DEFENSE)
|
||||
if (!f || f.exchangeId !== exchangeId) return false
|
||||
if (targetId) return f.actorId === targetId
|
||||
return true
|
||||
if (!target) return true
|
||||
if (target.uuid && f.actorUuid === target.uuid) return true
|
||||
return f.actorId === target.id
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import RollDialog from "./dialogs/rollDialog.mjs";
|
||||
import { initUserDice } from "./dice3d.mjs";
|
||||
import { VermineUtils } from "./roll.mjs";
|
||||
import { registerTours } from "./tour.mjs";
|
||||
import { postWelcomeMessage } from "./tour.mjs";
|
||||
|
||||
export const registerHooks = function () {
|
||||
/**
|
||||
@@ -67,9 +67,11 @@ export const registerHooks = function () {
|
||||
}
|
||||
})
|
||||
Hooks.once("ready", async () => {
|
||||
// System initialized
|
||||
//await registerTours();
|
||||
|
||||
// Relays player rerolls to the GM client (Foundry v14 ChatMessage
|
||||
// updates are GM-only).
|
||||
VermineUtils.registerRerollSocket();
|
||||
// Message de bienvenue (crédits), affiché à chaque chargement.
|
||||
await postWelcomeMessage();
|
||||
});
|
||||
|
||||
// changement de la pause
|
||||
|
||||
+100
-17
@@ -204,9 +204,10 @@ export class VermineUtils {
|
||||
* @returns {Promise<boolean>} Whether the reroll was successful
|
||||
*/
|
||||
static async onReroll(message, target) {
|
||||
// Verify user permissions
|
||||
const msgUserId = message.user?.id ?? message.user;
|
||||
if (msgUserId !== game.user?.id && !game.user?.isGM) {
|
||||
// Verify user permissions. Foundry v14 renamed the ChatMessage field
|
||||
// `user` to `author`, so `message.user` is undefined on this core.
|
||||
const msgAuthorId = message.author?.id ?? message.user?.id ?? message.user;
|
||||
if (msgAuthorId !== game.user?.id && !game.user?.isGM) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_cannot_reroll'));
|
||||
return false;
|
||||
}
|
||||
@@ -231,9 +232,6 @@ export class VermineUtils {
|
||||
|
||||
target.classList.add('rerolled');
|
||||
|
||||
// Set reroll flag
|
||||
await message.setFlag("world", "reroll", true);
|
||||
|
||||
// Get difficulty and dice type
|
||||
const ulElement = target.closest('ul');
|
||||
const difficulty = ulElement?.dataset.difficulty ?? 7;
|
||||
@@ -331,14 +329,55 @@ export class VermineUtils {
|
||||
+ mk('adapted', stats.adapted.count, stats.adapted.success);
|
||||
}
|
||||
|
||||
await message.update({
|
||||
content: messageContent.outerHTML
|
||||
});
|
||||
// Persist the reroll. The GM writes the message directly ; players
|
||||
// cannot update ChatMessages on Foundry v14 (permission "OWNER"), so
|
||||
// their reroll is relayed through the system socket and applied by the
|
||||
// GM client (see registerRerollSocket).
|
||||
if (game.user?.isGM) {
|
||||
await message.setFlag("world", "reroll", true);
|
||||
await message.update({
|
||||
content: messageContent.outerHTML
|
||||
});
|
||||
} else {
|
||||
game.socket?.emit(`system.${game.system.id}`, {
|
||||
type: "reroll",
|
||||
messageId: message.id,
|
||||
requestId: foundry.utils.randomID(8),
|
||||
content: messageContent.outerHTML
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the system socket listener which applies rerolls requested by
|
||||
* players. Foundry v14 only allows GMs to update ChatMessages, so player
|
||||
* rerolls are relayed to the GM client through `system.<id>` (see onReroll).
|
||||
* Call once after the world is ready; re-registration replaces the handler.
|
||||
*/
|
||||
static registerRerollSocket() {
|
||||
const eventName = `system.${game.system.id}`;
|
||||
const handler = async (payload, userId) => {
|
||||
if (!game.user?.isGM) return;
|
||||
if (payload?.type !== "reroll") return;
|
||||
const message = game.messages.get(payload.messageId);
|
||||
if (!message) return;
|
||||
// Security: only the message author may request the reroll update.
|
||||
if (message.author?.id !== userId) return;
|
||||
await message.setFlag("world", "reroll", true);
|
||||
await message.update({ content: payload.content });
|
||||
};
|
||||
if (VermineUtils.#rerollSocketHandler) {
|
||||
game.socket?.off(eventName, VermineUtils.#rerollSocketHandler);
|
||||
}
|
||||
VermineUtils.#rerollSocketHandler = handler;
|
||||
game.socket?.on(eventName, handler);
|
||||
}
|
||||
|
||||
static #rerollSocketHandler;
|
||||
|
||||
/**
|
||||
* Sets up event listeners for chat messages.
|
||||
* @param {HTMLElement} html - The HTML element containing chat events.
|
||||
@@ -376,34 +415,78 @@ export class VermineUtils {
|
||||
});
|
||||
}
|
||||
|
||||
// Add click event for granting rerolls
|
||||
// Add click event for spending Effort points as rerolls (règles p.35) :
|
||||
// 1 point d'Effort = 1 relance, ajouté aux relances de Compétence restantes.
|
||||
html.querySelectorAll("button.grant-reroll").forEach(el => {
|
||||
el.addEventListener('click', async (ev) => {
|
||||
const btn = ev.currentTarget;
|
||||
const rollMessage = btn.closest(".vermine-roll-message");
|
||||
const grantedRerollElement = html.querySelector('#granted-reroll');
|
||||
const allowedRerollElement = html.querySelector("#allowed_reroll");
|
||||
const requested = parseInt(grantedRerollElement?.innerText, 10) || 0;
|
||||
|
||||
if (grantedRerollElement && allowedRerollElement) {
|
||||
allowedRerollElement.innerText = grantedRerollElement.innerText;
|
||||
// Déduit les points d'Effort de l'acteur (max : ce qu'il lui reste).
|
||||
const actor = await VermineUtils.#resolveMessageActor(rollMessage);
|
||||
const effort = actor?.system?.attributes?.effort;
|
||||
let spent = 0;
|
||||
if (effort && requested > 0) {
|
||||
spent = Math.min(requested, parseInt(effort.value, 10) || 0);
|
||||
if (spent > 0) {
|
||||
const remaining = (parseInt(effort.value, 10) || 0) - spent;
|
||||
await actor.update({ 'system.attributes.effort.value': remaining });
|
||||
const poolEl = rollMessage?.querySelector('.effort-pool');
|
||||
if (poolEl) poolEl.innerText = `${remaining}/${effort.max || 0}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Les relances d'Effort s'ajoutent aux relances de Compétence restantes.
|
||||
if (allowedRerollElement) {
|
||||
const current = parseInt(allowedRerollElement.innerText, 10) || 0;
|
||||
allowedRerollElement.innerText = current + spent;
|
||||
}
|
||||
const effortSection = btn.closest('.reroll-from-effort');
|
||||
if (effortSection) effortSection.style.display = "none";
|
||||
|
||||
const mesEl = btn.closest('[data-message-id]');
|
||||
const messageId = mesEl?.dataset?.messageId;
|
||||
|
||||
if (messageId) {
|
||||
btn.closest('.reroll-from-effort').style.display = "none";
|
||||
|
||||
const rollMessage = btn.closest(".vermine-roll-message");
|
||||
if (rollMessage) {
|
||||
const content = rollMessage.outerHTML;
|
||||
const content = rollMessage.outerHTML;
|
||||
// GM applies the update directly ; players relay through the
|
||||
// system socket (Foundry v14 ChatMessage updates are GM-only).
|
||||
if (game.user?.isGM) {
|
||||
const message = await game.messages.get(messageId);
|
||||
await message.update({ content: content });
|
||||
} else {
|
||||
game.socket?.emit(`system.${game.system.id}`, {
|
||||
type: "reroll",
|
||||
messageId: messageId,
|
||||
requestId: foundry.utils.randomID(8),
|
||||
content: content
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Résout l'acteur à l'origine d'un message de jet depuis les attributs
|
||||
* data-actor-id / data-actor-uuid posés sur le message. Le uuid est
|
||||
* privilégié : il distingue les tokens non liés (PNJ mineurs) issus de la
|
||||
* même fiche, qui partagent le même id d'acteur monde.
|
||||
* @param {HTMLElement} rollMessage
|
||||
* @returns {Promise<Actor|null>}
|
||||
*/
|
||||
static async #resolveMessageActor(rollMessage) {
|
||||
if (!rollMessage) return null;
|
||||
const id = rollMessage.dataset.actorId ?? null;
|
||||
const uuid = rollMessage.dataset.actorUuid ?? null;
|
||||
if (!id && !uuid) return null;
|
||||
const { VermineExchange } = await import('./exchange.mjs');
|
||||
return VermineExchange.resolveDefender({ id, uuid });
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays dice rolls in 3D if available.
|
||||
* @param {Roll} roll - The roll to display
|
||||
|
||||
@@ -28,13 +28,4 @@ export const registerSettings = function () {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
game.settings.register("vermine2047", "first-run-tips-shown", {
|
||||
name: game.i18n.localize("SETTINGS.world.first_run.name"),
|
||||
hint: game.i18n.localize("SETTINGS.world.first_run.hint"),
|
||||
scope: "system",
|
||||
config: true,
|
||||
type: Boolean,
|
||||
default: false
|
||||
});
|
||||
}
|
||||
+9
-154
@@ -1,159 +1,14 @@
|
||||
function compact(array) {
|
||||
var index2 = -1, length = array == null ? 0 : array.length, resIndex = 0, result = [];
|
||||
while (++index2 < length) {
|
||||
var value2 = array[index2];
|
||||
if (value2) {
|
||||
result[resIndex++] = value2;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
class CreateActorDialog extends FormApplication {
|
||||
constructor() {
|
||||
super({});
|
||||
}
|
||||
async _updateObject() {
|
||||
}
|
||||
static get defaultOptions() {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
title: game.i18n.format("DOCUMENT.Create", {
|
||||
type: game.i18n.localize("DOCUMENT.Actor")
|
||||
}),
|
||||
template: "systems/vermine2047/templates/actor/create.hbs",
|
||||
id: "new-actor-dialog",
|
||||
resizable: false,
|
||||
classes: ["vermine2047", "sheet", "new-actor"],
|
||||
width: 650,
|
||||
height: 200
|
||||
});
|
||||
}
|
||||
getData(_options) {
|
||||
return {
|
||||
// sfenabled: IronswornSettings.starforgedToolsEnabled
|
||||
};
|
||||
}
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
html.find(".vermine__character__create").on("click", async (ev) => {
|
||||
await this._characterCreate.call(this, ev);
|
||||
});
|
||||
html.find(".vermine__shared__create").on("click", async (ev) => {
|
||||
await this._sharedCreate.call(this, ev);
|
||||
});
|
||||
html.find(".vermine__site__create").on("click", async (ev) => {
|
||||
await this._siteCreate.call(this, ev);
|
||||
});
|
||||
html.find(".vermine__foe__create").on("click", async (ev) => {
|
||||
await this._foeCreate.call(this, ev);
|
||||
});
|
||||
}
|
||||
}
|
||||
class VermineTour extends foundry.nue.Tour {
|
||||
/** @override */
|
||||
async _preStep() {
|
||||
var _a2, _b, _c, _d, _e;
|
||||
await super._preStep();
|
||||
if ((_a2 = this.currentStep) == null ? void 0 : _a2.sidebarTab) {
|
||||
await ((_b = ui.sidebar) == null ? void 0 : _b.activateTab(this.currentStep.sidebarTab));
|
||||
}
|
||||
if ((_c = this.currentStep) == null ? void 0 : _c.layer) {
|
||||
const layer = canvas == null ? void 0 : canvas[this.currentStep.layer];
|
||||
if (layer.active && this.currentStep.tool)
|
||||
(_d = ui.controls) == null ? void 0 : _d.initialize({ tool: this.currentStep.tool });
|
||||
else
|
||||
layer.activate({ tool: this.currentStep.tool });
|
||||
}
|
||||
if (((_e = this.currentStep) == null ? void 0 : _e.hook) != null) {
|
||||
await this.currentStep.hook();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WelcomeTour extends VermineTour {
|
||||
constructor() {
|
||||
super({
|
||||
title: "TOURS.Welcome.Title",
|
||||
description: "TOURS.Welcome.Description",
|
||||
canBeResumed: false,
|
||||
display: true,
|
||||
steps: []
|
||||
});
|
||||
}
|
||||
get steps() {
|
||||
var _a2;
|
||||
return compact([
|
||||
{
|
||||
id: "welcome",
|
||||
title: "TOURS.Welcome.WelcomeTitle",
|
||||
content: "TOURS.Welcome.WelcomeContent"
|
||||
},
|
||||
{
|
||||
id: "character-tab",
|
||||
title: "TOURS.Welcome.ActorTabTitle",
|
||||
content: "TOURS.Welcome.ActorTabContent",
|
||||
sidebarTab: "actors",
|
||||
selector: "#actors .create-document"
|
||||
},
|
||||
{
|
||||
id: "character-create",
|
||||
title: "TOURS.Welcome.CharacterCreateTitle",
|
||||
content: "TOURS.Welcome.CharacterCreateContent",
|
||||
hook: async () => {
|
||||
this.createActorDialog = new CreateActorDialog();
|
||||
await this.createActorDialog.render(true);
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
},
|
||||
selector: "#new-actor-dialog #new-group"
|
||||
},
|
||||
/*{
|
||||
id: "compendia",
|
||||
title: "TOURS.Welcome.CompendiumTitle",
|
||||
content: "TOURS.Welcome.CompendiumContent",
|
||||
hook: async () => {
|
||||
var _a3;
|
||||
return await ((_a3 = this.createActorDialog) == null ? void 0 : _a3.close());
|
||||
},
|
||||
sidebarTab: "compendium",
|
||||
selector: 'li[data-pack="vermine2047.verminescenes"]'
|
||||
},*/
|
||||
/*((_a2 = game.user) == null ? void 0 : _a2.viewedScene) && {
|
||||
id: "oracletool",
|
||||
title: "TOURS.Welcome.OracleToolTitle",
|
||||
content: "TOURS.Welcome.OracleToolContent",
|
||||
layer: "tokens",
|
||||
selector: '[data-tool="Oracles"]'
|
||||
},*/
|
||||
{
|
||||
id: "tours",
|
||||
title: "TOURS.Welcome.ToursTitle",
|
||||
content: "TOURS.Welcome.ToursContent",
|
||||
hook: async () => {
|
||||
var _a3;
|
||||
return await ((_a3 = this.createActorDialog) == null ? void 0 : _a3.close());
|
||||
},
|
||||
sidebarTab: "settings",
|
||||
selector: 'button[data-action="tours"]'
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
export async function registerTours() {
|
||||
game.tours.register("vermine2047", "welcome", new WelcomeTour());
|
||||
document.addEventListener("click", (el) => {
|
||||
if (el.target.closest("#chat-log #vermine-tour-chat-button")) {
|
||||
const tour = game.tours.get("vermine2047.welcome");
|
||||
tour?.start();
|
||||
}
|
||||
});
|
||||
if (game.settings.get("vermine2047", "first-run-tips-shown"))
|
||||
return;
|
||||
// Posting first-start messages
|
||||
/**
|
||||
* Message de bienvenue (crédits du système et du jeu) posté dans le chat
|
||||
* à chaque chargement du monde. Un seul exemplaire par refresh : il est
|
||||
* posté par le MJ actif uniquement.
|
||||
*/
|
||||
export async function postWelcomeMessage() {
|
||||
if (!game.user?.isActiveGM) return;
|
||||
const gms = ChatMessage.getWhisperRecipients("GM");
|
||||
ChatMessage.implementation;
|
||||
ChatMessage.create({
|
||||
await ChatMessage.create({
|
||||
whisper: gms,
|
||||
speaker: { alias: game.i18n.localize("VERMINE.name") },
|
||||
content: game.i18n.localize("TOURS.ChatMessage")
|
||||
});
|
||||
game.settings.set("vermine2047", "first-run-tips-shown", true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +168,7 @@
|
||||
"distance": 1,
|
||||
"units": "m"
|
||||
},
|
||||
"socket": true,
|
||||
"primaryTokenAttribute": "health",
|
||||
"secondaryTokenAttribute": "power",
|
||||
"url": "https://www.uberwald.me/gitea/uberwald/vermine2047",
|
||||
|
||||
@@ -71,4 +71,68 @@
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{!-- Ajustements manuels des valeurs calculées (disparités entre créatures
|
||||
de même gabarit : base dérivée + modificateur = valeur finale) --}}
|
||||
{{#if @root.isEditMode}}
|
||||
<section class="mdb">
|
||||
<h4 class="align-center">{{ localize 'VERMINE.adjustments' }}</h4>
|
||||
<table class="creature-adjustments">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ localize 'VERMINE.stat' }}</th>
|
||||
<th>{{ localize 'VERMINE.adjustment' }}</th>
|
||||
<th>{{ localize 'VERMINE.total' }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ localize 'VERMINE.total_attack' }}</td>
|
||||
<td><input type="number" name="system.adjustments.attack" value="{{system.adjustments.attack}}" data-dtype="Number" /></td>
|
||||
<td>{{ system.computed.attack }}D</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ localize 'ADVERSITY.damage' }}</td>
|
||||
<td><input type="number" name="system.adjustments.damage" value="{{system.adjustments.damage}}" data-dtype="Number" /></td>
|
||||
<td>{{ system.computed.damage }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ localize 'ADVERSITY.vigor' }}</td>
|
||||
<td><input type="number" name="system.adjustments.vigor" value="{{system.adjustments.vigor}}" data-dtype="Number" /></td>
|
||||
<td>{{ system.computed.vigor }}D</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ localize 'ADVERSITY.reaction' }}</td>
|
||||
<td><input type="number" name="system.adjustments.reaction" value="{{system.adjustments.reaction}}" data-dtype="Number" /></td>
|
||||
<td>{{ system.computed.reaction }}D{{#ifgt system.computed.reactionBonus 0}}+{{system.computed.reactionBonus}}{{/ifgt}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ localize 'ADVERSITY.reaction_bonus' }}</td>
|
||||
<td><input type="number" name="system.adjustments.reactionBonus" value="{{system.adjustments.reactionBonus}}" data-dtype="Number" /></td>
|
||||
<td>{{ system.computed.reactionBonus }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ localize 'ADVERSITY.pools' }}</td>
|
||||
<td><input type="number" name="system.adjustments.pools" value="{{system.adjustments.pools}}" data-dtype="Number" /></td>
|
||||
<td>{{ system.computed.pools }}D</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ localize 'ADVERSITY.gear' }}</td>
|
||||
<td><input type="number" name="system.adjustments.gear" value="{{system.adjustments.gear}}" data-dtype="Number" /></td>
|
||||
<td>{{ system.computed.gear }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ localize 'ADVERSITY.gear_hindrance' }}</td>
|
||||
<td><input type="number" name="system.adjustments.gearHindrance" value="{{system.adjustments.gearHindrance}}" data-dtype="Number" /></td>
|
||||
<td>{{ system.computed.gearHindrance }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ localize 'ADVERSITY.protection' }}</td>
|
||||
<td><input type="number" name="system.adjustments.protection" value="{{system.adjustments.protection}}" data-dtype="Number" /></td>
|
||||
<td>{{ system.computed.protection }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<label class="label" for="evolution-level">{{ localize 'VERMINE.evolution_level' }}</label>
|
||||
<select class="info-value" data-roll="true" id="evolution-level" name="evolution-level">
|
||||
{{#each levels}}
|
||||
<option value="{{ this.value }}">{{ this.value }}D — {{ this.cost }}D</option>
|
||||
<option value="{{ this.value }}">{{ this.value }}D</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -66,6 +66,42 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SPECIALTIES (limited to the attack skill) -->
|
||||
{{#if availableSpecialties.length}}
|
||||
<details class="specialties-section">
|
||||
<summary class="flexrow">
|
||||
<span class="label">{{localize 'VERMINE.specialty'}} (+1D)</span>
|
||||
<span class="current-specialty">{{localize 'VERMINE.none'}}</span>
|
||||
</summary>
|
||||
<div class="flexrow specialty-options">
|
||||
<label class="flexrow">
|
||||
<input
|
||||
type="radio"
|
||||
data-roll="true"
|
||||
name="usingSpecialization"
|
||||
id="usingSpecialization"
|
||||
value="aucune"
|
||||
checked
|
||||
/>
|
||||
<span>{{localize 'VERMINE.none'}}</span>
|
||||
</label>
|
||||
{{#each availableSpecialties as |spec ind|}}
|
||||
<label class="flexrow">
|
||||
<input
|
||||
type="radio"
|
||||
data-roll="true"
|
||||
name="usingSpecialization"
|
||||
id="usingSpecialization"
|
||||
value="{{spec.name}}"
|
||||
data-spec-skill="{{spec.system.skill}}"
|
||||
/>
|
||||
<span>{{spec.name}}</span>
|
||||
</label>
|
||||
{{/each}}
|
||||
</div>
|
||||
</details>
|
||||
{{/if}}
|
||||
|
||||
<!-- DIFFICULTY -->
|
||||
<details class="difficulty-section" open>
|
||||
<summary class="flexrow">
|
||||
@@ -165,7 +201,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flexrow bonus-item">
|
||||
<label class="label" for="group">{{localize 'VERMINE.group'}}</label>
|
||||
<label class="label" for="group">{{localize 'VERMINE.bonus_supplementary'}}</label>
|
||||
<input type="number" data-roll="true" class="numeric-entry" style="text-align: center;"
|
||||
name="group" id="group" min="0" max="5" value="0" />
|
||||
<span>D</span>
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
<div class="flexrow bonus-item">
|
||||
<label class="label" for="group-select">{{ localize 'VERMINE.collective_group' }}</label>
|
||||
<select class="info-value" id="group-select" name="group-select">
|
||||
<option value="">{{ localize 'VERMINE.none' }}</option>
|
||||
<option value="">{{ localize 'VERMINE.all_characters' }}</option>
|
||||
<option value="none">{{ localize 'VERMINE.no_group' }}</option>
|
||||
{{#each groups}}
|
||||
<option value="{{ this.id }}">{{ this.name }} ({{ this.dice }}D)</option>
|
||||
{{/each}}
|
||||
@@ -49,8 +50,14 @@
|
||||
{{#each characters}}
|
||||
<details class="difficulty-section" data-character-id="{{ this.id }}" data-group-id="{{ this.groupId }}" open>
|
||||
<summary class="flexrow">
|
||||
<label class="flexrow exp-include-label" title="{{ localize 'VERMINE.include_in_phase' }}">
|
||||
<input type="checkbox" data-roll="true" id="include-{{ this.id }}" name="include-{{ this.id }}" checked />
|
||||
</label>
|
||||
<span class="label">{{ this.name }}</span>
|
||||
<span class="current-values">{{ localize 'VERMINE.experience_dice' }}: {{ this.dice }}D</span>
|
||||
<span class="current-values">
|
||||
{{#if this.groupName}}[{{ this.groupName }}]{{ else }}[{{ localize 'VERMINE.no_group' }}]{{/if}}
|
||||
{{ localize 'VERMINE.experience_dice' }}: {{ this.dice }}D
|
||||
</span>
|
||||
</summary>
|
||||
<div class="flexrow difficulty-controls">
|
||||
<div class="flexcol">
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
<p class="learning-hint" id="socle-bonus-hint" style="display:none;"></p>
|
||||
</div>
|
||||
|
||||
<div id="learning-specialty-section" class="learning-targets" style="display:none;">
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flexrow bonus-item">
|
||||
<label class="label" for="group">{{localize 'VERMINE.group'}}</label>
|
||||
<label class="label" for="group">{{localize 'VERMINE.bonus_supplementary'}}</label>
|
||||
<input type="number" data-roll="true" class="numeric-entry" style="text-align: center;"
|
||||
name="group" id="group" min="0" max="5" value="0" />
|
||||
<span>D</span>
|
||||
|
||||
@@ -203,7 +203,7 @@
|
||||
|
||||
<!-- GROUP -->
|
||||
<div class="flexrow bonus-item">
|
||||
<label class="label" for="group">{{localize 'VERMINE.group'}}</label>
|
||||
<label class="label" for="group">{{localize 'VERMINE.bonus_supplementary'}}</label>
|
||||
<input
|
||||
type="number"
|
||||
data-roll="true"
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
{{#if item.system.effects}}
|
||||
<div class="resource flexcol">
|
||||
<span class="resource-label">{{ localize 'UI.effects' }}:</span>
|
||||
<span class="resource-label">{{ localize 'UI.effects.name' }}:</span>
|
||||
<ul class="unstyled">
|
||||
{{#each item.system.effects as |effect|}}
|
||||
<li>{{effect}}</li>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
{{#if item.system.effect}}
|
||||
<div class="resource flexrow">
|
||||
<span class="resource-label">{{ localize 'UI.effects' }}:</span>
|
||||
<span class="resource-label">{{ localize 'UI.effects.name' }}:</span>
|
||||
<span>{{item.system.effect}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="vermine-roll-message" data-free-successes="{{verdict.bonusSuccesses}}">
|
||||
<div class="vermine-roll-message" data-free-successes="{{verdict.bonusSuccesses}}" data-actor-id="{{param.actor.id}}" data-actor-uuid="{{param.actor.uuid}}">
|
||||
{{!-- Verdict --}}
|
||||
<div class="roll-verdict {{#if verdict.success}}success{{else}}failure{{/if}}">
|
||||
{{#if verdict.success}}
|
||||
@@ -191,7 +191,7 @@
|
||||
<span class="pool-item pool-malus">{{localize "VERMINE.pool_psychology"}} -{{param.poolBreakdown.psychology}}D</span>
|
||||
{{/if}}
|
||||
{{#if param.poolBreakdown.group}}
|
||||
<span class="pool-item">{{localize "VERMINE.pool_group"}} +{{param.poolBreakdown.group}}D</span>
|
||||
<span class="pool-item">{{localize "VERMINE.bonus_supplementary"}} +{{param.poolBreakdown.group}}D</span>
|
||||
{{/if}}
|
||||
{{#if param.poolBreakdown.pools}}
|
||||
<span class="pool-item">{{localize "VERMINE.pool_pools"}} +{{param.poolBreakdown.pools}}D</span>
|
||||
@@ -226,9 +226,11 @@
|
||||
<div class="reroll-fromroll">
|
||||
<h4>{{localize "VERMINE.rerolls_possible"}} : <span id="allowed_reroll">{{param.Reroll}}</span></h4>
|
||||
<div class="reroll flexrow">
|
||||
{{#ifgt param.actor.system.attributes.effort.value 0}}
|
||||
<div class="reroll-from-effort flexrow">
|
||||
<h4 class="flexcol">
|
||||
<span>{{localize "VERMINE.effort"}}</span>
|
||||
<span class="effort-pool">{{param.actor.system.attributes.effort.value}}/{{param.actor.system.attributes.effort.max}}</span>
|
||||
</h4>
|
||||
<input type="range"
|
||||
min="0"
|
||||
@@ -240,6 +242,7 @@
|
||||
<span id="granted-reroll">0</span>
|
||||
</button>
|
||||
</div>
|
||||
{{/ifgt}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user