[Feature/Fixes] Adversary Touchups (#359)

* Added support for automatic horde damage

* Active effects are shown on the token

* Fixed logic

* Fixed d20 dice lightmode color
This commit is contained in:
WBHarry 2025-07-16 20:17:34 +02:00 committed by GitHub
parent 727cb692b4
commit b40d053201
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 134 additions and 24 deletions

View file

@ -136,6 +136,7 @@ Hooks.once('init', () => {
CONFIG.Canvas.rulerClass = placeables.DhRuler;
CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer;
CONFIG.Token.objectClass = placeables.DhTokenPlaceable;
CONFIG.Combat.documentClass = documents.DhpCombat;
CONFIG.ui.combat = applications.ui.DhCombatTracker;
CONFIG.ui.chat = applications.ui.DhChatLog;

View file

@ -99,6 +99,7 @@
"tier": { "label": "Tier" },
"type": { "label": "Type" }
},
"hordeDamage": "Horde Damage",
"horderHp": "Horde/HP"
},
"Character": {
@ -1214,6 +1215,7 @@
"damage": "Damage",
"damageType": "Damage Type",
"description": "Description",
"difficulty": "Difficulty",
"duality": "Duality",
"dualityRoll": "Duality Roll",
"enabled": "Enabled",
@ -1224,7 +1226,11 @@
},
"fear": "Fear",
"features": "Features",
"hitPoints": "Hit Points",
"hitPoints": {
"single": "Hit Point",
"plural": "Hit Points",
"short": "HP"
},
"hope": "Hope",
"hordeHp": "Horde HP",
"inactiveEffects": "Inactive Effects",
@ -1251,7 +1257,8 @@
"use": "Use",
"used": "Used",
"uses": "Uses",
"value": "Value"
"value": "Value",
"withThing": "With {thing}"
},
"ITEMS": {
"FIELDS": {
@ -1355,9 +1362,9 @@
"label": "Action Points",
"hint": "Automatically give and take Action Points as combatants take their turns."
},
"countdowns": {
"label": "Countdowns",
"hint": "Automatically progress non-custom countdowns"
"hordeDamage": {
"label": "Automatic Horde Damage",
"hint": "Automatically active horde effect to lower damage when reaching half or lower HP."
}
}
},

View file

@ -1,4 +1,5 @@
export { default as DhMeasuredTemplate } from './measuredTemplate.mjs';
export { default as DhRuler } from './ruler.mjs';
export { default as DhTemplateLayer } from './templateLayer.mjs';
export { default as DhTokenPlaceable } from './token.mjs';
export { default as DhTokenRuler } from './tokenRuler.mjs';

View file

@ -0,0 +1,36 @@
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
/** @inheritDoc */
async _drawEffects() {
this.effects.renderable = false;
// Clear Effects Container
this.effects.removeChildren().forEach(c => c.destroy());
this.effects.bg = this.effects.addChild(new PIXI.Graphics());
this.effects.bg.zIndex = -1;
this.effects.overlay = null;
// Categorize effects
const activeEffects = this.actor ? Array.from(this.actor.effects).filter(x => !x.disabled) : [];
const overlayEffect = activeEffects.findLast(e => e.img && e.getFlag('core', 'overlay'));
// Draw effects
const promises = [];
for (const [i, effect] of activeEffects.entries()) {
if (!effect.img) continue;
const promise =
effect === overlayEffect
? this._drawOverlay(effect.img, effect.tint)
: this._drawEffect(effect.img, effect.tint);
promises.push(
promise.then(e => {
if (e) e.zIndex = i;
})
);
}
await Promise.allSettled(promises);
this.effects.sortChildren();
this.effects.renderable = true;
this.renderFlags.set({ refreshEffects: true });
}
}

View file

@ -6,6 +6,15 @@ export default class DHDamageAction extends DHBaseAction {
getFormulaValue(part, data) {
let formulaValue = part.value;
if (this.hasRoll && part.resultBased && data.system.roll.result.duality === -1) return part.valueAlt;
const isAdversary = this.actor.type === 'adversary';
if (isAdversary && this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) {
const hasHordeDamage = this.actor.effects.find(
x => x.name === game.i18n.localize('DAGGERHEART.CONFIG.AdversaryType.horde.label')
);
if (hasHordeDamage) return part.valueAlt;
}
return formulaValue;
}

View file

@ -52,7 +52,7 @@ export default class DhpAdversary extends BaseDataActor {
})
}),
resources: new fields.SchemaField({
hitPoints: resourceField(0, 'DAGGERHEART.GENERAL.hitPoints', true),
hitPoints: resourceField(0, 'DAGGERHEART.GENERAL.hitPoints.plural', true),
stress: resourceField(0, 'DAGGERHEART.GENERAL.stress', true)
}),
attack: new ActionField({
@ -109,4 +109,37 @@ export default class DhpAdversary extends BaseDataActor {
get features() {
return this.parent.items.filter(x => x.type === 'feature');
}
async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false;
if (this.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) {
if (changes.system?.resources?.hitPoints?.value) {
const halfHP = Math.ceil(this.resources.hitPoints.max / 2);
const newHitPoints = changes.system.resources.hitPoints.value;
const previouslyAboveHalf = this.resources.hitPoints.value < halfHP;
const loweredBelowHalf = previouslyAboveHalf && newHitPoints >= halfHP;
const raisedAboveHalf = !previouslyAboveHalf && newHitPoints < halfHP;
if (loweredBelowHalf) {
await this.parent.createEmbeddedDocuments('ActiveEffect', [
{
name: game.i18n.localize('DAGGERHEART.CONFIG.AdversaryType.horde.label'),
img: 'icons/magic/movement/chevrons-down-yellow.webp',
disabled: !game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation)
.hordeDamage
}
]);
} else if (raisedAboveHalf) {
const hordeEffects = this.parent.effects.filter(
x => x.name === game.i18n.localize('DAGGERHEART.CONFIG.AdversaryType.horde.label')
);
await this.parent.deleteEmbeddedDocuments(
'ActiveEffect',
hordeEffects.map(x => x.id)
);
}
}
}
}
}

View file

@ -21,7 +21,7 @@ export default class DhCharacter extends BaseDataActor {
return {
...super.defineSchema(),
resources: new fields.SchemaField({
hitPoints: resourceField(0, 'DAGGERHEART.GENERAL.hitPoints', true),
hitPoints: resourceField(0, 'DAGGERHEART.GENERAL.hitPoints.plural', true),
stress: resourceField(6, 'DAGGERHEART.GENERAL.stress', true),
hope: resourceField(6, 'DAGGERHEART.GENERAL.hope')
}),

View file

@ -24,7 +24,7 @@ export default class DHClass extends BaseDataItem {
integer: true,
min: 1,
initial: 5,
label: 'DAGGERHEART.GENERAL.hitPoints'
label: 'DAGGERHEART.GENERAL.hitPoints.plural'
}),
evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }),
hopeFeatures: new ForeignDocumentUUIDArrayField({ type: 'Item' }),

View file

@ -1,6 +1,4 @@
export default class DhAutomation extends foundry.abstract.DataModel {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.SETTINGS.Automation']; // Doesn't work for some reason
static defineSchema() {
const fields = foundry.data.fields;
return {
@ -20,6 +18,11 @@ export default class DhAutomation extends foundry.abstract.DataModel {
required: true,
initial: false,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.actionPoints.label'
}),
hordeDamage: new fields.BooleanField({
required: true,
initial: true,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hordeDamage.label'
})
};
}

View file

@ -1,6 +1,13 @@
@import '../../utils/colors.less';
@import '../../utils/fonts.less';
.theme-light .application.daggerheart.dialog.dh-style.views.roll-selection {
.roll-dialog-container .dices-section .dice-option .dice-icon.normal {
filter: brightness(0) saturate(100%) invert(13%) sepia(1%) saturate(0%) hue-rotate(10deg) brightness(98%)
contrast(100%);
}
}
.application.daggerheart.dialog.dh-style.views.roll-selection {
.roll-dialog-container {
display: flex;
@ -24,6 +31,7 @@
height: 70px;
object-fit: contain;
}
.dice-select {
display: flex;
align-items: center;

View file

@ -1,7 +1,7 @@
<fieldset class="one-column">
<legend>
Damage
{{localize "DAGGERHEART.GENERAL.damage"}}
{{#unless (eq path 'system.attack.')}}<a><i class="fa-solid fa-plus icon-button" data-action="addDamage"></i></a>{{/unless}}
</legend>
{{#unless (or @root.isNPC path)}}
@ -23,6 +23,16 @@
</div>
{{/if}}
{{formField ../fields.type value=dmg.type name=(concat ../path "damage.parts." index ".type") localize=true}}
{{#if ../horde}}
<fieldset class="one-column">
<legend>{{localize "DAGGERHEART.ACTORS.Adversary.hordeDamage"}}</legend>
<div class="nest-inputs">
{{formField ../fields.valueAlt.fields.flatMultiplier value=dmg.valueAlt.flatMultiplier name=(concat ../path "damage.parts." index ".valueAlt.flatMultiplier") label="Multiplier" classes="inline-child" }}
{{formField ../fields.valueAlt.fields.dice value=dmg.valueAlt.dice name=(concat ../path "damage.parts." index ".valueAlt.dice") classes="inline-child"}}
{{formField ../fields.valueAlt.fields.bonus value=dmg.valueAlt.bonus name=(concat ../path "damage.parts." index ".valueAlt.bonus") localize=true classes="inline-child"}}
</div>
</fieldset>
{{/if}}
{{else}}
{{#with (@root.getRealIndex index) as | realIndex |}}
<div class="nest-inputs">
@ -33,11 +43,11 @@
{{#if (and (not @root.isNPC) @root.hasRoll (not dmg.base) dmg.resultBased)}}
<div class="nest-inputs">
<fieldset class="one-column">
<legend>With Hope</legend>
<legend>{{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.hope")}}</legend>
{{> formula fields=../../fields.value.fields type=../../fields.type dmg=dmg source=dmg.value target="value" realIndex=realIndex}}
</fieldset>
<fieldset class="one-column">
<legend>With Fear</legend>
<legend>{{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.fear")}}</legend>
{{> formula fields=../../fields.valueAlt.fields type=../../fields.type dmg=dmg source=dmg.valueAlt target="valueAlt" realIndex=realIndex}}
</fieldset>
</div>

View file

@ -5,7 +5,7 @@
<div class="dices-section">
{{#if (eq @root.rollType 'D20Roll')}}
<div class="dice-option">
<img class="dice-icon" src="{{concat 'systems/daggerheart/assets/icons/dice/default/' @root.roll.d20.denomination '.svg'}}" alt="">
<img class="dice-icon normal" src="{{concat 'systems/daggerheart/assets/icons/dice/default/' @root.roll.d20.denomination '.svg'}}" alt="">
<div class="dice-select">
<select name="roll.dice.d20">
{{selectOptions diceOptions selected=@root.roll.d20.denomination}}

View file

@ -6,7 +6,7 @@
</div>
{{formGroup settingFields.schema.fields.actionPoints value=settingFields._source.actionPoints localize=true}}
{{formGroup settingFields.schema.fields.countdowns value=settingFields._source.countdowns localize=true}}
s {{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}}
<footer class="form-footer">
<button data-action="reset">

View file

@ -19,5 +19,7 @@
{{/if}}
{{/if}}
</fieldset>
{{> 'systems/daggerheart/templates/actionTypes/damage.hbs' fields=systemFields.attack.fields.damage.fields.parts.element.fields source=document.system.attack.damage path="system.attack."}}
{{#if (eq document.system.type 'horde')}}
{{> 'systems/daggerheart/templates/actionTypes/damage.hbs' fields=systemFields.attack.fields.damage.fields.parts.element.fields source=document.system.attack.damage path="system.attack." horde=true}}
{{/if}}
</section>

View file

@ -19,7 +19,7 @@
<div class="fieldsets-section">
<fieldset class="flex">
<legend>{{localize "DAGGERHEART.GENERAL.hitPoints"}}</legend>
<legend>{{localize "DAGGERHEART.GENERAL.hitPoints.plural"}}</legend>
{{formGroup systemFields.resources.fields.hitPoints.fields.value value=document.system.resources.hitPoints.value label=(localize "DAGGERHEART.ACTORS.Adversary.FIELDS.resources.hitPoints.value.label")}}
{{formGroup systemFields.resources.fields.hitPoints.fields.max value=document.system.resources.hitPoints.max}}
</fieldset>

View file

@ -20,14 +20,14 @@
{{#if (eq source.system.type 'horde')}}
<div class="tag">
<span>{{source.system.hordeHp}}</span>
<span>/HP</span>
<span>/{{localize "DAGGERHEART.GENERAL.hitPoints.short"}}</span>
</div>
{{/if}}
</div>
<line-div></line-div>
<div class="adversary-info">
<div class="description">
<i>{{source.system.description}}</i>
<i>{{{source.system.description}}}</i>
</div>
<div class="motives-and-tatics">
<b>{{localize 'DAGGERHEART.ACTORS.Adversary.FIELDS.motivesAndTactics.label'}}: </b>{{{source.system.motivesAndTactics}}}

View file

@ -1,5 +1,5 @@
<aside class="adversary-sidebar-sheet">
<div class="portrait {{#if (gte source.system.resources.hitPoints.value source.system.resources.hitPoints.max)}}death-roll{{/if}}">
<div class="portrait {{#if (and source.system.resources.hitPoints.max (gte source.system.resources.hitPoints.value source.system.resources.hitPoints.max))}}death-roll{{/if}}">
<img src="{{source.img}}" alt="{{source.name}}" data-action='editImage' data-edit="img">
<a class="death-roll-btn" data-tooltip="{{localize "DAGGERHEART.UI.Tooltip.makeDeathMove"}}" data-action="makeDeathMove"><i class="fas fa-skull death-save" ></i></a>
</div>

View file

@ -27,14 +27,14 @@
{{/if}}
</div>
<div class="status-label">
<h4>Difficulty</h4>
<h4>{{localize "DAGGERHEART.GENERAL.difficulty"}}</h4>
</div>
</div>
</div>
<line-div></line-div>
<div class="environment-info">
<div class="description">
<i>{{source.system.description}}</i>
<i>{{{source.system.description}}}</i>
</div>
<div class="impulses">
<b>{{localize 'DAGGERHEART.ACTORS.Environment.FIELDS.impulses.label'}}: </b>{{{source.system.impulses}}}

View file

@ -23,7 +23,7 @@
</div>
<div class="tooltip-information-section spaced">
<div class="tooltip-information">
<label>{{localize "DAGGERHEART.GENERAL.hitPoints"}}</label>
<label>{{localize "DAGGERHEART.GENERAL.hitPoints.plural"}}</label>
<div>{{item.system.resources.hitPoints.max}}</div>
</div>
<div class="tooltip-information">