Added action tokens and Request functionality

This commit is contained in:
WBHarry 2025-06-06 21:09:14 +02:00
parent 0f77697614
commit 0ca0ab360e
14 changed files with 340 additions and 28 deletions

View file

@ -61,6 +61,13 @@
"outline": "Outline", "outline": "Outline",
"edge": "Edge" "edge": "Edge"
} }
},
"VariantRules": {
"title": "Variant Rules",
"label": "Variant Rules",
"hint": "Apply variant rules from the Daggerheart system",
"name": "Variant Rules",
"actionTokens": "Action Tokens"
} }
}, },
"Automation": { "Automation": {
@ -101,6 +108,12 @@
"Hint": "Enable measuring of ranges with the ruler according to set distances." "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": { "DualityRollColor": {
"Name": "Duality Roll Colour Scheme", "Name": "Duality Roll Colour Scheme",
"Hint": "The display type for Duality Rolls", "Hint": "The display type for Duality Rolls",
@ -330,7 +343,9 @@
} }
}, },
"Combat": { "Combat": {
"takeSpotlight": "Take The Spotlight", "giveSpotlight": "Give The Spotlight",
"requestSpotlight": "Request The Spotlight",
"requestingSpotlight": "Requesting The Spotlight",
"combatStarted": "Active" "combatStarted": "Active"
}, },
"LevelUp": { "LevelUp": {

View file

@ -1,5 +1,7 @@
import DhAppearance from '../data/settings/Appearance.mjs'; import DhAppearance from '../data/settings/Appearance.mjs';
import DHAppearanceSettings from './settings/appearanceSettings.mjs'; import DHAppearanceSettings from './settings/appearanceSettings.mjs';
import DhVariantRules from '../data/settings/VariantRules.mjs';
import DHVariantRuleSettings from './settings/variantRuleSettings.mjs';
class DhpAutomationSettings extends FormApplication { class DhpAutomationSettings extends FormApplication {
constructor(object = {}, options = {}) { constructor(object = {}, options = {}) {
@ -181,7 +183,8 @@ export const registerDHSettings = () => {
type: Number, type: Number,
default: 0, default: 0,
onChange: () => { 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, type: Number,
default: 12, default: 12,
onChange: () => { 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, config: true,
type: String, type: String,
choices: { choices: {
'token': 'Tokens', token: 'Tokens',
'bar': 'Bar', bar: 'Bar',
'hide': 'Hide' hide: 'Hide'
}, },
default: 'token', default: 'token',
onChange: value => { onChange: value => {
if(ui.resources) { if (ui.resources) {
if(value === 'hide') ui.resources.close({allowed: true}); if (value === 'hide') ui.resources.close({ allowed: true });
else ui.resources.render({force: 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, { game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance, {
scope: 'client', scope: 'client',
config: false, config: false,
@ -291,4 +301,13 @@ export const registerDHSettings = () => {
type: DHAppearanceSettings, type: DHAppearanceSettings,
restricted: false 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
});
}; };

View file

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

View file

@ -10,6 +10,10 @@ export const menu = {
Range: { Range: {
Name: 'GameSettingsRange', Name: 'GameSettingsRange',
Icon: 'fa-solid fa-ruler' Icon: 'fa-solid fa-ruler'
},
VariantRules: {
Name: 'GameSettingsVariantrules',
Icon: 'fa-solid fa-scale-balanced'
} }
}; };
@ -27,5 +31,6 @@ export const gameSettings = {
AbilityArray: 'AbilityArray', AbilityArray: 'AbilityArray',
RangeMeasurement: 'RangeMeasurement' RangeMeasurement: 'RangeMeasurement'
}, },
appearance: 'Appearance' appearance: 'Appearance',
variantRules: 'VariantRules'
}; };

View file

@ -2,7 +2,10 @@ export default class DhCombatant extends foundry.abstract.TypeDataModel {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
active: new fields.BooleanField({ initial: false }), spotlight: new fields.SchemaField({
requesting: new fields.BooleanField({ required: true, initial: false }),
active: new fields.BooleanField({ required: true, initial: false })
}),
actionTokens: new fields.NumberField({ required: true, integer: true, initial: 3 }) actionTokens: new fields.NumberField({ required: true, integer: true, initial: 3 })
}; };
} }

View file

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

View file

@ -1,6 +1,6 @@
export default class DhpCombat extends Combat { export default class DhpCombat extends Combat {
get combatant() { get combatant() {
return this.combatants.contents.find(x => x.system.active) ?? null; return this.combatants.contents.find(x => x.system.spotlight.active) ?? null;
} }
async startCombat() { async startCombat() {

View file

@ -1,7 +1,9 @@
export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
actions: { actions: {
takeSpotlight: this.takeSpotlight requestSpotlight: this.requestSpotlight,
toggleSpotlight: this.toggleSpotlight,
setActionTokens: this.setActionTokens
} }
}; };
@ -17,6 +19,27 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
} }
}; };
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);
Object.assign(context, {
actionTokens: game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.variantRules).actionTokens
});
}
async _prepareTurnContext(combat, combatant, index) {
const turn = await super._prepareTurnContext(combat, combatant, index);
return { ...turn, system: combatant.system.toObject() };
}
_getCombatContextOptions() { _getCombatContextOptions() {
return [ return [
{ {
@ -34,12 +57,40 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
]; ];
} }
static async takeSpotlight(_, target) { static async requestSpotlight(_, target) {
const { combatantId } = target.closest('[data-combatant-id]')?.dataset ?? {}; const { combatantId } = target.closest('[data-combatant-id]')?.dataset ?? {};
for (var combatant of this.viewed.combatants) { const combatant = this.viewed.combatants.get(combatantId);
await combatant.update({ 'system.active': combatantId === combatant.id ? true : false }); await combatant.update({
} 'system.spotlight': {
requesting: !combatant.system.spotlight.requesting
}
});
this.render(); this.render();
} }
static async toggleSpotlight(_, target) {
const { combatantId } = target.closest('[data-combatant-id]')?.dataset ?? {};
for (var combatant of this.viewed.combatants) {
const giveSpotlight = combatant.id === combatantId;
await combatant.update({
'system.spotlight': {
requesting: giveSpotlight ? false : combatant.system.spotlight.requesting,
active: giveSpotlight ? !combatant.system.spotlight.active : 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();
}
} }

View file

@ -1322,16 +1322,50 @@
flex: 0 0 var(--input-height); flex: 0 0 var(--input-height);
align-self: stretch; align-self: stretch;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 16px;
}
.combat-sidebar .token-actions .action-tokens {
display: flex;
gap: 4px;
}
.combat-sidebar .token-actions .action-tokens .action-token {
height: 24px;
border: 1px solid;
border-radius: 8px;
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;
} }
.combat-sidebar .token-actions button { .combat-sidebar .token-actions button {
font-size: 22px; font-size: 22px;
} }
.combat-sidebar .token-actions button:hover { .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 .token-actions button.discrete:hover {
background: inherit; background: inherit;
} }
.combat-sidebar .token-actions .combatant-control:focus {
outline: none;
box-shadow: none;
}
.combat-sidebar .token-actions .combatant-control.requesting {
filter: drop-shadow(0 0 3px gold);
color: var(--button-hover-text-color);
}
.chat-message.duality { .chat-message.duality {
border-color: black; border-color: black;
padding: 8px 0 0 0; padding: 8px 0 0 0;
@ -3171,6 +3205,19 @@ div.daggerheart.views.multiclass {
.application.setting.dh-style footer button { .application.setting.dh-style footer button {
flex: 1; 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 { .system-daggerheart .tagify {
background: light-dark(transparent, transparent); background: light-dark(transparent, transparent);
border: 1px solid light-dark(#222, #efe6d8); border: 1px solid light-dark(#222, #efe6d8);

View file

@ -212,6 +212,22 @@
flex: 1; 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 { .system-daggerheart {

View file

@ -103,16 +103,59 @@
flex: 0 0 var(--input-height); flex: 0 0 var(--input-height);
align-self: stretch; align-self: stretch;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 16px;
.action-tokens {
display: flex;
gap: 4px;
.action-token {
height: 24px;
border: 1px solid;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
padding: 8px;
--button-size: 0;
&.used {
opacity: 0.5;
}
}
}
button { button {
font-size: 22px; font-size: 22px;
&:hover { &.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));
}
}
&.discrete:hover {
background: inherit; background: inherit;
} }
} }
.combatant-control {
&:focus {
outline: none;
box-shadow: none;
}
&.requesting {
filter: drop-shadow(0 0 3px gold);
color: var(--button-hover-text-color);
}
}
} }
} }

View file

@ -0,0 +1,22 @@
<div>
<div class="form-group">
<label>{{localize "DAGGERHEART.Settings.Menu.VariantRules.actionTokens"}}</label>
<div class="form-fields">
{{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)}}
</div>
</div>
<footer class="form-footer">
<button data-action="reset">
<i class="fa-solid fa-arrow-rotate-left"></i>
<span>{{localize "Reset"}}</span>
</button>
<button data-action="save" >
<i class="fa-solid fa-floppy-disk"></i>
<span>{{localize "Save Changes"}}</span>
</button>
</footer>
</div>

View file

@ -1,8 +1,6 @@
<ol class="combat-tracker plain"> <ol class="combat-tracker plain">
{{#each turns}} {{#each turns}}
<li class="combatant {{ css }}" data-combatant-id="{{ id }}" data-action="activateCombatant"> <li class="combatant {{ css }}" data-combatant-id="{{ id }}" data-action="activateCombatant">
{{!-- TODO: Targets --}}
{{!-- Image --}} {{!-- Image --}}
<img class="token-image" src="{{ img }}" alt="{{ name }}" loading="lazy"> <img class="token-image" src="{{ img }}" alt="{{ name }}" loading="lazy">
@ -38,14 +36,35 @@
{{!-- Resource --}} {{!-- Resource --}}
{{#if resource includeZero=true}} {{#if resource includeZero=true}}
<div class="token-resource"> <div class="token-resource">
<span class="resource">{{ resource }}</span> <span class="resource">{{ resource }}</span>
</div> </div>
{{/if}} {{/if}}
{{#if ../combat.system.started}} {{#if ../combat.system.started}}
<div class="token-actions"> <div class="token-actions">
<button type="button" class="inline-control combatant-control icon fa-solid fa-hand" data-action="takeSpotlight" data-combatant-id="{{id}}" data-tooltip aria-label="{{ localize "DAGGERHEART.Combat.takeSpotlight" }}"></button> {{#if isOwner}}
{{#if ../actionTokens.enabled}}
<div class="action-tokens">
{{#times ../actionTokens.tokens}}
<button type="button" class="inline-control main icon fa-solid fa-bolt-lightning action-token {{#if (lte ../system/actionTokens this)}}used{{/if}}" data-action="setActionTokens" data-combatant-id="{{../id}}" data-token-index="{{this}}">
</button>
{{/times}}
</div>
{{/if}}
{{#if @root.user.isGM}}
<button
type="button" class="inline-control combatant-control discrete icon fa-solid {{#if system.spotlight.requesting}}fa-hand-sparkles requesting{{else}}fa-hand{{/if}}"
data-action="toggleSpotlight" data-combatant-id="{{id}}" data-tooltip aria-label="{{localize "DAGGERHEART.Combat.giveSpotlight"}}"
></button>
{{else}}
<button
type="button" class="inline-control combatant-control discrete icon fa-solid {{#if system.spotlight.requesting}}fa-hand-sparkles requesting{{else}}fa-hand{{/if}}"
data-action="requestSpotlight" data-combatant-id="{{id}}" data-tooltip aria-label="{{#if system.spotlight.requesting}}{{localize "DAGGERHEART.Combat.requestingSpotlight"}}{{else}}{{localize "DAGGERHEART.Combat.requestSpotlight"}}{{/if}}"
></button>
{{/if}}
{{/if}}
</div> </div>
{{/if}} {{/if}}
</li> </li>

View file

@ -55,7 +55,7 @@
<div class="encounter-control-fear-container"> <div class="encounter-control-fear-container">
<img class="dice " src="../icons/svg/d12-grey.svg"/> <img class="dice " src="../icons/svg/d12-grey.svg"/>
<i class="fas fa-skull encounter-control-fear"></i> <i class="fas fa-skull encounter-control-fear"></i>
<div class="encounter-control-counter">4</div> <div class="encounter-control-counter">{{fear}}</div>
</div> </div>
{{/if}} {{/if}}