diff --git a/css/foundryvtt-reve-de-dragon.css b/css/foundryvtt-reve-de-dragon.css index 5b435f7a..2e66d55a 100644 --- a/css/foundryvtt-reve-de-dragon.css +++ b/css/foundryvtt-reve-de-dragon.css @@ -8737,6 +8737,50 @@ body { .system-foundryvtt-reve-de-dragon .rdd-creation-personnage .wizard-content textarea.attr-input { box-sizing: border-box; } +.system-foundryvtt-reve-de-dragon .rdd-creation-personnage .wizard-content .archetype-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); + gap: 0.5rem; + margin-top: 0.5rem; +} +.system-foundryvtt-reve-de-dragon .rdd-creation-personnage .wizard-content .archetype-grid .archetype-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.3rem; + padding: 0.5rem; + border: 1px solid var(--color-border-light-tertiary); + border-radius: 0.3rem; + cursor: pointer; + text-align: center; + background: rgba(0, 0, 0, 0.06); + transition: background 0.15s, border-color 0.15s; +} +.system-foundryvtt-reve-de-dragon .rdd-creation-personnage .wizard-content .archetype-grid .archetype-card:hover { + background: rgba(0, 0, 0, 0.12); +} +.system-foundryvtt-reve-de-dragon .rdd-creation-personnage .wizard-content .archetype-grid .archetype-card.selected { + border-color: var(--wizard-accent-gold); + background: var(--wizard-step-active); +} +.system-foundryvtt-reve-de-dragon .rdd-creation-personnage .wizard-content .archetype-grid .archetype-card img { + width: 4.5rem; + height: 4.5rem; + object-fit: cover; + border-radius: 0.2rem; +} +.system-foundryvtt-reve-de-dragon .rdd-creation-personnage .wizard-content .archetype-grid .archetype-card .archetype-name { + font-size: 0.85rem; + line-height: 1.1; + min-height: 2.2rem; + display: flex; + align-items: center; +} +.system-foundryvtt-reve-de-dragon .rdd-creation-personnage .wizard-content .archetype-lock { + margin-left: 0.5rem; + font-style: italic; + color: var(--wizard-accent-dream); +} .system-foundryvtt-reve-de-dragon .rdd-creation-personnage .wizard-content .equip-header { margin-bottom: 0.5rem; } @@ -8830,6 +8874,17 @@ body { width: 3rem; text-align: center; } +.system-foundryvtt-reve-de-dragon .rdd-creation-personnage .wizard-warning { + margin: 0.5rem 0; +} +.system-foundryvtt-reve-de-dragon .rdd-creation-personnage .wizard-warning ul { + margin: 0; + padding-left: 1.2rem; + list-style: none; +} +.system-foundryvtt-reve-de-dragon .rdd-creation-personnage .wizard-warning li { + padding: 0.15rem 0; +} .system-foundryvtt-reve-de-dragon .rdd-creation-personnage .wizard-erreurs { margin: 0.5rem 0; } diff --git a/less/foundryvtt-reve-de-dragon.less b/less/foundryvtt-reve-de-dragon.less index 9152e0f1..d5322466 100644 --- a/less/foundryvtt-reve-de-dragon.less +++ b/less/foundryvtt-reve-de-dragon.less @@ -3560,6 +3560,49 @@ .attr-input { width: 100%; } textarea.attr-input { box-sizing: border-box; } + .archetype-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); + gap: 0.5rem; + margin-top: 0.5rem; + .archetype-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.3rem; + padding: 0.5rem; + border: 1px solid var(--color-border-light-tertiary); + border-radius: 0.3rem; + cursor: pointer; + text-align: center; + background: rgba(0, 0, 0, 0.06); + transition: background 0.15s, border-color 0.15s; + &:hover { background: rgba(0, 0, 0, 0.12); } + &.selected { + border-color: var(--wizard-accent-gold); + background: var(--wizard-step-active); + } + img { + width: 4.5rem; + height: 4.5rem; + object-fit: cover; + border-radius: 0.2rem; + } + .archetype-name { + font-size: 0.85rem; + line-height: 1.1; + min-height: 2.2rem; + display: flex; + align-items: center; + } + } + } + .archetype-lock { + margin-left: 0.5rem; + font-style: italic; + color: var(--wizard-accent-dream); + } + .equip-header { margin-bottom: 0.5rem; .budget-labels { @@ -3630,6 +3673,18 @@ } /* ==================== ERREURS ==================== */ + .wizard-warning { + margin: 0.5rem 0; + ul { + margin: 0; + padding-left: 1.2rem; + list-style: none; + } + li { + padding: 0.15rem 0; + } + } + .wizard-erreurs { margin: 0.5rem 0; ul { diff --git a/module/actor/creation/creation-personnage-wizard.mjs b/module/actor/creation/creation-personnage-wizard.mjs index 2df2b045..8eb8b1a2 100644 --- a/module/actor/creation/creation-personnage-wizard.mjs +++ b/module/actor/creation/creation-personnage-wizard.mjs @@ -1,7 +1,8 @@ import { SystemCompendiums } from "../../settings/system-compendiums.js"; import { RdDItemTete } from "../../documents/_module.mjs"; import { Grammar } from "../../grammar.js"; -import { coutCompetenceCreation, coutSortCreation, sortAccessible, niveauxAccessibles } from "./cout-creation.mjs"; +import { ITEM_TYPES } from "../../constants.js"; +import { coutCompetenceCreation, coutPourAtteindre, coutSortCreation, sortAccessible, niveauxAccessibles } from "./cout-creation.mjs"; const { HandlebarsApplicationMixin } = foundry.applications.api; @@ -17,6 +18,11 @@ const TOTAL_CARACS_CIBLE = 160; const TOTAL_XP_PRELIMINAIRE = 3000; const MAX_XP_SORTS = 300; const FORTUNE_DEFAUT = 50; + +const TRONCS_COMPETENCES = [ + ["Esquive", "Dague", "Corps à corps"], + ["Epée à 1 main", "Epée à 2 mains", "Hache à 1 main", "Hache à 2 mains", "Lance", "Masse à 1 main", "Masse à 2 mains"], +]; const NIVEAU_MAX_CREATION = 3; const CARACS_PRIMAIRES = [ @@ -63,18 +69,22 @@ export class CreationPersonnageWizard extends HandlebarsApplicationMixin(foundry this.data = { name: "", hautRevant: false, + archetype: null, caracs: Object.fromEntries(CARACS_PRIMAIRES.map(c => [c, 10])), beaute: 10, competences: [], sortsChoisis: [], attributs: { sexe: "", age: "", taille: "", poids: "", yeux: "", cheveux: "", main: "droitier", heure: "", biographie: "", description: "" }, - equipement: { fortune: FORTUNE_DEFAUT, selection: {}, filtreType: "tous", recherche: "" }, + equipement: { fortune: FORTUNE_DEFAUT, selection: {}, filtreType: "tous", recherche: "", nonMappes: [] }, }; this._competencesBase = []; this._sortsDispo = []; this._equipement = []; this._raceHumain = null; this._donHautReve = null; + this._voyageurs = []; + this._archetypeImg = null; + this._archetypeIdMap = new Map(); this._precharge = false; } @@ -133,23 +143,33 @@ export class CreationPersonnageWizard extends HandlebarsApplicationMixin(foundry if (!this._donHautReve) { this._donHautReve = await RdDItemTete.teteDonDeHautReve(); } + if (this._voyageurs.length === 0) { + const pack = game.packs.get("foundryvtt-reve-de-dragon.voyageurs"); + if (pack) { + this._voyageurs = await pack.getDocuments(); + } + } if (this._equipement.length === 0) { const items = await SystemCompendiums.getItems('equipement'); this._equipement = items .filter(it => it.type !== 'monnaie') - .map(it => ({ - _id: it._id, - name: it.name, - type: it.type, - img: it.img, - system: { + .map(it => { + const structure = { ...it.system }; + return { + _id: it._id, + name: it.name, + type: it.type, + img: it.img, + structure, + system: { + cout: Number(it.system.cout ?? 0), + encombrement: Number(it.system.encombrement ?? 0), + quantite: it.system.quantite ?? 1, + }, cout: Number(it.system.cout ?? 0), encombrement: Number(it.system.encombrement ?? 0), - quantite: it.system.quantite ?? 1, - }, - cout: Number(it.system.cout ?? 0), - encombrement: Number(it.system.encombrement ?? 0), - })); + }; + }); } } @@ -176,6 +196,113 @@ export class CreationPersonnageWizard extends HandlebarsApplicationMixin(foundry })); } + async _appliquerArchetype(voyageurId) { + const voyageur = this._voyageurs.find(v => v.id === voyageurId) || this._voyageurs.find(v => v.name === voyageurId); + if (!voyageur) { + this.data.archetype = null; + return; + } + this.data.archetype = { id: voyageur.id, name: voyageur.name }; + const system = voyageur.system; + + // Caractéristiques + beauté + for (const c of CARACS_PRIMAIRES) { + const v = system.carac?.[c]?.value; + if (typeof v === "number") this.data.caracs[c] = v; + } + if (typeof system.beaute === "number") this.data.beaute = system.beaute; + + // Attributs physiques + for (const key of ["sexe", "age", "taille", "poids", "yeux", "cheveux", "main", "heure"]) { + if (system[key] !== undefined && system[key] !== null) { + this.data.attributs[key] = String(system[key]); + } + } + + // Haut-rêvant : forcé par la présence du don + const aDonHautReve = voyageur.items.some(i => i.type === ITEM_TYPES.tete && Grammar.equalsInsensitive(i.name, "Don de haut-rêve")); + this.data.hautRevant = aDonHautReve; + + // Compétences : mapper par _id (IDs identiques au compendium compétences) + const itemsCompetences = voyageur.items.filter(i => i.type === 'competence'); + for (const comp of this.data.competences) { + const arItem = itemsCompetences.find(i => i._id === comp._id) || itemsCompetences.find(i => Grammar.equalsInsensitive(i.name, comp.name)); + if (arItem && typeof arItem.system.niveau === "number") { + comp.system.niveau = arItem.system.niveau; + } + } + + // Sorts (si HR) : charger les sorts et pré-remplir + this.data.sortsChoisis = []; + if (this.data.hautRevant) { + if (this._sortsDispo.length === 0) { + this._sortsDispo = await this._chargerSorts(); + } + const sortsArchetype = voyageur.items.filter(i => i.type === 'sort'); + this.data.equipement.nonMappes = []; + for (const sortAr of sortsArchetype) { + const sort = this._sortsDispo.find(s => Grammar.equalsInsensitive(s.name, sortAr.name)); + if (sort && !this.data.sortsChoisis.includes(sort._id)) { + this.data.sortsChoisis.push(sort._id); + } else if (!sort) { + this.data.equipement.nonMappes.push(sortAr.name); + } + } + } + + // Équipement : mapper par nom, fortune = total monnaies de l'archétype. + // La structure (dont le contenu des conteneurs) est prise depuis l'item + // du voyageur, les coûts/encombrements depuis le compendium équipement. + this.data.equipement.selection = {}; + this._archetypeIdMap = new Map(); + const itemsEquip = voyageur.items.filter(i => ![ITEM_TYPES.competence, ITEM_TYPES.monnaie, ITEM_TYPES.sort, ITEM_TYPES.tete].includes(i.type)); + for (const item of itemsEquip) { + const equip = this._equipement.find(e => Grammar.equalsInsensitive(e.name, item.name)); + if (equip) { + const quantite = Number(item.system.quantite ?? 1); + if (quantite > 0) { + const structure = { ...item.system }; + delete structure._id; + this._archetypeIdMap.set(item.id, equip._id); + this.data.equipement.selection[equip._id] = { + item: { ...equip, structure }, + quantite, + }; + } + } else { + this.data.equipement.nonMappes.push(item.name); + } + } + const fortune = voyageur.items + .filter(i => i.type === ITEM_TYPES.monnaie) + .reduce((sum, m) => sum + (Number(m.system.cout ?? 0) * Number(m.system.quantite ?? 0)), 0); + this.data.equipement.fortune = Number(fortune.toFixed(2)); + + // Token / image + this._archetypeImg = voyageur.img; + + if (this.data.equipement.nonMappes.length > 0) { + ui.notifications.warn(`Archétype « ${voyageur.name} » : ${this.data.equipement.nonMappes.length} élément(s) non retrouvé(s) dans les compendiums (${this.data.equipement.nonMappes.join(", ")}).`); + } + } + + _resetArchetype() { + this.data.archetype = null; + this.data.caracs = Object.fromEntries(CARACS_PRIMAIRES.map(c => [c, 10])); + this.data.beaute = 10; + this.data.attributs = { sexe: "", age: "", taille: "", poids: "", yeux: "", cheveux: "", main: "droitier", heure: "", biographie: "", description: "" }; + this.data.hautRevant = false; + for (const comp of this.data.competences) { + comp.system.niveau = comp.system.base; + } + this.data.sortsChoisis = []; + this.data.equipement.selection = {}; + this.data.equipement.fortune = FORTUNE_DEFAUT; + this.data.equipement.nonMappes = []; + this._archetypeImg = null; + this._archetypeIdMap = new Map(); + } + _onRender(context, options) { super._onRender(context, options); const el = this.element; @@ -197,6 +324,17 @@ export class CreationPersonnageWizard extends HandlebarsApplicationMixin(foundry this._precharge = false; this.render({ force: true }); })); + el.querySelectorAll(".archetype-card").forEach(card => + card.addEventListener("click", async event => { + const id = event.currentTarget.dataset.archetypeId; + if (!id) { + this._resetArchetype(); + } else { + await this._appliquerArchetype(id); + } + this._precharge = false; + this.render({ force: true }); + })); break; case ETAPE_CARACTERISTIQUES: @@ -345,7 +483,7 @@ export class CreationPersonnageWizard extends HandlebarsApplicationMixin(foundry } else { const budget = this._budgetEquipement(); const depenseDelta = quantite * item.cout - (this.data.equipement.selection[itemId]?.quantite ?? 0) * item.cout; - if (depenseDelta > budget.restant) { + if (depenseDelta > 0 && depenseDelta > budget.restant) { ui.notifications.warn(`Budget dépassé : ${depenseDelta.toFixed(2)} sols nécessaires, ${budget.restant.toFixed(2)} disponibles.`); const input = el.querySelector(`input.equip-qte[data-item-id="${itemId}"]`); if (input) input.value = this.data.equipement.selection[itemId]?.quantite ?? 0; @@ -532,13 +670,29 @@ export class CreationPersonnageWizard extends HandlebarsApplicationMixin(foundry if (c.isDraconic && !this.data.hautRevant) return sum; return sum + coutCompetenceCreation(c, c.system.niveau); }, 0); + const economieTroncs = this._economieTroncs(); const coutSorts = this._budgetSorts().depense; return { - depense, - restant: TOTAL_XP_PRELIMINAIRE - depense - coutSorts, + depense: depense - economieTroncs, + restant: TOTAL_XP_PRELIMINAIRE - (depense - economieTroncs) - coutSorts, }; } + _economieTroncs() { + let economie = 0; + for (const tronc of TRONCS_COMPETENCES) { + const couts = tronc + .map(nom => this.data.competences.find(c => Grammar.equalsInsensitive(c.name, nom))) + .filter(c => c) + .map(c => coutPourAtteindre(c.system.base, Math.min(c.system.niveau, 0))) + .sort((a, b) => a - b); + if (couts.length > 1) { + economie += couts.slice(0, couts.length - 1).reduce((a, b) => a + b, 0); + } + } + return economie; + } + _budgetSorts() { if (!this.data.hautRevant) return { depense: 0, restant: MAX_XP_SORTS }; const depense = this.data.sortsChoisis.reduce((sum, id) => { @@ -640,6 +794,7 @@ export class CreationPersonnageWizard extends HandlebarsApplicationMixin(foundry const actor = await Actor.create({ name: this.data.name || "Nouveau personnage", type: 'personnage', + img: this._archetypeImg, system: { carac: caracsData, beaute: this.data.beaute, @@ -724,18 +879,48 @@ export class CreationPersonnageWizard extends HandlebarsApplicationMixin(foundry async _creerEquipementEtFortune(actor) { const selection = Object.values(this.data.equipement.selection).filter(sel => sel.quantite > 0); if (selection.length > 0) { - const items = selection.map(sel => { + // Créer les items individuellement (les IDs sont générés par Foundry) + // puis mapper ancien ID compendium -> nouveau ID par nom pour reconstruire + // la hiérarchie des conteneurs. + const itemsCrees = []; + for (const sel of selection) { const item = this._equipement.find(it => it._id === sel.item._id); - if (!item) return null; - return { + if (!item) continue; + const system = { + quantite: sel.quantite, + cout: item.cout, + encombrement: item.encombrement, + ...(item.structure ? { ...item.structure } : {}), + }; + delete system.contenu; // recréé après le mapping + delete system._id; + const created = await actor.createEmbeddedDocuments('Item', [{ name: item.name, type: item.type, img: item.img, - system: { quantite: sel.quantite, cout: item.cout, encombrement: item.encombrement }, - }; - }).filter(it => it); - if (items.length > 0) { - await actor.createEmbeddedDocuments('Item', items); + system, + }]); + if (created[0]) { + itemsCrees.push({ ancienId: item._id, nouveau: created[0] }); + } + } + const idMap = new Map(itemsCrees.map(c => [c.ancienId, c.nouveau.id])); + // Mapper les contenus des conteneurs vers les nouveaux IDs + const conteneurs = Object.values(this.data.equipement.selection) + .map(sel => sel.item) + .filter(item => item.type === 'conteneur' && Array.isArray(item.structure?.contenu) && item.structure.contenu.length > 0); + for (const conteneur of conteneurs) { + const nouveauConteneur = actor.items.get(idMap.get(conteneur._id)); + if (!nouveauConteneur) continue; + const nouveauxContenus = conteneur.structure.contenu + .map(ancienIdEnfant => { + const equipId = this._archetypeIdMap.get(ancienIdEnfant) ?? ancienIdEnfant; + return idMap.get(equipId); + }) + .filter(id => id); + if (nouveauxContenus.length > 0) { + await nouveauConteneur.update({ 'system.contenu': nouveauxContenus }); + } } } const budget = this._budgetEquipement(); @@ -771,6 +956,7 @@ export class CreationPersonnageWizard extends HandlebarsApplicationMixin(foundry TOTAL_XP_PRELIMINAIRE, MAX_XP_SORTS, FORTUNE_DEFAUT, + voyageurs: this._voyageurs.map(v => ({ id: v.id, name: v.name, img: v.img })), }; switch (this.step) { case ETAPE_COMPETENCES: diff --git a/templates/actor/creation/wizard-main.hbs b/templates/actor/creation/wizard-main.hbs index da0ca477..180fece8 100644 --- a/templates/actor/creation/wizard-main.hbs +++ b/templates/actor/creation/wizard-main.hbs @@ -21,9 +21,35 @@
Avant de commencer la création, choix fondamental : votre personnage sera-t-il haut-rêvant (magicien) ou non ? Le don de haut-rêve s'acquiert dans l'enfance ; un personnage adulte n'aura pratiquement aucune chance de le devenir ensuite.
Le principe de création est identique, mais les haut-rêvants doivent réserver des points pour les compétences de magie (Draconic) et pour les sorts.
Vous pouvez bâtir votre personnage sur un archétype du compendium Voyageurs : ses caractéristiques, compétences, sorts, équipement et fortune seront pré-remplis, puis personnalisables.
+Votre personnage commence avec une fortune de {{FORTUNE_DEFAUT}} sols (ajustable). Achetez vos vêtements, armes et équipement, puis gardez le reste en espèces.
+ {{#if data.equipement.nonMappes.length}} +Nom : {{data.name}}
Haut-rêvant : {{#if data.hautRevant}}Oui{{else}}Non{{/if}}
+ {{#if data.archetype}}Archétype : {{data.archetype.name}}
{{/if}}