Feature/89 gm fear display (#90)

* fear display
This commit is contained in:
Dapoulp 2025-06-01 02:48:14 +02:00 committed by GitHub
parent 4cdd2f05eb
commit d30ae91109
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 401 additions and 2 deletions

View file

@ -13,6 +13,7 @@ import DhpTokenRuler from './module/ui/tokenRuler.mjs';
import { dualityRollEnricher } from './module/enrichers/DualityRollEnricher.mjs'; import { dualityRollEnricher } from './module/enrichers/DualityRollEnricher.mjs';
import { getCommandTarget, rollCommandToJSON, setDiceSoNiceForDualityRoll } from './module/helpers/utils.mjs'; import { getCommandTarget, rollCommandToJSON, setDiceSoNiceForDualityRoll } from './module/helpers/utils.mjs';
import { abilities } from './module/config/actorConfig.mjs'; import { abilities } from './module/config/actorConfig.mjs';
import Resources from './module/applications/resources.mjs';
globalThis.SYSTEM = SYSTEM; globalThis.SYSTEM = SYSTEM;
@ -92,9 +93,11 @@ Hooks.once('init', () => {
CONFIG.Combat.documentClass = documents.DhpCombat; CONFIG.Combat.documentClass = documents.DhpCombat;
CONFIG.ui.combat = DhpCombatTracker; CONFIG.ui.combat = DhpCombatTracker;
CONFIG.ui.chat = DhpChatLog; CONFIG.ui.chat = DhpChatLog;
CONFIG.ui.players = DhpPlayers; // CONFIG.ui.players = DhpPlayers;
CONFIG.Token.rulerClass = DhpTokenRuler; CONFIG.Token.rulerClass = DhpTokenRuler;
CONFIG.ui.resources = Resources;
game.socket.on(`system.${SYSTEM.id}`, handleSocketEvent); game.socket.on(`system.${SYSTEM.id}`, handleSocketEvent);
// Make Compendium Dialog resizable // Make Compendium Dialog resizable
@ -106,6 +109,11 @@ Hooks.once('init', () => {
return preloadHandlebarsTemplates(); return preloadHandlebarsTemplates();
}); });
Hooks.on('ready', () => {
ui.resources = new CONFIG.ui.resources();
ui.resources.render({force: true});
})
Hooks.once('dicesoniceready', () => {}); Hooks.once('dicesoniceready', () => {});
Hooks.on(socketEvent.GMUpdate, async (action, uuid, update) => { Hooks.on(socketEvent.GMUpdate, async (action, uuid, update) => {

View file

@ -81,6 +81,14 @@
"Fear": { "Fear": {
"Name": "Fear", "Name": "Fear",
"Hint": "The Fear pool of the GM." "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": { "General": {

View file

@ -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);
}
}

View file

@ -182,6 +182,38 @@ export const registerDHSettings = () => {
default: 0 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, { game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope, {
name: game.i18n.localize('DAGGERHEART.Settings.Automation.Hope.Name'), name: game.i18n.localize('DAGGERHEART.Settings.Automation.Hope.Name'),
hint: game.i18n.localize('DAGGERHEART.Settings.Automation.Hope.Hint'), hint: game.i18n.localize('DAGGERHEART.Settings.Automation.Hope.Hint'),

View file

@ -19,7 +19,9 @@ export const gameSettings = {
ActionPoints: 'AutomationActionPoints' ActionPoints: 'AutomationActionPoints'
}, },
Resources: { Resources: {
Fear: 'ResourcesFear' Fear: 'ResourcesFear',
MaxFear: 'ResourcesMaxFear',
DisplayFear: 'DisplayFear'
}, },
General: { General: {
AbilityArray: 'AbilityArray', AbilityArray: 'AbilityArray',

View file

@ -2718,6 +2718,115 @@ div.daggerheart.views.multiclass {
.item-button .item-icon.checked { .item-button .item-icon.checked {
opacity: 1; 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 { .application.sheet.daggerheart.dh-style.feature .item-sheet-header {
display: flex; display: flex;
} }

View file

@ -9,6 +9,7 @@
@import './sheets/sheets.less'; @import './sheets/sheets.less';
@import './dialog.less'; @import './dialog.less';
@import '../node_modules/@yaireo/tagify/dist/tagify.css'; @import '../node_modules/@yaireo/tagify/dist/tagify.css';
@import './resources.less';
// new styles imports // new styles imports
@import './less/items/feature.less'; @import './less/items/feature.less';

113
styles/resources.less Normal file
View file

@ -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;
}
}

View file

@ -0,0 +1,16 @@
<div>
<div id="resource-fear" class="{{#if isGM}}isGM{{/if}}">
{{#if (eq display 'token')}}
{{#times max}}
<i class="fas fa-skull {{#if (gte @this ../current)}} inactive{{/if}}" style="filter: hue-rotate(calc(({{this}}/{{../max}})*75deg))" data-index="{{this}}" data-action="setFear"></i>
{{/times}}
{{/if}}
{{#if (eq display 'bar')}}
{{#if isGM}}<div class="controls control-minus {{#if (lte current 0)}} disabled{{/if}}" data-increment="-1" data-action="increaseFear">-</div>{{/if}}
<div class="resource-bar fear-bar" style="--fear-percent: {{percent}}%">
<span>{{current}}/{{max}}</span>
</div>
{{#if isGM}}<div class="controls control-plus {{#if (gte current max)}} disabled{{/if}}" data-increment="+1" data-action="increaseFear">+</div>{{/if}}
{{/if}}
</div>
</div>