This commit is contained in:
WBHarry 2025-06-24 15:57:17 +02:00
parent 0d60cd90b6
commit c952580f6b
10 changed files with 418 additions and 56 deletions

View file

@ -207,6 +207,12 @@
"Session": "Session",
"Shortrest": "Short Rest",
"Longrest": "Long Rest"
},
"Damage": {
"Severe": "Severe",
"Major": "Major",
"Minor": "Minor",
"None": "None"
}
},
"ActionType": {
@ -1084,6 +1090,16 @@
"Title": "Ownership Selection - {name}",
"Default": "Default Ownership"
},
"DamageReduction": {
"Title": "Damage Reduction",
"ArmorMarks": "Armor Marks",
"UsedMarks": "Used Marks",
"Stress": "Stress",
"Notifications": {
"DamageAlreadyNone": "The damage has already been reduced to none",
"NotEnoughArmor": "You don't have enough unspent armor marks"
}
},
"Sheets": {
"PC": {
"Name": "Name",

View file

@ -0,0 +1,112 @@
import { getDamageLabel } from '../helpers/utils.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class DamageReductionDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(resolve, reject, actor, damage) {
super({});
this.resolve = resolve;
this.reject = reject;
this.actor = actor;
this.damage = damage;
this.availableArmorMarks = {
max: actor.system.rules.maxArmorMarked.total + (actor.system.rules.stressExtra ?? 0),
maxUseable: actor.system.armorScore - actor.system.armor.system.marks.value,
stressIndex:
(actor.system.rules.stressExtra ?? 0) > 0 ? actor.system.rules.maxArmorMarked.total : undefined,
selected: 0
};
}
get title() {
return game.i18n.localize('DAGGERHEART.DamageReduction.Title');
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'views', 'damage-reduction'],
position: {
width: 240,
height: 'auto'
},
actions: {
setMarks: this.setMarks,
takeDamage: this.takeDamage
},
form: {
handler: this.updateData,
submitOnChange: true,
closeOnSubmit: false
}
};
/** @override */
static PARTS = {
damageSelection: {
id: 'damageReduction',
template: 'systems/daggerheart/templates/views/damageReduction.hbs'
}
};
/* -------------------------------------------- */
/** @inheritDoc */
get title() {
return `Damage Options`;
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.armorScore = this.actor.system.armorScore;
context.armorMarks = this.actor.system.armor.system.marks.value + this.availableArmorMarks.selected;
context.availableArmorMarks = this.availableArmorMarks;
context.damage = getDamageLabel(this.damage);
context.reducedDamage =
this.availableArmorMarks.selected > 0
? getDamageLabel(this.damage - this.availableArmorMarks.selected)
: null;
return context;
}
static updateData(event, _, formData) {
const form = foundry.utils.expandObject(formData.object);
this.render(true);
}
static setMarks(_, target) {
const index = Number(target.dataset.index);
if (index >= this.availableArmorMarks.maxUseable) {
ui.notifications.info(game.i18n.localize('DAGGERHEART.DamageReduction.Notifications.NotEnoughArmor'));
return;
}
const isDecreasing = index < this.availableArmorMarks.selected;
if (!isDecreasing && this.damage - this.availableArmorMarks.selected === 0) {
ui.notifications.info(game.i18n.localize('DAGGERHEART.DamageReduction.Notifications.DamageAlreadyNone'));
return;
}
this.availableArmorMarks.selected = isDecreasing ? index : index + 1;
this.render();
}
static async takeDamage() {
const armorSpent = this.availableArmorMarks.selected;
const modifiedDamage = this.damage - armorSpent;
this.resolve({ modifiedDamage, armorSpent });
await this.close(true);
}
async close(fromSave) {
if (!fromSave) {
this.reject();
}
await super.close({});
}
}

View file

@ -83,6 +83,18 @@ export default class DhCharacter extends BaseDataActor {
attack: new fields.NumberField({ integer: true, initial: 0 }),
spellcast: new fields.NumberField({ integer: true, initial: 0 }),
armorScore: new fields.NumberField({ integer: true, initial: 0 })
}),
rules: new fields.SchemaField({
maxArmorMarked: new fields.SchemaField({
value: new fields.NumberField({ required: true, integer: true, initial: 1 }),
bonus: new fields.NumberField({ required: true, integer: true, initial: 0 }),
stressExtra: new fields.NumberField({ required: true, integer: true, initial: 0 })
}),
stressDamageReduction: new fields.SchemaField({
enabled: new fields.BooleanField({ required: true, initial: false }),
cost: new fields.NumberField({ integer: true }),
fromSeverity: new fields.NumberField({ integer: true, max: 3 })
})
})
};
}
@ -232,6 +244,9 @@ export default class DhCharacter extends BaseDataActor {
experience.total = experience.value + experience.bonus;
}
this.rules.maxArmorMarked.total = this.rules.maxArmorMarked.value + this.rules.maxArmorMarked.bonus;
this.armorScore = this.armor ? this.armor.system.baseScore + (this.bonuses.armorScore ?? 0) : 0;
this.resources.hitPoints.maxTotal = this.resources.hitPoints.max + this.resources.hitPoints.bonus;
this.resources.stress.maxTotal = this.resources.stress.max + this.resources.stress.bonus;
this.evasion.total = (this.class?.evasion ?? 0) + this.evasion.bonus;

View file

@ -29,7 +29,6 @@ export default class DHArmor extends BaseDataItem {
})
),
marks: new fields.SchemaField({
max: new fields.NumberField({ initial: 6, integer: true }),
value: new fields.NumberField({ initial: 0, integer: true })
}),
baseThresholds: new fields.SchemaField({

View file

@ -4,6 +4,7 @@ import RollSelectionDialog from '../applications/rollSelectionDialog.mjs';
import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs';
import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
import DHDualityRoll from '../data/chat-message/dualityRoll.mjs';
import DamageReductionDialog from '../applications/damageReductionDialog.mjs';
export default class DhpActor extends Actor {
async _preCreate(data, options, user) {
@ -266,21 +267,21 @@ export default class DhpActor extends Actor {
*/
async diceRoll(config, action) {
// console.log(config)
config.source = {...(config.source ?? {}), actor: this.id};
config.source = { ...(config.source ?? {}), actor: this.id };
const newConfig = {
// data: {
...config,
/* action, */
// actor: this.getRollData(),
actor: this.system
...config,
/* action, */
// actor: this.getRollData(),
actor: this.system
// },
// options: {
// dialog: false,
// dialog: false,
// },
// event: config.event
}
};
// console.log(this, newConfig)
const roll = CONFIG.Dice.daggerheart[this.type === 'character' ? 'DualityRoll' : 'D20Roll'].build(newConfig)
const roll = CONFIG.Dice.daggerheart[this.type === 'character' ? 'DualityRoll' : 'D20Roll'].build(newConfig);
return config;
/* let hopeDice = 'd12',
fearDice = 'd12',
@ -508,67 +509,67 @@ export default class DhpActor extends Actor {
async takeDamage(damage, type) {
const hpDamage =
damage >= this.system.damageThresholds.severe
? -3
? 3
: damage >= this.system.damageThresholds.major
? -2
? 2
: damage >= this.system.damageThresholds.minor
? -1
? 1
: 0;
await this.modifyResource([{value: hpDamage, type}]);
/* const update = {
'system.resources.hitPoints.value': Math.min(
this.system.resources.hitPoints.value + hpDamage,
this.system.resources.hitPoints.max
)
};
if (game.user.isGM) {
await this.update(update);
} else {
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateDocument,
uuid: this.uuid,
update: update
}
if (
this.type === 'character' &&
this.system.armor &&
this.system.armor.system.marks.value < this.system.armorScore
) {
new Promise((resolve, reject) => {
new DamageReductionDialog(resolve, reject, this, hpDamage).render(true);
}).then(async ({ modifiedDamage, armorSpent }) => {
const resources = [
{ value: modifiedDamage, type: 'hitPoints' },
...(armorSpent ? [{ value: armorSpent, type: 'armorStack' }] : [])
];
await this.modifyResource(resources);
});
} */
} else {
await this.modifyResource([{ value: hpDamage, type: 'hitPoints' }]);
}
}
async modifyResource(resources) {
if(!resources.length) return;
let updates = { actor: { target: this, resources: {} }, armor: { target: this.armor, resources: {} } };
if (!resources.length) return;
let updates = { actor: { target: this, resources: {} }, armor: { target: this.system.armor, resources: {} } };
resources.forEach(r => {
switch (type) {
case 'armorStrack':
// resource = 'system.stacks.value';
// target = this.armor;
// update = Math.min(this.marks.value + value, this.marks.max);
updates.armor.resources['system.stacks.value'] = Math.min(this.marks.value + value, this.marks.max);
switch (r.type) {
case 'armorStack':
updates.armor.resources['system.marks.value'] = Math.min(
this.system.armor.system.marks.value + r.value,
this.system.armorScore
);
break;
default:
// resource = `system.resources.${type}`;
// target = this;
// update = Math.min(this.resources[type].value + value, this.resources[type].max);
updates.armor.resources[`system.resources.${type}`] = Math.min(this.resources[type].value + value, this.resources[type].max);
updates.actor.resources[`system.resources.${r.type}.value`] = Math.min(
this.system.resources[r.type].value + r.value,
this.system.resources[r.type].max
);
break;
}
})
Object.values(updates).forEach(async (u) => {
if (game.user.isGM) {
await u.target.update(u.resources);
} else {
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateDocument,
uuid: u.target.uuid,
update: u.resources
}
});
});
Object.values(updates).forEach(async u => {
if (Object.keys(u.resources).length > 0) {
if (game.user.isGM) {
await u.target.update(u.resources);
} else {
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateDocument,
uuid: u.target.uuid,
update: u.resources
}
});
}
}
})
});
}
/* async takeHealing(healing, type) {

View file

@ -235,3 +235,16 @@ Roll.replaceFormulaData = function (formula, data, { missing, warn = false } = {
formula = terms.reduce((a, c) => a.replaceAll(`@${c.term}`, data[c.term] ?? c.default), formula);
return nativeReplaceFormulaData(formula, data, { missing, warn });
};
export const getDamageLabel = damage => {
switch (damage) {
case 3:
return game.i18n.localize('DAGGERHEART.General.Damage.Severe');
case 2:
return game.i18n.localize('DAGGERHEART.General.Damage.Major');
case 1:
return game.i18n.localize('DAGGERHEART.General.Damage.Minor');
case 0:
return game.i18n.localize('DAGGERHEART.General.Damage.None');
}
};

View file

@ -3113,6 +3113,80 @@ div.daggerheart.views.multiclass {
.daggerheart.views.ownership-selection .ownership-outer-container .ownership-container select {
margin: 4px 0;
}
.daggerheart.views.damage-reduction .window-content {
padding: 8px 0;
}
.daggerheart.views.damage-reduction .damage-reduction-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.daggerheart.views.damage-reduction .damage-reduction-container .section-container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.daggerheart.views.damage-reduction .damage-reduction-container .padded {
padding: 0 8px;
}
.daggerheart.views.damage-reduction .damage-reduction-container .armor-title {
margin: 0;
}
.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
margin: 0;
}
.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-container {
cursor: pointer;
border: 1px solid light-dark(#18162e, #f3c267);
border-radius: 6px;
height: 26px;
width: 26px;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.4;
}
.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-container.selected {
opacity: 1;
}
.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-container.disabled {
cursor: initial;
}
.daggerheart.views.damage-reduction .damage-reduction-container .markers-subtitle {
margin: -4px 0 0 0;
}
.daggerheart.views.damage-reduction .damage-reduction-container .markers-subtitle.bold {
font-variant: all-small-caps;
font-weight: bold;
}
.daggerheart.views.damage-reduction .damage-reduction-container .damage-container {
display: flex;
justify-content: center;
gap: 4px;
font-weight: bold;
height: 18px;
}
.daggerheart.views.damage-reduction .damage-reduction-container .damage-container i {
font-size: 18px;
}
.daggerheart.views.damage-reduction .damage-reduction-container .damage-container .reduced-value {
opacity: 0.4;
text-decoration: line-through;
}
.daggerheart.views.damage-reduction .damage-reduction-container footer {
display: flex;
width: 100%;
}
.daggerheart.views.damage-reduction .damage-reduction-container footer button {
flex: 1;
}
:root {
--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 0.3s ease, border-color 0.3s ease, opacity 0.3s ease;

View file

@ -11,6 +11,7 @@
@import './characterCreation.less';
@import './levelup.less';
@import './ownershipSelection.less';
@import './damageReduction.less';
@import './resources.less';
@import './countdown.less';
@import './settings.less';

View file

@ -0,0 +1,91 @@
.daggerheart.views.damage-reduction {
.window-content {
padding: 8px 0;
}
.damage-reduction-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
.section-container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.padded {
padding: 0 8px;
}
.armor-title {
margin: 0;
}
.mark-selection {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
margin: 0;
.mark-container {
cursor: pointer;
border: 1px solid light-dark(#18162e, #f3c267);
border-radius: 6px;
height: 26px;
width: 26px;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.4;
&.selected {
opacity: 1;
}
&.disabled {
cursor: initial;
}
}
}
.markers-subtitle {
margin: -4px 0 0 0;
&.bold {
font-variant: all-small-caps;
font-weight: bold;
}
}
.damage-container {
display: flex;
justify-content: center;
gap: 4px;
font-weight: bold;
height: 18px;
i {
font-size: 18px;
}
.reduced-value {
opacity: 0.4;
text-decoration: line-through;
}
}
footer {
display: flex;
width: 100%;
button {
flex: 1;
}
}
}
}

View file

@ -0,0 +1,40 @@
<div class="damage-reduction-container">
<div class="section-container padded">
<h4 class="armor-title">{{localize "DAGGERHEART.DamageReduction.ArmorMarks"}}</h4>
<div class="markers-subtitle">{{armorMarks}}/{{armorScore}}</div>
</div>
<div class="section-container">
<h4 class="mark-selection divider">
{{#times availableArmorMarks.max}}
<div
class="mark-container {{#if (lt this @root.availableArmorMarks.selected)}}selected{{/if}} {{#if (gte this this.maxUseable)}}disabled{{/if}}"
data-action="setMarks" data-index="{{this}}"
>
{{#if (or (not @root.availableArmorMarks.stressIndex) (lt this @root.availableArmorMarks.stressIndex))}}
<i class="fa-solid fa-shield"></i>
{{else}}
<i class="fa-solid fa-bolt"></i>
{{/if}}
</div>
{{/times}}
</h4>
<div class="markers-subtitle bold">{{localize "DAGGERHEART.DamageReduction.UsedMarks"}}</div>
</div>
<div class="section-container">
<div>{{localize "Incoming Damage"}}</div>
<div class="damage-container">
<div class="{{#if this.reducedDamage}}reduced-value{{/if}}">{{this.damage}}</div>
{{#if this.reducedDamage}}
<i class="fa-solid fa-arrow-right-long"></i>
<div>{{this.reducedDamage}}</div>
{{/if}}
</div>
</div>
<footer class="padded">
<button type="button" data-action="takeDamage">{{localize "Take Damage"}}</button>
</footer>
</div>