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/daggerheart.mjs b/daggerheart.mjs index 2b704f36..aa08cd62 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/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/lang/en.json b/lang/en.json index a4c2511c..08700161 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/_types.d.ts b/module/_types.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/module/applications/resources.mjs b/module/applications/resources.mjs new file mode 100644 index 00000000..9b9781e0 --- /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); + } +} diff --git a/module/applications/settings.mjs b/module/applications/settings.mjs index 738cbe66..e39cab51 100644 --- a/module/applications/settings.mjs +++ b/module/applications/settings.mjs @@ -184,6 +184,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 15049d2b..a0702a80 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/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/styles/daggerheart.css b/styles/daggerheart.css index fbc1aa31..5ee85b31 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -2905,6 +2905,115 @@ div.daggerheart.views.multiclass { .daggerheart.levelup .levelup-footer .advancement-information-container .advancement-tier-info { font-size: 14px; } +: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 6e1a793f..6869e316 100755 --- a/styles/daggerheart.less +++ b/styles/daggerheart.less @@ -10,6 +10,7 @@ @import './dialog.less'; @import './levelup.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..91a97e2d --- /dev/null +++ b/styles/resources.less @@ -0,0 +1,115 @@ +: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; + .window-content { + padding: 0.5rem; + #resource-fear { + display: flex; + flex-direction: row; + 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, 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; + &.inactive { + filter: grayscale(1) !important; + opacity: 0.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: 0.5; + } + } + .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); + &: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; + } +} 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 diff --git a/tools/create-symlink.mjs b/tools/create-symlink.mjs new file mode 100644 index 00000000..0c8804c6 --- /dev/null +++ b/tools/create-symlink.mjs @@ -0,0 +1,47 @@ +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; +}