From 3489c9c2e82545f272f2ec67bac2ed9f95625297 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Wed, 13 Aug 2025 19:39:19 +0200 Subject: [PATCH] [Feature] 648 - Mark Defeated Actors (#914) * Improved death marking styling * Added automation for defeated status * Fixed so the tracker recognises and sets the correct defeated statuses depending on type * Fixed so missing statuses doesn't cause crashes * Increased companion sheet width by 40 pixels * Added missing inheritDoc * Removed fas --- lang/en.json | 17 ++++++- .../settings/automationSettings.mjs | 13 ++++- .../applications/sheets/actors/companion.mjs | 2 +- module/applications/ui/combatTracker.mjs | 51 +++++++++++++++++++ module/canvas/placeables/token.mjs | 14 ++--- module/config/generalConfig.mjs | 34 ++++++++----- module/data/actor/base.mjs | 22 ++++++++ module/data/combatant.mjs | 6 +++ module/data/settings/Automation.mjs | 30 +++++++++++ module/dice/dhRoll.mjs | 14 ++--- module/documents/activeEffect.mjs | 2 +- module/documents/actor.mjs | 17 +++++++ .../dialog/downtime/downtime-container.less | 6 +++ .../less/sheets/actors/adversary/sidebar.less | 43 +++++++++------- .../less/sheets/actors/character/sidebar.less | 44 +++++++++------- .../settings/automation-settings/footer.hbs | 10 ++++ .../general.hbs} | 22 +++----- .../settings/automation-settings/header.hbs | 3 ++ .../settings/automation-settings/rules.hbs | 17 +++++++ templates/sheets/actors/adversary/sidebar.hbs | 3 +- templates/sheets/actors/character/sidebar.hbs | 2 +- 21 files changed, 288 insertions(+), 84 deletions(-) create mode 100644 templates/settings/automation-settings/footer.hbs rename templates/settings/{automation-settings.hbs => automation-settings/general.hbs} (71%) create mode 100644 templates/settings/automation-settings/header.hbs create mode 100644 templates/settings/automation-settings/rules.hbs diff --git a/lang/en.json b/lang/en.json index c971e084..f1785185 100755 --- a/lang/en.json +++ b/lang/en.json @@ -821,6 +821,10 @@ "name": "Dead", "description": "The character is dead" }, + "defeated": { + "name": "Defeated", + "description": "This adversary is defeated." + }, "hidden": { "name": "Hidden", "description": "While Hidden, attacks cannot be made directly targeting them nd any rolls against them are at disadvantage.\nWhen a Hidden creature moves or attacks, they are no longer Hidden. However, if a creature is Hidden when they begin making an attack, the roll has advantage; the Hidden condition isn’t cleared until after the attack is resolved." @@ -1870,7 +1874,8 @@ "tier3": "Tier 3", "tier4": "tier 4", "domains": "Domains", - "downtime": "Downtime" + "downtime": "Downtime", + "rules": "Rules" }, "Tiers": { "singular": "Tier", @@ -2110,6 +2115,13 @@ "label": "Damage Reduction Rules Default", "hint": "Wether using armor and reductions has rules on by default" }, + "defeated": { + "enabled": { "label": "Enabled" }, + "overlay": { "label": "Overlay Effect" }, + "characterDefault": { "label": "Character Default Defeated Status" }, + "adversaryDefault": { "label": "Adversary Default Defeated Status" }, + "companionDefault": { "label": "Companion Default Defeated Status" } + }, "hopeFear": { "label": "Hope & Fear", "gm": { "label": "GM" }, @@ -2141,6 +2153,9 @@ "label": "Players Can Manually Edit Character Settings", "hint": "Players are allowed to access the manual Character Settings and change their statistics beyond the rules." } + }, + "defeated": { + "title": "Defeated Handling" } }, "Homebrew": { diff --git a/module/applications/settings/automationSettings.mjs b/module/applications/settings/automationSettings.mjs index 489bae02..0157e016 100644 --- a/module/applications/settings/automationSettings.mjs +++ b/module/applications/settings/automationSettings.mjs @@ -31,8 +31,19 @@ export default class DhAutomationSettings extends HandlebarsApplicationMixin(App }; static PARTS = { + tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, + header: { template: 'systems/daggerheart/templates/settings/automation-settings/header.hbs' }, + general: { template: 'systems/daggerheart/templates/settings/automation-settings/general.hbs' }, + rules: { template: 'systems/daggerheart/templates/settings/automation-settings/rules.hbs' }, + footer: { template: 'systems/daggerheart/templates/settings/automation-settings/footer.hbs' } + }; + + /** @inheritdoc */ + static TABS = { main: { - template: 'systems/daggerheart/templates/settings/automation-settings.hbs' + tabs: [{ id: 'general' }, { id: 'rules' }], + initial: 'general', + labelPrefix: 'DAGGERHEART.GENERAL.Tabs' } }; diff --git a/module/applications/sheets/actors/companion.mjs b/module/applications/sheets/actors/companion.mjs index dd102b1e..1105131d 100644 --- a/module/applications/sheets/actors/companion.mjs +++ b/module/applications/sheets/actors/companion.mjs @@ -6,7 +6,7 @@ import DHBaseActorSheet from '../api/base-actor.mjs'; export default class DhCompanionSheet extends DHBaseActorSheet { static DEFAULT_OPTIONS = { classes: ['actor', 'companion'], - position: { width: 300 }, + position: { width: 340 }, actions: { levelManagement: DhCompanionSheet.#levelManagement } diff --git a/module/applications/ui/combatTracker.mjs b/module/applications/ui/combatTracker.mjs index d3d8eb66..69c43b61 100644 --- a/module/applications/ui/combatTracker.mjs +++ b/module/applications/ui/combatTracker.mjs @@ -65,6 +65,57 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C ]; } + getDefeatedId(combatant) { + if (!combatant.actor) return CONFIG.specialStatusEffects.DEFEATED; + + const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated; + return settings[`${combatant.actor.type}Default`]; + } + + /** @inheritdoc */ + async _onToggleDefeatedStatus(combatant) { + const isDefeated = !combatant.isDefeated; + await combatant.update({ defeated: isDefeated }); + await combatant.actor?.toggleStatusEffect(this.getDefeatedId(combatant), { overlay: true, active: isDefeated }); + } + + /** @inheritdoc */ + async _prepareTurnContext(combat, combatant, index) { + const { id, name, isOwner, isDefeated, hidden, initiative, permission } = combatant; + const resource = permission >= CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER ? combatant.resource : null; + const hasDecimals = Number.isFinite(initiative) && !Number.isInteger(initiative); + const turn = { + hasDecimals, + hidden, + id, + isDefeated, + initiative, + isOwner, + name, + resource, + active: index === combat.turn, + canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'), + img: await this._getCombatantThumbnail(combatant) + }; + + turn.css = [turn.active ? 'active' : null, hidden ? 'hide' : null, isDefeated ? 'defeated' : null].filterJoin( + ' ' + ); + + const defeatedId = this.getDefeatedId(combatant); + const effects = []; + for (const effect of combatant.actor?.temporaryEffects ?? []) { + if (effect.statuses.has(defeatedId)) turn.isDefeated = true; + else if (effect.img) effects.push({ img: effect.img, name: effect.name }); + } + turn.effects = { + icons: effects, + tooltip: this._formatEffectsTooltip(effects) + }; + + return turn; + } + async setCombatantSpotlight(combatantId) { const update = { system: { diff --git a/module/canvas/placeables/token.mjs b/module/canvas/placeables/token.mjs index f2096c9a..09b3b192 100644 --- a/module/canvas/placeables/token.mjs +++ b/module/canvas/placeables/token.mjs @@ -20,12 +20,14 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { for (var status of effect.statuses) { if (!currentStatusActiveEffects.find(x => x.statuses.has(status))) { const statusData = statusMap.get(status); - acc.push({ - name: game.i18n.localize(statusData.name), - statuses: [status], - img: statusData.icon, - tint: effect.tint - }); + if (statusData) { + acc.push({ + name: game.i18n.localize(statusData.name), + statuses: [status], + img: statusData.icon, + tint: effect.tint + }); + } } } diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index ee0b6671..5abf2868 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -164,6 +164,27 @@ export const healingTypes = { } }; +export const defeatedConditions = { + defeated: { + id: 'defeated', + name: 'DAGGERHEART.CONFIG.Condition.defeated.name', + icon: 'icons/magic/control/fear-fright-mask-orange.webp', + description: 'DAGGERHEART.CONFIG.Condition.defeated.description' + }, + unconscious: { + id: 'unconscious', + name: 'DAGGERHEART.CONFIG.Condition.unconscious.name', + icon: 'icons/magic/control/sleep-bubble-purple.webp', + description: 'DAGGERHEART.CONFIG.Condition.unconscious.description' + }, + dead: { + id: 'dead', + name: 'DAGGERHEART.CONFIG.Condition.dead.name', + icon: 'icons/magic/death/grave-tombstone-glow-teal.webp', + description: 'DAGGERHEART.CONFIG.Condition.dead.description' + } +}; + export const conditions = { vulnerable: { id: 'vulnerable', @@ -183,18 +204,7 @@ export const conditions = { icon: 'icons/magic/control/debuff-chains-shackle-movement-red.webp', description: 'DAGGERHEART.CONFIG.Condition.restrained.description' }, - unconscious: { - id: 'unconscious', - name: 'DAGGERHEART.CONFIG.Condition.unconscious.name', - icon: 'icons/magic/control/sleep-bubble-purple.webp', - description: 'DAGGERHEART.CONFIG.Condition.unconscious.description' - }, - dead: { - id: 'dead', - name: 'DAGGERHEART.CONFIG.Condition.dead.name', - icon: 'icons/magic/death/grave-tombstone-glow-teal.webp', - description: 'DAGGERHEART.CONFIG.Condition.dead.description' - } + ...defeatedConditions }; export const defaultRestOptions = { diff --git a/module/data/actor/base.mjs b/module/data/actor/base.mjs index a32ac9dd..5b225228 100644 --- a/module/data/actor/base.mjs +++ b/module/data/actor/base.mjs @@ -106,6 +106,28 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel { }, []); options.scrollingTextData = textData; } + + if (changes.system?.resources) { + const defeatedSettings = game.settings.get( + CONFIG.DH.id, + CONFIG.DH.SETTINGS.gameSettings.Automation + ).defeated; + const typeForDefeated = ['character', 'adversary', 'companion'].find(x => x === this.parent.type); + if (defeatedSettings.enabled && typeForDefeated) { + const resource = typeForDefeated === 'companion' ? 'stress' : 'hitPoints'; + if (changes.system.resources[resource]) { + const becameMax = changes.system.resources[resource].value === this.resources[resource].max; + const wasMax = + this.resources[resource].value === this.resources[resource].max && + this.resources[resource].value !== changes.system.resources[resource].value; + if (becameMax) { + this.parent.toggleDefeated(true); + } else if (wasMax) { + this.parent.toggleDefeated(false); + } + } + } + } } _onUpdate(changes, options, userId) { diff --git a/module/data/combatant.mjs b/module/data/combatant.mjs index cae5d08f..bb54c798 100644 --- a/module/data/combatant.mjs +++ b/module/data/combatant.mjs @@ -8,4 +8,10 @@ export default class DhCombatant extends foundry.abstract.TypeDataModel { actionTokens: new fields.NumberField({ required: true, integer: true, initial: 3 }) }; } + + get isDefeated() { + const { unconscious, defeated, dead } = CONFIG.DH.GENERAL.conditions; + const defeatedConditions = new Set([unconscious.id, defeated.id, dead.id]); + return this.defeated || this.actor?.statuses.intersection(defeatedConditions)?.size; + } } diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index facaec17..e1d63669 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -50,6 +50,36 @@ export default class DhAutomation extends foundry.abstract.DataModel { required: true, initial: false, label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.playerCanEditSheet.label' + }), + defeated: new fields.SchemaField({ + enabled: new fields.BooleanField({ + required: true, + initial: false, + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.enabled.label' + }), + overlay: new fields.BooleanField({ + required: true, + initial: true, + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.overlay.label' + }), + characterDefault: new fields.StringField({ + required: true, + choices: CONFIG.DH.GENERAL.defeatedConditions, + initial: CONFIG.DH.GENERAL.defeatedConditions.unconscious.id, + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.characterDefault.label' + }), + adversaryDefault: new fields.StringField({ + required: true, + choices: CONFIG.DH.GENERAL.defeatedConditions, + initial: CONFIG.DH.GENERAL.defeatedConditions.defeated.id, + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.adversaryDefault.label' + }), + companionDefault: new fields.StringField({ + required: true, + choices: CONFIG.DH.GENERAL.defeatedConditions, + initial: CONFIG.DH.GENERAL.defeatedConditions.defeated.id, + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.companionDefault.label' + }) }) }; } diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index 695c94a4..88d40c32 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -225,7 +225,8 @@ export const registerRollDiceHooks = () => { const actor = await fromUuid(config.source.actor); let updates = []; if (!actor) return; - if (config.roll.isCritical || config.roll.result.duality === 1) updates.push({ key: 'hope', value: 1, total: -1, enabled: true }); + if (config.roll.isCritical || config.roll.result.duality === 1) + updates.push({ key: 'hope', value: 1, total: -1, enabled: true }); if (config.roll.isCritical) updates.push({ key: 'stress', value: -1, total: 1, enabled: true }); if (config.roll.result.duality === -1) updates.push({ key: 'fear', value: 1, total: -1, enabled: true }); @@ -233,16 +234,15 @@ export const registerRollDiceHooks = () => { if (config.rerolledRoll.isCritical || config.rerolledRoll.result.duality === 1) updates.push({ key: 'hope', value: -1, total: 1, enabled: true }); if (config.rerolledRoll.isCritical) updates.push({ key: 'stress', value: 1, total: -1, enabled: true }); - if (config.rerolledRoll.result.duality === -1) updates.push({ key: 'fear', value: -1, total: 1, enabled: true }); + if (config.rerolledRoll.result.duality === -1) + updates.push({ key: 'fear', value: -1, total: 1, enabled: true }); } if (updates.length) { const target = actor.system.partner ?? actor; - if (!['dead', 'unconscious'].some(x => actor.statuses.has(x))) { - if(config.rerolledRoll) - target.modifyResource(updates); - else - config.costs = [...(config.costs ?? []), ...updates]; + if (!['dead', 'defeated', 'unconscious'].some(x => actor.statuses.has(x))) { + if (config.rerolledRoll) target.modifyResource(updates); + else config.costs = [...(config.costs ?? []), ...updates]; } } diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index bf535b78..f46cc9db 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -118,7 +118,7 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { for (const statusId of this.statuses) { const status = CONFIG.statusEffects.find(s => s.id === statusId); - tags.push(game.i18n.localize(status.name)); + if (status) tags.push(game.i18n.localize(status.name)); } return tags; diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 68c12b6f..6b01c058 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -718,4 +718,21 @@ export default class DhpActor extends Actor { value: 1 }); } + + async toggleDefeated(defeatedState) { + const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated; + const { unconscious, defeated, dead } = CONFIG.DH.GENERAL.conditions; + const defeatedConditions = new Set([unconscious.id, defeated.id, dead.id]); + if (!defeatedState) { + for (let defeatedId of defeatedConditions) { + await this.toggleStatusEffect(defeatedId, { overlay: settings.overlay, active: defeatedState }); + } + } else { + const noDefeatedConditions = this.statuses.intersection(defeatedConditions).size === 0; + if (noDefeatedConditions) { + const condition = settings[`${this.type}Default`]; + await this.toggleStatusEffect(condition, { overlay: settings.overlay, active: defeatedState }); + } + } + } } diff --git a/styles/less/dialog/downtime/downtime-container.less b/styles/less/dialog/downtime/downtime-container.less index 55fb5b70..fe1344cd 100644 --- a/styles/less/dialog/downtime/downtime-container.less +++ b/styles/less/dialog/downtime/downtime-container.less @@ -6,6 +6,12 @@ .downtime-container .activity-container .activity-selected-marker { background-image: url(../assets/parchments/dh-parchment-light.png); } + + .refreshables-container { + .refreshable-container { + background-image: url(../assets/parchments/dh-parchment-light.png); + } + } } .daggerheart.dh-style.views.downtime { diff --git a/styles/less/sheets/actors/adversary/sidebar.less b/styles/less/sheets/actors/adversary/sidebar.less index 82d630f8..79927881 100644 --- a/styles/less/sheets/actors/adversary/sidebar.less +++ b/styles/less/sheets/actors/adversary/sidebar.less @@ -9,10 +9,14 @@ } } }, { - &.adversary { + &.sheet.actor.dh-style.adversary { .adversary-sidebar-sheet { background: transparent; } + + .portrait.death-roll .death-roll-btn { + filter: brightness(0) drop-shadow(0 0 3px @dark-blue); + } } }); @@ -27,6 +31,26 @@ border-bottom: 1px solid light-dark(@dark-blue, @golden); cursor: pointer; + &.death-roll { + img { + filter: grayscale(1); + } + + .death-roll-btn { + display: flex; + position: absolute; + top: 30%; + right: 30%; + font-size: 6rem; + color: @beige; + filter: grayscale(1) drop-shadow(0 0 3px black); + + &:hover { + text-shadow: 0 0 8px @beige; + } + } + } + img { height: 275px; width: 275px; @@ -37,23 +61,6 @@ .death-roll-btn { display: none; } - - &.death-roll { - filter: grayscale(1); - - .death-roll-btn { - display: flex; - position: absolute; - top: 30%; - right: 30%; - font-size: 6rem; - color: @beige; - - &:hover { - text-shadow: 0 0 8px @beige; - } - } - } } .threshold-section { diff --git a/styles/less/sheets/actors/character/sidebar.less b/styles/less/sheets/actors/character/sidebar.less index bbf961f6..3d8e829a 100644 --- a/styles/less/sheets/actors/character/sidebar.less +++ b/styles/less/sheets/actors/character/sidebar.less @@ -11,11 +11,16 @@ } } }, { - .character-sidebar-sheet { + &.sheet.actor.dh-style.character .character-sidebar-sheet { background: transparent; + .experience-value { background: url('../assets/svg/experience-shield-light.svg') no-repeat; } + + .portrait.death-roll .death-roll-btn { + filter: brightness(0) drop-shadow(0 0 3px @dark-blue); + } } }); @@ -29,6 +34,26 @@ border-bottom: 1px solid light-dark(@dark-blue, @golden); cursor: pointer; + &.death-roll { + img { + filter: grayscale(1); + } + + .death-roll-btn { + display: flex; + position: absolute; + top: 30%; + right: 30%; + font-size: 6rem; + color: @beige; + filter: grayscale(1) drop-shadow(0 0 3px black); + + &:hover { + text-shadow: 0 0 8px @beige; + } + } + } + img { height: 275px; width: 275px; @@ -40,23 +65,6 @@ display: none; } - &.death-roll { - filter: grayscale(1); - - .death-roll-btn { - display: flex; - position: absolute; - top: 30%; - right: 30%; - font-size: 6rem; - color: @beige; - - &:hover { - text-shadow: 0 0 8px @beige; - } - } - } - .icons-list { position: absolute; display: flex; diff --git a/templates/settings/automation-settings/footer.hbs b/templates/settings/automation-settings/footer.hbs new file mode 100644 index 00000000..54939c17 --- /dev/null +++ b/templates/settings/automation-settings/footer.hbs @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/templates/settings/automation-settings.hbs b/templates/settings/automation-settings/general.hbs similarity index 71% rename from templates/settings/automation-settings.hbs rename to templates/settings/automation-settings/general.hbs index 9e9da6bb..04d08a9f 100644 --- a/templates/settings/automation-settings.hbs +++ b/templates/settings/automation-settings/general.hbs @@ -1,7 +1,8 @@ -
-
-

{{localize 'DAGGERHEART.SETTINGS.Menu.automation.name'}}

-
+
{{formGroup settingFields.schema.fields.hopeFear.fields.gm value=settingFields._source.hopeFear.gm localize=true}} @@ -15,16 +16,5 @@ {{formGroup settingFields.schema.fields.playerCanEditSheet value=settingFields._source.playerCanEditSheet localize=true}} {{formGroup settingFields.schema.fields.damageReductionRulesDefault value=settingFields._source.damageReductionRulesDefault localize=true}} {{formGroup settingFields.schema.fields.resourceScrollTexts value=settingFields._source.resourceScrollTexts localize=true}} - -
- - -
-
+
\ No newline at end of file diff --git a/templates/settings/automation-settings/header.hbs b/templates/settings/automation-settings/header.hbs new file mode 100644 index 00000000..261fe6c4 --- /dev/null +++ b/templates/settings/automation-settings/header.hbs @@ -0,0 +1,3 @@ +
+

{{localize 'DAGGERHEART.SETTINGS.Menu.automation.name'}}

+
\ No newline at end of file diff --git a/templates/settings/automation-settings/rules.hbs b/templates/settings/automation-settings/rules.hbs new file mode 100644 index 00000000..8bbf4d93 --- /dev/null +++ b/templates/settings/automation-settings/rules.hbs @@ -0,0 +1,17 @@ +
+
+ + {{localize "DAGGERHEART.SETTINGS.Automation.defeated.title"}} + + + {{formGroup settingFields.schema.fields.defeated.fields.enabled value=settingFields._source.defeated.enabled localize=true}} + {{formGroup settingFields.schema.fields.defeated.fields.overlay value=settingFields._source.defeated.overlay localize=true}} + {{formGroup settingFields.schema.fields.defeated.fields.characterDefault value=settingFields._source.defeated.characterDefault labelAttr="name" localize=true}} + {{formGroup settingFields.schema.fields.defeated.fields.adversaryDefault value=settingFields._source.defeated.adversaryDefault labelAttr="name" localize=true}} + {{formGroup settingFields.schema.fields.defeated.fields.companionDefault value=settingFields._source.defeated.companionDefault labelAttr="name" localize=true}} +
+
\ No newline at end of file diff --git a/templates/sheets/actors/adversary/sidebar.hbs b/templates/sheets/actors/adversary/sidebar.hbs index 6f8b6de0..cc95c87f 100644 --- a/templates/sheets/actors/adversary/sidebar.hbs +++ b/templates/sheets/actors/adversary/sidebar.hbs @@ -1,8 +1,7 @@