From d30ae91109ea632ff5d93f67e89614b2d64fc3d3 Mon Sep 17 00:00:00 2001 From: Dapoulp <74197441+Dapoulp@users.noreply.github.com> Date: Sun, 1 Jun 2025 02:48:14 +0200 Subject: [PATCH 1/2] Feature/89 gm fear display (#90) * fear display --- daggerheart.mjs | 10 ++- lang/en.json | 8 +++ module/applications/resources.mjs | 110 +++++++++++++++++++++++++++++ module/applications/settings.mjs | 32 +++++++++ module/config/settingsConfig.mjs | 4 +- styles/daggerheart.css | 109 ++++++++++++++++++++++++++++ styles/daggerheart.less | 1 + styles/resources.less | 113 ++++++++++++++++++++++++++++++ templates/views/resources.hbs | 16 +++++ 9 files changed, 401 insertions(+), 2 deletions(-) create mode 100644 module/applications/resources.mjs create mode 100644 styles/resources.less create mode 100644 templates/views/resources.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index 879c3b58..27979ae1 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -13,6 +13,7 @@ import DhpTokenRuler from './module/ui/tokenRuler.mjs'; import { dualityRollEnricher } from './module/enrichers/DualityRollEnricher.mjs'; import { getCommandTarget, rollCommandToJSON, setDiceSoNiceForDualityRoll } from './module/helpers/utils.mjs'; import { abilities } from './module/config/actorConfig.mjs'; +import Resources from './module/applications/resources.mjs'; globalThis.SYSTEM = SYSTEM; @@ -92,9 +93,11 @@ Hooks.once('init', () => { CONFIG.Combat.documentClass = documents.DhpCombat; CONFIG.ui.combat = DhpCombatTracker; CONFIG.ui.chat = DhpChatLog; - CONFIG.ui.players = DhpPlayers; + // CONFIG.ui.players = DhpPlayers; CONFIG.Token.rulerClass = DhpTokenRuler; + CONFIG.ui.resources = Resources; + game.socket.on(`system.${SYSTEM.id}`, handleSocketEvent); // Make Compendium Dialog resizable @@ -106,6 +109,11 @@ Hooks.once('init', () => { return preloadHandlebarsTemplates(); }); +Hooks.on('ready', () => { + ui.resources = new CONFIG.ui.resources(); + ui.resources.render({force: true}); +}) + Hooks.once('dicesoniceready', () => {}); Hooks.on(socketEvent.GMUpdate, async (action, uuid, update) => { diff --git a/lang/en.json b/lang/en.json index b8a350be..75a803e1 100755 --- a/lang/en.json +++ b/lang/en.json @@ -81,6 +81,14 @@ "Fear": { "Name": "Fear", "Hint": "The Fear pool of the GM." + }, + "MaxFear": { + "Name": "Maximum amount of Fear", + "Hint": "The maximum amount of Fear the GM can get." + }, + "DisplayFear": { + "Name": "Fear display style", + "Hint": "Change how the GM Fear count should be displayed." } }, "General": { diff --git a/module/applications/resources.mjs b/module/applications/resources.mjs new file mode 100644 index 00000000..535a1631 --- /dev/null +++ b/module/applications/resources.mjs @@ -0,0 +1,110 @@ + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +/** + * A UI element which displays the Users defined for this world. + * Currently active users are always displayed, while inactive users can be displayed on toggle. + * + * @extends ApplicationV2 + * @mixes HandlebarsApplication + */ + +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 + }, + 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" + // template: "templates/ui/players.hbs" + } + }; + + 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); + } + + /* -------------------------------------------- */ + /* 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 _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); + } + + _onPosition(position) { + game.user.setFlag(SYSTEM.id, 'app.resources.position', position); + } + + 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 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); + await this.render(true); + } +} \ No newline at end of file diff --git a/module/applications/settings.mjs b/module/applications/settings.mjs index 631ce1ce..c86af3a7 100644 --- a/module/applications/settings.mjs +++ b/module/applications/settings.mjs @@ -182,6 +182,38 @@ export const registerDHSettings = () => { default: 0 }); + game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.MaxFear, { + name: game.i18n.localize('DAGGERHEART.Settings.Resources.MaxFear.Name'), + hint: game.i18n.localize('DAGGERHEART.Settings.Resources.MaxFear.Hint'), + scope: 'world', + config: true, + type: Number, + default: 12, + onChange: () => { + if(ui.resources) ui.resources.render({force: true}); + } + }); + + game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.DisplayFear, { + name: game.i18n.localize('DAGGERHEART.Settings.Resources.DisplayFear.Name'), + hint: game.i18n.localize('DAGGERHEART.Settings.Resources.DisplayFear.Hint'), + scope: 'client', + config: true, + type: String, + choices: { + '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}); + } + } + }); + game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope, { name: game.i18n.localize('DAGGERHEART.Settings.Automation.Hope.Name'), hint: game.i18n.localize('DAGGERHEART.Settings.Automation.Hope.Hint'), diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index be570260..26de2a48 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -19,7 +19,9 @@ export const gameSettings = { ActionPoints: 'AutomationActionPoints' }, Resources: { - Fear: 'ResourcesFear' + Fear: 'ResourcesFear', + MaxFear: 'ResourcesMaxFear', + DisplayFear: 'DisplayFear' }, General: { AbilityArray: 'AbilityArray', diff --git a/styles/daggerheart.css b/styles/daggerheart.css index e890794f..a5550802 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -2718,6 +2718,115 @@ div.daggerheart.views.multiclass { .item-button .item-icon.checked { opacity: 1; } +: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; +} +#resources { + min-height: calc(var(--header-height) + 4rem); + min-width: 4rem; + color: #d3d3d3; +} +#resources .window-content { + padding: 0.5rem; +} +#resources .window-content #resource-fear { + display: flex; + flex-direction: row; + gap: 0.5rem 0.25rem; + flex-wrap: wrap; +} +#resources .window-content #resource-fear i { + font-size: var(--font-size-18); + 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, 0.75); + box-shadow: 0px 0px 5px 1px rgba(0, 0, 0, 0.75); + color: #d3d3d3; + flex-grow: 0; +} +#resources .window-content #resource-fear i.inactive { + filter: grayscale(1) !important; + opacity: 0.5; +} +#resources .window-content #resource-fear .controls, +#resources .window-content #resource-fear .resource-bar { + border: 2px solid #997a4f; + background-color: #18162e; +} +#resources .window-content #resource-fear .controls { + display: flex; + align-self: center; + border-radius: 50%; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + font-size: var(--font-size-20); + cursor: pointer; +} +#resources .window-content #resource-fear .controls:hover { + font-size: 1.5rem; +} +#resources .window-content #resource-fear .controls.disabled { + opacity: 0.5; +} +#resources .window-content #resource-fear .resource-bar { + display: flex; + justify-content: center; + border-radius: 6px; + font-size: var(--font-size-20); + overflow: hidden; + position: relative; + padding: 0.25rem 0.5rem; + flex: 1; + text-shadow: var(--shadow-text-stroke); +} +#resources .window-content #resource-fear .resource-bar:before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: var(--fear-percent); + max-width: 100%; + background: linear-gradient(90deg, #020026 0%, #c701fc 100%); + z-index: 0; + border-radius: 4px; +} +#resources .window-content #resource-fear .resource-bar span { + position: inherit; + z-index: 1; +} +#resources .window-content #resource-fear.isGM i { + cursor: pointer; +} +#resources .window-content #resource-fear.isGM i:hover { + font-size: var(--font-size-20); +} +#resources button[data-action="close"] { + display: none; +} +#resources:not(:hover):not(.minimized) { + background: transparent; + box-shadow: unset; + border-color: transparent; +} +#resources:not(:hover):not(.minimized) header, +#resources:not(:hover):not(.minimized) .controls, +#resources:not(:hover):not(.minimized) .window-resize-handle { + visibility: hidden; +} +#resources:has(.fear-bar) { + min-width: 200px; +} .application.sheet.daggerheart.dh-style.feature .item-sheet-header { display: flex; } diff --git a/styles/daggerheart.less b/styles/daggerheart.less index 181cd8a3..2ae18980 100755 --- a/styles/daggerheart.less +++ b/styles/daggerheart.less @@ -9,6 +9,7 @@ @import './sheets/sheets.less'; @import './dialog.less'; @import '../node_modules/@yaireo/tagify/dist/tagify.css'; +@import './resources.less'; // new styles imports @import './less/items/feature.less'; diff --git a/styles/resources.less b/styles/resources.less new file mode 100644 index 00000000..71a00c59 --- /dev/null +++ b/styles/resources.less @@ -0,0 +1,113 @@ +: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; +} + +#resources { + min-height: calc(var(--header-height) + 4rem); + min-width: 4rem; + color: #d3d3d3; + .window-content { + padding: .5rem; + #resource-fear { + display: flex; + flex-direction: row; + gap: .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-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); + color: #d3d3d3; + flex-grow: 0; + &.inactive{ + filter: grayscale(1) !important; + opacity: .5; + } + } + .controls, .resource-bar { + border: 2px solid rgb(153 122 79); + background-color: rgb(24 22 46); + } + .controls { + display: flex; + align-self: center; + border-radius: 50%; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + font-size: var(--font-size-20); + cursor: pointer; + &:hover { + font-size: 1.5rem; + } + &.disabled { + opacity: .5; + } + } + .resource-bar { + display: flex; + justify-content: center; + border-radius: 6px; + font-size: var(--font-size-20); + overflow: hidden; + position: relative; + padding: .25rem .5rem; + flex: 1; + text-shadow: var(--shadow-text-stroke); + &:before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + 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%); + z-index: 0; + border-radius: 4px; + } + span { + position: inherit; + z-index: 1; + } + &.fear { + + } + } + &.isGM { + i { + cursor: pointer; + &:hover { + font-size: var(--font-size-20); + } + } + } + } + } + button[data-action="close"] { + display: none; + } + &:not(:hover):not(.minimized) { + background: transparent; + box-shadow: unset; + border-color: transparent; + header, .controls, .window-resize-handle { + visibility: hidden; + } + } + &:has(.fear-bar) { + min-width: 200px; + } +} \ No newline at end of file diff --git a/templates/views/resources.hbs b/templates/views/resources.hbs new file mode 100644 index 00000000..6832ab90 --- /dev/null +++ b/templates/views/resources.hbs @@ -0,0 +1,16 @@ +
+
+ {{#if (eq display 'token')}} + {{#times max}} + + {{/times}} + {{/if}} + {{#if (eq display 'bar')}} + {{#if isGM}}
-
{{/if}} +
+ {{current}}/{{max}} +
+ {{#if isGM}}
+
{{/if}} + {{/if}} +
+
\ No newline at end of file From 879299b6615963b45026d0f32150539915188584 Mon Sep 17 00:00:00 2001 From: joaquinpereyra98 <24190917+joaquinpereyra98@users.noreply.github.com> Date: Sat, 31 May 2025 21:54:45 -0300 Subject: [PATCH 2/2] #98 jsconfig and symlink setup (#99) * FEAT: add jsconfig.json file * FEAT: add new script createSymlink FEAT: add new file tools/create-symlink.mjs FEAT: add d.ts files FIX: add new foundry symlink to .gitignore --------- Co-authored-by: Joaquin Pereyra --- .gitignore | 3 ++- daggerheart.d.ts | 21 ++++++++++++++++ jsconfig.json | 15 ++++++++++++ module/_types.d.ts | 0 package.json | 3 ++- tools/create-symlink.mjs | 52 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 daggerheart.d.ts create mode 100644 jsconfig.json create mode 100644 module/_types.d.ts create mode 100644 tools/create-symlink.mjs diff --git a/.gitignore b/.gitignore index f597cf72..48fb3ad3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules /packs Build -/build \ No newline at end of file +/build +foundry \ No newline at end of file diff --git a/daggerheart.d.ts b/daggerheart.d.ts new file mode 100644 index 00000000..3b753baf --- /dev/null +++ b/daggerheart.d.ts @@ -0,0 +1,21 @@ +import './module/_types'; +import '@client/global.mjs'; +import Canvas from '@client/canvas/board.mjs'; + +// Foundry's use of `Object.assign(globalThis) means many globally available objects are not read as such +// This declare global hopefully fixes that +declare global { + /** + * A simple event framework used throughout Foundry Virtual Tabletop. + * When key actions or events occur, a "hook" is defined where user-defined callback functions can execute. + * This class manages the registration and execution of hooked callback functions. + */ + class Hooks extends foundry.helpers.Hooks {} + const fromUuid = foundry.utils.fromUuid; + const fromUuidSync = foundry.utils.fromUuidSync; + + /** + * The singleton game canvas + */ + const canvas: Canvas; +} diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 00000000..00bab1f5 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "ES6", + "target": "ES6", + "paths": { + "@client/*": ["./foundry/client/*"], + "@common/*": ["./foundry/common/*"] + } + }, + "exclude": ["node_modules", "**/node_modules/*"], + "include": ["daggerheart.mjs", "foundry/client/client.mjs", "daggerheart.d.ts"], + "typeAcquisition": { + "include": ["jquery"] + } +} diff --git a/module/_types.d.ts b/module/_types.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/package.json b/package.json index 7e76b5e7..f8be74b9 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "start": "concurrently \"rollup -c --watch\" \"node ../../../../FoundryDev/main.js --dataPath=../../../ --noupnp\" \"gulp\"", "start-test": "node ./resources/app/main.js --dataPath=./ && rollup -c --watch && gulp", "pushLDBtoYML": "node ./tools/pushLDBtoYML.mjs", - "pullYMLtoLDB": "node ./tools/pullYMLtoLDB.mjs" + "pullYMLtoLDB": "node ./tools/pullYMLtoLDB.mjs", + "createSymlink": "node ./tools/create-symlink.mjs" }, "devDependencies": { "@foundryvtt/foundryvtt-cli": "^1.0.2", diff --git a/tools/create-symlink.mjs b/tools/create-symlink.mjs new file mode 100644 index 00000000..19c29814 --- /dev/null +++ b/tools/create-symlink.mjs @@ -0,0 +1,52 @@ +import fs from "fs"; +import path from "path"; +import readline from "readline"; + +console.log("Reforging Symlinks"); + +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); + }) + ); +}; + +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; + +try { + await fs.promises.mkdir("foundry"); +} catch (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; + } +} + +// Language files +try { + await fs.promises.symlink( + path.join(fileRoot, "public", "lang"), + path.join("foundry", "lang") + ); +} catch (e) { + if (e.code !== "EEXIST") throw e; +}