Feature/112 items use action datamodel (#194)

* Fix action for items

* Cost & Range #1

* remove log

* actions

* Split methods

* Roll classes

* Begin damage

* g

* Actions

* before main merge

* Fix d20RollDialog costs check

* Fix submit on close

* Add uses in action dialog

* Adversary Attack

* 166 - Damage Reduction (#180)

* Temp

* Fixed Stress Reductions

* Changed from index based to object

* Fixed stress resources management for DamageReduction

* Fix Adversary attack multiplier

* Auto add Attack action to newly created weapon

* Few fixes

* 164 - Add Hope/Fear formula

* 163 - Actor Sub Datas (#182)

* Added rules/bonuses for all classes and subclasses

* More

* Add Save

* Fix delete action button

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
Co-authored-by: WBHarry <89362246+WBHarry@users.noreply.github.com>
This commit is contained in:
Dapoulp 2025-06-28 19:01:08 +02:00 committed by GitHub
parent 1135669d0b
commit 3593f44612
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
77 changed files with 3707 additions and 1828 deletions

View file

@ -16,6 +16,7 @@ import Resources from './module/applications/resources.mjs';
import { NarrativeCountdowns, registerCountdownApplicationHooks } from './module/applications/countdowns.mjs'; import { NarrativeCountdowns, registerCountdownApplicationHooks } from './module/applications/countdowns.mjs';
import DHDualityRoll from './module/data/chat-message/dualityRoll.mjs'; import DHDualityRoll from './module/data/chat-message/dualityRoll.mjs';
import { DualityRollColor } from './module/data/settings/Appearance.mjs'; import { DualityRollColor } from './module/data/settings/Appearance.mjs';
import { DHRoll, DualityRoll, D20Roll, DamageRoll, DualityDie } from './module/applications/roll.mjs';
import { DhMeasuredTemplate } from './module/placeables/_module.mjs'; import { DhMeasuredTemplate } from './module/placeables/_module.mjs';
import { renderDualityButton } from './module/enrichers/DualityRollEnricher.mjs'; import { renderDualityButton } from './module/enrichers/DualityRollEnricher.mjs';
import { renderMeasuredTemplate } from './module/enrichers/TemplateEnricher.mjs'; import { renderMeasuredTemplate } from './module/enrichers/TemplateEnricher.mjs';
@ -50,6 +51,15 @@ Hooks.once('init', () => {
name: game.i18n.localize(x.name) name: game.i18n.localize(x.name)
})); }));
CONFIG.Dice.daggerheart = {
DualityDie: DualityDie,
DHRoll: DHRoll,
DualityRoll: DualityRoll,
D20Roll: D20Roll,
DamageRoll: DamageRoll
};
CONFIG.Dice.rolls = [...CONFIG.Dice.rolls, ...[DHRoll, DualityRoll, D20Roll, DamageRoll]];
CONFIG.MeasuredTemplate.objectClass = DhMeasuredTemplate; CONFIG.MeasuredTemplate.objectClass = DhMeasuredTemplate;
CONFIG.Item.documentClass = documents.DhpItem; CONFIG.Item.documentClass = documents.DhpItem;
@ -307,9 +317,12 @@ const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/views/actionTypes/uuid.hbs', 'systems/daggerheart/templates/views/actionTypes/uuid.hbs',
'systems/daggerheart/templates/views/actionTypes/uses.hbs', 'systems/daggerheart/templates/views/actionTypes/uses.hbs',
'systems/daggerheart/templates/views/actionTypes/roll.hbs', 'systems/daggerheart/templates/views/actionTypes/roll.hbs',
'systems/daggerheart/templates/views/actionTypes/save.hbs',
'systems/daggerheart/templates/views/actionTypes/cost.hbs', 'systems/daggerheart/templates/views/actionTypes/cost.hbs',
'systems/daggerheart/templates/views/actionTypes/range-target.hbs', 'systems/daggerheart/templates/views/actionTypes/range-target.hbs',
'systems/daggerheart/templates/views/actionTypes/effect.hbs', 'systems/daggerheart/templates/views/actionTypes/effect.hbs',
'systems/daggerheart/templates/settings/components/settings-item-line.hbs' 'systems/daggerheart/templates/settings/components/settings-item-line.hbs',
'systems/daggerheart/templates/chat/parts/target-chat.hbs'
]); ]);
}; };

View file

@ -207,6 +207,12 @@
"Session": "Session", "Session": "Session",
"Shortrest": "Short Rest", "Shortrest": "Short Rest",
"Longrest": "Long Rest" "Longrest": "Long Rest"
},
"Damage": {
"Severe": "Severe",
"Major": "Major",
"Minor": "Minor",
"None": "None"
} }
}, },
"ActionType": { "ActionType": {
@ -617,6 +623,14 @@
"Stress": { "Stress": {
"Name": "Stress", "Name": "Stress",
"Stress": "STR" "Stress": "STR"
},
"Hope": {
"Name": "Hope",
"Abbreviation": "HO"
},
"ArmorStack": {
"Name": "Armor Stack",
"Stress": "AS"
} }
}, },
"ArmorFeature": { "ArmorFeature": {
@ -1004,14 +1018,20 @@
}, },
"AttackRoll": { "AttackRoll": {
"Title": "Attack - {attack}", "Title": "Attack - {attack}",
"RollDamage": "Roll Damage" "RollDamage": "Roll Damage",
"RollHealing": "Roll Healing",
"ApplyEffect": "Apply Effects"
}, },
"DamageRoll": { "DamageRoll": {
"Title": "Damage - {damage}", "Title": "Damage - {damage}",
"DealDamageToTargets": "Damage Hit Targets", "DealDamageToTargets": "Damage Hit Targets",
"DealDamage": "Deal Damage" "DealDamage": "Deal Damage"
}, },
"ApplyEffect": {
"Title": "Apply Effects - {name}"
},
"HealingRoll": { "HealingRoll": {
"Title": "Heal - {healing}",
"Heal": "Heal" "Heal": "Heal"
}, },
"DeathMove": { "DeathMove": {
@ -1070,6 +1090,21 @@
"Title": "Ownership Selection - {name}", "Title": "Ownership Selection - {name}",
"Default": "Default Ownership" "Default": "Default Ownership"
}, },
"DamageReduction": {
"Title": "Damage Reduction",
"ArmorMarks": "Armor Marks",
"UsedMarks": "Used Marks",
"Stress": "Stress",
"ArmorWithStress": "Spend 1 stress to use an extra mark",
"UnncessaryStress": "You don't need to expend stress",
"StressReduction": "Reduce By Stress",
"Notifications": {
"DamageAlreadyNone": "The damage has already been reduced to none",
"NoAvailableArmorMarks": "You have no more available armor marks",
"NotEnoughStress": "You don't have enough stress",
"DamageIgnore": "{character} did not take damage"
}
},
"Sheets": { "Sheets": {
"TABS": { "TABS": {
"features": "Features", "features": "Features",
@ -1506,6 +1541,11 @@
"Macro": { "Macro": {
"Name": "Macro" "Name": "Macro"
} }
},
"Settings": {
"ResultBased": {
"label": "Formula based on Hope/Fear result."
}
} }
}, },
"RollTypes": { "RollTypes": {

View file

@ -1,23 +1,36 @@
import { DualityRollColor } from '../data/settings/Appearance.mjs';
import DHDualityRoll from '../data/chat-message/dualityRoll.mjs';
export default class DhpChatMessage extends foundry.documents.ChatMessage { export default class DhpChatMessage extends foundry.documents.ChatMessage {
async renderHTML() { async renderHTML() {
if (this.type === 'dualityRoll' || this.type === 'adversaryRoll') { if(this.system.messageTemplate) this.content = await foundry.applications.handlebars.renderTemplate(this.system.messageTemplate, this.system);
this.content = await foundry.applications.handlebars.renderTemplate(this.content, this.system);
}
/* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */ /* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */
const html = await super.renderHTML(); const html = await super.renderHTML();
this.applyPermission(html);
if (this.type === 'dualityRoll') { if (this.type === 'dualityRoll') {
html.classList.add('duality'); html.classList.add('duality');
const dualityResult = this.system.dualityResult; switch (this.system.roll.result.duality) {
if (dualityResult === DHDualityRoll.dualityResult.hope) html.classList.add('hope'); case 1:
else if (dualityResult === DHDualityRoll.dualityResult.fear) html.classList.add('fear'); html.classList.add('hope');
else html.classList.add('critical'); break;
case -1:
html.classList.add('fear');
break;
default:
html.classList.add('critical');
break;
}
} }
return html; return html;
} }
applyPermission(html) {
const elements = html.querySelectorAll('[data-perm-id]');
elements.forEach(e => {
const uuid = e.dataset.permId,
document = fromUuidSync(uuid);
e.setAttribute('data-view-perm', document.testUserPermission(game.user, 'OBSERVER'));
e.setAttribute('data-use-perm', document.testUserPermission(game.user, 'OWNER'));
});
}
} }

View file

@ -38,6 +38,8 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
} }
}; };
static CLEAN_ARRAYS = ["damage.parts", "cost", "effects"];
_getTabs() { _getTabs() {
const tabs = { const tabs = {
base: { active: true, cssClass: '', group: 'primary', id: 'base', icon: null, label: 'Base' }, base: { active: true, cssClass: '', group: 'primary', id: 'base', icon: null, label: 'Base' },
@ -60,8 +62,14 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
context.tabs = this._getTabs(); context.tabs = this._getTabs();
context.config = SYSTEM; context.config = SYSTEM;
if (!!this.action.effects) context.effects = this.action.effects.map(e => this.action.item.effects.get(e._id)); if (!!this.action.effects) context.effects = this.action.effects.map(e => this.action.item.effects.get(e._id));
if (this.action.damage?.hasOwnProperty('includeBase')) context.hasBaseDamage = !!this.action.parent.damage; if (this.action.damage?.hasOwnProperty('includeBase') && this.action.type === 'attack')
context.hasBaseDamage = !!this.action.parent.damage;
context.getRealIndex = this.getRealIndex.bind(this); context.getRealIndex = this.getRealIndex.bind(this);
context.getEffectDetails = this.getEffectDetails.bind(this);
context.disableOption = this.disableOption.bind(this);
context.isNPC = this.action.actor && this.action.actor.type !== 'character';
context.hasRoll = this.action.hasRoll;
console.log(context)
return context; return context;
} }
@ -70,27 +78,47 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
this.render(true); this.render(true);
} }
disableOption(index, options, choices) {
const filtered = foundry.utils.deepClone(options);
Object.keys(filtered).forEach(o => {
if (choices.find((c, idx) => c.type === o && index !== idx)) delete filtered[o];
});
return filtered;
}
getRealIndex(index) { getRealIndex(index) {
const data = this.action.toObject(false); const data = this.action.toObject(false);
return data.damage.parts.find(d => d.base) ? index - 1 : index; return data.damage.parts.find(d => d.base) ? index - 1 : index;
} }
getEffectDetails(id) {
return this.action.item.effects.get(id);
}
_prepareSubmitData(event, formData) { _prepareSubmitData(event, formData) {
const submitData = foundry.utils.expandObject(formData.object); const submitData = foundry.utils.expandObject(formData.object);
// this.element.querySelectorAll("fieldset[disabled] :is(input, select)").forEach(input => { for ( const keyPath of this.constructor.CLEAN_ARRAYS ) {
// foundry.utils.setProperty(submitData, input.name, input.value); const data = foundry.utils.getProperty(submitData, keyPath);
// }); if ( data ) foundry.utils.setProperty(submitData, keyPath, Object.values(data));
}
return submitData; return submitData;
} }
static async updateForm(event, _, formData) { static async updateForm(event, _, formData) {
const submitData = this._prepareSubmitData(event, formData), const submitData = this._prepareSubmitData(event, formData),
data = foundry.utils.expandObject(foundry.utils.mergeObject(this.action.toObject(), submitData)), data = foundry.utils.mergeObject(this.action.toObject(), submitData),
container = foundry.utils.getProperty(this.action.parent, this.action.systemPath);
let newActions;
if (Array.isArray(container)) {
newActions = foundry.utils.getProperty(this.action.parent, this.action.systemPath).map(x => x.toObject()); // Find better way newActions = foundry.utils.getProperty(this.action.parent, this.action.systemPath).map(x => x.toObject()); // Find better way
if (!newActions.findSplice(x => x._id === data._id, data)) newActions.push(data); if (!newActions.findSplice(x => x._id === data._id, data)) newActions.push(data);
} else newActions = data;
const updates = await this.action.parent.parent.update({ [`system.${this.action.systemPath}`]: newActions }); const updates = await this.action.parent.parent.update({ [`system.${this.action.systemPath}`]: newActions });
if (!updates) return; if (!updates) return;
this.action = foundry.utils.getProperty(updates.system, this.action.systemPath)[this.action.index]; this.action = Array.isArray(container)
? foundry.utils.getProperty(updates.system, this.action.systemPath)[this.action.index]
: foundry.utils.getProperty(updates.system, this.action.systemPath);
this.render(); this.render();
} }
@ -103,6 +131,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
} }
static removeElement(event) { static removeElement(event) {
event.stopPropagation();
const data = this.action.toObject(), const data = this.action.toObject(),
key = event.target.closest('.action-category-data').dataset.key, key = event.target.closest('.action-category-data').dataset.key,
index = event.target.dataset.index; index = event.target.dataset.index;
@ -132,6 +161,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
data = this.action.toObject(); data = this.action.toObject();
data.effects.push({ _id: created._id }); data.effects.push({ _id: created._id });
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
this.action.item.effects.get(created._id).sheet.render(true);
} }
/** /**
@ -156,5 +186,8 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
this.action.item.deleteEmbeddedDocuments('ActiveEffect', [effectId]); this.action.item.deleteEmbeddedDocuments('ActiveEffect', [effectId]);
} }
static editEffect(event) {} static editEffect(event) {
const id = event.target.closest('[data-effect-id]')?.dataset?.effectId;
this.action.item.effects.get(id).sheet.render(true);
}
} }

View file

@ -21,4 +21,15 @@ export default class DhContextMenu extends ContextMenu {
item?.callback(this.#jQuery ? $(this.target) : this.target, event); item?.callback(this.#jQuery ? $(this.target) : this.target, event);
this.close(); this.close();
} }
static triggerContextMenu(event) {
event.preventDefault();
event.stopPropagation();
const { clientX, clientY } = event;
const selector = "[data-item-id]";
const target = event.target.closest(selector) ?? event.currentTarget.closest(selector);
target?.dispatchEvent(new PointerEvent("contextmenu", {
view: window, bubbles: true, cancelable: true, clientX, clientY
}));
}
} }

View file

@ -0,0 +1,66 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class CostSelectionDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(costs, uses, action, resolve) {
super({});
this.costs = costs;
this.uses = uses;
this.action = action;
this.resolve = resolve;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'views', 'damage-selection'],
position: {
width: 400,
height: 'auto'
},
actions: {
sendCost: this.sendCost
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
}
};
/** @override */
static PARTS = {
costSelection: {
id: 'costSelection',
template: 'systems/daggerheart/templates/views/costSelection.hbs'
}
};
/* -------------------------------------------- */
/** @inheritDoc */
get title() {
return `Cost Options`;
}
async _prepareContext(_options) {
const updatedCosts = this.action.calcCosts(this.costs),
updatedUses = this.action.calcUses(this.uses);
return {
costs: updatedCosts,
uses: updatedUses,
canUse: this.action.hasCost(updatedCosts) && this.action.hasUses(updatedUses)
};
}
static async updateForm(event, _, formData) {
const data = foundry.utils.expandObject(formData.object);
this.costs = foundry.utils.mergeObject(this.costs, data.costs);
this.uses = foundry.utils.mergeObject(this.uses, data.uses);
this.render(true);
}
static sendCost(event) {
event.preventDefault();
this.resolve({ costs: this.action.getRealCosts(this.costs), uses: this.uses });
this.close();
}
}

View file

@ -0,0 +1,220 @@
import { damageKeyToNumber, 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;
const maxArmorMarks = Math.min(
actor.system.armorScore - actor.system.armor.system.marks.value,
actor.system.rules.maxArmorMarked.total
);
const armor = [...Array(maxArmorMarks).keys()].reduce((acc, _) => {
acc[foundry.utils.randomID()] = { selected: false };
return acc;
}, {});
const stress = [...Array(actor.system.rules.maxArmorMarked.stressExtra ?? 0).keys()].reduce((acc, _) => {
acc[foundry.utils.randomID()] = { selected: false };
return acc;
}, {});
this.marks = { armor, stress };
this.availableStressReductions = Object.keys(actor.system.rules.stressDamageReduction).reduce((acc, key) => {
const dr = actor.system.rules.stressDamageReduction[key];
if (dr.enabled) {
if (acc === null) acc = {};
const damage = damageKeyToNumber(key);
acc[damage] = {
cost: dr.cost,
selected: false,
from: getDamageLabel(damage),
to: getDamageLabel(damage - 1)
};
}
return acc;
}, null);
}
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,
useStressReduction: this.useStressReduction,
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 game.i18n.localize('DAGGERHEART.DamageReduction.Title');
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
const { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage } =
this.getDamageInfo();
context.armorScore = this.actor.system.armorScore;
context.armorMarks = currentMarks;
context.basicMarksUsed = selectedArmorMarks.length === this.actor.system.rules.maxArmorMarked.total;
const stressReductionStress = this.availableStressReductions
? stressReductions.reduce((acc, red) => acc + red.cost, 0)
: 0;
context.stress =
selectedStressMarks.length > 0 || this.availableStressReductions
? {
value:
this.actor.system.resources.stress.value + selectedStressMarks.length + stressReductionStress,
maxTotal: this.actor.system.resources.stress.maxTotal
}
: null;
context.marks = this.marks;
context.availableStressReductions = this.availableStressReductions;
context.damage = getDamageLabel(this.damage);
context.reducedDamage = currentDamage !== this.damage ? getDamageLabel(currentDamage) : null;
context.currentDamage = context.reducedDamage ?? context.damage;
return context;
}
static updateData(event, _, formData) {
const form = foundry.utils.expandObject(formData.object);
this.render(true);
}
getDamageInfo = () => {
const selectedArmorMarks = Object.values(this.marks.armor).filter(x => x.selected);
const selectedStressMarks = Object.values(this.marks.stress).filter(x => x.selected);
const stressReductions = Object.values(this.availableStressReductions).filter(red => red.selected);
const currentMarks =
this.actor.system.armor.system.marks.value + selectedArmorMarks.length + selectedStressMarks.length;
const currentDamage =
this.damage - selectedArmorMarks.length - selectedStressMarks.length - stressReductions.length;
return { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage };
};
static setMarks(_, target) {
const currentMark = this.marks[target.dataset.type][target.dataset.key];
const { selectedStressMarks, stressReductions, currentMarks, currentDamage } = this.getDamageInfo();
if (!currentMark.selected && currentDamage === 0) {
ui.notifications.info(game.i18n.localize('DAGGERHEART.DamageReduction.Notifications.DamageAlreadyNone'));
return;
}
if (!currentMark.selected && currentMarks === this.actor.system.armorScore) {
ui.notifications.info(
game.i18n.localize('DAGGERHEART.DamageReduction.Notifications.NoAvailableArmorMarks')
);
return;
}
if (currentMark.selected) {
const currentDamageLabel = getDamageLabel(currentDamage);
for (let reduction of stressReductions) {
if (reduction.selected && reduction.to === currentDamageLabel) {
reduction.selected = false;
}
}
if (target.dataset.type === 'armor' && selectedStressMarks.length > 0) {
selectedStressMarks.forEach(mark => (mark.selected = false));
}
}
currentMark.selected = !currentMark.selected;
this.render();
}
static useStressReduction(_, target) {
const damageValue = Number(target.dataset.reduction);
const stressReduction = this.availableStressReductions[damageValue];
const { currentDamage, selectedStressMarks, stressReductions } = this.getDamageInfo();
if (stressReduction.selected) {
stressReduction.selected = false;
const currentDamageLabel = getDamageLabel(currentDamage);
for (let reduction of stressReductions) {
if (reduction.selected && reduction.to === currentDamageLabel) {
reduction.selected = false;
}
}
this.render();
} else {
const stressReductionStress = this.availableStressReductions
? stressReductions.reduce((acc, red) => acc + red.cost, 0)
: 0;
const currentStress =
this.actor.system.resources.stress.value + selectedStressMarks.length + stressReductionStress;
if (currentStress + stressReduction.cost > this.actor.system.resources.stress.maxTotal) {
ui.notifications.info(game.i18n.localize('DAGGERHEART.DamageReduction.Notifications.NotEnoughStress'));
return;
}
const reducedDamage = currentDamage !== this.damage ? getDamageLabel(currentDamage) : null;
const currentDamageLabel = reducedDamage ?? getDamageLabel(this.damage);
if (stressReduction.from !== currentDamageLabel) return;
stressReduction.selected = true;
this.render();
}
}
static async takeDamage() {
const { selectedArmorMarks, selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo();
const armorSpent = selectedArmorMarks.length + selectedStressMarks.length;
const stressSpent = selectedStressMarks.length + stressReductions.reduce((acc, red) => acc + red.cost, 0);
this.resolve({ modifiedDamage: currentDamage, armorSpent, stressSpent });
await this.close(true);
}
async close(fromSave) {
if (!fromSave) {
this.reject();
}
await super.close({});
}
}

View file

@ -0,0 +1,403 @@
import D20RollDialog from '../dialogs/d20RollDialog.mjs';
import DamageDialog from '../dialogs/damageDialog.mjs';
/*
- Damage & other resources roll
- Close dialog => don't roll
*/
export class DHRoll extends Roll {
constructor(formula, data, options) {
super(formula, data, options);
}
static async build(config = {}, message = {}) {
const roll = await this.buildConfigure(config, message);
if (!roll) return;
await this.buildEvaluate(roll, config, (message = {}));
await this.buildPost(roll, config, (message = {}));
return config;
}
static async buildConfigure(config = {}, message = {}) {
config.hooks = [...(config.hooks ?? []), ''];
config.dialog ??= {};
for (const hook of config.hooks) {
if (Hooks.call(`${SYSTEM.id}.preRoll${hook.capitalize()}`, config, message) === false) return null;
}
this.applyKeybindings(config);
if (config.dialog.configure !== false) {
// Open Roll Dialog
const DialogClass = config.dialog?.class ?? this.DefaultDialog;
config = await DialogClass.configure(config, message);
if (!config) return;
}
let roll = new this(config.formula, config.data, config);
for (const hook of config.hooks) {
if (Hooks.call(`${SYSTEM.id}.post${hook.capitalize()}RollConfiguration`, roll, config, message) === false)
return [];
}
return roll;
}
static async buildEvaluate(roll, config = {}, message = {}) {
if (config.evaluate !== false) await roll.evaluate();
this.postEvaluate(roll, config);
}
static async buildPost(roll, config, message) {
for (const hook of config.hooks) {
if (Hooks.call(`${SYSTEM.id}.postRoll${hook.capitalize()}`, config, message) === false) return null;
}
// Create Chat Message
if (message.data) {
} else {
const messageData = {};
config.message = await this.toMessage(roll, config);
}
}
static async postEvaluate(roll, config = {}) {}
static async toMessage(roll, config) {
const cls = getDocumentClass('ChatMessage'),
msg = {
type: this.messageType,
user: game.user.id,
sound: config.mute ? null : CONFIG.sounds.dice,
system: config,
rolls: [roll]
};
return await cls.create(msg);
}
static applyKeybindings(config) {
config.dialog.configure ??= true;
}
}
// DHopeDie
// DFearDie
// DualityDie
// D20Die
export class DualityDie extends foundry.dice.terms.Die {
constructor({ number = 1, faces = 12, ...args } = {}) {
super({ number, faces, ...args });
}
}
export class D20Roll extends DHRoll {
constructor(formula, data = {}, options = {}) {
super(formula, data, options);
this.createBaseDice();
this.configureModifiers();
this._formula = this.resetFormula();
}
static ADV_MODE = {
NORMAL: 0,
ADVANTAGE: 1,
DISADVANTAGE: -1
};
static messageType = 'adversaryRoll';
static CRITICAL_TRESHOLD = 20;
static DefaultDialog = D20RollDialog;
get d20() {
if (!(this.terms[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
return this.terms[0];
}
set d20(faces) {
if (!(this.terms[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
this.terms[0].faces = faces;
}
get dAdvantage() {
return this.dice[2];
}
get isCritical() {
if (!this.d20._evaluated) return;
return this.d20.total >= this.constructor.CRITICAL_TRESHOLD;
}
get hasAdvantage() {
return this.options.advantage === this.constructor.ADV_MODE.ADVANTAGE;
}
get hasDisadvantage() {
return this.options.advantage === this.constructor.ADV_MODE.DISADVANTAGE;
}
static applyKeybindings(config) {
const keys = {
normal: config.event.shiftKey || config.event.altKey || config.event.ctrlKey,
advantage: config.event.altKey,
disadvantage: config.event.ctrlKey
};
// Should the roll configuration dialog be displayed?
config.dialog.configure ??= !Object.values(keys).some(k => k);
// Determine advantage mode
const advantage = config.advantage || keys.advantage;
const disadvantage = config.disadvantage || keys.disadvantage;
if (advantage && !disadvantage) config.advantage = this.ADV_MODE.ADVANTAGE;
else if (!advantage && disadvantage) config.advantage = this.ADV_MODE.DISADVANTAGE;
else config.advantage = this.ADV_MODE.NORMAL;
}
createBaseDice() {
if (this.terms[0] instanceof foundry.dice.terms.Die) return;
this.terms[0] = new foundry.dice.terms.Die({ faces: 20 });
}
applyAdvantage() {
this.d20.modifiers.findSplice(m => ['kh', 'kl'].includes(m));
if (!this.hasAdvantage && !this.hasAdvantage) this.number = 1;
else {
this.d20.number = 2;
this.d20.modifiers.push(this.hasAdvantage ? 'kh' : 'kl');
}
}
// Trait bonus != Adversary
configureModifiers() {
this.applyAdvantage();
this.applyBaseBonus();
this.options.experiences?.forEach(m => {
if (this.options.data.experiences?.[m])
this.options.roll.modifiers.push({
label: this.options.data.experiences[m].description,
value: this.options.data.experiences[m].total
});
});
this.options.roll.modifiers?.forEach(m => {
this.terms.push(...this.formatModifier(m.value));
});
if (this.options.extraFormula)
this.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...this.constructor.parse(this.options.extraFormula, this.getRollData())
);
// this.resetFormula();
}
applyBaseBonus() {
if (this.options.type === 'attack')
this.terms.push(...this.formatModifier(this.options.data.attack.roll.bonus));
}
static async postEvaluate(roll, config = {}) {
if (config.targets?.length) {
config.targets.forEach(target => {
const difficulty = config.roll.difficulty ?? target.difficulty ?? target.evasion;
target.hit = this.isCritical || roll.total >= difficulty;
});
} else if (config.roll.difficulty) config.roll.success = roll.isCritical || roll.total >= config.roll.difficulty;
config.roll.total = roll.total;
config.roll.formula = roll.formula;
config.roll.advantage = {
type: config.advantage,
dice: roll.dAdvantage?.denomination,
value: roll.dAdvantage?.total
};
config.roll.modifierTotal = config.roll.modifiers.reduce((a, c) => a + c.value, 0);
config.roll.dice = [];
roll.dice.forEach(d => {
config.roll.dice.push({
dice: d.denomination,
total: d.total,
formula: d.formula,
results: d.results
});
});
}
getRollData() {
return this.options.data();
}
formatModifier(modifier) {
const numTerm = modifier < 0 ? '-' : '+';
return [
new foundry.dice.terms.OperatorTerm({ operator: numTerm }),
new foundry.dice.terms.NumericTerm({ number: Math.abs(modifier) })
];
}
resetFormula() {
return (this._formula = this.constructor.getFormula(this.terms));
}
}
export class DualityRoll extends D20Roll {
constructor(formula, data = {}, options = {}) {
super(formula, data, options);
}
static messageType = 'dualityRoll';
static DefaultDialog = D20RollDialog;
get dHope() {
// if ( !(this.terms[0] instanceof foundry.dice.terms.Die) ) return;
if (!(this.dice[0] instanceof CONFIG.Dice.daggerheart.DualityDie)) this.createBaseDice();
return this.dice[0];
// return this.#hopeDice;
}
set dHope(faces) {
if (!(this.dice[0] instanceof CONFIG.Dice.daggerheart.DualityDie)) this.createBaseDice();
this.terms[0].faces = faces;
// this.#hopeDice = `d${face}`;
}
get dFear() {
// if ( !(this.terms[1] instanceof foundry.dice.terms.Die) ) return;
if (!(this.dice[1] instanceof CONFIG.Dice.daggerheart.DualityDie)) this.createBaseDice();
return this.dice[1];
// return this.#fearDice;
}
set dFear(faces) {
if (!(this.dice[1] instanceof CONFIG.Dice.daggerheart.DualityDie)) this.createBaseDice();
this.dice[1].faces = faces;
// this.#fearDice = `d${face}`;
}
get dAdvantage() {
return this.dice[2];
}
get isCritical() {
if (!this.dHope._evaluated || !this.dFear._evaluated) return;
return this.dHope.total === this.dFear.total;
}
get withHope() {
if (!this._evaluated) return;
return this.dHope.total > this.dFear.total;
}
get withFear() {
if (!this._evaluated) return;
return this.dHope.total < this.dFear.total;
}
get hasBarRally() {
return null;
}
get totalLabel() {
const label = this.withHope
? 'DAGGERHEART.General.Hope'
: this.withFear
? 'DAGGERHEART.General.Fear'
: 'DAGGERHEART.General.CriticalSuccess';
return game.i18n.localize(label);
}
createBaseDice() {
if (
this.dice[0] instanceof CONFIG.Dice.daggerheart.DualityDie &&
this.dice[1] instanceof CONFIG.Dice.daggerheart.DualityDie
)
return;
if (!(this.dice[0] instanceof CONFIG.Dice.daggerheart.DualityDie))
this.terms[0] = new CONFIG.Dice.daggerheart.DualityDie();
this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' });
if (!(this.dice[2] instanceof CONFIG.Dice.daggerheart.DualityDie))
this.terms[2] = new CONFIG.Dice.daggerheart.DualityDie();
}
applyAdvantage() {
const dieFaces = 6,
bardRallyFaces = this.hasBarRally,
advDie = new foundry.dice.terms.Die({ faces: dieFaces });
if (this.hasAdvantage || this.hasDisadvantage || bardRallyFaces)
this.terms.push(new foundry.dice.terms.OperatorTerm({ operator: '+' }));
if (bardRallyFaces) {
const rallyDie = new foundry.dice.terms.Die({ faces: bardRallyFaces });
if (this.hasAdvantage) {
this.terms.push(
new foundry.dice.terms.PoolTerm({
terms: [advDie.formula, rallyDie.formula],
modifiers: ['kh']
})
);
} else if (this.hasDisadvantage) {
this.terms.push(advDie, new foundry.dice.terms.OperatorTerm({ operator: '+' }), rallyDie);
}
} else if (this.hasAdvantage || this.hasDisadvantage) this.terms.push(advDie);
}
applyBaseBonus() {
if (!this.options.roll.modifiers) this.options.roll.modifiers = [];
if (this.options.roll?.trait)
this.options.roll.modifiers.push({
label: `DAGGERHEART.Abilities.${this.options.roll.trait}.name`,
value: this.options.data.traits[this.options.roll.trait].total
});
}
static async postEvaluate(roll, config = {}) {
super.postEvaluate(roll, config);
config.roll.hope = {
dice: roll.dHope.denomination,
value: roll.dHope.total
};
config.roll.fear = {
dice: roll.dFear.denomination,
value: roll.dFear.total
};
config.roll.result = {
duality: roll.withHope ? 1 : roll.withFear ? -1 : 0,
total: roll.dHope.total + roll.dFear.total,
label: roll.totalLabel
};
}
}
export class DamageRoll extends DHRoll {
constructor(formula, data = {}, options = {}) {
super(formula, data, options);
}
static messageType = 'damageRoll';
static DefaultDialog = DamageDialog;
static async postEvaluate(roll, config = {}) {
config.roll = {
total: roll.total,
formula: roll.formula,
type: config.type
};
config.roll.dice = [];
roll.dice.forEach(d => {
config.roll.dice.push({
dice: d.denomination,
total: d.total,
formula: d.formula,
results: d.results
});
});
}
}

View file

@ -1,10 +1,12 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class RollSelectionDialog extends HandlebarsApplicationMixin(ApplicationV2) { export default class RollSelectionDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(experiences, hopeResource, resolve) { constructor(experiences, costs, action, resolve) {
super({}, {}); super({}, {});
this.experiences = experiences; this.experiences = experiences;
this.costs = costs;
this.action = action;
this.resolve = resolve; this.resolve = resolve;
this.isNpc; this.isNpc;
this.selectedExperiences = []; this.selectedExperiences = [];
@ -15,8 +17,7 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl
], ],
hope: ['d12'], hope: ['d12'],
fear: ['d12'], fear: ['d12'],
advantage: null, advantage: null
hopeResource: hopeResource
}; };
} }
@ -42,6 +43,10 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl
/** @override */ /** @override */
static PARTS = { static PARTS = {
costSelection: {
id: 'costSelection',
template: 'systems/daggerheart/templates/views/costSelection.hbs'
},
damageSelection: { damageSelection: {
id: 'damageSelection', id: 'damageSelection',
template: 'systems/daggerheart/templates/views/rollSelection.hbs' template: 'systems/daggerheart/templates/views/rollSelection.hbs'
@ -60,15 +65,19 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl
context.fear = this.data.fear; context.fear = this.data.fear;
context.advantage = this.data.advantage; context.advantage = this.data.advantage;
context.experiences = Object.keys(this.experiences).map(id => ({ id, ...this.experiences[id] })); context.experiences = Object.keys(this.experiences).map(id => ({ id, ...this.experiences[id] }));
context.hopeResource = this.data.hopeResource + 1; if(this.costs?.length) {
const updatedCosts = this.action.calcCosts(this.costs);
context.costs = updatedCosts
context.canRoll = this.action.getRealCosts(updatedCosts)?.hasCost;
} else context.canRoll = true;
return context; return context;
} }
static updateSelection(event, _, formData) { static updateSelection(event, _, formData) {
const { ...rest } = foundry.utils.expandObject(formData.object); const { ...rest } = foundry.utils.expandObject(formData.object);
this.data = foundry.utils.mergeObject(this.data, rest); this.data = foundry.utils.mergeObject(this.data, rest);
this.costs = foundry.utils.mergeObject(this.costs, rest.costs);
this.render(); this.render();
} }
@ -90,10 +99,10 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl
static async finish() { static async finish() {
const { diceOptions, ...rest } = this.data; const { diceOptions, ...rest } = this.data;
this.resolve({ this.resolve({
...rest, ...rest,
experiences: this.selectedExperiences.map(x => ({ id: x, ...this.experiences[x] })) experiences: this.selectedExperiences.map(x => ({ id: x, ...this.experiences[x] })),
costs: this.action.getRealCosts(this.costs)
}); });
this.close(); this.close();
} }

View file

@ -1,3 +1,4 @@
import DHActionConfig from '../config/Action.mjs';
import DaggerheartSheet from './daggerheart-sheet.mjs'; import DaggerheartSheet from './daggerheart-sheet.mjs';
const { ActorSheetV2 } = foundry.applications.sheets; const { ActorSheetV2 } = foundry.applications.sheets;
@ -9,6 +10,7 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) {
actions: { actions: {
reactionRoll: this.reactionRoll, reactionRoll: this.reactionRoll,
attackRoll: this.attackRoll, attackRoll: this.attackRoll,
attackConfigure: this.attackConfigure,
addExperience: this.addExperience, addExperience: this.addExperience,
removeExperience: this.removeExperience, removeExperience: this.removeExperience,
toggleHP: this.toggleHP, toggleHP: this.toggleHP,
@ -51,7 +53,10 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.document = this.document; context.document = this.document;
context.tabs = super._getTabs(this.constructor.TABS); context.tabs = super._getTabs(this.constructor.TABS);
context.systemFields.attack.fields = this.document.system.attack.schema.fields;
context.getEffectDetails = this.getEffectDetails.bind(this);
context.isNPC = true;
console.log(context)
return context; return context;
} }
@ -77,26 +82,16 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) {
this.actor.diceRoll(config); this.actor.diceRoll(config);
} }
getEffectDetails(id) {
return {};
}
static async attackRoll(event) { static async attackRoll(event) {
const { modifier, damage, name: attackName } = this.actor.system.attack, this.actor.system.attack.use(event);
config = { }
event: event,
title: attackName, static async attackConfigure(event) {
roll: { await new DHActionConfig(this.document.system.attack).render(true);
modifier: modifier,
type: 'action'
},
chatMessage: {
type: 'adversaryRoll',
template: 'systems/daggerheart/templates/chat/adversary-attack-roll.hbs'
},
damage: {
value: damage.value,
type: damage.type
},
checkTarget: true
};
this.actor.diceRoll(config);
} }
static async addExperience() { static async addExperience() {

View file

@ -139,9 +139,10 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
*/ */
static async #removeAction(event, button) { static async #removeAction(event, button) {
event.stopPropagation(); event.stopPropagation();
const actionIndex = button.closest('[data-index]').dataset.index;
await this.document.update({ await this.document.update({
'system.actions': this.document.system.actions.filter( 'system.actions': this.document.system.actions.filter(
(_, index) => index !== Number.parseInt(button.dataset.index) (_, index) => index !== Number.parseInt(actionIndex)
) )
}); });
} }

View file

@ -28,7 +28,6 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
toggleLoadoutView: this.toggleLoadoutView, toggleLoadoutView: this.toggleLoadoutView,
attackRoll: this.attackRoll, attackRoll: this.attackRoll,
useDomainCard: this.useDomainCard, useDomainCard: this.useDomainCard,
removeCard: this.removeDomainCard,
selectClass: this.selectClass, selectClass: this.selectClass,
selectSubclass: this.selectSubclass, selectSubclass: this.selectSubclass,
selectAncestry: this.selectAncestry, selectAncestry: this.selectAncestry,
@ -50,7 +49,8 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
toggleEquipItem: this.toggleEquipItem, toggleEquipItem: this.toggleEquipItem,
toggleVault: this.toggleVault, toggleVault: this.toggleVault,
levelManagement: this.levelManagement, levelManagement: this.levelManagement,
editImage: this._onEditImage editImage: this._onEditImage,
triggerContextMenu: this.triggerContextMenu
}, },
window: { window: {
resizable: true resizable: true
@ -223,14 +223,18 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
useItem: { useItem: {
name: 'DAGGERHEART.Sheets.PC.ContextMenu.UseItem', name: 'DAGGERHEART.Sheets.PC.ContextMenu.UseItem',
icon: '<i class="fa-solid fa-burst"></i>', icon: '<i class="fa-solid fa-burst"></i>',
callback: (element, event) => this.constructor.useItem.bind(this)(event, element) condition: el => {
const item = this.getItem(el);
return !['class', 'subclass'].includes(item.type);
},
callback: (button, event) => this.constructor.useItem.bind(this)(event, button)
}, },
equip: { equip: {
name: 'DAGGERHEART.Sheets.PC.ContextMenu.Equip', name: 'DAGGERHEART.Sheets.PC.ContextMenu.Equip',
icon: '<i class="fa-solid fa-hands"></i>', icon: '<i class="fa-solid fa-hands"></i>',
condition: el => { condition: el => {
const item = foundry.utils.fromUuidSync(el.dataset.uuid); const item = this.getItem(el);
return !item.system.equipped; return ['weapon', 'armor'].includes(item.type) && !item.system.equipped;
}, },
callback: this.constructor.toggleEquipItem.bind(this) callback: this.constructor.toggleEquipItem.bind(this)
}, },
@ -238,11 +242,34 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
name: 'DAGGERHEART.Sheets.PC.ContextMenu.Unequip', name: 'DAGGERHEART.Sheets.PC.ContextMenu.Unequip',
icon: '<i class="fa-solid fa-hands"></i>', icon: '<i class="fa-solid fa-hands"></i>',
condition: el => { condition: el => {
const item = foundry.utils.fromUuidSync(el.dataset.uuid); const item = this.getItem(el);
return item.system.equipped; return ['weapon', 'armor'].includes(item.type) && item.system.equipped;
}, },
callback: this.constructor.toggleEquipItem.bind(this) callback: this.constructor.toggleEquipItem.bind(this)
}, },
sendToLoadout: {
name: 'DAGGERHEART.Sheets.PC.ContextMenu.ToLoadout',
icon: '<i class="fa-solid fa-arrow-up"></i>',
condition: el => {
const item = this.getItem(el);
return ['domainCard'].includes(item.type) && item.system.inVault;
},
callback: this.constructor.toggleVault.bind(this)
},
sendToVault: {
name: 'DAGGERHEART.Sheets.PC.ContextMenu.ToVault',
icon: '<i class="fa-solid fa-arrow-down"></i>',
condition: el => {
const item = this.getItem(el);
return ['domainCard'].includes(item.type) && !item.system.inVault;
},
callback: this.constructor.toggleVault.bind(this)
},
sendToChat: {
name: 'DAGGERHEART.Sheets.PC.ContextMenu.SendToChat',
icon: '<i class="fa-regular fa-message"></i>',
callback: this.constructor.toChat.bind(this)
},
edit: { edit: {
name: 'DAGGERHEART.Sheets.PC.ContextMenu.Edit', name: 'DAGGERHEART.Sheets.PC.ContextMenu.Edit',
icon: '<i class="fa-solid fa-pen-to-square"></i>', icon: '<i class="fa-solid fa-pen-to-square"></i>',
@ -252,66 +279,12 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
name: 'DAGGERHEART.Sheets.PC.ContextMenu.Delete', name: 'DAGGERHEART.Sheets.PC.ContextMenu.Delete',
icon: '<i class="fa-solid fa-trash"></i>', icon: '<i class="fa-solid fa-trash"></i>',
callback: this.constructor.deleteItem.bind(this) callback: this.constructor.deleteItem.bind(this)
},
sendToLoadout: {
name: 'DAGGERHEART.Sheets.PC.ContextMenu.ToLoadout',
icon: '<i class="fa-solid fa-arrow-up"></i>',
condition: el => {
const item = foundry.utils.fromUuidSync(el.dataset.uuid);
return item.system.inVault;
},
callback: this.constructor.toggleVault.bind(this)
},
sendToVault: {
name: 'DAGGERHEART.Sheets.PC.ContextMenu.ToVault',
icon: '<i class="fa-solid fa-arrow-down"></i>',
condition: el => {
const item = foundry.utils.fromUuidSync(el.dataset.uuid);
return !item.system.inVault;
},
callback: this.constructor.toggleVault.bind(this)
},
sendToChat: {
name: 'DAGGERHEART.Sheets.PC.ContextMenu.SendToChat',
icon: '<i class="fa-regular fa-message"></i>',
callback: this.constructor.toChat.bind(this)
} }
}; };
const getMenuOptions = type => () => { this._createContextMenu(() => Object.values(allOptions), `[data-item-id]`, {
let menuItems = ['class', 'subclass'].includes(type) ? [] : [allOptions.useItem]; parentClassHooks: false,
switch (type) { fixed: true
case 'weapon':
case 'armor':
menuItems.push(...[allOptions.equip, allOptions.unequip]);
break;
case 'domainCard':
menuItems.push(...[allOptions.sendToLoadout, allOptions.sendToVault]);
break;
}
menuItems.push(...[allOptions.sendToChat, allOptions.edit, allOptions.delete]);
return menuItems;
};
const menuConfigs = [
'armor',
'weapon',
'miscellaneous',
'consumable',
'domainCard',
'miscellaneous',
'ancestry',
'community',
'class',
'subclass'
];
menuConfigs.forEach(type => {
this._createContextMenu(getMenuOptions(type), `.${type}-context-menu`, {
eventName: 'click',
parentClassHooks: false,
fixed: true
});
}); });
} }
@ -319,10 +292,16 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
super._attachPartListeners(partId, htmlElement, options); super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelector('.level-value')?.addEventListener('change', this.onLevelChange.bind(this)); htmlElement.querySelector('.level-value')?.addEventListener('change', this.onLevelChange.bind(this));
// To Remove when ContextMenu Handler is made }
htmlElement
.querySelectorAll('[data-item-id]') getItem(element) {
.forEach(element => element.addEventListener('contextmenu', this.editItem.bind(this))); const itemId = (element.target ?? element).closest('[data-item-id]').dataset.itemId,
item = this.document.items.get(itemId);
return item;
}
static triggerContextMenu(event, button) {
return CONFIG.ux.ContextMenu.triggerContextMenu(event);
} }
static _onEditImage() { static _onEditImage() {
@ -469,51 +448,12 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
const abilityLabel = game.i18n.localize(abilities[button.dataset.attribute].label); const abilityLabel = game.i18n.localize(abilities[button.dataset.attribute].label);
const config = { const config = {
event: event, event: event,
title: game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', { title: game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', { ability: abilityLabel }),
ability: abilityLabel
}),
roll: { roll: {
label: abilityLabel, trait: button.dataset.attribute
modifier: button.dataset.value
},
chatMessage: {
template: 'systems/daggerheart/templates/chat/duality-roll.hbs'
} }
}; };
this.document.diceRoll(config); this.document.diceRoll(config);
// Delete when new roll logic test done
/* const { roll, hope, fear, advantage, disadvantage, modifiers } = await this.document.dualityRoll(
{ title: game.i18n.localize(abilities[button.dataset.attribute].label), value: button.dataset.value },
event.shiftKey
);
const cls = getDocumentClass('ChatMessage');
const systemContent = new DHDualityRoll({
title: game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', {
ability: game.i18n.localize(abilities[button.dataset.attribute].label)
}),
origin: this.document.id,
roll: roll._formula,
modifiers: modifiers,
hope: hope,
fear: fear,
advantage: advantage,
disadvantage: disadvantage
});
await cls.create({
type: 'dualityRoll',
sound: CONFIG.sounds.dice,
system: systemContent,
user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/duality-roll.hbs',
systemContent
),
rolls: [roll]
}); */
} }
static async toggleMarks(_, button) { static async toggleMarks(_, button) {
@ -592,8 +532,9 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
new DhlevelUp(this.document).render(true); new DhlevelUp(this.document).render(true);
} }
static async useDomainCard(_, button) { static async useDomainCard(event, button) {
const card = this.document.items.find(x => x.uuid === button.dataset.key); const card = this.getItem(event);
if (!card) return;
const cls = getDocumentClass('ChatMessage'); const cls = getDocumentClass('ChatMessage');
const systemData = { const systemData = {
@ -617,13 +558,6 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
cls.create(msg.toObject()); cls.create(msg.toObject());
} }
static async removeDomainCard(_, button) {
if (button.dataset.type === 'domainCard') {
const card = this.document.items.find(x => x.uuid === button.dataset.key);
await card.delete();
}
}
static async selectClass() { static async selectClass() {
(await game.packs.get('daggerheart.classes'))?.render(true); (await game.packs.get('daggerheart.classes'))?.render(true);
} }
@ -659,23 +593,22 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
} }
static async useItem(event, button) { static async useItem(event, button) {
const id = button.closest('a').id, const item = this.getItem(button);
item = this.document.items.get(id); if (!item) return;
const wasUsed = await item.use(event); const wasUsed = await item.use(event);
if (wasUsed && item.type === 'weapon') { if (wasUsed && item.type === 'weapon') {
Hooks.callAll(SYSTEM.HOOKS.characterAttack, {}); Hooks.callAll(SYSTEM.HOOKS.characterAttack, {});
} }
} }
static async viewObject(element, button) { static async viewObject(event, button) {
const object = await fromUuid((button ?? element).dataset.uuid); const item = this.getItem(event);
if (!item) return;
object.sheet.render(true); item.sheet.render(true);
} }
editItem(event) { editItem(event) {
const uuid = event.target.closest('[data-item-id]').dataset.itemId, const item = this.getItem(event);
item = this.document.items.find(i => i.uuid === uuid);
if (!item) return; if (!item) return;
if (item.sheet.editMode) item.sheet.editMode = false; if (item.sheet.editMode) item.sheet.editMode = false;
@ -719,29 +652,26 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
} }
} }
async itemUpdate(event) {
const name = event.currentTarget.dataset.item;
const item = await fromUuid($(event.currentTarget).closest('[data-item-id]')[0].dataset.itemId);
await item.update({ [name]: event.currentTarget.value });
}
async onLevelChange(event) { async onLevelChange(event) {
await this.document.updateLevel(Number(event.currentTarget.value)); await this.document.updateLevel(Number(event.currentTarget.value));
this.render(); this.render();
} }
static async deleteItem(element, button) { static async deleteItem(event, button) {
const item = await fromUuid((button ?? element).closest('a').dataset.uuid); const item = this.getItem(event);
if (!item) return;
await item.delete(); await item.delete();
} }
static async setItemQuantity(button, value) { static async setItemQuantity(button, value) {
const item = await fromUuid($(button).closest('[data-item-id]')[0].dataset.itemId); const item = this.getItem(button);
if (!item) return;
await item.update({ 'system.quantity': Math.max(item.system.quantity + value, 1) }); await item.update({ 'system.quantity': Math.max(item.system.quantity + value, 1) });
} }
static async useFeature(_, button) { static async useFeature(event, button) {
const item = await fromUuid(button.dataset.id); const item = this.getItem(event);
if (!item) return;
const cls = getDocumentClass('ChatMessage'); const cls = getDocumentClass('ChatMessage');
const systemData = { const systemData = {
@ -765,7 +695,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
cls.create(msg.toObject()); cls.create(msg.toObject());
} }
static async toChat(element, button) { static async toChat(event, button) {
if (button?.dataset?.type === 'experience') { if (button?.dataset?.type === 'experience') {
const experience = this.document.system.experiences[button.dataset.uuid]; const experience = this.document.system.experiences[button.dataset.uuid];
const cls = getDocumentClass('ChatMessage'); const cls = getDocumentClass('ChatMessage');
@ -787,7 +717,8 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
cls.create(msg.toObject()); cls.create(msg.toObject());
} else { } else {
const item = await fromUuid((button ?? element).dataset.uuid); const item = this.getItem(event);
if (!item) return;
item.toChat(this.document.id); item.toChat(this.document.id);
} }
} }
@ -846,9 +777,9 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
cls.create(msg.toObject()); cls.create(msg.toObject());
} }
static async toggleEquipItem(element, button) { static async toggleEquipItem(event, button) {
const id = (button ?? element).closest('a').id; const item = this.getItem(event);
const item = this.document.items.get(id); if (!item) return;
if (item.system.equipped) { if (item.system.equipped) {
await item.update({ 'system.equipped': false }); await item.update({ 'system.equipped': false });
return; return;
@ -872,9 +803,9 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
this.render(); this.render();
} }
static async toggleVault(element, button) { static async toggleVault(event, button) {
const id = (button ?? element).closest('a').id; const item = this.getItem(event);
const item = this.document.items.get(id); if (!item) return;
await item.update({ 'system.inVault': !item.system.inVault }); await item.update({ 'system.inVault': !item.system.inVault });
} }

View file

@ -53,6 +53,7 @@ export default class DhpEnvironment extends DaggerheartSheet(ActorSheetV2) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.document = this.document; context.document = this.document;
context.tabs = super._getTabs(this.constructor.TABS); context.tabs = super._getTabs(this.constructor.TABS);
context.getEffectDetails = this.getEffectDetails.bind(this);
return context; return context;
} }
@ -62,6 +63,10 @@ export default class DhpEnvironment extends DaggerheartSheet(ActorSheetV2) {
this.render(); this.render();
} }
getEffectDetails(id) {
return {};
}
static async addAdversary() { static async addAdversary() {
await this.document.update({ await this.document.update({
[`system.potentialAdversaries.${foundry.utils.randomID()}.label`]: game.i18n.localize( [`system.potentialAdversaries.${foundry.utils.randomID()}.label`]: game.i18n.localize(

View file

@ -4,21 +4,21 @@ export const actionTypes = {
name: 'DAGGERHEART.Actions.Types.Attack.Name', name: 'DAGGERHEART.Actions.Types.Attack.Name',
icon: 'fa-swords' icon: 'fa-swords'
}, },
spellcast: { // spellcast: {
id: 'spellcast', // id: 'spellcast',
name: 'DAGGERHEART.Actions.Types.Spellcast.Name', // name: 'DAGGERHEART.Actions.Types.Spellcast.Name',
icon: 'fa-book-sparkles' // icon: 'fa-book-sparkles'
}, // },
healing: { healing: {
id: 'healing', id: 'healing',
name: 'DAGGERHEART.Actions.Types.Healing.Name', name: 'DAGGERHEART.Actions.Types.Healing.Name',
icon: 'fa-kit-medical' icon: 'fa-kit-medical'
}, },
resource: { // resource: {
id: 'resource', // id: 'resource',
name: 'DAGGERHEART.Actions.Types.Resource.Name', // name: 'DAGGERHEART.Actions.Types.Resource.Name',
icon: 'fa-honey-pot' // icon: 'fa-honey-pot'
}, // },
damage: { damage: {
id: 'damage', id: 'damage',
name: 'DAGGERHEART.Actions.Types.Damage.Name', name: 'DAGGERHEART.Actions.Types.Damage.Name',
@ -46,8 +46,34 @@ export const targetTypes = {
id: 'self', id: 'self',
label: 'Self' label: 'Self'
}, },
other: { friendly: {
id: 'other', id: 'friendly',
label: 'Other' label: 'Friendly'
},
hostile: {
id: 'hostile',
label: 'Hostile'
},
any: {
id: 'any',
label: 'Any'
} }
}; };
export const damageOnSave = {
none: {
id: 'none',
label: 'None',
mod: 0
},
half: {
id: 'half',
label: 'Half Damage',
mod: 0.5
},
full: {
id: 'full',
label: 'Full damage',
mod: 1
}
}

View file

@ -68,8 +68,8 @@ export const damageTypes = {
}; };
export const healingTypes = { export const healingTypes = {
health: { hitPoints: {
id: 'health', id: 'hitPoints',
label: 'DAGGERHEART.HealingType.HitPoints.Name', label: 'DAGGERHEART.HealingType.HitPoints.Name',
abbreviation: 'DAGGERHEART.HealingType.HitPoints.Abbreviation' abbreviation: 'DAGGERHEART.HealingType.HitPoints.Abbreviation'
}, },
@ -77,6 +77,16 @@ export const healingTypes = {
id: 'stress', id: 'stress',
label: 'DAGGERHEART.HealingType.Stress.Name', label: 'DAGGERHEART.HealingType.Stress.Name',
abbreviation: 'DAGGERHEART.HealingType.Stress.Abbreviation' abbreviation: 'DAGGERHEART.HealingType.Stress.Abbreviation'
},
hope: {
id: 'hope',
label: 'DAGGERHEART.HealingType.Hope.Name',
abbreviation: 'DAGGERHEART.HealingType.Hope.Abbreviation'
},
armorStack: {
id: 'armorStack',
label: 'DAGGERHEART.HealingType.ArmorStack.Name',
abbreviation: 'DAGGERHEART.HealingType.ArmorStack.Abbreviation'
} }
}; };
@ -290,13 +300,15 @@ export const diceTypes = {
d4: 'd4', d4: 'd4',
d6: 'd6', d6: 'd6',
d8: 'd8', d8: 'd8',
d10: 'd10',
d12: 'd12', d12: 'd12',
d20: 'd20' d20: 'd20'
}; };
export const multiplierTypes = { export const multiplierTypes = {
proficiency: 'Proficiency', proficiency: 'Proficiency',
spellcast: 'Spellcast' spellcast: 'Spellcast',
flat: 'Flat'
}; };
export const getDiceSoNicePresets = () => { export const getDiceSoNicePresets = () => {
@ -360,7 +372,35 @@ export const abilityCosts = {
}, },
stress: { stress: {
id: 'stress', id: 'stress',
label: 'Stress' label: 'DAGGERHEART.HealingType.Stress.Name'
},
armor: {
id: 'armor',
label: 'Armor Stack'
},
hp: {
id: 'hp',
label: 'DAGGERHEART.HealingType.HitPoints.Name'
},
prayer: {
id: 'prayer',
label: 'Prayer Dice'
},
favor: {
id: 'favor',
label: 'Favor Points'
},
slayer: {
id: 'slayer',
label: 'Slayer Dice'
},
tide: {
id: 'tide',
label: 'Tide'
},
chaos: {
id: 'chaos',
label: 'Chaos'
} }
}; };

View file

@ -5,16 +5,16 @@ import {
DHEffectAction, DHEffectAction,
DHHealingAction, DHHealingAction,
DHMacroAction, DHMacroAction,
DHResourceAction, // DHResourceAction,
DHSpellCastAction, // DHSpellCastAction,
DHSummonAction DHSummonAction
} from './action.mjs'; } from './action.mjs';
export const actionsTypes = { export const actionsTypes = {
base: DHBaseAction, base: DHBaseAction,
attack: DHAttackAction, attack: DHAttackAction,
spellcast: DHSpellCastAction, // spellcast: DHSpellCastAction,
resource: DHResourceAction, // resource: DHResourceAction,
damage: DHDamageAction, damage: DHDamageAction,
healing: DHHealingAction, healing: DHHealingAction,
summon: DHSummonAction, summon: DHSummonAction,

View file

@ -1,64 +1,38 @@
import { abilities } from '../../config/actorConfig.mjs'; import CostSelectionDialog from '../../applications/costSelectionDialog.mjs';
import { DHActionDiceData, DHDamageData, DHDamageField } from './actionDice.mjs'; import { DHActionDiceData, DHDamageData, DHDamageField } from './actionDice.mjs';
import DhpActor from '../../documents/actor.mjs';
export default class DHAction extends foundry.abstract.DataModel { import D20RollDialog from '../../dialogs/d20RollDialog.mjs';
static defineSchema() {
const fields = foundry.data.fields;
return {
id: new fields.DocumentIdField(),
name: new fields.StringField({ initial: 'New Action' }),
damage: new fields.SchemaField({
type: new fields.StringField({ choices: SYSTEM.GENERAL.damageTypes, nullable: true, initial: null }),
value: new fields.StringField({})
}),
healing: new fields.SchemaField({
type: new fields.StringField({ choices: SYSTEM.GENERAL.healingTypes, nullable: true, initial: null }),
value: new fields.StringField()
}),
conditions: new fields.ArrayField(
new fields.SchemaField({
name: new fields.StringField(),
icon: new fields.StringField(),
description: new fields.StringField()
})
),
cost: new fields.SchemaField({
type: new fields.StringField({ choices: SYSTEM.GENERAL.abilityCosts, nullable: true, initial: null }),
value: new fields.NumberField({ nullable: true, initial: null })
}),
target: new fields.SchemaField({
type: new fields.StringField({
choices: SYSTEM.ACTIONS.targetTypes,
initial: SYSTEM.ACTIONS.targetTypes.other.id
})
})
};
}
}
const fields = foundry.data.fields; const fields = foundry.data.fields;
/*
!!! I'm currently refactoring the whole Action thing, it's a WIP !!!
*/
/* /*
ToDo ToDo
- Apply ActiveEffect => Add to Chat message like Damage Button ? - Add setting and/or checkbox for cost and damage like
- Add Drag & Drop for documentUUID field (Macro & Summon) - Target Check / Target Picker
- Add optionnal Role for Healing ?
- Handle Roll result as part of formula if needed
- Target Check
- Cost Check
- Range Check - Range Check
- Area of effect and measurement placement - Area of effect and measurement placement
- Auto use costs and action - Summon Action create method
Other
- Auto use action <= Into Roll
*/ */
export class DHBaseAction extends foundry.abstract.DataModel { export class DHBaseAction extends foundry.abstract.DataModel {
static extraSchemas = [];
static defineSchema() { static defineSchema() {
return { return {
_id: new fields.DocumentIdField(), _id: new fields.DocumentIdField(),
systemPath: new fields.StringField({ required: true, initial: 'actions' }), systemPath: new fields.StringField({ required: true, initial: 'actions' }),
type: new fields.StringField({ initial: undefined, readonly: true, required: true }), type: new fields.StringField({ initial: undefined, readonly: true, required: true }),
name: new fields.StringField({ initial: undefined }), name: new fields.StringField({ initial: undefined }),
description: new fields.HTMLField(),
img: new fields.FilePathField({ initial: undefined, categories: ['IMAGE'], base64: false }), img: new fields.FilePathField({ initial: undefined, categories: ['IMAGE'], base64: false }),
chatDisplay: new fields.BooleanField({ initial: true, label: 'Display in chat' }),
actionType: new fields.StringField({ choices: SYSTEM.ITEM.actionTypes, initial: 'action', nullable: true }), actionType: new fields.StringField({ choices: SYSTEM.ITEM.actionTypes, initial: 'action', nullable: true }),
cost: new fields.ArrayField( cost: new fields.ArrayField(
new fields.SchemaField({ new fields.SchemaField({
@ -84,35 +58,85 @@ export class DHBaseAction extends foundry.abstract.DataModel {
}), }),
range: new fields.StringField({ range: new fields.StringField({
choices: SYSTEM.GENERAL.range, choices: SYSTEM.GENERAL.range,
required: true, required: false,
blank: false, blank: true
initial: 'self' // initial: null
}) }),
...this.defineExtraSchema()
}; };
} }
static defineExtraSchema() {
const extraFields = {
damage: new DHDamageField(),
roll: new fields.SchemaField({
type: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.GENERAL.rollTypes }),
trait: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.ACTOR.abilities }),
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
bonus: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 })
}),
save: new fields.SchemaField({
trait: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.ACTOR.abilities }),
difficulty: new fields.NumberField({ nullable: true, initial: 10, integer: true, min: 0 }),
damageMod: new fields.StringField({ initial: SYSTEM.ACTIONS.damageOnSave.none.id, choices: SYSTEM.ACTIONS.damageOnSave })
}),
target: new fields.SchemaField({
type: new fields.StringField({
choices: SYSTEM.ACTIONS.targetTypes,
initial: SYSTEM.ACTIONS.targetTypes.any.id,
nullable: true,
initial: null
}),
amount: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 })
}),
effects: new fields.ArrayField( // ActiveEffect
new fields.SchemaField({
_id: new fields.DocumentIdField(),
onSave: new fields.BooleanField({ initial: false })
})
),
healing: new fields.SchemaField({
type: new fields.StringField({
choices: SYSTEM.GENERAL.healingTypes,
required: true,
blank: false,
initial: SYSTEM.GENERAL.healingTypes.hitPoints.id,
label: 'Healing'
}),
resultBased: new fields.BooleanField({ initial: false, label: "DAGGERHEART.Actions.Settings.ResultBased.label" }),
value: new fields.EmbeddedDataField(DHActionDiceData),
valueAlt: new fields.EmbeddedDataField(DHActionDiceData),
})
},
extraSchemas = {};
this.extraSchemas.forEach(s => (extraSchemas[s] = extraFields[s]));
return extraSchemas;
}
prepareData() {} prepareData() {}
get index() { get index() {
return foundry.utils.getProperty(this.parent, this.systemPath).indexOf(this); return foundry.utils.getProperty(this.parent, this.systemPath).indexOf(this);
} }
get id() {
return this._id;
}
get item() { get item() {
return this.parent.parent; return this.parent.parent;
} }
get actor() { get actor() {
return this.item?.actor; return this.item instanceof DhpActor ? this.item : this.item?.actor;
} }
get chatTemplate() { get chatTemplate() {
return 'systems/daggerheart/templates/chat/duality-roll.hbs'; return 'systems/daggerheart/templates/chat/duality-roll.hbs';
} }
get chatTitle() {
return this.item.name;
}
static getRollType() { static getRollType(parent) {
return 'ability'; return 'ability';
} }
@ -121,96 +145,425 @@ export class DHBaseAction extends foundry.abstract.DataModel {
updateSource.img ??= parent?.img ?? parent?.system?.img; updateSource.img ??= parent?.img ?? parent?.system?.img;
if (parent?.system?.trait) { if (parent?.system?.trait) {
updateSource['roll'] = { updateSource['roll'] = {
type: this.getRollType(), type: this.getRollType(parent),
trait: parent.system.trait trait: parent.system.trait
}; };
} }
if (parent?.type === 'weapon' && !!this.schema.fields.damage) {
updateSource['damage'] = { includeBase: true };
}
if (parent?.system?.range) { if (parent?.system?.range) {
updateSource['range'] = parent?.system?.range; updateSource['range'] = parent?.system?.range;
} }
return updateSource; return updateSource;
} }
async use(event) { getRollData() {
if (this.roll.type && this.roll.trait) { const actorData = this.actor.getRollData(false);
const modifierValue =
this.actor.system.traits[this.roll.trait].value + (this.actor.system.bonuses.attack ?? 0); // Remove when included directly in Actor getRollData
const config = { actorData.prof = actorData.proficiency?.value ?? 1,
event: event, actorData.cast = actorData.spellcast?.value ?? 1,
title: this.chatTitle, actorData.scale = this.cost.length
roll: { ? this.cost.reduce((a, c) => {
modifier: modifierValue, a[c.type] = c.value;
label: game.i18n.localize(abilities[this.roll.trait].label), return a;
type: this.actionType, }, {})
difficulty: this.roll?.difficulty : 1,
}, actorData.roll = {}
chatMessage: {
template: this.chatTemplate return actorData;
}
async use(event, ...args) {
const isFastForward = event.shiftKey || (!this.hasRoll && !this.hasSave);
// Prepare base Config
const initConfig = this.initActionConfig(event);
// let config = this.initActionConfig(event);
// Prepare Targets
const targetConfig = this.prepareTarget();
if (isFastForward && !targetConfig) return ui.notifications.warn('Too many targets selected for that actions.');
// config = this.prepareTarget(config);
// Prepare Range
const rangeConfig = this.prepareRange();
// config = this.prepareRange(config);
// Prepare Costs
const costsConfig = this.prepareCost();
if(isFastForward && !this.hasCost(costsConfig)) return ui.notifications.warn("You don't have the resources to use that action.");
// config = this.prepareUseCost(config)
// Prepare Uses
const usesConfig = this.prepareUse();
if(isFastForward && !this.hasUses(usesConfig)) return ui.notifications.warn("That action doesn't have remaining uses.");
// config = this.prepareUseCost(config)
// Prepare Roll Data
const actorData = this.getRollData();
let config = {
...initConfig,
targets: targetConfig,
range: rangeConfig,
costs: costsConfig,
uses: usesConfig,
data: actorData
}
if ( Hooks.call(`${SYSTEM.id}.preUseAction`, this, config) === false ) return;
// Display configuration window if necessary
if ( config.dialog.configure && this.requireConfigurationDialog(config) ) {
config = await D20RollDialog.configure(config);
if (!config) return;
}
if ( this.hasRoll ) {
const rollConfig = this.prepareRoll(config);
config.roll = rollConfig;
config = await this.actor.diceRoll(config);
if (!config) return;
}
if( this.hasSave ) {
/* config.targets.forEach((t) => {
if(t.hit) {
const target = game.canvas.tokens.get(t.id),
actor = target?.actor;
console.log(actor)
if(!actor) return;
actor.saveRoll({
event,
title: 'Roll Save',
roll: {
trait: this.save.trait,
difficulty: this.save.difficulty
},
dialog: {
configure: false
},
data: actor.getRollData()
}).then(async (result) => {
t.saved = result;
setTimeout(async () => {
const message = ui.chat.collection.get(config.message.id),
msgTargets = message.system.targets,
msgTarget = msgTargets.find(mt => mt.id === t.id);
msgTarget.saved = result;
await message.update({'system.targets': msgTargets});
},100)
})
} }
}; }) */
if (this.target?.type) config.checkTarget = true; }
if (this.damage.parts.length) {
config.damage = { if ( this.doFollowUp() ) {
value: this.damage.parts.map(p => p.getFormula(this.actor)).join(' + '), if(this.rollDamage) await this.rollDamage(event, config);
type: this.damage.parts[0].type if(this.rollHealing) await this.rollHealing(event, config);
}; if(this.trigger) await this.trigger(event, config);
} }
if (this.effects.length) {
// Apply Active Effects. In Chat Message ? // Consume resources
} await this.consume(config);
return this.actor.diceRoll(config);
if ( Hooks.call(`${SYSTEM.id}.postUseAction`, this, config) === false ) return;
return config;
}
/* */
initActionConfig(event) {
return {
event,
title: this.item.name,
source: {
item: this.item._id,
action: this._id
// action: this
},
dialog: {
configure: true
},
type: this.type,
hasDamage: !!this.damage?.parts?.length,
hasHealing: !!this.healing,
hasEffect: !!this.effects?.length,
hasSave: this.hasSave
} }
} }
}
const extraDefineSchema = (field, option) => { requireConfigurationDialog(config) {
return { return !config.event.shiftkey && !this.hasRoll && (config.costs?.length || config.uses);
[field]: { }
// damage: new fields.SchemaField({
// parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData))
// }),
damage: new DHDamageField(option),
roll: new fields.SchemaField({
type: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.GENERAL.rollTypes }),
trait: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.ACTOR.abilities }),
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 })
}),
target: new fields.SchemaField({
type: new fields.StringField({
choices: SYSTEM.ACTIONS.targetTypes,
initial: SYSTEM.ACTIONS.targetTypes.other.id
})
}),
effects: new fields.ArrayField( // ActiveEffect
new fields.SchemaField({
_id: new fields.DocumentIdField()
})
)
}[field]
};
};
export class DHAttackAction extends DHBaseAction { prepareCost() {
static defineSchema() { const costs = this.cost?.length ? foundry.utils.deepClone(this.cost) : [];
return costs;
}
prepareUse() {
const uses = this.uses?.max ? foundry.utils.deepClone(this.uses) : null;
if (uses && !uses.value) uses.value = 0;
return uses;
}
prepareTarget() {
let targets;
if (this.target?.type === SYSTEM.ACTIONS.targetTypes.self.id)
targets = this.formatTarget(this.actor.token ?? this.actor.prototypeToken);
targets = Array.from(game.user.targets);
// foundry.CONST.TOKEN_DISPOSITIONS.FRIENDLY
if (this.target?.type && this.target.type !== SYSTEM.ACTIONS.targetTypes.any.id) {
targets = targets.filter(t => this.isTargetFriendly(t));
if (this.target.amount && targets.length > this.target.amount) targets = [];
}
targets = targets.map(t => this.formatTarget(t));
return targets;
}
prepareRange() {
const range = this.range ?? null;
return range;
}
prepareRoll() {
const roll = {
modifiers: [],
trait: this.roll?.trait,
label: 'Attack',
type: this.actionType,
difficulty: this.roll?.difficulty
};
return roll;
}
doFollowUp(config) {
return !this.hasRoll;
}
async consume(config) {
const resources = config.costs.filter(c => c.enabled !== false).map(c => {
return { type: c.type, value: c.total * -1 };
});
await this.actor.modifyResource(resources);
if(config.uses?.enabled) {
const newActions = foundry.utils.getProperty(this.item.system, this.systemPath).map(x => x.toObject());
newActions[this.index].uses.value++;
await this.item.update({ [`system.${this.systemPath}`]: newActions });
}
}
/* */
/* ROLL */
get hasRoll() {
return !!this.roll?.type;
}
/* ROLL */
/* SAVE */
get hasSave() {
return !!this.save?.trait;
}
/* SAVE */
/* COST */
getRealCosts(costs) {
const realCosts = costs?.length ? costs.filter(c => c.enabled) : [];
return realCosts;
}
calcCosts(costs) {
return costs.map(c => {
c.scale = c.scale ?? 1;
c.step = c.step ?? 1;
c.total = c.value * c.scale * c.step;
c.enabled = c.hasOwnProperty('enabled') ? c.enabled : true;
return c;
});
}
hasCost(costs) {
const realCosts = this.getRealCosts(costs);
return realCosts.reduce((a, c) => a && this.actor.system.resources[c.type]?.value >= (c.total ?? c.value), true);
}
/* COST */
/* USES */
calcUses(uses) {
if(!uses) return null;
return { return {
...super.defineSchema(), ...uses,
...extraDefineSchema('damage', true), enabled: uses.hasOwnProperty('enabled') ? uses.enabled : true
...extraDefineSchema('roll'),
...extraDefineSchema('target'),
...extraDefineSchema('effects')
}; };
} }
static getRollType() { hasUses(uses) {
return 'weapon'; if(!uses) return true;
return (uses.hasOwnProperty('enabled') && !uses.enabled) || uses.value + 1 <= uses.max;
}
/* USES */
/* TARGET */
isTargetFriendly(target) {
const actorDisposition = this.actor.token
? this.actor.token.disposition
: this.actor.prototypeToken.disposition,
targetDisposition = target.document.disposition;
return (
(this.target.type === SYSTEM.ACTIONS.targetTypes.friendly.id && actorDisposition === targetDisposition) ||
(this.target.type === SYSTEM.ACTIONS.targetTypes.hostile.id && actorDisposition + targetDisposition === 0)
);
} }
get chatTitle() { formatTarget(actor) {
return game.i18n.format('DAGGERHEART.Chat.AttackRoll.Title', { return {
attack: this.item.name id: actor.id,
actorId: actor.actor.uuid,
name: actor.actor.name,
img: actor.actor.img,
difficulty: actor.actor.system.difficulty,
evasion: actor.actor.system.evasion?.total
};
}
/* TARGET */
/* RANGE */
/* RANGE */
/* EFFECTS */
async applyEffects(event, data, force = false) {
if (!this.effects?.length || !data.system.targets.length) return;
let effects = this.effects;
data.system.targets.forEach(async token => {
if (!token.hit && !force) return;
if(this.hasSave && token.saved.success === true) {
effects = this.effects.filter(e => e.onSave === true)
}
if(!effects.length) return;
effects.forEach(async e => {
const actor = canvas.tokens.get(token.id)?.actor,
effect = this.item.effects.get(e._id);
if (!actor || !effect) return;
await this.applyEffect(effect, actor);
});
}); });
} }
async applyEffect(effect, actor) {
// Enable an existing effect on the target if it originated from this effect
const existingEffect = actor.effects.find(e => e.origin === origin.uuid);
if (existingEffect) {
return existingEffect.update(
foundry.utils.mergeObject({
...effect.constructor.getInitialDuration(),
disabled: false
})
);
}
// Otherwise, create a new effect on the target
const effectData = foundry.utils.mergeObject({
...effect.toObject(),
disabled: false,
transfer: false,
origin: origin.uuid
});
await ActiveEffect.implementation.create(effectData, { parent: actor });
}
/* EFFECTS */
/* SAVE */
async rollSave(target, event, message) {
if(!target?.actor) return;
target.actor.diceRoll({
event,
title: 'Roll Save',
roll: {
trait: this.save.trait,
difficulty: this.save.difficulty,
type: "reaction"
},
data: target.actor.getRollData()
}).then(async (result) => {
this.updateChatMessage(message, target.id, {result: result.roll.total, success: result.roll.success});
})
}
async updateChatMessage(message, targetId, changes, chain=true) {
setTimeout(async () => {
const chatMessage = ui.chat.collection.get(message._id),
msgTargets = chatMessage.system.targets,
msgTarget = msgTargets.find(mt => mt.id === targetId);
msgTarget.saved = changes;
await chatMessage.update({'system.targets': msgTargets});
},100);
if(chain) {
if(message.system.source.message) this.updateChatMessage(ui.chat.collection.get(message.system.source.message), targetId, changes, false);
const relatedChatMessages = ui.chat.collection.filter(c => c.system.source.message === message._id);
relatedChatMessages.forEach(c => {
this.updateChatMessage(c, targetId, changes, false);
})
}
}
/* SAVE */
}
export class DHDamageAction extends DHBaseAction {
static extraSchemas = ['damage', 'target', 'effects'];
/* async use(event, ...args) {
const config = await super.use(event, args);
if (!config || ['error', 'warning'].includes(config.type)) return;
if (!this.directDamage) return;
return await this.rollDamage(event, config);
} */
getFormulaValue(part, data) {
let formulaValue = part.value;
if(this.hasRoll && part.resultBased && data.system.roll.result.duality === -1) return part.valueAlt;
return formulaValue;
}
async rollDamage(event, data) {
let formula = this.damage.parts.map(p => this.getFormulaValue(p, data).getFormula(this.actor)).join(' + ');
if (!formula || formula == '') return;
let roll = { formula: formula, total: formula },
bonusDamage = [];
const config = {
title: game.i18n.format('DAGGERHEART.Chat.DamageRoll.Title', { damage: this.name }),
formula,
targets: (data.system?.targets.filter(t => t.hit) ?? data.targets),
hasSave: this.hasSave,
source: data.system?.source
};
if(this.hasSave) config.onSave = this.save.damageMod;
if(data.system) {
config.source.message = data._id;
config.directDamage = false;
}
roll = CONFIG.Dice.daggerheart.DamageRoll.build(config);
}
}
export class DHAttackAction extends DHDamageAction {
static extraSchemas = [...super.extraSchemas, ...['roll', 'save']];
static getRollType(parent) {
return parent.type === 'weapon' ? 'weapon' : 'spellcast';
}
get chatTemplate() {
return 'systems/daggerheart/templates/chat/duality-roll.hbs';
}
prepareData() { prepareData() {
super.prepareData(); super.prepareData();
if (this.damage.includeBase && !!this.item?.system?.damage) { if (this.damage.includeBase && !!this.item?.system?.damage) {
@ -221,115 +574,49 @@ export class DHAttackAction extends DHBaseAction {
getParentDamage() { getParentDamage() {
return { return {
multiplier: 'proficiency', value: {
dice: this.item?.system?.damage.value, multiplier: 'proficiency',
bonus: this.item?.system?.damage.bonus ?? 0, dice: this.item?.system?.damage.value,
bonus: this.item?.system?.damage.bonus ?? 0
},
type: this.item?.system?.damage.type, type: this.item?.system?.damage.type,
base: true base: true
}; };
} }
// Temporary until full formula parser
// getDamageFormula() {
// return this.damage.parts.map(p => p.formula).join(' + ');
// }
}
export class DHSpellCastAction extends DHBaseAction {
static defineSchema() {
return {
...super.defineSchema(),
...extraDefineSchema('damage'),
...extraDefineSchema('roll'),
...extraDefineSchema('target'),
...extraDefineSchema('effects')
};
}
static getRollType() {
return 'spellcast';
}
}
export class DHDamageAction extends DHBaseAction {
static defineSchema() {
return {
...super.defineSchema(),
...extraDefineSchema('damage', false),
...extraDefineSchema('target'),
...extraDefineSchema('effects')
};
}
async use(event) {
const formula = this.damage.parts.map(p => p.getFormula(this.actor)).join(' + ');
if (!formula || formula == '') return;
let roll = { formula: formula, total: formula };
if (isNaN(formula)) {
roll = await new Roll(formula).evaluate();
}
const cls = getDocumentClass('ChatMessage');
const msg = new cls({
user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/damage-roll.hbs',
{
roll: roll.formula,
total: roll.total,
type: this.damage.parts.map(p => p.type)
}
)
});
cls.create(msg.toObject());
}
} }
export class DHHealingAction extends DHBaseAction { export class DHHealingAction extends DHBaseAction {
static defineSchema() { static extraSchemas = ['target', 'effects', 'healing', 'roll'];
return {
...super.defineSchema(), static getRollType(parent) {
healing: new fields.SchemaField({ return 'spellcast';
type: new fields.StringField({
choices: SYSTEM.GENERAL.healingTypes,
required: true,
blank: false,
initial: SYSTEM.GENERAL.healingTypes.health.id,
label: 'Healing'
}),
value: new fields.EmbeddedDataField(DHActionDiceData)
}),
...extraDefineSchema('target'),
...extraDefineSchema('effects')
};
} }
async use(event) { getFormulaValue(data) {
const formula = this.healing.value.getFormula(this.actor); let formulaValue = this.healing.value;
if(this.hasRoll && this.healing.resultBased && data.system.roll.result.duality === -1) return this.healing.valueAlt;
return formulaValue;
}
async rollHealing(event, data) {
let formulaValue = this.getFormulaValue(data),
formula = formulaValue.getFormula(this.actor);
if (!formula || formula == '') return; if (!formula || formula == '') return;
let roll = { formula: formula, total: formula },
bonusDamage = [];
const config = {
title: game.i18n.format('DAGGERHEART.Chat.HealingRoll.Title', {
healing: game.i18n.localize(SYSTEM.GENERAL.healingTypes[this.healing.type].label)
}),
formula,
targets: (data.system?.targets ?? data.targets).filter(t => t.hit),
messageType: 'healing',
type: this.healing.type
};
// const roll = await super.use(event); roll = CONFIG.Dice.daggerheart.DamageRoll.build(config);
let roll = { formula: formula, total: formula };
if (isNaN(formula)) {
roll = await new Roll(formula).evaluate();
}
const cls = getDocumentClass('ChatMessage');
const msg = new cls({
user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/healing-roll.hbs',
{
roll: roll.formula,
total: roll.total,
type: this.healing.type
}
)
});
cls.create(msg.toObject());
} }
get chatTemplate() { get chatTemplate() {
@ -337,42 +624,60 @@ export class DHHealingAction extends DHBaseAction {
} }
} }
export class DHResourceAction extends DHBaseAction {
static defineSchema() {
return {
...super.defineSchema(),
// ...extraDefineSchema('roll'),
...extraDefineSchema('target'),
...extraDefineSchema('effects'),
resource: new fields.SchemaField({
type: new fields.StringField({
choices: [],
blank: true,
required: false,
initial: '',
label: 'Resource'
}),
value: new fields.NumberField({ initial: 0, label: 'Value' })
})
};
}
}
export class DHSummonAction extends DHBaseAction { export class DHSummonAction extends DHBaseAction {
static defineSchema() { static defineSchema() {
return { return {
...super.defineSchema(), ...super.defineSchema(),
documentUUID: new fields.StringField({ blank: true, initial: '', placeholder: 'Enter a Creature UUID' }) documentUUID: new fields.DocumentUUIDField({ type: 'Actor' })
}; };
} }
async trigger(event, ...args) {
if (!this.canSummon || !canvas.scene) return;
// const config = await super.use(event, args);
}
get canSummon() {
return game.user.can('TOKEN_CREATE');
}
} }
export class DHEffectAction extends DHBaseAction { export class DHEffectAction extends DHBaseAction {
static defineSchema() { static extraSchemas = ['effects', 'target'];
return {
...super.defineSchema(), async use(event, ...args) {
...extraDefineSchema('effects') const config = await super.use(event, args);
}; if (['error', 'warning'].includes(config.type)) return;
return await this.chatApplyEffects(event, config);
}
async chatApplyEffects(event, data) {
const cls = getDocumentClass('ChatMessage'),
systemData = {
title: game.i18n.format('DAGGERHEART.Chat.ApplyEffect.Title', { name: this.name }),
origin: this.actor._id,
description: '',
targets: data.targets.map(x => ({ id: x.id, name: x.name, img: x.img, hit: true })),
action: {
itemId: this.item._id,
actionId: this._id
}
},
msg = new cls({
type: 'applyEffect',
user: game.user.id,
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/apply-effects.hbs',
systemData
)
});
cls.create(msg.toObject());
}
get chatTemplate() {
return 'systems/daggerheart/templates/chat/apply-effects.hbs';
} }
} }
@ -380,11 +685,13 @@ export class DHMacroAction extends DHBaseAction {
static defineSchema() { static defineSchema() {
return { return {
...super.defineSchema(), ...super.defineSchema(),
documentUUID: new fields.StringField({ blank: true, initial: '', placeholder: 'Enter a macro UUID' }) documentUUID: new fields.DocumentUUIDField({ type: 'Macro' })
}; };
} }
async use(event) { async trigger(event, ...args) {
// const config = await super.use(event, args);
// if (['error', 'warning'].includes(config.type)) return;
const fixUUID = !this.documentUUID.includes('Macro.') ? `Macro.${this.documentUUID}` : this.documentUUID, const fixUUID = !this.documentUUID.includes('Macro.') ? `Macro.${this.documentUUID}` : this.documentUUID,
macro = await fromUuid(fixUUID); macro = await fromUuid(fixUUID);
try { try {

View file

@ -11,6 +11,7 @@ export class DHActionDiceData extends foundry.abstract.DataModel {
initial: 'proficiency', initial: 'proficiency',
label: 'Multiplier' label: 'Multiplier'
}), }),
flatMultiplier: new fields.NumberField({ nullable: true, initial: 1, label: 'Flat Multiplier' }),
dice: new fields.StringField({ choices: SYSTEM.GENERAL.diceTypes, initial: 'd6', label: 'Formula' }), dice: new fields.StringField({ choices: SYSTEM.GENERAL.diceTypes, initial: 'd6', label: 'Formula' }),
bonus: new fields.NumberField({ nullable: true, initial: null, label: 'Bonus' }), bonus: new fields.NumberField({ nullable: true, initial: null, label: 'Bonus' }),
custom: new fields.SchemaField({ custom: new fields.SchemaField({
@ -21,27 +22,29 @@ export class DHActionDiceData extends foundry.abstract.DataModel {
} }
getFormula(actor) { getFormula(actor) {
const multiplier = this.multiplier === 'flat' ? this.flatMultiplier : actor.system[this.multiplier]?.total;
return this.custom.enabled return this.custom.enabled
? this.custom.formula ? this.custom.formula
: `${actor.system[this.multiplier]?.total ?? 1}${this.dice}${this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : ''}`; : `${multiplier ?? 1}${this.dice}${this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : ''}`;
} }
} }
export class DHDamageField extends fields.SchemaField { export class DHDamageField extends fields.SchemaField {
constructor(hasBase, options, context = {}) { constructor(options, context = {}) {
const damageFields = { const damageFields = {
parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)) parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)),
includeBase: new fields.BooleanField({ initial: false })
}; };
if (hasBase) damageFields.includeBase = new fields.BooleanField({ initial: true }); // if (hasBase) damageFields.includeBase = new fields.BooleanField({ initial: true });
super(damageFields, options, context); super(damageFields, options, context);
} }
} }
export class DHDamageData extends DHActionDiceData { export class DHDamageData extends foundry.abstract.DataModel {
/** @override */ /** @override */
static defineSchema() { static defineSchema() {
return { return {
...super.defineSchema(), // ...super.defineSchema(),
base: new fields.BooleanField({ initial: false, readonly: true, label: 'Base' }), base: new fields.BooleanField({ initial: false, readonly: true, label: 'Base' }),
type: new fields.StringField({ type: new fields.StringField({
choices: SYSTEM.GENERAL.damageTypes, choices: SYSTEM.GENERAL.damageTypes,
@ -49,7 +52,10 @@ export class DHDamageData extends DHActionDiceData {
label: 'Type', label: 'Type',
nullable: false, nullable: false,
required: true required: true
}) }),
resultBased: new fields.BooleanField({ initial: false, label: "DAGGERHEART.Actions.Settings.ResultBased.label" }),
value: new fields.EmbeddedDataField(DHActionDiceData),
valueAlt: new fields.EmbeddedDataField(DHActionDiceData),
}; };
} }
} }

View file

@ -1,3 +1,4 @@
import ActionField from '../fields/actionField.mjs';
import BaseDataActor from './base.mjs'; import BaseDataActor from './base.mjs';
const resourceField = () => const resourceField = () =>
@ -39,30 +40,41 @@ export default class DhpAdversary extends BaseDataActor {
hitPoints: resourceField(), hitPoints: resourceField(),
stress: resourceField() stress: resourceField()
}), }),
attack: new fields.SchemaField({ attack: new ActionField({
name: new fields.StringField({}), initial: {
modifier: new fields.NumberField({ required: true, integer: true, initial: 0 }), name: 'Attack',
range: new fields.StringField({ _id: foundry.utils.randomID(),
required: true, systemPath: 'attack',
choices: SYSTEM.GENERAL.range, type: 'attack',
initial: SYSTEM.GENERAL.range.melee.id range: 'melee',
}), target: {
damage: new fields.SchemaField({ type: 'any',
value: new fields.StringField(), amount: 1
type: new fields.StringField({ },
required: true, roll: {
choices: SYSTEM.GENERAL.damageTypes, type: 'weapon'
initial: SYSTEM.GENERAL.damageTypes.physical.id },
}) damage: {
}) parts: [
{
multiplier: 'flat'
}
]
}
}
}), }),
experiences: new fields.TypedObjectField( experiences: new fields.TypedObjectField(
new fields.SchemaField({ new fields.SchemaField({
name: new fields.StringField(), name: new fields.StringField(),
value: new fields.NumberField({ required: true, integer: true, initial: 1 }) value: new fields.NumberField({ required: true, integer: true, initial: 1 })
}) })
) ),
/* Features waiting on pseudo-document data model addition */ bonuses: new fields.SchemaField({
difficulty: new fields.SchemaField({
all: new fields.NumberField({ integer: true, initial: 0 }),
reaction: new fields.NumberField({ integer: true, initial: 0 })
})
})
}; };
} }
} }

View file

@ -17,6 +17,12 @@ const resourceField = max =>
max: new foundry.data.fields.NumberField({ initial: max, integer: true }) max: new foundry.data.fields.NumberField({ initial: max, integer: true })
}); });
const stressDamageReductionRule = () =>
new foundry.data.fields.SchemaField({
enabled: new foundry.data.fields.BooleanField({ required: true, initial: false }),
cost: new foundry.data.fields.NumberField({ integer: true })
});
export default class DhCharacter extends BaseDataActor { export default class DhCharacter extends BaseDataActor {
static get metadata() { static get metadata() {
return foundry.utils.mergeObject(super.metadata, { return foundry.utils.mergeObject(super.metadata, {
@ -35,7 +41,9 @@ export default class DhCharacter extends BaseDataActor {
bonus: new foundry.data.fields.NumberField({ initial: 0, integer: true }) bonus: new foundry.data.fields.NumberField({ initial: 0, integer: true })
}), }),
stress: resourceField(6), stress: resourceField(6),
hope: resourceField(6) hope: resourceField(6),
tokens: new fields.ObjectField(),
dice: new fields.ObjectField()
}), }),
traits: new fields.SchemaField({ traits: new fields.SchemaField({
agility: attributeField(), agility: attributeField(),
@ -90,9 +98,42 @@ export default class DhCharacter extends BaseDataActor {
}), }),
levelData: new fields.EmbeddedDataField(DhPCLevelData), levelData: new fields.EmbeddedDataField(DhPCLevelData),
bonuses: new fields.SchemaField({ bonuses: new fields.SchemaField({
attack: new fields.NumberField({ integer: true, initial: 0 }), armorScore: new fields.NumberField({ integer: true, initial: 0 }),
spellcast: new fields.NumberField({ integer: true, initial: 0 }), damageThresholds: new fields.SchemaField({
armorScore: new fields.NumberField({ integer: true, initial: 0 }) severe: new fields.NumberField({ integer: true, initial: 0 }),
major: new fields.NumberField({ integer: true, initial: 0 })
}),
roll: new fields.SchemaField({
attack: new fields.NumberField({ integer: true, initial: 0 }),
spellcast: new fields.NumberField({ integer: true, initial: 0 }),
action: new fields.NumberField({ integer: true, initial: 0 }),
hopeOrFear: new fields.NumberField({ integer: true, initial: 0 })
}),
damage: new fields.SchemaField({
all: new fields.NumberField({ integer: true, initial: 0 }),
physical: new fields.NumberField({ integer: true, initial: 0 }),
magic: 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({
severe: stressDamageReductionRule(),
major: stressDamageReductionRule(),
minor: stressDamageReductionRule()
}),
strangePatterns: new fields.NumberField({
integer: true,
min: 1,
max: 12,
nullable: true,
initial: null
}),
runeWard: new fields.BooleanField({ initial: false })
}) })
}; };
} }
@ -246,6 +287,9 @@ export default class DhCharacter extends BaseDataActor {
experience.total = experience.value + experience.bonus; 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.class.value?.system?.hitPoints ?? 0) + this.resources.hitPoints.bonus; this.resources.hitPoints.maxTotal = (this.class.value?.system?.hitPoints ?? 0) + this.resources.hitPoints.bonus;
this.resources.stress.maxTotal = this.resources.stress.max + this.resources.stress.bonus; this.resources.stress.maxTotal = this.resources.stress.max + this.resources.stress.bonus;
this.evasion.total = (this.class?.evasion ?? 0) + this.evasion.bonus; this.evasion.total = (this.class?.evasion ?? 0) + this.evasion.bonus;
@ -256,7 +300,11 @@ export default class DhCharacter extends BaseDataActor {
const data = super.getRollData(); const data = super.getRollData();
return { return {
...data, ...data,
tier: this.tier ...this.resources.tokens,
...this.resources.dice,
...this.bonuses,
tier: this.tier,
level: this.levelData.level.current
}; };
} }
} }

View file

@ -1,13 +1,21 @@
import DHAbilityUse from './abilityUse.mjs'; import DHAbilityUse from "./abilityUse.mjs";
import DHAdversaryRoll from './adversaryRoll.mjs'; import DHAdversaryRoll from "./adversaryRoll.mjs";
import DHDamageRoll from './damageRoll.mjs'; import DHDamageRoll from "./damageRoll.mjs";
import DHDualityRoll from './dualityRoll.mjs'; import DHDualityRoll from "./dualityRoll.mjs";
import DHApplyEffect from './applyEffects.mjs'
export { DHAbilityUse, DHAdversaryRoll, DHDamageRoll, DHDualityRoll }; export {
DHAbilityUse,
DHAdversaryRoll,
DHDamageRoll,
DHDualityRoll,
DHApplyEffect
}
export const config = { export const config = {
abilityUse: DHAbilityUse, abilityUse: DHAbilityUse,
adversaryRoll: DHAdversaryRoll, adversaryRoll: DHAdversaryRoll,
damageRoll: DHDamageRoll, damageRoll: DHDamageRoll,
dualityRoll: DHDualityRoll dualityRoll: DHDualityRoll,
applyEffect: DHApplyEffect
}; };

View file

@ -4,43 +4,35 @@ export default class DHAdversaryRoll extends foundry.abstract.TypeDataModel {
return { return {
title: new fields.StringField(), title: new fields.StringField(),
origin: new fields.StringField({ required: true }),
dice: new fields.DataField(),
roll: new fields.DataField(), roll: new fields.DataField(),
modifiers: new fields.ArrayField(
new fields.SchemaField({
value: new fields.NumberField({ integer: true }),
label: new fields.StringField({})
})
),
advantageState: new fields.BooleanField({ nullable: true, initial: null }),
advantage: new fields.SchemaField({
dice: new fields.StringField({}),
value: new fields.NumberField({ integer: true })
}),
targets: new fields.ArrayField( targets: new fields.ArrayField(
new fields.SchemaField({ new fields.SchemaField({
id: new fields.StringField({}), id: new fields.StringField({}),
actorId: new fields.StringField({}),
name: new fields.StringField({}), name: new fields.StringField({}),
img: new fields.StringField({}), img: new fields.StringField({}),
difficulty: new fields.NumberField({ integer: true, nullable: true }), difficulty: new fields.NumberField({ integer: true, nullable: true }),
evasion: new fields.NumberField({ integer: true }), evasion: new fields.NumberField({ integer: true }),
hit: new fields.BooleanField({ initial: false }) hit: new fields.BooleanField({ initial: false }),
saved: new fields.SchemaField({
result: new fields.NumberField(),
success: new fields.BooleanField({ nullable: true, initial: null })
})
}) })
), ),
damage: new fields.SchemaField( hasDamage: new fields.BooleanField({ initial: false }),
{ hasHealing: new fields.BooleanField({ initial: false }),
value: new fields.StringField({}), hasEffect: new fields.BooleanField({ initial: false }),
type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false }) hasSave: new fields.BooleanField({ initial: false }),
}, source: new fields.SchemaField({
{ nullable: true, initial: null } actor: new fields.StringField(),
) item: new fields.StringField(),
action: new fields.StringField()
})
}; };
} }
prepareDerivedData() { get messageTemplate() {
this.targets.forEach(target => { return 'systems/daggerheart/templates/chat/adversary-roll.hbs';
target.hit = target.difficulty ? this.total >= target.difficulty : this.total >= target.evasion;
});
} }
} }

View file

@ -0,0 +1,23 @@
export default class DHApplyEffect extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
title: new fields.StringField(),
origin: new fields.StringField({}),
description: new fields.StringField({}),
targets: new fields.ArrayField(
new fields.SchemaField({
id: new fields.StringField({ required: true }),
name: new fields.StringField(),
img: new fields.StringField(),
hit: new fields.BooleanField({ initial: false })
})
),
action: new fields.SchemaField({
itemId: new fields.StringField(),
actionId: new fields.StringField()
})
};
}
}

View file

@ -3,32 +3,35 @@ export default class DHDamageRoll extends foundry.abstract.TypeDataModel {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
messageType: new fields.StringField({initial: 'damage'}),
title: new fields.StringField(), title: new fields.StringField(),
roll: new fields.StringField({ required: true }), roll: new fields.DataField({}),
damage: new fields.SchemaField({
total: new fields.NumberField({ required: true, integer: true }),
type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false })
}),
dice: new fields.ArrayField(
new fields.SchemaField({
type: new fields.StringField({ required: true }),
rolls: new fields.ArrayField(new fields.NumberField({ required: true, integer: true })),
total: new fields.NumberField({ integer: true })
})
),
modifiers: new fields.ArrayField(
new fields.SchemaField({
value: new fields.NumberField({ required: true, integer: true }),
operator: new fields.StringField({ required: true, choices: ['+', '-', '*', '/'] })
})
),
targets: new fields.ArrayField( targets: new fields.ArrayField(
new fields.SchemaField({ new fields.SchemaField({
id: new fields.StringField({ required: true }), id: new fields.StringField({ required: true }),
actorId: new fields.StringField({}),
name: new fields.StringField(), name: new fields.StringField(),
img: new fields.StringField() img: new fields.StringField(),
hit: new fields.BooleanField({ initial: false }),
saved: new fields.SchemaField({
result: new fields.NumberField(),
success: new fields.BooleanField({ nullable: true, initial: null })
})
}) })
) ),
hasSave: new fields.BooleanField({ initial: false }),
onSave: new fields.StringField(),
source: new fields.SchemaField({
actor: new fields.StringField(),
item: new fields.StringField(),
action: new fields.StringField(),
message: new fields.StringField()
}),
directDamage: new fields.BooleanField({initial: true})
}; };
} }
get messageTemplate() {
return `systems/daggerheart/templates/chat/${this.messageType}-roll.hbs`;
}
} }

View file

@ -1,11 +1,4 @@
import { DualityRollColor } from '../settings/Appearance.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
const diceField = () =>
new fields.SchemaField({
dice: new fields.StringField({}),
value: new fields.NumberField({ integer: true })
});
export default class DHDualityRoll extends foundry.abstract.TypeDataModel { export default class DHDualityRoll extends foundry.abstract.TypeDataModel {
static dualityResult = { static dualityResult = {
@ -17,92 +10,35 @@ export default class DHDualityRoll extends foundry.abstract.TypeDataModel {
static defineSchema() { static defineSchema() {
return { return {
title: new fields.StringField(), title: new fields.StringField(),
origin: new fields.StringField({ required: true }),
roll: new fields.DataField({}), roll: new fields.DataField({}),
modifiers: new fields.ArrayField(
new fields.SchemaField({
value: new fields.NumberField({ integer: true }),
label: new fields.StringField({})
})
),
hope: diceField(),
fear: diceField(),
advantageState: new fields.BooleanField({ nullable: true, initial: null }),
advantage: diceField(),
targets: new fields.ArrayField( targets: new fields.ArrayField(
new fields.SchemaField({ new fields.SchemaField({
id: new fields.StringField({}), id: new fields.StringField({}),
actorId: new fields.StringField({}),
name: new fields.StringField({}), name: new fields.StringField({}),
img: new fields.StringField({}), img: new fields.StringField({}),
difficulty: new fields.NumberField({ integer: true, nullable: true }), difficulty: new fields.NumberField({ integer: true, nullable: true }),
evasion: new fields.NumberField({ integer: true }), evasion: new fields.NumberField({ integer: true }),
hit: new fields.BooleanField({ initial: false }) hit: new fields.BooleanField({ initial: false }),
saved: new fields.SchemaField({
result: new fields.NumberField(),
success: new fields.BooleanField({ nullable: true, initial: null })
})
}) })
), ),
damage: new fields.SchemaField({ hasDamage: new fields.BooleanField({ initial: false }),
value: new fields.StringField({}), hasHealing: new fields.BooleanField({ initial: false }),
type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false }), hasEffect: new fields.BooleanField({ initial: false }),
bonusDamage: new fields.ArrayField( hasSave: new fields.BooleanField({ initial: false }),
new fields.SchemaField({ source: new fields.SchemaField({
value: new fields.StringField({}), actor: new fields.StringField(),
type: new fields.StringField({ item: new fields.StringField(),
choices: Object.keys(SYSTEM.GENERAL.damageTypes), action: new fields.StringField()
integer: false
}),
initiallySelected: new fields.BooleanField(),
appliesOn: new fields.StringField(
{ choices: Object.keys(SYSTEM.EFFECTS.applyLocations) },
{ nullable: true, initial: null }
),
description: new fields.StringField({}),
hopeIncrease: new fields.StringField({ nullable: true })
}),
{ nullable: true, initial: null }
)
}) })
}; };
} }
get diceTotal() { get messageTemplate() {
return this.hope.value + this.fear.value; return 'systems/daggerheart/templates/chat/duality-roll.hbs';
}
get modifierTotal() {
const total = this.modifiers.reduce((acc, x) => acc + x.value, 0);
return {
value: total,
label: total > 0 ? `+${total}` : total < 0 ? `${total}` : ''
};
}
get dualityResult() {
return this.hope.value > this.fear.value
? this.constructor.dualityResult.hope
: this.fear.value > this.hope.value
? this.constructor.dualityResult.fear
: this.constructor.dualityResult.critical;
}
get totalLabel() {
const label =
this.hope.value > this.fear.value
? 'DAGGERHEART.General.Hope'
: this.fear.value > this.hope.value
? 'DAGGERHEART.General.Fear'
: 'DAGGERHEART.General.CriticalSuccess';
return game.i18n.localize(label);
}
get colorful() {
return (
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).dualityColorScheme ===
DualityRollColor.colorful.value
);
}
prepareDerivedData() {
this.hope.discarded = this.hope.value < this.fear.value;
this.fear.discarded = this.fear.value < this.hope.value;
} }
} }

View file

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

View file

@ -1,3 +1,5 @@
import { actionsTypes } from '../action/_module.mjs';
/** /**
* Describes metadata about the item data model type * Describes metadata about the item data model type
* @typedef {Object} ItemDataModelMetadata * @typedef {Object} ItemDataModelMetadata
@ -50,4 +52,24 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
const data = { ...actorRollData, item: { ...this } }; const data = { ...actorRollData, item: { ...this } };
return data; return data;
} }
async _preCreate(data, options, user) {
if(!this.constructor.metadata.hasInitialAction || !foundry.utils.isEmpty(this.actions)) return;
const actionType = {
weapon: 'attack'
}[this.constructor.metadata.type],
cls = actionsTypes.attack,
action = new cls(
{
_id: foundry.utils.randomID(),
type: actionType,
name: game.i18n.localize(SYSTEM.ACTIONS.actionTypes[actionType].name),
...cls.getSourceConfig(this.parent)
},
{
parent: this.parent
}
);
this.updateSource({actions: [action]});
}
} }

View file

@ -1,5 +1,4 @@
import { getTier } from '../../helpers/utils.mjs'; import { getTier } from '../../helpers/utils.mjs';
import DHAction from '../action/action.mjs';
import BaseDataItem from './base.mjs'; import BaseDataItem from './base.mjs';
import ActionField from '../fields/actionField.mjs'; import ActionField from '../fields/actionField.mjs';

View file

@ -2,7 +2,7 @@ import BaseDataItem from './base.mjs';
import FormulaField from '../fields/formulaField.mjs'; import FormulaField from '../fields/formulaField.mjs';
import ActionField from '../fields/actionField.mjs'; import ActionField from '../fields/actionField.mjs';
import { weaponFeatures } from '../../config/itemConfig.mjs'; import { weaponFeatures } from '../../config/itemConfig.mjs';
import { actionsTypes } from '../../data/_module.mjs'; import { actionsTypes } from '../action/_module.mjs';
export default class DHWeapon extends BaseDataItem { export default class DHWeapon extends BaseDataItem {
/** @inheritDoc */ /** @inheritDoc */
@ -14,7 +14,8 @@ export default class DHWeapon extends BaseDataItem {
isQuantifiable: true, isQuantifiable: true,
embedded: { embedded: {
feature: 'featureTest' feature: 'featureTest'
} },
hasInitialAction: true
}); });
} }

View file

@ -0,0 +1,113 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class D20RollDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(config = {}, options = {}) {
super(options);
this.config = config;
this.config.experiences = [];
if (config.source?.action) {
this.item = config.data.parent.items.get(config.source.item);
this.action =
config.data.attack?._id == config.source.action
? config.data.attack
: this.item.system.actions.find(a => a._id === config.source.action);
}
}
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'roll-selection',
classes: ['daggerheart', 'views', 'roll-selection'],
position: {
width: 400,
height: 'auto'
},
actions: {
updateIsAdvantage: this.updateIsAdvantage,
selectExperience: this.selectExperience,
submitRoll: this.submitRoll
},
form: {
handler: this.updateRollConfiguration,
submitOnChange: true,
submitOnClose: false
}
};
/** @override */
static PARTS = {
costSelection: {
id: 'costSelection',
template: 'systems/daggerheart/templates/views/costSelection.hbs'
},
rollSelection: {
id: 'rollSelection',
template: 'systems/daggerheart/templates/views/rollSelection.hbs'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.hasRoll = !!this.config.roll;
context.experiences = Object.keys(this.config.data.experiences).map(id => ({
id,
...this.config.data.experiences[id]
}));
context.selectedExperiences = this.config.experiences;
context.advantage = this.config.advantage;
/* context.diceOptions = this.diceOptions; */
context.canRoll = true;
if (this.config.costs?.length) {
const updatedCosts = this.action.calcCosts(this.config.costs);
context.costs = updatedCosts;
context.canRoll = this.action.hasCost(updatedCosts);
}
if (this.config.uses?.max) {
context.uses = this.action.calcUses(this.config.uses);
context.canRoll = context.canRoll && this.action.hasUses(context.uses);
}
console.log(context, _options)
return context;
}
static updateRollConfiguration(event, _, formData) {
const { ...rest } = foundry.utils.expandObject(formData.object);
if (this.config.costs) this.config.costs = foundry.utils.mergeObject(this.config.costs, rest.costs);
if (this.config.uses) this.config.uses = foundry.utils.mergeObject(this.config.uses, rest.uses);
this.render();
}
static updateIsAdvantage(_, button) {
const advantage = Number(button.dataset.advantage);
this.config.advantage = this.config.advantage === advantage ? 0 : advantage;
this.render();
}
static selectExperience(_, button) {
if (this.config.experiences.find(x => x === button.dataset.key)) {
this.config.experiences = this.config.experiences.filter(x => x !== button.dataset.key);
} else {
this.config.experiences = [...this.config.experiences, button.dataset.key];
}
this.render();
}
static async submitRoll() {
await this.close({ submitted: true });
}
/** @override */
_onClose(options = {}) {
if (!options.submitted) this.config = false;
}
static async configure(config = {}, options={}) {
return new Promise(resolve => {
const app = new this(config, options);
app.addEventListener('close', () => resolve(app.config), { once: true });
app.render({ force: true });
});
}
}

View file

@ -0,0 +1,59 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class DamageDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(config={}, options={}) {
super(options);
this.config = config;
}
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'roll-selection',
classes: ['daggerheart', 'views', 'damage-selection'],
position: {
width: 400,
height: 'auto'
},
actions: {
submitRoll: this.submitRoll
},
form: {
handler: this.updateRollConfiguration,
submitOnChange: true,
submitOnClose: false
}
};
/** @override */
static PARTS = {
damageSelection: {
id: 'damageSelection',
template: 'systems/daggerheart/templates/views/damageSelection.hbs'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.title = this.config.title;
context.formula = this.config.formula;
return context;
}
static async submitRoll() {
await this.close({ submitted: true });
}
/** @override */
_onClose(options={}) {
if ( !options.submitted ) this.config = false;
}
static async configure(config={}) {
return new Promise(resolve => {
const app = new this(config);
app.addEventListener("close", () => resolve(app.config), { once: true });
app.render({ force: true });
});
}
}

View file

@ -1,9 +1,6 @@
import DamageSelectionDialog from '../applications/damageSelectionDialog.mjs'; import DamageSelectionDialog from '../applications/damageSelectionDialog.mjs';
import NpcRollSelectionDialog from '../applications/npcRollSelectionDialog.mjs';
import RollSelectionDialog from '../applications/rollSelectionDialog.mjs';
import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs'; import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs';
import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; import DamageReductionDialog from '../applications/damageReductionDialog.mjs';
import DHDualityRoll from '../data/chat-message/dualityRoll.mjs';
export default class DhpActor extends Actor { export default class DhpActor extends Actor {
async _preCreate(data, options, user) { async _preCreate(data, options, user) {
@ -268,150 +265,26 @@ export default class DhpActor extends Actor {
* @param {boolean} [config.roll.simple=false] * @param {boolean} [config.roll.simple=false]
* @param {string} [config.roll.type] * @param {string} [config.roll.type]
* @param {number} [config.roll.difficulty] * @param {number} [config.roll.difficulty]
* @param {any} [config.damage] * @param {boolean} [config.hasDamage]
* @param {boolean} [config.hasEffect]
* @param {object} [config.chatMessage] * @param {object} [config.chatMessage]
* @param {string} config.chatMessage.template * @param {string} config.chatMessage.template
* @param {boolean} [config.chatMessage.mute] * @param {boolean} [config.chatMessage.mute]
* @param {boolean} [config.checkTarget] * @param {object} [config.targets]
* @param {object} [config.costs]
*/ */
async diceRoll(config) { async diceRoll(config) {
let hopeDice = 'd12', config.source = {...(config.source ?? {}), actor: this.uuid};
fearDice = 'd12', config.data = this.getRollData();
advantageDice = 'd6', return await this.rollClass.build(config);
disadvantageDice = 'd6', }
advantage = config.event.altKey ? true : config.event.ctrlKey ? false : null,
targets,
damage = config.damage,
modifiers = this.formatRollModifier(config.roll),
rollConfig,
formula,
hope,
fear;
if (!config.event.shiftKey && !config.event.altKey && !config.event.ctrlKey) { get rollClass() {
const dialogClosed = new Promise((resolve, _) => { return CONFIG.Dice.daggerheart[this.type === 'character' ? 'DualityRoll' : 'D20Roll'];
this.type === 'character' }
? new RollSelectionDialog(
this.system.experiences,
this.system.resources.hope.value,
resolve
).render(true)
: new NpcRollSelectionDialog(this.system.experiences, resolve).render(true);
});
rollConfig = await dialogClosed;
advantage = rollConfig.advantage; getRollData() {
hopeDice = rollConfig.hope; return this.system;
fearDice = rollConfig.fear;
rollConfig.experiences.forEach(x =>
modifiers.push({
value: x.value,
label: x.value >= 0 ? `+${x.value}` : `-${x.value}`,
title: x.description
})
);
if (this.type === 'character') {
const automateHope = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation).hope;
if (automateHope && result.hopeUsed) {
await this.update({
'system.resources.hope.value': this.system.resources.hope.value - result.hopeUsed
});
}
}
}
if (this.type === 'character') {
formula = `1${hopeDice} + 1${fearDice}${advantage === true ? ` + 1d6` : advantage === false ? ` - 1d6` : ''}`;
} else {
formula = `${advantage === true || advantage === false ? 2 : 1}d20${advantage === true ? 'kh' : advantage === false ? 'kl' : ''}`;
}
formula += ` ${modifiers.map(x => `+ ${x.value}`).join(' ')}`;
const roll = await Roll.create(formula).evaluate();
const dice = roll.dice.flatMap(dice => ({
denomination: dice.denomination,
number: dice.number,
total: dice.total,
results: dice.results.map(result => ({ result: result.result, discarded: !result.active }))
}));
if (this.type === 'character') {
setDiceSoNiceForDualityRoll(roll, advantage);
hope = roll.dice[0].results[0].result;
fear = roll.dice[1].results[0].result;
if (
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation).hope &&
config.roll.type === 'action'
) {
if (hope > fear) {
await this.update({
'system.resources.hope.value': Math.min(
this.system.resources.hope.value + 1,
this.system.resources.hope.max
)
});
} else if (hope === fear) {
await this.update({
'system.resources': {
'hope.value': Math.min(
this.system.resources.hope.value + 1,
this.system.resources.hope.max
),
'stress.value': Math.max(this.system.resources.stress.value - 1, 0)
}
});
}
}
}
if (config.checkTarget) {
targets = Array.from(game.user.targets).map(x => {
const target = {
id: x.id,
name: x.actor.name,
img: x.actor.img,
difficulty: x.actor.system.difficulty,
evasion: x.actor.system.evasion?.value
};
target.hit = target.difficulty ? roll.total >= target.difficulty : roll.total >= target.evasion;
return target;
});
}
if (config.chatMessage) {
const configRoll = {
title: config.title,
origin: this.id,
dice,
roll,
modifiers: modifiers.filter(x => x.label),
advantageState: advantage
};
if (this.type === 'character') {
configRoll.hope = { dice: hopeDice, value: hope };
configRoll.fear = { dice: fearDice, value: fear };
configRoll.advantage = { dice: advantageDice, value: roll.dice[2]?.results[0].result ?? null };
}
if (damage) configRoll.damage = damage;
if (targets) configRoll.targets = targets;
const systemData =
this.type === 'character' && !config.roll.simple ? new DHDualityRoll(configRoll) : configRoll,
cls = getDocumentClass('ChatMessage'),
msg = new cls({
type: config.chatMessage.type ?? 'dualityRoll',
sound: config.chatMessage.mute ? null : CONFIG.sounds.dice,
system: systemData,
content: config.chatMessage.template,
rolls: [roll]
});
await cls.create(msg.toObject());
}
return roll;
} }
formatRollModifier(roll) { formatRollModifier(roll) {
@ -507,124 +380,76 @@ export default class DhpActor extends Actor {
? 1 ? 1
: 0; : 0;
const update = { if (
'system.resources.hitPoints.value': Math.min( this.type === 'character' &&
this.system.resources.hitPoints.value + hpDamage, this.system.armor &&
this.system.resources.hitPoints.max this.system.armor.system.marks.value < this.system.armorScore
) ) {
}; new Promise((resolve, reject) => {
new DamageReductionDialog(resolve, reject, this, hpDamage).render(true);
if (game.user.isGM) { })
await this.update(update); .then(async ({ modifiedDamage, armorSpent, stressSpent }) => {
const resources = [
{ value: modifiedDamage, type: 'hitPoints' },
...(armorSpent ? [{ value: armorSpent, type: 'armorStack' }] : []),
...(stressSpent ? [{ value: stressSpent, type: 'stress' }] : [])
];
await this.modifyResource(resources);
})
.catch(() => {
const cls = getDocumentClass('ChatMessage');
const msg = new cls({
user: game.user.id,
content: game.i18n.format('DAGGERHEART.DamageReduction.Notifications.DamageIgnore', {
character: this.name
})
});
cls.create(msg.toObject());
});
} else { } else {
await game.socket.emit(`system.${SYSTEM.id}`, { await this.modifyResource([{ value: hpDamage, type: 'hitPoints' }]);
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateDocument,
uuid: this.uuid,
update: update
}
});
} }
} }
async takeHealing(healing, type) { async takeHealing(resources) {
let update = {}; resources.forEach(r => (r.value *= -1));
switch (type) { await this.modifyResource(resources);
case SYSTEM.GENERAL.healingTypes.health.id:
update = {
'system.resources.hitPoints.value': Math.min(
this.system.resources.hitPoints.value + healing,
this.system.resources.hitPoints.max
)
};
break;
case SYSTEM.GENERAL.healingTypes.stress.id:
update = {
'system.resources.stress.value': Math.min(
this.system.resources.stress.value + healing,
this.system.resources.stress.max
)
};
break;
}
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
}
});
}
} }
//Move to action-scope? async modifyResource(resources) {
async useAction(action) { if (!resources.length) return;
const userTargets = Array.from(game.user.targets); let updates = { actor: { target: this, resources: {} }, armor: { target: this.system.armor, resources: {} } };
const otherTarget = action.target.type === SYSTEM.ACTIONS.targetTypes.other.id; resources.forEach(r => {
if (otherTarget && userTargets.length === 0) { switch (r.type) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.ActionRequiresTarget')); case 'armorStack':
return; updates.armor.resources['system.marks.value'] = Math.max(
} Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore),
0
if (action.cost.type != null && action.cost.value != null) { );
if ( break;
this.system.resources[action.cost.type].value - action.cost.value <= default:
this.system.resources[action.cost.type].min updates.actor.resources[`system.resources.${r.type}.value`] = Math.max(
) { Math.min(this.system.resources[r.type].value + r.value, this.system.resources[r.type].max),
ui.notifications.error(game.i18n.localize(`Insufficient ${action.cost.type} to use this ability`)); 0
return; );
break;
} }
} });
Object.values(updates).forEach(async u => {
// const targets = otherTarget ? userTargets : [game.user.character]; if (Object.keys(u.resources).length > 0) {
if (action.damage.type) { if (game.user.isGM) {
let roll = { formula: action.damage.value, result: action.damage.value }; await u.target.update(u.resources);
if (Number.isNaN(Number.parseInt(action.damage.value))) { } else {
roll = await new Roll(`1${action.damage.value}`).evaluate(); await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateDocument,
uuid: u.target.uuid,
update: u.resources
}
});
}
} }
});
const cls = getDocumentClass('ChatMessage');
const msg = new cls({
user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/damage-roll.hbs',
{
roll: roll.formula,
total: roll.result,
type: action.damage.type
}
)
});
cls.create(msg.toObject());
}
if (action.healing.type) {
let roll = { formula: action.healing.value, result: action.healing.value };
if (Number.isNaN(Number.parseInt(action.healing.value))) {
roll = await new Roll(`1${action.healing.value}`).evaluate();
}
const cls = getDocumentClass('ChatMessage');
const msg = new cls({
user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/healing-roll.hbs',
{
roll: roll.formula,
total: roll.result,
type: action.healing.type
}
)
});
cls.create(msg.toObject());
}
} }
} }

View file

@ -131,14 +131,7 @@ export default class DhpItem extends Item {
action = await this.selectActionDialog(); action = await this.selectActionDialog();
} }
if (action) response = action.use(event); if (action) response = action.use(event);
// Check Target
// If action.roll => Roll Dialog
// Else If action.cost => Cost Dialog
// Then
// Apply Cost
// Apply Effect
} }
// Display Item Card in chat
return response; return response;
} }

View file

@ -233,3 +233,29 @@ Roll.replaceFormulaData = function (formula, data, { missing, warn = false } = {
formula = terms.reduce((a, c) => a.replaceAll(`@${c.term}`, data[c.term] ?? c.default), formula); formula = terms.reduce((a, c) => a.replaceAll(`@${c.term}`, data[c.term] ?? c.default), formula);
return nativeReplaceFormulaData(formula, data, { missing, warn }); 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');
}
};
export const damageKeyToNumber = key => {
switch (key) {
case 'severe':
return 3;
case 'major':
return 2;
case 'minor':
return 1;
case 'none':
return 0;
}
};

View file

@ -16,9 +16,18 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
} }
addChatListeners = async (app, html, data) => { addChatListeners = async (app, html, data) => {
html.querySelectorAll('.duality-action').forEach(element => html.querySelectorAll('.duality-action-damage').forEach(element =>
element.addEventListener('click', event => this.onRollDamage(event, data.message)) element.addEventListener('click', event => this.onRollDamage(event, data.message))
); );
html.querySelectorAll('.duality-action-healing').forEach(element =>
element.addEventListener('click', event => this.onRollHealing(event, data.message))
);
html.querySelectorAll('.target-save-container').forEach(element =>
element.addEventListener('click', event => this.onRollSave(event, data.message))
);
html.querySelectorAll('.duality-action-effect').forEach(element =>
element.addEventListener('click', event => this.onApplyEffect(event, data.message))
);
html.querySelectorAll('.target-container').forEach(element => { html.querySelectorAll('.target-container').forEach(element => {
element.addEventListener('mouseenter', this.hoverTarget); element.addEventListener('mouseenter', this.hoverTarget);
element.addEventListener('mouseleave', this.unhoverTarget); element.addEventListener('mouseleave', this.unhoverTarget);
@ -27,7 +36,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
html.querySelectorAll('.damage-button').forEach(element => html.querySelectorAll('.damage-button').forEach(element =>
element.addEventListener('click', event => this.onDamage(event, data.message)) element.addEventListener('click', event => this.onDamage(event, data.message))
); );
html.querySelectorAll('.healing-button').forEach(element => element.addEventListener('click', this.onHealing)); html.querySelectorAll('.healing-button').forEach(element =>
element.addEventListener('click', event => this.onHealing(event, data.message))
);
html.querySelectorAll('.target-indicator').forEach(element => html.querySelectorAll('.target-indicator').forEach(element =>
element.addEventListener('click', this.onToggleTargets) element.addEventListener('click', this.onToggleTargets)
); );
@ -54,17 +65,65 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
super.close(options); super.close(options);
} }
async getActor(id) {
// return game.actors.get(id);
return await fromUuid(id);
}
getAction(actor, itemId, actionId) {
const item = actor.items.get(itemId),
action =
actor.system.attack?._id === actionId
? actor.system.attack
: item?.system?.actions?.find(a => a._id === actionId);
return action;
}
onRollDamage = async (event, message) => { onRollDamage = async (event, message) => {
event.stopPropagation(); event.stopPropagation();
const actor = game.actors.get(message.system.origin); const actor = await this.getActor(message.system.source.actor);
if (!actor || !game.user.isGM) return true; if (!actor || !game.user.isGM) return true;
if (message.system.source.item && message.system.source.action) {
const action = this.getAction(actor, message.system.source.item, message.system.source.action);
if (!action || !action?.rollDamage) return;
await action.rollDamage(event, message);
}
};
await actor.damageRoll( onRollHealing = async (event, message) => {
message.system.title, event.stopPropagation();
message.system.damage, const actor = await this.getActor(message.system.source.actor);
message.system.targets.filter(x => x.hit).map(x => ({ id: x.id, name: x.name, img: x.img })), if (!actor || !game.user.isGM) return true;
event.shiftKey if (message.system.source.item && message.system.source.action) {
); const action = this.getAction(actor, message.system.source.item, message.system.source.action);
if (!action || !action?.rollHealing) return;
await action.rollHealing(event, message);
}
};
onRollSave = async (event, message) => {
event.stopPropagation();
const actor = await this.getActor(message.system.source.actor),
tokenId = event.target.closest('[data-token]')?.dataset.token,
token = game.canvas.tokens.get(tokenId);
if (!token?.actor || !token.isOwner) return true;
console.log(token.actor.canUserModify(game.user, 'update'));
if (message.system.source.item && message.system.source.action) {
const action = this.getAction(actor, message.system.source.item, message.system.source.action);
if (!action || !action?.hasSave) return;
action.rollSave(token, event, message);
}
};
onApplyEffect = async (event, message) => {
event.stopPropagation();
const actor = await this.getActor(message.system.source.actor);
if (!actor || !game.user.isGM) return true;
if (message.system.source.item && message.system.source.action) {
const action = this.getAction(actor, message.system.source.item, message.system.source.action);
if (!action || !action?.applyEffects) return;
await action.applyEffects(event, message);
}
}; };
hoverTarget = event => { hoverTarget = event => {
@ -95,24 +154,36 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
? message.system.targets.map(target => game.canvas.tokens.get(target.id)) ? message.system.targets.map(target => game.canvas.tokens.get(target.id))
: Array.from(game.user.targets); : Array.from(game.user.targets);
if(message.system.onSave && event.currentTarget.dataset.targetHit) {
console.log(message.system.targets)
const pendingingSaves = message.system.targets.filter(target => target.hit && target.saved.success === null);
if(pendingingSaves.length) {
const confirm = await foundry.applications.api.DialogV2.confirm({
window: {title: "Pending Reaction Rolls found"},
content: `<p>Some Tokens still need to roll their Reaction Roll.</p><p>Are you sure you want to continue ?</p><p><i>Undone reaction rolls will be considered as failed</i></p>`
});
if ( !confirm ) return;
}
}
if (targets.length === 0) if (targets.length === 0)
ui.notifications.info(game.i18n.localize('DAGGERHEART.Notification.Info.NoTargetsSelected')); ui.notifications.info(game.i18n.localize('DAGGERHEART.Notification.Info.NoTargetsSelected'));
for (let target of targets) {
for (var target of targets) { let damage = message.system.roll.total;
await target.actor.takeDamage(message.system.damage.total, message.system.damage.type); if(message.system.onSave && message.system.targets.find(t => t.id === target.id)?.saved?.success === true) damage = Math.ceil(damage * (SYSTEM.ACTIONS.damageOnSave[message.system.onSave]?.mod ?? 1));
await target.actor.takeDamage(damage, message.system.roll.type);
} }
}; };
onHealing = async event => { onHealing = async (event, message) => {
event.stopPropagation(); event.stopPropagation();
const healing = Number.parseInt(event.currentTarget.dataset.value);
const targets = Array.from(game.user.targets); const targets = Array.from(game.user.targets);
if (targets.length === 0) if (targets.length === 0)
ui.notifications.info(game.i18n.localize('DAGGERHEART.Notification.Info.NoTargetsSelected')); ui.notifications.info(game.i18n.localize('DAGGERHEART.Notification.Info.NoTargetsSelected'));
for (var target of targets) { for (var target of targets) {
await target.actor.takeHealing(healing, event.currentTarget.dataset.type); await target.actor.takeHealing([{ value: message.system.roll.total, type: message.system.roll.type }]);
} }
}; };
@ -139,7 +210,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
event.stopPropagation(); event.stopPropagation();
const action = message.system.actions[Number.parseInt(event.currentTarget.dataset.index)]; const action = message.system.actions[Number.parseInt(event.currentTarget.dataset.index)];
const actor = game.actors.get(message.system.origin); const actor = game.actors.get(message.system.source.actor);
await actor.useAction(action); await actor.useAction(action);
}; };

View file

@ -227,6 +227,10 @@ div.daggerheart.views.multiclass {
filter: invert(0%) sepia(100%) saturate(0%) hue-rotate(21deg) brightness(17%) contrast(103%); filter: invert(0%) sepia(100%) saturate(0%) hue-rotate(21deg) brightness(17%) contrast(103%);
} }
} }
#roll-selection-costSelection footer {
display: none;
}
.roll-dialog-container { .roll-dialog-container {
.disadvantage, .disadvantage,
@ -368,6 +372,7 @@ div.daggerheart.views.multiclass {
} }
} }
.roll-dialog-experience-container { .roll-dialog-experience-container {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;

View file

@ -6,6 +6,13 @@
} }
} }
fieldset.daggerheart.chat {
padding: 0;
border-left-width: 0;
border-right-width: 0;
border-bottom-width: 0;
}
.daggerheart.chat { .daggerheart.chat {
&.downtime { &.downtime {
display: flex; display: flex;
@ -227,19 +234,35 @@
background: @miss; background: @miss;
} }
img { img, .target-save-container {
flex: 0;
width: 22px; width: 22px;
height: 22px; height: 22px;
margin-left: 8px;
align-self: center; align-self: center;
border-color: transparent; border-color: transparent;
} }
img {
flex: 0;
margin-left: 8px;
}
.target-save-container {
margin-right: 8px;
justify-content: center;
display: flex;
align-items: center;
min-height: unset;
border: 1px solid black;
}
.target-inner-container { .target-inner-container {
flex: 1; flex: 1;
display: flex; display: flex;
justify-content: center; justify-content: center;
font-size: var(--font-size-16);
}
&:not(:has(.target-save-container)) .target-inner-container {
margin-right: @hugeMargin; margin-right: @hugeMargin;
} }
} }
@ -314,12 +337,29 @@
width: 80px; width: 80px;
} }
} }
[data-use-perm='false'] {
pointer-events: none;
border-color: transparent;
}
[data-view-perm='false'] {
> * {
display: none;
}
&::after {
content: "??";
}
}
} }
.theme-colorful { .theme-colorful {
.chat-message.duality { .chat-message.duality {
border-color: black; border-color: black;
padding: 8px 0 0 0; padding: 8px 0 0 0;
fieldset.daggerheart.chat {
border-top-width: 0;
display: contents;
}
.message-header { .message-header {
color: var(--color-light-3); color: var(--color-light-3);
padding: 0 8px; padding: 0 8px;
@ -450,6 +490,10 @@
.duality-action { .duality-action {
border-radius: 0 6px 0 0; border-radius: 0 6px 0 0;
margin-left: -8px; margin-left: -8px;
&.duality-action-effect {
border-top-left-radius: 6px;
margin-left: initial;
}
} }
.duality-result { .duality-result {
border-radius: 6px 0 0 0; border-radius: 6px 0 0 0;

View file

@ -1400,6 +1400,12 @@
.chat-message .dice-title { .chat-message .dice-title {
display: none; display: none;
} }
fieldset.daggerheart.chat {
padding: 0;
border-left-width: 0;
border-right-width: 0;
border-bottom-width: 0;
}
.daggerheart.chat.downtime { .daggerheart.chat.downtime {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -1553,18 +1559,32 @@
.daggerheart.chat.roll .target-section .target-container.miss { .daggerheart.chat.roll .target-section .target-container.miss {
background: #ff0000; background: #ff0000;
} }
.daggerheart.chat.roll .target-section .target-container img { .daggerheart.chat.roll .target-section .target-container img,
flex: 0; .daggerheart.chat.roll .target-section .target-container .target-save-container {
width: 22px; width: 22px;
height: 22px; height: 22px;
margin-left: 8px;
align-self: center; align-self: center;
border-color: transparent; border-color: transparent;
} }
.daggerheart.chat.roll .target-section .target-container img {
flex: 0;
margin-left: 8px;
}
.daggerheart.chat.roll .target-section .target-container .target-save-container {
margin-right: 8px;
justify-content: center;
display: flex;
align-items: center;
min-height: unset;
border: 1px solid black;
}
.daggerheart.chat.roll .target-section .target-container .target-inner-container { .daggerheart.chat.roll .target-section .target-container .target-inner-container {
flex: 1; flex: 1;
display: flex; display: flex;
justify-content: center; justify-content: center;
font-size: var(--font-size-16);
}
.daggerheart.chat.roll .target-section .target-container:not(:has(.target-save-container)) .target-inner-container {
margin-right: 32px; margin-right: 32px;
} }
.daggerheart.chat.roll .dice-actions { .daggerheart.chat.roll .dice-actions {
@ -1622,10 +1642,24 @@
.daggerheart.chat.domain-card img { .daggerheart.chat.domain-card img {
width: 80px; width: 80px;
} }
.daggerheart.chat [data-use-perm='false'] {
pointer-events: none;
border-color: transparent;
}
.daggerheart.chat [data-view-perm='false'] > * {
display: none;
}
.daggerheart.chat [data-view-perm='false']::after {
content: "??";
}
.theme-colorful .chat-message.duality { .theme-colorful .chat-message.duality {
border-color: black; border-color: black;
padding: 8px 0 0 0; padding: 8px 0 0 0;
} }
.theme-colorful .chat-message.duality fieldset.daggerheart.chat {
border-top-width: 0;
display: contents;
}
.theme-colorful .chat-message.duality .message-header { .theme-colorful .chat-message.duality .message-header {
color: var(--color-light-3); color: var(--color-light-3);
padding: 0 8px; padding: 0 8px;
@ -1752,6 +1786,10 @@
border-radius: 0 6px 0 0; border-radius: 0 6px 0 0;
margin-left: -8px; margin-left: -8px;
} }
.theme-colorful .chat-message.duality .message-content .dice-result .dice-actions .duality-action.duality-action-effect {
border-top-left-radius: 6px;
margin-left: initial;
}
.theme-colorful .chat-message.duality .message-content .dice-result .dice-actions .duality-result { .theme-colorful .chat-message.duality .message-content .dice-result .dice-actions .duality-result {
border-radius: 6px 0 0 0; border-radius: 6px 0 0 0;
margin-right: -8px; margin-right: -8px;
@ -1978,6 +2016,9 @@ div.daggerheart.views.multiclass {
.daggerheart.views.roll-selection .roll-selection-container i { .daggerheart.views.roll-selection .roll-selection-container i {
filter: invert(0%) sepia(100%) saturate(0%) hue-rotate(21deg) brightness(17%) contrast(103%); filter: invert(0%) sepia(100%) saturate(0%) hue-rotate(21deg) brightness(17%) contrast(103%);
} }
.daggerheart.views.roll-selection #roll-selection-costSelection footer {
display: none;
}
.daggerheart.views.roll-selection .roll-dialog-container .disadvantage, .daggerheart.views.roll-selection .roll-dialog-container .disadvantage,
.daggerheart.views.roll-selection .roll-dialog-container .advantage { .daggerheart.views.roll-selection .roll-dialog-container .advantage {
border: 2px solid #708090; border: 2px solid #708090;
@ -3110,6 +3151,125 @@ div.daggerheart.views.multiclass {
.daggerheart.views.ownership-selection .ownership-outer-container .ownership-container select { .daggerheart.views.ownership-selection .ownership-outer-container .ownership-container select {
margin: 4px 0; 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;
white-space: nowrap;
}
.daggerheart.views.damage-reduction .damage-reduction-container .resources-container {
display: flex;
gap: 8px;
width: 100%;
}
.daggerheart.views.damage-reduction .damage-reduction-container .resources-container .resource-container {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection {
display: flex;
align-items: center;
width: 100%;
margin: 0;
}
.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-selection-inner {
display: flex;
gap: 2px;
}
.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-selection-inner:not(:last-child) {
margin-right: 8px;
}
.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-selection-inner .mark-container {
cursor: pointer;
border: 1px solid light-dark(#18162e, #f3c267);
border-radius: 6px;
height: 26px;
padding: 0 1px;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.4;
}
.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-selection-inner .mark-container.selected {
opacity: 1;
}
.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-selection-inner .mark-container.inactive {
cursor: initial;
opacity: 0.2;
}
.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-selection-inner .mark-container .fa-shield {
position: relative;
right: 0.5px;
}
.daggerheart.views.damage-reduction .damage-reduction-container .stress-reduction-container {
margin: 0;
width: 100%;
}
.daggerheart.views.damage-reduction .damage-reduction-container .stress-reduction-container .stress-reduction {
border: 1px solid light-dark(#18162e, #f3c267);
border-radius: 6px;
height: 26px;
padding: 0 4px;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
opacity: 0.4;
}
.daggerheart.views.damage-reduction .damage-reduction-container .stress-reduction-container .stress-reduction.active {
opacity: 1;
cursor: pointer;
}
.daggerheart.views.damage-reduction .damage-reduction-container .stress-reduction-container .stress-reduction.selected {
opacity: 1;
background: var(--color-warm-2);
color: white;
}
.daggerheart.views.damage-reduction .damage-reduction-container .stress-reduction-container .stress-reduction .stress-reduction-cost {
display: flex;
align-items: center;
}
.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 footer {
display: flex;
width: 100%;
}
.daggerheart.views.damage-reduction .damage-reduction-container footer button {
flex: 1;
}
.daggerheart.views.damage-reduction .damage-reduction-container footer button .damage-value {
font-weight: bold;
}
.daggerheart.views.damage-reduction .damage-reduction-container footer button .damage-value.reduced-value {
opacity: 0.4;
text-decoration: line-through;
}
:root { :root {
--shadow-text-stroke: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; --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; --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 './characterCreation.less';
@import './levelup.less'; @import './levelup.less';
@import './ownershipSelection.less'; @import './ownershipSelection.less';
@import './damageReduction.less';
@import './resources.less'; @import './resources.less';
@import './countdown.less'; @import './countdown.less';
@import './settings.less'; @import './settings.less';

145
styles/damageReduction.less Normal file
View file

@ -0,0 +1,145 @@
.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;
white-space: nowrap;
}
.resources-container {
display: flex;
gap: 8px;
width: 100%;
.resource-container {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
}
.mark-selection {
display: flex;
align-items: center;
width: 100%;
margin: 0;
.mark-selection-inner {
display: flex;
gap: 2px;
&:not(:last-child) {
margin-right: 8px;
}
.mark-container {
cursor: pointer;
border: 1px solid light-dark(@dark-blue, @golden);
border-radius: 6px;
height: 26px;
padding: 0 1px;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.4;
&.selected {
opacity: 1;
}
&.inactive {
cursor: initial;
opacity: 0.2;
}
.fa-shield {
position: relative;
right: 0.5px;
}
}
}
}
.stress-reduction-container {
margin: 0;
width: 100%;
.stress-reduction {
border: 1px solid light-dark(@dark-blue, @golden);
border-radius: 6px;
height: 26px;
padding: 0 4px;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
opacity: 0.4;
&.active {
opacity: 1;
cursor: pointer;
}
&.selected {
opacity: 1;
background: var(--color-warm-2);
color: white;
}
.stress-reduction-cost {
display: flex;
align-items: center;
}
}
}
.markers-subtitle {
margin: -4px 0 0 0;
&.bold {
font-variant: all-small-caps;
font-weight: bold;
}
}
footer {
display: flex;
width: 100%;
button {
flex: 1;
.damage-value {
font-weight: bold;
&.reduced-value {
opacity: 0.4;
text-decoration: line-through;
}
}
}
}
}
}

View file

@ -1,11 +1,11 @@
@import '../utils/colors.less'; @import '../utils/colors.less';
@import '../utils/fonts.less'; @import '../utils/fonts.less';
.application.sheet.daggerheart.actor.dh-style.character { .application.sheet.daggerheart.actor.dh-style.character {
.window-content { .window-content {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
} }

View file

@ -1,20 +1,20 @@
@import '../../utils/colors.less'; @import '../../utils/colors.less';
@import '../../utils/fonts.less'; @import '../../utils/fonts.less';
.application.sheet.daggerheart.actor.dh-style.character { .application.sheet.daggerheart.actor.dh-style.character {
.tab.biography { .tab.biography {
.items-section { .items-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
mask-image: linear-gradient(0deg, transparent 0%, black 10%, black 98%, transparent 100%); mask-image: linear-gradient(0deg, transparent 0%, black 10%, black 98%, transparent 100%);
padding-bottom: 40px; padding-bottom: 40px;
height: 100%; height: 100%;
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: light-dark(@dark-blue, @golden) transparent; scrollbar-color: light-dark(@dark-blue, @golden) transparent;
} }
} }
} }

View file

@ -1,20 +1,20 @@
@import '../../utils/colors.less'; @import '../../utils/colors.less';
@import '../../utils/fonts.less'; @import '../../utils/fonts.less';
.application.sheet.daggerheart.actor.dh-style.character { .application.sheet.daggerheart.actor.dh-style.character {
.tab.features { .tab.features {
.features-sections { .features-sections {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
overflow-y: auto; overflow-y: auto;
mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%);
padding: 20px 0; padding: 20px 0;
padding-top: 10px; padding-top: 10px;
height: 95%; height: 95%;
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: light-dark(@dark-blue, @golden) transparent; scrollbar-color: light-dark(@dark-blue, @golden) transparent;
} }
} }
} }

View file

@ -1,65 +1,65 @@
@import '../../utils/colors.less'; @import '../../utils/colors.less';
@import '../../utils/fonts.less'; @import '../../utils/fonts.less';
.application.sheet.daggerheart.actor.dh-style.character { .application.sheet.daggerheart.actor.dh-style.character {
.tab.inventory { .tab.inventory {
.search-section { .search-section {
display: flex; display: flex;
gap: 10px; gap: 10px;
align-items: center; align-items: center;
.search-bar { .search-bar {
position: relative; position: relative;
color: light-dark(@dark-blue-50, @beige-50); color: light-dark(@dark-blue-50, @beige-50);
width: 100%; width: 100%;
padding-top: 5px; padding-top: 5px;
input { input {
border-radius: 50px; border-radius: 50px;
font-family: @font-body; font-family: @font-body;
background: light-dark(@dark-blue-10, @golden-10); background: light-dark(@dark-blue-10, @golden-10);
border: none; border: none;
outline: 2px solid transparent; outline: 2px solid transparent;
transition: all 0.3s ease; transition: all 0.3s ease;
padding: 0 20px; padding: 0 20px;
&:hover { &:hover {
outline: 2px solid light-dark(@dark, @golden); outline: 2px solid light-dark(@dark, @golden);
} }
&:placeholder { &:placeholder {
color: light-dark(@dark-blue-50, @beige-50); color: light-dark(@dark-blue-50, @beige-50);
} }
} }
.icon { .icon {
align-content: center; align-content: center;
height: 32px; height: 32px;
position: absolute; position: absolute;
right: 20px; right: 20px;
font-size: 16px; font-size: 16px;
z-index: 1; z-index: 1;
color: light-dark(@dark-blue-50, @beige-50); color: light-dark(@dark-blue-50, @beige-50);
} }
} }
} }
.items-section { .items-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
overflow-y: auto; overflow-y: auto;
mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%);
padding: 20px 0; padding: 20px 0;
height: 80%; height: 80%;
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: light-dark(@dark-blue, @golden) transparent; scrollbar-color: light-dark(@dark-blue, @golden) transparent;
} }
.currency-section { .currency-section {
display: flex; display: flex;
gap: 10px; gap: 10px;
} }
} }
} }

View file

@ -1,101 +1,101 @@
@import '../../utils/colors.less'; @import '../../utils/colors.less';
@import '../../utils/fonts.less'; @import '../../utils/fonts.less';
.application.sheet.daggerheart.actor.dh-style.character { .application.sheet.daggerheart.actor.dh-style.character {
.tab.loadout { .tab.loadout {
.search-section { .search-section {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
.search-bar { .search-bar {
position: relative; position: relative;
color: light-dark(@dark-blue-50, @beige-50); color: light-dark(@dark-blue-50, @beige-50);
width: 80%; width: 80%;
padding-top: 5px; padding-top: 5px;
input { input {
border-radius: 50px; border-radius: 50px;
font-family: @font-body; font-family: @font-body;
background: light-dark(@dark-blue-10, @golden-10); background: light-dark(@dark-blue-10, @golden-10);
border: none; border: none;
outline: 2px solid transparent; outline: 2px solid transparent;
transition: all 0.3s ease; transition: all 0.3s ease;
padding: 0 20px; padding: 0 20px;
&:hover { &:hover {
outline: 2px solid light-dark(@dark, @golden); outline: 2px solid light-dark(@dark, @golden);
} }
&:placeholder { &:placeholder {
color: light-dark(@dark-blue-50, @beige-50); color: light-dark(@dark-blue-50, @beige-50);
} }
} }
.icon { .icon {
align-content: center; align-content: center;
height: 32px; height: 32px;
position: absolute; position: absolute;
right: 20px; right: 20px;
font-size: 16px; font-size: 16px;
z-index: 1; z-index: 1;
color: light-dark(@dark-blue-50, @beige-50); color: light-dark(@dark-blue-50, @beige-50);
} }
} }
.btn-toggle-view { .btn-toggle-view {
background: light-dark(@dark-blue-10, @dark-blue); background: light-dark(@dark-blue-10, @dark-blue);
border: 1px solid light-dark(@dark-blue, @golden); border: 1px solid light-dark(@dark-blue, @golden);
border-radius: 15px; border-radius: 15px;
padding: 0; padding: 0;
gap: 0; gap: 0;
width: 62px; width: 62px;
span { span {
margin: 1px; margin: 1px;
width: 26px; width: 26px;
color: light-dark(@dark-blue, @golden); color: light-dark(@dark-blue, @golden);
&.list-icon { &.list-icon {
i { i {
margin-left: 3px; margin-left: 3px;
} }
} }
&.grid-icon { &.grid-icon {
i { i {
margin-right: 3px; margin-right: 3px;
} }
} }
&.list-active { &.list-active {
border-radius: 32px 3px 3px 32px; border-radius: 32px 3px 3px 32px;
background-color: light-dark(@dark-blue, @golden); background-color: light-dark(@dark-blue, @golden);
color: light-dark(@beige, @dark-blue); color: light-dark(@beige, @dark-blue);
padding: 2px; padding: 2px;
} }
&.grid-active { &.grid-active {
border-radius: 3px 32px 32px 3px; border-radius: 3px 32px 32px 3px;
background-color: light-dark(@dark-blue, @golden); background-color: light-dark(@dark-blue, @golden);
color: light-dark(@beige, @dark-blue); color: light-dark(@beige, @dark-blue);
padding: 2px; padding: 2px;
} }
} }
} }
} }
.items-section { .items-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
mask-image: linear-gradient(0deg, transparent 0%, black 10%, black 98%, transparent 100%); mask-image: linear-gradient(0deg, transparent 0%, black 10%, black 98%, transparent 100%);
padding: 20px 0; padding: 20px 0;
height: 90%; height: 90%;
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: light-dark(@dark-blue, @golden) transparent; scrollbar-color: light-dark(@dark-blue, @golden) transparent;
} }
} }
} }

View file

@ -1,32 +1,32 @@
@import '../../utils/colors.less'; @import '../../utils/colors.less';
@import '../../utils/fonts.less'; @import '../../utils/fonts.less';
.application.sheet.daggerheart.actor.dh-style.character { .application.sheet.daggerheart.actor.dh-style.character {
.window-content { .window-content {
display: grid; display: grid;
grid-template-columns: 275px 1fr; grid-template-columns: 275px 1fr;
grid-template-rows: 283px 1fr; grid-template-rows: 283px 1fr;
gap: 15px 0; gap: 15px 0;
height: 100%; height: 100%;
width: 100%; width: 100%;
.character-sidebar-sheet { .character-sidebar-sheet {
grid-row: 1 / span 2; grid-row: 1 / span 2;
grid-column: 1; grid-column: 1;
} }
.character-header-sheet { .character-header-sheet {
grid-row: 1; grid-row: 1;
grid-column: 2; grid-column: 2;
} }
.tab { .tab {
grid-row: 2; grid-row: 2;
grid-column: 2; grid-column: 2;
} }
.old-sheet { .old-sheet {
width: 500px; width: 500px;
} }
} }
} }

View file

@ -1,310 +1,310 @@
@import '../../utils/colors.less'; @import '../../utils/colors.less';
@import '../../utils/fonts.less'; @import '../../utils/fonts.less';
.application.sheet.daggerheart.actor.dh-style.character { .application.sheet.daggerheart.actor.dh-style.character {
.character-sidebar-sheet { .character-sidebar-sheet {
width: 275px; width: 275px;
min-width: 275px; min-width: 275px;
border-right: 1px solid light-dark(@dark-blue, @golden); border-right: 1px solid light-dark(@dark-blue, @golden);
background-image: url('../assets/parchments/dh-parchment-dark.png'); background-image: url('../assets/parchments/dh-parchment-dark.png');
.theme-light & { .theme-light & {
background: transparent; background: transparent;
} }
.portrait { .portrait {
position: relative; position: relative;
height: 235px; height: 235px;
width: 275px; width: 275px;
border-bottom: 1px solid light-dark(@dark-blue, @golden); border-bottom: 1px solid light-dark(@dark-blue, @golden);
cursor: pointer; cursor: pointer;
img { img {
height: 235px; height: 235px;
width: 275px; width: 275px;
object-fit: cover; object-fit: cover;
} }
.death-roll-btn { .death-roll-btn {
display: none; display: none;
} }
&.death-roll { &.death-roll {
filter: grayscale(1); filter: grayscale(1);
.death-roll-btn { .death-roll-btn {
display: flex; display: flex;
position: absolute; position: absolute;
top: 30%; top: 30%;
right: 30%; right: 30%;
font-size: 6rem; font-size: 6rem;
color: @beige; color: @beige;
&:hover { &:hover {
text-shadow: 0 0 8px @beige; text-shadow: 0 0 8px @beige;
} }
} }
} }
} }
.info-section { .info-section {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
top: -20px; top: -20px;
gap: 30px; gap: 30px;
margin-bottom: -10px; margin-bottom: -10px;
.resources-section { .resources-section {
display: flex; display: flex;
justify-content: space-evenly; justify-content: space-evenly;
.status-bar { .status-bar {
position: relative; position: relative;
width: 100px; width: 100px;
height: 40px; height: 40px;
justify-items: center; justify-items: center;
.status-label { .status-label {
position: relative; position: relative;
top: 40px; top: 40px;
height: 22px; height: 22px;
width: 79px; width: 79px;
clip-path: path('M0 0H79L74 16.5L39 22L4 16.5L0 0Z'); clip-path: path('M0 0H79L74 16.5L39 22L4 16.5L0 0Z');
background: light-dark(@dark-blue, @golden); background: light-dark(@dark-blue, @golden);
h4 { h4 {
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
line-height: 18px; line-height: 18px;
color: light-dark(@beige, @dark-blue); color: light-dark(@beige, @dark-blue);
} }
} }
.status-value { .status-value {
position: absolute; position: absolute;
display: flex; display: flex;
padding: 0 6px; padding: 0 6px;
font-size: 1.5rem; font-size: 1.5rem;
align-items: center; align-items: center;
width: 100px; width: 100px;
height: 40px; height: 40px;
justify-content: center; justify-content: center;
text-align: center; text-align: center;
z-index: 2; z-index: 2;
color: @beige; color: @beige;
input[type='number'] { input[type='number'] {
background: transparent; background: transparent;
font-size: 1.5rem; font-size: 1.5rem;
width: 40px; width: 40px;
height: 30px; height: 30px;
text-align: center; text-align: center;
border: none; border: none;
outline: 2px solid transparent; outline: 2px solid transparent;
color: @beige; color: @beige;
&.bar-input { &.bar-input {
padding: 0; padding: 0;
color: @beige; color: @beige;
backdrop-filter: none; backdrop-filter: none;
background: transparent; background: transparent;
transition: all 0.3s ease; transition: all 0.3s ease;
&:hover, &:hover,
&:focus { &:focus {
background: @semi-transparent-dark-blue; background: @semi-transparent-dark-blue;
backdrop-filter: blur(9.5px); backdrop-filter: blur(9.5px);
} }
} }
} }
.bar-label { .bar-label {
width: 40px; width: 40px;
} }
} }
.progress-bar { .progress-bar {
position: absolute; position: absolute;
appearance: none; appearance: none;
width: 100px; width: 100px;
height: 40px; height: 40px;
border: 1px solid light-dark(@dark-blue, @golden); border: 1px solid light-dark(@dark-blue, @golden);
border-radius: 6px; border-radius: 6px;
z-index: 1; z-index: 1;
&::-webkit-progress-bar { &::-webkit-progress-bar {
border: none; border: none;
background: @dark-blue; background: @dark-blue;
border-radius: 6px; border-radius: 6px;
} }
&::-webkit-progress-value { &::-webkit-progress-value {
background: @gradient-hp; background: @gradient-hp;
border-radius: 6px; border-radius: 6px;
} }
&.stress-color::-webkit-progress-value { &.stress-color::-webkit-progress-value {
background: @gradient-stress; background: @gradient-stress;
border-radius: 6px; border-radius: 6px;
} }
&::-moz-progress-value, &::-moz-progress-value,
&::-moz-progress-bar { &::-moz-progress-bar {
border-radius: 6px; border-radius: 6px;
} }
&::-moz-progress-bar { &::-moz-progress-bar {
background: @gradient-hp; background: @gradient-hp;
} }
&.stress-color::-moz-progress-bar { &.stress-color::-moz-progress-bar {
background: @gradient-stress; background: @gradient-stress;
border-radius: 6px; border-radius: 6px;
} }
} }
} }
} }
.status-section { .status-section {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 5px; gap: 5px;
justify-content: center; justify-content: center;
.status-number { .status-number {
justify-items: center; justify-items: center;
.status-value { .status-value {
position: relative; position: relative;
display: flex; display: flex;
width: 50px; width: 50px;
height: 30px; height: 30px;
border: 1px solid light-dark(@dark-blue, @golden); border: 1px solid light-dark(@dark-blue, @golden);
border-bottom: none; border-bottom: none;
border-radius: 6px 6px 0 0; border-radius: 6px 6px 0 0;
padding: 0 6px; padding: 0 6px;
font-size: 1.2rem; font-size: 1.2rem;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: light-dark(transparent, @dark-blue); background: light-dark(transparent, @dark-blue);
z-index: 2; z-index: 2;
&.armor-slots { &.armor-slots {
width: 80px; width: 80px;
height: 30px; height: 30px;
} }
} }
.status-label { .status-label {
padding: 2px 10px; padding: 2px 10px;
width: 100%; width: 100%;
border-radius: 3px; border-radius: 3px;
background: light-dark(@dark-blue, @golden); background: light-dark(@dark-blue, @golden);
h4 { h4 {
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
line-height: 18px; line-height: 18px;
font-size: 12px; font-size: 12px;
color: light-dark(@beige, @dark-blue); color: light-dark(@beige, @dark-blue);
} }
} }
} }
} }
} }
.items-sidebar-list { .items-sidebar-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 5px; gap: 5px;
.inventory-item { .inventory-item {
padding: 0 10px; padding: 0 10px;
} }
} }
.equipment-section { .equipment-section {
.title { .title {
display: flex; display: flex;
gap: 15px; gap: 15px;
align-items: center; align-items: center;
h3 { h3 {
font-size: 20px; font-size: 20px;
} }
} }
.items-list { .items-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
align-items: center; align-items: center;
} }
} }
.loadout-section { .loadout-section {
.title { .title {
display: flex; display: flex;
gap: 15px; gap: 15px;
align-items: center; align-items: center;
h3 { h3 {
font-size: 20px; font-size: 20px;
} }
} }
} }
.experience-section { .experience-section {
.title { .title {
display: flex; display: flex;
gap: 15px; gap: 15px;
align-items: center; align-items: center;
h3 { h3 {
font-size: 20px; font-size: 20px;
} }
} }
.experience-list { .experience-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 5px; gap: 5px;
width: 100%; width: 100%;
margin-top: 10px; margin-top: 10px;
align-items: center; align-items: center;
.experience-row { .experience-row {
display: flex; display: flex;
gap: 5px; gap: 5px;
width: 250px; width: 250px;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
input[type='text'] { input[type='text'] {
height: 32px; height: 32px;
width: 180px; width: 180px;
border: 1px solid transparent; border: 1px solid transparent;
outline: 2px solid transparent; outline: 2px solid transparent;
font-size: 14px; font-size: 14px;
font-family: @font-body; font-family: @font-body;
transition: all 0.3s ease; transition: all 0.3s ease;
color: light-dark(@dark, @beige); color: light-dark(@dark, @beige);
&:hover { &:hover {
outline: 2px solid light-dark(@dark, @beige); outline: 2px solid light-dark(@dark, @beige);
} }
} }
} }
.experience-value { .experience-value {
height: 25px; height: 25px;
width: 35px; width: 35px;
font-size: 14px; font-size: 14px;
font-family: @font-body; font-family: @font-body;
color: light-dark(@dark, @beige); color: light-dark(@dark, @beige);
align-content: center; align-content: center;
text-align: center; text-align: center;
background: url(../assets/svg/experience-shield.svg) no-repeat; background: url(../assets/svg/experience-shield.svg) no-repeat;
.theme-light & { .theme-light & {
background: url('../assets/svg/experience-shield-light.svg') no-repeat; background: url('../assets/svg/experience-shield-light.svg') no-repeat;
} }
} }
} }
} }
} }
} }

View file

@ -1,17 +1,17 @@
@import '../utils/colors.less'; @import '../utils/colors.less';
@import '../utils/fonts.less'; @import '../utils/fonts.less';
.application.sheet.daggerheart.actor.dh-style.character { .application.sheet.daggerheart.actor.dh-style.character {
.items-list { .items-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
align-items: center; align-items: center;
} }
.card-list { .card-list {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 10px; gap: 10px;
align-items: center; align-items: center;
} }
} }

View file

@ -1,133 +1,133 @@
@import '../utils/colors.less'; @import '../utils/colors.less';
@import '../utils/fonts.less'; @import '../utils/fonts.less';
.application.sheet.daggerheart.actor.dh-style.character { .application.sheet.daggerheart.actor.dh-style.character {
.inventory-item { .inventory-item {
display: grid; display: grid;
grid-template-columns: 40px 1fr 60px; grid-template-columns: 40px 1fr 60px;
gap: 10px; gap: 10px;
width: 100%; width: 100%;
.item-img { .item-img {
height: 40px; height: 40px;
width: 40px; width: 40px;
border-radius: 3px; border-radius: 3px;
border: none; border: none;
cursor: pointer; cursor: pointer;
object-fit: cover; object-fit: cover;
} }
.item-label { .item-label {
font-family: @font-body; font-family: @font-body;
align-self: center; align-self: center;
.item-name { .item-name {
font-size: 14px; font-size: 14px;
} }
.item-tags, .item-tags,
.item-labels { .item-labels {
display: flex; display: flex;
gap: 10px; gap: 10px;
.tag { .tag {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 3px 5px; padding: 3px 5px;
font-size: 12px; font-size: 12px;
background: light-dark(@dark-15, @beige-15); background: light-dark(@dark-15, @beige-15);
border: 1px solid light-dark(@dark, @beige); border: 1px solid light-dark(@dark, @beige);
border-radius: 3px; border-radius: 3px;
} }
.label { .label {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
font-size: 12px; font-size: 12px;
} }
} }
} }
.controls { .controls {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: end; justify-content: end;
gap: 8px; gap: 8px;
a { a {
text-align: center; text-align: center;
&.unequipped { &.unequipped {
opacity: 0.4; opacity: 0.4;
} }
} }
} }
} }
.card-item { .card-item {
position: relative; position: relative;
height: 120px; height: 120px;
width: 100px; width: 100px;
border: 1px solid light-dark(@dark-blue, @golden); border: 1px solid light-dark(@dark-blue, @golden);
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
.card-label { .card-label {
padding-top: 15px; padding-top: 15px;
.controls { .controls {
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
transition: all 0.3s ease; transition: all 0.3s ease;
max-height: 16px; max-height: 16px;
} }
} }
} }
.card-img { .card-img {
height: 100%; height: 100%;
width: 100%; width: 100%;
object-fit: cover; object-fit: cover;
} }
.card-label { .card-label {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: fit-content; height: fit-content;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
padding-top: 5px; padding-top: 5px;
padding-bottom: 5px; padding-bottom: 5px;
width: 100%; width: 100%;
position: absolute; position: absolute;
background-color: @dark-blue; background-color: @dark-blue;
bottom: 0; bottom: 0;
mask-image: linear-gradient(180deg, transparent 0%, black 20%); mask-image: linear-gradient(180deg, transparent 0%, black 20%);
.card-name { .card-name {
font-family: @font-body; font-family: @font-body;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-size: 12px; font-size: 12px;
line-height: 15px; line-height: 15px;
color: @beige; color: @beige;
} }
.controls { .controls {
display: flex; display: flex;
gap: 15px; gap: 15px;
align-items: center; align-items: center;
max-height: 0px; max-height: 0px;
opacity: 0; opacity: 0;
visibility: collapse; visibility: collapse;
transition: all 0.3s ease; transition: all 0.3s ease;
color: @beige; color: @beige;
} }
} }
} }
} }

View file

@ -1,18 +1,18 @@
@import '../utils/colors.less'; @import '../utils/colors.less';
@import '../utils/fonts.less'; @import '../utils/fonts.less';
.sheet.daggerheart.dh-style { .sheet.daggerheart.dh-style {
.tab-navigation { .tab-navigation {
margin: 5px 0; margin: 5px 0;
height: 40px; height: 40px;
.feature-tab { .feature-tab {
border: none; border: none;
a { a {
color: light-dark(@dark-blue, @golden); color: light-dark(@dark-blue, @golden);
font-family: @font-body; font-family: @font-body;
} }
} }
} }
} }

View file

@ -35,6 +35,9 @@
}, },
{ {
"name": "JimCanE" "name": "JimCanE"
},
{
"name": "Po0lp"
} }
], ],
"scripts": ["build/daggerheart.js"], "scripts": ["build/daggerheart.js"],
@ -252,7 +255,8 @@
"dualityRoll": {}, "dualityRoll": {},
"adversaryRoll": {}, "adversaryRoll": {},
"damageRoll": {}, "damageRoll": {},
"abilityUse": {} "abilityUse": {},
"applyEffect": {}
} }
}, },
"primaryTokenAttribute": "resources.health", "primaryTokenAttribute": "resources.health",

View file

@ -41,7 +41,7 @@
</div> </div>
{{/if}} {{/if}}
<div class="flexrow"> <div class="flexrow">
<button class="duality-action" data-value="{{roll.total}}" data-damage="{{damage.value}}" data-damage-type="{{damage.type}}"{{#if damage.disabled}} disabled{{/if}}><span>Roll Damage</span></button> <button class="duality-action" data-value="{{roll.total}}"><span>Roll Damage</span></button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,33 +1,43 @@
<div class="dice-roll daggerheart chat roll" data-action="expandRoll"> <div class="dice-roll daggerheart chat roll" data-action="expandRoll">
<div class="dice-flavor">{{title}}</div>
<div class="dice-result"> <div class="dice-result">
<div class="dice-formula">{{roll.formula}}</div> <div class="dice-formula">{{roll.formula}}</div>
<div class="dice-tooltip"> <div class="dice-tooltip">
<ol class="dice-rolls"> <div class="wrapper">
<div class="dice-hope-container"> <section class="tooltip-part">
{{#each dice}} <div class="dice">
{{#each roll.dice}}
<header class="part-header flexrow"> <header class="part-header flexrow">
<span class="part-formula">{{number}}{{denomination}}</span> <span class="part-formula">{{formula}}</span>
<span class="part-total">{{total}}</span> <span class="part-total">{{total}}</span>
</header> </header>
<div class="flexrow"> <div class="flexrow">
<ol class="dice-rolls"> <ol class="dice-rolls">
{{#each results}} {{#each results}}
<li class="roll die {{../denomination}}{{#if discarded}} discarded{{/if}} min">{{result}}</li> <li class="roll die {{../dice}}{{#if discarded}} discarded{{/if}} min">{{result}}</li>
{{/each}} {{/each}}
</ol> </ol>
<div class="attack-roll-advantage-container">{{#if ../advantageState}}{{localize "DAGGERHEART.General.Advantage.Full"}}{{/if}}{{#if (eq ../advantageState false)}}{{localize "DAGGERHEART.General.Disadvantage.Full"}}{{/if}}</div> <div class="attack-roll-advantage-container">
{{#if (eq ../roll.advantage.type 1)}}{{localize "DAGGERHEART.General.Advantage.Full"}}{{/if}}{{#if (eq ../roll.advantage.type -1)}}{{localize "DAGGERHEART.General.Disadvantage.Full"}}{{/if}}
</div>
</div> </div>
{{/each}} {{/each}}
</div> </div>
<div class="modifiers-container"> </section>
{{#each modifiers}} </div>
<li class="modifier-value" data-value="{{value}}" title="{{title}}">{{label}}</li>
{{/each}}
</div>
</ol>
</div> </div>
<div class="dice-total"> <div class="dice-total">
<div class="dice-total-value">{{roll.total}}</div> <div class="dice-total-value">{{roll.total}}</div>
</div> </div>
</div> </div>
</div> </div>
{{> 'systems/daggerheart/templates/chat/parts/target-chat.hbs'}}
{{#if hasDamage}}
<div class="dice-roll daggerheart chat roll">
<div class="dice-result">
<div class="flexrow">
<button class="duality-action duality-action-damage" data-value="{{roll.total}}"><span>Roll Damage</span></button>
</div>
</div>
</div>
{{/if}}

View file

@ -0,0 +1,9 @@
<div class="dice-roll daggerheart chat roll" data-action="expandRoll">
<div class="dice-flavor">{{title}}</div>
<div>{{{description}}}</div>
<div class="dice-result">
<div class="dice-actions">
<button class="duality-action-effect">{{localize "DAGGERHEART.Chat.AttackRoll.ApplyEffect"}}</button>
</div>
</div>
</div>

View file

@ -1,21 +1,19 @@
<div class="dice-roll daggerheart chat roll" data-action="expandRoll"> <div class="dice-roll daggerheart chat roll" data-action="expandRoll">
<div class="dice-flavor">{{title}}</div> <div class="dice-flavor">{{title}}</div>
<div class="dice-result"> <div class="dice-result">
<div class="dice-formula">{{roll}}</div> <div class="dice-formula">{{roll.formula}}</div>
<div class="dice-tooltip"> <div class="dice-tooltip">
<div class="wrapper"> <div class="wrapper">
<section class="tooltip-part"> <section class="tooltip-part">
{{#each dice}} {{#each roll.dice}}
<div class="dice"> <div class="dice">
<header class="part-header flexrow"> <header class="part-header flexrow">
<span class="part-formula">{{rolls.length}}{{type}}</span> <span class="part-formula">{{formula}}</span>
<span class="part-total">{{total}}</span>
<span class="part-total">{{this.total}}</span>
</header> </header>
<ol class="dice-rolls"> <ol class="dice-rolls">
{{#each rolls}} {{#each results}}
<li class="roll die {{../type}} min">{{this}}</li> <li class="roll die {{../denomination}} min">{{result}}</li>
{{/each}} {{/each}}
</ol> </ol>
</div> </div>
@ -23,7 +21,12 @@
</section> </section>
</div> </div>
</div> </div>
<div class="dice-total">{{damage.total}}</div> <div class="dice-total">{{roll.total}}</div>
</div>
</div>
{{> 'systems/daggerheart/templates/chat/parts/target-chat.hbs'}}
<div class="dice-roll daggerheart chat roll">
<div class="dice-result">
<div class="dice-actions"> <div class="dice-actions">
<button class="damage-button" data-target-hit="true" {{#if (eq targets.length 0)}}disabled{{/if}}>{{localize "DAGGERHEART.Chat.DamageRoll.DealDamageToTargets"}}</button> <button class="damage-button" data-target-hit="true" {{#if (eq targets.length 0)}}disabled{{/if}}>{{localize "DAGGERHEART.Chat.DamageRoll.DealDamageToTargets"}}</button>
<button class="damage-button">{{localize "DAGGERHEART.Chat.DamageRoll.DealDamage"}}</button> <button class="damage-button">{{localize "DAGGERHEART.Chat.DamageRoll.DealDamage"}}</button>

View file

@ -1,17 +1,17 @@
<div class="dice-roll daggerheart chat roll" data-action="expandRoll"> <div class="dice-roll daggerheart chat roll" data-action="expandRoll">
<div class="dice-flavor">{{title}}</div> <div class="dice-flavor">{{title}}</div>
<div class="duality-modifiers"> <div class="duality-modifiers">
{{#each modifiers}} {{#each roll.modifiers}}
<div class="duality-modifier"> <div class="duality-modifier">
{{label}} {{localize label}} {{#if (gte value 0)}}+{{/if}}{{value}}
</div> </div>
{{/each}} {{/each}}
{{#if advantageState}} {{#if (eq roll.advantage.type 1)}}
<div class="duality-modifier"> <div class="duality-modifier">
{{localize "DAGGERHEART.General.Advantage.Full"}} {{localize "DAGGERHEART.General.Advantage.Full"}}
</div> </div>
{{/if}} {{/if}}
{{#if (eq advantageState false)}} {{#if (eq roll.advantage.type -1)}}
<div class="duality-modifier"> <div class="duality-modifier">
{{localize "DAGGERHEART.General.Disadvantage.Full"}} {{localize "DAGGERHEART.General.Disadvantage.Full"}}
</div> </div>
@ -25,56 +25,56 @@
<div class="dice"> <div class="dice">
<header class="part-header flexrow"> <header class="part-header flexrow">
<span class="part-formula"> <span class="part-formula">
<span>1{{hope.dice}}</span> <span>1{{roll.hope.dice}}</span>
| |
<span>1{{fear.dice}}</span> <span>1{{roll.fear.dice}}</span>
</span> </span>
<span class="part-total">{{diceTotal}}</span> <span class="part-total">{{roll.result.total}}</span>
</header> </header>
<div class="flexrow"> <div class="flexrow">
<ol class="dice-rolls duality"> <ol class="dice-rolls duality">
<li class="roll die {{hope.dice}}" title="{{localize "DAGGERHEART.General.Hope"}}"> <li class="roll die {{roll.hope.dice}}" title="{{localize "DAGGERHEART.General.Hope"}}">
<div class="dice-container"> <div class="dice-container">
<div class="dice-title">{{localize "DAGGERHEART.General.Hope"}}</div> <div class="dice-title">{{localize "DAGGERHEART.General.Hope"}}</div>
<div class="dice-inner-container hope" title="{{localize "DAGGERHEART.General.Hope"}}"> <div class="dice-inner-container hope" title="{{localize "DAGGERHEART.General.Hope"}}">
<div class="dice-wrapper"> <div class="dice-wrapper">
<img class="dice" src="../icons/svg/d12-grey.svg"/> <img class="dice" src="../icons/svg/d12-grey.svg"/>
</div> </div>
<div class="dice-value">{{hope.value}}</div> <div class="dice-value">{{roll.hope.value}}</div>
</div> </div>
</div> </div>
</li> </li>
<li class="roll die {{fear.dice}}" title="{{localize "DAGGERHEART.General.Fear"}}"> <li class="roll die {{roll.fear.dice}}" title="{{localize "DAGGERHEART.General.Fear"}}">
<div class="dice-container"> <div class="dice-container">
<div class="dice-title">{{localize "DAGGERHEART.General.Fear"}}</div> <div class="dice-title">{{localize "DAGGERHEART.General.Fear"}}</div>
<div class="dice-inner-container fear" title="{{localize "DAGGERHEART.General.Fear"}}"> <div class="dice-inner-container fear" title="{{localize "DAGGERHEART.General.Fear"}}">
<div class="dice-wrapper"> <div class="dice-wrapper">
<img class="dice" src="../icons/svg/d12-grey.svg"/> <img class="dice" src="../icons/svg/d12-grey.svg"/>
</div> </div>
<div class="dice-value">{{fear.value}}</div> <div class="dice-value">{{roll.fear.value}}</div>
</div> </div>
</div> </div>
</li> </li>
</ol> </ol>
</div> </div>
</div> </div>
{{#if advantageState}} {{#if roll.advantage.type}}
<div class="dice"> <div class="dice">
<header class="part-header flexrow"> <header class="part-header flexrow">
<span class="part-formula"> <span class="part-formula">
<span>1{{advantage.dice}}</span> <span>1{{roll.advantage.dice}}</span>
</span> </span>
<span class="part-total">{{advantage.value}}</span> <span class="part-total">{{roll.advantage.value}}</span>
</header> </header>
<div class="flexrow"> <div class="flexrow">
<ol class="dice-rolls"> <ol class="dice-rolls">
<li class="roll die {{advantage.dice}}"> <li class="roll die {{roll.advantage.dice}}">
<div class="dice-container"> <div class="dice-container">
<div class="dice-inner-container advantage"> <div class="dice-inner-container {{#if (eq roll.advantage.type 1)}}advantage{{else}}disadvantage{{/if}}">
<div class="dice-wrapper"> <div class="dice-wrapper">
<img class="dice" src="../icons/svg/d6-grey.svg"/> <img class="dice" src="../icons/svg/d6-grey.svg"/>
</div> </div>
<div class="dice-value">{{advantage.value}}</div> <div class="dice-value">{{roll.advantage.value}}</div>
</div> </div>
</div> </div>
</li> </li>
@ -82,58 +82,34 @@
</div> </div>
</div> </div>
{{/if}} {{/if}}
{{#if (eq advantageState false)}} {{#if roll.modifierTotal}}<div class="duality-modifier">{{#if (gt roll.modifierTotal 0)}}+{{/if}}{{roll.modifierTotal}}</div>{{/if}}
<div class="dice">
<header class="part-header flexrow">
<span class="part-formula">
<span>1{{advantage.dice}}</span>
</span>
<span class="part-total">{{advantage.value}}</span>
</header>
<div class="flexrow">
<ol class="dice-rolls">
<li class="roll die {{advantage.dice}}">
<div class="dice-container">
<div class="dice-inner-container disadvantage">
<div class="dice-wrapper">
<img class="dice" src="../icons/svg/d6-grey.svg"/>
</div>
<div class="dice-value">{{advantage.value}}</div>
</div>
</div>
</li>
</ol>
</div>
</div>
{{/if}}
{{#if modifierTotal.value}}<div class="duality-modifier">{{modifierTotal.label}}</div>{{/if}}
</section> </section>
</div> </div>
</div> </div>
<div class="dice-total duality {{#if fear.discarded}}hope{{else}}{{#if hope.discarded}}fear{{else}}critical{{/if}}{{/if}}"> <div class="dice-total duality {{#if (eq roll.result.duality 1)}}hope{{else}}{{#if (eq roll.result.duality -1)}}fear{{else}}critical{{/if}}{{/if}}">
<div class="dice-total-label">{{totalLabel}}</div> <div class="dice-total-label">{{roll.result.label}}</div>
<div class="dice-total-value"> <div class="dice-total-value">
{{roll.total}} {{roll.total}}
</div> </div>
</div> </div>
{{#if (gt targets.length 0)}} </div>
<div class="target-section"> </div>
{{#each targets as |target|}} {{> 'systems/daggerheart/templates/chat/parts/target-chat.hbs'}}
<div class="dice-total target-container {{#if target.hit}}hit{{else}}{{#if (not ../total.alternate)}}miss{{/if}}{{/if}}" data-token="{{target.id}}"> <div class="dice-roll daggerheart chat roll">
<img src="{{target.img}}" /> <div class="dice-result">
<div class="target-inner-container"> <div class="dice-actions{{#unless (or hasDamage hasHealing)}} duality-alone{{/unless}}">
{{#if target.hit}}{{localize "Hit"}}{{else}}{{#if (not ../total.alternate)}}{{localize "Miss"}}{{else}}?{{/if}}{{/if}} {{#if hasDamage}}
</div> <button class="duality-action duality-action-damage" data-value="{{roll.total}}"><span>{{localize "DAGGERHEART.Chat.AttackRoll.RollDamage"}}</span></button>
</div> {{else}}
{{/each}} {{#if hasHealing}}
</div> <button class="duality-action duality-action-healing" data-value="{{roll.total}}"><span>{{localize "DAGGERHEART.Chat.AttackRoll.RollHealing"}}</span></button>
{{/if}} {{/if}}
<div class="dice-actions{{#unless damage.value}} duality-alone{{/unless}}"> {{/if}}
{{#if damage.value}} {{#if hasEffect}}
<button class="duality-action" data-value="{{roll.total}}" data-damage="{{damage.value}}" data-damage-type="{{damage.type}}" {{#if damage.disabled}}disabled{{/if}}><span>{{localize "DAGGERHEART.Chat.AttackRoll.RollDamage"}}</span></button> <button class="duality-action{{#if (or hasDamage hasHealing)}} duality-action-effect{{/if}}" data-value="{{roll.total}}"><span>{{localize "DAGGERHEART.Chat.AttackRoll.ApplyEffect"}}</span></button>
{{/if}} {{/if}}
<div class="duality-result"> <div class="duality-result">
<div>{{roll.total}} {{#if (eq dualityResult 1)}}With Hope{{else}}{{#if (eq dualityResult 2)}}With Fear{{else}}Critical Success{{/if}}{{/if}}</div> <div>{{roll.total}} {{#if (eq roll.result.duality 1)}}With Hope{{else}}{{#if (eq roll.result.duality -1)}}With Fear{{else}}Critical Success{{/if}}{{/if}}</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,19 +1,29 @@
<div class="dice-roll daggerheart chat roll"> <div class="dice-roll daggerheart chat roll" data-action="expandRoll">
<div class="dice-flavor">{{title}}</div>
<div class="dice-result"> <div class="dice-result">
<div class="dice-formula">{{this.roll}}</div> <div class="dice-formula">{{roll.formula}}</div>
<div class="dice-tooltip"> <div class="dice-tooltip">
<ol class="dice-rolls"> <div class="wrapper">
{{#each dice}} <section class="tooltip-part">
<li class="roll die {{this.type}}">{{this.value}}</li> {{#each roll.dice}}
{{/each}} <div class="dice">
{{#each modifiers}} <header class="part-header flexrow">
<li class="modifier-value">{{this}}</li> <span class="part-formula">{{formula}}</span>
{{/each}} <span class="part-total">{{total}}</span>
</ol> </header>
<ol class="dice-rolls">
{{#each results}}
<li class="roll die {{../denomination}} min">{{result}}</li>
{{/each}}
</ol>
</div>
{{/each}}
</section>
</div>
</div> </div>
<div class="dice-total">{{this.total}}</div> <div class="dice-total">{{roll.total}}</div>
<div class="flexrow"> <div class="flexrow">
<button class="healing-button" data-value="{{this.total}}" data-type="{{this.type}}"><span>{{localize "DAGGERHEART.Chat.HealingRoll.Heal"}}</span></button> <button class="healing-button"><span>{{localize "DAGGERHEART.Chat.HealingRoll.Heal"}}</span></button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,32 @@
{{#if (gt targets.length 0)}}
<fieldset class="dice-roll daggerheart chat roll expanded" data-action="expandRoll">
<legend class="dice-flavor">Targets</legend>
<div class="dice-result">
<div class="dice-tooltip">
<div class="wrapper">
<div class="target-section">
{{#each targets as |target|}}
<div class="dice-total target-container {{#if target.hit}}hit{{else}}{{#if (not ../total.alternate)}}miss{{/if}}{{/if}}" data-token="{{target.id}}">
<img src="{{target.img}}" />
<div class="target-inner-container">
{{#if ../directDamage}}
<div data-perm-id="{{target.actorId}}"><span>{{target.name}}</span></div>
{{else}}
{{#if target.hit}}{{localize "Hit"}}{{else}}{{#if (not ../total.alternate)}}{{localize "Miss"}}{{else}}?{{/if}}{{/if}}
{{/if}}
</div>
{{#if ../hasSave}}
<button class="target-save-container{{#if target.saved.result includeZero=true}} is-rolled{{/if}}"{{#unless target.hit}} style="visibility: hidden;"{{/unless}} data-perm-id="{{target.actorId}}">
{{!-- {{target.saved.result}} --}}
<i class="fa-solid {{#if target.saved.result includeZero=true}}{{#if target.saved.success}}fa-check{{else}}fa-xmark{{/if}}{{else}}fa-shield{{/if}}">
</i>
</button>
{{/if}}
</div>
{{/each}}
</div>
</div>
</div>
</div>
</fieldset>
{{/if}}

View file

@ -49,13 +49,32 @@
<fieldset class="two-columns even"> <fieldset class="two-columns even">
<legend>{{localize "DAGGERHEART.Sheets.Adversary.Attack"}}</legend> <legend>{{localize "DAGGERHEART.Sheets.Adversary.Attack"}}</legend>
<button data-action="attackConfigure">Configure</button>
{{formGroup systemFields.attack.fields.name value=source.system.attack.name}}
<button data-action="attackRoll">Attack</button> <button data-action="attackRoll">Attack</button>
{{formGroup systemFields.attack.fields.modifier value=source.system.attack.modifier}} <fieldset class="action-category" style="grid-column: 1 / -1;">
{{formGroup systemFields.attack.fields.range value=source.system.attack.range localize=true}} <legend class="action-category-label" data-action="toggleSection" data-section="range">
{{formGroup systemFields.attack.fields.damage.fields.value value=source.system.attack.damage.value}} <div>Name</div>
{{formGroup systemFields.attack.fields.damage.fields.type value=source.system.attack.damage.type localize=true}} </legend>
<div class="action-category-data open">
{{formGroup systemFields.attack.fields.name value=source.system.attack.name name="system.attack.name"}}
{{formGroup systemFields.attack.fields.img value=source.img label="Icon" name="system.attack.img"}}
</div>
</fieldset>
<div>
<fieldset class="action-category">
<legend class="action-category-label" data-action="toggleSection" data-section="range">
<div>Bonus to Hit</div>
</legend>
<div class="action-category-data open">
{{formField systemFields.attack.fields.roll.fields.bonus value=source.system.attack.roll.bonus name="system.attack.roll.bonus"}}
</div>
</fieldset>
{{> 'systems/daggerheart/templates/views/actionTypes/range-target.hbs' fields=(object range=systemFields.attack.fields.range target=systemFields.attack.fields.target.fields) source=(object target=source.system.attack.target range=source.system.attack.range) path="system.attack."}}
</div>
{{> 'systems/daggerheart/templates/views/actionTypes/damage.hbs' fields=systemFields.attack.fields.damage.fields.parts.element.fields source=source.system.attack.damage path="system.attack."}}
<div style="grid-column: 1 / -1;">
{{> 'systems/daggerheart/templates/views/actionTypes/effect.hbs' fields=systemFields.attack.fields.effects.element.fields source=source.system.attack.effects}}
</div>
</fieldset> </fieldset>
</div> </div>
</section> </section>

View file

@ -75,8 +75,8 @@
<div class="hope-section"> <div class="hope-section">
<h4>{{localize "DAGGERHEART.General.Hope"}}</h4> <h4>{{localize "DAGGERHEART.General.Hope"}}</h4>
{{#times document.system.resources.hope.max}} {{#times document.system.resources.hope.max}}
<span class='hope-value' data-action='toggleHope' data-value="{{this}}"> <span class='hope-value' data-action='toggleHope' data-value="{{add this 1}}">
{{#if (gte ../document.system.resources.hope.value this)}} {{#if (gte ../document.system.resources.hope.value (add this 1))}}
<i class='fa-solid fa-diamond'></i> <i class='fa-solid fa-diamond'></i>
{{else}} {{else}}
<i class='fa-regular fa-circle'></i> <i class='fa-regular fa-circle'></i>

View file

@ -13,7 +13,7 @@
<li class="item inventory-item"> <li class="item inventory-item">
<div class="inventory-row" data-item-id="{{item.uuid}}"> <div class="inventory-row" data-item-id="{{item.uuid}}">
<div class="inventory-item-title-container"> <div class="inventory-item-title-container">
<div data-action="viewObject" data-value="{{item.uuid}}" class="inventory-item-title"> <div data-action="useItem" data-value="{{item.uuid}}" class="inventory-item-title">
<img src="{{item.img}}" /> <img src="{{item.img}}" />
{{item.name}} {{item.name}}
</div> </div>

View file

@ -1,5 +1,5 @@
<li class="card-item"> <li class="card-item" data-item-id="{{item.id}}">
<img src="{{item.img}}" data-action="viewObject" data-uuid="{{item.uuid}}" class="card-img" /> <img src="{{item.img}}" data-action="useItem" class="card-img" />
<div class="card-label"> <div class="card-label">
<div class="controls"> <div class="controls">
{{#if (eq type 'weapon')}} {{#if (eq type 'weapon')}}
@ -14,18 +14,18 @@
{{/if}} {{/if}}
{{#if (eq type 'domainCard')}} {{#if (eq type 'domainCard')}}
{{#unless item.system.inVault}} {{#unless item.system.inVault}}
<a data-action="toggleVault" id="{{item.id}}" data-tooltip="{{localize 'DAGGERHEART.Tooltip.sendToVault'}}"> <a data-action="toggleVault" data-tooltip="{{localize 'DAGGERHEART.Tooltip.sendToVault'}}">
<i class="fa-solid fa-arrow-down"></i> <i class="fa-solid fa-arrow-down"></i>
</a> </a>
{{else}} {{else}}
<a data-action="toggleVault" id="{{item.id}}" data-tooltip="{{localize 'DAGGERHEART.Tooltip.sendToLoadout'}}"> <a data-action="toggleVault" data-tooltip="{{localize 'DAGGERHEART.Tooltip.sendToLoadout'}}">
<i class="fa-solid fa-arrow-up"></i> <i class="fa-solid fa-arrow-up"></i>
</a> </a>
{{/unless}} {{/unless}}
{{/if}} {{/if}}
<a data-action="toChat" data-uuid="{{item.uuid}}" data-tooltip="{{localize 'DAGGERHEART.Tooltip.sendToChat'}}"><i class="fa-regular fa-message"></i></a> <a data-action="toChat" data-tooltip="{{localize 'DAGGERHEART.Tooltip.sendToChat'}}"><i class="fa-regular fa-message"></i></a>
<a class="{{concat type "-context-menu"}}" data-type="{{type}}" data-uuid="{{item.uuid}}" id="{{item.id}}" data-tooltip="{{localize 'DAGGERHEART.Tooltip.moreOptions'}}"><i class="fa-solid fa-ellipsis-vertical"></i></a> <a data-action="triggerContextMenu" data-tooltip="{{localize 'DAGGERHEART.Tooltip.moreOptions'}}"><i class="fa-solid fa-ellipsis-vertical"></i></a>
</div> </div>
<div class="card-name">{{item.name}}</div> <div class="card-name">{{item.name}}</div>
</div> </div>

View file

@ -1,5 +1,5 @@
<li class="inventory-item"> <li class="inventory-item" data-item-id="{{item.id}}">
<img src="{{item.img}}" data-action="viewObject" data-uuid="{{item.uuid}}" class="item-img" /> <img src="{{item.img}}" class="item-img" data-action="useItem"/>
<div class="item-label"> <div class="item-label">
<div class="item-name">{{item.name}}</div> <div class="item-name">{{item.name}}</div>
{{#if (eq type 'weapon')}} {{#if (eq type 'weapon')}}
@ -103,28 +103,28 @@
</div> </div>
<div class="controls"> <div class="controls">
{{#if (eq type 'weapon')}} {{#if (eq type 'weapon')}}
<a class="{{#unless item.system.equipped}}unequipped{{/unless}}" data-action="toggleEquipItem" id="{{item.id}}" data-tooltip="{{#unless item.system.equipped}}{{localize 'DAGGERHEART.Tooltip.equip'}}{{else}}{{localize 'DAGGERHEART.Tooltip.unequip'}}{{/unless}}"> <a class="{{#unless item.system.equipped}}unequipped{{/unless}}" data-action="toggleEquipItem" data-tooltip="{{#unless item.system.equipped}}{{localize 'DAGGERHEART.Tooltip.equip'}}{{else}}{{localize 'DAGGERHEART.Tooltip.unequip'}}{{/unless}}">
<i class="fa-solid fa-hands"></i> <i class="fa-solid fa-hands"></i>
</a> </a>
{{/if}} {{/if}}
{{#if (eq type 'armor')}} {{#if (eq type 'armor')}}
<a class="{{#unless item.system.equipped}}unequipped{{/unless}}" data-action="toggleEquipItem" id="{{item.id}}" data-tooltip="{{#unless item.system.equipped}}{{localize 'DAGGERHEART.Tooltip.equip'}}{{else}}{{localize 'DAGGERHEART.Tooltip.unequip'}}{{/unless}}"> <a class="{{#unless item.system.equipped}}unequipped{{/unless}}" data-action="toggleEquipItem" data-tooltip="{{#unless item.system.equipped}}{{localize 'DAGGERHEART.Tooltip.equip'}}{{else}}{{localize 'DAGGERHEART.Tooltip.unequip'}}{{/unless}}">
<i class="fa-solid fa-shield"></i> <i class="fa-solid fa-shield"></i>
</a> </a>
{{/if}} {{/if}}
{{#if (eq type 'domainCard')}} {{#if (eq type 'domainCard')}}
{{#unless item.system.inVault}} {{#unless item.system.inVault}}
<a data-action="toggleVault" id="{{item.id}}" data-tooltip="{{localize 'DAGGERHEART.Tooltip.sendToVault'}}"> <a data-action="toggleVault" data-tooltip="{{localize 'DAGGERHEART.Tooltip.sendToVault'}}">
<i class="fa-solid fa-arrow-down"></i> <i class="fa-solid fa-arrow-down"></i>
</a> </a>
{{else}} {{else}}
<a data-action="toggleVault" id="{{item.id}}" data-tooltip="{{localize 'DAGGERHEART.Tooltip.sendToLoadout'}}"> <a data-action="toggleVault" data-tooltip="{{localize 'DAGGERHEART.Tooltip.sendToLoadout'}}">
<i class="fa-solid fa-arrow-up"></i> <i class="fa-solid fa-arrow-up"></i>
</a> </a>
{{/unless}} {{/unless}}
{{/if}} {{/if}}
<a data-action="toChat" data-uuid="{{item.uuid}}" data-tooltip="{{localize 'DAGGERHEART.Tooltip.sendToChat'}}"><i class="fa-regular fa-message"></i></a> <a data-action="toChat" data-tooltip="{{localize 'DAGGERHEART.Tooltip.sendToChat'}}"><i class="fa-regular fa-message"></i></a>
<a class="{{concat type '-context-menu'}}" data-type="{{type}}" data-uuid="{{item.uuid}}" id="{{item.id}}" data-tooltip="{{localize 'DAGGERHEART.Tooltip.moreOptions'}}"><i class="fa-solid fa-ellipsis-vertical"></i></a> <a data-action="triggerContextMenu" data-tooltip="{{localize 'DAGGERHEART.Tooltip.moreOptions'}}"><i class="fa-solid fa-ellipsis-vertical"></i></a>
</div> </div>
</li> </li>

View file

@ -7,12 +7,14 @@
<legend>{{localize "DAGGERHEART.Sheets.Global.Actions"}} <a><i class="fa-solid fa-plus icon-button" data-action="addAction"></i></a></legend> <legend>{{localize "DAGGERHEART.Sheets.Global.Actions"}} <a><i class="fa-solid fa-plus icon-button" data-action="addAction"></i></a></legend>
<div class="actions-list"> <div class="actions-list">
{{#each document.system.actions as |action index|}} {{#each document.system.actions as |action index|}}
<div class="action-item"> <div class="action-item"
data-action="editAction"
data-index="{{index}}"
>
<img class="image" src="{{action.img}}" /> <img class="image" src="{{action.img}}" />
<span>{{action.name}}</span> <span>{{action.name}}</span>
<div class="controls"> <div class="controls">
<a data-action="editAction" data-index="{{index}}"><i class="fa-solid fa-pen-to-square"></i></a> <a data-action="removeAction"><i class="fa-solid fa-trash"></i></a>
<a data-action="removeAction" data-index="{{index}}"><i class="fa-solid fa-trash"></i></a>
</div> </div>
</div> </div>
{{/each}} {{/each}}

View file

@ -6,7 +6,7 @@
<div class="hope-container"> <div class="hope-container">
{{#times 6}} {{#times 6}}
<span class="hope-inner-container"> <span class="hope-inner-container">
<input class="hope-value" type="checkbox" data-action="toggleHope" data-value="{{this}}" {{ checked (gte ../document.system.resources.hope.value this) }} {{#if (gte this ../document.system.resources.hope.max)}}disabled{{/if}} /> <input class="hope-value" type="checkbox" data-action="toggleHope" data-value="{{add this 1}}" {{ checked (gte ../document.system.resources.hope.value (add this 1)) }} {{#if (gte this ../document.system.resources.hope.max)}}disabled{{/if}} />
{{#if (gte this ../document.system.resources.hope.max)}}<i class="fa-solid fa-droplet-slash hope-scar"></i>{{/if}} {{#if (gte this ../document.system.resources.hope.max)}}<i class="fa-solid fa-droplet-slash hope-scar"></i>{{/if}}
</span> </span>
{{/times}} {{/times}}

View file

@ -20,21 +20,33 @@
{{formField fields.name value=source.name label="Name" name="name"}} {{formField fields.name value=source.name label="Name" name="name"}}
{{formField fields.img value=source.img label="Icon" name="img"}} {{formField fields.img value=source.img label="Icon" name="img"}}
{{formField fields.actionType value=source.actionType label="Type" name="actionType" localize=true}} {{formField fields.actionType value=source.actionType label="Type" name="actionType" localize=true}}
{{formField fields.chatDisplay value=source.chatDisplay name="chatDisplay"}}
</div>
</fieldset>
<fieldset class="action-category">
<legend class="action-category-label" data-action="toggleSection" data-section="identity">
<div>Description</div>
</legend>
<div class="action-category-data open">
{{formInput fields.description value=source.description name="description" }}
</div> </div>
</fieldset> </fieldset>
{{#if fields.roll}}{{> 'systems/daggerheart/templates/views/actionTypes/roll.hbs' fields=fields.roll.fields source=source.roll}}{{/if}}
</div> </div>
<div class="tab {{this.tabs.config.cssClass}}" data-group="primary" data-tab="config"> <div class="tab {{this.tabs.config.cssClass}}" data-group="primary" data-tab="config">
{{#unless isNPC}}
{{> 'systems/daggerheart/templates/views/actionTypes/uses.hbs' fields=fields.uses.fields source=source.uses}} {{> 'systems/daggerheart/templates/views/actionTypes/uses.hbs' fields=fields.uses.fields source=source.uses}}
{{> 'systems/daggerheart/templates/views/actionTypes/cost.hbs' fields=fields.cost.element.fields source=source.cost}} {{> 'systems/daggerheart/templates/views/actionTypes/cost.hbs' fields=fields.cost.element.fields source=source.cost}}
{{/unless}}
{{#if fields.target}}{{> 'systems/daggerheart/templates/views/actionTypes/range-target.hbs' fields=(object range=fields.range target=fields.target.fields) source=(object target=source.target range=source.range)}}{{/if}} {{#if fields.target}}{{> 'systems/daggerheart/templates/views/actionTypes/range-target.hbs' fields=(object range=fields.range target=fields.target.fields) source=(object target=source.target range=source.range)}}{{/if}}
</div> </div>
<div class="tab {{this.tabs.effect.cssClass}}" data-group="primary" data-tab="effect"> <div class="tab {{this.tabs.effect.cssClass}}" data-group="primary" data-tab="effect">
{{#if fields.roll}}{{> 'systems/daggerheart/templates/views/actionTypes/roll.hbs' fields=fields.roll.fields source=source.roll}}{{/if}}
{{#if fields.save}}{{> 'systems/daggerheart/templates/views/actionTypes/save.hbs' fields=fields.save.fields source=source.save}}{{/if}}
{{#if fields.damage}}{{> 'systems/daggerheart/templates/views/actionTypes/damage.hbs' fields=fields.damage.fields.parts.element.fields source=source.damage}}{{/if}} {{#if fields.damage}}{{> 'systems/daggerheart/templates/views/actionTypes/damage.hbs' fields=fields.damage.fields.parts.element.fields source=source.damage}}{{/if}}
{{#if fields.healing}}{{> 'systems/daggerheart/templates/views/actionTypes/healing.hbs' fields=fields.healing.fields source=source.healing}}{{/if}} {{#if fields.healing}}{{> 'systems/daggerheart/templates/views/actionTypes/healing.hbs' fields=fields.healing.fields source=source.healing}}{{/if}}
{{#if fields.resource}}{{> 'systems/daggerheart/templates/views/actionTypes/resource.hbs' fields=fields.resource.fields source=source.resource}}{{/if}} {{#if fields.resource}}{{> 'systems/daggerheart/templates/views/actionTypes/resource.hbs' fields=fields.resource.fields source=source.resource}}{{/if}}
{{#if fields.documentUUID}}{{> 'systems/daggerheart/templates/views/actionTypes/uuid.hbs' fields=fields.documentUUID source=source.documentUUID}}{{/if}} {{#if fields.documentUUID}}{{> 'systems/daggerheart/templates/views/actionTypes/uuid.hbs' fields=fields.documentUUID source=source.documentUUID}}{{/if}}
{{#if fields.effects}}{{> 'systems/daggerheart/templates/views/actionTypes/effect.hbs'}}{{/if}} {{#if fields.effects}}{{> 'systems/daggerheart/templates/views/actionTypes/effect.hbs' fields=fields.effects.element.fields source=source.effects}}{{/if}}
</div> </div>
</section> </section>
</div> </div>

View file

@ -7,7 +7,7 @@
{{#each source as |cost index|}} {{#each source as |cost index|}}
<fieldset> <fieldset>
<div class="multi-display"> <div class="multi-display">
{{formField ../fields.type label="Resource" value=cost.type name=(concat "cost." index ".type") localize=true}} {{formField ../fields.type choices=(@root.disableOption index ../fields.type.choices ../source) label="Resource" value=cost.type name=(concat "cost." index ".type") localize=true}}
{{formField ../fields.value label="Value" value=cost.value name=(concat "cost." index ".value")}} {{formField ../fields.value label="Value" value=cost.value name=(concat "cost." index ".value")}}
</div> </div>
<div class="multi-display"> <div class="multi-display">

View file

@ -4,33 +4,72 @@
<div>Damage</div> <div>Damage</div>
</legend> </legend>
<div class="action-category-data open"> <div class="action-category-data open">
<div class="fas fa-plus icon-button" data-action="addDamage"></div> {{#unless @root.isNPC}}
{{#if @root.hasBaseDamage}} <div class="fas fa-plus icon-button" data-action="addDamage"></div>
<div> {{#if @root.hasBaseDamage}}
{{!-- <input type="checkbox" data-action="addBaseDamage"{{#if @root.hasBaseDamage}} checked{{/if}}> --}} <div>
{{formField @root.fields.damage.fields.includeBase value=@root.source.damage.includeBase label="Include Item Damage" name="damage.includeBase" }} {{formField @root.fields.damage.fields.includeBase value=@root.source.damage.includeBase label="Include Item Damage" name="damage.includeBase" }}
</div> </div>
{{/if}} {{/if}}
{{/unless}}
{{#each source.parts as |dmg index|}} {{#each source.parts as |dmg index|}}
{{#with (@root.getRealIndex index) as | realIndex |}} {{#if @root.isNPC}}
<fieldset{{#if dmg.base}} disabled{{/if}}> {{formField ../fields.value.fields.custom.fields.enabled value=dmg.value.custom.enabled name=(concat ../path "damage.parts." index ".value.custom.enabled")}}
{{#unless dmg.base}} <input type="hidden" name="{{../path}}damage.parts.{{index}}.value.multiplier" value="{{dmg.value.multiplier}}">
{{formField ../../fields.custom.fields.enabled value=dmg.custom.enabled name=(concat "damage.parts." realIndex ".custom.enabled")}} {{#if dmg.value.custom.enabled}}
{{/unless}} {{formField ../fields.value.fields.custom.fields.formula value=dmg.value.custom.formula name=(concat ../path "damage.parts." index ".value.custom.formula") localize=true}}
{{#if dmg.custom.enabled}}
{{formField ../../fields.custom.fields.formula value=dmg.custom.formula name=(concat "damage.parts." realIndex ".custom.formula") localize=true}}
{{else}} {{else}}
<div class="multi-display"> <div class="multi-display">
{{formField ../../fields.multiplier value=dmg.multiplier name=(concat "damage.parts." realIndex ".multiplier") localize=true}} {{formField ../fields.value.fields.flatMultiplier value=dmg.value.flatMultiplier name=(concat ../path "damage.parts." index ".value.flatMultiplier") label="Multiplier" }}
{{formField ../../fields.dice value=dmg.dice name=(concat "damage.parts." realIndex ".dice")}} {{formField ../fields.value.fields.dice value=dmg.value.dice name=(concat ../path "damage.parts." index ".value.dice")}}
{{formField ../../fields.bonus value=dmg.bonus name=(concat "damage.parts." realIndex ".bonus") localize=true}} {{formField ../fields.value.fields.bonus value=dmg.value.bonus name=(concat ../path "damage.parts." index ".value.bonus") localize=true}}
</div> </div>
{{/if}} {{/if}}
{{formField ../../fields.type value=dmg.type name=(concat "damage.parts." realIndex ".type") localize=true}} {{formField ../fields.type value=dmg.type name=(concat ../path "damage.parts." index ".type") localize=true}}
<input type="hidden" name="damage.parts.{{realIndex}}.base" value="{{dmg.base}}"> {{else}}
{{#unless dmg.base}}<div class="fas fa-trash" data-action="removeDamage" data-index="{{realIndex}}"></div>{{/unless}} {{#with (@root.getRealIndex index) as | realIndex |}}
</fieldset> <fieldset{{#if dmg.base}} disabled{{/if}}>
{{/with}} {{#if (and (not @root.isNPC) @root.hasRoll (not dmg.base))}}
{{formField ../../fields.resultBased value=dmg.resultBased name=(concat "damage.parts." realIndex ".resultBased") localize=true}}
{{/if}}
{{#if (and (not @root.isNPC) @root.hasRoll (not dmg.base) dmg.resultBased)}}
<fieldset>
<legend>
<div>With Hope</div>
</legend>
{{> formula fields=../../fields.value.fields type=../../fields.type dmg=dmg source=dmg.value target="value" realIndex=realIndex}}
</fieldset>
<fieldset>
<legend>
<div>With Fear</div>
</legend>
{{> formula fields=../../fields.valueAlt.fields type=../../fields.type dmg=dmg source=dmg.valueAlt target="valueAlt" realIndex=realIndex}}
</fieldset>
{{else}}
{{> formula fields=../../fields.value.fields type=../fields.type dmg=dmg source=dmg.value target="value" realIndex=realIndex}}
{{/if}}
{{formField ../../fields.type value=dmg.type name=(concat "damage.parts." realIndex ".type") localize=true}}
<input type="hidden" name="damage.parts.{{realIndex}}.base" value="{{dmg.base}}">
{{#unless dmg.base}}<div class="fas fa-trash" data-action="removeDamage" data-index="{{realIndex}}"></div>{{/unless}}
</fieldset>
{{/with}}
{{/if}}
{{/each}} {{/each}}
</div> </div>
</fieldset> </fieldset>
{{#*inline "formula"}}
{{#unless dmg.base}}
{{formField fields.custom.fields.enabled value=source.custom.enabled name=(concat "damage.parts." realIndex "." target ".custom.enabled")}}
{{/unless}}
{{#if source.custom.enabled}}
{{formField fields.custom.fields.formula value=source.custom.formula name=(concat "damage.parts." realIndex "." target ".custom.formula") localize=true}}
{{else}}
<div class="multi-display">
{{formField fields.multiplier value=source.multiplier name=(concat "damage.parts." realIndex "." target ".multiplier") localize=true}}
{{#if (eq source.multiplier 'flat')}}{{formField fields.flatMultiplier value=source.flatMultiplier name=(concat "damage.parts." realIndex ".flatMultiplier") }}{{/if}}
{{formField fields.dice value=source.dice name=(concat "damage.parts." realIndex "." target ".dice")}}
{{formField fields.bonus value=source.bonus name=(concat "damage.parts." realIndex "." target ".bonus") localize=true}}
</div>
{{/if}}
{{/inline}}

View file

@ -4,15 +4,31 @@
</legend> </legend>
<div class="action-category-data open" data-key="effects"> <div class="action-category-data open" data-key="effects">
<div class="fas fa-plus icon-button" data-action="addEffect"></div> <div class="fas fa-plus icon-button" data-action="addEffect"></div>
{{#each @root.effects as | effect index | }} {{!-- {{#each @root.effects as | effect index | }}
<fieldset> <fieldset>
{{!-- <div class="multi-display"> --}} <div class="multi-display">
<div class="form-group"> <div class="form-group">
<img src="{{img}}"> <img src="{{img}}">
<label data-action="editEffect">{{name}}</label> <label data-action="editEffect">{{name}}</label>
<div class="fas fa-trash" data-action="removeEffect" data-index="{{index}}"></div> <div class="fas fa-trash" data-action="removeEffect" data-index="{{index}}"></div>
</div> </div>
{{!-- </div> --}} {{formfield }}
</div>
</fieldset>
{{/each}} --}}
{{#each source as | effect index |}}
<fieldset data-effect-id="{{effect._id}}">
<div class="multi-display">
{{#with (@root.getEffectDetails effect._id) as | details |}}
<div class="form-group" data-action="editEffect">
<img src="{{img}}">
<label>{{name}}</label>
<div class="fas fa-trash" data-action="removeEffect" data-index="{{index}}"></div>
</div>
{{/with}}
<input type="hidden" name="effects.{{index}}._id" value="{{effect._id}}">
{{formField ../fields.onSave value=effect.onSave name=(concat "effects." index ".onSave")}}
</div>
</fieldset> </fieldset>
{{/each}} {{/each}}
</div> </div>

View file

@ -6,16 +6,38 @@
<div class="action-category-data open"> <div class="action-category-data open">
<fieldset> <fieldset>
{{formField fields.type value=source.type name="healing.type" localize=true}} {{formField fields.type value=source.type name="healing.type" localize=true}}
<div class="multi-display"> {{#if (and (not @root.isNPC) @root.hasRoll)}}
{{formField fields.value.fields.custom.fields.enabled value=source.value.custom.enabled name="healing.value.custom.enabled"}} {{formField fields.resultBased value=source.resultBased name="healing.resultBased" localize=true}}
{{#if source.value.custom.enabled}} {{/if}}
{{formField fields.value.fields.custom.fields.formula value=source.value.custom.formula name="healing.value.custom.formula" localize=true}} {{#if (and (not @root.isNPC) @root.hasRoll source.resultBased)}}
{{else}} <fieldset>
{{formField fields.value.fields.multiplier value=source.value.multiplier name="healing.value.multiplier" localize=true}} <legend>
{{formField fields.value.fields.dice value=source.value.dice name="healing.value.dice"}} <div>With Hope</div>
{{formField fields.value.fields.bonus value=source.value.bonus name="healing.value.bonus" localize=true}} </legend>
{{/if}} {{> formula fields=fields.value.fields source=source.value target="value"}}
</div> </fieldset>
<fieldset>
<legend>
<div>With Fear</div>
</legend>
{{> formula fields=fields.valueAlt.fields source=source.valueAlt target="valueAlt"}}
</fieldset>
{{else}}
{{> formula fields=fields.value.fields source=source.value target="value"}}
{{/if}}
</fieldset> </fieldset>
</div> </div>
</fieldset> </fieldset>
{{#*inline "formula"}}
<div class="multi-display">
{{formField fields.custom.fields.enabled value=source.custom.enabled name=(concat "healing." target ".custom.enabled")}}
{{#if source.custom.enabled}}
{{formField fields.custom.fields.formula value=source.custom.formula name=(concat "healing." target ".custom.formula") localize=true}}
{{else}}
{{formField fields.multiplier value=source.multiplier name=(concat "healing." target ".multiplier") localize=true}}
{{formField fields.dice value=source.dice name=(concat "healing." target ".dice")}}
{{formField fields.bonus value=source.bonus name=(concat "healing." target ".bonus") localize=true}}
{{/if}}
</div>
{{/inline}}

View file

@ -3,11 +3,16 @@
<div>Range{{#if fields.target}} & Target{{/if}}</div> <div>Range{{#if fields.target}} & Target{{/if}}</div>
</legend> </legend>
<div class="action-category-data open"> <div class="action-category-data open">
{{formField fields.range value=source.range label="Range" name="range" localize=true}} {{formField fields.range value=source.range label="Range" name=(concat path "range") localize=true}}
</div> </div>
{{#if fields.target}} {{#if fields.target}}
<div class="action-category-data open"> <div class="action-category-data open">
{{formField fields.target.type value=source.target.type label="Target" name="target.type" localize=true}} <div class="multi-display">
{{#if (and source.target.type (not (eq source.target.type 'self')))}}
{{ formField fields.target.amount value=source.target.amount label="Amount" name=(concat path "target.amount") }}
{{/if}}
{{ formField fields.target.type value=source.target.type label="Target" name=(concat path "target.type") localize=true }}
</div>
</div> </div>
{{/if}} {{/if}}
</fieldset> </fieldset>

View file

@ -3,8 +3,12 @@
<div>Roll</div> <div>Roll</div>
</legend> </legend>
<div class="action-category-data open"> <div class="action-category-data open">
{{formField fields.type label="Type" name="roll.type" value=source.type localize=true}} {{#if @root.isNPC}}
{{formField fields.trait label="Trait" name="roll.trait" value=source.trait localize=true}} {{formField fields.bonus label="Bonus" name="roll.bonus" value=source.bonus}}
{{formField fields.difficulty label="Difficulty" name="roll.difficulty" value=source.difficulty}} {{else}}
{{formField fields.type label="Type" name="roll.type" value=source.type localize=true}}
{{formField fields.trait label="Trait" name="roll.trait" value=source.trait localize=true disabled=(not source.type)}}
{{formField fields.difficulty label="Difficulty" name="roll.difficulty" value=source.difficulty disabled=(not source.type)}}
{{/if}}
</div> </div>
</fieldset> </fieldset>

View file

@ -0,0 +1,10 @@
<fieldset class="action-category">
<legend class="action-category-label" data-action="toggleSection" data-section="roll">
<div>Save</div>
</legend>
<div class="action-category-data open">
{{formField fields.trait label="Trait" name="save.trait" value=source.trait localize=true}}
{{formField fields.difficulty label="Difficulty" name="save.difficulty" value=source.difficulty disabled=(not source.trait)}}
{{formField fields.damageMod label="Damage on Save" name="save.damageMod" value=source.damageMod localize=true disabled=(not source.trait)}}
</div>
</fieldset>

View file

@ -0,0 +1,24 @@
<div>
{{#if uses}}
<div class="form-group">
<div class="form-fields">
<label for="uses.enabled">Uses: {{uses.value}}/{{uses.max}}</label>
<input name="uses.enabled" type="checkbox"{{#if uses.enabled}} checked{{/if}}>
</div>
</div>
{{/if}}
{{#each costs as | cost index |}}
<div class="form-group">
<div class="form-fields">
<label for="{{type}}">{{type}}: {{total}}</label>
<input name="costs.{{index}}.enabled" type="checkbox"{{#if enabled}} checked{{/if}}>
{{#if scalable}}
<input type="range" value="{{scale}}" min="1" max="10" step="{{step}}" name="costs.{{index}}.scale">
{{/if}}
</div>
</div>
{{/each}}
{{!-- <footer>
<button data-action="sendCost"{{#unless canUse}} disabled{{/unless}}>Accept</button>
</footer> --}}
</div>

View file

@ -0,0 +1,76 @@
<div class="damage-reduction-container">
<div class="section-container padded">
<div class="resources-container">
<div class="resource-container">
<h4 class="armor-title">{{localize "DAGGERHEART.DamageReduction.ArmorMarks"}}</h4>
<div class="markers-subtitle">{{armorMarks}}/{{armorScore}}</div>
</div>
{{#if this.stress}}
<div class="resource-container">
<h4 class="armor-title">{{localize "DAGGERHEART.DamageReduction.Stress"}}</h4>
<div class="markers-subtitle">{{this.stress.value}}/{{this.stress.maxTotal}}</div>
</div>
{{/if}}
</div>
</div>
<div class="section-container">
<h4 class="mark-selection divider">
<div class="mark-selection-inner">
{{#each marks.armor}}
<div
class="mark-container {{#if this.selected}}selected{{/if}}"
data-action="setMarks" data-key="{{@key}}" data-type="armor"
>
<i class="fa-solid fa-shield"></i>
</div>
{{/each}}
</div>
<div class="mark-selection-inner">
{{#each marks.stress}}
<div
class="mark-container {{#if this.selected}}selected{{/if}} {{#if (not @root.basicMarksUsed)}}inactive{{/if}}"
{{#if @root.basicMarksUsed}}data-action="setMarks"{{/if}} data-key="{{@key}}" data-type="stress" data-tooltip="{{#if @root.basicMarksUsed}}{{localize "DAGGERHEART.DamageReduction.ArmorWithStress"}}{{else}}{{localize "DAGGERHEART.DamageReduction.UnncessaryStress"}}{{/if}}"
>
<i class="fa-solid fa-bolt"></i>
</div>
{{/each}}
</div>
</h4>
<div class="markers-subtitle bold">{{localize "DAGGERHEART.DamageReduction.UsedMarks"}}</div>
</div>
<div class="resources-container">
<div class="resource-container">
<h4 class="armor-title">{{localize "DAGGERHEART.DamageReduction.StressReduction"}}</h4>
</div>
</div>
{{#each availableStressReductions}}
<div class="section-container">
<h4 class="stress-reduction-container divider">
<div class="stress-reduction {{#if (eq this.from @root.currentDamage)}}active{{/if}} {{#if this.selected}}selected{{/if}}" data-action="useStressReduction" data-reduction="{{@key}}">
{{this.from}}
<i class="fa-solid fa-arrow-right-long"></i>
{{this.to}}
<div class="stress-reduction-cost">
{{this.cost}}
<i class="fa-solid fa-bolt"></i>
</div>
</div>
</h4>
</div>
{{/each}}
<footer class="padded">
<button type="button" data-action="takeDamage">
{{localize "Take"}}
<div class="damage-value {{#if this.reducedDamage}}reduced-value{{/if}}">{{this.damage}}</div>
{{#if this.reducedDamage}}
<i class="fa-solid fa-arrow-right-long"></i>
<div class="damage-value">{{this.reducedDamage}}</div>
{{/if}}
{{localize "Damage"}}
</button>
</footer>
</div>

View file

@ -1,11 +1,11 @@
<div> <div>
<div class="form-group"> <div class="form-group">
<label><strong>Total Damage</strong></label> <label><strong>{{title}}</strong></label>
<div class="form-fields"> <div class="form-fields">
<input type="text" value="{{this.rollString}}" disabled /> <input type="text" value="{{formula}}" disabled />
</div> </div>
</div> </div>
{{#each this.bonusDamage as |damage index|}} {{!-- {{#each bonusDamage as |damage index|}}
<div class="form-group"> <div class="form-group">
<label><strong>{{damage.description}}</strong></label> <label><strong>{{damage.description}}</strong></label>
<div class="form-fields"> <div class="form-fields">
@ -22,8 +22,8 @@
{{/if}} {{/if}}
</div> </div>
</div> </div>
{{/each}} {{/each}} --}}
<footer> <footer>
<button data-action="rollDamage">Roll</button> <button data-action="submitRoll">Roll</button>
</footer> </footer>
</div> </div>

View file

@ -1,26 +1,27 @@
<div> <div>
{{#if @root.hasRoll}}
<div class="roll-dialog-container"> <div class="roll-dialog-container">
<div class="flexcol"> <div class="flexcol">
<div class="roll-dialog-experience-container"> <div class="roll-dialog-experience-container">
{{#each this.experiences}} {{#each experiences}}
{{#if this.description}} {{#if description}}
<div class="roll-dialog-chip {{#if this.selected}}selected{{/if}}" data-action="selectExperience" data-key="{{this.id}}"> <div class="roll-dialog-chip {{#if (includes ../selectedExperiences id)}}selected{{/if}}" data-action="selectExperience" data-key="{{id}}">
<span>{{this.description}}</span> <span>{{description}}</span>
<span>+{{this.value}}</span> <span>+{{value}}</span>
</div> </div>
{{/if}} {{/if}}
{{/each}} {{/each}}
</div> </div>
<div class="flexrow"> <div class="flexrow">
<button class="disadvantage flex1 {{#if this.advantage}}selected{{/if}}" data-action="updateIsAdvantage" data-advantage="true">{{localize "DAGGERHEART.General.Advantage.Full"}}</button> <button class="disadvantage flex1 {{#if (eq advantage 1)}}selected{{/if}}" data-action="updateIsAdvantage" data-advantage="1">{{localize "DAGGERHEART.General.Advantage.Full"}}</button>
<button class="disadvantage flex1 {{#if (eq this.advantage false)}}selected{{/if}}" data-action="updateIsAdvantage">{{localize "DAGGERHEART.General.Disadvantage.Full"}}</button> <button class="disadvantage flex1 {{#if (eq advantage -1)}}selected{{/if}}" data-action="updateIsAdvantage" data-advantage="-1">{{localize "DAGGERHEART.General.Disadvantage.Full"}}</button>
</div> </div>
{{#if (not this.isNpc)}} {{!-- {{#if (not isNpc)}} --}}
<div class="form-group"> {{!-- <div class="form-group">
<label>Hope</label> <label>Hope</label>
<div class="form-fields"> <div class="form-fields">
<select name="hope"> <select name="hope">
{{selectOptions this.diceOptions selected=this.hope valueAttr="value" labelAttr="name" localize=true}} {{selectOptions diceOptions selected=hope valueAttr="value" labelAttr="name" localize=true}}
</select> </select>
</div> </div>
</div> </div>
@ -28,14 +29,15 @@
<label>Fear</label> <label>Fear</label>
<div class="form-fields"> <div class="form-fields">
<select name="fear"> <select name="fear">
{{selectOptions this.diceOptions selected=this.fear valueAttr="value" labelAttr="name" localize=true}} {{selectOptions diceOptions selected=fear valueAttr="value" labelAttr="name" localize=true}}
</select> </select>
</div> </div>
</div> </div> --}}
{{/if}} {{!-- {{/if}} --}}
</div> </div>
<footer>
<button data-action="finish">Roll</button>
</footer>
</div> </div>
{{/if}}
<footer>
<button data-action="submitRoll"{{#unless canRoll}} disabled{{/unless}}>Roll</button>
</footer>
</div> </div>