From 32730b3aac1c157763b750419ca1d66b23434770 Mon Sep 17 00:00:00 2001 From: Dapoulp <74197441+Dapoulp@users.noreply.github.com> Date: Wed, 4 Jun 2025 00:46:05 +0200 Subject: [PATCH 1/5] Feature/89 gm fear display (#104) * gm fear display * clean up * Make Fear Panel resizable * Update for evil light mode * Fix clients fear update * minimizable false * hover animation * fix --- module/applications/resources.mjs | 13 +++++++------ styles/resources.less | 7 ++++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/module/applications/resources.mjs b/module/applications/resources.mjs index 86f85178..b25e374f 100644 --- a/module/applications/resources.mjs +++ b/module/applications/resources.mjs @@ -20,14 +20,15 @@ export default class Resources extends HandlebarsApplicationMixin(ApplicationV2) classes: [], tag: "div", window: { - frame: true, - title: "Fear", - positioned: true, - resizable: true + frame: true, + title: "Fear", + positioned: true, + resizable: true, + minimizable: false }, actions: { - setFear: Resources.setFear, - increaseFear: Resources.increaseFear + setFear: Resources.setFear, + increaseFear: Resources.increaseFear }, position: { width: 222, diff --git a/styles/resources.less b/styles/resources.less index 71a00c59..ce0f9560 100644 --- a/styles/resources.less +++ b/styles/resources.less @@ -2,12 +2,17 @@ --primary-color-fear: rgba(9, 71, 179, .75); --secondary-color-fear: rgba(9, 71, 179, .75); --shadow-text-stroke: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; + --fear-animation : background .3s ease, box-shadow .3s ease, border-color .3s ease, opacity .3s ease; } #resources { min-height: calc(var(--header-height) + 4rem); min-width: 4rem; color: #d3d3d3; + transition: var(--fear-animation); + header, .controls, .window-resize-handle { + transition: var(--fear-animation); + } .window-content { padding: .5rem; #resource-fear { @@ -104,7 +109,7 @@ box-shadow: unset; border-color: transparent; header, .controls, .window-resize-handle { - visibility: hidden; + opacity: 0; } } &:has(.fear-bar) { From aa8fe6a7a1c8bd5c6b56bda4e9d605cef217a68f Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sat, 7 Jun 2025 00:06:54 +0200 Subject: [PATCH 2/5] Combat and CombatTracker (#108) * Added Combat and CombatTracker * Some cleneaup * Fixing and cleaning up * Added categories for combatants * Style improvements * Layout change --- daggerheart.mjs | 16 +- lang/en.json | 27 ++ module/applications/resources.mjs | 171 ++++++----- module/applications/settings.mjs | 35 ++- .../settings/variantRuleSettings.mjs | 59 ++++ module/config/settingsConfig.mjs | 7 +- module/data/_module.mjs | 4 +- module/data/combat.mjs | 7 +- module/data/combatant.mjs | 7 +- module/data/settings/VariantRules.mjs | 13 + module/documents/combat.mjs | 49 +-- module/ui/combatTracker.mjs | 287 ++++++------------ module/ui/players.mjs | 53 ---- styles/daggerheart.css | 138 ++++++--- styles/less/global/elements.less | 16 + styles/ui.less | 137 +++++---- templates/settings/variant-rules.hbs | 22 ++ templates/ui/combat/combatTracker.hbs | 4 + templates/ui/combat/combatTrackerFooter.hbs | 17 ++ templates/ui/combat/combatTrackerHeader.hbs | 86 ++++++ templates/ui/combat/combatTrackerSection.hbs | 64 ++++ templates/ui/combatTracker.hbs | 160 ---------- templates/ui/players.hbs | 35 --- 23 files changed, 730 insertions(+), 684 deletions(-) create mode 100644 module/applications/settings/variantRuleSettings.mjs create mode 100644 module/data/settings/VariantRules.mjs delete mode 100644 module/ui/players.mjs create mode 100644 templates/settings/variant-rules.hbs create mode 100644 templates/ui/combat/combatTracker.hbs create mode 100644 templates/ui/combat/combatTrackerFooter.hbs create mode 100644 templates/ui/combat/combatTrackerHeader.hbs create mode 100644 templates/ui/combat/combatTrackerSection.hbs delete mode 100644 templates/ui/combatTracker.hbs delete mode 100644 templates/ui/players.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index 27979ae1..0153a62c 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -3,11 +3,10 @@ import * as applications from './module/applications/_module.mjs'; import * as models from './module/data/_module.mjs'; import * as documents from './module/documents/_module.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; -import DhpCombatTracker from './module/ui/combatTracker.mjs'; +import DhCombatTracker from './module/ui/combatTracker.mjs'; import { GMUpdateEvent, handleSocketEvent, socketEvent } from './module/helpers/socket.mjs'; import { registerDHSettings } from './module/applications/settings.mjs'; import DhpChatLog from './module/ui/chatLog.mjs'; -import DhpPlayers from './module/ui/players.mjs'; import DhpRuler from './module/ui/ruler.mjs'; import DhpTokenRuler from './module/ui/tokenRuler.mjs'; import { dualityRollEnricher } from './module/enrichers/DualityRollEnricher.mjs'; @@ -74,11 +73,11 @@ Hooks.once('init', () => { Actors.registerSheet(SYSTEM.id, applications.DhpEnvironment, { types: ['environment'], makeDefault: true }); CONFIG.Combat.dataModels = { - base: models.DhpCombat + base: models.DhCombat }; CONFIG.Combatant.dataModels = { - base: models.DhpCombatant + base: models.DhCombatant }; CONFIG.ChatMessage.dataModels = { @@ -91,7 +90,7 @@ Hooks.once('init', () => { CONFIG.Canvas.rulerClass = DhpRuler; CONFIG.Combat.documentClass = documents.DhpCombat; - CONFIG.ui.combat = DhpCombatTracker; + CONFIG.ui.combat = DhCombatTracker; CONFIG.ui.chat = DhpChatLog; // CONFIG.ui.players = DhpPlayers; CONFIG.Token.rulerClass = DhpTokenRuler; @@ -111,8 +110,8 @@ Hooks.once('init', () => { Hooks.on('ready', () => { ui.resources = new CONFIG.ui.resources(); - ui.resources.render({force: true}); -}) + ui.resources.render({ force: true }); +}); Hooks.once('dicesoniceready', () => {}); @@ -295,6 +294,7 @@ const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/sheets/pc/parts/heritageCard.hbs', 'systems/daggerheart/templates/sheets/pc/parts/advancementCard.hbs', 'systems/daggerheart/templates/views/parts/level.hbs', - 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' + 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs', + 'systems/daggerheart/templates/ui/combat/combatTrackerSection.hbs' ]); }; diff --git a/lang/en.json b/lang/en.json index 75a803e1..a2656b4f 100755 --- a/lang/en.json +++ b/lang/en.json @@ -61,6 +61,13 @@ "outline": "Outline", "edge": "Edge" } + }, + "VariantRules": { + "title": "Variant Rules", + "label": "Variant Rules", + "hint": "Apply variant rules from the Daggerheart system", + "name": "Variant Rules", + "actionTokens": "Action Tokens" } }, "Automation": { @@ -101,6 +108,12 @@ "Hint": "Enable measuring of ranges with the ruler according to set distances." } }, + "VariantRules": { + "ActionTokens": { + "Name": "Action Tokens", + "Hint": "Give each player action tokens to use in combat" + } + }, "DualityRollColor": { "Name": "Duality Roll Colour Scheme", "Hint": "The display type for Duality Rolls", @@ -150,6 +163,14 @@ "Or": "Or", "Description": "Description", "Features": "Features", + "Adversary": { + "Singular": "Adversary", + "Plural": "Adversaries" + }, + "Character": { + "Singular": "Character", + "Plural": "Characters" + }, "RefreshType": { "Session": "Session", "Shortrest": "Short Rest", @@ -329,6 +350,12 @@ "grimoire": "Grimoire" } }, + "Combat": { + "giveSpotlight": "Give The Spotlight", + "requestSpotlight": "Request The Spotlight", + "requestingSpotlight": "Requesting The Spotlight", + "combatStarted": "Active" + }, "LevelUp": { "Tier1": { "Label": "Level 2-4", diff --git a/module/applications/resources.mjs b/module/applications/resources.mjs index b25e374f..bbd47fc5 100644 --- a/module/applications/resources.mjs +++ b/module/applications/resources.mjs @@ -1,4 +1,3 @@ - const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; /** @@ -10,101 +9,101 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; */ export default class Resources extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(options={}) { - super(options); - } - - /** @inheritDoc */ - static DEFAULT_OPTIONS = { - id: "resources", - classes: [], - tag: "div", - window: { - frame: true, - title: "Fear", - positioned: true, - resizable: true, - minimizable: false - }, - actions: { - setFear: Resources.setFear, - increaseFear: Resources.increaseFear - }, - position: { - width: 222, - height: 222, - // top: "200px", - // left: "120px" + constructor(options = {}) { + super(options); } - }; - /** @override */ - static PARTS = { - resources: { - root: true, - template: "systems/daggerheart/templates/views/resources.hbs" - // template: "templates/ui/players.hbs" + /** @inheritDoc */ + static DEFAULT_OPTIONS = { + id: 'resources', + classes: [], + tag: 'div', + window: { + frame: true, + title: 'Fear', + positioned: true, + resizable: true, + minimizable: false + }, + actions: { + setFear: Resources.setFear, + increaseFear: Resources.increaseFear + }, + position: { + width: 222, + height: 222 + // top: "200px", + // left: "120px" + } + }; + + /** @override */ + static PARTS = { + resources: { + root: true, + template: 'systems/daggerheart/templates/views/resources.hbs' + } + }; + + get currentFear() { + return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); } - }; - get currentFear() { - return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); - } + get maxFear() { + return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.MaxFear); + } - get maxFear() { - return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.MaxFear); - } + /* -------------------------------------------- */ + /* Rendering */ + /* -------------------------------------------- */ - /* -------------------------------------------- */ - /* Rendering */ - /* -------------------------------------------- */ + /** @override */ + async _prepareContext(_options) { + const display = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.DisplayFear), + current = this.currentFear, + max = this.maxFear, + percent = (current / max) * 100, + isGM = game.user.isGM; + // Return the data for rendering + return { display, current, max, percent, isGM }; + } - /** @override */ - async _prepareContext(_options) { - const display = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.DisplayFear), - current = this.currentFear, - max = this.maxFear, - percent = (current / max) * 100, - isGM = game.user.isGM; - // Return the data for rendering - return {display, current, max, percent, isGM}; - } + /** @override */ + async _preFirstRender(context, options) { + options.position = game.user.getFlag(SYSTEM.id, 'app.resources.position') ?? Resources.DEFAULT_OPTIONS.position; + } - /** @override */ - async _preFirstRender(context, options) { - options.position = game.user.getFlag(SYSTEM.id, 'app.resources.position') ?? Resources.DEFAULT_OPTIONS.position; - } + /** @override */ + async _preRender(context, options) { + if (this.currentFear > this.maxFear) + await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, this.maxFear); + } - /** @override */ - async _preRender(context, options) { - if(this.currentFear > this.maxFear) await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, this.maxFear); - } + _onPosition(position) { + game.user.setFlag(SYSTEM.id, 'app.resources.position', position); + } - _onPosition(position) { - game.user.setFlag(SYSTEM.id, 'app.resources.position', position); - } + async close(options = {}) { + if (!options.allowed) return; + else super.close(options); + } - async close(options={}) { - if(!options.allowed) return; - else super.close(options); - } + static async setFear(event, target) { + if (!game.user.isGM) return; + const fearCount = Number(target.dataset.index ?? 0); + await this.updateFear(this.currentFear === fearCount + 1 ? fearCount : fearCount + 1); + } - static async setFear(event, target) { - if(!game.user.isGM) return; - const fearCount = Number(target.dataset.index ?? 0); - await this.updateFear(this.currentFear === fearCount + 1 ? fearCount : fearCount + 1); - } + static async increaseFear(event, target) { + let value = target.dataset.increment ?? 0, + operator = value.split('')[0] ?? null; + value = Number(value); + await this.updateFear(operator ? this.currentFear + value : value); + } - static async increaseFear(event, target) { - let value = target.dataset.increment ?? 0, - operator = value.split('')[0] ?? null; - value = Number(value); - await this.updateFear(operator ? this.currentFear + value : value); - } - - async updateFear(value) { - if(!game.user.isGM) return; - value = Math.max(0, Math.min(this.maxFear, value)); - await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, value); - } -} \ No newline at end of file + async updateFear(value) { + if (!game.user.isGM) return; + value = Math.max(0, Math.min(this.maxFear, value)); + await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, value); + } +} diff --git a/module/applications/settings.mjs b/module/applications/settings.mjs index 4a885d17..291b0882 100644 --- a/module/applications/settings.mjs +++ b/module/applications/settings.mjs @@ -1,5 +1,7 @@ import DhAppearance from '../data/settings/Appearance.mjs'; import DHAppearanceSettings from './settings/appearanceSettings.mjs'; +import DhVariantRules from '../data/settings/VariantRules.mjs'; +import DHVariantRuleSettings from './settings/variantRuleSettings.mjs'; class DhpAutomationSettings extends FormApplication { constructor(object = {}, options = {}) { @@ -181,7 +183,8 @@ export const registerDHSettings = () => { type: Number, default: 0, onChange: () => { - if(ui.resources) ui.resources.render({force: true}); + if (ui.resources) ui.resources.render({ force: true }); + ui.combat.render({ force: true }); } }); @@ -193,7 +196,7 @@ export const registerDHSettings = () => { type: Number, default: 12, onChange: () => { - if(ui.resources) ui.resources.render({force: true}); + if (ui.resources) ui.resources.render({ force: true }); } }); @@ -204,15 +207,15 @@ export const registerDHSettings = () => { config: true, type: String, choices: { - 'token': 'Tokens', - 'bar': 'Bar', - 'hide': 'Hide' + token: 'Tokens', + bar: 'Bar', + hide: 'Hide' }, default: 'token', onChange: value => { - if(ui.resources) { - if(value === 'hide') ui.resources.close({allowed: true}); - else ui.resources.render({force: true}); + if (ui.resources) { + if (value === 'hide') ui.resources.close({ allowed: true }); + else ui.resources.render({ force: true }); } } }); @@ -251,6 +254,13 @@ export const registerDHSettings = () => { } }); + game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.variantRules, { + scope: 'world', + config: false, + type: DhVariantRules, + default: DhVariantRules.defaultSchema + }); + game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance, { scope: 'client', config: false, @@ -291,4 +301,13 @@ export const registerDHSettings = () => { type: DHAppearanceSettings, restricted: false }); + + game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.VariantRules.Name, { + name: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.title'), + label: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.label'), + hint: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.hint'), + icon: SYSTEM.SETTINGS.menu.VariantRules.Icon, + type: DHVariantRuleSettings, + restricted: false + }); }; diff --git a/module/applications/settings/variantRuleSettings.mjs b/module/applications/settings/variantRuleSettings.mjs new file mode 100644 index 00000000..101d3e42 --- /dev/null +++ b/module/applications/settings/variantRuleSettings.mjs @@ -0,0 +1,59 @@ +import DhVariantRules from '../../data/settings/VariantRules.mjs'; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export default class DHVariantRuleSettings extends HandlebarsApplicationMixin(ApplicationV2) { + constructor() { + super({}); + + this.settings = new DhVariantRules( + game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.variantRules).toObject() + ); + } + + get title() { + return game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.name'); + } + + static DEFAULT_OPTIONS = { + tag: 'form', + id: 'daggerheart-appearance-settings', + classes: ['daggerheart', 'setting', 'dh-style'], + position: { width: '600', height: 'auto' }, + actions: { + reset: this.reset, + save: this.save + }, + form: { handler: this.updateData, submitOnChange: true } + }; + + static PARTS = { + main: { + template: 'systems/daggerheart/templates/settings/variant-rules.hbs' + } + }; + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.settingFields = this.settings; + + return context; + } + + static async updateData(event, element, formData) { + const updatedSettings = foundry.utils.expandObject(formData.object); + + await this.settings.updateSource(updatedSettings); + this.render(); + } + + static async reset() { + this.settings = new DhVariantRules(); + this.render(); + } + + static async save() { + await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.variantRules, this.settings.toObject()); + this.close(); + } +} diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index 26de2a48..b2e17549 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -10,6 +10,10 @@ export const menu = { Range: { Name: 'GameSettingsRange', Icon: 'fa-solid fa-ruler' + }, + VariantRules: { + Name: 'GameSettingsVariantrules', + Icon: 'fa-solid fa-scale-balanced' } }; @@ -27,5 +31,6 @@ export const gameSettings = { AbilityArray: 'AbilityArray', RangeMeasurement: 'RangeMeasurement' }, - appearance: 'Appearance' + appearance: 'Appearance', + variantRules: 'VariantRules' }; diff --git a/module/data/_module.mjs b/module/data/_module.mjs index 4822229f..dfd69ad2 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -1,8 +1,8 @@ export { default as DhpPC } from './pc.mjs'; export { default as DhpClass } from './class.mjs'; export { default as DhpSubclass } from './subclass.mjs'; -export { default as DhpCombat } from './combat.mjs'; -export { default as DhpCombatant } from './combatant.mjs'; +export { default as DhCombat } from './combat.mjs'; +export { default as DhCombatant } from './combatant.mjs'; export { default as DhpAdversary } from './adversary.mjs'; export { default as DhpFeature } from './feature.mjs'; export { default as DhpDomainCard } from './domainCard.mjs'; diff --git a/module/data/combat.mjs b/module/data/combat.mjs index 3ad52b8b..e0490286 100644 --- a/module/data/combat.mjs +++ b/module/data/combat.mjs @@ -1,9 +1,6 @@ -export default class DhpCombat extends foundry.abstract.TypeDataModel { +export default class DhCombat extends foundry.abstract.TypeDataModel { static defineSchema() { const fields = foundry.data.fields; - return { - actions: new fields.NumberField({ initial: 0, integer: true }), - activeCombatant: new fields.StringField({}) - }; + return {}; } } diff --git a/module/data/combatant.mjs b/module/data/combatant.mjs index 60c32db6..cae5d08f 100644 --- a/module/data/combatant.mjs +++ b/module/data/combatant.mjs @@ -1,8 +1,11 @@ -export default class DhpCombatant extends foundry.abstract.TypeDataModel { +export default class DhCombatant extends foundry.abstract.TypeDataModel { static defineSchema() { const fields = foundry.data.fields; return { - active: new fields.BooleanField({ initial: false }) + spotlight: new fields.SchemaField({ + requesting: new fields.BooleanField({ required: true, initial: false }) + }), + actionTokens: new fields.NumberField({ required: true, integer: true, initial: 3 }) }; } } diff --git a/module/data/settings/VariantRules.mjs b/module/data/settings/VariantRules.mjs new file mode 100644 index 00000000..2a1f948d --- /dev/null +++ b/module/data/settings/VariantRules.mjs @@ -0,0 +1,13 @@ +export default class DhVariantRules extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + return { + actionTokens: new fields.SchemaField({ + enabled: new fields.BooleanField({ required: true, initial: false }), + tokens: new fields.NumberField({ required: true, integer: true, initial: 3 }) + }) + }; + } + + static defaultSchema = {}; +} diff --git a/module/documents/combat.mjs b/module/documents/combat.mjs index c7905605..3ad3189e 100644 --- a/module/documents/combat.mjs +++ b/module/documents/combat.mjs @@ -1,44 +1,19 @@ -import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs'; - export default class DhpCombat extends Combat { - _sortCombatants(a, b) { - if (a.isNPC !== b.isNPC) { - const aVal = a.isNPC ? 0 : 1; - const bVal = b.isNPC ? 0 : 1; + async startCombat() { + this._playCombatSound('startEncounter'); + const updateData = { round: 1, turn: null }; + Hooks.callAll('combatStart', this, updateData); + await this.update(updateData); + return this; + } - return aVal - bVal; + _sortCombatants(a, b) { + const aNPC = Number(a.isNPC); + const bNPC = Number(b.isNPC); + if (aNPC !== bNPC) { + return aNPC - bNPC; } return a.name.localeCompare(b.name); } - - async useActionToken(combatantId) { - const automateActionPoints = await game.settings.get( - SYSTEM.id, - SYSTEM.SETTINGS.gameSettings.Automation.ActionPoints - ); - - if (game.user.isGM) { - if (this.system.actions < 1) return; - - const update = automateActionPoints - ? { 'system.activeCombatant': combatantId, 'system.actions': Math.max(this.system.actions - 1, 0) } - : { 'system.activeCombatant': combatantId }; - - await this.update(update); - } else { - const update = automateActionPoints - ? { 'system.activeCombatant': combatantId, 'system.actions': this.system.actions + 1 } - : { 'system.activeCombatant': combatantId }; - - await game.socket.emit(`system.${SYSTEM.id}`, { - action: socketEvent.GMUpdate, - data: { - action: GMUpdateEvent.UpdateDocument, - uuid: this.uuid, - update: update - } - }); - } - } } diff --git a/module/ui/combatTracker.mjs b/module/ui/combatTracker.mjs index 86002cb6..46f7f318 100644 --- a/module/ui/combatTracker.mjs +++ b/module/ui/combatTracker.mjs @@ -1,199 +1,100 @@ -import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs'; - -export default class DhpCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { - constructor(data, context) { - super(data, context); - - Hooks.on(socketEvent.DhpFearUpdate, this.onFearUpdate); - } - - get template() { - return 'systems/daggerheart/templates/ui/combatTracker.hbs'; - } - - activateListeners(html) { - super.activateListeners(html); - html.on('click', '.token-action-tokens .use-action-token', this.useActionToken.bind(this)); - html.on('click', '.encounter-gm-resources .trade-actions', this.tradeActions.bind(this)); - html.on('click', '.encounter-gm-resources .trade-fear', this.tradeFear.bind(this)); - html.on('click', '.encounter-gm-resources .icon-button.up', this.increaseResource.bind(this)); - html.on('click', '.encounter-gm-resources .icon-button.down', this.decreaseResource.bind(this)); - } - - async useActionToken(event) { - event.stopPropagation(); - const combatant = event.currentTarget.dataset.combatant; - await game.combat.useActionToken(combatant); - } - - async tradeActions(event) { - if (event.currentTarget.classList.contains('disabled')) return; - - const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); - const value = currentFear + 1; - - if (value <= 6) { - Hooks.callAll(socketEvent.GMUpdate, GMUpdateEvent.UpdateFear, null, value); - await game.socket.emit(`system.${SYSTEM.id}`, { - action: socketEvent.GMUpdate, - data: { action: GMUpdateEvent.UpdateFear, update: value } - }); - await game.combat.update({ 'system.actions': game.combat.system.actions - 2 }); +export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { + static DEFAULT_OPTIONS = { + actions: { + requestSpotlight: this.requestSpotlight, + toggleSpotlight: this.toggleSpotlight, + setActionTokens: this.setActionTokens } - } - - async tradeFear() { - if (event.currentTarget.classList.contains('disabled')) return; - - const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); - const value = currentFear - 1; - if (value >= 0) { - Hooks.callAll(socketEvent.GMUpdate, GMUpdateEvent.UpdateFear, null, value); - await game.socket.emit(`system.${SYSTEM.id}`, { - action: socketEvent.GMUpdate, - data: { action: GMUpdateEvent.UpdateFear, update: value } - }); - await game.combat.update({ 'system.actions': game.combat.system.actions + 2 }); - } - } - - async increaseResource(event) { - if (event.currentTarget.dataset.type === 'action') { - await game.combat.update({ 'system.actions': game.combat.system.actions + 1 }); - } - - const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); - const value = currentFear + 1; - if (event.currentTarget.dataset.type === 'fear' && value <= 6) { - Hooks.callAll(socketEvent.GMUpdate, GMUpdateEvent.UpdateFear, null, value); - await game.socket.emit(`system.${SYSTEM.id}`, { - action: socketEvent.GMUpdate, - data: { action: GMUpdateEvent.UpdateFear, update: value } - }); - } - - this.render(); - } - - async decreaseResource(event) { - if (event.currentTarget.dataset.type === 'action' && game.combat.system.actions - 1 >= 0) { - await game.combat.update({ 'system.actions': game.combat.system.actions - 1 }); - } - - const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); - const value = currentFear - 1; - if (event.currentTarget.dataset.type === 'fear' && value >= 0) { - Hooks.callAll(socketEvent.GMUpdate, GMUpdateEvent.UpdateFear, null, value); - await game.socket.emit(`system.${SYSTEM.id}`, { - action: socketEvent.GMUpdate, - data: { action: GMUpdateEvent.UpdateFear, update: value } - }); - } - - this.render(); - } - - async getData(options = {}) { - let context = await super.getData(options); - - // Get the combat encounters possible for the viewed Scene - const combat = this.viewed; - const hasCombat = combat !== null; - const combats = this.combats; - const currentIdx = combats.findIndex(c => c === combat); - const previousId = currentIdx > 0 ? combats[currentIdx - 1].id : null; - const nextId = currentIdx < combats.length - 1 ? combats[currentIdx + 1].id : null; - const settings = game.settings.get('core', Combat.CONFIG_SETTING); - - // Prepare rendering data - context = foundry.utils.mergeObject(context, { - combats: combats, - currentIndex: currentIdx + 1, - combatCount: combats.length, - hasCombat: hasCombat, - combat, - turns: [], - previousId, - nextId, - started: this.started, - control: false, - settings, - linked: combat?.scene !== null, - labels: {} - }); - context.labels.scope = game.i18n.localize(`COMBAT.${context.linked ? 'Linked' : 'Unlinked'}`); - if (!hasCombat) return context; - - // Format information about each combatant in the encounter - let hasDecimals = false; - const turns = []; - for (let [i, combatant] of combat.turns.entries()) { - if (!combatant.visible) continue; - - // Prepare turn data - const resource = - combatant.permission >= CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER ? combatant.resource : null; - const turn = { - id: combatant.id, - name: combatant.name, - img: await this._getCombatantThumbnail(combatant), - active: combatant.id === combat.system.activeCombatant, - owner: combatant.isOwner, - defeated: combatant.isDefeated, - hidden: combatant.hidden, - initiative: combatant.initiative, - hasRolled: combatant.initiative !== null, - hasResource: resource !== null, - resource: resource, - canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'), - playerCharacter: game.user?.character?.id === combatant.actor.id, - ownedByPlayer: combatant.hasPlayerOwner - }; - if (turn.initiative !== null && !Number.isInteger(turn.initiative)) hasDecimals = true; - turn.css = [turn.active ? 'active' : '', turn.hidden ? 'hidden' : '', turn.defeated ? 'defeated' : ''] - .join(' ') - .trim(); - - // Actor and Token status effects - turn.effects = new Set(); - if (combatant.token) { - combatant.token.effects.forEach(e => turn.effects.add(e)); - if (combatant.token.overlayEffect) turn.effects.add(combatant.token.overlayEffect); - } - if (combatant.actor) { - for (const effect of combatant.actor.temporaryEffects) { - if (effect.statuses.has(CONFIG.specialStatusEffects.DEFEATED)) turn.defeated = true; - else if (effect.icon) turn.effects.add(effect.icon); - } - } - turns.push(turn); - } - - // Format initiative numeric precision - const precision = CONFIG.Combat.initiative.decimals; - turns.forEach(t => { - if (t.initiative !== null) t.initiative = t.initiative.toFixed(hasDecimals ? precision : 0); - }); - - const fear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); - - // Merge update data for rendering - return foundry.utils.mergeObject(context, { - round: combat.round, - turn: combat.turn, - turns: turns, - control: combat.combatant?.players?.includes(game.user), - fear: fear - }); - } - - onFearUpdate = async () => { - this.render(true); }; - async close(options) { - Hooks.off(socketEvent.DhpFearUpdate, this.onFearUpdate); + static PARTS = { + header: { + template: 'systems/daggerheart/templates/ui/combat/combatTrackerHeader.hbs' + }, + tracker: { + template: 'systems/daggerheart/templates/ui/combat/combatTracker.hbs' + }, + footer: { + template: 'systems/daggerheart/templates/ui/combat/combatTrackerFooter.hbs' + } + }; - return super.close(options); + async _prepareCombatContext(context, options) { + await super._prepareCombatContext(context, options); + + Object.assign(context, { + fear: game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear) + }); + } + + async _prepareTrackerContext(context, options) { + await super._prepareTrackerContext(context, options); + + const adversaries = context.turns.filter(x => x.isNPC); + const characters = context.turns.filter(x => !x.isNPC); + + Object.assign(context, { + actionTokens: game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.variantRules).actionTokens, + adversaries, + characters + }); + } + + async _prepareTurnContext(combat, combatant, index) { + const turn = await super._prepareTurnContext(combat, combatant, index); + return { ...turn, isNPC: combatant.isNPC, system: combatant.system.toObject() }; + } + + _getCombatContextOptions() { + return [ + { + name: 'COMBAT.ClearMovementHistories', + icon: '', + condition: () => game.user.isGM && this.viewed?.combatants.size > 0, + callback: () => this.viewed.clearMovementHistories() + }, + { + name: 'COMBAT.Delete', + icon: '', + condition: () => game.user.isGM && !!this.viewed, + callback: () => this.viewed.endCombat() + } + ]; + } + + static async requestSpotlight(_, target) { + const { combatantId } = target.closest('[data-combatant-id]')?.dataset ?? {}; + const combatant = this.viewed.combatants.get(combatantId); + await combatant.update({ + 'system.spotlight': { + requesting: !combatant.system.spotlight.requesting + } + }); + + this.render(); + } + + static async toggleSpotlight(_, target) { + const { combatantId } = target.closest('[data-combatant-id]')?.dataset ?? {}; + const combatant = this.viewed.combatants.get(combatantId); + + const toggleTurn = this.viewed.combatants.contents + .sort(this.viewed._sortCombatants) + .map(x => x.id) + .indexOf(combatantId); + + await this.viewed.update({ turn: this.viewed.turn === toggleTurn ? null : toggleTurn }); + await combatant.update({ 'system.spotlight.requesting': false }); + } + + static async setActionTokens(_, target) { + const { combatantId, tokenIndex } = target.closest('[data-combatant-id]')?.dataset ?? {}; + + const combatant = this.viewed.combatants.get(combatantId); + const changeIndex = Number(tokenIndex); + const newIndex = combatant.system.actionTokens > changeIndex ? changeIndex : changeIndex + 1; + + await combatant.update({ 'system.actionTokens': newIndex }); + this.render(); } } diff --git a/module/ui/players.mjs b/module/ui/players.mjs deleted file mode 100644 index cbce702b..00000000 --- a/module/ui/players.mjs +++ /dev/null @@ -1,53 +0,0 @@ -import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs'; - -export default class DhpPlayers extends foundry.applications.ui.Players { - constructor(data, context) { - super(data, context); - - Hooks.on(socketEvent.DhpFearUpdate, this.onFearUpdate); - } - - get template() { - return 'systems/daggerheart/templates/ui/players.hbs'; - } - - async getData(options = {}) { - const context = super.getData(options); - context.fear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); - context.user = game.user; - - return context; - } - - activateListeners(html) { - // Toggle online/offline - html.find('.players-mode').click(this._onToggleOfflinePlayers.bind(this)); - html.find('.fear-control.up').click(async event => await this.updateFear(event, 1)); - html.find('.fear-control.down').click(async event => await this.updateFear(event, -1)); - - // Context menu - const contextOptions = this._getUserContextOptions(); - Hooks.call('getUserContextOptions', html, contextOptions); - new ContextMenu(html, '.player', contextOptions); - } - - async updateFear(_, change) { - const fear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); - const value = Math.max(Math.min(fear + change, 6), 0); - Hooks.callAll(socketEvent.GMUpdate, GMUpdateEvent.UpdateFear, null, value); - await game.socket.emit(`system.${SYSTEM.id}`, { - action: socketEvent.GMUpdate, - data: { action: GMUpdateEvent.UpdateFear, update: value } - }); - } - - onFearUpdate = async () => { - this.render(true); - }; - - async close(options) { - Hooks.off(socketEvent.DhpFearUpdate, this.onFearUpdate); - - return super.close(options); - } -} diff --git a/styles/daggerheart.css b/styles/daggerheart.css index a99ffbd7..73f54a18 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -1293,60 +1293,92 @@ .daggerheart.sheet.pc div[data-application-part] .sheet-body .inventory-container .inventory-item-list .inventory-row img { width: 32px; } -.combat-sidebar .encounter-gm-resources { - flex: 0; - display: flex; - justify-content: center; - padding: 8px 0; +.combat-sidebar .encounter-controls.combat { + justify-content: space-between; } -.combat-sidebar .encounter-gm-resources .gm-resource-controls { +.combat-sidebar .encounter-controls.combat .encounter-control-fear-container { display: flex; - flex-direction: column; + position: relative; align-items: center; - padding: 0 4px; justify-content: center; + color: black; } -.combat-sidebar .encounter-gm-resources .gm-resource-tools { - display: flex; - flex-direction: column; - justify-content: center; - padding: 0 5px 0 4px; +.combat-sidebar .encounter-controls.combat .encounter-control-fear-container .dice { + height: 24px; } -.combat-sidebar .encounter-gm-resources .gm-resource-tools i { - margin: 0 2px; +.combat-sidebar .encounter-controls.combat .encounter-control-fear-container .encounter-control-fear { + position: absolute; font-size: 16px; } -.combat-sidebar .encounter-gm-resources .gm-resource-tools i.disabled { - opacity: 0.6; +.combat-sidebar .encounter-controls.combat .encounter-control-fear-container .encounter-control-counter { + position: absolute; + right: -10px; + color: var(--color-text-secondary); } -.combat-sidebar .encounter-gm-resources .gm-resource-tools i:hover:not(.disabled) { - cursor: pointer; - filter: drop-shadow(0 0 3px red); +.combat-sidebar .encounter-controls.combat .control-buttons { + width: min-content; } -.combat-sidebar .encounter-gm-resources .gm-resource { - background: rgba(255, 255, 255, 0.1); - padding: 4px; - border-radius: 8px; - border: 2px solid black; - font-size: 20px; +.combat-sidebar .combatant-controls { + flex: 0; } -.combat-sidebar .token-action-tokens { - flex: 0 0 48px; +.combat-sidebar .token-actions { + align-self: stretch; + display: flex; + align-items: top; + justify-content: center; + gap: 16px; +} +.combat-sidebar .token-actions .action-tokens { + display: flex; + gap: 4px; +} +.combat-sidebar .token-actions .action-tokens .action-token { + height: 22px; + width: 22px; + border: 1px solid; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + padding: 8px; + --button-size: 0; +} +.combat-sidebar .token-actions .action-tokens .action-token.used { + opacity: 0.5; + background: transparent; +} +.combat-sidebar .token-actions button { + font-size: 22px; + height: 24px; + width: 24px; +} +.combat-sidebar .token-actions button.main { + background: var(--button-hover-background-color); + color: var(--button-hover-text-color); + border-color: var(--button-hover-border-color); +} +.combat-sidebar .token-actions button.main:hover { + filter: drop-shadow(0 0 3px var(--button-hover-text-color)); +} +.combat-sidebar .spotlight-control { + font-size: 26px; +} +.combat-sidebar .spotlight-control:focus { + outline: none; + box-shadow: none; +} +.combat-sidebar .spotlight-control.discrete:hover { + background: inherit; +} +.combat-sidebar .spotlight-control.requesting { + filter: drop-shadow(0 0 3px gold); + color: var(--button-hover-text-color); +} +.combat-sidebar h4 { + margin: 0; text-align: center; } -.combat-sidebar .token-action-tokens .use-action-token.disabled { - opacity: 0.6; -} -.combat-sidebar .icon-button.spaced { - margin-left: 4px; -} -.combat-sidebar .icon-button.disabled { - opacity: 0.6; -} -.combat-sidebar .icon-button:hover:not(.disabled) { - cursor: pointer; - filter: drop-shadow(0 0 3px red); -} .chat-message.duality { border-color: black; padding: 8px 0 0 0; @@ -2722,11 +2754,18 @@ div.daggerheart.views.multiclass { --primary-color-fear: rgba(9, 71, 179, 0.75); --secondary-color-fear: rgba(9, 71, 179, 0.75); --shadow-text-stroke: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; + --fear-animation: background 0.3s ease, box-shadow .3s ease, border-color .3s ease, opacity .3s ease; } #resources { min-height: calc(var(--header-height) + 4rem); min-width: 4rem; color: #d3d3d3; + transition: var(--fear-animation); +} +#resources header, +#resources .controls, +#resources .window-resize-handle { + transition: var(--fear-animation); } #resources .window-content { padding: 0.5rem; @@ -2822,7 +2861,7 @@ div.daggerheart.views.multiclass { #resources:not(:hover):not(.minimized) header, #resources:not(:hover):not(.minimized) .controls, #resources:not(:hover):not(.minimized) .window-resize-handle { - visibility: hidden; + opacity: 0; } #resources:has(.fear-bar) { min-width: 200px; @@ -2903,7 +2942,7 @@ div.daggerheart.views.multiclass { font-style: normal; font-weight: 700; font-display: swap; - src: url(https://fonts.gstatic.com/s/cinzeldecorative/v17/daaHSScvJGqLYhG8nNt8KPPswUAPniZoaelD.ttf) format('truetype'); + src: url(https://fonts.gstatic.com/s/cinzeldecorative/v18/daaHSScvJGqLYhG8nNt8KPPswUAPniZoaelD.ttf) format('truetype'); } @font-face { font-family: 'Montserrat'; @@ -3186,6 +3225,19 @@ div.daggerheart.views.multiclass { .application.setting.dh-style footer button { flex: 1; } +.application.setting.dh-style .form-group { + display: flex; + justify-content: space-between; + align-items: center; +} +.application.setting.dh-style .form-group label { + font-size: 16px; +} +.application.setting.dh-style .form-group .form-fields { + display: flex; + gap: 4px; + align-items: center; +} .system-daggerheart .tagify { background: light-dark(transparent, transparent); border: 1px solid light-dark(#222, #efe6d8); diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index 8ad2c97f..a4bb7c99 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -212,6 +212,22 @@ flex: 1; } } + + .form-group { + display: flex; + justify-content: space-between; + align-items: center; + + label { + font-size: 16px; + } + + .form-fields { + display: flex; + gap: 4px; + align-items: center; + } + } } .system-daggerheart { diff --git a/styles/ui.less b/styles/ui.less index 7d1ff690..c54b0b3b 100644 --- a/styles/ui.less +++ b/styles/ui.less @@ -1,71 +1,106 @@ .combat-sidebar { - .encounter-gm-resources { - flex: 0; - display: flex; - justify-content: center; - padding: @largePadding 0; + .encounter-controls.combat { + justify-content: space-between; - .gm-resource-controls { + .encounter-control-fear-container { display: flex; - flex-direction: column; + position: relative; align-items: center; - padding: 0 4px; justify-content: center; - } + color: black; - .gm-resource-tools { - display: flex; - flex-direction: column; - justify-content: center; - padding: 0 5px 0 @fullPadding; + .dice { + height: 24px; + } - i { - margin: 0 @tinyMargin; + .encounter-control-fear { + position: absolute; font-size: 16px; + } - &.disabled { - opacity: 0.6; - } + .encounter-control-counter { + position: absolute; + right: -10px; + color: var(--color-text-secondary); + } + } - &:hover:not(.disabled) { - cursor: pointer; - filter: drop-shadow(0 0 3px @mainShadow); + .control-buttons { + width: min-content; + } + } + + .combatant-controls { + flex: 0; + } + + .token-actions { + align-self: stretch; + display: flex; + align-items: top; + justify-content: center; + gap: 16px; + + .action-tokens { + display: flex; + gap: 4px; + + .action-token { + height: 22px; + width: 22px; + border: 1px solid; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + padding: 8px; + --button-size: 0; + + &.used { + opacity: 0.5; + background: transparent; } } } - .gm-resource { - background: rgba(255, 255, 255, 0.1); - padding: @fullPadding; - border-radius: 8px; - border: @normalBorder solid black; - font-size: 20px; + button { + font-size: 22px; + height: 24px; + width: 24px; + + &.main { + background: var(--button-hover-background-color); + color: var(--button-hover-text-color); + border-color: var(--button-hover-border-color); + + &:hover { + filter: drop-shadow(0 0 3px var(--button-hover-text-color)); + } + } } } - .token-action-tokens { - flex: 0 0 48px; + .spotlight-control { + font-size: 26px; + + &:focus { + outline: none; + box-shadow: none; + } + + &.discrete:hover { + background: inherit; + } + + &.requesting { + filter: drop-shadow(0 0 3px gold); + color: var(--button-hover-text-color); + } + } + + h4 { + margin: 0; text-align: center; - - .use-action-token { - &.disabled { - opacity: 0.6; - } - } - } - - .icon-button { - &.spaced { - margin-left: @halfMargin; - } - - &.disabled { - opacity: 0.6; - } - - &:hover:not(.disabled) { - cursor: pointer; - filter: drop-shadow(0 0 3px @mainShadow); - } } } diff --git a/templates/settings/variant-rules.hbs b/templates/settings/variant-rules.hbs new file mode 100644 index 00000000..f39cb2a9 --- /dev/null +++ b/templates/settings/variant-rules.hbs @@ -0,0 +1,22 @@ +
+
+ + +
+ {{formInput settingFields.schema.fields.actionTokens.fields.enabled value=settingFields._source.actionTokens.enabled}} + {{formInput settingFields.schema.fields.actionTokens.fields.tokens value=settingFields._source.actionTokens.tokens disabled=(not settingFields._source.actionTokens.enabled)}} +
+
+ + +
+ \ No newline at end of file diff --git a/templates/ui/combat/combatTracker.hbs b/templates/ui/combat/combatTracker.hbs new file mode 100644 index 00000000..683c599b --- /dev/null +++ b/templates/ui/combat/combatTracker.hbs @@ -0,0 +1,4 @@ +
+ {{> 'systems/daggerheart/templates/ui/combat/combatTrackerSection.hbs' this title=(localize "DAGGERHEART.General.Character.Plural") turns=this.characters}} + {{> 'systems/daggerheart/templates/ui/combat/combatTrackerSection.hbs' this title=(localize "DAGGERHEART.General.Adversary.Plural") turns=this.adversaries}} +
\ No newline at end of file diff --git a/templates/ui/combat/combatTrackerFooter.hbs b/templates/ui/combat/combatTrackerFooter.hbs new file mode 100644 index 00000000..4bb9bb10 --- /dev/null +++ b/templates/ui/combat/combatTrackerFooter.hbs @@ -0,0 +1,17 @@ + diff --git a/templates/ui/combat/combatTrackerHeader.hbs b/templates/ui/combat/combatTrackerHeader.hbs new file mode 100644 index 00000000..a2f5d557 --- /dev/null +++ b/templates/ui/combat/combatTrackerHeader.hbs @@ -0,0 +1,86 @@ +
+ + {{!-- Encounter Controls --}} + {{#if user.isGM}} + + {{/if}} + +
+ {{#if hasCombat}} +
+ + +
{{fear}}
+
+ {{/if}} + + {{!-- Combat Status --}} + + {{#if combats.length}} + {{#if combat.round}} + {{ localize "DAGGERHEART.Combat.combatStarted" }} + {{else}} + {{ localize "COMBAT.NotStarted" }} + {{/if}} + {{else}} + {{ localize "COMBAT.None" }} + {{/if}} + + + {{!-- Combat Controls --}} + {{#if hasCombat}} +
+
+ +
+ {{/if}} + +
+ +
diff --git a/templates/ui/combat/combatTrackerSection.hbs b/templates/ui/combat/combatTrackerSection.hbs new file mode 100644 index 00000000..688b4efc --- /dev/null +++ b/templates/ui/combat/combatTrackerSection.hbs @@ -0,0 +1,64 @@ +
+

{{title}}

+
    + {{#each turns}} +
  1. + {{!-- Image --}} + {{ name }} + + {{!-- Name & Controls --}} +
    + {{ name }} +
    +
    + {{#if @root.user.isGM}} + + + {{/if}} + {{#if canPing}} + + {{/if}} + {{#unless @root.user.isGM}} + + {{/unless}} +
    + + {{#if ../combat.round}} +
    + {{#if isOwner}} + {{#if (and (not isNPC) ../actionTokens.enabled)}} +
    + {{#times ../actionTokens.tokens}} + + {{/times}} +
    + {{/if}} + {{/if}} +
    + {{/if}} +
    +
    + + {{#if @root.user.isGM}} + + {{else}} + + {{/if}} +
  2. + {{/each}} +
+
\ No newline at end of file diff --git a/templates/ui/combatTracker.hbs b/templates/ui/combatTracker.hbs deleted file mode 100644 index 04d1a91b..00000000 --- a/templates/ui/combatTracker.hbs +++ /dev/null @@ -1,160 +0,0 @@ -
-
- {{#if user.isGM}} - - {{/if}} - -
- {{#if user.isGM}} - - - - - - - {{/if}} - - {{#if combatCount}} - {{#if combat.round}} -

{{localize 'COMBAT.Round'}} {{combat.round}}

- {{else}} -

{{localize 'COMBAT.NotStarted'}}

- {{/if}} - {{else}} -

{{localize "COMBAT.None"}}

- {{/if}} - - {{#if user.isGM}} - - - - - - - {{/if}} - - - -
-
- -
- {{#if combat.system}} -
- - -
-
- - {{combat.system.actions}} -
-
- - -
-
- - {{fear}} -
-
- - -
- {{/if}} -
- -
    - {{#each turns}} -
  1. - {{this.name}} -
    -

    {{this.name}}

    -
    - {{#if ../user.isGM}} - - - - - - - {{/if}} - {{#if this.canPing}} - - - - {{/if}} - {{#unless ../user.isGM}} - - - - {{/unless}} -
    - {{#each this.effects}} - - {{/each}} -
    -
    -
    - - {{#if this.hasResource}} -
    - {{this.resource}} -
    - {{/if}} - -
    - {{#if this.playerCharacter}} - - {{else if (and (not this.ownedByPlayer) ../user.isGM)}} - - {{/if}} - {{!-- {{#if this.hasRolled}} - {{this.initiative}} - {{else if this.owner}} - - {{/if}} --}} -
    -
  2. - {{/each}} -
- - -
diff --git a/templates/ui/players.hbs b/templates/ui/players.hbs deleted file mode 100644 index 3912e955..00000000 --- a/templates/ui/players.hbs +++ /dev/null @@ -1,35 +0,0 @@ - From 57fdf9e07d1d7653e30d4ec8f0cdb65ce273b365 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Sat, 7 Jun 2025 00:12:02 +0200 Subject: [PATCH 3/5] Corrected CombatTracker flexing --- templates/ui/combat/combatTracker.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/ui/combat/combatTracker.hbs b/templates/ui/combat/combatTracker.hbs index 683c599b..8de5640a 100644 --- a/templates/ui/combat/combatTracker.hbs +++ b/templates/ui/combat/combatTracker.hbs @@ -1,4 +1,4 @@ -
+
{{> 'systems/daggerheart/templates/ui/combat/combatTrackerSection.hbs' this title=(localize "DAGGERHEART.General.Character.Plural") turns=this.characters}} {{> 'systems/daggerheart/templates/ui/combat/combatTrackerSection.hbs' this title=(localize "DAGGERHEART.General.Adversary.Plural") turns=this.adversaries}}
\ No newline at end of file From 47a6abddfb5bcfd38e68ee179eb1b1c479784dad Mon Sep 17 00:00:00 2001 From: WBHarry Date: Sat, 7 Jun 2025 00:26:21 +0200 Subject: [PATCH 4/5] CombatTracker was throwing an error when there were no combatants --- module/ui/combatTracker.mjs | 4 ++-- templates/ui/combat/combatTracker.hbs | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/module/ui/combatTracker.mjs b/module/ui/combatTracker.mjs index 46f7f318..8b71f627 100644 --- a/module/ui/combatTracker.mjs +++ b/module/ui/combatTracker.mjs @@ -30,8 +30,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C async _prepareTrackerContext(context, options) { await super._prepareTrackerContext(context, options); - const adversaries = context.turns.filter(x => x.isNPC); - const characters = context.turns.filter(x => !x.isNPC); + const adversaries = context.turns?.filter(x => x.isNPC) ?? []; + const characters = context.turns?.filter(x => !x.isNPC) ?? []; Object.assign(context, { actionTokens: game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.variantRules).actionTokens, diff --git a/templates/ui/combat/combatTracker.hbs b/templates/ui/combat/combatTracker.hbs index 8de5640a..13bd512e 100644 --- a/templates/ui/combat/combatTracker.hbs +++ b/templates/ui/combat/combatTracker.hbs @@ -1,4 +1,8 @@
- {{> 'systems/daggerheart/templates/ui/combat/combatTrackerSection.hbs' this title=(localize "DAGGERHEART.General.Character.Plural") turns=this.characters}} - {{> 'systems/daggerheart/templates/ui/combat/combatTrackerSection.hbs' this title=(localize "DAGGERHEART.General.Adversary.Plural") turns=this.adversaries}} + {{#if (gt this.characters.length 0)}} + {{> 'systems/daggerheart/templates/ui/combat/combatTrackerSection.hbs' this title=(localize "DAGGERHEART.General.Character.Plural") turns=this.characters}} + {{/if}} + {{#if (gt this.adversaries.length 0)}} + {{> 'systems/daggerheart/templates/ui/combat/combatTrackerSection.hbs' this title=(localize "DAGGERHEART.General.Adversary.Plural") turns=this.adversaries}} + {{/if}}
\ No newline at end of file From a92221778eab4d18710205222e4fde0ddca5ade5 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sat, 7 Jun 2025 01:50:50 +0200 Subject: [PATCH 5/5] Levelup Remake (#100) * Set up DhLevelTier datamodel * Added Levelup data model and started at the render * Fixed data handling in the LevelUp view * Added back the save function * Finalised levelup selections and propagating to PC * Added level advancement selection data * Added DomainCard selection * Css merge commit * Added PC level/delevel benefits of leveling up * Fixed sticky previous selections on continous leveling * Fixed up Summary. Fixed multiclass/subclass blocking on selection * Removed unused level.hbs * Fixed attribute base for PC * Improved naming of attribute properties * Renamed/structured resources/evasion/proficiency * Improved trait marking * Rework to level up once at a time * Added markers * Removed tabs when in Summary * Fixed multilevel buttons * Improved multiclass/subclass recognition * Fixed tagify error on selection * Review fixes --- daggerheart.mjs | 3 +- lang/en.json | 115 ++- module/applications/levelup.mjs | 966 ++++++++++++------ module/applications/settings.mjs | 19 + module/applications/sheets/adversary.mjs | 2 +- module/applications/sheets/pc.mjs | 62 +- module/config/actorConfig.mjs | 14 +- module/config/settingsConfig.mjs | 13 + module/data/levelTier.mjs | 340 ++++++ module/data/levelup.mjs | 311 ++++++ module/data/pc.mjs | 339 ++---- module/documents/actor.mjs | 144 ++- module/helpers/handlebarsHelper.mjs | 49 +- module/helpers/utils.mjs | 91 ++ styles/daggerheart.css | 309 +++++- styles/daggerheart.less | 1 + styles/less/global/elements.less | 102 ++ styles/less/global/feature-section.less | 102 +- styles/less/global/item-header.less | 304 +++--- styles/less/global/sheet.less | 160 +-- styles/less/global/tab-actions.less | 92 +- styles/less/items/class.css | 167 +-- styles/less/items/domainCard.less | 23 +- styles/less/items/feature.less | 40 +- styles/levelup.less | 261 +++++ styles/resources.less | 43 +- styles/variables/colors.less | 3 + templates/components/card-preview.hbs | 13 + templates/sheets/adversary.hbs | 2 +- templates/sheets/parts/attributes.hbs | 13 +- templates/sheets/parts/defense.hbs | 2 +- templates/sheets/parts/health.hbs | 8 +- templates/sheets/pc/pc.hbs | 8 +- templates/views/levelup.hbs | 20 - .../levelup/parts/selectable-card-preview.hbs | 11 + templates/views/levelup/tabs/advancements.hbs | 40 + templates/views/levelup/tabs/selections.hbs | 95 ++ templates/views/levelup/tabs/summary.hbs | 134 +++ .../views/levelup/tabs/tab-navigation.hbs | 40 + templates/views/parts/level.hbs | 40 - tools/create-symlink.mjs | 61 +- 41 files changed, 3279 insertions(+), 1283 deletions(-) create mode 100644 module/data/levelTier.mjs create mode 100644 module/data/levelup.mjs create mode 100644 styles/levelup.less create mode 100644 templates/components/card-preview.hbs delete mode 100644 templates/views/levelup.hbs create mode 100644 templates/views/levelup/parts/selectable-card-preview.hbs create mode 100644 templates/views/levelup/tabs/advancements.hbs create mode 100644 templates/views/levelup/tabs/selections.hbs create mode 100644 templates/views/levelup/tabs/summary.hbs create mode 100644 templates/views/levelup/tabs/tab-navigation.hbs delete mode 100644 templates/views/parts/level.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index 0153a62c..7e77fbb3 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -293,7 +293,8 @@ const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/sheets/pc/sections/loadout.hbs', 'systems/daggerheart/templates/sheets/pc/parts/heritageCard.hbs', 'systems/daggerheart/templates/sheets/pc/parts/advancementCard.hbs', - 'systems/daggerheart/templates/views/parts/level.hbs', + 'systems/daggerheart/templates/components/card-preview.hbs', + 'systems/daggerheart/templates/views/levelup/parts/selectable-card-preview.hbs', 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs', 'systems/daggerheart/templates/ui/combat/combatTrackerSection.hbs' ]); diff --git a/lang/en.json b/lang/en.json index a2656b4f..39d5225c 100755 --- a/lang/en.json +++ b/lang/en.json @@ -163,6 +163,12 @@ "Or": "Or", "Description": "Description", "Features": "Features", + "proficiency": "Proficiency", + "unarmored": "Unarmored", + "Experience": { + "Single": "Experience", + "plural": "Experiences" + }, "Adversary": { "Singular": "Adversary", "Plural": "Adversaries" @@ -357,39 +363,43 @@ "combatStarted": "Active" }, "LevelUp": { - "Tier1": { - "Label": "Level 2-4", - "InfoLabel": "At Level 2, take an additional Experience.", - "Pretext": "When you level up, record it on your character sheet, then choose two available options from the list below and mark them.", - "Posttext": "Then increase your Severe Damage Threshold by +2 and choose a new Domain Deck card at your Level or lower." + "Options": { + "trait": "Gain a +1 bonus to two unmarked character traits and mark them.", + "hitPoint": "Permanently gain one Hit Point slot.", + "stress": "Permanently gain one Stress slot.", + "experience": "Permanently gain a +1 bonus to two experiences.", + "domainCard": "Choose an additional domain card of your level or lower from a domain you have access to (up to level {maxLevel})", + "evasion": "Permanently gain a +1 bonus to your Evasion.", + "subclass": "Take an upgraded subclass card. Then cross out the multiclass option for this tier.", + "proficiency": "Increase your Proficiency by +1.", + "multiclass": "Multiclass: Choose an additional class for your character, then cross out an unused “Take an upgraded subclass card” and the other multiclass option on this sheet." }, "Tier2": { - "Label": "Level 5-7", - "InfoLabel": "At Level 5, take an additional Experience and clear all marks on Character Traits.", - "Pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.", - "Posttext": "Then, increase your Damage Thresholds: Major by +1 and Severe by +3. Then choose a new Domain Deck card at your Level or lower. If your loadout is full, you may choose a card to swap." + "Label": "Levels 2-4", + "InfoLabel": "At Level 2, gain an additional Experience at +2 and gain a +1 bonus to your Proficiency.", + "Pretext": "Choose two options from the list below", + "Posttext": "Take an additional domain card of your level or lower from a domain you have access to." }, "Tier3": { - "Label": "Level 8-10", + "Label": "Levels 5-7", + "InfoLabel": "At Level 5, take an additional Experience and clear all marks on Character Traits.", + "Pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.", + "Posttext": "Take an additional domain card of your level or lower from a domain you have access to." + }, + "Tier4": { + "Label": "Levels 8-10", "InfoLabel": "At Level 8, take an additional Experience and clear all marks on Character Traits.", "Pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.", - "Posttext": "Then, increase your Damage Thresholds: Minor by +1, Major by +2, and Severe by +4. Then choose a new Domain Deck card at your Level or lower. If your loadout is full, you may choose a card to swap." + "Posttext": "Take an additional domain card of your level or lower from a domain you have access to." }, "ChoiceDescriptions": { - "Attributes": "Increase two unmarked Character Traits by +1 and mark them.", - "HitPointSlots": "Permanently add one Hit Point Slot.", - "StressSlots": "Permanently add one Stress Slot.", - "Experiences": "Increase two Experiences by +1.", - "Proficiency": "Increase your Proficiency by +1", - "ArmorOrEvasionSlot": "Permanently add one Armor Slot or take +1 to your Evasion.", - "MajorDamageThreshold2": "Increase your Major Damage Threshold by +2.", - "SevereDamageThreshold2": "Increase your Severe Damage Threshold by +2.", - "MinorDamageThreshold2": "Increase your Minor Damage Threshold by +2.", - "SevereDamageThreshold3": "Increase your Severe Damage Threshold by +3.", - "Major2OrSevere4DamageThreshold": "Increase your Major Damage Threshold by +2 or Severe Damage Threshold by +4", - "Minor1OrMajor1DamageThreshold": "Increase your Minor or Major Damage Threshold by +1.", - "SevereDamageThreshold4": "Increase your Severe Damage Threshold by +4.", - "MajorDamageThreshold1": "Increase your Major Damage Threshold by +1.", + "Attributes": "Gain a +1 bonus to two unmarked character traits and mark them.", + "HitPointSlots": "Permanently gain one Hit Point slot.", + "StressSlots": "Permanently gain one Stress slot.", + "Experiences": "Permanently gain a +1 bonus to two experiences.", + "DomainCard": "Choose an additional domain card of your level or lower from a domain you have access to (up to level {maxLevel})", + "Evasion": "Permanently gain a +1 bonus to your Evasion.", + "Proficiency": "Increase your Proficiency by +1.", "Subclass": "Take an upgraded subclass card. Then cross out the multiclass option for this tier.", "Multiclass": "Multiclass: Choose an additional class for your character, then cross out an unused “Take an upgraded subclass card” and the other multiclass option on this sheet." } @@ -775,8 +785,54 @@ "TakeDowntime": "Take Downtime" }, "LevelUp": { - "AdvanceLevel": "Continue To Level {level}", - "TakeLevelUp": "Finish Level Up" + "Title": "{actor} Level Up", + "Tabs": { + "advancement": "Level Advancement", + "selections": "Advancement Choices", + "summary": "Summary" + }, + "navigateLevel": "To Level {level}", + "navigateToLevelup": "Return To Levelup", + "navigateToSummary": "To Summary", + "TakeLevelUp": "Finish Level Up", + "Delevel": { + "title": "Go back to previous level", + "content": "Returning to the previous level selection will remove all selections made for this level. Do you want to proceed?" + }, + "Selections": { + "emptyDomainCardHint": "Domain Card Level {level} or below" + }, + "summary": { + "levelAchievements": "Level Achievements", + "levelAdvancements": "Level Advancements", + "proficiencyIncrease": "Proficiency Increased: {proficiency}", + "hpIncrease": "Hit Points Increased: {hitPoints}", + "stressIncrease": "Stress Increased: {stress}", + "evasionIncrease": "Evasion Increased: {evasion}", + "damageThresholdMajorIncrease": "Major: {threshold}", + "damageThresholdSevereIncrease": "Severe: {threshold}", + "newExperiences": "New Experiences", + "experiencePlaceholder": "A new experience..", + "domainCards": "Domain Cards", + "subclass": "Subclass", + "multiclass": "Multiclass", + "traits": "Increased Traits", + "experienceIncreases": "Experience Increases", + "damageThresholds": "Damage Thresholds" + }, + "notifications": { + "info": { + "insufficentAdvancements": "You don't have enough advancements left.", + "insufficientTierAdvancements": "You have no available advancements for this tier." + }, + "error": { + "domainCardWrongDomain": "You don't have access to that Domain", + "domainCardToHighLevel": "The Domain Card is too high level to be selected", + "domainCardDuplicate": "You already have that domain card!", + "noSelectionsLeft": "Nothing more to select!", + "alreadySelectedClass": "You already have that class!" + } + } }, "DeathMove": { "Title": "{actor} - Death Move", @@ -937,7 +993,10 @@ }, "NewItem": "New Item", "NewScar": "New Scar", - "DeleteConfirmation": "Are you sure you want to delete the item - {item}?" + "DeleteConfirmation": "Are you sure you want to delete the item - {item}?", + "Errors": { + "missingClassOrSubclass": "The character doesn't have a class and subclass" + } }, "Adversary": { "Description": "Description", diff --git a/module/applications/levelup.mjs b/module/applications/levelup.mjs index 90500e2a..7e69a6a1 100644 --- a/module/applications/levelup.mjs +++ b/module/applications/levelup.mjs @@ -1,371 +1,673 @@ -import SelectDialog from '../dialogs/selectDialog.mjs'; -import { getTier } from '../helpers/utils.mjs'; -import DhpMulticlassDialog from './multiclassDialog.mjs'; +import { abilities } from '../config/actorConfig.mjs'; +import { domains } from '../config/domainConfig.mjs'; +import { DhLevelup } from '../data/levelup.mjs'; +import { getDeleteKeys, tagifyElement } from '../helpers/utils.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; -export default class DhpLevelup extends HandlebarsApplicationMixin(ApplicationV2) { +export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) { constructor(actor) { super({}); this.actor = actor; - this.data = foundry.utils.deepClone(actor.system.levelData); - this.activeLevel = actor.system.levelData.currentLevel + 1; + this.levelTiers = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.LevelTiers); + + const playerLevelupData = actor.system.levelData; + this.levelup = new DhLevelup(DhLevelup.initializeData(this.levelTiers, playerLevelupData, actor.system.level)); + + this._dragDrop = this._createDragDropHandlers(); + this.tabGroups.primary = 'advancements'; } get title() { - return `${this.actor.name} - Level Up`; + return game.i18n.format('DAGGERHEART.Application.LevelUp.Title', { actor: this.actor.name }); } static DEFAULT_OPTIONS = { - classes: ['daggerheart', 'views', 'levelup'], - position: { width: 1200, height: 'auto' }, + tag: 'form', + classes: ['daggerheart', 'levelup'], + position: { width: 1000, height: 'auto' }, window: { resizable: true }, - actions: { - toggleBox: this.toggleBox, - advanceLevel: this.advanceLevel, - finishLevelup: this.finishLevelup - } + save: this.save, + viewCompendium: this.viewCompendium, + selectPreview: this.selectPreview, + selectDomain: this.selectDomain, + updateCurrentLevel: this.updateCurrentLevel, + activatePart: this.activatePart + }, + form: { + handler: this.updateForm, + submitOnChange: true, + closeOnSubmit: false + }, + dragDrop: [{ dragSelector: null, dropSelector: '.levelup-card-selection .card-preview-container' }] }; static PARTS = { - form: { - id: 'levelup', - template: 'systems/daggerheart/templates/views/levelup.hbs' + tabs: { template: 'systems/daggerheart/templates/views/levelup/tabs/tab-navigation.hbs' }, + advancements: { template: 'systems/daggerheart/templates/views/levelup/tabs/advancements.hbs' }, + selections: { template: 'systems/daggerheart/templates/views/levelup/tabs/selections.hbs' }, + summary: { template: 'systems/daggerheart/templates/views/levelup/tabs/summary.hbs' } + }; + + static TABS = { + advancements: { + active: true, + cssClass: '', + group: 'primary', + id: 'advancements', + icon: null, + label: 'DAGGERHEART.Application.LevelUp.Tabs.advancement' + }, + selections: { + active: false, + cssClass: '', + group: 'primary', + id: 'selections', + icon: null, + label: 'DAGGERHEART.Application.LevelUp.Tabs.selections' + }, + summary: { + active: false, + cssClass: '', + group: 'primary', + id: 'summary', + icon: null, + label: 'DAGGERHEART.Application.LevelUp.Tabs.summary' } }; async _prepareContext(_options) { - let selectedChoices = 0, - multiclassing = {}, - subclassing = {}; - const leveledTiers = Object.keys(this.data.levelups).reduce( - (acc, levelKey) => { - const levelData = this.data.levelups[levelKey]; - ['tier1', 'tier2', 'tier3'].forEach(tierKey => { - let tierUpdate = {}; - const tierData = levelData[tierKey]; - if (tierData) { - tierUpdate = Object.keys(tierData).reduce((acc, propertyKey) => { - const values = tierData[propertyKey]; - const level = Number.parseInt(levelKey); + const context = await super._prepareContext(_options); + context.levelup = this.levelup; + context.tabs = this._getTabs(this.constructor.TABS); - acc[propertyKey] = Object.values(values).map(value => { - if (value && level === this.activeLevel) selectedChoices++; - if (propertyKey === 'multiclass') multiclassing[levelKey] = true; - if (propertyKey === 'subclass') subclassing[tierKey] = true; + return context; + } - return { level: level, value: value }; - }); - - return acc; - }, {}); + async _preparePartContext(partId, context) { + const currentLevel = this.levelup.levels[this.levelup.currentLevel]; + switch (partId) { + case 'tabs': + const previous = + this.levelup.currentLevel === this.levelup.startLevel ? null : this.levelup.currentLevel - 1; + const next = this.levelup.currentLevel === this.levelup.endLevel ? null : this.levelup.currentLevel + 1; + context.navigate = { + previous: { + disabled: !previous, + label: previous + ? game.i18n.format('DAGGERHEART.Application.LevelUp.navigateLevel', { level: previous }) + : '', + fromSummary: this.tabGroups.primary === 'summary' + }, + next: { + disabled: !this.levelup.currentLevelFinished, + label: next + ? game.i18n.format('DAGGERHEART.Application.LevelUp.navigateLevel', { level: next }) + : '', + toSummary: !next, + show: this.tabGroups.primary !== 'summary' } + }; - Object.keys(tierUpdate).forEach(propertyKey => { - const property = tierUpdate[propertyKey]; - const propertyValues = foundry.utils.getProperty(acc, `${tierKey}.${propertyKey}`) ?? []; - foundry.utils.setProperty(acc, `${tierKey}.${propertyKey}`, [...propertyValues, ...property]); + const { selections } = currentLevel.nrSelections; + context.tabs.advancements.progress = { selected: selections, max: currentLevel.maxSelections }; + context.showTabs = this.tabGroups.primary !== 'summary'; + break; + case 'selections': + const advancementChoices = Object.keys(currentLevel.choices).reduce((acc, choiceKey) => { + Object.keys(currentLevel.choices[choiceKey]).forEach(checkboxNr => { + const checkbox = currentLevel.choices[choiceKey][checkboxNr]; + const data = { + ...checkbox, + path: `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}`, + level: this.levelup.currentLevel + }; + + if (!acc[choiceKey]) acc[choiceKey] = []; + acc[choiceKey].push(data); }); - }); - - return acc; - }, - { tier1: {}, tier2: {}, tier3: {} } - ); - - const activeTier = getTier(this.activeLevel); - const data = Object.keys(SYSTEM.ACTOR.levelupData).reduce((acc, tierKey) => { - const tier = SYSTEM.ACTOR.levelupData[tierKey]; - acc[tierKey] = { - label: game.i18n.localize(tier.label), - info: game.i18n.localize(tier.info), - pretext: game.i18n.localize(tier.pretext), - postext: game.i18n.localize(tier.posttext), - active: tierKey <= activeTier, - choices: Object.keys(tier.choices).reduce((acc, propertyKey) => { - const property = tier.choices[propertyKey]; - acc[propertyKey] = { description: property.description, cost: property.cost ?? 1, values: [] }; - for (var i = 0; i < property.maxChoices; i++) { - const leveledValue = leveledTiers[tierKey][propertyKey]?.[i]; - const subclassLock = - propertyKey === 'subclass' && - Object.keys(multiclassing).find(x => getTier(Number.parseInt(x)) === tierKey); - const subclassMulticlassLock = propertyKey === 'multiclass' && subclassing[tierKey]; - const multiclassLock = - propertyKey === 'multiclass' && - Object.keys(multiclassing).length > 0 && - !( - leveledValue && - Object.keys(multiclassing).find(x => Number.parseInt(x) === leveledValue.level) - ); - const locked = - (leveledValue && leveledValue.level !== this.activeLevel) || - subclassLock || - subclassMulticlassLock || - multiclassLock; - const disabled = - tierKey > activeTier || - (selectedChoices === 2 && !(leveledValue && leveledValue.level === this.activeLevel)) || - locked; - - acc[propertyKey].values.push({ - selected: leveledValue?.value !== undefined, - path: `levelups.${this.activeLevel}.${tierKey}.${propertyKey}.${i}`, - description: game.i18n.localize(property.description), - disabled: disabled, - locked: locked - }); - } return acc; - }, {}) + }, {}); + + const traits = Object.values(advancementChoices.trait ?? {}); + const traitValues = traits.filter(trait => trait.data.length > 0).flatMap(trait => trait.data); + context.traits = { + values: traitValues, + active: traits.length > 0, + progress: { + selected: traitValues.length, + max: traits.reduce((acc, exp) => acc + exp.amount, 0) + } + }; + + const experienceIncreases = Object.values(advancementChoices.experience ?? {}); + const experienceIncreaseValues = experienceIncreases + .filter(exp => exp.data.length > 0) + .flatMap(exp => + exp.data.map(data => this.actor.system.experiences.find(x => x.id === data).description) + ); + context.experienceIncreases = { + values: experienceIncreaseValues, + active: experienceIncreases.length > 0, + progress: { + selected: experienceIncreaseValues.length, + max: experienceIncreases.reduce((acc, exp) => acc + exp.amount, 0) + } + }; + + context.newExperiences = Object.keys(currentLevel.achievements.experiences).map(key => { + const experience = currentLevel.achievements.experiences[key]; + return { + ...experience, + level: this.levelup.currentLevel, + key: key + }; + }); + + const allDomainCards = { + ...advancementChoices.domainCard, + ...currentLevel.achievements.domainCards + }; + const allDomainCardKeys = Object.keys(allDomainCards); + + context.domainCards = []; + for (var key of allDomainCardKeys) { + const domainCard = allDomainCards[key]; + if (domainCard.level > this.levelup.endLevel) continue; + + const uuid = domainCard.data?.length > 0 ? domainCard.data[0] : domainCard.uuid; + const card = uuid ? await foundry.utils.fromUuid(uuid) : {}; + + context.domainCards.push({ + ...(card.toObject?.() ?? card), + emptySubtext: game.i18n.format( + 'DAGGERHEART.Application.LevelUp.Selections.emptyDomainCardHint', + { level: domainCard.level } + ), + path: domainCard.data + ? `${domainCard.path}.data` + : `levels.${domainCard.level}.achievements.domainCards.${key}.uuid`, + limit: domainCard.level, + compendium: 'domains' + }); + } + + const subclassSelections = advancementChoices.subclass?.flatMap(x => x.data) ?? []; + + const multiclassSubclass = this.actor.system.multiclass?.system?.subclasses?.[0]; + const possibleSubclasses = [ + this.actor.system.subclass, + ...(multiclassSubclass ? [multiclassSubclass] : []) + ]; + const selectedSubclasses = possibleSubclasses.filter(x => subclassSelections.includes(x.uuid)); + context.subclassCards = []; + if (advancementChoices.subclass?.length > 0) { + for (var subclass of possibleSubclasses) { + const data = await foundry.utils.fromUuid(subclass.uuid); + const selected = selectedSubclasses.some(x => x.uuid === data.uuid); + context.subclassCards.push({ + ...data.toObject(), + uuid: data.uuid, + selected: selected + }); + } + } + + const multiclasses = Object.values(advancementChoices.multiclass ?? {}); + if (multiclasses?.[0]) { + const data = multiclasses[0]; + const multiclass = data.data.length > 0 ? await foundry.utils.fromUuid(data.data[0]) : {}; + + context.multiclass = { + ...data, + ...(multiclass.toObject?.() ?? multiclass), + uuid: multiclass.uuid, + domains: + multiclass?.system?.domains.map(key => { + const domain = domains[key]; + const alreadySelected = this.actor.system.class.system.domains.includes(key); + + return { + ...domain, + selected: key === data.secondaryData, + disabled: (data.secondaryData && key !== data.secondaryData) || alreadySelected + }; + }) ?? [], + compendium: 'classes', + limit: 1 + }; + } + + break; + case 'summary': + const { current: currentActorLevel, changed: changedActorLevel } = this.actor.system.levelData.level; + const actorArmor = this.actor.system.armor; + const levelKeys = Object.keys(this.levelup.levels); + let achivementProficiency = 0; + const achievementCards = []; + let achievementExperiences = []; + for (var levelKey of levelKeys) { + const level = this.levelup.levels[levelKey]; + if (Number(levelKey) < this.levelup.startLevel) continue; + + achivementProficiency += level.achievements.proficiency ?? 0; + const cards = level.achievements.domainCards ? Object.values(level.achievements.domainCards) : null; + if (cards) { + for (var card of cards) { + const itemCard = await foundry.utils.fromUuid(card.uuid); + achievementCards.push(itemCard); + } + } + + achievementExperiences = level.achievements.experiences + ? Object.values(level.achievements.experiences).reduce((acc, experience) => { + if (experience.name) acc.push(experience); + return acc; + }, []) + : []; + } + + context.achievements = { + proficiency: { + old: this.actor.system.proficiency.value, + new: this.actor.system.proficiency.value + achivementProficiency, + shown: achivementProficiency > 0 + }, + damageThresholds: { + major: { + old: this.actor.system.damageThresholds.major, + new: this.actor.system.damageThresholds.major + changedActorLevel - currentActorLevel + }, + severe: { + old: this.actor.system.damageThresholds.severe, + new: + this.actor.system.damageThresholds.severe + + (actorArmor + ? changedActorLevel - currentActorLevel + : (changedActorLevel - currentActorLevel) * 2) + }, + unarmored: !actorArmor + }, + domainCards: { + values: achievementCards, + shown: achievementCards.length > 0 + }, + experiences: { + values: achievementExperiences + } + }; + + const advancement = {}; + for (var levelKey of levelKeys) { + const level = this.levelup.levels[levelKey]; + if (Number(levelKey) < this.levelup.startLevel) continue; + + for (var choiceKey of Object.keys(level.choices)) { + const choice = level.choices[choiceKey]; + for (var checkbox of Object.values(choice)) { + switch (choiceKey) { + case 'proficiency': + case 'hitPoint': + case 'stress': + case 'evasion': + advancement[choiceKey] = advancement[choiceKey] + ? advancement[choiceKey] + Number(checkbox.value) + : Number(checkbox.value); + break; + case 'domainCard': + if (!advancement[choiceKey]) advancement[choiceKey] = []; + if (checkbox.data.length === 1) { + const choiceItem = await foundry.utils.fromUuid(checkbox.data[0]); + advancement[choiceKey].push(choiceItem.toObject()); + } + break; + case 'experience': + if (!advancement[choiceKey]) advancement[choiceKey] = []; + const data = checkbox.data.map( + data => + this.actor.system.experiences.find(x => x.id === data)?.description ?? '' + ); + advancement[choiceKey].push({ data: data, value: checkbox.value }); + break; + } + } + } + } + + context.advancements = { + statistics: { + proficiency: { + old: context.achievements.proficiency.new, + new: context.achievements.proficiency.new + (advancement.proficiency ?? 0) + }, + hitPoints: { + old: this.actor.system.resources.hitPoints.max, + new: this.actor.system.resources.hitPoints.max + (advancement.hitPoint ?? 0) + }, + stress: { + old: this.actor.system.resources.stress.max, + new: this.actor.system.resources.stress.max + (advancement.stress ?? 0) + }, + evasion: { + old: this.actor.system.evasion.value, + new: this.actor.system.evasion.value + (advancement.evasion ?? 0) + } + }, + traits: + advancement.trait?.flatMap(x => + x.data.map(data => game.i18n.localize(abilities[data].label)) + ) ?? [], + domainCards: advancement.domainCard ?? [], + experiences: + advancement.experience?.flatMap(x => x.data.map(data => ({ name: data, modifier: x.value }))) ?? + [] + }; + + context.advancements.statistics.proficiency.shown = + context.advancements.statistics.proficiency.new > context.advancements.statistics.proficiency.old; + context.advancements.statistics.hitPoints.shown = + context.advancements.statistics.hitPoints.new > context.advancements.statistics.hitPoints.old; + context.advancements.statistics.stress.shown = + context.advancements.statistics.stress.new > context.advancements.statistics.stress.old; + context.advancements.statistics.evasion.shown = + context.advancements.statistics.evasion.new > context.advancements.statistics.evasion.old; + context.advancements.statistics.shown = + context.advancements.statistics.proficiency.shown || + context.advancements.statistics.hitPoints.shown || + context.advancements.statistics.stress.shown || + context.advancements.statistics.evasion.shown; + + break; + } + + return context; + } + + _getTabs(tabs) { + for (const v of Object.values(tabs)) { + v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active; + v.cssClass = v.active ? 'active' : ''; + } + + return tabs; + } + + _createDragDropHandlers() { + return this.options.dragDrop.map(d => { + d.callbacks = { + drop: this._onDrop.bind(this) }; + return new foundry.applications.ux.DragDrop.implementation(d); + }); + } + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + htmlElement + .querySelectorAll('.selection-checkbox') + .forEach(element => element.addEventListener('change', this.selectionClick.bind(this))); + + const traitsTagify = htmlElement.querySelector('.levelup-trait-increases'); + if (traitsTagify) { + tagifyElement(traitsTagify, abilities, this.tagifyUpdate('trait').bind(this)); + } + + const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases'); + if (experienceIncreaseTagify) { + tagifyElement( + experienceIncreaseTagify, + this.actor.system.experiences.reduce((acc, experience) => { + acc[experience.id] = { label: experience.description }; + + return acc; + }, {}), + this.tagifyUpdate('experience').bind(this) + ); + } + + this._dragDrop.forEach(d => d.bind(htmlElement)); + } + + tagifyUpdate = + type => + async (_, { option, removed }) => { + const updatePath = Object.keys(this.levelup.levels[this.levelup.currentLevel].choices).reduce( + (acc, choiceKey) => { + const choice = this.levelup.levels[this.levelup.currentLevel].choices[choiceKey]; + Object.keys(choice).forEach(checkboxNr => { + const checkbox = choice[checkboxNr]; + if ( + choiceKey === type && + (removed ? checkbox.data.includes(option) : checkbox.data.length < checkbox.amount) + ) { + acc = `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}.data`; + } + }); + + return acc; + }, + null + ); + + if (!updatePath) { + ui.notifications.error( + game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.noSelectionsLeft') + ); + return; + } + + const currentData = foundry.utils.getProperty(this.levelup, updatePath); + const updatedData = removed ? currentData.filter(x => x !== option) : [...currentData, option]; + await this.levelup.updateSource({ [updatePath]: updatedData }); + this.render(); + }; + + static async updateForm(event, _, formData) { + const { levelup } = foundry.utils.expandObject(formData.object); + await this.levelup.updateSource(levelup); + this.render(); + } + + async _onDrop(event) { + const data = foundry.applications.ux.TextEditor.getDragEventData(event); + const item = await fromUuid(data.uuid); + if (event.target.closest('.domain-cards')) { + const target = event.target.closest('.card-preview-container'); + if (item.type === 'domainCard') { + if ( + !this.actor.system.class.system.domains.includes(item.system.domain) && + this.levelup.classUpgradeChoices?.multiclass?.domain !== item.system.domain + ) { + ui.notifications.error( + game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.domainCardWrongDomain') + ); + return; + } + + if (item.system.level > Number(target.dataset.limit)) { + ui.notifications.error( + game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.domainCardToHighLevel') + ); + return; + } + + if ( + Object.values(this.levelup.levels).some(level => { + const achievementExists = Object.values(level.achievements.domainCards).some( + card => card.uuid === item.uuid + ); + const advancementExists = Object.keys(level.choices).some(choiceKey => { + if (choiceKey !== 'domainCard') return false; + const choice = level.choices[choiceKey]; + return Object.values(choice).some(checkbox => checkbox.data.includes(item.uuid)); + }); + + return achievementExists || advancementExists; + }) + ) { + ui.notifications.error( + game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.domainCardDuplicate') + ); + return; + } + + await this.levelup.updateSource({ [target.dataset.path]: item.uuid }); + this.render(); + } + } else if (event.target.closest('.multiclass-cards')) { + const target = event.target.closest('.multiclass-cards'); + if (item.type === 'class') { + if (item.name === this.actor.system.class.name) { + ui.notifications.error( + game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.alreadySelectedClass') + ); + return; + } + + await this.levelup.updateSource({ + multiclass: { + class: item.uuid, + level: this.levelup.currentLevel, + tier: Number(target.dataset.tier) + }, + [target.dataset.path]: { + tier: Number(target.dataset.tier), + minCost: Number(target.dataset.minCost), + amount: target.dataset.amount ? Number(target.dataset.amount) : null, + value: target.dataset.value, + type: target.dataset.type, + data: item.uuid, + secondaryData: null + } + }); + this.render(); + } + } + } + + async selectionClick(event) { + event.stopPropagation(); + const button = event.currentTarget; + + const update = {}; + if (!button.checked) { + if (button.dataset.cost > 1) { + // Simple handling that doesn't cover potential Custom LevelTiers. + update[`levels.${this.levelup.currentLevel}.choices.-=${button.dataset.option}`] = null; + } else { + update[ + `levels.${this.levelup.currentLevel}.choices.${button.dataset.option}.-=${button.dataset.checkboxNr}` + ] = null; + } + } else { + if (!this.levelup.levels[this.levelup.currentLevel].nrSelections.available) { + ui.notifications.info( + game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.info.insufficentAdvancements') + ); + this.render(); + return; + } + + update[ + `levels.${this.levelup.currentLevel}.choices.${button.dataset.option}.${button.dataset.checkboxNr}` + ] = { + tier: Number(button.dataset.tier), + minCost: Number(button.dataset.cost), + amount: button.dataset.amount ? Number(button.dataset.amount) : null, + value: button.dataset.value, + type: button.dataset.type + }; + } + + await this.levelup.updateSource(update); + this.render(); + } + + static async viewCompendium(_, button) { + (await game.packs.get(`daggerheart.${button.dataset.compendium}`))?.render(true); + } + + static async selectPreview(_, button) { + const remove = button.dataset.selected; + const selectionData = Object.values(this.levelup.selectionData); + const option = remove + ? selectionData.find(x => x.type === 'subclass' && x.data.includes(button.dataset.uuid)) + : selectionData.find(x => x.type === 'subclass' && x.data.length === 0); + if (!option) return; + + const path = `tiers.${option.tier}.levels.${option.level}.optionSelections.${option.optionKey}.${option.checkboxNr}.data`; + await this.levelup.updateSource({ [path]: remove ? [] : button.dataset.uuid }); + this.render(); + } + + static async selectDomain(_, button) { + const option = foundry.utils.getProperty(this.levelup, button.dataset.path); + const domain = option.secondaryData ? null : button.dataset.domain; + + await this.levelup.updateSource({ + multiclass: { domain }, + [`${button.dataset.path}.secondaryData`]: domain + }); + this.render(); + } + + static async updateCurrentLevel(_, button) { + if (!button.dataset.forward) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.localize('DAGGERHEART.Application.LevelUp.Delevel.title') + }, + content: game.i18n.format('DAGGERHEART.Application.LevelUp.Delevel.content') + }); + + if (!confirmed) return; + + await this.levelup.updateSource({ + currentLevel: Math.min(this.levelup.currentLevel - 1, this.levelup.startLevel), + levels: Object.keys(this.levelup.levels).reduce((acc, key) => { + const level = this.levelup.levels[key]; + if (Number(key) === this.levelup.currentLevel) { + acc[key] = { + achievements: { + experiences: getDeleteKeys(level.achievements.experiences, 'name', ''), + domainCards: getDeleteKeys(level.achievements.domainCards, 'uuid', null) + }, + choices: getDeleteKeys(level.choices) + }; + } + return acc; + }, {}) + }); + } else { + await this.levelup.updateSource({ + currentLevel: Math.min(this.levelup.currentLevel + 1, this.levelup.endLevel) + }); + } + + this.tabGroups.primary = 'advancements'; + this.render(); + } + + static activatePart(_, button) { + this.tabGroups.primary = button.dataset.part; + this.render(); + } + + static async save() { + const levelupData = Object.keys(this.levelup.levels).reduce((acc, level) => { + if (level >= this.levelup.startLevel) { + acc[level] = this.levelup.levels[level].toObject(); + } return acc; }, {}); - return { - data: data, - activeLevel: this.activeLevel, - changedLevel: this.actor.system.levelData.changedLevel, - completedSelection: selectedChoices === 2 - }; - } - - static async toggleBox(_, button) { - const path = button.dataset.path; - if (foundry.utils.getProperty(this.data, path)) { - const pathParts = path.split('.'); - const arrayPart = pathParts.slice(0, pathParts.length - 1).join('.'); - let array = foundry.utils.getProperty(this.data, arrayPart); - if (button.dataset.levelAttribute === 'multiclass') { - array = []; - } else { - delete array[Number.parseInt(pathParts[pathParts.length - 1])]; - } - foundry.utils.setProperty(this.data, arrayPart, array); - } else { - const updates = [{ path: path, value: { level: this.activeLevel } }]; - const levelChoices = SYSTEM.ACTOR.levelChoices[button.dataset.levelAttribute]; - if (button.dataset.levelAttribute === 'subclass') { - if (!this.actor.system.multiclassSubclass) { - updates[0].value.value = { - multiclass: false, - feature: this.actor.system.subclass.system.specializationFeature.unlocked - ? 'mastery' - : 'specialization' - }; - } else { - const choices = [ - { name: this.actor.system.subclass.name, value: this.actor.system.subclass.uuid }, - { - name: this.actor.system.multiclassSubclass.name, - value: this.actor.system.multiclassSubclass.uuid - } - ]; - const indexes = await SelectDialog.selectItem({ - actor: this.actor, - choices: choices, - title: levelChoices.title, - nrChoices: 1 - }); - if (indexes.length === 0) { - this.render(); - return; - } - const multiclassSubclass = choices[indexes[0]].name === this.actor.system.multiclassSubclass.name; - updates[0].value.value = { - multiclass: multiclassSubclass, - feature: this.actor.system.multiclassSubclass.system.specializationFeature.unlocked - ? 'mastery' - : 'specialization' - }; - } - } else if (button.dataset.levelAttribute === 'multiclass') { - const multiclassAwait = new Promise(resolve => { - new DhpMulticlassDialog(this.actor.name, this.actor.system.class, resolve).render(true); - }); - const multiclassData = await multiclassAwait; - if (!multiclassData) { - this.render(); - return; - } - - const pathParts = path.split('.'); - const arrayPart = pathParts.slice(0, pathParts.length - 1).join('.'); - updates[0] = { - path: [arrayPart, '0'].join('.'), - value: { - level: this.activeLevel, - value: { - class: multiclassData.class, - subclass: multiclassData.subclass, - domain: multiclassData.domain, - level: this.activeLevel - } - } - }; - updates[1] = { - path: [arrayPart, '1'].join('.'), - value: { - level: this.activeLevel, - value: { - class: multiclassData.class, - subclass: multiclassData.subclass, - domain: multiclassData.domain, - level: this.activeLevel - } - } - }; - } else { - if (levelChoices.choices.length > 0) { - if (typeof levelChoices.choices === 'string') { - const choices = foundry.utils - .getProperty(this.actor, levelChoices.choices) - .map(x => ({ name: x.description, value: x.id })); - const indexes = await SelectDialog.selectItem({ - actor: this.actor, - choices: choices, - title: levelChoices.title, - nrChoices: levelChoices.nrChoices - }); - if (indexes.length === 0) { - this.render(); - return; - } - updates[0].value.value = choices - .filter((_, index) => indexes.includes(index)) - .map(x => x.value); - } else { - const indexes = await SelectDialog.selectItem({ - actor: this.actor, - choices: levelChoices.choices, - title: levelChoices.title, - nrChoices: levelChoices.nrChoices - }); - if (indexes.length === 0) { - this.render(); - return; - } - updates[0].value.value = levelChoices.choices[indexes[0]].path; - } - } - } - - const update = updates.reduce((acc, x) => { - acc[x.path] = x.value; - - return acc; - }, {}); - - this.data = foundry.utils.mergeObject(this.data, update); - } - - this.render(); - } - - static advanceLevel() { - this.activeLevel += 1; - this.render(); - } - - static async finishLevelup() { - this.data.currentLevel = this.data.changedLevel; - let multiclass = null; - for (var level in this.data.levelups) { - for (var tier in this.data.levelups[level]) { - for (var category in this.data.levelups[level][tier]) { - for (var value in this.data.levelups[level][tier][category]) { - if (category === 'multiclass') { - multiclass = this.data.levelups[level][tier][category][value].value; - this.data.levelups[level][tier][category][value] = true; - } else { - this.data.levelups[level][tier][category][value] = - this.data.levelups[level][tier][category][value].value ?? true; - } - } - } - } - } - - const tiersMoved = - getTier(this.actor.system.levelData.changedLevel, true) - - getTier(this.actor.system.levelData.currentLevel, true); - const experiences = Array.from(Array(tiersMoved), (_, index) => ({ - id: foundry.utils.randomID(), - level: this.actor.system.experiences.length + index * 3, - description: '', - value: 1 - })); - - await this.actor.update( - { - system: { - levelData: this.data, - experiences: [...this.actor.system.experiences, ...experiences] - } - }, - { diff: false } - ); - - if (!this.actor.multiclass && multiclass) { - const multiclassClass = (await fromUuid(multiclass.class.uuid)).toObject(); - multiclassClass.system.domains = [multiclass.domain.id]; - multiclassClass.system.multiclass = multiclass.level; - - const multiclassFeatures = []; - for (var i = 0; i < multiclassClass.system.features.length; i++) { - const feature = (await fromUuid(multiclassClass.system.features[i].uuid)).toObject(); - feature.system.multiclass = multiclass.level; - multiclassFeatures.push(feature); - } - - const multiclassSubclass = (await fromUuid(multiclass.subclass.uuid)).toObject(); - multiclassSubclass.system.multiclass = multiclass.level; - - const multiclassSubclassFeatures = {}; - const features = [ - multiclassSubclass.system.foundationFeature, - multiclassSubclass.system.specializationFeature, - multiclassSubclass.system.masteryFeature - ]; - for (var i = 0; i < features.length; i++) { - const path = i === 0 ? 'foundationFeature' : i === 1 ? 'specializationFeature' : 'masteryFeature'; - const feature = features[i]; - for (var ability of feature.abilities) { - const data = (await fromUuid(ability.uuid)).toObject(); - if (i > 0) data.system.disabled = true; - data.system.multiclass = multiclass.level; - if (!multiclassSubclassFeatures[path]) multiclassSubclassFeatures[path] = [data]; - else multiclassSubclassFeatures[path].push(data); - // data.uuid = feature.uuid; - - // const abilityData = await this._onDropItemCreate(data); - // ability.uuid = abilityData[0].uuid; - - // createdItems.push(abilityData); - } - } - - for (let subclassFeaturesKey in multiclassSubclassFeatures) { - const values = multiclassSubclassFeatures[subclassFeaturesKey]; - const abilityResults = await this.actor.createEmbeddedDocuments('Item', values); - for (var i = 0; i < abilityResults.length; i++) { - multiclassSubclass.system[subclassFeaturesKey].abilities[i].uuid = abilityResults[i].uuid; - } - } - - await this.actor.createEmbeddedDocuments('Item', [ - multiclassClass, - ...multiclassFeatures, - multiclassSubclass - ]); - } - + await this.actor.levelUp(levelupData); this.close(); } } diff --git a/module/applications/settings.mjs b/module/applications/settings.mjs index 291b0882..a7eed1b9 100644 --- a/module/applications/settings.mjs +++ b/module/applications/settings.mjs @@ -1,3 +1,5 @@ +import { DualityRollColor } from '../config/settingsConfig.mjs'; +import { defaultLevelTiers, DhLevelTiers } from '../data/levelTier.mjs'; import DhAppearance from '../data/settings/Appearance.mjs'; import DHAppearanceSettings from './settings/appearanceSettings.mjs'; import DhVariantRules from '../data/settings/VariantRules.mjs'; @@ -268,6 +270,23 @@ export const registerDHSettings = () => { default: DhAppearance.defaultSchema }); + game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.DualityRollColor, { + name: game.i18n.localize('DAGGERHEART.Settings.DualityRollColor.Name'), + hint: game.i18n.localize('DAGGERHEART.Settings.DualityRollColor.Hint'), + scope: 'world', + config: true, + type: Number, + choices: Object.values(DualityRollColor), + default: DualityRollColor.colorful.value + }); + + game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.LevelTiers, { + scope: 'world', + config: false, + type: DhLevelTiers, + default: defaultLevelTiers + }); + game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.Automation.Name, { name: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Name'), label: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Label'), diff --git a/module/applications/sheets/adversary.mjs b/module/applications/sheets/adversary.mjs index 2087ea0a..b9180bf3 100644 --- a/module/applications/sheets/adversary.mjs +++ b/module/applications/sheets/adversary.mjs @@ -362,7 +362,7 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) { name: x.actor.name, img: x.actor.img, difficulty: x.actor.system.difficulty, - evasion: x.actor.system.evasion + evasion: x.actor.system.evasion.value })); const cls = getDocumentClass('ChatMessage'); diff --git a/module/applications/sheets/pc.mjs b/module/applications/sheets/pc.mjs index d6167085..03aa2d77 100644 --- a/module/applications/sheets/pc.mjs +++ b/module/applications/sheets/pc.mjs @@ -1,10 +1,10 @@ import { capitalize } from '../../helpers/utils.mjs'; import DhpDeathMove from '../deathMove.mjs'; import DhpDowntime from '../downtime.mjs'; -import DhpLevelup from '../levelup.mjs'; import AncestrySelectionDialog from '../ancestrySelectionDialog.mjs'; import DaggerheartSheet from './daggerheart-sheet.mjs'; import { abilities } from '../../config/actorConfig.mjs'; +import DhlevelUp from '../levelup.mjs'; const { ActorSheetV2 } = foundry.applications.sheets; const { TextEditor } = foundry.applications.ux; @@ -167,13 +167,23 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { _attachPartListeners(partId, htmlElement, options) { super._attachPartListeners(partId, htmlElement, options); - $(htmlElement).find('.attribute-value').on('change', this.attributeChange.bind(this)); - $(htmlElement).find('.tab-selector').on('click', this.tabSwitch.bind(this)); - $(htmlElement).find('.level-title.levelup').on('click', this.openLevelUp.bind(this)); - $(htmlElement).find('.feature-input').on('change', this.onFeatureInputBlur.bind(this)); - $(htmlElement).find('.experience-description').on('change', this.experienceDescriptionChange.bind(this)); - $(htmlElement).find('.experience-value').on('change', this.experienceValueChange.bind(this)); - $(htmlElement).find('[data-item]').on('change', this.itemUpdate.bind(this)); + htmlElement + .querySelectorAll('.attribute-value') + .forEach(element => element.addEventListener('change', this.attributeChange.bind(this))); + htmlElement + .querySelectorAll('.tab-selector') + .forEach(element => element.addEventListener('click', this.tabSwitch.bind(this))); + htmlElement.querySelector('.level-title.levelup')?.addEventListener('click', this.openLevelUp.bind(this)); + htmlElement + .querySelectorAll('.feature-input') + .forEach(element => element.addEventListener('change', this.onFeatureInputBlur.bind(this))); + htmlElement + .querySelectorAll('.experience-description') + .forEach(element => element.addEventListener('change', this.experienceDescriptionChange.bind(this))); + htmlElement + .querySelectorAll('.experience-value') + .forEach(element => element.addEventListener('change', this.experienceValueChange.bind(this))); + htmlElement.querySelector('.level-value').addEventListener('change', this.onLevelChange.bind(this)); } async _prepareContext(_options) { @@ -188,7 +198,7 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { context.storyEditor = this.storyEditor; context.multiclassFeatureSetSelected = this.multiclassFeatureSetSelected; - const selectedAttributes = Object.values(this.document.system.attributes).map(x => x.data.base); + const selectedAttributes = Object.values(this.document.system.traits).map(x => x.base); context.abilityScoreArray = JSON.parse( await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.AbilityArray) ).reduce((acc, x) => { @@ -215,9 +225,9 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { } : {}; - context.attributes = Object.keys(this.document.system.attributes).reduce((acc, key) => { + context.attributes = Object.keys(this.document.system.traits).reduce((acc, key) => { acc[key] = { - ...this.document.system.attributes[key], + ...this.document.system.traits[key], name: game.i18n.localize(SYSTEM.ACTOR.abilities[key].name), verbs: SYSTEM.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x)) }; @@ -479,7 +489,7 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { } async attributeChange(event) { - const path = `system.attributes.${event.currentTarget.dataset.attribute}.data.base`; + const path = `system.traits.${event.currentTarget.dataset.attribute}.base`; await this.document.update({ [path]: event.currentTarget.value }); } @@ -531,21 +541,21 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { } static async toggleAttributeMark(_, button) { - const attribute = this.document.system.attributes[button.dataset.attribute]; + const attribute = this.document.system.traits[button.dataset.attribute]; const newMark = this.document.system.availableAttributeMarks - .filter(x => x > Math.max.apply(null, this.document.system.attributes[button.dataset.attribute].levelMarks)) + .filter(x => x > Math.max.apply(null, this.document.system.traits[button.dataset.attribute].levelMarks)) .sort((a, b) => (a > b ? 1 : -1))[0]; if (attribute.levelMark || !newMark) return; - const path = `system.attributes.${button.dataset.attribute}.levelMarks`; + const path = `system.traits.${button.dataset.attribute}.levelMarks`; await this.document.update({ [path]: [...attribute.levelMarks, newMark] }); } static async toggleHP(_, button) { const healthValue = Number.parseInt(button.dataset.value); - const newValue = this.document.system.resources.health.value >= healthValue ? healthValue - 1 : healthValue; - await this.document.update({ 'system.resources.health.value': newValue }); + const newValue = this.document.system.resources.hitPoints.value >= healthValue ? healthValue - 1 : healthValue; + await this.document.update({ 'system.resources.hitPoints.value': newValue }); } static async toggleStress(_, button) { @@ -576,7 +586,7 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { type: weapon.system.damage.type, bonusDamage: this.document.system.bonuses.damage }; - const modifier = this.document.system.attributes[weapon.system.trait].data.value; + const modifier = this.document.system.traits[weapon.system.trait].value; const { roll, hope, fear, advantage, disadvantage, modifiers, bonusDamageString } = await this.document.dualityRoll( @@ -592,7 +602,7 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { name: x.actor.name, img: x.actor.img, difficulty: x.actor.system.difficulty, - evasion: x.actor.system.evasion + evasion: x.actor.system.evasion.value })); const systemData = { @@ -633,7 +643,12 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { } openLevelUp() { - new DhpLevelup(this.document).render(true); + if (!this.document.system.class || !this.document.system.subclass) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Sheets.PC.Errors.missingClassOrSubclass')); + return; + } + + new DhlevelUp(this.document).render(true); } static domainCardsTab(toVault) { @@ -781,7 +796,7 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { } static async makeDeathMove() { - if (this.document.system.resources.health.value === this.document.system.resources.health.max) { + if (this.document.system.resources.hitPoints.value === this.document.system.resources.hitPoints.max) { await new DhpDeathMove(this.document).render(true); await this.minimize(); } @@ -866,6 +881,11 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { await item.update({ [name]: event.currentTarget.value }); } + async onLevelChange(event) { + await this.document.updateLevel(Number(event.currentTarget.value)); + this.render(); + } + static async deleteItem(_, button) { const item = await fromUuid($(button).closest('[data-item-id]')[0].dataset.itemId); await item.delete(); diff --git a/module/config/actorConfig.mjs b/module/config/actorConfig.mjs index 241d3302..4db5ca9c 100644 --- a/module/config/actorConfig.mjs +++ b/module/config/actorConfig.mjs @@ -52,31 +52,31 @@ export const abilities = { export const featureProperties = { agility: { name: 'DAGGERHEART.Abilities.agility.name', - path: actor => actor.system.attributes.agility.data.value + path: actor => actor.system.traits.agility.data.value }, strength: { name: 'DAGGERHEART.Abilities.strength.name', - path: actor => actor.system.attributes.strength.data.value + path: actor => actor.system.traits.strength.data.value }, finesse: { name: 'DAGGERHEART.Abilities.finesse.name', - path: actor => actor.system.attributes.finesse.data.value + path: actor => actor.system.traits.finesse.data.value }, instinct: { name: 'DAGGERHEART.Abilities.instinct.name', - path: actor => actor.system.attributes.instinct.data.value + path: actor => actor.system.traits.instinct.data.value }, presence: { name: 'DAGGERHEART.Abilities.presence.name', - path: actor => actor.system.attributes.presence.data.value + path: actor => actor.system.traits.presence.data.value }, knowledge: { name: 'DAGGERHEART.Abilities.knowledge.name', - path: actor => actor.system.attributes.knowledge.data.value + path: actor => actor.system.traits.knowledge.data.value }, spellcastingTrait: { name: 'DAGGERHEART.FeatureProperty.SpellcastingTrait', - path: actor => actor.system.attributes[actor.system.subclass.system.spellcastingTrait].data.value + path: actor => actor.system.traits[actor.system.subclass.system.spellcastingTrait].data.value } }; diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index b2e17549..3a33e61b 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -31,6 +31,19 @@ export const gameSettings = { AbilityArray: 'AbilityArray', RangeMeasurement: 'RangeMeasurement' }, + DualityRollColor: 'DualityRollColor', + LevelTiers: 'LevelTiers', appearance: 'Appearance', variantRules: 'VariantRules' }; + +export const DualityRollColor = { + colorful: { + value: 0, + label: 'DAGGERHEART.Settings.DualityRollColor.Options.Colorful' + }, + normal: { + value: 1, + label: 'DAGGERHEART.Settings.DualityRollColor.Options.Normal' + } +}; diff --git a/module/data/levelTier.mjs b/module/data/levelTier.mjs new file mode 100644 index 00000000..6cf11252 --- /dev/null +++ b/module/data/levelTier.mjs @@ -0,0 +1,340 @@ +export class DhLevelTiers extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + tiers: new fields.TypedObjectField(new fields.EmbeddedDataField(DhLevelTier)) + }; + } + + get availableChoicesPerLevel() { + return Object.values(this.tiers).reduce((acc, tier) => { + for (var level = tier.levels.start; level < tier.levels.end + 1; level++) { + acc[level] = tier.availableOptions; + } + + return acc; + }, {}); + } +} + +class DhLevelTier extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + tier: new fields.NumberField({ required: true, integer: true }), + name: new fields.StringField({ required: true }), + levels: new fields.SchemaField({ + start: new fields.NumberField({ required: true, integer: true }), + end: new fields.NumberField({ required: true, integer: true }) + }), + initialAchievements: new fields.SchemaField({ + experience: new fields.SchemaField({ + nr: new fields.NumberField({ required: true, initial: 1 }), + modifier: new fields.NumberField({ required: true, initial: 2 }) + }), + proficiency: new fields.NumberField({ integer: true, initial: 1 }) + }), + availableOptions: new fields.NumberField({ required: true, initial: 2 }), + domainCardByLevel: new fields.NumberField({ initial: 1 }), + options: new fields.TypedObjectField(new fields.EmbeddedDataField(DhLevelOption)) + }; + } +} + +class DhLevelOption extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + label: new fields.StringField({ required: true }), + checkboxSelections: new fields.NumberField({ required: true, integer: true, initial: 1 }), + minCost: new fields.NumberField({ required: true, integer: true, initial: 1 }), + type: new fields.StringField({ required: true, choices: LevelOptionType }), + value: new fields.NumberField({ integer: true }), + amount: new fields.NumberField({ integer: true }) + }; + } +} + +export const LevelOptionType = { + trait: { + id: 'trait', + label: 'Character Trait', + dataPath: '' + }, + hitPoint: { + id: 'hitPoint', + label: 'Hit Points', + dataPath: 'resources.hitPoints', + dataPathData: { + property: 'max', + dependencies: ['value'] + } + }, + stress: { + id: 'stress', + label: 'Stress', + dataPath: 'resources.stress', + dataPathData: { + property: 'max', + dependencies: ['value'] + } + }, + evasion: { + id: 'evasion', + label: 'Evasion', + dataPath: 'evasion' + }, + proficiency: { + id: 'proficiency', + label: 'Proficiency' + }, + experience: { + id: 'experience', + label: 'Experience' + }, + domainCard: { + id: 'domainCard', + label: 'Domain Card' + }, + subclass: { + id: 'subclass', + label: 'Subclass' + }, + multiclass: { + id: 'multiclass', + label: 'Multiclass' + } +}; + +export const defaultLevelTiers = { + tiers: { + 2: { + tier: 2, + name: 'Tier 2', + levels: { + start: 2, + end: 4 + }, + initialAchievements: { + experience: { + nr: 1, + modifier: 2 + }, + proficiency: 1 + }, + availableOptions: 2, + domainCardByLevel: 1, + options: { + trait: { + label: 'DAGGERHEART.LevelUp.Options.trait', + checkboxSelections: 3, + minCost: 1, + type: LevelOptionType.trait.id, + amount: 2 + }, + hitPoint: { + label: 'DAGGERHEART.LevelUp.Options.hitPoint', + checkboxSelections: 2, + minCost: 1, + type: LevelOptionType.hitPoint.id, + value: 1, + value: 1 + }, + stress: { + label: 'DAGGERHEART.LevelUp.Options.stress', + checkboxSelections: 2, + minCost: 1, + type: LevelOptionType.stress.id, + value: 1 + }, + experience: { + label: 'DAGGERHEART.LevelUp.Options.experience', + checkboxSelections: 1, + minCost: 1, + type: LevelOptionType.experience.id, + value: 1, + amount: 2 + }, + domainCard: { + label: 'DAGGERHEART.LevelUp.Options.domainCard', + checkboxSelections: 1, + minCost: 1, + type: LevelOptionType.domainCard.id, + amount: 1 + }, + evasion: { + label: 'DAGGERHEART.LevelUp.Options.evasion', + checkboxSelections: 1, + minCost: 1, + type: LevelOptionType.evasion.id, + value: 1 + } + } + }, + 3: { + tier: 3, + name: 'Tier 3', + levels: { + start: 5, + end: 7 + }, + initialAchievements: { + experience: { + nr: 1, + modifier: 2 + }, + proficiency: 1 + }, + availableOptions: 2, + domainCardByLevel: 1, + options: { + trait: { + label: 'DAGGERHEART.LevelUp.Options.trait', + checkboxSelections: 3, + minCost: 1, + type: LevelOptionType.trait.id, + amount: 2 + }, + hitPoint: { + label: 'DAGGERHEART.LevelUp.Options.hitPoint', + checkboxSelections: 2, + minCost: 1, + type: LevelOptionType.hitPoint.id, + value: 1 + }, + stress: { + label: 'DAGGERHEART.LevelUp.Options.stress', + checkboxSelections: 2, + minCost: 1, + type: LevelOptionType.stress.id, + value: 1 + }, + experience: { + label: 'DAGGERHEART.LevelUp.Options.experience', + checkboxSelections: 1, + minCost: 1, + type: LevelOptionType.experience.id, + value: 1, + amount: 2 + }, + domainCard: { + label: 'DAGGERHEART.LevelUp.Options.domainCard', + checkboxSelections: 1, + minCost: 1, + type: LevelOptionType.domainCard.id, + amount: 1 + }, + evasion: { + label: 'DAGGERHEART.LevelUp.Options.evasion', + checkboxSelections: 1, + minCost: 1, + type: LevelOptionType.evasion.id, + value: 1 + }, + subclass: { + label: 'DAGGERHEART.LevelUp.Options.subclass', + checkboxSelections: 1, + minCost: 1, + type: LevelOptionType.subclass.id + }, + proficiency: { + label: 'DAGGERHEART.LevelUp.Options.proficiency', + checkboxSelections: 2, + minCost: 2, + type: LevelOptionType.proficiency.id, + value: 1 + }, + multiclass: { + label: 'DAGGERHEART.LevelUp.Options.multiclass', + checkboxSelections: 2, + minCost: 2, + type: LevelOptionType.multiclass.id + } + } + }, + 4: { + tier: 4, + name: 'Tier 4', + levels: { + start: 8, + end: 10 + }, + initialAchievements: { + experience: { + nr: 1, + modifier: 2 + }, + proficiency: 1 + }, + availableOptions: 2, + domainCardByLevel: 1, + options: { + trait: { + label: 'DAGGERHEART.LevelUp.Options.trait', + checkboxSelections: 3, + minCost: 1, + type: LevelOptionType.trait.id, + amount: 2 + }, + hitPoint: { + label: 'DAGGERHEART.LevelUp.Options.hitPoint', + checkboxSelections: 2, + minCost: 1, + type: LevelOptionType.hitPoint.id, + value: 1 + }, + stress: { + label: 'DAGGERHEART.LevelUp.Options.stress', + checkboxSelections: 2, + minCost: 1, + type: LevelOptionType.stress.id, + value: 1 + }, + experience: { + label: 'DAGGERHEART.LevelUp.Options.experience', + checkboxSelections: 1, + minCost: 1, + type: LevelOptionType.experience.id, + value: 1, + amount: 2 + }, + domainCard: { + label: 'DAGGERHEART.LevelUp.Options.domainCard', + checkboxSelections: 1, + minCost: 1, + type: LevelOptionType.domainCard.id, + amount: 1 + }, + evasion: { + label: 'DAGGERHEART.LevelUp.Options.evasion', + checkboxSelections: 1, + minCost: 1, + type: LevelOptionType.evasion.id, + value: 1 + }, + subclass: { + label: 'DAGGERHEART.LevelUp.Options.subclass', + checkboxSelections: 1, + minCost: 1, + type: LevelOptionType.subclass.id + }, + proficiency: { + label: 'DAGGERHEART.LevelUp.Options.proficiency', + checkboxSelections: 2, + minCost: 2, + type: LevelOptionType.proficiency.id, + value: 1 + }, + multiclass: { + label: 'DAGGERHEART.LevelUp.Options.multiclass', + checkboxSelections: 2, + minCost: 2, + type: LevelOptionType.multiclass.id + } + } + } + } +}; diff --git a/module/data/levelup.mjs b/module/data/levelup.mjs new file mode 100644 index 00000000..0f9204e0 --- /dev/null +++ b/module/data/levelup.mjs @@ -0,0 +1,311 @@ +import { chunkify } from '../helpers/utils.mjs'; +import { LevelOptionType } from './levelTier.mjs'; + +export class DhLevelup extends foundry.abstract.DataModel { + static initializeData(levelTierData, pcLevelData) { + const startLevel = pcLevelData.level.current + 1; + const currentLevel = pcLevelData.level.current + 1; + const endLevel = pcLevelData.level.changed; + + const tiers = {}; + const levels = {}; + const tierKeys = Object.keys(levelTierData.tiers); + tierKeys.forEach(key => { + const tier = levelTierData.tiers[key]; + const belongingLevels = []; + for (var i = tier.levels.start; i <= tier.levels.end; i++) { + if (i <= endLevel) { + const initialAchievements = i === tier.levels.start ? tier.initialAchievements : {}; + const experiences = initialAchievements.experience + ? [...Array(initialAchievements.experience.nr).keys()].reduce((acc, _) => { + acc[foundry.utils.randomID()] = { + name: '', + modifier: initialAchievements.experience.modifier + }; + return acc; + }, {}) + : {}; + const domainCards = [...Array(tier.domainCardByLevel).keys()].reduce((acc, _) => { + const id = foundry.utils.randomID(); + acc[id] = { uuid: null, itemUuid: null, level: i }; + return acc; + }, {}); + + levels[i] = DhLevelupLevel.initializeData(pcLevelData.levelups[i], tier.availableOptions, { + ...initialAchievements, + experiences, + domainCards + }); + } + + belongingLevels.push(i); + } + + tiers[key] = { + name: tier.name, + belongingLevels: belongingLevels, + options: Object.keys(tier.options).reduce((acc, key) => { + acc[key] = tier.options[key].toObject(); + return acc; + }, {}) + }; + }); + + return { + tiers, + levels, + startLevel, + currentLevel, + endLevel + }; + } + + static defineSchema() { + const fields = foundry.data.fields; + + return { + tiers: new fields.TypedObjectField( + new fields.SchemaField({ + name: new fields.StringField({ required: true }), + belongingLevels: new fields.ArrayField(new fields.NumberField({ required: true, integer: true })), + options: new fields.TypedObjectField( + new fields.SchemaField({ + label: new fields.StringField({ required: true }), + checkboxSelections: new fields.NumberField({ required: true, integer: true }), + minCost: new fields.NumberField({ required: true, integer: true }), + type: new fields.StringField({ required: true, choices: LevelOptionType }), + value: new fields.NumberField({ integer: true }), + amount: new fields.NumberField({ integer: true }) + }) + ) + }) + ), + levels: new fields.TypedObjectField(new fields.EmbeddedDataField(DhLevelupLevel)), + startLevel: new fields.NumberField({ required: true, integer: true }), + currentLevel: new fields.NumberField({ required: true, integer: true }), + endLevel: new fields.NumberField({ required: true, integer: true }) + }; + } + + #levelFinished(levelKey) { + const allSelectionsMade = this.levels[levelKey].nrSelections.available === 0; + const allChoicesMade = Object.keys(this.levels[levelKey].choices).every(choiceKey => { + const choice = this.levels[levelKey].choices[choiceKey]; + return Object.values(choice).every(checkbox => { + switch (choiceKey) { + case 'trait': + case 'experience': + case 'domainCard': + case 'subclass': + return checkbox.amount ? checkbox.data.length === checkbox.amount : checkbox.data.length === 1; + case 'multiclass': + const classSelected = checkbox.data.length === 1; + const domainSelected = checkbox.secondaryData; + return classSelected && domainSelected; + default: + return true; + } + }); + }); + const experiencesSelected = !this.levels[levelKey].achievements.experiences + ? true + : Object.values(this.levels[levelKey].achievements.experiences).every(exp => exp.name); + const domainCardsSelected = Object.values(this.levels[levelKey].achievements.domainCards) + .filter(x => x.level <= this.endLevel) + .every(card => card.uuid); + const allAchievementsSelected = experiencesSelected && domainCardsSelected; + + return allSelectionsMade && allChoicesMade && allAchievementsSelected; + } + + get currentLevelFinished() { + return this.#levelFinished(this.currentLevel); + } + + get allLevelsFinished() { + return Object.keys(this.levels) + .filter(level => Number(level) >= this.startLevel) + .every(this.#levelFinished.bind(this)); + } + + get classUpgradeChoices() { + let subclass = null; + let multiclass = null; + Object.keys(this.levels).forEach(levelKey => { + const level = this.levels[levelKey]; + Object.values(level.choices).forEach(choice => { + Object.values(choice).forEach(checkbox => { + if (checkbox.type === 'multiclass') { + multiclass = { + class: checkbox.data.length > 0 ? checkbox.data[0] : null, + domain: checkbox.secondaryData ?? null, + tier: checkbox.tier, + level: levelKey + }; + } + if (checkbox.type === 'subclass') { + subclass = { + tier: checkbox.tier, + level: levelKey + }; + } + }); + }); + }); + return { subclass, multiclass }; + } + + get tiersForRendering() { + const tierKeys = Object.keys(this.tiers); + const selections = Object.keys(this.levels).reduce( + (acc, key) => { + const level = this.levels[key]; + Object.keys(level.choices).forEach(optionKey => { + const choice = level.choices[optionKey]; + Object.keys(choice).forEach(checkboxNr => { + const checkbox = choice[checkboxNr]; + if (!acc[checkbox.tier][optionKey]) acc[checkbox.tier][optionKey] = {}; + Object.keys(choice).forEach(checkboxNr => { + acc[checkbox.tier][optionKey][checkboxNr] = { ...checkbox, level: Number(key) }; + }); + }); + }); + + return acc; + }, + tierKeys.reduce((acc, key) => { + acc[key] = {}; + return acc; + }, {}) + ); + + const { multiclass, subclass } = this.classUpgradeChoices; + return tierKeys.map(tierKey => { + const tier = this.tiers[tierKey]; + const multiclassInTier = multiclass?.tier === Number(tierKey); + const subclassInTier = subclass?.tier === Number(tierKey); + + return { + name: tier.name, + active: this.currentLevel >= Math.min(...tier.belongingLevels), + groups: Object.keys(tier.options).map(optionKey => { + const option = tier.options[optionKey]; + + const checkboxes = [...Array(option.checkboxSelections).keys()].flatMap(index => { + const checkboxNr = index + 1; + const checkboxData = selections[tierKey]?.[optionKey]?.[checkboxNr]; + const checkbox = { ...option, checkboxNr, tier: tierKey }; + + if (checkboxData) { + checkbox.level = checkboxData.level; + checkbox.selected = true; + checkbox.disabled = checkbox.level !== this.currentLevel; + } + + if (optionKey === 'multiclass') { + if ((multiclass && !multiclassInTier) || subclassInTier) { + checkbox.disabled = true; + } + } + + if (optionKey === 'subclass' && multiclassInTier) { + checkbox.disabled = true; + } + + return checkbox; + }); + return { + label: game.i18n.localize(option.label), + checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => { + const anySelected = chunkedBoxes.some(x => x.selected); + const anyDisabled = chunkedBoxes.some(x => x.disabled); + return { + multi: option.minCost > 1, + checkboxes: chunkedBoxes.map(x => ({ + ...x, + selected: anySelected, + disabled: anyDisabled + })) + }; + }) + }; + }) + }; + }); + } +} + +export class DhLevelupLevel extends foundry.abstract.DataModel { + static initializeData(levelData = { selections: [] }, maxSelections, achievements) { + return { + maxSelections: maxSelections, + achievements: { + experiences: levelData.achievements?.experiences ?? achievements.experiences ?? {}, + domainCards: levelData.achievements?.domainCards + ? levelData.achievements.domainCards.reduce((acc, card, index) => { + acc[index] = { ...card }; + return acc; + }, {}) + : (achievements.domainCards ?? {}), + proficiency: levelData.achievements?.proficiency ?? achievements.proficiency ?? null + }, + choices: levelData.selections.reduce((acc, data) => { + if (!acc[data.optionKey]) acc[data.optionKey] = {}; + acc[data.optionKey][data.checkboxNr] = { ...data }; + + return acc; + }, {}) + }; + } + + static defineSchema() { + const fields = foundry.data.fields; + + return { + maxSelections: new fields.NumberField({ required: true, integer: true }), + achievements: new fields.SchemaField({ + experiences: new fields.TypedObjectField( + new fields.SchemaField({ + name: new fields.StringField({ required: true }), + modifier: new fields.NumberField({ required: true, integer: true }) + }) + ), + domainCards: new fields.TypedObjectField( + new fields.SchemaField({ + uuid: new fields.StringField({ required: true, nullable: true, initial: null }), + itemUuid: new fields.StringField({ required: true }), + level: new fields.NumberField({ required: true, integer: true }) + }) + ), + proficiency: new fields.NumberField({ integer: true }) + }), + choices: new fields.TypedObjectField( + new fields.TypedObjectField( + new fields.SchemaField({ + tier: new fields.NumberField({ required: true, integer: true }), + minCost: new fields.NumberField({ required: true, integer: true }), + amount: new fields.NumberField({ integer: true }), + value: new fields.StringField(), + data: new fields.ArrayField(new fields.StringField()), + secondaryData: new fields.StringField(), + type: new fields.StringField({ required: true }) + }) + ) + ) + }; + } + + get nrSelections() { + const selections = Object.keys(this.choices).reduce((acc, choiceKey) => { + const choice = this.choices[choiceKey]; + acc += Object.values(choice).reduce((acc, x) => acc + x.minCost, 0); + + return acc; + }, 0); + + return { + selections: selections, + available: this.maxSelections - selections + }; + } +} diff --git a/module/data/pc.mjs b/module/data/pc.mjs index 9ab9dbd4..a259ee9d 100644 --- a/module/data/pc.mjs +++ b/module/data/pc.mjs @@ -1,50 +1,29 @@ -import { getPathValue, getTier } from '../helpers/utils.mjs'; +import { getPathValue } from '../helpers/utils.mjs'; +import { LevelOptionType } from './levelTier.mjs'; const fields = foundry.data.fields; const attributeField = () => new fields.SchemaField({ - data: new fields.SchemaField({ - value: new fields.NumberField({ initial: 0, integer: true }), - base: new fields.NumberField({ initial: 0, integer: true }), - bonus: new fields.NumberField({ initial: 0, integer: true }), - actualValue: new fields.NumberField({ initial: 0, integer: true }), - overrideValue: new fields.NumberField({ initial: 0, integer: true }) - }), - levelMarks: new fields.ArrayField(new fields.NumberField({ nullable: true, initial: null, integer: true })), - levelMark: new fields.NumberField({ nullable: true, initial: null, integer: true }) + bonus: new fields.NumberField({ initial: 0, integer: true }), + base: new fields.NumberField({ initial: 0, integer: true }), + tierMarked: new fields.BooleanField({ required: true, initial: false }) }); -const levelUpTier = () => ({ - attributes: new fields.TypedObjectField(new fields.BooleanField()), - hitPointSlots: new fields.TypedObjectField(new fields.BooleanField()), - stressSlots: new fields.TypedObjectField(new fields.BooleanField()), - experiences: new fields.TypedObjectField(new fields.ArrayField(new fields.StringField({}))), - proficiency: new fields.TypedObjectField(new fields.BooleanField()), - armorOrEvasionSlot: new fields.TypedObjectField(new fields.StringField({})), - subclass: new fields.TypedObjectField( - new fields.SchemaField({ - multiclass: new fields.BooleanField(), - feature: new fields.StringField({}) - }) - ), - multiclass: new fields.TypedObjectField(new fields.BooleanField()) -}); +const resourceField = max => + new fields.SchemaField({ + value: new fields.NumberField({ initial: 0, integer: true }), + bonus: new fields.NumberField({ initial: 0, integer: true }), + min: new fields.NumberField({ initial: 0, integer: true }), + baseMax: new fields.NumberField({ initial: max, integer: true }) + }); export default class DhpPC extends foundry.abstract.TypeDataModel { static defineSchema() { return { resources: new fields.SchemaField({ - health: new fields.SchemaField({ - value: new fields.NumberField({ initial: 0, integer: true }), - min: new fields.NumberField({ initial: 0, integer: true }), - max: new fields.NumberField({ initial: 6, integer: true }) - }), - stress: new fields.SchemaField({ - value: new fields.NumberField({ initial: 0, integer: true }), - min: new fields.NumberField({ initial: 0, integer: true }), - max: new fields.NumberField({ initial: 6, integer: true }) - }), + hitPoints: resourceField(6), + stress: resourceField(6), hope: new fields.SchemaField({ value: new fields.NumberField({ initial: -1, integer: true }), // FIXME. Logic is gte and needs -1 in PC/Hope. Change to 0 min: new fields.NumberField({ initial: 0, integer: true }) @@ -61,7 +40,7 @@ export default class DhpPC extends foundry.abstract.TypeDataModel { }) ) }), - attributes: new fields.SchemaField({ + traits: new fields.SchemaField({ agility: attributeField(), strength: attributeField(), finesse: attributeField(), @@ -70,22 +49,22 @@ export default class DhpPC extends foundry.abstract.TypeDataModel { knowledge: attributeField() }), proficiency: new fields.SchemaField({ - value: new fields.NumberField({ initial: 1, integer: true }), - min: new fields.NumberField({ initial: 1, integer: true }), - max: new fields.NumberField({ initial: 6, integer: true }) + base: new fields.NumberField({ required: true, initial: 1, integer: true }), + bonus: new fields.NumberField({ required: true, initial: 0, integer: true }) + }), + evasion: new fields.SchemaField({ + bonus: new fields.NumberField({ initial: 0, integer: true }) }), - evasion: new fields.NumberField({ initial: 0, integer: true }), experiences: new fields.ArrayField( new fields.SchemaField({ id: new fields.StringField({ required: true }), - level: new fields.NumberField({ required: true, integer: true }), description: new fields.StringField({}), value: new fields.NumberField({ integer: true, nullable: true, initial: null }) }), { initial: [ - { id: foundry.utils.randomID(), level: 1, description: '', value: 2 }, - { id: foundry.utils.randomID(), level: 1, description: '', value: 2 } + { id: foundry.utils.randomID(), description: '', value: 2 }, + { id: foundry.utils.randomID(), description: '', value: 2 } ] } ), @@ -100,30 +79,6 @@ export default class DhpPC extends foundry.abstract.TypeDataModel { maxLoadout: new fields.NumberField({ initial: 2, integer: true }), maxCards: new fields.NumberField({ initial: 2, integer: true }) }), - levelData: new fields.SchemaField({ - currentLevel: new fields.NumberField({ initial: 1, integer: true }), - changedLevel: new fields.NumberField({ initial: 1, integer: true }), - levelups: new fields.TypedObjectField( - new fields.SchemaField({ - level: new fields.NumberField({ required: true, integer: true }), - tier1: new fields.SchemaField({ - ...levelUpTier() - }), - tier2: new fields.SchemaField( - { - ...levelUpTier() - }, - { nullable: true, initial: null } - ), - tier3: new fields.SchemaField( - { - ...levelUpTier() - }, - { nullable: true, initial: null } - ) - }) - ) - }), story: new fields.SchemaField({ background: new fields.HTMLField(), appearance: new fields.HTMLField(), @@ -140,15 +95,11 @@ export default class DhpPC extends foundry.abstract.TypeDataModel { armorMarks: new fields.SchemaField({ max: new fields.NumberField({ initial: 6, integer: true }), value: new fields.NumberField({ initial: 0, integer: true }) - }) + }), + levelData: new fields.EmbeddedDataField(DhPCLevelData) }; } - get canLevelUp() { - // return Object.values(this.levels.data).some(x => !x.completed); - return this.levelData.currentLevel !== this.levelData.changedLevel; - } - get tier() { return this.#getTier(this.levelData.currentLevel); } @@ -281,42 +232,6 @@ export default class DhpPC extends foundry.abstract.TypeDataModel { } } - get inventoryWeapons() { - const inventoryWeaponFirst = this.parent.items.find(x => x.type === 'weapon' && x.system.inventoryWeapon === 1); - const inventoryWeaponSecond = this.parent.items.find( - x => x.type === 'weapon' && x.system.inventoryWeapon === 2 - ); - return { - first: this.#weaponData(inventoryWeaponFirst), - second: this.#weaponData(inventoryWeaponSecond) - }; - } - - get totalAttributeMarks() { - return Object.keys(this.levelData.levelups).reduce((nr, level) => { - const nrAttributeMarks = Object.keys(this.levelData.levelups[level]).reduce((nr, tier) => { - nr += Object.keys(this.levelData.levelups[level][tier]?.attributes ?? {}).length * 2; - - return nr; - }, 0); - - nr.push(...Array(nrAttributeMarks).fill(Number.parseInt(level))); - - return nr; - }, []); - } - - get availableAttributeMarks() { - const attributeMarks = Object.keys(this.attributes).flatMap(y => this.attributes[y].levelMarks); - return this.totalAttributeMarks.reduce((acc, attribute) => { - if (!attributeMarks.findSplice(x => x === attribute)) { - acc.push(attribute); - } - - return acc; - }, []); - } - get effects() { return this.parent.items.reduce((acc, item) => { const effects = item.system.effectData; @@ -367,141 +282,37 @@ export default class DhpPC extends foundry.abstract.TypeDataModel { : null; } + prepareBaseData() { + this.resources.hitPoints.max = this.resources.hitPoints.baseMax + this.resources.hitPoints.bonus; + this.resources.stress.max = this.resources.stress.baseMax + this.resources.stress.bonus; + this.evasion.value = (this.class?.system?.evasion ?? 0) + this.evasion.bonus; + this.proficiency.value = this.proficiency.base + this.proficiency.bonus; + + for (var attributeKey in this.traits) { + const attribute = this.traits[attributeKey]; + attribute.value = attribute.base + attribute.bonus; + } + } + prepareDerivedData() { this.resources.hope.max = 6 - this.story.scars.length; if (this.resources.hope.value >= this.resources.hope.max) { this.resources.hope.value = Math.max(this.resources.hope.max - 1, 0); } - for (var attributeKey in this.attributes) { - const attribute = this.attributes[attributeKey]; + const armor = this.armor; + this.damageThresholds = { + major: armor + ? armor.system.baseThresholds.major + this.levelData.level.current + : this.levelData.level.current, + severe: armor + ? armor.system.baseThresholds.severe + this.levelData.level.current + : this.levelData.level.current * 2 + }; - attribute.levelMark = attribute.levelMarks.find(x => this.isSameTier(x)) ?? null; - - const actualValue = attribute.data.base + attribute.levelMarks.length + attribute.data.bonus; - attribute.data.actualValue = actualValue; - attribute.data.value = attribute.data.overrideValue - ? attribute.data.overrideValue - : attribute.data.actualValue; - } - - this.evasion = this.class?.system?.evasion ?? 0; - // this.armor.value = this.activeArmor?.baseScore ?? 0; - this.damageThresholds = this.computeDamageThresholds(); - - this.applyLevels(); this.applyEffects(); } - computeDamageThresholds() { - // TODO: missing weapon features and domain cards calculation - if (!this.armor) { - return { - major: this.levelData.currentLevel, - severe: this.levelData.currentLevel * 2 - }; - } - const { - baseThresholds: { major = 0, severe = 0 } - } = this.armor.system; - return { - major: major + this.levelData.currentLevel, - severe: severe + this.levelData.currentLevel - }; - } - - applyLevels() { - let healthBonus = 0, - stressBonus = 0, - proficiencyBonus = 0, - evasionBonus = 0, - armorBonus = 0; - let experienceBonuses = {}; - let advancementFirst = null, - advancementSecond = null; - for (var level in this.levelData.levelups) { - var levelData = this.levelData.levelups[level]; - for (var tier in levelData) { - var tierData = levelData[tier]; - if (tierData) { - healthBonus += Object.keys(tierData.hitPointSlots).length; - stressBonus += Object.keys(tierData.stressSlots).length; - proficiencyBonus += Object.keys(tierData.proficiency).length; - advancementFirst = - Object.keys(tierData.subclass).length > 0 && level >= 5 && level <= 7 - ? { ...tierData.subclass[0], tier: getTier(Number.parseInt(level), true) } - : advancementFirst; - advancementSecond = - Object.keys(tierData.subclass).length > 0 && level >= 8 && level <= 10 - ? { ...tierData.subclass[0], tier: getTier(Number.parseInt(level), true) } - : advancementSecond; - - for (var index in Object.keys(tierData.experiences)) { - for (var experienceKey in tierData.experiences[index]) { - var experience = tierData.experiences[index][experienceKey]; - experienceBonuses[experience] = experienceBonuses[experience] - ? experienceBonuses[experience] + 1 - : 1; - } - } - - evasionBonus += Object.keys(tierData.armorOrEvasionSlot).filter( - x => tierData.armorOrEvasionSlot[x] === 'evasion' - ).length; - armorBonus += Object.keys(tierData.armorOrEvasionSlot).filter( - x => tierData.armorOrEvasionSlot[x] === 'armor' - ).length; - } - } - } - - this.resources.health.max += healthBonus; - this.resources.stress.max += stressBonus; - this.proficiency.value += proficiencyBonus; - this.evasion += evasionBonus; - this.armorMarks = { - max: this.armor ? this.armor.system.marks.max + armorBonus : 0, - value: this.armor ? this.armor.system.marks.value : 0 - }; - - this.experiences = this.experiences.map(x => ({ ...x, value: x.value + (experienceBonuses[x.id] ?? 0) })); - - const subclassFeatures = this.subclassFeatures; - if (advancementFirst) { - if (advancementFirst.multiclass) { - this.multiclassSubclass.system[`${advancementFirst.feature}Feature`].unlocked = true; - this.multiclassSubclass.system[`${advancementFirst.feature}Feature`].tier = advancementFirst.tier; - subclassFeatures.multiclassSubclass[advancementFirst.feature].forEach(x => (x.system.disabled = false)); - } else { - this.subclass.system[`${advancementFirst.feature}Feature`].unlocked = true; - this.subclass.system[`${advancementFirst.feature}Feature`].tier = advancementFirst.tier; - subclassFeatures.subclass[advancementFirst.feature].forEach(x => (x.system.disabled = false)); - } - } - if (advancementSecond) { - if (advancementSecond.multiclass) { - this.multiclassSubclass.system[`${advancementSecond.feature}Feature`].unlocked = true; - this.multiclassSubclass.system[`${advancementSecond.feature}Feature`].tier = advancementSecond.tier; - subclassFeatures.multiclassSubclass[advancementSecond.feature].forEach( - x => (x.system.disabled = false) - ); - } else { - this.subclass.system[`${advancementSecond.feature}Feature`].unlocked = true; - this.subclass.system[`${advancementSecond.feature}Feature`].tier = advancementSecond.tier; - subclassFeatures.subclass[advancementSecond.feature].forEach(x => (x.system.disabled = false)); - } - } - - //General progression - for (var i = 0; i < this.levelData.currentLevel; i++) { - const tier = getTier(i + 1); - if (tier !== 'tier0') { - this.domainData.maxLoadout = Math.min(this.domainData.maxLoadout + 1, 5); - this.domainData.maxCards += 1; - } - } - } - applyEffects() { const effects = this.effects; for (var key in effects) { @@ -509,10 +320,10 @@ export default class DhpPC extends foundry.abstract.TypeDataModel { for (var effect of effectType) { switch (key) { case SYSTEM.EFFECTS.effectTypes.health.id: - this.resources.health.max += effect.value.valueData.value; + this.resources.hitPoints.bonus += effect.value.valueData.value; break; case SYSTEM.EFFECTS.effectTypes.stress.id: - this.resources.stress.max += effect.value.valueData.value; + this.resources.stress.bonus += effect.value.valueData.value; break; case SYSTEM.EFFECTS.effectTypes.damage.id: this.bonuses.damage.push({ @@ -539,10 +350,6 @@ export default class DhpPC extends foundry.abstract.TypeDataModel { return twoHanded ? 'twoHanded' : oneHanded ? 'oneHanded' : null; } - isSameTier(level) { - return this.#getTier(this.levelData.currentLevel) === this.#getTier(level); - } - #getTier(level) { if (level >= 8) return 3; else if (level >= 5) return 2; @@ -550,3 +357,55 @@ export default class DhpPC extends foundry.abstract.TypeDataModel { else return 0; } } + +class DhPCLevelData extends foundry.abstract.DataModel { + static defineSchema() { + return { + level: new fields.SchemaField({ + current: new fields.NumberField({ required: true, integer: true, initial: 1 }), + changed: new fields.NumberField({ required: true, integer: true, initial: 1 }) + }), + levelups: new fields.TypedObjectField( + new fields.SchemaField({ + achievements: new fields.SchemaField( + { + experiences: new fields.TypedObjectField( + new fields.SchemaField({ + name: new fields.StringField({ required: true }), + modifier: new fields.NumberField({ required: true, integer: true }) + }) + ), + domainCards: new fields.ArrayField( + new fields.SchemaField({ + uuid: new fields.StringField({ required: true }), + itemUuid: new fields.StringField({ required: true }) + }) + ), + proficiency: new fields.NumberField({ integer: true }) + }, + { nullable: true, initial: null } + ), + selections: new fields.ArrayField( + new fields.SchemaField({ + tier: new fields.NumberField({ required: true, integer: true }), + level: new fields.NumberField({ required: true, integer: true }), + optionKey: new fields.StringField({ required: true }), + type: new fields.StringField({ required: true, choices: LevelOptionType }), + checkboxNr: new fields.NumberField({ required: true, integer: true }), + value: new fields.NumberField({ integer: true }), + minCost: new fields.NumberField({ integer: true }), + amount: new fields.NumberField({ integer: true }), + data: new fields.ArrayField(new fields.StringField({ required: true })), + secondaryData: new fields.StringField(), + itemUuid: new fields.StringField({ required: true }) + }) + ) + }) + ) + }; + } + + get canLevelUp() { + return this.level.current < this.level.changed; + } +} diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 3d38c485..bc116550 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -6,13 +6,16 @@ import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; export default class DhpActor extends Actor { async _preCreate(data, options, user) { - if ( (await super._preCreate(data, options, user)) === false ) return false; - + if ((await super._preCreate(data, options, user)) === false) return false; + // Configure prototype token settings const prototypeToken = {}; - if ( this.type === "pc" ) Object.assign(prototypeToken, { - sight: { enabled: true }, actorLink: true, disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY - }); + if (this.type === 'pc') + Object.assign(prototypeToken, { + sight: { enabled: true }, + actorLink: true, + disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY + }); this.updateSource({ prototypeToken }); } @@ -21,46 +24,103 @@ export default class DhpActor extends Actor { } async _preUpdate(changed, options, user) { - //Level Down - if ( - changed.system?.levelData?.changedLevel && - this.system.levelData.currentLevel > changed.system.levelData.changedLevel - ) { - changed.system.levelData.currentLevel = changed.system.levelData.changedLevel; - changed.system.levelData.levelups = Object.keys(this.system.levelData.levelups).reduce((acc, x) => { - if (x > changed.system.levelData.currentLevel) { - acc[`-=${x}`] = null; + super._preUpdate(changed, options, user); + } + + async updateLevel(newLevel) { + if (this.type !== 'pc' || newLevel === this.system.levelData.level.changed) return; + + if (newLevel > this.system.levelData.level.current) { + await this.update({ 'system.levelData.level.changed': newLevel }); + } else { + const updatedLevelups = Object.keys(this.system.levelData.levelups).reduce((acc, level) => { + if (Number(level) > newLevel) acc[`-=${level}`] = null; + + return acc; + }, {}); + + const domainCards = Object.keys(this.system.levelData.levelups) + .filter(x => x > newLevel) + .flatMap(levelKey => { + const level = this.system.levelData.levelups[levelKey]; + const achievementCards = level.achievements.domainCards.map(x => x.itemUuid); + const advancementCards = level.selections.filter(x => x.type === 'domainCard').map(x => x.itemUuid); + return [...achievementCards, ...advancementCards]; + }); + + for (var domainCard of domainCards) { + const itemCard = await this.items.find(x => x.uuid === domainCard); + itemCard.delete(); + } + + await this.update({ + system: { + levelData: { + level: { + current: newLevel, + changed: newLevel + }, + levelups: updatedLevelups + } } + }); + } + } - return acc; - }, {}); + async levelUp(levelupData) { + const levelups = {}; + for (var levelKey of Object.keys(levelupData)) { + const level = levelupData[levelKey]; + const achievementDomainCards = []; + for (var card of Object.values(level.achievements.domainCards)) { + const item = await foundry.utils.fromUuid(card.uuid); + const embeddedItem = await this.createEmbeddedDocuments('Item', [item.toObject()]); + card.itemUuid = embeddedItem[0].uuid; + achievementDomainCards.push(card); + } - changed.system.attributes = Object.keys(this.system.attributes).reduce((acc, key) => { - acc[key] = { - levelMarks: this.system.attributes[key].levelMarks.filter( - x => x <= changed.system.levelData.currentLevel - ) - }; + const selections = []; + for (var optionKey of Object.keys(level.choices)) { + const selection = level.choices[optionKey]; + for (var checkboxNr of Object.keys(selection)) { + const checkbox = selection[checkboxNr]; + let itemUuid = null; - return acc; - }, {}); + if (checkbox.type === 'domainCard') { + const item = await foundry.utils.fromUuid(checkbox.data[0]); + const embeddedItem = await this.createEmbeddedDocuments('Item', [item.toObject()]); + itemUuid = embeddedItem[0].uuid; + } - changed.system.experiences = this.system.experiences.filter( - x => x.level <= changed.system.levelData.currentLevel - ); - - if ( - this.system.multiclass && - this.system.multiclass.system.multiclass > changed.system.levelData.changedLevel - ) { - const multiclassFeatures = this.items.filter(x => x.system.multiclass); - for (var feature of multiclassFeatures) { - await feature.delete(); + selections.push({ + ...checkbox, + level: Number(levelKey), + optionKey: optionKey, + checkboxNr: Number(checkboxNr), + itemUuid + }); } } + + levelups[levelKey] = { + achievements: { + ...level.achievements, + domainCards: achievementDomainCards + }, + selections: selections + }; } - super._preUpdate(changed, options, user); + await this.update({ + system: { + levelData: { + level: { + current: this.system.levelData.level.changed + }, + levelups: levelups + } + } + }); } async diceRoll(modifier, shiftKey) { @@ -286,9 +346,9 @@ export default class DhpActor extends Actor { : 0; const update = { - 'system.resources.health.value': Math.min( - this.system.resources.health.value + hpDamage, - this.system.resources.health.max + 'system.resources.hitPoints.value': Math.min( + this.system.resources.hitPoints.value + hpDamage, + this.system.resources.hitPoints.max ) }; @@ -311,9 +371,9 @@ export default class DhpActor extends Actor { switch (type) { case SYSTEM.GENERAL.healingTypes.health.id: update = { - 'system.resources.health.value': Math.min( - this.system.resources.health.value + healing, - this.system.resources.health.max + 'system.resources.hitPoints.value': Math.min( + this.system.resources.hitPoints.value + healing, + this.system.resources.hitPoints.max ) }; break; diff --git a/module/helpers/handlebarsHelper.mjs b/module/helpers/handlebarsHelper.mjs index 8a099175..87d1fb7f 100644 --- a/module/helpers/handlebarsHelper.mjs +++ b/module/helpers/handlebarsHelper.mjs @@ -3,22 +3,19 @@ import { getWidthOfText } from './utils.mjs'; export default class RegisterHandlebarsHelpers { static registerHelpers() { Handlebars.registerHelper({ - looseEq: this.looseEq, times: this.times, join: this.join, add: this.add, subtract: this.subtract, objectSelector: this.objectSelector, includes: this.includes, - simpleEditor: this.simpleEditor, - debug: this.debug + debug: this.debug, + signedNumber: this.signedNumber, + switch: this.switch, + case: this.case }); } - static looseEq(a, b) { - return a == b; - } - static times(nr, block) { var accum = ''; for (var i = 0; i < nr; ++i) accum += block.fn(i); @@ -77,33 +74,25 @@ export default class RegisterHandlebarsHelpers { return new Handlebars.SafeString(html); } - static rangePicker(options) { - let { name, value, min, max, step } = options.hash; - name = name || 'range'; - value = value ?? ''; - if (Number.isNaN(value)) value = ''; - const html = ` - ${value}`; - return new Handlebars.SafeString(html); - } - static includes(list, item) { return list.includes(item); } - static simpleEditor(content, options) { - const { - target, - editable = true, - button, - engine = 'tinymce', - collaborate = false, - class: cssClass - } = options.hash; - const config = { name: target, value: content, button, collaborate, editable, engine }; - const element = foundry.applications.fields.createEditorInput(config); - if (cssClass) element.querySelector('.editor-content').classList.add(cssClass); - return new Handlebars.SafeString(element.outerHTML); + static signedNumber(number) { + return number >= 0 ? `+${number}` : number; + } + + static switch(value, options) { + this.switch_value = value; + this.switch_break = false; + return options.fn(this); + } + + static case(value, options) { + if (value == this.switch_value) { + this.switch_break = true; + return options.fn(this); + } } static debug(a) { diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 753974f5..3fbe89c3 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -1,4 +1,5 @@ import { getDiceSoNicePresets } from '../config/generalConfig.mjs'; +import Tagify from '@yaireo/tagify'; export const loadCompendiumOptions = async compendiums => { const compendiumValues = []; @@ -131,3 +132,93 @@ export const setDiceSoNiceForDualityRoll = (rollResult, advantage, disadvantage) rollResult.dice[2].options.appearance = diceSoNicePresets.disadvantage; } }; + +export const chunkify = (array, chunkSize, mappingFunc) => { + var chunkifiedArray = []; + for (let i = 0; i < array.length; i += chunkSize) { + const chunk = array.slice(i, i + chunkSize); + if (mappingFunc) { + chunkifiedArray.push(mappingFunc(chunk)); + } else { + chunkifiedArray.push(chunk); + } + } + + return chunkifiedArray; +}; + +export const tagifyElement = (element, options, onChange, tagifyOptions = {}) => { + const { maxTags } = tagifyOptions; + const tagifyElement = new Tagify(element, { + tagTextProp: 'name', + enforceWhitelist: true, + whitelist: Object.keys(options).map(key => { + const option = options[key]; + return { + value: key, + name: game.i18n.localize(option.label), + src: option.src + }; + }), + maxTags: maxTags, + dropdown: { + mapValueTo: 'name', + searchKeys: ['name'], + enabled: 0, + maxItems: 20, + closeOnSelect: true, + highlightFirst: false + }, + templates: { + tag(tagData) { + return ` + +
+ ${tagData[this.settings.tagTextProp] || tagData.value} + ${tagData.src ? `` : ''} +
+
`; + } + } + }); + + const onSelect = async event => { + const inputElement = event.detail.tagify.DOM.originalInput; + const selectedOptions = event.detail?.value ? JSON.parse(event.detail.value) : []; + + const unusedDropDownItems = event.detail.tagify.suggestedListItems; + const missingOptions = Object.keys(options).filter(x => !unusedDropDownItems.find(item => item.value === x)); + const removedItem = missingOptions.find(x => !selectedOptions.find(item => item.value === x)); + const addedItem = removedItem + ? null + : selectedOptions.find(x => !missingOptions.find(item => item === x.value)); + + const changedItem = { option: removedItem ?? addedItem.value, removed: Boolean(removedItem) }; + + onChange(selectedOptions, changedItem, inputElement); + }; + tagifyElement.on('change', onSelect); +}; + +export const getDeleteKeys = (property, innerProperty, innerPropertyDefaultValue) => { + return Object.keys(property).reduce((acc, key) => { + if (innerProperty) { + if (innerPropertyDefaultValue !== undefined) { + acc[`${key}`] = { + [innerProperty]: innerPropertyDefaultValue + }; + } else { + acc[`${key}.-=${innerProperty}`] = null; + } + } else { + acc[`-=${key}`] = null; + } + + return acc; + }, {}); +}; diff --git a/styles/daggerheart.css b/styles/daggerheart.css index 73f54a18..78af5ecc 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -7,6 +7,7 @@ /* Drop Shadows */ /* Background */ /* Duality */ +/* Fear */ @import '../node_modules/@yaireo/tagify/dist/tagify.css'; .daggerheart.sheet.class .editor { height: 500px; @@ -2750,11 +2751,222 @@ div.daggerheart.views.multiclass { .item-button .item-icon.checked { opacity: 1; } +.theme-light .daggerheart.levelup .tiers-container .tier-container { + background-image: url('../assets/parchments/dh-parchment-light.png'); +} +.daggerheart.levelup .window-content { + max-height: 960px; + overflow: auto; +} +.daggerheart.levelup div[data-application-part='form'] { + display: flex; + flex-direction: column; + gap: 8px; +} +.daggerheart.levelup section .section-container { + display: flex; + flex-direction: column; + gap: 8px; +} +.daggerheart.levelup .levelup-navigation-container { + display: flex; + align-items: center; + gap: 22px; + height: 36px; +} +.daggerheart.levelup .levelup-navigation-container nav { + flex: 1; +} +.daggerheart.levelup .levelup-navigation-container nav .levelup-tab-container { + display: flex; + align-items: center; + gap: 4px; +} +.daggerheart.levelup .levelup-navigation-container .levelup-navigation-actions { + width: 306px; + display: flex; + justify-content: end; + gap: 16px; + margin-right: 4px; +} +.daggerheart.levelup .levelup-navigation-container .levelup-navigation-actions * { + width: calc(50% - 8px); +} +.daggerheart.levelup .tiers-container { + display: flex; + gap: 16px; +} +.daggerheart.levelup .tiers-container .tier-container { + flex: 1; + display: flex; + flex-direction: column; + gap: 8px; + background-image: url('../assets/parchments/dh-parchment-dark.png'); +} +.daggerheart.levelup .tiers-container .tier-container.inactive { + opacity: 0.4; + pointer-events: none; +} +.daggerheart.levelup .tiers-container .tier-container legend { + margin-left: auto; + margin-right: auto; + font-size: 22px; + font-weight: bold; + padding: 0 12px; +} +.daggerheart.levelup .tiers-container .tier-container .checkbox-group-container { + display: grid; + grid-template-columns: 1fr 3fr; + gap: 4px; +} +.daggerheart.levelup .tiers-container .tier-container .checkbox-group-container .checkboxes-container { + display: flex; + justify-content: end; + gap: 4px; +} +.daggerheart.levelup .tiers-container .tier-container .checkbox-group-container .checkboxes-container .checkbox-grouping-coontainer { + display: flex; + height: min-content; +} +.daggerheart.levelup .tiers-container .tier-container .checkbox-group-container .checkboxes-container .checkbox-grouping-coontainer.multi { + border: 2px solid grey; + padding: 2.4px 2.5px 0; + border-radius: 4px; + gap: 2px; +} +.daggerheart.levelup .tiers-container .tier-container .checkbox-group-container .checkboxes-container .checkbox-grouping-coontainer.multi .selection-checkbox { + margin-left: 0; + margin-right: 0; +} +.daggerheart.levelup .tiers-container .tier-container .checkbox-group-container .checkboxes-container .checkbox-grouping-coontainer .selection-checkbox { + margin: 0; +} +.daggerheart.levelup .tiers-container .tier-container .checkbox-group-container .checkbox-group-label { + font-size: 14px; + font-style: italic; +} +.daggerheart.levelup .levelup-selections-container .achievement-experience-cards { + display: flex; + gap: 8px; +} +.daggerheart.levelup .levelup-selections-container .achievement-experience-cards .achievement-experience-card { + border: 1px solid; + border-radius: 4px; + padding-right: 4px; + font-size: 18px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 4px; +} +.daggerheart.levelup .levelup-selections-container .achievement-experience-cards .achievement-experience-card .achievement-experience-marker { + border: 1px solid; + border-radius: 50%; + height: 18px; + width: 18px; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; +} +.daggerheart.levelup .levelup-selections-container .levelup-card-selection { + display: flex; + flex-wrap: wrap; + gap: 40px; +} +.daggerheart.levelup .levelup-selections-container .levelup-card-selection .card-preview-container { + width: calc(100% * (1 / 5)); +} +.daggerheart.levelup .levelup-selections-container .levelup-card-selection .levelup-domains-selection-container { + display: flex; + flex-direction: column; + gap: 8px; +} +.daggerheart.levelup .levelup-selections-container .levelup-card-selection .levelup-domains-selection-container .levelup-domain-selection-container { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; + position: relative; + cursor: pointer; +} +.daggerheart.levelup .levelup-selections-container .levelup-card-selection .levelup-domains-selection-container .levelup-domain-selection-container.disabled { + pointer-events: none; + opacity: 0.4; +} +.daggerheart.levelup .levelup-selections-container .levelup-card-selection .levelup-domains-selection-container .levelup-domain-selection-container .levelup-domain-label { + position: absolute; + text-align: center; + top: 4px; + background: grey; + padding: 0 12px; + border-radius: 6px; +} +.daggerheart.levelup .levelup-selections-container .levelup-card-selection .levelup-domains-selection-container .levelup-domain-selection-container img { + height: 124px; +} +.daggerheart.levelup .levelup-selections-container .levelup-card-selection .levelup-domains-selection-container .levelup-domain-selection-container .levelup-domain-selected { + position: absolute; + height: 54px; + width: 54px; + border-radius: 50%; + border: 2px solid; + font-size: 48px; + display: flex; + align-items: center; + justify-content: center; + background-image: url(../assets/parchments/dh-parchment-light.png); + color: var(--color-dark-5); + top: calc(50% - 29px); +} +.daggerheart.levelup .levelup-selections-container .levelup-card-selection .levelup-domains-selection-container .levelup-domain-selection-container .levelup-domain-selected i { + position: relative; + right: 2px; +} +.daggerheart.levelup .levelup-selections-container .levelup-selections-title { + display: flex; + align-items: center; + gap: 4px; +} +.daggerheart.levelup .levelup-summary-container .level-achievements-container, +.daggerheart.levelup .levelup-summary-container .level-advancements-container { + display: flex; + flex-direction: column; + gap: 8px; +} +.daggerheart.levelup .levelup-summary-container .level-achievements-container h2, +.daggerheart.levelup .levelup-summary-container .level-advancements-container h2, +.daggerheart.levelup .levelup-summary-container .level-achievements-container h3, +.daggerheart.levelup .levelup-summary-container .level-advancements-container h3, +.daggerheart.levelup .levelup-summary-container .level-achievements-container h4, +.daggerheart.levelup .levelup-summary-container .level-advancements-container h4, +.daggerheart.levelup .levelup-summary-container .level-achievements-container h5, +.daggerheart.levelup .levelup-summary-container .level-advancements-container h5 { + margin: 0; + color: var(--color-text-secondary); +} +.daggerheart.levelup .levelup-summary-container .increase-container { + display: flex; + align-items: center; + gap: 4px; + font-size: 20px; +} +.daggerheart.levelup .levelup-summary-container .summary-selection-container { + display: flex; + gap: 8px; +} +.daggerheart.levelup .levelup-summary-container .summary-selection-container .summary-selection { + border: 2px solid; + border-radius: 6px; + padding: 0 4px; + font-size: 18px; +} +.daggerheart.levelup .levelup-footer { + display: flex; +} :root { - --primary-color-fear: rgba(9, 71, 179, 0.75); - --secondary-color-fear: rgba(9, 71, 179, 0.75); --shadow-text-stroke: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; - --fear-animation: background 0.3s ease, box-shadow .3s ease, border-color .3s ease, opacity .3s ease; + --fear-animation: background 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease, opacity 0.3s ease; } #resources { min-height: calc(var(--header-height) + 4rem); @@ -2785,7 +2997,7 @@ div.daggerheart.views.multiclass { justify-content: center; align-items: center; width: 3rem; - background-color: var(--primary-color-fear); + background-color: var(rgba(9, 71, 179, 0.75)); -webkit-box-shadow: 0px 0px 5px 1px rgba(0, 0, 0, 0.75); box-shadow: 0px 0px 5px 1px rgba(0, 0, 0, 0.75); color: #d3d3d3; @@ -2850,7 +3062,7 @@ div.daggerheart.views.multiclass { #resources .window-content #resource-fear.isGM i:hover { font-size: var(--font-size-20); } -#resources button[data-action="close"] { +#resources button[data-action='close'] { display: none; } #resources:not(:hover):not(.minimized) { @@ -3280,6 +3492,93 @@ div.daggerheart.views.multiclass { .system-daggerheart.theme-light .tagify__dropdown .tagify__dropdown__item--active { color: #efe6d8; } +.theme-light .application .component.dh-style.card-preview-container { + background-image: url('../assets/parchments/dh-parchment-light.png'); +} +.theme-light .application .component.dh-style.card-preview-container .preview-text-container { + background-image: url(../assets/parchments/dh-parchment-dark.png); +} +.theme-light .application .component.dh-style.card-preview-container .preview-selected-icon-container { + background-image: url(../assets/parchments/dh-parchment-dark.png); + color: var(--color-light-5); +} +.application .component.dh-style.card-preview-container { + position: relative; + border-radius: 6px; + border: 2px solid var(--color-tabs-border); + display: flex; + flex-direction: column; + aspect-ratio: 0.75; + background-image: url('../assets/parchments/dh-parchment-dark.png'); +} +.application .component.dh-style.card-preview-container.selectable { + cursor: pointer; +} +.application .component.dh-style.card-preview-container.disabled { + pointer-events: none; + opacity: 0.4; +} +.application .component.dh-style.card-preview-container .preview-image-outer-container { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} +.application .component.dh-style.card-preview-container .preview-image-container { + flex: 1; + border-radius: 4px 4px 0 0; +} +.application .component.dh-style.card-preview-container .preview-text-container { + flex: 1; + border-radius: 0 0 4px 4px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + text-align: center; + color: var(--color-text-selection-bg); + background-image: url(../assets/parchments/dh-parchment-light.png); +} +.application .component.dh-style.card-preview-container .preview-empty-container { + pointer-events: none; + position: relative; + display: flex; + align-items: center; + justify-content: center; + flex: 1; +} +.application .component.dh-style.card-preview-container .preview-empty-container .preview-empty-inner-container { + width: 100%; + display: flex; + justify-content: center; +} +.application .component.dh-style.card-preview-container .preview-empty-container .preview-empty-inner-container .preview-add-icon { + font-size: 48px; +} +.application .component.dh-style.card-preview-container .preview-empty-container .preview-empty-inner-container .preview-empty-subtext { + position: absolute; + top: 10%; + font-size: 18px; + font-variant: small-caps; + text-align: center; +} +.application .component.dh-style.card-preview-container .preview-selected-icon-container { + position: absolute; + height: 54px; + width: 54px; + border-radius: 50%; + border: 2px solid; + font-size: 48px; + display: flex; + align-items: center; + justify-content: center; + background-image: url(../assets/parchments/dh-parchment-light.png); + color: var(--color-dark-5); +} +.application .component.dh-style.card-preview-container .preview-selected-icon-container i { + position: relative; + right: 2px; +} .sheet.daggerheart.dh-style .tab-navigation { margin: 5px 0; height: 40px; diff --git a/styles/daggerheart.less b/styles/daggerheart.less index 2ae18980..6869e316 100755 --- a/styles/daggerheart.less +++ b/styles/daggerheart.less @@ -8,6 +8,7 @@ @import './application.less'; @import './sheets/sheets.less'; @import './dialog.less'; +@import './levelup.less'; @import '../node_modules/@yaireo/tagify/dist/tagify.css'; @import './resources.less'; diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index a4bb7c99..077d2226 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -288,3 +288,105 @@ } } } + +.theme-light .application .component.dh-style.card-preview-container { + background-image: url('../assets/parchments/dh-parchment-light.png'); + + .preview-text-container { + background-image: url(../assets/parchments/dh-parchment-dark.png); + } + + .preview-selected-icon-container { + background-image: url(../assets/parchments/dh-parchment-dark.png); + color: var(--color-light-5); + } +} + +.application .component.dh-style.card-preview-container { + position: relative; + border-radius: 6px; + border: 2px solid var(--color-tabs-border); + display: flex; + flex-direction: column; + aspect-ratio: 0.75; + background-image: url('../assets/parchments/dh-parchment-dark.png'); + + &.selectable { + cursor: pointer; + } + + &.disabled { + pointer-events: none; + opacity: 0.4; + } + + .preview-image-outer-container { + position: relative; + display: flex; + align-items: center; + justify-content: center; + } + + .preview-image-container { + flex: 1; + border-radius: 4px 4px 0 0; + } + + .preview-text-container { + flex: 1; + border-radius: 0 0 4px 4px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + text-align: center; + color: var(--color-text-selection-bg); + background-image: url(../assets/parchments/dh-parchment-light.png); + } + + .preview-empty-container { + pointer-events: none; + position: relative; + display: flex; + align-items: center; + justify-content: center; + flex: 1; + + .preview-empty-inner-container { + width: 100%; + display: flex; + justify-content: center; + + .preview-add-icon { + font-size: 48px; + } + + .preview-empty-subtext { + position: absolute; + top: 10%; + font-size: 18px; + font-variant: small-caps; + text-align: center; + } + } + } + + .preview-selected-icon-container { + position: absolute; + height: 54px; + width: 54px; + border-radius: 50%; + border: 2px solid; + font-size: 48px; + display: flex; + align-items: center; + justify-content: center; + background-image: url(../assets/parchments/dh-parchment-light.png); + color: var(--color-dark-5); + + i { + position: relative; + right: 2px; + } + } +} diff --git a/styles/less/global/feature-section.less b/styles/less/global/feature-section.less index 50ffaefa..a294926f 100644 --- a/styles/less/global/feature-section.less +++ b/styles/less/global/feature-section.less @@ -1,51 +1,51 @@ -@import '../utils/colors.less'; -@import '../utils/fonts.less'; - -.sheet.daggerheart.dh-style.item { - .tab.features { - padding: 0 10px; - max-height: 265px; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; - .feature-list { - display: flex; - flex-direction: column; - list-style: none; - padding: 0; - margin: 0; - width: 100%; - .feature-item { - margin-bottom: 10px; - &:last-child { - margin-bottom: 0px; - } - .feature-line { - display: grid; - align-items: center; - grid-template-columns: 1fr 4fr 1fr; - h4 { - font-family: @font-body; - font-weight: lighter; - color: light-dark(@dark, @beige); - } - .image { - height: 40px; - width: 40px; - object-fit: cover; - border-radius: 6px; - border: none; - } - .controls { - display: flex; - justify-content: center; - gap: 10px; - a { - text-shadow: none; - } - } - } - } - } - } -} +@import '../utils/colors.less'; +@import '../utils/fonts.less'; + +.sheet.daggerheart.dh-style.item { + .tab.features { + padding: 0 10px; + max-height: 265px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: light-dark(@dark-blue, @golden) transparent; + .feature-list { + display: flex; + flex-direction: column; + list-style: none; + padding: 0; + margin: 0; + width: 100%; + .feature-item { + margin-bottom: 10px; + &:last-child { + margin-bottom: 0px; + } + .feature-line { + display: grid; + align-items: center; + grid-template-columns: 1fr 4fr 1fr; + h4 { + font-family: @font-body; + font-weight: lighter; + color: light-dark(@dark, @beige); + } + .image { + height: 40px; + width: 40px; + object-fit: cover; + border-radius: 6px; + border: none; + } + .controls { + display: flex; + justify-content: center; + gap: 10px; + a { + text-shadow: none; + } + } + } + } + } + } +} diff --git a/styles/less/global/item-header.less b/styles/less/global/item-header.less index 06919b77..f517ad5d 100755 --- a/styles/less/global/item-header.less +++ b/styles/less/global/item-header.less @@ -1,152 +1,152 @@ -@import '../utils/colors.less'; - -.application.sheet.daggerheart.dh-style { - .item-sheet-header { - display: flex; - - .profile { - height: 150px; - width: 150px; - object-fit: cover; - border-right: 1px solid light-dark(@dark-blue, @golden); - border-bottom: 1px solid light-dark(@dark-blue, @golden); - box-sizing: border-box; - cursor: pointer; - } - - .item-info { - display: flex; - flex-direction: column; - align-items: center; - gap: 5px; - margin-top: 36px; - text-align: center; - width: 80%; - - .item-name input[type='text'] { - font-size: 32px; - height: 42px; - text-align: center; - width: 90%; - transition: all 0.3s ease; - outline: 2px solid transparent; - border: 1px solid transparent; - - &:hover[type='text'], - &:focus[type='text'] { - box-shadow: none; - outline: 2px solid light-dark(@dark-blue, @golden); - } - } - - .item-description { - display: flex; - flex-direction: column; - gap: 10px; - } - - h3 { - font-size: 1rem; - } - } - } - - .item-card-header { - display: flex; - flex-direction: column; - justify-content: start; - text-align: center; - - .profile { - height: 300px; - width: 100%; - object-fit: cover; - mask-image: linear-gradient(0deg, transparent 0%, black 10%); - cursor: pointer; - } - - .item-icons-list { - position: absolute; - display: flex; - align-items: center; - justify-content: center; - top: 50px; - right: 10px; - - .item-icon { - display: flex; - align-items: center; - justify-content: end; - text-align: center; - padding-right: 8px; - max-width: 50px; - height: 50px; - font-size: 1.2rem; - background: light-dark(@light-black, @semi-transparent-dark-blue); - border: 4px double light-dark(@beige, @golden); - color: light-dark(@beige, @golden); - border-radius: 999px; - transition: all 0.3s ease; - - .recall-label { - font-size: 14px; - opacity: 0; - margin-right: 0.3rem; - transition: all 0.3s ease; - } - - i { - font-size: 0.8rem; - } - - &:hover { - max-width: 300px; - padding: 0 10px; - border-radius: 60px; - - .recall-label { - opacity: 1; - } - } - } - } - - .item-info { - display: flex; - flex-direction: column; - align-items: center; - position: relative; - top: -25px; - gap: 5px; - margin-bottom: -20px; - - .item-name { - input[type='text'] { - font-size: 32px; - height: 42px; - text-align: center; - width: 90%; - transition: all 0.3s ease; - outline: 2px solid transparent; - border: 1px solid transparent; - - &:hover[type='text'], - &:focus[type='text'] { - box-shadow: none; - outline: 2px solid light-dark(@dark-blue, @golden); - } - } - } - - .item-description { - display: flex; - flex-direction: column; - gap: 10px; - } - - h3 { - font-size: 1rem; - } - } - } -} +@import '../utils/colors.less'; + +.application.sheet.daggerheart.dh-style { + .item-sheet-header { + display: flex; + + .profile { + height: 150px; + width: 150px; + object-fit: cover; + border-right: 1px solid light-dark(@dark-blue, @golden); + border-bottom: 1px solid light-dark(@dark-blue, @golden); + box-sizing: border-box; + cursor: pointer; + } + + .item-info { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + margin-top: 36px; + text-align: center; + width: 80%; + + .item-name input[type='text'] { + font-size: 32px; + height: 42px; + text-align: center; + width: 90%; + transition: all 0.3s ease; + outline: 2px solid transparent; + border: 1px solid transparent; + + &:hover[type='text'], + &:focus[type='text'] { + box-shadow: none; + outline: 2px solid light-dark(@dark-blue, @golden); + } + } + + .item-description { + display: flex; + flex-direction: column; + gap: 10px; + } + + h3 { + font-size: 1rem; + } + } + } + + .item-card-header { + display: flex; + flex-direction: column; + justify-content: start; + text-align: center; + + .profile { + height: 300px; + width: 100%; + object-fit: cover; + mask-image: linear-gradient(0deg, transparent 0%, black 10%); + cursor: pointer; + } + + .item-icons-list { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + top: 50px; + right: 10px; + + .item-icon { + display: flex; + align-items: center; + justify-content: end; + text-align: center; + padding-right: 8px; + max-width: 50px; + height: 50px; + font-size: 1.2rem; + background: light-dark(@light-black, @semi-transparent-dark-blue); + border: 4px double light-dark(@beige, @golden); + color: light-dark(@beige, @golden); + border-radius: 999px; + transition: all 0.3s ease; + + .recall-label { + font-size: 14px; + opacity: 0; + margin-right: 0.3rem; + transition: all 0.3s ease; + } + + i { + font-size: 0.8rem; + } + + &:hover { + max-width: 300px; + padding: 0 10px; + border-radius: 60px; + + .recall-label { + opacity: 1; + } + } + } + } + + .item-info { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + top: -25px; + gap: 5px; + margin-bottom: -20px; + + .item-name { + input[type='text'] { + font-size: 32px; + height: 42px; + text-align: center; + width: 90%; + transition: all 0.3s ease; + outline: 2px solid transparent; + border: 1px solid transparent; + + &:hover[type='text'], + &:focus[type='text'] { + box-shadow: none; + outline: 2px solid light-dark(@dark-blue, @golden); + } + } + } + + .item-description { + display: flex; + flex-direction: column; + gap: 10px; + } + + h3 { + font-size: 1rem; + } + } + } +} diff --git a/styles/less/global/sheet.less b/styles/less/global/sheet.less index e557ae7e..975841db 100755 --- a/styles/less/global/sheet.less +++ b/styles/less/global/sheet.less @@ -1,80 +1,80 @@ -@import '../utils/colors.less'; -@import '../utils/fonts.less'; - -.application.sheet.dh-style .window-header { - background: transparent; - border-bottom: none; - justify-content: end; - - h1 { - color: light-dark(@dark-blue, @beige); - font-family: @font-body; - } - - button { - background: light-dark(transparent, @deep-black); - color: light-dark(@dark-blue, @beige); - border: 1px solid light-dark(@dark-blue, transparent); - padding: 0; - - &:hover { - border: 1px solid light-dark(@dark-blue, @golden); - color: light-dark(@dark-blue, @golden); - } - } -} - -.application.sheet.dh-style:not(.minimized) { - .window-title, - .window-icon { - display: none; - opacity: 0; - transition: opacity 0.3s ease; - } -} - -.application.sheet.dh-style.minimized { - .window-content { - display: none; - opacity: 0; - transition: opacity 0.1s ease; - } -} - -.application.sheet.dh-style:not(.minimized) { - .window-content { - opacity: 1; - transition: opacity 0.3s ease; - } -} - -.application.sheet.dh-style .window-content { - overflow: initial; - backdrop-filter: none; - padding: 0; -} - -.theme-dark { - .application.sheet.dh-style { - backdrop-filter: blur(4px); - } -} - -.theme-light { - .application.sheet.dh-style { - background-image: url('../assets/parchments/dh-parchment-light.png'); - background-repeat: no-repeat; - background-position: center; - } -} - -.application.sheet.daggerheart.dh-style { - .window-content { - position: relative; - top: -36px; - - .tab { - padding: 0 10px; - } - } -} +@import '../utils/colors.less'; +@import '../utils/fonts.less'; + +.application.sheet.dh-style .window-header { + background: transparent; + border-bottom: none; + justify-content: end; + + h1 { + color: light-dark(@dark-blue, @beige); + font-family: @font-body; + } + + button { + background: light-dark(transparent, @deep-black); + color: light-dark(@dark-blue, @beige); + border: 1px solid light-dark(@dark-blue, transparent); + padding: 0; + + &:hover { + border: 1px solid light-dark(@dark-blue, @golden); + color: light-dark(@dark-blue, @golden); + } + } +} + +.application.sheet.dh-style:not(.minimized) { + .window-title, + .window-icon { + display: none; + opacity: 0; + transition: opacity 0.3s ease; + } +} + +.application.sheet.dh-style.minimized { + .window-content { + display: none; + opacity: 0; + transition: opacity 0.1s ease; + } +} + +.application.sheet.dh-style:not(.minimized) { + .window-content { + opacity: 1; + transition: opacity 0.3s ease; + } +} + +.application.sheet.dh-style .window-content { + overflow: initial; + backdrop-filter: none; + padding: 0; +} + +.theme-dark { + .application.sheet.dh-style { + backdrop-filter: blur(4px); + } +} + +.theme-light { + .application.sheet.dh-style { + background-image: url('../assets/parchments/dh-parchment-light.png'); + background-repeat: no-repeat; + background-position: center; + } +} + +.application.sheet.daggerheart.dh-style { + .window-content { + position: relative; + top: -36px; + + .tab { + padding: 0 10px; + } + } +} diff --git a/styles/less/global/tab-actions.less b/styles/less/global/tab-actions.less index fb4f8850..7279688d 100644 --- a/styles/less/global/tab-actions.less +++ b/styles/less/global/tab-actions.less @@ -1,46 +1,46 @@ -@import '../utils/colors.less'; -@import '../utils/fonts.less'; - -.sheet.daggerheart.dh-style { - .tab.actions { - .actions-list { - display: flex; - flex-direction: column; - list-style: none; - padding: 0; - margin: 0; - width: 100%; - gap: 5px; - - .action-item { - display: grid; - align-items: center; - grid-template-columns: 1fr 4fr 1fr; - cursor: pointer; - - h4 { - font-family: @font-body; - font-weight: lighter; - color: @beige; - } - - .image { - height: 40px; - width: 40px; - object-fit: cover; - border-radius: 6px; - border: none; - } - - .controls { - display: flex; - justify-content: center; - gap: 10px; - a { - text-shadow: none; - } - } - } - } - } -} +@import '../utils/colors.less'; +@import '../utils/fonts.less'; + +.sheet.daggerheart.dh-style { + .tab.actions { + .actions-list { + display: flex; + flex-direction: column; + list-style: none; + padding: 0; + margin: 0; + width: 100%; + gap: 5px; + + .action-item { + display: grid; + align-items: center; + grid-template-columns: 1fr 4fr 1fr; + cursor: pointer; + + h4 { + font-family: @font-body; + font-weight: lighter; + color: @beige; + } + + .image { + height: 40px; + width: 40px; + object-fit: cover; + border-radius: 6px; + border: none; + } + + .controls { + display: flex; + justify-content: center; + gap: 10px; + a { + text-shadow: none; + } + } + } + } + } +} diff --git a/styles/less/items/class.css b/styles/less/items/class.css index 36c3afda..19f17576 100644 --- a/styles/less/items/class.css +++ b/styles/less/items/class.css @@ -1,131 +1,132 @@ @font-face { - font-family: 'Cinzel'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url(https://fonts.gstatic.com/s/cinzel/v23/8vIU7ww63mVu7gtR-kwKxNvkNOjw-tbnTYo.ttf) format('truetype'); + font-family: 'Cinzel'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(https://fonts.gstatic.com/s/cinzel/v23/8vIU7ww63mVu7gtR-kwKxNvkNOjw-tbnTYo.ttf) format('truetype'); } @font-face { - font-family: 'Cinzel'; - font-style: normal; - font-weight: 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/cinzel/v23/8vIU7ww63mVu7gtR-kwKxNvkNOjw-jHgTYo.ttf) format('truetype'); + font-family: 'Cinzel'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/cinzel/v23/8vIU7ww63mVu7gtR-kwKxNvkNOjw-jHgTYo.ttf) format('truetype'); } @font-face { - font-family: 'Cinzel Decorative'; - font-style: normal; - font-weight: 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/cinzeldecorative/v17/daaHSScvJGqLYhG8nNt8KPPswUAPniZoaelD.ttf) format('truetype'); + font-family: 'Cinzel Decorative'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/cinzeldecorative/v17/daaHSScvJGqLYhG8nNt8KPPswUAPniZoaelD.ttf) + format('truetype'); } @font-face { - font-family: 'Montserrat'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url(https://fonts.gstatic.com/s/montserrat/v30/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Ew-.ttf) format('truetype'); + font-family: 'Montserrat'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(https://fonts.gstatic.com/s/montserrat/v30/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Ew-.ttf) format('truetype'); } @font-face { - font-family: 'Montserrat'; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url(https://fonts.gstatic.com/s/montserrat/v30/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCu170w-.ttf) format('truetype'); + font-family: 'Montserrat'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(https://fonts.gstatic.com/s/montserrat/v30/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCu170w-.ttf) format('truetype'); } .application.sheet.daggerheart.dh-style h1 { - font-family: 'Cinzel Decorative', serif; - margin: 0; - border: none; - font-weight: normal; + font-family: 'Cinzel Decorative', serif; + margin: 0; + border: none; + font-weight: normal; } .application.sheet.daggerheart.dh-style h2, .application.sheet.daggerheart.dh-style h3 { - font-family: 'Cinzel', serif; - margin: 0; - border: none; - font-weight: normal; + font-family: 'Cinzel', serif; + margin: 0; + border: none; + font-weight: normal; } .application.sheet.daggerheart.dh-style h4 { - font-family: 'Montserrat', sans-serif; - font-size: 14px; - border: none; - font-weight: 700; - margin: 0; - text-shadow: none; - color: #f3c267; - font-weight: normal; + font-family: 'Montserrat', sans-serif; + font-size: 14px; + border: none; + font-weight: 700; + margin: 0; + text-shadow: none; + color: #f3c267; + font-weight: normal; } .application.sheet.daggerheart.dh-style h5 { - font-size: 14px; - color: #f3c267; - margin: 0; - font-weight: normal; + font-size: 14px; + color: #f3c267; + margin: 0; + font-weight: normal; } .application.sheet.daggerheart.dh-style p, .application.sheet.daggerheart.dh-style span { - font-family: 'Montserrat', sans-serif; + font-family: 'Montserrat', sans-serif; } .application.sheet.daggerheart.dh-style small { - font-family: 'Montserrat', sans-serif; - opacity: 0.8; + font-family: 'Montserrat', sans-serif; + opacity: 0.8; } .application.sheet.daggerheart.dh-style.class .tagify { - background: light-dark(transparent, transparent); - border: 1px solid light-dark(#222, #efe6d8); - height: 34px; - border-radius: 3px; - margin-right: 1px; + background: light-dark(transparent, transparent); + border: 1px solid light-dark(#222, #efe6d8); + height: 34px; + border-radius: 3px; + margin-right: 1px; } .application.sheet.daggerheart.dh-style.class .tagify tag div { - display: flex; - justify-content: space-between; - align-items: center; - height: 22px; + display: flex; + justify-content: space-between; + align-items: center; + height: 22px; } .application.sheet.daggerheart.dh-style.class .tagify tag div span { - font-weight: 400; + font-weight: 400; } .application.sheet.daggerheart.dh-style.class .tagify tag div img { - margin-left: 8px; - height: 20px; - width: 20px; + margin-left: 8px; + height: 20px; + width: 20px; } .application.sheet.daggerheart.dh-style.class .tab.settings .fieldsets-section { - display: grid; - gap: 10px; - grid-template-columns: 1fr 1.5fr 1.5fr; + display: grid; + gap: 10px; + grid-template-columns: 1fr 1.5fr 1.5fr; } .application.sheet.daggerheart.dh-style.class .tab.settings .list-items { - margin-bottom: 10px; - width: 100%; + margin-bottom: 10px; + width: 100%; } .application.sheet.daggerheart.dh-style.class .tab.settings .list-items:last-child { - margin-bottom: 0px; + margin-bottom: 0px; } .application.sheet.daggerheart.dh-style.class .tab.settings .list-items .item-line { - display: grid; - align-items: center; - gap: 10px; - grid-template-columns: 1fr 3fr 1fr; + display: grid; + align-items: center; + gap: 10px; + grid-template-columns: 1fr 3fr 1fr; } .application.sheet.daggerheart.dh-style.class .tab.settings .list-items .item-line h4 { - font-family: 'Montserrat', sans-serif; - font-weight: lighter; - color: light-dark(#222, #efe6d8); + font-family: 'Montserrat', sans-serif; + font-weight: lighter; + color: light-dark(#222, #efe6d8); } .application.sheet.daggerheart.dh-style.class .tab.settings .list-items .item-line .image { - height: 40px; - width: 40px; - object-fit: cover; - border-radius: 6px; - border: none; + height: 40px; + width: 40px; + object-fit: cover; + border-radius: 6px; + border: none; } .application.sheet.daggerheart.dh-style.class .tab.settings .list-items .item-line .controls { - display: flex; - justify-content: center; - gap: 10px; + display: flex; + justify-content: center; + gap: 10px; } .application.sheet.daggerheart.dh-style.class .tab.settings .list-items .item-line .controls a { - text-shadow: none; + text-shadow: none; } diff --git a/styles/less/items/domainCard.less b/styles/less/items/domainCard.less index 2cb1e361..93c00558 100644 --- a/styles/less/items/domainCard.less +++ b/styles/less/items/domainCard.less @@ -1,12 +1,11 @@ -@import '../utils/colors.less'; -@import '../utils/fonts.less'; - -.application.sheet.daggerheart.dh-style.domain-card { - - section.tab { - height: 400px; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; - } -} +@import '../utils/colors.less'; +@import '../utils/fonts.less'; + +.application.sheet.daggerheart.dh-style.domain-card { + section.tab { + height: 400px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: light-dark(@dark-blue, @golden) transparent; + } +} diff --git a/styles/less/items/feature.less b/styles/less/items/feature.less index f4c117eb..c27cfb65 100755 --- a/styles/less/items/feature.less +++ b/styles/less/items/feature.less @@ -1,20 +1,20 @@ -@import '../utils/colors.less'; -@import '../utils/fonts.less'; - -.application.sheet.daggerheart.dh-style.feature { - .item-sheet-header { - display: flex; - - .profile { - height: 130px; - width: 130px; - } - } - - section.tab { - height: 400px; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; - } -} +@import '../utils/colors.less'; +@import '../utils/fonts.less'; + +.application.sheet.daggerheart.dh-style.feature { + .item-sheet-header { + display: flex; + + .profile { + height: 130px; + width: 130px; + } + } + + section.tab { + height: 400px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: light-dark(@dark-blue, @golden) transparent; + } +} diff --git a/styles/levelup.less b/styles/levelup.less new file mode 100644 index 00000000..3363d0a0 --- /dev/null +++ b/styles/levelup.less @@ -0,0 +1,261 @@ +.theme-light { + .daggerheart.levelup { + .tiers-container { + .tier-container { + background-image: url('../assets/parchments/dh-parchment-light.png'); + } + } + } +} + +.daggerheart.levelup { + .window-content { + max-height: 960px; + overflow: auto; + } + + div[data-application-part='form'] { + display: flex; + flex-direction: column; + gap: 8px; + } + + section { + .section-container { + display: flex; + flex-direction: column; + gap: 8px; + } + } + + .levelup-navigation-container { + display: flex; + align-items: center; + gap: 22px; + height: 36px; + + nav { + flex: 1; + + .levelup-tab-container { + display: flex; + align-items: center; + gap: 4px; + } + } + + .levelup-navigation-actions { + width: 306px; + display: flex; + justify-content: end; + gap: 16px; + margin-right: 4px; + + * { + width: calc(50% - 8px); + } + } + } + + .tiers-container { + display: flex; + gap: 16px; + + .tier-container { + flex: 1; + display: flex; + flex-direction: column; + gap: 8px; + background-image: url('../assets/parchments/dh-parchment-dark.png'); + + &.inactive { + opacity: 0.4; + pointer-events: none; + } + + legend { + margin-left: auto; + margin-right: auto; + font-size: 22px; + font-weight: bold; + padding: 0 12px; + } + + .checkbox-group-container { + display: grid; + grid-template-columns: 1fr 3fr; + gap: 4px; + + .checkboxes-container { + display: flex; + justify-content: end; + gap: 4px; + + .checkbox-grouping-coontainer { + display: flex; + height: min-content; + + &.multi { + border: 2px solid grey; + padding: 2.4px 2.5px 0; + border-radius: 4px; + gap: 2px; + + .selection-checkbox { + margin-left: 0; + margin-right: 0; + } + } + + .selection-checkbox { + margin: 0; + } + } + } + + .checkbox-group-label { + font-size: 14px; + font-style: italic; + } + } + } + } + + .levelup-selections-container { + .achievement-experience-cards { + display: flex; + gap: 8px; + + .achievement-experience-card { + border: 1px solid; + border-radius: 4px; + padding-right: 4px; + font-size: 18px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 4px; + + .achievement-experience-marker { + border: 1px solid; + border-radius: 50%; + height: 18px; + width: 18px; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + } + } + } + + .levelup-card-selection { + display: flex; + flex-wrap: wrap; + gap: 40px; + + .card-preview-container { + width: calc(100% * (1 / 5)); + } + + .levelup-domains-selection-container { + display: flex; + flex-direction: column; + gap: 8px; + + .levelup-domain-selection-container { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; + position: relative; + cursor: pointer; + + &.disabled { + pointer-events: none; + opacity: 0.4; + } + + .levelup-domain-label { + position: absolute; + text-align: center; + top: 4px; + background: grey; + padding: 0 12px; + border-radius: 6px; + } + + img { + height: 124px; + } + + .levelup-domain-selected { + position: absolute; + height: 54px; + width: 54px; + border-radius: 50%; + border: 2px solid; + font-size: 48px; + display: flex; + align-items: center; + justify-content: center; + background-image: url(../assets/parchments/dh-parchment-light.png); + color: var(--color-dark-5); + top: calc(50% - 29px); + + i { + position: relative; + right: 2px; + } + } + } + } + } + + .levelup-selections-title { + display: flex; + align-items: center; + gap: 4px; + } + } + + .levelup-summary-container { + .level-achievements-container, + .level-advancements-container { + display: flex; + flex-direction: column; + gap: 8px; + + h2, + h3, + h4, + h5 { + margin: 0; + color: var(--color-text-secondary); + } + } + + .increase-container { + display: flex; + align-items: center; + gap: 4px; + font-size: 20px; + } + + .summary-selection-container { + display: flex; + gap: 8px; + + .summary-selection { + border: 2px solid; + border-radius: 6px; + padding: 0 4px; + font-size: 18px; + } + } + } + + .levelup-footer { + display: flex; + } +} diff --git a/styles/resources.less b/styles/resources.less index ce0f9560..a1679dab 100644 --- a/styles/resources.less +++ b/styles/resources.less @@ -1,8 +1,6 @@ :root { - --primary-color-fear: rgba(9, 71, 179, .75); - --secondary-color-fear: rgba(9, 71, 179, .75); --shadow-text-stroke: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; - --fear-animation : background .3s ease, box-shadow .3s ease, border-color .3s ease, opacity .3s ease; + --fear-animation: background 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease, opacity 0.3s ease; } #resources { @@ -10,37 +8,39 @@ min-width: 4rem; color: #d3d3d3; transition: var(--fear-animation); - header, .controls, .window-resize-handle { + header, + .controls, + .window-resize-handle { transition: var(--fear-animation); } .window-content { - padding: .5rem; + padding: 0.5rem; #resource-fear { display: flex; flex-direction: row; - gap: .5rem 0.25rem; + gap: 0.5rem 0.25rem; flex-wrap: wrap; i { font-size: var(--font-size-18); - // flex: 1 1 calc(25% - 0.25rem); - border: 1px solid rgba(0,0,0,.5); + border: 1px solid rgba(0, 0, 0, 0.5); border-radius: 50%; aspect-ratio: 1; display: flex; justify-content: center; align-items: center; width: 3rem; - background-color: var(--primary-color-fear); - -webkit-box-shadow: 0px 0px 5px 1px rgba(0,0,0,.75); - box-shadow: 0px 0px 5px 1px rgba(0,0,0,.75); + background-color: var(@primary-color-fear); + -webkit-box-shadow: 0px 0px 5px 1px rgba(0, 0, 0, 0.75); + box-shadow: 0px 0px 5px 1px rgba(0, 0, 0, 0.75); color: #d3d3d3; flex-grow: 0; - &.inactive{ + &.inactive { filter: grayscale(1) !important; - opacity: .5; + opacity: 0.5; } } - .controls, .resource-bar { + .controls, + .resource-bar { border: 2px solid rgb(153 122 79); background-color: rgb(24 22 46); } @@ -58,7 +58,7 @@ font-size: 1.5rem; } &.disabled { - opacity: .5; + opacity: 0.5; } } .resource-bar { @@ -68,7 +68,7 @@ font-size: var(--font-size-20); overflow: hidden; position: relative; - padding: .25rem .5rem; + padding: 0.25rem 0.5rem; flex: 1; text-shadow: var(--shadow-text-stroke); &:before { @@ -79,7 +79,7 @@ left: 0; width: var(--fear-percent); max-width: 100%; - background: linear-gradient(90deg,rgba(2, 0, 38, 1) 0%, rgba(199, 1, 252, 1) 100%); + background: linear-gradient(90deg, rgba(2, 0, 38, 1) 0%, rgba(199, 1, 252, 1) 100%); z-index: 0; border-radius: 4px; } @@ -88,7 +88,6 @@ z-index: 1; } &.fear { - } } &.isGM { @@ -101,18 +100,20 @@ } } } - button[data-action="close"] { + button[data-action='close'] { display: none; } &:not(:hover):not(.minimized) { background: transparent; box-shadow: unset; border-color: transparent; - header, .controls, .window-resize-handle { + header, + .controls, + .window-resize-handle { opacity: 0; } } &:has(.fear-bar) { min-width: 200px; } -} \ No newline at end of file +} diff --git a/styles/variables/colors.less b/styles/variables/colors.less index d189af51..0468c1e6 100644 --- a/styles/variables/colors.less +++ b/styles/variables/colors.less @@ -32,3 +32,6 @@ @criticalAccent: #66159c; @criticalBackgroundStart: rgba(37, 8, 37, 0.6); @criticalBackgroundEnd: rgba(128, 0, 128, 0.6); + +/* Fear */ +@primary-color-fear: rgba(9, 71, 179, 0.75); diff --git a/templates/components/card-preview.hbs b/templates/components/card-preview.hbs new file mode 100644 index 00000000..dc132b94 --- /dev/null +++ b/templates/components/card-preview.hbs @@ -0,0 +1,13 @@ +
+ {{#if this.img}} + +
{{this.name}}
+ {{else}} +
+
+ +
{{this.emptySubtext}}
+
+
+ {{/if}} +
\ No newline at end of file diff --git a/templates/sheets/adversary.hbs b/templates/sheets/adversary.hbs index a9c5619f..67230fdf 100644 --- a/templates/sheets/adversary.hbs +++ b/templates/sheets/adversary.hbs @@ -103,7 +103,7 @@
- +
diff --git a/templates/sheets/parts/attributes.hbs b/templates/sheets/parts/attributes.hbs index 46a15317..2a89fe1f 100644 --- a/templates/sheets/parts/attributes.hbs +++ b/templates/sheets/parts/attributes.hbs @@ -7,22 +7,19 @@ {{#each this.attributes as |attribute key|}}
- +
{{key}}
-
- -
{{#if ../editAttributes}} - + {{#if (not (eq attribute.base 0))}}{{/if}} {{#each ../abilityScoreArray as |option|}} - + {{/each}} {{else}} -
{{attribute.data.value}}
+
{{attribute.value}}
{{/if}}
diff --git a/templates/sheets/parts/defense.hbs b/templates/sheets/parts/defense.hbs index cf5ef747..f56e5096 100644 --- a/templates/sheets/parts/defense.hbs +++ b/templates/sheets/parts/defense.hbs @@ -4,7 +4,7 @@
-
{{document.system.evasion}}
+
{{document.system.evasion.value}}
{{localize "DAGGERHEART.Sheets.PC.Defense.Evasion"}}
diff --git a/templates/sheets/parts/health.hbs b/templates/sheets/parts/health.hbs index 64dbc4c2..4b5a9fac 100644 --- a/templates/sheets/parts/health.hbs +++ b/templates/sheets/parts/health.hbs @@ -21,7 +21,7 @@
{{localize "DAGGERHEART.Sheets.PC.Health.Severe"}}
- +
@@ -30,12 +30,12 @@
- {{#times document.system.resources.health.max}} + {{#times document.system.resources.hitPoints.max}} {{#with (add this 1)}} - + {{/with}} {{/times}} - {{#times (subtract 12 document.system.resources.health.max)}} + {{#times (subtract 12 document.system.resources.hitPoints.max)}} {{/times}}
diff --git a/templates/sheets/pc/pc.hbs b/templates/sheets/pc/pc.hbs index 2505ad5f..25704b15 100644 --- a/templates/sheets/pc/pc.hbs +++ b/templates/sheets/pc/pc.hbs @@ -41,13 +41,13 @@
-
+
- - {{#if document.system.canLevelUp}}
*
{{/if}} + + {{#if document.system.levelData.canLevelUp}}
*
{{/if}}
-
{{localize "DAGGERHEART.Sheets.PC.Level"}}
+
{{localize "DAGGERHEART.Sheets.PC.Level"}}
diff --git a/templates/views/levelup.hbs b/templates/views/levelup.hbs deleted file mode 100644 index 1c718f7e..00000000 --- a/templates/views/levelup.hbs +++ /dev/null @@ -1,20 +0,0 @@ -
-
Level {{activeLevel}}
-
- {{#each data}} - {{> "systems/daggerheart/templates/views/parts/level.hbs" data=this }} - {{/each}} - {{!-- {{#each levelupConfig as |configData key|}} - {{> "systems/daggerheart/templates/views/parts/level.hbs" configData=configData levelData=(lookup ../levelData key) completedSelection=../completedSelection activeTier=../activeTier activeLevel=../activeLevel category=key }} - {{/each}} --}} -
-
- - {{#if (eq activeLevel changedLevel )}} - - {{else}} - - {{/if}} - -
-
\ No newline at end of file diff --git a/templates/views/levelup/parts/selectable-card-preview.hbs b/templates/views/levelup/parts/selectable-card-preview.hbs new file mode 100644 index 00000000..039983e5 --- /dev/null +++ b/templates/views/levelup/parts/selectable-card-preview.hbs @@ -0,0 +1,11 @@ +
+
+ + {{#if this.selected}} +
+ +
+ {{/if}} +
+
{{this.name}}
+
\ No newline at end of file diff --git a/templates/views/levelup/tabs/advancements.hbs b/templates/views/levelup/tabs/advancements.hbs new file mode 100644 index 00000000..86940ad4 --- /dev/null +++ b/templates/views/levelup/tabs/advancements.hbs @@ -0,0 +1,40 @@ +
+
+
+ {{#each this.levelup.tiersForRendering as |tier key|}} +
+ {{tier.name}} + + {{#each tier.groups}} +
+
+ {{#each this.checkboxGroups}} +
+ {{#each this.checkboxes}} + + {{/each}} +
+ {{/each}} +
+
{{this.label}}
+
+ {{/each}} +
+ {{/each}} +
+
+
\ No newline at end of file diff --git a/templates/views/levelup/tabs/selections.hbs b/templates/views/levelup/tabs/selections.hbs new file mode 100644 index 00000000..1f8b2b21 --- /dev/null +++ b/templates/views/levelup/tabs/selections.hbs @@ -0,0 +1,95 @@ +
+
+ {{#if (gt this.newExperiences.length 0)}} +
+

{{localize "DAGGERHEART.Application.LevelUp.summary.newExperiences"}}

+
+ {{#each this.newExperiences}} +
+
+ +
{{signedNumber this.modifier}}
+
+
+ {{#if this.name}}{{/if}} +
+
+ {{/each}} +
+
+ {{/if}} + + {{#if this.traits.active}} +
+

+
{{localize "DAGGERHEART.Application.LevelUp.summary.traits"}}
+
{{this.traits.progress.selected}}/{{this.traits.progress.max}}
+

+ + +
+ {{/if}} + + {{#if this.experienceIncreases.active}} +
+

+
{{localize "DAGGERHEART.Application.LevelUp.summary.experienceIncreases"}}
+
{{this.experienceIncreases.progress.selected}}/{{this.experienceIncreases.progress.max}}
+

+ + +
+ {{/if}} + + {{#if (gt this.domainCards.length 0)}} +
+

{{localize "DAGGERHEART.Application.LevelUp.summary.domainCards"}}

+ +
+ {{#each this.domainCards}} + {{> "systems/daggerheart/templates/components/card-preview.hbs" this }} + {{/each}} +
+
+ {{/if}} + + {{#if (gt this.subclassCards.length 0)}} +
+

{{localize "DAGGERHEART.Application.LevelUp.summary.subclass"}}

+ +
+ {{#each this.subclassCards}} + {{> "systems/daggerheart/templates/views/levelup/parts/selectable-card-preview.hbs" img=this.img name=this.name path=this.path selected=this.selected uuid=this.uuid disabled=this.disabled }} + {{/each}} +
+
+ {{/if}} + + {{#if this.multiclass}} +
+

{{localize "DAGGERHEART.Application.LevelUp.summary.multiclass"}}

+ +
+ {{> "systems/daggerheart/templates/components/card-preview.hbs" this.multiclass }} +
+ {{#each this.multiclass.domains}} +
+
{{localize this.label}}
+ + {{#if this.selected}} +
+ +
+ {{/if}} +
+ {{/each}} +
+
+
+ {{/if}} +
+
\ No newline at end of file diff --git a/templates/views/levelup/tabs/summary.hbs b/templates/views/levelup/tabs/summary.hbs new file mode 100644 index 00000000..77925cff --- /dev/null +++ b/templates/views/levelup/tabs/summary.hbs @@ -0,0 +1,134 @@ +
+
+
+ {{localize "DAGGERHEART.Application.LevelUp.summary.levelAchievements"}} + +
+ {{#if this.achievements.proficiency.shown}} +
+
+ {{localize "DAGGERHEART.Application.LevelUp.summary.proficiencyIncrease" proficiency=this.achievements.proficiency.old }} + + {{this.achievements.proficiency.new}} +
+
+ {{/if}} +
+
{{localize "DAGGERHEART.Application.LevelUp.summary.damageThresholds"}}{{#if this.levelAchievements.damageThresholds.unarmored}}({{localize "DAGGERHEART.General.unarmored"}}){{/if}}
+
+ {{localize "DAGGERHEART.Application.LevelUp.summary.damageThresholdMajorIncrease" threshold=this.achievements.damageThresholds.major.old }} + + {{this.achievements.damageThresholds.major.new}} +
+
+ {{localize "DAGGERHEART.Application.LevelUp.summary.damageThresholdSevereIncrease" threshold=this.achievements.damageThresholds.severe.old }} + + {{this.achievements.damageThresholds.severe.new}} +
+
+ {{#if this.achievements.domainCards.shown}} +
+
{{localize "DAGGERHEART.Application.LevelUp.summary.domainCards"}}
+
+ {{#each this.achievements.domainCards.values}} +
{{this.name}}
+ {{/each}} +
+
+ {{/if}} + {{#if this.achievements.experiences.shown}} +
+
{{localize "DAGGERHEART.Application.LevelUp.summary.newExperiences"}}
+
+ {{#each this.achievements.experiences.values}} +
{{this.name}} {{signedNumber this.modifier}}
+ {{/each}} +
+
+ {{/if}} +
+
+ +
+ {{localize "DAGGERHEART.Application.LevelUp.summary.levelAdvancements"}} + +
+ {{#if this.advancements.statistics.shown}} +
+ {{#if this.advancements.statistics.proficiency.shown}} +
+ {{localize "DAGGERHEART.Application.LevelUp.summary.proficiencyIncrease" proficiency=this.advancements.statistics.proficiency.old }} + + {{this.advancements.statistics.proficiency.new}} +
+ {{/if}} + + {{#if this.advancements.statistics.hitPoints.shown}} +
+ {{localize "DAGGERHEART.Application.LevelUp.summary.hpIncrease" hitPoints=this.advancements.statistics.hitPoints.old }} + + {{this.advancements.statistics.hitPoints.new}} +
+ {{/if}} + + {{#if this.advancements.statistics.stress.shown}} +
+ {{localize "DAGGERHEART.Application.LevelUp.summary.stressIncrease" stress=this.advancements.statistics.stress.old }} + + {{this.advancements.statistics.stress.new}} +
+ {{/if}} + {{#if this.advancements.statistics.evasion.shown}} +
+ {{localize "DAGGERHEART.Application.LevelUp.summary.evasionIncrease" evasion=this.advancements.statistics.evasion.old }} + + {{this.advancements.statistics.evasion.new}} +
+ {{/if}} +
+ {{/if}} + + {{#if this.advancements.traits}} +
+
{{localize "DAGGERHEART.Application.LevelUp.summary.traits"}}
+
+ {{#each this.advancements.traits}} +
{{this}}
+ {{/each}} +
+
+ {{/if}} + + {{#if this.advancements.domainCards}} +
+
{{localize "DAGGERHEART.Application.LevelUp.summary.domainCards"}}
+
+ {{#each this.advancements.domainCards}} +
{{this.name}}
+ {{/each}} +
+
+ {{/if}} + + {{#if this.advancements.experiences}} +
+
{{localize "DAGGERHEART.Application.LevelUp.summary.experienceIncreases"}}
+
+ {{#each this.advancements.experiences}} +
{{this.name}} {{signedNumber this.modifier}}
+ {{/each}} +
+
+ {{/if}} +
+
+ +
+ +
+
+
\ No newline at end of file diff --git a/templates/views/levelup/tabs/tab-navigation.hbs b/templates/views/levelup/tabs/tab-navigation.hbs new file mode 100644 index 00000000..3552825b --- /dev/null +++ b/templates/views/levelup/tabs/tab-navigation.hbs @@ -0,0 +1,40 @@ +
+ +
+ {{#if this.showTabs}} + + {{/if}} +
+ {{#if this.navigate.previous.fromSummary}} + + {{else}} + {{#if (not this.navigate.previous.disabled)}} + + {{/if}} + {{/if}} + {{#if this.navigate.next.show}} + {{#if this.navigate.next.toSummary}} + + {{else}} + + {{/if}} + {{else}} +
+ {{/if}} +
+
+ +
\ No newline at end of file diff --git a/templates/views/parts/level.hbs b/templates/views/parts/level.hbs deleted file mode 100644 index 744f4fe9..00000000 --- a/templates/views/parts/level.hbs +++ /dev/null @@ -1,40 +0,0 @@ -
-
- - {{data.label}} - - -
{{data.info}}
-
{{data.pretext}}
-
- {{#each data.choices as |choice choiceKey|}} -
-
- {{#each choice.values as |value valueKey|}} - {{#times choice.cost}} -
- - {{#if (lt (add this 1) ../../cost)}} - - {{/if}} - {{#if ../locked}} - - {{/if}} -
- {{/times}} - {{/each}} -
-
{{localize choice.description}}
-
- {{/each}} -
-
{{data.posttext}}
-
-
\ No newline at end of file diff --git a/tools/create-symlink.mjs b/tools/create-symlink.mjs index 19c29814..0c8804c6 100644 --- a/tools/create-symlink.mjs +++ b/tools/create-symlink.mjs @@ -1,52 +1,47 @@ -import fs from "fs"; -import path from "path"; -import readline from "readline"; +import fs from 'fs'; +import path from 'path'; +import readline from 'readline'; -console.log("Reforging Symlinks"); +console.log('Reforging Symlinks'); -const askQuestion = (question) => { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); +const askQuestion = question => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); - return new Promise((resolve) => - rl.question(question, (answer) => { - rl.close(); - resolve(answer); - }) - ); + return new Promise(resolve => + rl.question(question, answer => { + rl.close(); + resolve(answer); + }) + ); }; -const installPath = await askQuestion("Enter your Foundry install path: "); +const installPath = await askQuestion('Enter your Foundry install path: '); // Determine if it's an Electron install (nested structure) -const nested = fs.existsSync(path.join(installPath, "resources", "app")); -const fileRoot = nested - ? path.join(installPath, "resources", "app") - : installPath; +const nested = fs.existsSync(path.join(installPath, 'resources', 'app')); +const fileRoot = nested ? path.join(installPath, 'resources', 'app') : installPath; try { - await fs.promises.mkdir("foundry"); + await fs.promises.mkdir('foundry'); } catch (e) { - if (e.code !== "EEXIST") throw e; + if (e.code !== 'EEXIST') throw e; } // JavaScript files -for (const p of ["client", "common", "tsconfig.json"]) { - try { - await fs.promises.symlink(path.join(fileRoot, p), path.join("foundry", p)); - } catch (e) { - if (e.code !== "EEXIST") throw e; - } +for (const p of ['client', 'common', 'tsconfig.json']) { + try { + await fs.promises.symlink(path.join(fileRoot, p), path.join('foundry', p)); + } catch (e) { + if (e.code !== 'EEXIST') throw e; + } } // Language files try { - await fs.promises.symlink( - path.join(fileRoot, "public", "lang"), - path.join("foundry", "lang") - ); + await fs.promises.symlink(path.join(fileRoot, 'public', 'lang'), path.join('foundry', 'lang')); } catch (e) { - if (e.code !== "EEXIST") throw e; + if (e.code !== 'EEXIST') throw e; }