diff --git a/daggerheart.mjs b/daggerheart.mjs index 27979ae1..fcff2bbd 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -3,11 +3,10 @@ import * as applications from './module/applications/_module.mjs'; import * as models from './module/data/_module.mjs'; import * as documents from './module/documents/_module.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; -import DhpCombatTracker from './module/ui/combatTracker.mjs'; +import DhCombatTracker from './module/ui/combatTracker.mjs'; import { GMUpdateEvent, handleSocketEvent, socketEvent } from './module/helpers/socket.mjs'; import { registerDHSettings } from './module/applications/settings.mjs'; import DhpChatLog from './module/ui/chatLog.mjs'; -import DhpPlayers from './module/ui/players.mjs'; import DhpRuler from './module/ui/ruler.mjs'; import DhpTokenRuler from './module/ui/tokenRuler.mjs'; import { dualityRollEnricher } from './module/enrichers/DualityRollEnricher.mjs'; @@ -74,11 +73,11 @@ Hooks.once('init', () => { Actors.registerSheet(SYSTEM.id, applications.DhpEnvironment, { types: ['environment'], makeDefault: true }); CONFIG.Combat.dataModels = { - base: models.DhpCombat + base: models.DhCombat }; CONFIG.Combatant.dataModels = { - base: models.DhpCombatant + base: models.DhCombatant }; CONFIG.ChatMessage.dataModels = { @@ -91,7 +90,7 @@ Hooks.once('init', () => { CONFIG.Canvas.rulerClass = DhpRuler; CONFIG.Combat.documentClass = documents.DhpCombat; - CONFIG.ui.combat = DhpCombatTracker; + CONFIG.ui.combat = DhCombatTracker; CONFIG.ui.chat = DhpChatLog; // CONFIG.ui.players = DhpPlayers; CONFIG.Token.rulerClass = DhpTokenRuler; @@ -111,8 +110,8 @@ Hooks.once('init', () => { Hooks.on('ready', () => { ui.resources = new CONFIG.ui.resources(); - ui.resources.render({force: true}); -}) + ui.resources.render({ force: true }); +}); Hooks.once('dicesoniceready', () => {}); diff --git a/lang/en.json b/lang/en.json index 75a803e1..96185acb 100755 --- a/lang/en.json +++ b/lang/en.json @@ -61,6 +61,13 @@ "outline": "Outline", "edge": "Edge" } + }, + "VariantRules": { + "title": "Variant Rules", + "label": "Variant Rules", + "hint": "Apply variant rules from the Daggerheart system", + "name": "Variant Rules", + "actionTokens": "Action Tokens" } }, "Automation": { @@ -101,6 +108,12 @@ "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": { "Name": "Duality Roll Colour Scheme", "Hint": "The display type for Duality Rolls", @@ -329,6 +342,12 @@ "grimoire": "Grimoire" } }, + "Combat": { + "giveSpotlight": "Give The Spotlight", + "requestSpotlight": "Request The Spotlight", + "requestingSpotlight": "Requesting The Spotlight", + "combatStarted": "Active" + }, "LevelUp": { "Tier1": { "Label": "Level 2-4", diff --git a/module/applications/settings.mjs b/module/applications/settings.mjs index 4a885d17..291b0882 100644 --- a/module/applications/settings.mjs +++ b/module/applications/settings.mjs @@ -1,5 +1,7 @@ import DhAppearance from '../data/settings/Appearance.mjs'; import DHAppearanceSettings from './settings/appearanceSettings.mjs'; +import DhVariantRules from '../data/settings/VariantRules.mjs'; +import DHVariantRuleSettings from './settings/variantRuleSettings.mjs'; class DhpAutomationSettings extends FormApplication { constructor(object = {}, options = {}) { @@ -181,7 +183,8 @@ export const registerDHSettings = () => { type: Number, default: 0, 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, default: 12, 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, type: String, choices: { - 'token': 'Tokens', - 'bar': 'Bar', - 'hide': 'Hide' + 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}); + if (ui.resources) { + if (value === 'hide') ui.resources.close({ allowed: 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, { scope: 'client', config: false, @@ -291,4 +301,13 @@ export const registerDHSettings = () => { type: DHAppearanceSettings, 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 + }); }; diff --git a/module/applications/settings/variantRuleSettings.mjs b/module/applications/settings/variantRuleSettings.mjs new file mode 100644 index 00000000..101d3e42 --- /dev/null +++ b/module/applications/settings/variantRuleSettings.mjs @@ -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(); + } +} diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index 26de2a48..b2e17549 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -10,6 +10,10 @@ export const menu = { Range: { Name: 'GameSettingsRange', Icon: 'fa-solid fa-ruler' + }, + VariantRules: { + Name: 'GameSettingsVariantrules', + Icon: 'fa-solid fa-scale-balanced' } }; @@ -27,5 +31,6 @@ export const gameSettings = { AbilityArray: 'AbilityArray', RangeMeasurement: 'RangeMeasurement' }, - appearance: 'Appearance' + appearance: 'Appearance', + variantRules: 'VariantRules' }; diff --git a/module/data/_module.mjs b/module/data/_module.mjs index 4822229f..74b8b5dd 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -1,8 +1,8 @@ export { default as DhpPC } from './pc.mjs'; export { default as DhpClass } from './class.mjs'; export { default as DhpSubclass } from './subclass.mjs'; -export { default as DhpCombat } from './combat.mjs'; -export { default as DhpCombatant } from './combatant.mjs'; +export { default as DhCombat } from './combat.mjs'; +export { default as DhCombatant } from './combatant.mjs'; export { default as DhpAdversary } from './adversary.mjs'; export { default as DhpFeature } from './feature.mjs'; export { default as DhpDomainCard } from './domainCard.mjs'; @@ -17,3 +17,6 @@ export { default as DhpAdversaryRoll } from './adversaryRoll.mjs'; export { default as DhpDamageRoll } from './damageRoll.mjs'; export { default as DhpAbilityUse } from './abilityUse.mjs'; export { default as DhpEnvironment } from './environment.mjs'; + +export * as items from './item/_module.mjs'; +export * as messages from './chat-message/_modules.mjs'; diff --git a/module/data/combat.mjs b/module/data/combat.mjs index 3ad52b8b..60936003 100644 --- a/module/data/combat.mjs +++ b/module/data/combat.mjs @@ -1,9 +1,9 @@ -export default class DhpCombat extends foundry.abstract.TypeDataModel { +export default class DhCombat extends foundry.abstract.TypeDataModel { static defineSchema() { const fields = foundry.data.fields; return { actions: new fields.NumberField({ initial: 0, integer: true }), - activeCombatant: new fields.StringField({}) + started: new fields.BooleanField({ required: true, initial: false }) }; } } diff --git a/module/data/combatant.mjs b/module/data/combatant.mjs index 60c32db6..955087de 100644 --- a/module/data/combatant.mjs +++ b/module/data/combatant.mjs @@ -1,8 +1,12 @@ -export default class DhpCombatant extends foundry.abstract.TypeDataModel { +export default class DhCombatant extends foundry.abstract.TypeDataModel { static defineSchema() { const fields = foundry.data.fields; 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 }) }; } } diff --git a/module/data/settings/VariantRules.mjs b/module/data/settings/VariantRules.mjs new file mode 100644 index 00000000..2a1f948d --- /dev/null +++ b/module/data/settings/VariantRules.mjs @@ -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 = {}; +} diff --git a/module/documents/combat.mjs b/module/documents/combat.mjs index c7905605..25b9ed3c 100644 --- a/module/documents/combat.mjs +++ b/module/documents/combat.mjs @@ -1,44 +1,53 @@ -import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs'; - export default class DhpCombat extends Combat { - _sortCombatants(a, b) { - if (a.isNPC !== b.isNPC) { - const aVal = a.isNPC ? 0 : 1; - const bVal = b.isNPC ? 0 : 1; + get combatant() { + return this.combatants.contents.find(x => x.system.spotlight.active) ?? null; + } - return aVal - bVal; + async startCombat() { + this._playCombatSound('startEncounter'); + const updateData = { 'system.started': true }; + Hooks.callAll('combatStart', this, updateData); + await this.update(updateData); + return this; + } + + _sortCombatants(a, b) { + const aNPC = Number(a.isNPC); + const bNPC = Number(b.isNPC); + if (aNPC !== bNPC) { + return aNPC - bNPC; } return a.name.localeCompare(b.name); } - async useActionToken(combatantId) { - const automateActionPoints = await game.settings.get( - SYSTEM.id, - SYSTEM.SETTINGS.gameSettings.Automation.ActionPoints - ); + // async useActionToken(combatantId) { + // const automateActionPoints = await game.settings.get( + // SYSTEM.id, + // SYSTEM.SETTINGS.gameSettings.Automation.ActionPoints + // ); - if (game.user.isGM) { - if (this.system.actions < 1) return; + // if (game.user.isGM) { + // if (this.system.actions < 1) return; - const update = automateActionPoints - ? { 'system.activeCombatant': combatantId, 'system.actions': Math.max(this.system.actions - 1, 0) } - : { 'system.activeCombatant': combatantId }; + // const update = automateActionPoints + // ? { 'system.activeCombatant': combatantId, 'system.actions': Math.max(this.system.actions - 1, 0) } + // : { 'system.activeCombatant': combatantId }; - await this.update(update); - } else { - const update = automateActionPoints - ? { 'system.activeCombatant': combatantId, 'system.actions': this.system.actions + 1 } - : { 'system.activeCombatant': combatantId }; + // await this.update(update); + // } else { + // const update = automateActionPoints + // ? { 'system.activeCombatant': combatantId, 'system.actions': this.system.actions + 1 } + // : { 'system.activeCombatant': combatantId }; - await game.socket.emit(`system.${SYSTEM.id}`, { - action: socketEvent.GMUpdate, - data: { - action: GMUpdateEvent.UpdateDocument, - uuid: this.uuid, - update: update - } - }); - } - } + // await game.socket.emit(`system.${SYSTEM.id}`, { + // action: socketEvent.GMUpdate, + // data: { + // action: GMUpdateEvent.UpdateDocument, + // uuid: this.uuid, + // update: update + // } + // }); + // } + // } } diff --git a/module/ui/combatTracker.mjs b/module/ui/combatTracker.mjs index 86002cb6..e07ff327 100644 --- a/module/ui/combatTracker.mjs +++ b/module/ui/combatTracker.mjs @@ -1,199 +1,96 @@ -import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs'; - -export default class DhpCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { - constructor(data, context) { - super(data, context); - - Hooks.on(socketEvent.DhpFearUpdate, this.onFearUpdate); - } - - get template() { - return 'systems/daggerheart/templates/ui/combatTracker.hbs'; - } - - activateListeners(html) { - super.activateListeners(html); - html.on('click', '.token-action-tokens .use-action-token', this.useActionToken.bind(this)); - html.on('click', '.encounter-gm-resources .trade-actions', this.tradeActions.bind(this)); - html.on('click', '.encounter-gm-resources .trade-fear', this.tradeFear.bind(this)); - html.on('click', '.encounter-gm-resources .icon-button.up', this.increaseResource.bind(this)); - html.on('click', '.encounter-gm-resources .icon-button.down', this.decreaseResource.bind(this)); - } - - async useActionToken(event) { - event.stopPropagation(); - const combatant = event.currentTarget.dataset.combatant; - await game.combat.useActionToken(combatant); - } - - async tradeActions(event) { - if (event.currentTarget.classList.contains('disabled')) return; - - const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); - const value = currentFear + 1; - - if (value <= 6) { - Hooks.callAll(socketEvent.GMUpdate, GMUpdateEvent.UpdateFear, null, value); - await game.socket.emit(`system.${SYSTEM.id}`, { - action: socketEvent.GMUpdate, - data: { action: GMUpdateEvent.UpdateFear, update: value } - }); - await game.combat.update({ 'system.actions': game.combat.system.actions - 2 }); +export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { + static DEFAULT_OPTIONS = { + actions: { + requestSpotlight: this.requestSpotlight, + toggleSpotlight: this.toggleSpotlight, + setActionTokens: this.setActionTokens } - } - - async tradeFear() { - if (event.currentTarget.classList.contains('disabled')) return; - - const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); - const value = currentFear - 1; - if (value >= 0) { - Hooks.callAll(socketEvent.GMUpdate, GMUpdateEvent.UpdateFear, null, value); - await game.socket.emit(`system.${SYSTEM.id}`, { - action: socketEvent.GMUpdate, - data: { action: GMUpdateEvent.UpdateFear, update: value } - }); - await game.combat.update({ 'system.actions': game.combat.system.actions + 2 }); - } - } - - async increaseResource(event) { - if (event.currentTarget.dataset.type === 'action') { - await game.combat.update({ 'system.actions': game.combat.system.actions + 1 }); - } - - const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); - const value = currentFear + 1; - if (event.currentTarget.dataset.type === 'fear' && value <= 6) { - Hooks.callAll(socketEvent.GMUpdate, GMUpdateEvent.UpdateFear, null, value); - await game.socket.emit(`system.${SYSTEM.id}`, { - action: socketEvent.GMUpdate, - data: { action: GMUpdateEvent.UpdateFear, update: value } - }); - } - - this.render(); - } - - async decreaseResource(event) { - if (event.currentTarget.dataset.type === 'action' && game.combat.system.actions - 1 >= 0) { - await game.combat.update({ 'system.actions': game.combat.system.actions - 1 }); - } - - const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); - const value = currentFear - 1; - if (event.currentTarget.dataset.type === 'fear' && value >= 0) { - Hooks.callAll(socketEvent.GMUpdate, GMUpdateEvent.UpdateFear, null, value); - await game.socket.emit(`system.${SYSTEM.id}`, { - action: socketEvent.GMUpdate, - data: { action: GMUpdateEvent.UpdateFear, update: value } - }); - } - - this.render(); - } - - async getData(options = {}) { - let context = await super.getData(options); - - // Get the combat encounters possible for the viewed Scene - const combat = this.viewed; - const hasCombat = combat !== null; - const combats = this.combats; - const currentIdx = combats.findIndex(c => c === combat); - const previousId = currentIdx > 0 ? combats[currentIdx - 1].id : null; - const nextId = currentIdx < combats.length - 1 ? combats[currentIdx + 1].id : null; - const settings = game.settings.get('core', Combat.CONFIG_SETTING); - - // Prepare rendering data - context = foundry.utils.mergeObject(context, { - combats: combats, - currentIndex: currentIdx + 1, - combatCount: combats.length, - hasCombat: hasCombat, - combat, - turns: [], - previousId, - nextId, - started: this.started, - control: false, - settings, - linked: combat?.scene !== null, - labels: {} - }); - context.labels.scope = game.i18n.localize(`COMBAT.${context.linked ? 'Linked' : 'Unlinked'}`); - if (!hasCombat) return context; - - // Format information about each combatant in the encounter - let hasDecimals = false; - const turns = []; - for (let [i, combatant] of combat.turns.entries()) { - if (!combatant.visible) continue; - - // Prepare turn data - const resource = - combatant.permission >= CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER ? combatant.resource : null; - const turn = { - id: combatant.id, - name: combatant.name, - img: await this._getCombatantThumbnail(combatant), - active: combatant.id === combat.system.activeCombatant, - owner: combatant.isOwner, - defeated: combatant.isDefeated, - hidden: combatant.hidden, - initiative: combatant.initiative, - hasRolled: combatant.initiative !== null, - hasResource: resource !== null, - resource: resource, - canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'), - playerCharacter: game.user?.character?.id === combatant.actor.id, - ownedByPlayer: combatant.hasPlayerOwner - }; - if (turn.initiative !== null && !Number.isInteger(turn.initiative)) hasDecimals = true; - turn.css = [turn.active ? 'active' : '', turn.hidden ? 'hidden' : '', turn.defeated ? 'defeated' : ''] - .join(' ') - .trim(); - - // Actor and Token status effects - turn.effects = new Set(); - if (combatant.token) { - combatant.token.effects.forEach(e => turn.effects.add(e)); - if (combatant.token.overlayEffect) turn.effects.add(combatant.token.overlayEffect); - } - if (combatant.actor) { - for (const effect of combatant.actor.temporaryEffects) { - if (effect.statuses.has(CONFIG.specialStatusEffects.DEFEATED)) turn.defeated = true; - else if (effect.icon) turn.effects.add(effect.icon); - } - } - turns.push(turn); - } - - // Format initiative numeric precision - const precision = CONFIG.Combat.initiative.decimals; - turns.forEach(t => { - if (t.initiative !== null) t.initiative = t.initiative.toFixed(hasDecimals ? precision : 0); - }); - - const fear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); - - // Merge update data for rendering - return foundry.utils.mergeObject(context, { - round: combat.round, - turn: combat.turn, - turns: turns, - control: combat.combatant?.players?.includes(game.user), - fear: fear - }); - } - - onFearUpdate = async () => { - this.render(true); }; - async close(options) { - Hooks.off(socketEvent.DhpFearUpdate, this.onFearUpdate); + static PARTS = { + header: { + template: 'systems/daggerheart/templates/ui/combat/combatTrackerHeader.hbs' + }, + tracker: { + template: 'systems/daggerheart/templates/ui/combat/combatTracker.hbs' + }, + footer: { + template: 'systems/daggerheart/templates/ui/combat/combatTrackerFooter.hbs' + } + }; - return super.close(options); + 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, isNPC: combatant.isNPC, system: combatant.system.toObject() }; + } + + _getCombatContextOptions() { + return [ + { + name: 'COMBAT.ClearMovementHistories', + icon: '', + condition: () => game.user.isGM && this.viewed?.combatants.size > 0, + callback: () => this.viewed.clearMovementHistories() + }, + { + name: 'COMBAT.Delete', + icon: '', + condition: () => game.user.isGM && !!this.viewed, + callback: () => this.viewed.endCombat() + } + ]; + } + + static async requestSpotlight(_, target) { + const { combatantId } = target.closest('[data-combatant-id]')?.dataset ?? {}; + const combatant = this.viewed.combatants.get(combatantId); + await combatant.update({ + 'system.spotlight': { + requesting: !combatant.system.spotlight.requesting + } + }); + + 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(); } } diff --git a/module/ui/combatTrackerOld.mjs b/module/ui/combatTrackerOld.mjs new file mode 100644 index 00000000..86002cb6 --- /dev/null +++ b/module/ui/combatTrackerOld.mjs @@ -0,0 +1,199 @@ +import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs'; + +export default class DhpCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { + constructor(data, context) { + super(data, context); + + Hooks.on(socketEvent.DhpFearUpdate, this.onFearUpdate); + } + + get template() { + return 'systems/daggerheart/templates/ui/combatTracker.hbs'; + } + + activateListeners(html) { + super.activateListeners(html); + html.on('click', '.token-action-tokens .use-action-token', this.useActionToken.bind(this)); + html.on('click', '.encounter-gm-resources .trade-actions', this.tradeActions.bind(this)); + html.on('click', '.encounter-gm-resources .trade-fear', this.tradeFear.bind(this)); + html.on('click', '.encounter-gm-resources .icon-button.up', this.increaseResource.bind(this)); + html.on('click', '.encounter-gm-resources .icon-button.down', this.decreaseResource.bind(this)); + } + + async useActionToken(event) { + event.stopPropagation(); + const combatant = event.currentTarget.dataset.combatant; + await game.combat.useActionToken(combatant); + } + + async tradeActions(event) { + if (event.currentTarget.classList.contains('disabled')) return; + + const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); + const value = currentFear + 1; + + if (value <= 6) { + Hooks.callAll(socketEvent.GMUpdate, GMUpdateEvent.UpdateFear, null, value); + await game.socket.emit(`system.${SYSTEM.id}`, { + action: socketEvent.GMUpdate, + data: { action: GMUpdateEvent.UpdateFear, update: value } + }); + await game.combat.update({ 'system.actions': game.combat.system.actions - 2 }); + } + } + + async tradeFear() { + if (event.currentTarget.classList.contains('disabled')) return; + + const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); + const value = currentFear - 1; + if (value >= 0) { + Hooks.callAll(socketEvent.GMUpdate, GMUpdateEvent.UpdateFear, null, value); + await game.socket.emit(`system.${SYSTEM.id}`, { + action: socketEvent.GMUpdate, + data: { action: GMUpdateEvent.UpdateFear, update: value } + }); + await game.combat.update({ 'system.actions': game.combat.system.actions + 2 }); + } + } + + async increaseResource(event) { + if (event.currentTarget.dataset.type === 'action') { + await game.combat.update({ 'system.actions': game.combat.system.actions + 1 }); + } + + const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); + const value = currentFear + 1; + if (event.currentTarget.dataset.type === 'fear' && value <= 6) { + Hooks.callAll(socketEvent.GMUpdate, GMUpdateEvent.UpdateFear, null, value); + await game.socket.emit(`system.${SYSTEM.id}`, { + action: socketEvent.GMUpdate, + data: { action: GMUpdateEvent.UpdateFear, update: value } + }); + } + + this.render(); + } + + async decreaseResource(event) { + if (event.currentTarget.dataset.type === 'action' && game.combat.system.actions - 1 >= 0) { + await game.combat.update({ 'system.actions': game.combat.system.actions - 1 }); + } + + const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); + const value = currentFear - 1; + if (event.currentTarget.dataset.type === 'fear' && value >= 0) { + Hooks.callAll(socketEvent.GMUpdate, GMUpdateEvent.UpdateFear, null, value); + await game.socket.emit(`system.${SYSTEM.id}`, { + action: socketEvent.GMUpdate, + data: { action: GMUpdateEvent.UpdateFear, update: value } + }); + } + + this.render(); + } + + async getData(options = {}) { + let context = await super.getData(options); + + // Get the combat encounters possible for the viewed Scene + const combat = this.viewed; + const hasCombat = combat !== null; + const combats = this.combats; + const currentIdx = combats.findIndex(c => c === combat); + const previousId = currentIdx > 0 ? combats[currentIdx - 1].id : null; + const nextId = currentIdx < combats.length - 1 ? combats[currentIdx + 1].id : null; + const settings = game.settings.get('core', Combat.CONFIG_SETTING); + + // Prepare rendering data + context = foundry.utils.mergeObject(context, { + combats: combats, + currentIndex: currentIdx + 1, + combatCount: combats.length, + hasCombat: hasCombat, + combat, + turns: [], + previousId, + nextId, + started: this.started, + control: false, + settings, + linked: combat?.scene !== null, + labels: {} + }); + context.labels.scope = game.i18n.localize(`COMBAT.${context.linked ? 'Linked' : 'Unlinked'}`); + if (!hasCombat) return context; + + // Format information about each combatant in the encounter + let hasDecimals = false; + const turns = []; + for (let [i, combatant] of combat.turns.entries()) { + if (!combatant.visible) continue; + + // Prepare turn data + const resource = + combatant.permission >= CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER ? combatant.resource : null; + const turn = { + id: combatant.id, + name: combatant.name, + img: await this._getCombatantThumbnail(combatant), + active: combatant.id === combat.system.activeCombatant, + owner: combatant.isOwner, + defeated: combatant.isDefeated, + hidden: combatant.hidden, + initiative: combatant.initiative, + hasRolled: combatant.initiative !== null, + hasResource: resource !== null, + resource: resource, + canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'), + playerCharacter: game.user?.character?.id === combatant.actor.id, + ownedByPlayer: combatant.hasPlayerOwner + }; + if (turn.initiative !== null && !Number.isInteger(turn.initiative)) hasDecimals = true; + turn.css = [turn.active ? 'active' : '', turn.hidden ? 'hidden' : '', turn.defeated ? 'defeated' : ''] + .join(' ') + .trim(); + + // Actor and Token status effects + turn.effects = new Set(); + if (combatant.token) { + combatant.token.effects.forEach(e => turn.effects.add(e)); + if (combatant.token.overlayEffect) turn.effects.add(combatant.token.overlayEffect); + } + if (combatant.actor) { + for (const effect of combatant.actor.temporaryEffects) { + if (effect.statuses.has(CONFIG.specialStatusEffects.DEFEATED)) turn.defeated = true; + else if (effect.icon) turn.effects.add(effect.icon); + } + } + turns.push(turn); + } + + // Format initiative numeric precision + const precision = CONFIG.Combat.initiative.decimals; + turns.forEach(t => { + if (t.initiative !== null) t.initiative = t.initiative.toFixed(hasDecimals ? precision : 0); + }); + + const fear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); + + // Merge update data for rendering + return foundry.utils.mergeObject(context, { + round: combat.round, + turn: combat.turn, + turns: turns, + control: combat.combatant?.players?.includes(game.user), + fear: fear + }); + } + + onFearUpdate = async () => { + this.render(true); + }; + + async close(options) { + Hooks.off(socketEvent.DhpFearUpdate, this.onFearUpdate); + + return super.close(options); + } +} diff --git a/styles/daggerheart.css b/styles/daggerheart.css index a99ffbd7..cb56dddb 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -1293,59 +1293,77 @@ .daggerheart.sheet.pc div[data-application-part] .sheet-body .inventory-container .inventory-item-list .inventory-row img { width: 32px; } -.combat-sidebar .encounter-gm-resources { - flex: 0; - display: flex; - justify-content: center; - padding: 8px 0; +.combat-sidebar .encounter-controls.combat { + justify-content: space-between; } -.combat-sidebar .encounter-gm-resources .gm-resource-controls { +.combat-sidebar .encounter-controls.combat .encounter-control-fear-container { display: flex; - flex-direction: column; + position: relative; align-items: center; - padding: 0 4px; justify-content: center; + color: black; } -.combat-sidebar .encounter-gm-resources .gm-resource-tools { - display: flex; - flex-direction: column; - justify-content: center; - padding: 0 5px 0 4px; +.combat-sidebar .encounter-controls.combat .encounter-control-fear-container .dice { + height: 24px; } -.combat-sidebar .encounter-gm-resources .gm-resource-tools i { - margin: 0 2px; +.combat-sidebar .encounter-controls.combat .encounter-control-fear-container .encounter-control-fear { + position: absolute; font-size: 16px; } -.combat-sidebar .encounter-gm-resources .gm-resource-tools i.disabled { - opacity: 0.6; +.combat-sidebar .encounter-controls.combat .encounter-control-fear-container .encounter-control-counter { + position: absolute; + right: -10px; + color: var(--color-text-secondary); } -.combat-sidebar .encounter-gm-resources .gm-resource-tools i:hover:not(.disabled) { - cursor: pointer; - filter: drop-shadow(0 0 3px red); +.combat-sidebar .encounter-controls.combat .control-buttons { + width: min-content; } -.combat-sidebar .encounter-gm-resources .gm-resource { - background: rgba(255, 255, 255, 0.1); - padding: 4px; +.combat-sidebar .token-actions { + align-self: stretch; + display: flex; + align-items: 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; - border: 2px solid black; - font-size: 20px; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + padding: 8px; + --button-size: 0; } -.combat-sidebar .token-action-tokens { - flex: 0 0 48px; - text-align: center; +.combat-sidebar .token-actions .action-tokens .action-token.used { + opacity: 0.5; } -.combat-sidebar .token-action-tokens .use-action-token.disabled { - opacity: 0.6; +.combat-sidebar .token-actions button { + font-size: 22px; } -.combat-sidebar .icon-button.spaced { - margin-left: 4px; +.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 .icon-button.disabled { - opacity: 0.6; +.combat-sidebar .token-actions button.main:hover { + filter: drop-shadow(0 0 3px var(--button-hover-text-color)); } -.combat-sidebar .icon-button:hover:not(.disabled) { - cursor: pointer; - filter: drop-shadow(0 0 3px red); +.combat-sidebar .token-actions button.discrete:hover { + 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 { border-color: black; @@ -2722,11 +2740,18 @@ div.daggerheart.views.multiclass { --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; + --fear-animation: background 0.3s ease, box-shadow .3s ease, border-color .3s ease, opacity .3s ease; } #resources { min-height: calc(var(--header-height) + 4rem); min-width: 4rem; color: #d3d3d3; + transition: var(--fear-animation); +} +#resources header, +#resources .controls, +#resources .window-resize-handle { + transition: var(--fear-animation); } #resources .window-content { padding: 0.5rem; @@ -2822,7 +2847,7 @@ div.daggerheart.views.multiclass { #resources:not(:hover):not(.minimized) header, #resources:not(:hover):not(.minimized) .controls, #resources:not(:hover):not(.minimized) .window-resize-handle { - visibility: hidden; + opacity: 0; } #resources:has(.fear-bar) { min-width: 200px; @@ -2903,7 +2928,7 @@ div.daggerheart.views.multiclass { font-style: normal; font-weight: 700; font-display: swap; - src: url(https://fonts.gstatic.com/s/cinzeldecorative/v17/daaHSScvJGqLYhG8nNt8KPPswUAPniZoaelD.ttf) format('truetype'); + src: url(https://fonts.gstatic.com/s/cinzeldecorative/v18/daaHSScvJGqLYhG8nNt8KPPswUAPniZoaelD.ttf) format('truetype'); } @font-face { font-family: 'Montserrat'; @@ -3186,6 +3211,19 @@ div.daggerheart.views.multiclass { .application.setting.dh-style footer button { 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 { background: light-dark(transparent, transparent); border: 1px solid light-dark(#222, #efe6d8); diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index 8ad2c97f..a4bb7c99 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -212,6 +212,22 @@ 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 { diff --git a/styles/ui.less b/styles/ui.less index 7d1ff690..498aca68 100644 --- a/styles/ui.less +++ b/styles/ui.less @@ -1,71 +1,160 @@ .combat-sidebar { - .encounter-gm-resources { - flex: 0; - display: flex; - justify-content: center; - padding: @largePadding 0; + // .encounter-gm-resources { + // flex: 0; + // display: flex; + // justify-content: center; + // padding: @largePadding 0; - .gm-resource-controls { + // .gm-resource-controls { + // display: flex; + // flex-direction: column; + // align-items: center; + // padding: 0 4px; + // justify-content: center; + // } + + // .gm-resource-tools { + // display: flex; + // flex-direction: column; + // justify-content: center; + // padding: 0 5px 0 @fullPadding; + + // i { + // margin: 0 @tinyMargin; + // font-size: 16px; + + // &.disabled { + // opacity: 0.6; + // } + + // &:hover:not(.disabled) { + // cursor: pointer; + // filter: drop-shadow(0 0 3px @mainShadow); + // } + // } + // } + + // .gm-resource { + // background: rgba(255, 255, 255, 0.1); + // padding: @fullPadding; + // border-radius: 8px; + // border: @normalBorder solid black; + // font-size: 20px; + // } + // } + + // .token-action-tokens { + // flex: 0 0 48px; + // text-align: center; + + // .use-action-token { + // &.disabled { + // opacity: 0.6; + // } + // } + // } + + // .icon-button { + // &.spaced { + // margin-left: @halfMargin; + // } + + // &.disabled { + // opacity: 0.6; + // } + + // &:hover:not(.disabled) { + // cursor: pointer; + // filter: drop-shadow(0 0 3px @mainShadow); + // } + // } + .encounter-controls.combat { + justify-content: space-between; + + .encounter-control-fear-container { display: flex; - flex-direction: column; + position: relative; align-items: center; - padding: 0 4px; justify-content: center; - } + color: black; - .gm-resource-tools { - display: flex; - flex-direction: column; - justify-content: center; - padding: 0 5px 0 @fullPadding; + .dice { + height: 24px; + } - i { - margin: 0 @tinyMargin; + .encounter-control-fear { + position: absolute; font-size: 16px; + } - &.disabled { - opacity: 0.6; - } + .encounter-control-counter { + position: absolute; + right: -10px; + color: var(--color-text-secondary); + } + } - &:hover:not(.disabled) { - cursor: pointer; - filter: drop-shadow(0 0 3px @mainShadow); + .control-buttons { + width: min-content; + } + } + + .token-actions { + align-self: stretch; + display: flex; + align-items: 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; } } } - .gm-resource { - background: rgba(255, 255, 255, 0.1); - padding: @fullPadding; - border-radius: 8px; - border: @normalBorder solid black; - font-size: 20px; - } - } + button { + font-size: 22px; - .token-action-tokens { - flex: 0 0 48px; - text-align: center; + &.main { + background: var(--button-hover-background-color); + color: var(--button-hover-text-color); + border-color: var(--button-hover-border-color); - .use-action-token { - &.disabled { - opacity: 0.6; + &:hover { + filter: drop-shadow(0 0 3px var(--button-hover-text-color)); + } + } + + &.discrete:hover { + background: inherit; } } - } - .icon-button { - &.spaced { - margin-left: @halfMargin; - } + .combatant-control { + &:focus { + outline: none; + box-shadow: none; + } - &.disabled { - opacity: 0.6; - } - - &:hover:not(.disabled) { - cursor: pointer; - filter: drop-shadow(0 0 3px @mainShadow); + &.requesting { + filter: drop-shadow(0 0 3px gold); + color: var(--button-hover-text-color); + } } } } diff --git a/templates/settings/variant-rules.hbs b/templates/settings/variant-rules.hbs new file mode 100644 index 00000000..f39cb2a9 --- /dev/null +++ b/templates/settings/variant-rules.hbs @@ -0,0 +1,22 @@ +
+
+ + +
+ {{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)}} +
+
+ + +
+ \ No newline at end of file diff --git a/templates/ui/combat/combatTracker.hbs b/templates/ui/combat/combatTracker.hbs new file mode 100644 index 00000000..c26392d6 --- /dev/null +++ b/templates/ui/combat/combatTracker.hbs @@ -0,0 +1,72 @@ +
    + {{#each turns}} +
  1. + {{!-- Image --}} + {{ name }} + + {{!-- Name & Controls --}} +
    + {{ name }} +
    + {{#if @root.user.isGM}} + + + {{/if}} + {{#if canPing}} + + {{/if}} + {{#unless @root.user.isGM}} + + {{/unless}} + {{!-- TODO: Target Control --}} +
    + {{#each effects.icons}} + {{ name }} + {{/each}} +
    +
    +
    + + {{!-- Resource --}} + {{#if resource includeZero=true}} +
    + {{ resource }} +
    + {{/if}} + + {{#if ../combat.system.started}} +
    + {{#if isOwner}} + {{#if (and (not isNPC) ../actionTokens.enabled)}} +
    + {{#times ../actionTokens.tokens}} + + {{/times}} +
    + {{/if}} + + {{#if @root.user.isGM}} + + {{else}} + + {{/if}} + {{/if}} +
    + {{/if}} +
  2. + {{/each}} +
diff --git a/templates/ui/combat/combatTrackerFooter.hbs b/templates/ui/combat/combatTrackerFooter.hbs new file mode 100644 index 00000000..72680508 --- /dev/null +++ b/templates/ui/combat/combatTrackerFooter.hbs @@ -0,0 +1,17 @@ + diff --git a/templates/ui/combat/combatTrackerHeader.hbs b/templates/ui/combat/combatTrackerHeader.hbs new file mode 100644 index 00000000..9d54058c --- /dev/null +++ b/templates/ui/combat/combatTrackerHeader.hbs @@ -0,0 +1,86 @@ +
+ + {{!-- Encounter Controls --}} + {{#if user.isGM}} + + {{/if}} + +
+ {{#if hasCombat}} +
+ + +
{{fear}}
+
+ {{/if}} + + {{!-- Combat Status --}} + + {{#if combats.length}} + {{#if combat.system.started}} + {{ localize "DAGGERHEART.Combat.combatStarted" }} + {{else}} + {{ localize "COMBAT.NotStarted" }} + {{/if}} + {{else}} + {{ localize "COMBAT.None" }} + {{/if}} + + + {{!-- Combat Controls --}} + {{#if hasCombat}} +
+
+ +
+ {{/if}} + +
+ +
diff --git a/templates/ui/combatTracker.hbs b/templates/ui/combatTrackerOld.hbs similarity index 100% rename from templates/ui/combatTracker.hbs rename to templates/ui/combatTrackerOld.hbs