From 7abea8e9d4542078beb962e53721f55da5c1e5a7 Mon Sep 17 00:00:00 2001 From: LeRatierBretonnier Date: Mon, 9 Feb 2026 22:46:44 +0100 Subject: [PATCH] Add class adancement --- assets/icons/advancement.svg | 19 +++ css/fvtt-prism-rpg.css | 112 +++++++++++++++++ lang/en.json | 8 +- module/applications/sheets/class-sheet.mjs | 137 +++++++++++++++++++++ module/models/class.mjs | 67 ++++++++++ styles/class.less | 135 ++++++++++++++++++++ system.json | 2 +- templates/class.hbs | 54 ++++++++ 8 files changed, 532 insertions(+), 2 deletions(-) create mode 100644 assets/icons/advancement.svg diff --git a/assets/icons/advancement.svg b/assets/icons/advancement.svg new file mode 100644 index 0000000..411997b --- /dev/null +++ b/assets/icons/advancement.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/css/fvtt-prism-rpg.css b/css/fvtt-prism-rpg.css index 110e845..9436c5b 100644 --- a/css/fvtt-prism-rpg.css +++ b/css/fvtt-prism-rpg.css @@ -3227,6 +3227,118 @@ i.prismrpg { .prismrpg .class-content label { flex: 10%; } +.prismrpg .class-content .advancement-level { + margin-bottom: 1.5rem; + padding: 0.5rem; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 4px; +} +.prismrpg .class-content .advancement-level h3 { + display: flex; + align-items: center; + justify-content: space-between; + margin: 0 0 0.5rem 0; + padding-bottom: 0.5rem; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); +} +.prismrpg .class-content .advancement-level h3 .level-title { + color: #000000; + font-weight: bold; + font-size: 80%; +} +.prismrpg .class-content .advancement-level h3 .add-advancement { + padding: 0.25rem 0.5rem; + background: var(--color-control-bg); + border: 1px solid var(--color-border-dark); + border-radius: 3px; + cursor: pointer; +} +.prismrpg .class-content .advancement-level h3 .add-advancement:hover { + background: var(--color-control-bg-hover); +} +.prismrpg .class-content .advancement-level h3 .add-advancement i { + margin: 0; +} +.prismrpg .class-content .advancement-level .empty-advancements { + font-style: italic; + color: rgba(0, 0, 0, 0.5); + margin: 0.5rem 0; +} +.prismrpg .class-content .advancement-list { + display: flex; + flex-direction: column; + gap: 1rem; +} +.prismrpg .class-content .advancement-item { + padding: 0.75rem; + background: rgba(0, 0, 0, 0.05); + border-radius: 4px; +} +.prismrpg .class-content .advancement-item .advancement-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} +.prismrpg .class-content .advancement-item .advancement-header .advancement-icon { + width: 40px; + height: 40px; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 3px; + cursor: pointer; + flex-shrink: 0; +} +.prismrpg .class-content .advancement-item .advancement-header .advancement-icon:hover { + border-color: var(--color-border-highlight); +} +.prismrpg .class-content .advancement-item .advancement-header input[type="text"] { + flex: 1; + font-weight: bold; +} +.prismrpg .class-content .advancement-item .advancement-header .toggle-advancement-description { + padding: 0.25rem 0.5rem; + background: var(--color-control-bg); + border: 1px solid var(--color-border-dark); + border-radius: 3px; + cursor: pointer; + flex-shrink: 0; + margin-left: 0.25rem; +} +.prismrpg .class-content .advancement-item .advancement-header .toggle-advancement-description:hover { + background: var(--color-control-bg-hover); +} +.prismrpg .class-content .advancement-item .advancement-header .toggle-advancement-description i { + margin: 0; + color: rgba(0, 0, 0, 0.7); +} +.prismrpg .class-content .advancement-item .advancement-header .delete-advancement { + padding: 0.25rem 0.5rem; + background: var(--color-control-bg); + border: 1px solid var(--color-border-dark); + border-radius: 3px; + cursor: pointer; + flex-shrink: 0; +} +.prismrpg .class-content .advancement-item .advancement-header .delete-advancement:hover { + background: rgba(255, 0, 0, 0.1); + border-color: rgba(255, 0, 0, 0.5); +} +.prismrpg .class-content .advancement-item .advancement-header .delete-advancement i { + margin: 0; + color: rgba(255, 0, 0, 0.7); +} +.prismrpg .class-content .advancement-item .advancement-description { + margin-top: 0.5rem; + overflow: hidden; + transition: max-height 0.3s ease-out, opacity 0.3s ease-out; + max-height: 500px; + opacity: 1; +} +.prismrpg .class-content .advancement-item .advancement-description.collapsed { + max-height: 0; + opacity: 0; + margin-top: 0; +} .prismrpg .character-path-content { font-family: var(--font-primary); font-size: calc(var(--font-size-standard) * 1); diff --git a/lang/en.json b/lang/en.json index 914195d..b438ce2 100644 --- a/lang/en.json +++ b/lang/en.json @@ -688,7 +688,13 @@ "spellcastingTypeMana": "Mana", "spellcastingTypeFaith": "Faith", "attributeBonuses": "Attribute Bonuses", - "classFeatures": "Class Features" + "classFeatures": "Class Features", + "advancement": "Advancement", + "addAdvancement": "Add Advancement", + "deleteAdvancement": "Delete Advancement", + "advancementName": "Advancement Name", + "noAdvancements": "No advancements defined for this level", + "toggleDescription": "Toggle Description" }, "CoreSkill": { "acrobatics": "Acrobatics", diff --git a/module/applications/sheets/class-sheet.mjs b/module/applications/sheets/class-sheet.mjs index 84faf63..55cd5ce 100644 --- a/module/applications/sheets/class-sheet.mjs +++ b/module/applications/sheets/class-sheet.mjs @@ -32,6 +32,7 @@ export default class PrismRPGClassSheet extends PrismRPGItemSheet { #getTabs() { const tabs = { details: { id: "details", group: "primary", label: "PRISMRPG.Label.details" }, + advancements: { id: "advancements", group: "primary", label: "PRISMRPG.Label.advancement" }, description: { id: "description", group: "primary", label: "PRISMRPG.Label.description" }, } for (const v of Object.values(tabs)) { @@ -56,6 +57,142 @@ export default class PrismRPGClassSheet extends PrismRPGItemSheet { context.enrichedFeatures[key] = await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.features[key], { async: true }) } + // Enrich all advancement descriptions + context.enrichedAdvancements = {} + context.advancementsByLevel = [] + + for (let i = 1; i <= 10; i++) { + const key = `level${i}` + const advancements = this.document.system.advancements[key] || [] + context.enrichedAdvancements[key] = [] + + const enrichedAdvancementsList = [] + for (let j = 0; j < advancements.length; j++) { + const enrichedDesc = await foundry.applications.ux.TextEditor.implementation.enrichHTML(advancements[j].description, { async: true }) + const enrichedAdv = { + ...advancements[j], + enrichedDescription: enrichedDesc, + index: j, + levelKey: key + } + context.enrichedAdvancements[key].push(enrichedAdv) + enrichedAdvancementsList.push(enrichedAdv) + } + + context.advancementsByLevel.push({ + level: i, + levelKey: key, + advancements: enrichedAdvancementsList + }) + } + return context } + + /** @override */ + _onRender(context, options) { + super._onRender(context, options) + + // Add advancement button listeners + this.element.querySelectorAll(".add-advancement").forEach(btn => { + btn.addEventListener("click", this._onAddAdvancement.bind(this)) + }) + + // Delete advancement button listeners + this.element.querySelectorAll(".delete-advancement").forEach(btn => { + btn.addEventListener("click", this._onDeleteAdvancement.bind(this)) + }) + + // Edit advancement icon listeners + this.element.querySelectorAll(".advancement-icon").forEach(img => { + img.addEventListener("click", this._onEditAdvancementIcon.bind(this)) + }) + + // Toggle advancement description listeners + this.element.querySelectorAll(".toggle-advancement-description").forEach(btn => { + btn.addEventListener("click", this._onToggleAdvancementDescription.bind(this)) + }) + } + + /** + * Handle toggling advancement description visibility + * @param {Event} event + */ + _onToggleAdvancementDescription(event) { + event.preventDefault() + const button = event.currentTarget + const item = button.closest(".advancement-item") + const description = item.querySelector(".advancement-description") + const icon = button.querySelector("i") + + description.classList.toggle("collapsed") + + if (description.classList.contains("collapsed")) { + icon.classList.remove("fa-chevron-up") + icon.classList.add("fa-chevron-down") + } else { + icon.classList.remove("fa-chevron-down") + icon.classList.add("fa-chevron-up") + } + } + + /** + * Handle adding a new advancement to a level + * @param {Event} event + */ + async _onAddAdvancement(event) { + event.preventDefault() + const level = event.currentTarget.dataset.level + const advancements = foundry.utils.deepClone(this.document.system.advancements[level] || []) + + advancements.push({ + icon: "systems/fvtt-prism-rpg/assets/icons/advancement.svg", + name: "", + description: "" + }) + + await this.document.update({ + [`system.advancements.${level}`]: advancements + }) + } + + /** + * Handle deleting an advancement from a level + * @param {Event} event + */ + async _onDeleteAdvancement(event) { + event.preventDefault() + const level = event.currentTarget.dataset.level + const index = parseInt(event.currentTarget.dataset.index) + + const advancements = foundry.utils.deepClone(this.document.system.advancements[level] || []) + advancements.splice(index, 1) + + await this.document.update({ + [`system.advancements.${level}`]: advancements + }) + } + + /** + * Handle editing an advancement icon + * @param {Event} event + */ + async _onEditAdvancementIcon(event) { + event.preventDefault() + const level = event.currentTarget.dataset.level + const index = parseInt(event.currentTarget.dataset.index) + + const fp = new FilePicker({ + type: "image", + current: this.document.system.advancements[level][index].icon, + callback: async (path) => { + const advancements = foundry.utils.deepClone(this.document.system.advancements[level] || []) + advancements[index].icon = path + await this.document.update({ + [`system.advancements.${level}`]: advancements + }) + } + }) + fp.render(true) + } } diff --git a/module/models/class.mjs b/module/models/class.mjs index 9c60726..192e8f5 100644 --- a/module/models/class.mjs +++ b/module/models/class.mjs @@ -68,6 +68,41 @@ export default class PrismRPGClass extends foundry.abstract.TypeDataModel { level10: new fields.HTMLField({ initial: "" }) }) + // Advancements (list of advancements per level with icon, name and description) + const advancementSchema = () => new fields.ArrayField( + new fields.SchemaField({ + icon: new fields.StringField({ + required: true, + initial: "", + label: "Icon" + }), + name: new fields.StringField({ + required: true, + initial: "", + label: "Name" + }), + description: new fields.HTMLField({ + required: true, + initial: "", + label: "Description" + }) + }), + { initial: [] } + ) + + schema.advancements = new fields.SchemaField({ + level1: advancementSchema(), + level2: advancementSchema(), + level3: advancementSchema(), + level4: advancementSchema(), + level5: advancementSchema(), + level6: advancementSchema(), + level7: advancementSchema(), + level8: advancementSchema(), + level9: advancementSchema(), + level10: advancementSchema() + }) + // Proficiencies granted by this class schema.weaponProficiencies = new fields.StringField({ required: true, @@ -156,4 +191,36 @@ export default class PrismRPGClass extends foundry.abstract.TypeDataModel { } return features } + + /** + * Get the current level's advancements + */ + get currentLevelAdvancements() { + return this.advancements[`level${this.level}`] || [] + } + + /** + * Get all advancements up to current level + */ + get allAdvancementsUpToLevel() { + const advancements = [] + for (let i = 1; i <= this.level; i++) { + const levelAdvancements = this.advancements[`level${i}`] + if (levelAdvancements && levelAdvancements.length > 0) { + advancements.push({ + level: i, + advancements: levelAdvancements + }) + } + } + return advancements + } + + /** + * Get advancements for a specific level + */ + getAdvancementsForLevel(level) { + if (level < 1 || level > 10) return [] + return this.advancements[`level${level}`] || [] + } } diff --git a/styles/class.less b/styles/class.less index 91bdbd4..5440fba 100644 --- a/styles/class.less +++ b/styles/class.less @@ -13,4 +13,139 @@ label { flex: 10%; } + + .advancement-level { + margin-bottom: 1.5rem; + padding: 0.5rem; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 4px; + + h3 { + display: flex; + align-items: center; + justify-content: space-between; + margin: 0 0 0.5rem 0; + padding-bottom: 0.5rem; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + + .level-title { + color: #000000; + font-weight: bold; + font-size: 80%; + } + + .add-advancement { + padding: 0.25rem 0.5rem; + background: var(--color-control-bg); + border: 1px solid var(--color-border-dark); + border-radius: 3px; + cursor: pointer; + + &:hover { + background: var(--color-control-bg-hover); + } + + i { + margin: 0; + } + } + } + + .empty-advancements { + font-style: italic; + color: rgba(0, 0, 0, 0.5); + margin: 0.5rem 0; + } + } + + .advancement-list { + display: flex; + flex-direction: column; + gap: 1rem; + } + + .advancement-item { + padding: 0.75rem; + background: rgba(0, 0, 0, 0.05); + border-radius: 4px; + + .advancement-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; + + .advancement-icon { + width: 40px; + height: 40px; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 3px; + cursor: pointer; + flex-shrink: 0; + + &:hover { + border-color: var(--color-border-highlight); + } + } + + input[type="text"] { + flex: 1; + font-weight: bold; + } + + .toggle-advancement-description { + padding: 0.25rem 0.5rem; + background: var(--color-control-bg); + border: 1px solid var(--color-border-dark); + border-radius: 3px; + cursor: pointer; + flex-shrink: 0; + margin-left: 0.25rem; + + &:hover { + background: var(--color-control-bg-hover); + } + + i { + margin: 0; + color: rgba(0, 0, 0, 0.7); + } + } + + .delete-advancement { + padding: 0.25rem 0.5rem; + background: var(--color-control-bg); + border: 1px solid var(--color-border-dark); + border-radius: 3px; + cursor: pointer; + flex-shrink: 0; + + &:hover { + background: rgba(255, 0, 0, 0.1); + border-color: rgba(255, 0, 0, 0.5); + } + + i { + margin: 0; + color: rgba(255, 0, 0, 0.7); + } + } + } + + .advancement-description { + margin-top: 0.5rem; + overflow: hidden; + transition: + max-height 0.3s ease-out, + opacity 0.3s ease-out; + max-height: 500px; + opacity: 1; + + &.collapsed { + max-height: 0; + opacity: 0; + margin-top: 0; + } + } + } } diff --git a/system.json b/system.json index 3103e22..a815efe 100644 --- a/system.json +++ b/system.json @@ -6,7 +6,7 @@ "download": "#{DOWNLOAD}#", "url": "#{URL}#", "license": "LICENSE", - "version": "13.0.0", + "version": "13.0.1", "authors": [ { "name": "Uberwald", diff --git a/templates/class.hbs b/templates/class.hbs index aeb8e78..7bea5f6 100644 --- a/templates/class.hbs +++ b/templates/class.hbs @@ -7,6 +7,7 @@ {{! Navigation des onglets }} @@ -73,6 +74,59 @@ + {{! Onglet Advancements }} +
+
+ {{localize "PRISMRPG.Label.advancement"}} + + {{#each advancementsByLevel as |levelData|}} +
+

+ {{localize "PRISMRPG.Label.level"}} {{levelData.level}} + +

+ + {{#if levelData.advancements.length}} +
+ {{#each levelData.advancements as |advancement|}} +
+
+ + {{formInput + ../../systemFields.advancements.fields.level1.element.fields.name + value=advancement.name + name=(concat "system.advancements." advancement.levelKey "." advancement.index ".name") + placeholder=(localize "PRISMRPG.Label.advancementName") + }} + + +
+ +
+ {{/each}} +
+ {{else}} +

{{localize "PRISMRPG.Label.noAdvancements"}}

+ {{/if}} +
+ {{/each}} +
+
+ {{! Onglet Description }}