120 - Countdowns (#158)

* Added the shell of the Countdown application

* Added countdown automation

* Fixed overflow layout and added confirmation on countdown removal

* Added ownership to countdowns
This commit is contained in:
WBHarry 2025-06-21 21:37:22 +02:00 committed by GitHub
parent 3464717958
commit c15d55a505
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1222 additions and 137 deletions

View file

@ -12,4 +12,4 @@ export { default as DhpWeapon } from './sheets/items/weapon.mjs';
export { default as DhpArmor } from './sheets/items/armor.mjs';
export { default as DhpChatMessage } from './chatMessage.mjs';
export { default as DhpEnvironment } from './sheets/environment.mjs';
export { default as DhActiveEffectConfig } from './sheets/activeEffectConfig.mjs';
export { default as DhActiveEffectConfig } from './sheets/activeEffectConfig.mjs';

View file

@ -10,9 +10,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
/* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */
const html = await super.renderHTML();
if (
this.type === 'dualityRoll'
) {
if (this.type === 'dualityRoll') {
html.classList.add('duality');
const dualityResult = this.system.dualityResult;
if (dualityResult === DHDualityRoll.dualityResult.hope) html.classList.add('hope');

View file

@ -0,0 +1,343 @@
import { countdownTypes } from '../config/generalConfig.mjs';
import { GMUpdateEvent, RefreshType, socketEvent } from '../helpers/socket.mjs';
import OwnershipSelection from './ownershipSelection.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(basePath) {
super({});
this.basePath = basePath;
}
get title() {
return game.i18n.format('DAGGERHEART.Countdown.Title', {
type: game.i18n.localize(`DAGGERHEART.Countdown.Types.${this.basePath}`)
});
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'dh-style', 'countdown'],
tag: 'form',
position: { width: 740, height: 700 },
window: {
frame: true,
title: 'Countdowns',
resizable: true,
minimizable: true
},
actions: {
addCountdown: this.addCountdown,
removeCountdown: this.removeCountdown,
editImage: this.onEditImage,
openOwnership: this.openOwnership,
openCountdownOwnership: this.openCountdownOwnership
},
form: { handler: this.updateData, submitOnChange: true }
};
static PARTS = {
countdowns: {
template: 'systems/daggerheart/templates/views/countdowns.hbs',
scrollable: ['.expanded-view']
}
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelectorAll('.mini-countdown-container').forEach(element => {
element.addEventListener('click', event => this.updateCountdownValue.bind(this)(event, true));
element.addEventListener('contextmenu', event => this.updateCountdownValue.bind(this)(event, false));
});
}
async _onFirstRender(context, options) {
super._onFirstRender(context, options);
this.element.querySelector('.expanded-view').classList.toggle('hidden');
this.element.querySelector('.minimized-view').classList.toggle('hidden');
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
const countdownData = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath];
context.isGM = game.user.isGM;
context.base = this.basePath;
context.canCreate = countdownData.playerOwnership[game.user.id].value === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER;
context.source = {
...countdownData,
countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => {
const countdown = countdownData.countdowns[key];
const ownershipValue = countdown.playerOwnership[game.user.id].value;
if (ownershipValue > CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) {
acc[key] = { ...countdown, canEdit: ownershipValue === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER };
}
return acc;
}, {})
};
context.systemFields = countdownData.schema.fields;
context.countdownFields = context.systemFields.countdowns.element.fields;
context.minimized = this.minimized || _options.isFirstRender;
return context;
}
static async updateData(event, _, formData) {
const data = foundry.utils.expandObject(formData.object);
const newSetting = foundry.utils.mergeObject(
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns).toObject(),
data
);
if (game.user.isGM) {
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, newSetting);
this.render();
} else {
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: SYSTEM.SETTINGS.gameSettings.Countdowns,
update: newSetting
}
});
}
}
async minimize() {
await super.minimize();
this.element.querySelector('.expanded-view').classList.toggle('hidden');
this.element.querySelector('.minimized-view').classList.toggle('hidden');
}
async maximize() {
if (this.minimized) {
const settings = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath];
if (settings.playerOwnership[game.user.id].value <= CONST.DOCUMENT_OWNERSHIP_LEVELS.LIMITED) {
ui.notifications.info(game.i18n.localize('DAGGERHEART.Countdown.Notifications.LimitedOwnership'));
return;
}
this.element.querySelector('.expanded-view').classList.toggle('hidden');
this.element.querySelector('.minimized-view').classList.toggle('hidden');
}
await super.maximize();
}
async updateSetting(update) {
if (game.user.isGM) {
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, update);
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.Countdown,
application: `${this.basePath}-countdowns`
}
});
this.render();
} else {
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: SYSTEM.SETTINGS.gameSettings.Countdowns,
update: update,
refresh: { refreshType: RefreshType.Countdown, application: `${this.basePath}-countdowns` }
}
});
}
}
static onEditImage(_, target) {
const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath];
const current = setting.countdowns[target.dataset.countdown].img;
const fp = new FilePicker({
current,
type: 'image',
callback: async path => this.updateImage.bind(this)(path, target.dataset.countdown),
top: this.position.top + 40,
left: this.position.left + 10
});
return fp.browse();
}
async updateImage(path, countdown) {
const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns);
await setting.updateSource({
[`${this.basePath}.countdowns.${countdown}.img`]: path
});
await this.updateSetting(setting);
}
static openOwnership(_, target) {
new Promise((resolve, reject) => {
const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath];
const ownership = { default: setting.ownership.default, players: setting.playerOwnership };
new OwnershipSelection(resolve, reject, this.title, ownership).render(true);
}).then(async ownership => {
const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns);
await setting.updateSource({
[`${this.basePath}.ownership`]: ownership
});
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, setting.toObject());
this.render();
});
}
static openCountdownOwnership(_, target) {
const countdownId = target.dataset.countdown;
new Promise((resolve, reject) => {
const countdown = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath]
.countdowns[countdownId];
const ownership = { default: countdown.ownership.default, players: countdown.playerOwnership };
new OwnershipSelection(resolve, reject, countdown.name, ownership).render(true);
}).then(async ownership => {
const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns);
await setting.updateSource({
[`${this.basePath}.countdowns.${countdownId}.ownership`]: ownership
});
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, setting);
this.render();
});
}
async updateCountdownValue(event, increase) {
const countdownSetting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns);
const countdown = countdownSetting[this.basePath].countdowns[event.currentTarget.dataset.countdown];
if (countdown.playerOwnership[game.user.id] < CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER) {
return;
}
const currentValue = countdown.progress.current;
if (increase && currentValue === countdown.progress.max) return;
if (!increase && currentValue === 0) return;
await countdownSetting.updateSource({
[`${this.basePath}.countdowns.${event.currentTarget.dataset.countdown}.progress.current`]: increase
? currentValue + 1
: currentValue - 1
});
await this.updateSetting(countdownSetting.toObject());
}
static async addCountdown() {
const countdownSetting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns);
await countdownSetting.updateSource({
[`${this.basePath}.countdowns.${foundry.utils.randomID()}`]: {
name: game.i18n.localize('DAGGERHEART.Countdown.NewCountdown'),
ownership: game.user.isGM
? {}
: {
players: {
[game.user.id]: { type: CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER }
}
}
}
});
await this.updateSetting(countdownSetting.toObject());
}
static async removeCountdown(_, target) {
const countdownSetting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns);
const countdownName = countdownSetting[this.basePath].countdowns[target.dataset.countdown].name;
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize('DAGGERHEART.Countdown.RemoveCountdownTitle')
},
content: game.i18n.format('DAGGERHEART.Countdown.RemoveCountdownText', { name: countdownName })
});
if (!confirmed) return;
await countdownSetting.updateSource({ [`${this.basePath}.countdowns.-=${target.dataset.countdown}`]: null });
await this.updateSetting(countdownSetting.toObject());
}
async open() {
await this.render(true);
if (
Object.keys(game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath].countdowns)
.length > 0
) {
this.minimize();
}
}
}
export class NarrativeCountdowns extends Countdowns {
constructor() {
super('narrative');
}
static DEFAULT_OPTIONS = {
id: 'narrative-countdowns'
};
}
export class EncounterCountdowns extends Countdowns {
constructor() {
super('encounter');
}
static DEFAULT_OPTIONS = {
id: 'encounter-countdowns'
};
}
export const registerCountdownApplicationHooks = () => {
const updateCountdowns = async shouldIncrease => {
if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation).countdowns) {
const countdownSetting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns);
for (let countdownCategoryKey in countdownSetting) {
const countdownCategory = countdownSetting[countdownCategoryKey];
for (let countdownKey in countdownCategory.countdowns) {
const countdown = countdownCategory.countdowns[countdownKey];
if (shouldIncrease(countdown)) {
await countdownSetting.updateSource({
[`${countdownCategoryKey}.countdowns.${countdownKey}.progress.current`]:
countdown.progress.current + 1
});
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, countdownSetting);
foundry.applications.instances.get(`${countdownCategoryKey}-countdowns`)?.render();
}
}
}
}
};
Hooks.on(SYSTEM.HOOKS.characterAttack, async () => {
updateCountdowns(countdown => {
return (
countdown.progress.type.value === countdownTypes.characterAttack.id &&
countdown.progress.current < countdown.progress.max
);
});
});
Hooks.on(SYSTEM.HOOKS.spotlight, async () => {
updateCountdowns(countdown => {
return (
countdown.progress.type.value === countdownTypes.spotlight.id &&
countdown.progress.current < countdown.progress.max
);
});
});
};

View file

@ -0,0 +1,72 @@
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class OwnershipSelection extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(resolve, reject, name, ownership) {
super({});
this.resolve = resolve;
this.reject = reject;
this.name = name;
this.ownership = ownership;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'views', 'ownership-selection'],
position: {
width: 600,
height: 'auto'
},
form: { handler: this.updateData }
};
static PARTS = {
selection: {
template: 'systems/daggerheart/templates/views/ownershipSelection.hbs'
}
};
get title() {
return game.i18n.format('DAGGERHEART.OwnershipSelection.Title', { name: this.name });
}
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,
name: user.name,
ownership: this.ownership.players[x].value
};
}
return acc;
}, {})
};
return context;
}
static async updateData(event, _, formData) {
const { ownership } = foundry.utils.expandObject(formData.object);
this.resolve(ownership);
this.close(true);
}
async close(fromSave) {
if (!fromSave) {
this.reject();
}
await super.close();
}
}

View file

@ -1,4 +1,5 @@
import { defaultLevelTiers, DhLevelTiers } from '../data/levelTier.mjs';
import DhCountdowns from '../data/countdowns.mjs';
import {
DhAppearance,
DhAutomation,
@ -130,4 +131,10 @@ const registerNonConfigSettings = () => {
ui.combat.render({ force: true });
}
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, {
scope: 'world',
config: false,
type: DhCountdowns
});
};

View file

@ -371,7 +371,11 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
static async attackRoll(event, button) {
const weapon = await fromUuid(button.dataset.weapon);
if (!weapon) return;
weapon.use(event);
const wasUsed = await weapon.use(event);
if (wasUsed) {
Hooks.callAll(SYSTEM.HOOKS.characterAttack, {});
}
}
static openLevelUp() {

View file

@ -364,6 +364,20 @@ export const abilityCosts = {
}
};
export const countdownTypes = {
spotlight: {
id: 'spotlight',
label: 'DAGGERHEART.Countdown.Type.Spotlight'
},
characterAttack: {
id: 'characterAttack',
label: 'DAGGERHEART.Countdown.Type.CharacterAttack'
},
custom: {
id: 'custom',
label: 'DAGGERHEART.Countdown.Type.Custom'
}
};
export const rollTypes = {
weapon: {
id: 'weapon',

View file

@ -0,0 +1,4 @@
export const hooks = {
characterAttack: 'characterAttackHook',
spotlight: 'spotlightHook'
};

View file

@ -26,7 +26,8 @@ export const gameSettings = {
Resources: {
Fear: 'ResourcesFear'
},
LevelTiers: 'LevelTiers'
LevelTiers: 'LevelTiers',
Countdowns: 'Countdowns'
};
export const DualityRollColor = {

View file

@ -3,6 +3,7 @@ import * as DOMAIN from './domainConfig.mjs';
import * as ACTOR from './actorConfig.mjs';
import * as ITEM from './itemConfig.mjs';
import * as SETTINGS from './settingsConfig.mjs';
import { hooks as HOOKS } from './hooksConfig.mjs';
import * as EFFECTS from './effectConfig.mjs';
import * as ACTIONS from './actionConfig.mjs';
@ -15,6 +16,7 @@ export const SYSTEM = {
ACTOR,
ITEM,
SETTINGS,
HOOKS,
EFFECTS,
ACTIONS,
ACTIONS
};

View file

@ -1,18 +1,13 @@
import DHAbilityUse from "./abilityUse.mjs";
import DHAdversaryRoll from "./adversaryRoll.mjs";
import DHDamageRoll from "./damageRoll.mjs";
import DHDualityRoll from "./dualityRoll.mjs";
import DHAbilityUse from './abilityUse.mjs';
import DHAdversaryRoll from './adversaryRoll.mjs';
import DHDamageRoll from './damageRoll.mjs';
import DHDualityRoll from './dualityRoll.mjs';
export {
DHAbilityUse,
DHAdversaryRoll,
DHDamageRoll,
DHDualityRoll,
}
export { DHAbilityUse, DHAdversaryRoll, DHDamageRoll, DHDualityRoll };
export const config = {
abilityUse: DHAbilityUse,
adversaryRoll: DHAdversaryRoll,
damageRoll: DHDamageRoll,
dualityRoll: DHDualityRoll,
};
abilityUse: DHAbilityUse,
adversaryRoll: DHAdversaryRoll,
damageRoll: DHDamageRoll,
dualityRoll: DHDualityRoll
};

139
module/data/countdowns.mjs Normal file
View file

@ -0,0 +1,139 @@
import { countdownTypes } from '../config/generalConfig.mjs';
import { RefreshType, socketEvent } from '../helpers/socket.mjs';
export default class DhCountdowns extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
narrative: new fields.EmbeddedDataField(DhCountdownData),
encounter: new fields.EmbeddedDataField(DhCountdownData)
};
}
static CountdownCategories = { narrative: 'narrative', combat: 'combat' };
}
class DhCountdownData extends foundry.abstract.DataModel {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.Countdown']; // Nots ure why this won't work. Setting labels manually for now
static defineSchema() {
const fields = foundry.data.fields;
return {
countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhCountdown)),
ownership: new fields.SchemaField({
default: 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
})
})
)
})
};
}
get playerOwnership() {
return Array.from(game.users).reduce((acc, user) => {
acc[user.id] = {
value: user.isGM
? CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER
: this.ownership.players[user.id] && this.ownership.players[user.id].type !== -1
? this.ownership.players[user.id].type
: this.ownership.default,
isGM: user.isGM
};
return acc;
}, {});
}
}
class DhCountdown extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
name: new fields.StringField({
required: true,
label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.name.label'
}),
img: new fields.FilePathField({
categories: ['IMAGE'],
base64: false,
initial: 'icons/magic/time/hourglass-yellow-green.webp'
}),
ownership: new fields.SchemaField({
default: 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
})
})
)
}),
progress: new fields.SchemaField({
current: new fields.NumberField({
required: true,
integer: true,
initial: 0,
label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.progress.current.label'
}),
max: new fields.NumberField({
required: true,
integer: true,
initial: 1,
label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.progress.max.label'
}),
type: new fields.SchemaField({
value: new fields.StringField({
required: true,
choices: countdownTypes,
initial: countdownTypes.spotlight.id,
label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.progress.type.value.label'
}),
label: new fields.StringField({
label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.progress.type.label.label'
})
})
})
};
}
get playerOwnership() {
return Array.from(game.users).reduce((acc, user) => {
acc[user.id] = {
value: user.isGM
? CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER
: this.ownership.players[user.id] && this.ownership.players[user.id].type !== -1
? this.ownership.players[user.id].type
: this.ownership.default,
isGM: user.isGM
};
return acc;
}, {});
}
}
export const registerCountdownHooks = () => {
Hooks.on(socketEvent.Refresh, ({ refreshType, application }) => {
if (refreshType === RefreshType.Countdown) {
foundry.applications.instances.get(application)?.render();
return false;
}
});
};

View file

@ -10,45 +10,44 @@
const fields = foundry.data.fields;
export default class BaseDataItem extends foundry.abstract.TypeDataModel {
/** @returns {ItemDataModelMetadata}*/
static get metadata() {
return {
label: "Base Item",
type: "base",
hasDescription: false,
isQuantifiable: false,
};
}
/** @returns {ItemDataModelMetadata}*/
static get metadata() {
return {
label: 'Base Item',
type: 'base',
hasDescription: false,
isQuantifiable: false
};
}
/** @inheritDoc */
static defineSchema() {
const schema = {};
/** @inheritDoc */
static defineSchema() {
const schema = {};
if (this.metadata.hasDescription)
schema.description = new fields.HTMLField({ required: true, nullable: true });
if (this.metadata.hasDescription) schema.description = new fields.HTMLField({ required: true, nullable: true });
if (this.metadata.isQuantifiable)
schema.quantity = new fields.NumberField({ integer: true, initial: 1, min: 0, required: true });
if (this.metadata.isQuantifiable)
schema.quantity = new fields.NumberField({ integer: true, initial: 1, min: 0, required: true });
return schema;
}
return schema;
}
/**
* Convenient access to the item's actor, if it exists.
* @returns {foundry.documents.Actor | null}
*/
get actor() {
return this.parent.actor;
}
/**
* Convenient access to the item's actor, if it exists.
* @returns {foundry.documents.Actor | null}
*/
get actor() {
return this.parent.actor;
}
/**
* Obtain a data object used to evaluate any dice rolls associated with this Item Type
* @param {object} [options] - Options which modify the getRollData method.
* @returns {object}
*/
getRollData(options = {}) {
const actorRollData = this.actor?.getRollData() ?? {};
const data = { ...actorRollData, item: { ...this } };
return data;
}
}
/**
* Obtain a data object used to evaluate any dice rolls associated with this Item Type
* @param {object} [options] - Options which modify the getRollData method.
* @returns {object}
*/
getRollData(options = {}) {
const actorRollData = this.actor?.getRollData() ?? {};
const data = { ...actorRollData, item: { ...this } };
return data;
}
}

View file

@ -19,16 +19,16 @@ export default class DHClass extends BaseDataItem {
return {
...super.defineSchema(),
domains: new fields.ArrayField(new fields.StringField(), { max: 2 }),
classItems: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}),
classItems: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
evasion: new fields.NumberField({ initial: 0, integer: true }),
hopeFeatures: new foundry.data.fields.ArrayField(new ActionField()),
classFeatures: new foundry.data.fields.ArrayField(new ActionField()),
subclasses: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}),
subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
inventory: new fields.SchemaField({
take: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}),
choiceA: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}),
choiceB: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}),
take: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
choiceA: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
choiceB: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false })
}),
characterGuide: new fields.SchemaField({
suggestedTraits: new fields.SchemaField({

View file

@ -1,14 +1,14 @@
import BaseDataItem from "./base.mjs";
import BaseDataItem from './base.mjs';
import ActionField from '../fields/actionField.mjs';
export default class DHConsumable extends BaseDataItem {
/** @inheritDoc */
/** @inheritDoc */
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
label: "TYPES.Item.consumable",
type: "consumable",
label: 'TYPES.Item.consumable',
type: 'consumable',
hasDescription: true,
isQuantifiable: true,
isQuantifiable: true
});
}

View file

@ -5,10 +5,10 @@ export default class DHMiscellaneous extends BaseDataItem {
/** @inheritDoc */
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
label: "TYPES.Item.miscellaneous",
type: "miscellaneous",
label: 'TYPES.Item.miscellaneous',
type: 'miscellaneous',
hasDescription: true,
isQuantifiable: true,
isQuantifiable: true
});
}

View file

@ -5,7 +5,7 @@ import BaseDataItem from './base.mjs';
const featureSchema = () => {
return new foundry.data.fields.SchemaField({
name: new foundry.data.fields.StringField({ required: true }),
effects: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}),
effects: new ForeignDocumentUUIDArrayField({ type: 'ActiveEffect', required: false }),
actions: new foundry.data.fields.ArrayField(new ActionField())
});
};

View file

@ -5,7 +5,8 @@ export default class DhAutomation extends foundry.abstract.DataModel {
const fields = foundry.data.fields;
return {
hope: new fields.BooleanField({ required: true, initial: false }),
actionPoints: new fields.BooleanField({ required: true, initial: false })
actionPoints: new fields.BooleanField({ required: true, initial: false }),
countdowns: new fields.BooleanField({ requireD: true, initial: false })
};
}
}

View file

@ -301,7 +301,7 @@ export default class DhpActor extends Actor {
);
if (this.type === 'character') {
const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope);
const automateHope = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation).hope;
if (automateHope && result.hopeUsed) {
await this.update({
@ -330,7 +330,7 @@ export default class DhpActor extends Actor {
hope = roll.dice[0].results[0].result;
fear = roll.dice[1].results[0].result;
if (
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope) &&
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation).hope &&
config.roll.type === 'action'
) {
if (hope > fear) {

View file

@ -1,20 +1,68 @@
export function handleSocketEvent({ action = null, data = {} } = {}) {
switch (action) {
case socketEvent.GMUpdate:
Hooks.callAll(socketEvent.GMUpdate, data.action, data.uuid, data.update);
Hooks.callAll(socketEvent.GMUpdate, data);
break;
case socketEvent.DhpFearUpdate:
Hooks.callAll(socketEvent.DhpFearUpdate);
break;
case socketEvent.Refresh:
Hooks.call(socketEvent.Refresh, data);
break;
}
}
export const socketEvent = {
GMUpdate: 'DhpGMUpdate',
DhpFearUpdate: 'DhpFearUpdate'
GMUpdate: 'DhGMUpdate',
Refresh: 'DhRefresh',
DhpFearUpdate: 'DhFearUpdate'
};
export const GMUpdateEvent = {
UpdateDocument: 'DhpGMUpdateDocument',
UpdateFear: 'DhpUpdateFear'
UpdateDocument: 'DhGMUpdateDocument',
UpdateSetting: 'DhGMUpdateSetting',
UpdateFear: 'DhGMUpdateFear'
};
export const RefreshType = {
Countdown: 'DhCoundownRefresh'
};
export const registerSocketHooks = () => {
Hooks.on(socketEvent.GMUpdate, async data => {
if (game.user.isGM) {
const document = data.uuid ? await fromUuid(data.uuid) : null;
switch (data.action) {
case GMUpdateEvent.UpdateDocument:
if (document && data.update) {
await document.update(data.update);
}
break;
case GMUpdateEvent.UpdateSetting:
if (game.user.isGM) {
await game.settings.set(SYSTEM.id, data.uuid, data.update);
}
break;
case GMUpdateEvent.UpdateFear:
if (game.user.isGM) {
await game.settings.set(
SYSTEM.id,
SYSTEM.SETTINGS.gameSettings.Resources.Fear,
Math.max(Math.min(data.update, 6), 0)
);
Hooks.callAll(socketEvent.DhpFearUpdate);
await game.socket.emit(`system.${SYSTEM.id}`, { action: socketEvent.DhpFearUpdate });
}
break;
}
if (data.refresh) {
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.Refresh,
data: data.refresh
});
Hooks.call(socketEvent.Refresh, data.refresh);
}
}
});
};

View file

@ -1,9 +1,12 @@
import { EncounterCountdowns } from '../applications/countdowns.mjs';
export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
static DEFAULT_OPTIONS = {
actions: {
requestSpotlight: this.requestSpotlight,
toggleSpotlight: this.toggleSpotlight,
setActionTokens: this.setActionTokens
setActionTokens: this.setActionTokens,
openCountdowns: this.openCountdowns
}
};
@ -83,6 +86,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
.map(x => x.id)
.indexOf(combatantId);
if (this.viewed.turn !== toggleTurn) Hooks.callAll(SYSTEM.HOOKS.spotlight, {});
await this.viewed.update({ turn: this.viewed.turn === toggleTurn ? null : toggleTurn });
await combatant.update({ 'system.spotlight.requesting': false });
}
@ -97,4 +102,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
await combatant.update({ 'system.actionTokens': newIndex });
this.render();
}
static openCountdowns() {
new EncounterCountdowns().open();
}
}