diff --git a/daggerheart.mjs b/daggerheart.mjs index 27979ae1..0153a62c 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', () => {}); @@ -295,6 +294,7 @@ const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/sheets/pc/parts/heritageCard.hbs', 'systems/daggerheart/templates/sheets/pc/parts/advancementCard.hbs', 'systems/daggerheart/templates/views/parts/level.hbs', - 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' + 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs', + 'systems/daggerheart/templates/ui/combat/combatTrackerSection.hbs' ]); }; diff --git a/lang/en.json b/lang/en.json index 75a803e1..a2656b4f 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", @@ -150,6 +163,14 @@ "Or": "Or", "Description": "Description", "Features": "Features", + "Adversary": { + "Singular": "Adversary", + "Plural": "Adversaries" + }, + "Character": { + "Singular": "Character", + "Plural": "Characters" + }, "RefreshType": { "Session": "Session", "Shortrest": "Short Rest", @@ -329,6 +350,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/resources.mjs b/module/applications/resources.mjs index b25e374f..bbd47fc5 100644 --- a/module/applications/resources.mjs +++ b/module/applications/resources.mjs @@ -1,4 +1,3 @@ - const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; /** @@ -10,101 +9,101 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; */ export default class Resources extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(options={}) { - super(options); - } - - /** @inheritDoc */ - static DEFAULT_OPTIONS = { - id: "resources", - classes: [], - tag: "div", - window: { - frame: true, - title: "Fear", - positioned: true, - resizable: true, - minimizable: false - }, - actions: { - setFear: Resources.setFear, - increaseFear: Resources.increaseFear - }, - position: { - width: 222, - height: 222, - // top: "200px", - // left: "120px" + constructor(options = {}) { + super(options); } - }; - /** @override */ - static PARTS = { - resources: { - root: true, - template: "systems/daggerheart/templates/views/resources.hbs" - // template: "templates/ui/players.hbs" + /** @inheritDoc */ + static DEFAULT_OPTIONS = { + id: 'resources', + classes: [], + tag: 'div', + window: { + frame: true, + title: 'Fear', + positioned: true, + resizable: true, + minimizable: false + }, + actions: { + setFear: Resources.setFear, + increaseFear: Resources.increaseFear + }, + position: { + width: 222, + height: 222 + // top: "200px", + // left: "120px" + } + }; + + /** @override */ + static PARTS = { + resources: { + root: true, + template: 'systems/daggerheart/templates/views/resources.hbs' + } + }; + + get currentFear() { + return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); } - }; - get currentFear() { - return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); - } + get maxFear() { + return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.MaxFear); + } - get maxFear() { - return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.MaxFear); - } + /* -------------------------------------------- */ + /* Rendering */ + /* -------------------------------------------- */ - /* -------------------------------------------- */ - /* Rendering */ - /* -------------------------------------------- */ + /** @override */ + async _prepareContext(_options) { + const display = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.DisplayFear), + current = this.currentFear, + max = this.maxFear, + percent = (current / max) * 100, + isGM = game.user.isGM; + // Return the data for rendering + return { display, current, max, percent, isGM }; + } - /** @override */ - async _prepareContext(_options) { - const display = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.DisplayFear), - current = this.currentFear, - max = this.maxFear, - percent = (current / max) * 100, - isGM = game.user.isGM; - // Return the data for rendering - return {display, current, max, percent, isGM}; - } + /** @override */ + async _preFirstRender(context, options) { + options.position = game.user.getFlag(SYSTEM.id, 'app.resources.position') ?? Resources.DEFAULT_OPTIONS.position; + } - /** @override */ - async _preFirstRender(context, options) { - options.position = game.user.getFlag(SYSTEM.id, 'app.resources.position') ?? Resources.DEFAULT_OPTIONS.position; - } + /** @override */ + async _preRender(context, options) { + if (this.currentFear > this.maxFear) + await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, this.maxFear); + } - /** @override */ - async _preRender(context, options) { - if(this.currentFear > this.maxFear) await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, this.maxFear); - } + _onPosition(position) { + game.user.setFlag(SYSTEM.id, 'app.resources.position', position); + } - _onPosition(position) { - game.user.setFlag(SYSTEM.id, 'app.resources.position', position); - } + async close(options = {}) { + if (!options.allowed) return; + else super.close(options); + } - async close(options={}) { - if(!options.allowed) return; - else super.close(options); - } + static async setFear(event, target) { + if (!game.user.isGM) return; + const fearCount = Number(target.dataset.index ?? 0); + await this.updateFear(this.currentFear === fearCount + 1 ? fearCount : fearCount + 1); + } - static async setFear(event, target) { - if(!game.user.isGM) return; - const fearCount = Number(target.dataset.index ?? 0); - await this.updateFear(this.currentFear === fearCount + 1 ? fearCount : fearCount + 1); - } + static async increaseFear(event, target) { + let value = target.dataset.increment ?? 0, + operator = value.split('')[0] ?? null; + value = Number(value); + await this.updateFear(operator ? this.currentFear + value : value); + } - static async increaseFear(event, target) { - let value = target.dataset.increment ?? 0, - operator = value.split('')[0] ?? null; - value = Number(value); - await this.updateFear(operator ? this.currentFear + value : value); - } - - async updateFear(value) { - if(!game.user.isGM) return; - value = Math.max(0, Math.min(this.maxFear, value)); - await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, value); - } -} \ No newline at end of file + async updateFear(value) { + if (!game.user.isGM) return; + value = Math.max(0, Math.min(this.maxFear, value)); + await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, value); + } +} 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..dfd69ad2 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'; diff --git a/module/data/combat.mjs b/module/data/combat.mjs index 3ad52b8b..e0490286 100644 --- a/module/data/combat.mjs +++ b/module/data/combat.mjs @@ -1,9 +1,6 @@ -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({}) - }; + return {}; } } diff --git a/module/data/combatant.mjs b/module/data/combatant.mjs index 60c32db6..cae5d08f 100644 --- a/module/data/combatant.mjs +++ b/module/data/combatant.mjs @@ -1,8 +1,11 @@ -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 }) + }), + 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..3ad3189e 100644 --- a/module/documents/combat.mjs +++ b/module/documents/combat.mjs @@ -1,44 +1,19 @@ -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; + async startCombat() { + this._playCombatSound('startEncounter'); + const updateData = { round: 1, turn: null }; + Hooks.callAll('combatStart', this, updateData); + await this.update(updateData); + return this; + } - return aVal - bVal; + _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 - ); - - 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 }; - - 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 - } - }); - } - } } diff --git a/module/ui/combatTracker.mjs b/module/ui/combatTracker.mjs index 86002cb6..46f7f318 100644 --- a/module/ui/combatTracker.mjs +++ b/module/ui/combatTracker.mjs @@ -1,199 +1,100 @@ -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); + + const adversaries = context.turns.filter(x => x.isNPC); + const characters = context.turns.filter(x => !x.isNPC); + + Object.assign(context, { + actionTokens: game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.variantRules).actionTokens, + adversaries, + characters + }); + } + + 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 ?? {}; + const combatant = this.viewed.combatants.get(combatantId); + + const toggleTurn = this.viewed.combatants.contents + .sort(this.viewed._sortCombatants) + .map(x => x.id) + .indexOf(combatantId); + + await this.viewed.update({ turn: this.viewed.turn === toggleTurn ? null : toggleTurn }); + await combatant.update({ 'system.spotlight.requesting': 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/players.mjs b/module/ui/players.mjs deleted file mode 100644 index cbce702b..00000000 --- a/module/ui/players.mjs +++ /dev/null @@ -1,53 +0,0 @@ -import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs'; - -export default class DhpPlayers extends foundry.applications.ui.Players { - constructor(data, context) { - super(data, context); - - Hooks.on(socketEvent.DhpFearUpdate, this.onFearUpdate); - } - - get template() { - return 'systems/daggerheart/templates/ui/players.hbs'; - } - - async getData(options = {}) { - const context = super.getData(options); - context.fear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); - context.user = game.user; - - return context; - } - - activateListeners(html) { - // Toggle online/offline - html.find('.players-mode').click(this._onToggleOfflinePlayers.bind(this)); - html.find('.fear-control.up').click(async event => await this.updateFear(event, 1)); - html.find('.fear-control.down').click(async event => await this.updateFear(event, -1)); - - // Context menu - const contextOptions = this._getUserContextOptions(); - Hooks.call('getUserContextOptions', html, contextOptions); - new ContextMenu(html, '.player', contextOptions); - } - - async updateFear(_, change) { - const fear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); - const value = Math.max(Math.min(fear + change, 6), 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 } - }); - } - - 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..73f54a18 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -1293,60 +1293,92 @@ .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; - border-radius: 8px; - border: 2px solid black; - font-size: 20px; +.combat-sidebar .combatant-controls { + flex: 0; } -.combat-sidebar .token-action-tokens { - flex: 0 0 48px; +.combat-sidebar .token-actions { + align-self: stretch; + display: flex; + align-items: top; + justify-content: center; + gap: 16px; +} +.combat-sidebar .token-actions .action-tokens { + display: flex; + gap: 4px; +} +.combat-sidebar .token-actions .action-tokens .action-token { + height: 22px; + width: 22px; + border: 1px solid; + border-radius: 50%; + 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; + background: transparent; +} +.combat-sidebar .token-actions button { + font-size: 22px; + height: 24px; + width: 24px; +} +.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 .spotlight-control { + font-size: 26px; +} +.combat-sidebar .spotlight-control:focus { + outline: none; + box-shadow: none; +} +.combat-sidebar .spotlight-control.discrete:hover { + background: inherit; +} +.combat-sidebar .spotlight-control.requesting { + filter: drop-shadow(0 0 3px gold); + color: var(--button-hover-text-color); +} +.combat-sidebar h4 { + margin: 0; text-align: center; } -.combat-sidebar .token-action-tokens .use-action-token.disabled { - opacity: 0.6; -} -.combat-sidebar .icon-button.spaced { - margin-left: 4px; -} -.combat-sidebar .icon-button.disabled { - opacity: 0.6; -} -.combat-sidebar .icon-button:hover:not(.disabled) { - cursor: pointer; - filter: drop-shadow(0 0 3px red); -} .chat-message.duality { border-color: black; padding: 8px 0 0 0; @@ -2722,11 +2754,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 +2861,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 +2942,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 +3225,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..c54b0b3b 100644 --- a/styles/ui.less +++ b/styles/ui.less @@ -1,71 +1,106 @@ .combat-sidebar { - .encounter-gm-resources { - flex: 0; - display: flex; - justify-content: center; - padding: @largePadding 0; + .encounter-controls.combat { + justify-content: space-between; - .gm-resource-controls { + .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; + } + } + + .combatant-controls { + flex: 0; + } + + .token-actions { + align-self: stretch; + display: flex; + align-items: top; + justify-content: center; + gap: 16px; + + .action-tokens { + display: flex; + gap: 4px; + + .action-token { + height: 22px; + width: 22px; + border: 1px solid; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + padding: 8px; + --button-size: 0; + + &.used { + opacity: 0.5; + background: transparent; } } } - .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; + height: 24px; + width: 24px; + + &.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)); + } + } } } - .token-action-tokens { - flex: 0 0 48px; + .spotlight-control { + font-size: 26px; + + &:focus { + outline: none; + box-shadow: none; + } + + &.discrete:hover { + background: inherit; + } + + &.requesting { + filter: drop-shadow(0 0 3px gold); + color: var(--button-hover-text-color); + } + } + + h4 { + margin: 0; 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); - } } } 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 @@ +