7 Commits
Author SHA1 Message Date
uberwald ad4daf0b00 fix: exception si le dialogue d'apprentissage est fermé pendant le jet
Release Creation / build (release) Failing after 1m38s
- #getSpecialtyName accédait à this.#el sans garde : fermer le dialogue
  pendant l'animation du jet levait "Uncaught (in promise)". Chaînage
  optionnel sur l'élément (le jet se résout quand même et dépense le dé).
2026-08-06 08:14:06 +02:00
uberwald c96e8d69d7 fix: retours review - validation de phase, drop de véhicule et dés de Réserve
- resetPhaseLimits ne touche plus au compteur de phase (seul le
  dialogue d'attribution du MJ l'avance) : évite le double-incrément
  quand joueur et MJ valident la même phase.
- _onDropActor : import d'une copie monde pour un véhicule déposé
  depuis un compendium, confirmation avant de transférer un véhicule
  déjà rattaché à un autre Groupe, try/catch avec retour utilisateur.
- #onClickRadioHexa : garde isEditable (pas d'erreur de permission
  pour un utilisateur non éditeur).
2026-08-05 21:24:33 +02:00
uberwald 282fb2794a feat: validation de phase d'Expérience (réinitialisation des limites)
- Nouveau bouton « Valider la phase » sur les fiches Personnage et
  Groupe : incrémente experience.phase et réinitialise les limites
  d'apprentissage (learned.*) pour recommencer une nouvelle phase,
  sans repasser par le dialogue d'attribution du MJ.
- Helper VermineExperience.completePhase(actor) mutualisé
  (personnage ou Groupe) ; i18n FR/EN.
2026-08-05 20:58:46 +02:00
uberwald 46b9512256 fix: sous-titres de la fiche de Groupe illisibles (jaune sur fond beige)
- Les h5 (Objectifs majeurs/mineurs) passent du jaune @color-amber à
  @color-chitin-dark, lisible sur le fond box_background.webp.
2026-08-05 18:56:04 +02:00
uberwald 8316112741 fix: toggle Chaman de l'onglet Totem étiré sur toute la largeur
- Style compact pour .chaman-toggle (width fit-content, centré, label
  DistressBlack, checkbox de taille fixe) dans less/actor/totem.less.
2026-08-05 18:52:41 +02:00
uberwald 75f39551f8 feat: fiche de Groupe - drag&drop de véhicules et dés de Réserve cliquables
- Dépôt d'un acteur Véhicule sur la fiche de Groupe : le véhicule est
  lié via system.ownerId (pas de duplication), apparaît dans la liste
  Véhicules de l'onglet Matériel et s'ouvre au clic (_onDropActor).
- Onglet Réserve : dés de Réserve hexagonaux cliquables (1..max),
  même comportement que les réserves du personnage (cocher/décocher).
- ESLint : fromUuid ajouté aux globals (corrige aussi 3 erreurs
  préexistantes dans base-actor-sheet.mjs).
2026-08-05 18:05:13 +02:00
uberwald ef76ae1116 feat: fiche de Groupe - ordre des sections Matériel
- Onglet Matériel : les Objets passent sous les Armes et les
  Protections (ordre aligné sur la fiche PJ : Armes, Protections,
  Objets, Véhicules, Notes).
2026-08-05 17:49:22 +02:00
14 changed files with 212 additions and 43 deletions
+2 -1
View File
@@ -34,7 +34,8 @@ module.exports = {
Dialog: 'readonly', Dialog: 'readonly',
foundry: 'readonly', foundry: 'readonly',
Handlebars: 'readonly', Handlebars: 'readonly',
renderTemplate: 'readonly' renderTemplate: 'readonly',
fromUuid: 'readonly'
}, },
rules: { rules: {
// Possible Problems // Possible Problems
+7
View File
@@ -376,6 +376,13 @@
"evolution_dice": "Evolution dice", "evolution_dice": "Evolution dice",
"open_experience_phase": "Experience phase", "open_experience_phase": "Experience phase",
"open_learning": "Learning", "open_learning": "Learning",
"validate_phase": "Validate phase",
"validate_phase_done": "Experience phase validated: learning limits reset.",
"error_not_editable": "You do not have permission to modify this element.",
"vehicle_assign": "Attach vehicle",
"vehicle_assign_confirm": "This vehicle already belongs to \"{group}\". Attach it to this group?",
"vehicle_assign_failed": "Could not attach this vehicle to the group.",
"vehicle_import_failed": "Could not import this vehicle from the compendium.",
"learning_limits": "Learning limits (per phase)", "learning_limits": "Learning limits (per phase)",
"learned_skill": "Skill", "learned_skill": "Skill",
"learned_specialty": "Specialty", "learned_specialty": "Specialty",
+7
View File
@@ -359,6 +359,13 @@
"evolution_dice": "Dés d'Évolution", "evolution_dice": "Dés d'Évolution",
"open_experience_phase": "Phase d'Expérience", "open_experience_phase": "Phase d'Expérience",
"open_learning": "Apprentissage", "open_learning": "Apprentissage",
"validate_phase": "Valider la phase",
"validate_phase_done": "Phase d'Expérience validée : limites d'apprentissage réinitialisées.",
"error_not_editable": "Vous n'avez pas la permission de modifier cet élément.",
"vehicle_assign": "Attacher le véhicule",
"vehicle_assign_confirm": "Ce véhicule appartient déjà à « {group} ». L'attacher à ce Groupe ?",
"vehicle_assign_failed": "Impossible d'attacher ce véhicule au Groupe.",
"vehicle_import_failed": "Impossible d'importer ce véhicule depuis le compendium.",
"learning_limits": "Limites d'apprentissage (par phase)", "learning_limits": "Limites d'apprentissage (par phase)",
"learned_skill": "Compétence", "learned_skill": "Compétence",
"learned_specialty": "Spécialité", "learned_specialty": "Spécialité",
+27
View File
@@ -368,6 +368,33 @@
} }
} }
// Fiche de Groupe : mêmes dés de Réserve cliquables (AppV2).
&.group div.hexa {
.hexa-style();
.transition();
margin: 0.2rem;
position: relative;
&:hover {
.hexa-style(rgba(255, 255, 255, 0.425), rgba(0, 0, 0, 0.288));
}
input {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
z-index: 1;
margin: 0;
cursor: pointer;
}
&.checked {
.hexa-style(rgb(0, 0, 0), rgba(0, 0, 0, 0.288));
&:hover { .hexa-style(rgb(43, 43, 43), rgba(0, 0, 0, 0.288)); }
}
}
.sheet-header-toggle { .sheet-header-toggle {
text-align: right; text-align: right;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
+2 -1
View File
@@ -42,7 +42,8 @@
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
text-transform: uppercase; text-transform: uppercase;
font-size: @font-size-12; font-size: @font-size-12;
color: @color-amber; // Brun foncé lisible sur le fond beige de la fiche (le jaune était illisible).
color: @color-chitin-dark;
margin: 8px 0 4px; margin: 8px 0 4px;
text-align: left; text-align: left;
border-bottom: none; border-bottom: none;
+23
View File
@@ -18,6 +18,29 @@
} }
} }
// Toggle « Chaman » de l'onglet Totem : ne pas étirer sur toute la largeur.
.chaman-toggle {
width: fit-content;
margin: 0.5rem auto;
gap: 0.5rem;
label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-12;
color: @theme-color-dark;
cursor: pointer;
margin: 0;
}
input[type="checkbox"] {
width: 1rem;
height: 1rem;
margin: 0;
cursor: pointer;
}
}
div.minor-totems { div.minor-totems {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -311,6 +311,11 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
static #onClickRadioHexa(event, target) { static #onClickRadioHexa(event, target) {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
// Évite une erreur de permission pour un utilisateur non éditeur.
if (!this.isEditable) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_not_editable"))
return
}
const input = target const input = target
const update = {} const update = {}
let current = this.document let current = this.document
@@ -9,7 +9,8 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
actions: { actions: {
addSpecialty: VermineCharacterSheetV2.#onAddSpecialty, addSpecialty: VermineCharacterSheetV2.#onAddSpecialty,
openExperiencePhase: VermineCharacterSheetV2.#onOpenExperiencePhase, openExperiencePhase: VermineCharacterSheetV2.#onOpenExperiencePhase,
openLearning: VermineCharacterSheetV2.#onOpenLearning openLearning: VermineCharacterSheetV2.#onOpenLearning,
validatePhase: VermineCharacterSheetV2.#onValidatePhase
} }
} }
@@ -125,4 +126,11 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
const dialog = await LearningDialog.create({ actorId: this.document.id }) const dialog = await LearningDialog.create({ actorId: this.document.id })
if (dialog) dialog.render(true) if (dialog) dialog.render(true)
} }
/** Valide la phase d'Expérience courante et réinitialise les limites. */
static async #onValidatePhase() {
const { VermineExperience } = await import("../../system/experience.mjs")
await VermineExperience.resetPhaseLimits(this.document)
ui.notifications.info(game.i18n.localize("VERMINE.validate_phase_done"))
}
} }
+53 -1
View File
@@ -15,7 +15,8 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
addObjective: VermineGroupSheetV2.#onAddObjective, addObjective: VermineGroupSheetV2.#onAddObjective,
createTotemAbility: VermineGroupSheetV2.#onCreateTotemAbility, createTotemAbility: VermineGroupSheetV2.#onCreateTotemAbility,
openLearning: VermineGroupSheetV2.#onOpenLearning, openLearning: VermineGroupSheetV2.#onOpenLearning,
openExperiencePhase: VermineGroupSheetV2.#onOpenExperiencePhase openExperiencePhase: VermineGroupSheetV2.#onOpenExperiencePhase,
validatePhase: VermineGroupSheetV2.#onValidatePhase
} }
} }
@@ -109,6 +110,50 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
return game.actors.filter(a => a.type === "vehicle" && a.system.ownerId === this.document.id) return game.actors.filter(a => a.type === "vehicle" && a.system.ownerId === this.document.id)
} }
/**
* Dépôt d'un acteur Véhicule sur la fiche : lie le véhicule au Groupe
* (system.ownerId) au lieu de le dupliquer, puis affiche la liste.
* Gère les dépôts depuis un compendium (copie importée dans le monde)
* et demande confirmation si le véhicule appartient déjà à un autre Groupe.
* @override
*/
async _onDropActor(event, data) {
let actor = data instanceof foundry.abstract.Document ? data : await fromUuid(data.uuid)
if (actor?.type !== "vehicle") {
return super._onDropActor?.(event, data)
}
// Véhicule issu d'un compendium : importer une copie dans le monde.
if (actor.compendium) {
try {
const copy = await game.actors.importFromCompendium(actor.compendium, actor._id, { renderSheet: false })
if (!copy) return
actor = copy
} catch {
ui.notifications.warn(game.i18n.localize("VERMINE.vehicle_import_failed"))
return
}
}
// Véhicule déjà rattaché à un autre Groupe : confirmation avant transfert.
const currentOwner = actor.system?.ownerId
if (currentOwner && currentOwner !== this.document.id) {
const other = game.actors.get(currentOwner)
const content = game.i18n.localize("VERMINE.vehicle_assign_confirm").replace("{group}", other?.name ?? currentOwner)
const confirmed = await Dialog.confirm({
title: game.i18n.localize("VERMINE.vehicle_assign"),
content,
defaultYes: false
})
if (!confirmed) return
}
try {
await actor.update({ "system.ownerId": this.document.id })
} catch {
ui.notifications.warn(game.i18n.localize("VERMINE.vehicle_assign_failed"))
return
}
this.render()
}
// Actions : délégation aux applications AppV1 existantes pour TotemPicker/ActorPicker // Actions : délégation aux applications AppV1 existantes pour TotemPicker/ActorPicker
static async #onChooseTotem(event, target) { static async #onChooseTotem(event, target) {
const { TotemPicker } = await import("../../system/applications.mjs") const { TotemPicker } = await import("../../system/applications.mjs")
@@ -172,4 +217,11 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
const dialog = await GroupLearningDialog.create({ actorId: this.document.id }) const dialog = await GroupLearningDialog.create({ actorId: this.document.id })
if (dialog) dialog.render(true) if (dialog) dialog.render(true)
} }
/** Valide la phase d'Expérience courante du Groupe et réinitialise les limites. */
static async #onValidatePhase() {
const { VermineExperience } = await import("../../system/experience.mjs")
await VermineExperience.resetPhaseLimits(this.document)
ui.notifications.info(game.i18n.localize("VERMINE.validate_phase_done"))
}
} }
+3 -1
View File
@@ -182,7 +182,9 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
} }
#getSpecialtyName() { #getSpecialtyName() {
return this.#el.querySelector("#specialty-name")?.value?.trim() || game.i18n.localize("ITEMS.new_specialty"); // Le dialogue peut avoir été fermé pendant le jet (this.#el null).
const el = this.#el?.querySelector("#specialty-name");
return el?.value?.trim() || game.i18n.localize("ITEMS.new_specialty");
} }
#compute() { #compute() {
+20
View File
@@ -213,4 +213,24 @@ export class VermineExperience {
const preferred = actor.system?.skill_categories?.preferred const preferred = actor.system?.skill_categories?.preferred
return Boolean(category && preferred && category === preferred) return Boolean(category && preferred && category === preferred)
} }
/**
* Réinitialise les limites d'apprentissage (learned.*) de la phase
* courante, pour un personnage ou un Groupe, sans toucher au compteur
* de phase (avancé uniquement par le dialogue d'attribution du MJ).
* @param {foundry.abstract.Document} actor personnage ou Groupe
* @returns {Promise<void>}
*/
static async resetPhaseLimits(actor) {
const ex = actor.system?.experience
if (!ex) return
const update = {}
if (ex.learned && typeof ex.learned === "object") {
for (const key of Object.keys(ex.learned)) {
if (key === "phase") continue
update[`system.experience.learned.${key}`] = false
}
}
if (Object.keys(update).length) await actor.update(update)
}
} }
@@ -25,6 +25,9 @@
<button type="button" data-action="openLearning"> <button type="button" data-action="openLearning">
<i class="fas fa-graduation-cap"></i> {{ localize 'VERMINE.open_learning' }} <i class="fas fa-graduation-cap"></i> {{ localize 'VERMINE.open_learning' }}
</button> </button>
<button type="button" data-action="validatePhase">
<i class="fas fa-check"></i> {{ localize 'VERMINE.validate_phase' }}
</button>
</div> </div>
<section class="exp-rewards"> <section class="exp-rewards">
+43 -38
View File
@@ -1,45 +1,50 @@
<div class="tab gear sheet-part {{#if @root.tabs.gear.active}}active{{/if}}" data-group="sheet" data-tab="gear"> <div class="tab gear sheet-part {{#if @root.tabs.gear.active}}active{{/if}}" data-group="sheet" data-tab="gear">
<div class="grid grid-2col"> <div class="weapons">
<div> <h4>{{localize 'ITEMS.weapons'}}</h4>
<ol class="items-list"> {{> "systems/vermine2047/templates/actor/parts/actor-weapons.hbs"}}
<li class="item flexrow items-header"> </div>
<div class="item-name" style="flex:4;">{{ localize 'IDENTITY.name'}}</div> <div class="protections">
<div class="item-quantity">{{ localize 'VERMINE.qty'}}</div> <h4>{{localize 'ITEMS.defenses'}}</h4>
<div class="item-weight">{{ localize 'VERMINE.weight'}}</div> {{> "systems/vermine2047/templates/actor/parts/actor-defenses.hbs"}}
</div>
<div class="objects">
<h4>{{localize 'ITEMS.items'}}</h4>
<ol class="items-list">
<li class="item flexrow items-header">
<div class="item-name" style="flex:4;">{{ localize 'IDENTITY.name'}}</div>
<div class="item-quantity">{{ localize 'VERMINE.qty'}}</div>
<div class="item-weight">{{ localize 'VERMINE.weight'}}</div>
<div class="item-controls">
{{#if isEditMode}}
<a class="item-control item-create" data-action="create" data-type="item" title="Create item"><i class="fas fa-plus"></i></a>
{{/if}}
</div>
</li>
{{#each gear as |item id|}}
<li class="item flexrow flex-group-center" data-item-id="{{item._id}}">
<div class="item-name" style="flex:4;">
<div class="item-image">
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" data-roll-type="item"><img src="{{item.img}}" title="{{item.name}}" width="24" height="24"/></a>
</div>
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">{{item.name}}</a>
</div>
<div><a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">{{item.system.quantity}}</a></div>
<div><a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">{{item.system.weight}}</a></div>
<div class="item-controls"> <div class="item-controls">
{{#if isEditMode}} {{#if @root.isEditMode}}
<a class="item-control item-create" data-action="create" data-type="item" title="Create item"><i class="fas fa-plus"></i></a> <a class="item-control item-delete" data-action="delete" data-item-id="{{item._id}}" title="Delete Item"><i class="fas fa-trash"></i></a>
{{/if}} {{/if}}
</div> </div>
</li> </li>
{{#each gear as |item id|}} {{/each}}
<li class="item flexrow flex-group-center" data-item-id="{{item._id}}"> </ol>
<div class="item-name" style="flex:4;"> </div>
<div class="item-image"> <div class="vehicles">
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" data-roll-type="item"><img src="{{item.img}}" title="{{item.name}}" width="24" height="24"/></a> <h4>{{localize 'ITEMS.vehicles'}}</h4>
</div> {{> "systems/vermine2047/templates/actor/group/group-vehicles.hbs"}}
<a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">{{item.name}}</a> </div>
</div> <div class="equipment-notes">
<div><a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">{{item.system.quantity}}</a></div> <h4>{{ localize 'IDENTITY.notes'}}</h4>
<div><a class="item-control item-edit" data-action="edit" data-item-id="{{item._id}}" title="Edit Item">{{item.system.weight}}</a></div> {{formInput systemFields.equipment.fields.description enriched=enrichedEquipmentNotes value=system.equipment.description name="system.equipment.description" toggled=true}}
<div class="item-controls">
{{#if @root.isEditMode}}
<a class="item-control item-delete" data-action="delete" data-item-id="{{item._id}}" title="Delete Item"><i class="fas fa-trash"></i></a>
{{/if}}
</div>
</li>
{{/each}}
</ol>
</div>
<div>
<h4>{{ localize 'IDENTITY.notes'}}</h4>
{{formInput systemFields.equipment.fields.description enriched=enrichedEquipmentNotes value=system.equipment.description name="system.equipment.description" toggled=true}}
</div>
</div> </div>
<h4>{{localize 'ITEMS.weapons'}}</h4>
{{> "systems/vermine2047/templates/actor/parts/actor-weapons.hbs"}}
<h4>{{localize 'ITEMS.defenses'}}</h4>
{{> "systems/vermine2047/templates/actor/parts/actor-defenses.hbs"}}
<h4>{{localize 'ITEMS.vehicles'}}</h4>
{{> "systems/vermine2047/templates/actor/group/group-vehicles.hbs"}}
</div> </div>
+8
View File
@@ -13,6 +13,13 @@
{{/if}} {{/if}}
<span>/ {{system.reserve.max}}D</span> <span>/ {{system.reserve.max}}D</span>
</div> </div>
<div class="reserve-dice flexrow">
{{#repeat system.reserve.max 1 "d"}}
<div class="hexa {{#iflteq @d @root.system.reserve.value}}checked{{/iflteq}}">
<input type="radio" data-dtype="Number" name="system.reserve.value" value="{{@d}}" data-action="clickRadio" />
</div>
{{/repeat}}
</div>
<div class="morale-control flexrow flex-group-center"> <div class="morale-control flexrow flex-group-center">
<label>{{ localize 'VERMINE.morale' }}:</label> <label>{{ localize 'VERMINE.morale' }}:</label>
{{#if isEditMode}} {{#if isEditMode}}
@@ -46,6 +53,7 @@
<div class="group-experience-actions flexrow flex-group-center"> <div class="group-experience-actions flexrow flex-group-center">
<button type="button" data-action="openLearning"><i class="fas fa-graduation-cap"></i> {{ localize 'VERMINE.open_learning' }}</button> <button type="button" data-action="openLearning"><i class="fas fa-graduation-cap"></i> {{ localize 'VERMINE.open_learning' }}</button>
<button type="button" data-action="openExperiencePhase"><i class="fas fa-star"></i> {{ localize 'VERMINE.experience_phase' }}</button> <button type="button" data-action="openExperiencePhase"><i class="fas fa-star"></i> {{ localize 'VERMINE.experience_phase' }}</button>
<button type="button" data-action="validatePhase"><i class="fas fa-check"></i> {{ localize 'VERMINE.validate_phase' }}</button>
</div> </div>
</div> </div>