Added CountdownEdit view

This commit is contained in:
WBHarry 2025-09-14 13:55:53 +02:00
parent 58f039ce96
commit 2c51f06f86
15 changed files with 469 additions and 69 deletions

View file

@ -331,7 +331,8 @@
"label": { "label": "Label", "hint": "Used for custom" },
"value": { "label": "Value" }
}
}
},
"type": { "label": "Countdown Type" }
}
}
},
@ -346,6 +347,25 @@
"encounter": "Encounter"
}
},
"CountdownEdit": {
"title": "Countdown Edit",
"viewTitle": "Countdowns",
"editTitle": "Edit Countdowns",
"newCountdown": "New Countdown",
"removeCountdownTitle": "Remove Countdown",
"removeCountdownText": "Are you sure you want to remove the countdown: {name}?",
"current": "Current",
"max": "Max",
"currentCountdownValue": "Current: {value}",
"currentCountdownMax": "Max: {value}",
"category": "Category",
"type": "Type",
"defaultOwnershipTooltip": "The default player ownership of countdowns"
},
"DaggerheartMenu": {
"title": "GM Tools",
"countdowns": "Edit Countdowns"
},
"DeleteConfirmation": {
"title": "Delete {type} - {name}",
"text": "Are you sure you want to delete {name}?"

View file

@ -1,13 +1,12 @@
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class OwnershipSelection extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(resolve, reject, name, ownership) {
constructor(name, ownership, defaultOwnership) {
super({});
this.resolve = resolve;
this.reject = reject;
this.name = name;
this.ownership = ownership;
this.ownership = foundry.utils.deepClone(ownership);
this.defaultOwnership = defaultOwnership;
}
static DEFAULT_OPTIONS = {
@ -30,43 +29,48 @@ export default class OwnershipSelection extends HandlebarsApplicationMixin(Appli
return game.i18n.format('DAGGERHEART.APPLICATIONS.OwnershipSelection.title', { name: this.name });
}
getOwnershipData(id) {
return this.ownership[id] ?? CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT;
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.ownershipOptions = Object.keys(CONST.DOCUMENT_OWNERSHIP_LEVELS).map(level => ({
value: CONST.DOCUMENT_OWNERSHIP_LEVELS[level],
label: game.i18n.localize(`OWNERSHIP.${level}`)
}));
context.ownership = {
default: this.ownership.default,
players: Object.keys(this.ownership.players).reduce((acc, x) => {
const user = game.users.get(x);
if (!user.isGM) {
acc[x] = {
img: user.character?.img ?? 'icons/svg/cowled.svg',
name: user.name,
ownership: this.ownership.players[x].value
};
}
context.ownershipDefaultOptions = CONFIG.DH.GENERAL.basicOwnershiplevels;
context.ownershipOptions = CONFIG.DH.GENERAL.simpleOwnershiplevels;
context.defaultOwnership = this.defaultOwnership;
context.ownership = game.users.reduce((acc, user) => {
if (!user.isGM) {
acc[user.id] = {
...user,
img: user.character?.img ?? 'icons/svg/cowled.svg',
ownership: this.getOwnershipData(user.id)
};
}
return acc;
}, {})
};
return acc;
}, {});
return context;
}
static async updateData(event, _, formData) {
const { ownership } = foundry.utils.expandObject(formData.object);
this.resolve(ownership);
this.close(true);
const data = foundry.utils.expandObject(formData.object);
this.close(data);
}
async close(fromSave) {
if (!fromSave) {
this.reject();
async close(data) {
if (data) {
this.saveData = data;
}
await super.close();
}
static async configure(name, ownership, defaultOwnership) {
return new Promise(resolve => {
const app = new this(name, ownership, defaultOwnership);
app.addEventListener('close', () => resolve(app.saveData), { once: true });
app.render({ force: true });
});
}
}

View file

@ -29,7 +29,8 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
},
actions: {
selectRefreshable: DaggerheartMenu.#selectRefreshable,
refreshActors: DaggerheartMenu.#refreshActors
refreshActors: DaggerheartMenu.#refreshActors,
editCountdowns: DaggerheartMenu.#editCountdowns
}
};
@ -157,4 +158,8 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
this.render();
}
static async #editCountdowns() {
new game.system.api.applications.ui.CountdownEdit().render(true);
}
}

View file

@ -1,3 +1,4 @@
export { default as CountdownEdit } from './countdownEdit.mjs';
export { default as DhChatLog } from './chatLog.mjs';
export { default as DhCombatTracker } from './combatTracker.mjs';
export * as DhCountdowns from './countdowns.mjs';

View file

@ -0,0 +1,129 @@
import { DhCountdown } from '../../data/countdowns.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class CountdownEdit extends HandlebarsApplicationMixin(ApplicationV2) {
constructor() {
super();
this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
this.editingCountdowns = new Set();
}
get title() {
return game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.title');
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'dh-style', 'countdown-edit'],
tag: 'form',
position: { width: 600 },
window: { icon: 'fa-solid fa-clock-rotate-left' },
actions: {
addCountdown: CountdownEdit.#addCountdown,
toggleCountdownEdit: CountdownEdit.#toggleCountdownEdit,
editCountdownImage: CountdownEdit.#editCountdownImage,
editCountdownOwnership: CountdownEdit.#editCountdownOwnership,
removeCountdown: CountdownEdit.#removeCountdown
},
form: { handler: this.updateData, submitOnChange: true }
};
static PARTS = {
countdowns: {
template: 'systems/daggerheart/templates/ui/countdown-edit.hbs',
scrollable: ['.expanded-view']
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.ownershipDefaultOptions = CONFIG.DH.GENERAL.basicOwnershiplevels;
context.defaultOwnership = this.data.defaultOwnership;
context.countdownBaseTypes = CONFIG.DH.GENERAL.countdownBaseTypes;
context.countdownTypes = CONFIG.DH.GENERAL.countdownTypes;
context.countdowns = Object.keys(this.data.countdowns).reduce((acc, key) => {
const countdown = this.data.countdowns[key];
acc[key] = {
...countdown,
typeName: game.i18n.localize(CONFIG.DH.GENERAL.countdownBaseTypes[countdown.type].name),
progress: {
...countdown.progress,
typeName: game.i18n.localize(CONFIG.DH.GENERAL.countdownTypes[countdown.progress.type].label)
},
editing: this.editingCountdowns.has(key)
};
return acc;
}, {});
return context;
}
async updateSetting(update) {
await this.data.updateSource(update);
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, this.data);
this.render();
}
static async updateData(_event, _, formData) {
this.updateSetting(foundry.utils.expandObject(formData.object));
}
static #addCountdown() {
this.updateSetting({
[`countdowns.${foundry.utils.randomID()}`]: DhCountdown.defaultCountdown()
});
}
static #editCountdownImage(_, target) {
const countdown = this.data.countdowns[target.id];
const fp = new foundry.applications.apps.FilePicker.implementation({
current: countdown.img,
type: 'image',
callback: async path => this.updateSetting({ [`countdowns.${target.id}.img`]: path }),
top: this.position.top + 40,
left: this.position.left + 10
});
return fp.browse();
}
static #toggleCountdownEdit(_, button) {
const { countdownId } = button.dataset;
const isEditing = this.editingCountdowns.has(countdownId);
if (isEditing) this.editingCountdowns.delete(countdownId);
else this.editingCountdowns.add(countdownId);
this.render();
}
static async #editCountdownOwnership(_, button) {
const countdown = this.data.countdowns[button.dataset.countdownId];
const data = await game.system.api.applications.dialogs.OwnershipSelection.configure(
countdown.name,
countdown.ownership,
this.data.defaultOwnership
);
if (!data) return;
this.updateSetting({ [`countdowns.${button.dataset.countdownId}`]: data });
}
static async #removeCountdown(_, button) {
const { countdownId } = button.dataset;
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.removeCountdownTitle')
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.CountdownEdit.removeCountdownText', {
name: this.data.countdowns[countdownId].name
})
});
if (!confirmed) return;
if (this.editingCountdowns.has(countdownId)) this.editingCountdowns.delete(countdownId);
this.updateSetting({ [`countdowns.-=${countdownId}`]: null });
}
}

View file

@ -650,3 +650,25 @@ export const fearDisplay = {
bar: { value: 'bar', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.bar' },
hide: { value: 'hide', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.hide' }
};
export const basicOwnershiplevels = {
0: { value: 0, label: 'OWNERSHIP.NONE' },
2: { value: 2, label: 'OWNERSHIP.OBSERVER' },
3: { value: 3, label: 'OWNERSHIP.OWNER' }
};
export const simpleOwnershiplevels = {
[-1]: { value: -1, label: 'OWNERSHIP.INHERIT' },
...basicOwnershiplevels
};
export const countdownBaseTypes = {
narrative: {
id: 'narrative',
name: 'DAGGERHEART.APPLICATIONS.Countdown.types.narrative'
},
encounter: {
id: 'encounter',
name: 'DAGGERHEART.APPLICATIONS.Countdown.types.encounter'
}
};

View file

@ -5,17 +5,22 @@ export default class DhCountdowns extends foundry.abstract.DataModel {
const fields = foundry.data.fields;
return {
/* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */
narrative: new fields.EmbeddedDataField(DhCountdownData),
encounter: new fields.EmbeddedDataField(DhCountdownData)
encounter: new fields.EmbeddedDataField(DhCountdownData),
/**/
countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhCountdown)),
defaultOwnership: new fields.NumberField({
required: true,
choices: CONFIG.DH.GENERAL.basicOwnershiplevels,
initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER
})
};
}
static CountdownCategories = { narrative: 'narrative', combat: 'combat' };
}
/* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */
class DhCountdownData extends foundry.abstract.DataModel {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.APPLICATIONS.Countdown']; // Nots ure why this won't work. Setting labels manually for now
static defineSchema() {
const fields = foundry.data.fields;
return {
@ -56,10 +61,15 @@ class DhCountdownData extends foundry.abstract.DataModel {
}
}
class DhCountdown extends foundry.abstract.DataModel {
export class DhCountdown extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
type: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.countdownBaseTypes,
label: 'DAGGERHEART.GENERAL.type'
}),
name: new fields.StringField({
required: true,
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.name.label'
@ -69,22 +79,13 @@ class DhCountdown extends foundry.abstract.DataModel {
base64: false,
initial: 'icons/magic/time/hourglass-yellow-green.webp'
}),
ownership: new fields.SchemaField({
default: new fields.NumberField({
ownership: new fields.TypedObjectField(
new fields.NumberField({
required: true,
choices: Object.values(CONST.DOCUMENT_OWNERSHIP_LEVELS),
initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE
}),
players: new fields.TypedObjectField(
new fields.SchemaField({
type: new fields.NumberField({
required: true,
choices: Object.values(CONST.DOCUMENT_OWNERSHIP_LEVELS),
initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
})
})
)
}),
choices: CONFIG.DH.GENERAL.simpleOwnershiplevels,
initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
})
),
progress: new fields.SchemaField({
current: new fields.NumberField({
required: true,
@ -98,21 +99,28 @@ class DhCountdown extends foundry.abstract.DataModel {
initial: 1,
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.max.label'
}),
type: new fields.SchemaField({
value: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.countdownTypes,
initial: CONFIG.DH.GENERAL.countdownTypes.custom.id,
label: 'DAGGERHEART.GENERAL.type'
}),
label: new fields.StringField({
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.type.label.label'
})
type: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.countdownTypes,
initial: CONFIG.DH.GENERAL.countdownTypes.custom.id,
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.type.label'
})
})
};
}
static defaultCountdown(type) {
return {
type: type ?? CONFIG.DH.GENERAL.countdownBaseTypes.narrative.id,
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.newCountdown'),
img: 'icons/magic/time/hourglass-yellow-green.webp',
progress: {
current: 1,
max: 1
}
};
}
get playerOwnership() {
return Array.from(game.users).reduce((acc, user) => {
acc[user.id] = {

View file

@ -97,6 +97,7 @@ export async function runMigrations() {
}
if (foundry.utils.isNewerVersion('1.2.0', lastMigrationVersion)) {
/* Migrate old action costs */
const lockedPacks = [];
const compendiumItems = [];
for (let pack of game.packs) {
@ -148,6 +149,14 @@ export async function runMigrations() {
await pack.configure({ locked: true });
}
/* Migrate old countdown structure */
const { narrative, encounter } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
if (narrative) {
narrative.countdowns;
}
if (encounter) {
}
lastMigrationVersion = '1.2.0';
}

View file

@ -0,0 +1,117 @@
.theme-light .daggerheart.application.dh-style.countdown-edit {
background-image: url('../assets/parchments/dh-parchment-light.png');
}
.daggerheart.application.dh-style.countdown-edit {
color: light-dark(@dark, @beige);
background-image: url('../assets/parchments/dh-parchment-dark.png');
.edit-container {
display: flex;
flex-direction: column;
gap: 8px;
h2 {
text-align: center;
color: light-dark(@dark, @golden);
}
.header-tools {
display: grid;
grid-template-columns: 1fr 144px;
gap: 8px;
.header-main-button {
flex: 1;
}
.default-ownership-tools {
display: flex;
align-items: center;
gap: 8px;
select {
flex: 1;
background: light-dark(@beige, @dark-blue);
}
}
}
.edit-content {
display: flex;
flex-direction: column;
gap: 8px;
.countdown-edit-container {
display: grid;
grid-template-columns: 48px 1fr 64px;
align-items: center;
gap: 8px;
&.viewing {
padding: 0 16px;
}
img {
width: 52px;
height: 52px;
}
.countdown-edit-text {
display: flex;
flex-direction: column;
justify-content: center;
gap: 8px;
.countdown-edit-subtext {
display: flex;
gap: 8px;
.countdown-edit-sub-tag {
border: 1px solid;
border-radius: 4px;
padding: 2px 4px;
background: light-dark(@beige, @dark-blue);
}
}
}
.countdown-edit-tools {
display: flex;
gap: 8px;
&.same-row {
margin-top: 17.5px;
}
a {
font-size: 16px;
}
}
}
.countdown-edit-subrow {
display: flex;
gap: 16px;
margin: 0 72px 0 56px;
}
.countdown-edit-input {
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
&.tiny {
flex: 0;
}
input,
select {
background: light-dark(@beige, @dark-blue);
}
}
}
}
}

View file

@ -13,6 +13,7 @@
@import './item-browser/item-browser.less';
@import './countdown/countdown.less';
@import './countdown/countdown-edit.less';
@import './countdown/sheet.less';
@import './ownership-selection/ownership-selection.less';

View file

@ -1,5 +1,17 @@
.tab.sidebar-tab.daggerheartMenu-sidebar {
padding: 0 4px;
padding: 4px;
div[data-application-part] {
display: flex;
flex-direction: column;
gap: 8px;
}
h2 {
margin-top: 8px;
text-align: center;
font-weight: bold;
}
.menu-refresh-container {
display: flex;

View file

@ -2,17 +2,17 @@
<div class="form-group">
<div class="form-fields">
<label>{{localize "DAGGERHEART.APPLICATIONS.OwnershipSelection.default"}}</label>
<select name="ownership.default" data-dtype="Number">
{{selectOptions @root.ownershipOptions selected=ownership.default labelAttr="label" valueAttr="value" }}
<select name="default" data-dtype="Number" disabled>
{{selectOptions ownershipDefaultOptions selected=defaultOwnership labelAttr="label" valueAttr="value" localize=true }}
</select>
</div>
</div>
{{#each ownership.players as |player id|}}
{{#each ownership as |player id|}}
<div class="ownership-container">
<img src="{{player.img}}" />
<div>{{player.name}}</div>
<select name="{{concat "ownership.players." id ".type"}}" data-dtype="Number">
{{selectOptions @root.ownershipOptions selected=player.ownership labelAttr="label" valueAttr="value" }}
<select name="{{concat "ownership." id}}" data-dtype="Number">
{{selectOptions @root.ownershipOptions selected=player.ownership labelAttr="label" valueAttr="value" localize=true }}
</select>
</div>
{{/each}}

View file

@ -2,7 +2,7 @@
<div class="tiers-container">
{{#each this.tiers as |tier key|}}
<fieldset class="tier-container">
<legend>{{tier.name}}</legend>
<legend>{{localize tier.name}}</legend>
{{#each tier.groups}}
<div class="checkbox-group-container">

View file

@ -1,4 +1,6 @@
<div>
<h2>{{localize "DAGGERHEART.APPLICATIONS.DaggerheartMenu.title"}}</h2>
<fieldset>
<legend>{{localize "Refresh Features"}}</legend>
@ -19,4 +21,6 @@
<button data-action="refreshActors" {{disabled disableRefresh}}>{{localize "Refresh"}}</button>
</div>
</fieldset>
<button data-action="editCountdowns">{{localize "DAGGERHEART.APPLICATIONS.DaggerheartMenu.countdowns"}}</button>
</div>

View file

@ -0,0 +1,68 @@
<div>
<div class="edit-container">
<h2>{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.editTitle"}}</h2>
<div class="header-tools">
<button class="header-main-button" data-action="addCountdown">{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.newCountdown"}}</button>
<div class="default-ownership-tools">
<i class="fa-solid fa-eye" data-tooltip={{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.defaultOwnershipTooltip"}}></i>
<select name="defaultOwnership">
{{selectOptions ownershipDefaultOptions selected=defaultOwnership labelAttr="label" valueAttr="value" localize=true}}
</select>
</div>
</div>
<div class="edit-content">
{{#each countdowns as | countdown id | }}
<div class="countdown-edit-container {{#unless countdown.editing}}viewing{{/unless}}">
<a data-action="editCountdownImage" id="{{id}}"><img src="{{countdown.img}}" /></a>
{{#unless countdown.editing}}
<div class="countdown-edit-text">
<h4>{{countdown.name}}</h4>
<div class="countdown-edit-subtext">
<div class="countdown-edit-sub-tag">{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.currentCountdownValue" value=countdown.progress.current}}</div>
<div class="countdown-edit-sub-tag">{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.currentCountdownMax" value=countdown.progress.max}}</div>
<div class="countdown-edit-sub-tag">{{countdown.typeName}}</div>
<div class="countdown-edit-sub-tag">{{countdown.progress.typeName}}</div>
</div>
</div>
{{else}}
<div class="countdown-edit-input">
<label>{{localize "Name"}}</label>
<input type="text" name="{{concat "countdowns." id ".name"}}" value="{{countdown.name}}" />
</div>
{{/unless}}
<div class="countdown-edit-tools {{#if countdown.editing}}same-row{{/if}}">
<a data-action="toggleCountdownEdit" data-countdown-id="{{id}}"><i class="fa-solid {{#unless countdown.editing}}fa-pen-to-square{{else}}fa-check{{/unless}}"></i></a>
<a data-action="editCountdownOwnership" data-countdown-id="{{id}}"><i class="fa-solid fa-users"></i></a>
<a data-action="removeCountdown" data-countdown-id="{{id}}"><i class="fa-solid fa-trash"></i></a>
</div>
</div>
{{#if countdown.editing}}
<div class="countdown-edit-subrow">
<div class="countdown-edit-input tiny">
<label>{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.current"}}</label>
<input type="text" name="{{concat "countdowns." id ".progress.current"}}" value="{{countdown.progress.current}}" />
</div>
<div class="countdown-edit-input tiny">
<label>{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.max"}}</label>
<input type="text" name="{{concat "countdowns." id ".progress.max"}}" value="{{countdown.progress.max}}" />
</div>
<div class="countdown-edit-input">
<label>{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.category"}}</label>
<select name="{{concat "countdowns." id ".type"}}">
{{selectOptions ../countdownBaseTypes selected=countdown.type valueAttr="id" labelAttr="name" localize=true}}
</select>
</div>
<div class="countdown-edit-input">
<label>{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.type"}}</label>
<select name="{{concat "countdowns." id ".progress.type"}}">
{{selectOptions ../countdownTypes selected=countdown.progress.type valueAttr="id" labelAttr="label" localize=true}}
</select>
</div>
</div>
{{/if}}
{{/each}}
</div>
</div>
</div>