Merge branch 'main' into release

This commit is contained in:
WBHarry 2026-01-25 16:22:39 +01:00
commit 6b5c1ff965
94 changed files with 3068 additions and 411 deletions

View file

@ -3,13 +3,15 @@ import * as applications from './module/applications/_module.mjs';
import * as data from './module/data/_module.mjs'; import * as data from './module/data/_module.mjs';
import * as models from './module/data/_module.mjs'; import * as models from './module/data/_module.mjs';
import * as documents from './module/documents/_module.mjs'; import * as documents from './module/documents/_module.mjs';
import * as collections from './module/documents/collections/_module.mjs';
import * as dice from './module/dice/_module.mjs'; import * as dice from './module/dice/_module.mjs';
import * as fields from './module/data/fields/_module.mjs'; import * as fields from './module/data/fields/_module.mjs';
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs'; import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs';
import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs'; import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs';
import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll } from './module/dice/_module.mjs'; import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll } from './module/dice/_module.mjs';
import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs'; import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs';
import { enrichedFateRoll, getFateTypeData } from './module/enrichers/FateRollEnricher.mjs';
import { import {
handlebarsRegistration, handlebarsRegistration,
runMigrations, runMigrations,
@ -24,16 +26,18 @@ import TokenManager from './module/documents/tokenManager.mjs';
CONFIG.DH = SYSTEM; CONFIG.DH = SYSTEM;
CONFIG.TextEditor.enrichers.push(...enricherConfig); CONFIG.TextEditor.enrichers.push(...enricherConfig);
CONFIG.Dice.rolls = [BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll]; CONFIG.Dice.rolls = [BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll];
CONFIG.Dice.daggerheart = { CONFIG.Dice.daggerheart = {
DHRoll: DHRoll, DHRoll: DHRoll,
DualityRoll: DualityRoll, DualityRoll: DualityRoll,
D20Roll: D20Roll, D20Roll: D20Roll,
DamageRoll: DamageRoll DamageRoll: DamageRoll,
FateRoll: FateRoll
}; };
CONFIG.Actor.documentClass = documents.DhpActor; CONFIG.Actor.documentClass = documents.DhpActor;
CONFIG.Actor.dataModels = models.actors.config; CONFIG.Actor.dataModels = models.actors.config;
CONFIG.Actor.collection = collections.DhActorCollection;
CONFIG.Item.documentClass = documents.DHItem; CONFIG.Item.documentClass = documents.DHItem;
CONFIG.Item.dataModels = models.items.config; CONFIG.Item.dataModels = models.items.config;
@ -56,6 +60,9 @@ CONFIG.Canvas.layers.tokens.layerClass = DhTokenLayer;
CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate; CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate;
CONFIG.RollTable.documentClass = documents.DhRollTable;
CONFIG.RollTable.resultTemplate = 'systems/daggerheart/templates/ui/chat/table-result.hbs';
CONFIG.Scene.documentClass = documents.DhScene; CONFIG.Scene.documentClass = documents.DhScene;
CONFIG.Token.documentClass = documents.DhToken; CONFIG.Token.documentClass = documents.DhToken;
@ -103,7 +110,7 @@ Hooks.once('init', () => {
type: game.i18n.localize(typePath) type: game.i18n.localize(typePath)
}); });
const { Items, Actors } = foundry.documents.collections; const { Items, Actors, RollTables } = foundry.documents.collections;
Items.unregisterSheet('core', foundry.applications.sheets.ItemSheetV2); Items.unregisterSheet('core', foundry.applications.sheets.ItemSheetV2);
Items.registerSheet(SYSTEM.id, applications.sheets.items.Ancestry, { Items.registerSheet(SYSTEM.id, applications.sheets.items.Ancestry, {
types: ['ancestry'], types: ['ancestry'],
@ -188,6 +195,12 @@ Hooks.once('init', () => {
label: sheetLabel('TYPES.Actor.party') label: sheetLabel('TYPES.Actor.party')
}); });
RollTables.unregisterSheet('core', foundry.applications.sheets.RollTableSheet);
RollTables.registerSheet(SYSTEM.id, applications.sheets.rollTables.RollTableSheet, {
types: ['base'],
makeDefault: true
});
DocumentSheetConfig.unregisterSheet( DocumentSheetConfig.unregisterSheet(
CONFIG.ActiveEffect.documentClass, CONFIG.ActiveEffect.documentClass,
'core', 'core',
@ -296,13 +309,15 @@ Hooks.on('chatMessage', (_, message) => {
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value ? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
: undefined; : undefined;
const difficulty = rollCommand.difficulty; const difficulty = rollCommand.difficulty;
const grantResources = Boolean(rollCommand.grantResources);
const target = getCommandTarget({ allowNull: true }); const target = getCommandTarget({ allowNull: true });
const title = traitValue const title =
? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { (flavor ?? traitValue)
ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label) ? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
}) ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label)
: game.i18n.localize('DAGGERHEART.GENERAL.duality'); })
: game.i18n.localize('DAGGERHEART.GENERAL.duality');
enrichedDualityRoll({ enrichedDualityRoll({
reaction, reaction,
@ -312,7 +327,36 @@ Hooks.on('chatMessage', (_, message) => {
title, title,
label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'), label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'),
actionType: null, actionType: null,
advantage advantage,
grantResources
});
return false;
}
if (message.startsWith('/fr')) {
const result =
message.trim().toLowerCase() === '/fr' ? { result: {} } : rollCommandToJSON(message.replace(/\/fr\s?/, ''));
if (!result) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateParsing'));
return false;
}
const { result: rollCommand, flavor } = result;
const fateTypeData = getFateTypeData(rollCommand?.type);
if (!fateTypeData)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
const { value: fateType, label: fateTypeLabel } = fateTypeData;
const target = getCommandTarget({ allowNull: true });
const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll');
enrichedFateRoll({
target,
title,
label: fateTypeLabel,
fateType
}); });
return false; return false;
} }
@ -381,8 +425,8 @@ Hooks.on('targetToken', () => {
debouncedRangeEffectCall(); debouncedRangeEffectCall();
}); });
Hooks.on('refreshToken', (_, options) => { Hooks.on('refreshToken', (token, options) => {
if (options.refreshPosition) { if (options.refreshPosition && !token._original) {
debouncedRangeEffectCall(); debouncedRangeEffectCall();
} }
}); });

View file

@ -237,10 +237,13 @@
"confirmText": "Would you like to level up your companion {name} by {levelChange} levels at this time? (You can do it manually later)" "confirmText": "Would you like to level up your companion {name} by {levelChange} levels at this time? (You can do it manually later)"
}, },
"viewLevelups": "View Levelups", "viewLevelups": "View Levelups",
"resetCharacter": "Reset Character",
"viewParty": "View Party", "viewParty": "View Party",
"InvalidOldCharacterImportTitle": "Old Character Import", "InvalidOldCharacterImportTitle": "Old Character Import",
"InvalidOldCharacterImportText": "Character data exported prior to system version 1.1 will not generate a complete character. Do you wish to continue?", "InvalidOldCharacterImportText": "Character data exported prior to system version 1.1 will not generate a complete character. Do you wish to continue?",
"cancelBeastform": "Cancel Beastform" "cancelBeastform": "Cancel Beastform",
"resetCharacterConfirmationTitle": "Reset Character",
"resetCharacterConfirmationContent": "You are reseting all character data except name and portrait. Are you sure?"
}, },
"Companion": { "Companion": {
"FIELDS": { "FIELDS": {
@ -314,6 +317,8 @@
"selectPrimaryWeapon": "Select Primary Weapon", "selectPrimaryWeapon": "Select Primary Weapon",
"selectSecondaryWeapon": "Select Secondary Weapon", "selectSecondaryWeapon": "Select Secondary Weapon",
"selectSubclass": "Select Subclass", "selectSubclass": "Select Subclass",
"setupSkipTitle": "Skipping Character Setup",
"setupSkipContent": "You are skipping the Character Setup by adding this manually. The character setup is the blinking arrows in the top-right. Are you sure you want to continue?",
"startingItems": "Starting Items", "startingItems": "Starting Items",
"story": "Story", "story": "Story",
"storyExplanation": "Select which background and connection prompts you want to copy into your character's background.", "storyExplanation": "Select which background and connection prompts you want to copy into your character's background.",
@ -325,6 +330,12 @@
"title": "{actor} - Character Setup", "title": "{actor} - Character Setup",
"traitIncreases": "Trait Increases" "traitIncreases": "Trait Increases"
}, },
"CharacterReset": {
"title": "Reset Character",
"alwaysDeleteSection": "Deleted Data",
"optionalDeleteSection": "Optional Data",
"headerTitle": "Select which data you'd like to keep"
},
"CombatTracker": { "CombatTracker": {
"combatStarted": "Active", "combatStarted": "Active",
"giveSpotlight": "Give The Spotlight", "giveSpotlight": "Give The Spotlight",
@ -477,7 +488,9 @@
"tokenHUD": { "tokenHUD": {
"genericEffects": "Foundry Effects", "genericEffects": "Foundry Effects",
"depositPartyTokens": "Deposit Party Tokens", "depositPartyTokens": "Deposit Party Tokens",
"retrievePartyTokens": "Retrieve Party Tokens" "retrievePartyTokens": "Retrieve Party Tokens",
"depositCompanionTokens": "Deposit Companion Token",
"retrieveCompanionTokens": "Retrieve Companion Token"
} }
}, },
"ImageSelect": { "ImageSelect": {
@ -615,6 +628,13 @@
"title": "{name} Resource", "title": "{name} Resource",
"rerollDice": "Reroll Dice" "rerollDice": "Reroll Dice"
}, },
"RiskItAllDialog": {
"title": "{name} - Risk It All",
"subtitle": "Clear Stress and Hit Points",
"remainingTitle": "Remaining Points",
"clearResource": "Clear {resource}",
"finalTitle": "Final Character Resources"
},
"TagTeamSelect": { "TagTeamSelect": {
"title": "Tag Team Roll", "title": "Tag Team Roll",
"leaderTitle": "Initiating Character", "leaderTitle": "Initiating Character",
@ -962,6 +982,10 @@
"outsideRange": "Outside Range" "outsideRange": "Outside Range"
}, },
"Condition": { "Condition": {
"deathMove": {
"name": "Death Move",
"description": "The character is about to make a Death Move"
},
"dead": { "dead": {
"name": "Dead", "name": "Dead",
"description": "The character is dead" "description": "The character is dead"
@ -1013,15 +1037,15 @@
"DeathMoves": { "DeathMoves": {
"avoidDeath": { "avoidDeath": {
"name": "Avoid Death", "name": "Avoid Death",
"description": "You drop unconscious temporarily and work with the GM to describe how the situation gets much worse because of it. Then roll your Fear die; if its value is equal to or under your Level, take a Scar." "description": "Your character avoids death and faces the consequences. They temporarily drop unconscious, and then you work with the GM to describe how the situation worsens. While unconscious, your character can't move or act, and they can't be targeted by an attack. They return to consciousness when an ally clears 1 or more of their marked Hit Points or when the party finishes a long rest. After your character falls unconscious, roll your Hope Die. If its value is equal to or less than your character's level, they gain a scar: permanently cross out a Hope slot and work with the GM to determine its lasting narrative impact and how, if possible, it can be restored. If you ever cross out your last Hope slot, your character's journey ends."
}, },
"riskItAll": { "riskItAll": {
"name": "Risk It All", "name": "Risk It All",
"description": "Roll your Duality Dice. If Hope is higher, you stay on your feet and clear an amount of Hit Points and/or Stress equal to the value of the Hope die (divide the Hope die value up between these however youd prefer). If your Fear die is higher, you cross through the veil of death. If the Duality Dice are tied, you stay on your feet and clear all Hit Points and Stress." "description": "Roll your Duality Dice. If the Hope Die is higher, your character stays on their feet and clears a number of Hit Points or Stress equal to the value of the Hope Die (you can divide the Hope Die value between Hit Points and Stress however you'd prefer). If the Fear Die is higher, your character crosses through the veil of death. If the Duality Dice show matching results, your character stays up and clears all Hit Points and Stress."
}, },
"blazeOfGlory": { "blazeOfGlory": {
"name": "Blaze Of Glory", "name": "Blaze Of Glory",
"description": "With Blaze of Glory, the player is accepting death for the character. Take one action (at GM discretion), which becomes an automatic critical success, then cross through the veil of death." "description": "Your character embraces death and goes out in a blaze of glory. Take one final action. It automatically critically succeeds (with GM approval), and then you cross through the veil of death. NOTE: A Blaze of Glory effect has been added to your character. Any Duality Roll will automatically be a critical."
} }
}, },
"DomainCardTypes": { "DomainCardTypes": {
@ -2062,6 +2086,7 @@
"description": "Description", "description": "Description",
"main": "Data", "main": "Data",
"information": "Information", "information": "Information",
"itemFeatures": "Item Features",
"notes": "Notes", "notes": "Notes",
"inventory": "Inventory", "inventory": "Inventory",
"loadout": "Loadout", "loadout": "Loadout",
@ -2137,6 +2162,7 @@
"dropActorsHere": "Drop Actors here", "dropActorsHere": "Drop Actors here",
"dropFeaturesHere": "Drop Features here", "dropFeaturesHere": "Drop Features here",
"duality": "Duality", "duality": "Duality",
"dualityDice": "Duality Dice",
"dualityRoll": "Duality Roll", "dualityRoll": "Duality Roll",
"enabled": "Enabled", "enabled": "Enabled",
"evasion": "Evasion", "evasion": "Evasion",
@ -2146,11 +2172,14 @@
"plural": "Experiences" "plural": "Experiences"
}, },
"failure": "Failure", "failure": "Failure",
"fate": "Fate",
"fateRoll": "Fate Roll",
"fear": "Fear", "fear": "Fear",
"features": "Features", "features": "Features",
"formula": "Formula", "formula": "Formula",
"general": "General", "general": "General",
"gm": "GM", "gm": "GM",
"guaranteedCriticalSuccess": "Guaranteed Critical Success",
"healing": "Healing", "healing": "Healing",
"healingRoll": "Healing Roll", "healingRoll": "Healing Roll",
"hit": { "hit": {
@ -2194,6 +2223,7 @@
"single": "Player", "single": "Player",
"plurial": "Players" "plurial": "Players"
}, },
"portrait": "Portrait",
"proficiency": "Proficiency", "proficiency": "Proficiency",
"quantity": "Quantity", "quantity": "Quantity",
"range": "Range", "range": "Range",
@ -2210,6 +2240,7 @@
"rollWith": "{roll} Roll", "rollWith": "{roll} Roll",
"save": "Save", "save": "Save",
"scalable": "Scalable", "scalable": "Scalable",
"scars": "Scars",
"situationalBonus": "Situational Bonus", "situationalBonus": "Situational Bonus",
"spent": "Spent", "spent": "Spent",
"step": "Step", "step": "Step",
@ -2352,6 +2383,12 @@
"secondaryWeapon": "Secondary Weapon" "secondaryWeapon": "Secondary Weapon"
} }
}, },
"ROLLTABLES": {
"FIELDS": {
"formulaName": { "label": "Formula Name" }
},
"formula": "Formula"
},
"SETTINGS": { "SETTINGS": {
"Appearance": { "Appearance": {
"FIELDS": { "FIELDS": {
@ -2418,7 +2455,11 @@
"overlay": { "label": "Overlay Effect" }, "overlay": { "label": "Overlay Effect" },
"characterDefault": { "label": "Character Default Defeated Status" }, "characterDefault": { "label": "Character Default Defeated Status" },
"adversaryDefault": { "label": "Adversary Default Defeated Status" }, "adversaryDefault": { "label": "Adversary Default Defeated Status" },
"companionDefault": { "label": "Companion Default Defeated Status" } "companionDefault": { "label": "Companion Default Defeated Status" },
"deathMove": { "label": "Death Move" },
"dead": { "label": "Dead" },
"defeated": { "label": "Defeated" },
"unconscious": { "label": "Unconscious" }
}, },
"hopeFear": { "hopeFear": {
"label": "Hope & Fear", "label": "Hope & Fear",
@ -2451,10 +2492,6 @@
"label": "Show Resource Change Scrolltexts", "label": "Show Resource Change Scrolltexts",
"hint": "When a character is damaged, uses armor etc, a scrolling text will briefly appear by the token to signify this." "hint": "When a character is damaged, uses armor etc, a scrolling text will briefly appear by the token to signify this."
}, },
"playerCanEditSheet": {
"label": "Players Can Manually Edit Character Settings",
"hint": "Players are allowed to access the manual Character Settings and change their statistics beyond the rules."
},
"roll": { "roll": {
"roll": { "roll": {
"label": "Roll", "label": "Roll",
@ -2511,6 +2548,7 @@
"resetMovesText": "Are you sure you want to reset?", "resetMovesText": "Are you sure you want to reset?",
"FIELDS": { "FIELDS": {
"maxFear": { "label": "Max Fear" }, "maxFear": { "label": "Max Fear" },
"maxHope": { "label": "Max Hope" },
"traitArray": { "label": "Initial Trait Modifiers" }, "traitArray": { "label": "Initial Trait Modifiers" },
"maxLoadout": { "maxLoadout": {
"label": "Max Cards in Loadout", "label": "Max Cards in Loadout",
@ -2658,7 +2696,16 @@
"currentTarget": "Current" "currentTarget": "Current"
}, },
"deathMove": { "deathMove": {
"title": "Death Move" "title": "Death Move",
"gainScar": "You gained a scar.",
"avoidScar": "You have avoided a new scar.",
"journeysEnd": "You have {scars} Scars and have crossed out your last Hope slot. Your character's journey ends.",
"riskItAllCritical": "Critical Rolled, clearing all marked Stress and Hit Points.",
"riskItAllFailure": "The fear die rolled higher. You have crossed through the veil of death.",
"blazeOfGlory": "Blaze of Glory Effect Added!",
"riskItAllDialogButton": "Clear Stress And Hit Points.",
"riskItAllSuccessWithEnoughHope": "The Hope value is more than the marked Stress and Hit Points. Both are cleared fully.",
"riskItAllSuccess": "The hope die rolled higher, clear up to {hope} Stress And Hit Points."
}, },
"dicePool": { "dicePool": {
"title": "Dice Pool" "title": "Dice Pool"
@ -2780,7 +2827,9 @@
"noAssignedPlayerCharacter": "You have no assigned character.", "noAssignedPlayerCharacter": "You have no assigned character.",
"noSelectedToken": "You have no selected token", "noSelectedToken": "You have no selected token",
"onlyUseableByPC": "This can only be used with a PC token", "onlyUseableByPC": "This can only be used with a PC token",
"dualityParsing": "Duality roll not properly formated", "dualityParsing": "Duality roll not properly formatted",
"fateParsing": "Fate roll not properly formatted",
"fateTypeParsing": "Fate roll not properly formatted, bad fate type. Valid types are 'Hope' and 'Fear'",
"attributeFaulty": "The supplied Attribute doesn't exist", "attributeFaulty": "The supplied Attribute doesn't exist",
"domainCardWrongDomain": "You don't have access to that Domain", "domainCardWrongDomain": "You don't have access to that Domain",
"domainCardToHighLevel": "The Domain Card is too high level to be selected", "domainCardToHighLevel": "The Domain Card is too high level to be selected",
@ -2844,7 +2893,8 @@
"documentIsMissing": "The {documentType} is missing from the world.", "documentIsMissing": "The {documentType} is missing from the world.",
"tokenActorMissing": "{name} is missing an Actor", "tokenActorMissing": "{name} is missing an Actor",
"tokenActorsMissing": "[{names}] missing Actors", "tokenActorsMissing": "[{names}] missing Actors",
"domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used" "domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used",
"knowTheTide": "Know The Tide gained a token"
}, },
"Sidebar": { "Sidebar": {
"actorDirectory": { "actorDirectory": {

View file

@ -1,5 +1,6 @@
export { default as AttributionDialog } from './attributionDialog.mjs'; export { default as AttributionDialog } from './attributionDialog.mjs';
export { default as BeastformDialog } from './beastformDialog.mjs'; export { default as BeastformDialog } from './beastformDialog.mjs';
export { default as CharacterResetDialog } from './characterResetDialog.mjs';
export { default as d20RollDialog } from './d20RollDialog.mjs'; export { default as d20RollDialog } from './d20RollDialog.mjs';
export { default as DamageDialog } from './damageDialog.mjs'; export { default as DamageDialog } from './damageDialog.mjs';
export { default as DamageReductionDialog } from './damageReductionDialog.mjs'; export { default as DamageReductionDialog } from './damageReductionDialog.mjs';
@ -14,3 +15,4 @@ export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs';
export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs'; export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs';
export { default as GroupRollDialog } from './group-roll-dialog.mjs'; export { default as GroupRollDialog } from './group-roll-dialog.mjs';
export { default as TagTeamDialog } from './tagTeamDialog.mjs'; export { default as TagTeamDialog } from './tagTeamDialog.mjs';
export { default as RiskItAllDialog } from './riskItAllDialog.mjs';

View file

@ -0,0 +1,105 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class CharacterResetDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actor, options = {}) {
super(options);
this.actor = actor;
this.data = {
delete: {
class: { keep: false, label: 'TYPES.Item.class' },
subclass: { keep: false, label: 'TYPES.Item.subclass' },
ancestry: { keep: false, label: 'TYPES.Item.ancestry' },
community: { keep: false, label: 'TYPES.Item.community' }
},
optional: {
portrait: { keep: true, label: 'DAGGERHEART.GENERAL.portrait' },
name: { keep: true, label: 'Name' },
biography: { keep: true, label: 'DAGGERHEART.GENERAL.Tabs.biography' },
inventory: { keep: true, label: 'DAGGERHEART.GENERAL.inventory' }
}
};
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'character-reset'],
window: {
icon: 'fa-solid fa-arrow-rotate-left',
title: 'DAGGERHEART.APPLICATIONS.CharacterReset.title'
},
actions: {
finishSelection: this.#finishSelection
},
form: {
handler: this.updateData,
submitOnChange: true,
submitOnClose: false
}
};
/** @override */
static PARTS = {
resourceDice: {
id: 'resourceDice',
template: 'systems/daggerheart/templates/dialogs/characterReset.hbs'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.data = this.data;
return context;
}
static async updateData(event, _, formData) {
const { data } = foundry.utils.expandObject(formData.object);
this.data = foundry.utils.mergeObject(this.data, data);
this.render();
}
static getUpdateData() {
const update = {};
if (!this.data.optional.portrait) update.if(!this.data.optional.biography);
if (!this.data.optional.inventory) return update;
}
static async #finishSelection() {
const update = {};
if (!this.data.optional.name.keep) {
const defaultName = game.system.api.documents.DhpActor.defaultName({ type: 'character' });
foundry.utils.setProperty(update, 'name', defaultName);
foundry.utils.setProperty(update, 'prototypeToken.name', defaultName);
}
if (!this.data.optional.portrait.keep) {
foundry.utils.setProperty(update, 'img', this.actor.schema.fields.img.initial(this.actor));
foundry.utils.setProperty(update, 'prototypeToken.==texture', {});
foundry.utils.setProperty(update, 'prototypeToken.==ring', {});
}
if (this.data.optional.biography.keep)
foundry.utils.setProperty(update, 'system.biography', this.actor.system.biography);
if (this.data.optional.inventory.keep) foundry.utils.setProperty(update, 'system.gold', this.actor.system.gold);
const { system, ...rest } = update;
await this.actor.update({
...rest,
'==system': system ?? {}
});
const inventoryItemTypes = ['weapon', 'armor', 'consumable', 'loot'];
await this.actor.deleteEmbeddedDocuments(
'Item',
this.actor.items
.filter(x => !inventoryItemTypes.includes(x.type) || !this.data.optional.inventory.keep)
.map(x => x.id)
);
this.close();
}
}

View file

@ -109,11 +109,17 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.roll = this.roll; context.roll = this.roll;
context.rollType = this.roll?.constructor.name; context.rollType = this.roll?.constructor.name;
context.rallyDie = this.roll.rallyChoices; context.rallyDie = this.roll.rallyChoices;
const experiences = this.config.data?.system?.experiences || {};
const actorExperiences = this.config.data?.system?.experiences || {};
const companionExperiences = this.config.roll.companionRoll
? (this.config.data?.companion?.system.experiences ?? {})
: null;
const experiences = companionExperiences ?? actorExperiences;
context.experiences = Object.keys(experiences).map(id => ({ context.experiences = Object.keys(experiences).map(id => ({
id, id,
...experiences[id] ...experiences[id]
})); }));
context.selectedExperiences = this.config.experiences; context.selectedExperiences = this.config.experiences;
context.advantage = this.config.roll?.advantage; context.advantage = this.config.roll?.advantage;
context.disadvantage = this.config.roll?.disadvantage; context.disadvantage = this.config.roll?.disadvantage;
@ -123,7 +129,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.formula = this.roll.constructFormula(this.config); context.formula = this.roll.constructFormula(this.config);
if (this.actor?.system?.traits) context.abilities = this.getTraitModifiers(); if (this.actor?.system?.traits) context.abilities = this.getTraitModifiers();
context.showReaction = !this.config.roll?.type || context.rollType === 'DualityRoll'; context.showReaction = !this.config.skips?.reaction && context.rollType === 'DualityRoll';
context.reactionOverride = this.reactionOverride; context.reactionOverride = this.reactionOverride;
} }

View file

@ -1,11 +1,16 @@
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; import { enrichedFateRoll } from '../../enrichers/FateRollEnricher.mjs';
import { enrichedDualityRoll } from '../../enrichers/DualityRollEnricher.mjs';
export default class DhpDeathMove extends HandlebarsApplicationMixin(ApplicationV2) { const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actor) { constructor(actor) {
super({}); super({});
this.actor = actor; this.actor = actor;
this.selectedMove = null; this.selectedMove = null;
this.showRiskItAllButton = false;
this.riskItAllButtonLabel = '';
this.riskItAllHope = 0;
} }
get title() { get title() {
@ -38,6 +43,111 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application
return context; return context;
} }
async handleAvoidDeath() {
const target = this.actor.uuid;
const config = await enrichedFateRoll({
target,
title: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.avoidDeath.name'),
label: `${game.i18n.localize('DAGGERHEART.GENERAL.hope')} ${game.i18n.localize('DAGGERHEART.GENERAL.fateRoll')}`,
fateType: 'Hope'
});
if (!config.roll.fate) return;
let returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.avoidScar');
if (config.roll.fate.value <= this.actor.system.levelData.level.current) {
const newScarAmount = this.actor.system.scars + 1;
await this.actor.update({
system: {
scars: newScarAmount
}
});
if (newScarAmount >= this.actor.system.resources.hope.max) {
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id);
return game.i18n.format('DAGGERHEART.UI.Chat.deathMove.journeysEnd', { scars: newScarAmount });
}
returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.gainScar');
}
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.unconscious.id);
return returnMessage;
}
async handleRiskItAll() {
const config = await enrichedDualityRoll({
reaction: true,
traitValue: null,
target: this.actor,
difficulty: null,
title: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.riskItAll.name'),
label: game.i18n.localize('DAGGERHEART.GENERAL.dualityDice'),
actionType: null,
advantage: null,
grantResources: false,
customConfig: { skips: { resources: true, reaction: true } }
});
if (!config.roll.result) return;
const clearAllStressAndHitpointsUpdates = [
{ key: 'hitPoints', clear: true },
{ key: 'stress', clear: true }
];
let chatMessage = '';
if (config.roll.isCritical) {
config.resourceUpdates.addResources(clearAllStressAndHitpointsUpdates);
chatMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.riskItAllCritical');
}
if (config.roll.result.duality == 1) {
if (
config.roll.hope.value >=
this.actor.system.resources.hitPoints.value + this.actor.system.resources.stress.value
) {
config.resourceUpdates.addResources(clearAllStressAndHitpointsUpdates);
chatMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.riskItAllSuccessWithEnoughHope');
} else {
chatMessage = game.i18n.format('DAGGERHEART.UI.Chat.deathMove.riskItAllSuccess', {
hope: config.roll.hope.value
});
this.showRiskItAllButton = true;
this.riskItAllHope = config.roll.hope.value;
this.riskItAllButtonLabel = game.i18n.format('DAGGERHEART.UI.Chat.deathMove.riskItAllDialogButton');
}
}
if (config.roll.result.duality == -1) {
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id);
chatMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.riskItAllFailure');
}
await config.resourceUpdates.updateResources();
return chatMessage;
}
async handleBlazeOfGlory() {
this.actor.createEmbeddedDocuments('ActiveEffect', [
{
name: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.name'),
description: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.description'),
img: CONFIG.DH.GENERAL.deathMoves.blazeOfGlory.img,
changes: [
{
key: 'system.rules.roll.guaranteedCritical',
mode: 2,
value: 'true'
}
]
}
]);
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id);
return game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.blazeOfGlory');
}
static selectMove(_, button) { static selectMove(_, button) {
const move = button.dataset.move; const move = button.dataset.move;
this.selectedMove = CONFIG.DH.GENERAL.deathMoves[move]; this.selectedMove = CONFIG.DH.GENERAL.deathMoves[move];
@ -46,23 +156,49 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application
} }
static async takeMove() { static async takeMove() {
this.close();
let result = '';
if (CONFIG.DH.GENERAL.deathMoves.blazeOfGlory === this.selectedMove) {
result = await this.handleBlazeOfGlory();
}
if (CONFIG.DH.GENERAL.deathMoves.avoidDeath === this.selectedMove) {
result = await this.handleAvoidDeath();
}
if (CONFIG.DH.GENERAL.deathMoves.riskItAll === this.selectedMove) {
result = await this.handleRiskItAll();
}
if (!result) return;
const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
.expandRollMessage?.desc;
const cls = getDocumentClass('ChatMessage'); const cls = getDocumentClass('ChatMessage');
const msg = { const msg = {
user: game.user.id, user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate( content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/deathMove.hbs', 'systems/daggerheart/templates/ui/chat/deathMove.hbs',
{ {
player: this.actor.name, player: this.actor.name,
actor: { name: this.actor.name, img: this.actor.img }, actor: this.actor,
actorId: this.actor._id,
author: game.users.get(game.user.id), author: game.users.get(game.user.id),
title: game.i18n.localize(this.selectedMove.name), title: game.i18n.localize(this.selectedMove.name),
img: this.selectedMove.img, img: this.selectedMove.img,
description: game.i18n.localize(this.selectedMove.description) description: game.i18n.localize(this.selectedMove.description),
result: result,
open: autoExpandDescription ? 'open' : '',
chevron: autoExpandDescription ? 'fa-chevron-up' : 'fa-chevron-down',
showRiskItAllButton: this.showRiskItAllButton,
riskItAllButtonLabel: this.riskItAllButtonLabel,
riskItAllHope: this.riskItAllHope
} }
), ),
title: game.i18n.localize( title: game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.title'),
'DAGGERHEART.UI.Chat.deathMove.title'
),
speaker: cls.getSpeaker(), speaker: cls.getSpeaker(),
flags: { flags: {
daggerheart: { daggerheart: {
@ -72,7 +208,5 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application
}; };
cls.create(msg); cls.create(msg);
this.close();
} }
} }

View file

@ -0,0 +1,94 @@
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class RiskItAllDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actor, resourceValue) {
super({});
this.actor = actor;
this.resourceValue = resourceValue;
this.choices = {
hitPoints: 0,
stress: 0
};
}
get title() {
return game.i18n.format('DAGGERHEART.APPLICATIONS.RiskItAllDialog.title', { name: this.actor.name });
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'dh-style', 'dialog', 'views', 'risk-it-all'],
position: { width: 280, height: 'auto' },
window: { icon: 'fa-solid fa-dice fa-xl' },
actions: {
finish: RiskItAllDialog.#finish
}
};
static PARTS = {
application: {
id: 'risk-it-all',
template: 'systems/daggerheart/templates/dialogs/riskItAllDialog.hbs'
}
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
for (const input of htmlElement.querySelectorAll('.resource-container input'))
input.addEventListener('change', this.updateChoice.bind(this));
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.resourceValue = this.resourceValue;
context.maxHitPointsValue = Math.min(this.resourceValue, this.actor.system.resources.hitPoints.max);
context.maxStressValue = Math.min(this.resourceValue, this.actor.system.resources.stress.max);
context.remainingResource = this.resourceValue - this.choices.hitPoints - this.choices.stress;
context.unfinished = context.remainingResource !== 0;
context.choices = this.choices;
context.final = {
hitPoints: {
value: this.actor.system.resources.hitPoints.value - this.choices.hitPoints,
max: this.actor.system.resources.hitPoints.max
},
stress: {
value: this.actor.system.resources.stress.value - this.choices.stress,
max: this.actor.system.resources.stress.max
}
};
context;
return context;
}
updateChoice(event) {
let value = Number.parseInt(event.target.value);
const choiceKey = event.target.dataset.choice;
const actorValue = this.actor.system.resources[choiceKey].value;
const remaining = this.resourceValue - this.choices.hitPoints - this.choices.stress;
const changeAmount = value - this.choices[choiceKey];
/* If trying to increase beyond remaining resource points, just increase to max available */
if (remaining - changeAmount < 0) value = this.choices[choiceKey] + remaining;
else if (actorValue - value < 0) value = actorValue;
this.choices[choiceKey] = value;
this.render();
}
static async #finish() {
const resourceUpdate = Object.keys(this.choices).reduce((acc, resourceKey) => {
const value = this.actor.system.resources[resourceKey].value - this.choices[resourceKey];
acc[resourceKey] = { value };
return acc;
}, {});
await this.actor.update({
'system.resources': resourceUpdate
});
this.close();
}
}

View file

@ -5,7 +5,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
classes: ['daggerheart'], classes: ['daggerheart'],
actions: { actions: {
combat: DHTokenHUD.#onToggleCombat, combat: DHTokenHUD.#onToggleCombat,
togglePartyTokens: DHTokenHUD.#togglePartyTokens togglePartyTokens: DHTokenHUD.#togglePartyTokens,
toggleCompanions: DHTokenHUD.#toggleCompanions
} }
}; };
@ -26,7 +27,7 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
context.partyOnCanvas = context.partyOnCanvas =
this.actor.type === 'party' && this.actor.type === 'party' &&
this.actor.system.partyMembers.some(member => member.getActiveTokens().length > 0); this.actor.system.partyMembers.some(member => member.getActiveTokens().length > 0);
context.icons.toggleParty = 'systems/daggerheart/assets/icons/arrow-dunk.png'; context.icons.toggleClowncar = 'systems/daggerheart/assets/icons/arrow-dunk.png';
context.actorType = this.actor.type; context.actorType = this.actor.type;
context.usesEffects = this.actor.type !== 'party'; context.usesEffects = this.actor.type !== 'party';
context.canToggleCombat = DHTokenHUD.#nonCombatTypes.includes(this.actor.type) context.canToggleCombat = DHTokenHUD.#nonCombatTypes.includes(this.actor.type)
@ -56,6 +57,9 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
}, {}) }, {})
: null; : null;
context.hasCompanion = this.actor.system.companion;
context.companionOnCanvas = context.hasCompanion && this.actor.system.companion.getActiveTokens().length > 0;
return context; return context;
} }
@ -101,8 +105,24 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
: 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositPartyTokens' : 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositPartyTokens'
); );
await this.toggleClowncar(this.actor.system.partyMembers);
}
static async #toggleCompanions(_, button) {
const icon = button.querySelector('img');
icon.classList.toggle('flipped');
button.dataset.tooltip = game.i18n.localize(
icon.classList.contains('flipped')
? 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.retrieveCompanionTokens'
: 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositCompanionTokens'
);
await this.toggleClowncar([this.actor.system.companion]);
}
async toggleClowncar(actors) {
const animationDuration = 500; const animationDuration = 500;
const activeTokens = this.actor.system.partyMembers.flatMap(member => member.getActiveTokens()); const activeTokens = actors.flatMap(member => member.getActiveTokens());
const { x: actorX, y: actorY } = this.document; const { x: actorX, y: actorY } = this.document;
if (activeTokens.length > 0) { if (activeTokens.length > 0) {
for (let token of activeTokens) { for (let token of activeTokens) {
@ -114,14 +134,15 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
} }
} else { } else {
const activeScene = game.scenes.find(x => x.id === game.user.viewedScene); const activeScene = game.scenes.find(x => x.id === game.user.viewedScene);
const partyTokenData = []; const tokenData = [];
for (let member of this.actor.system.partyMembers) { for (let member of actors) {
const data = await member.getTokenDocument(); const data = await member.getTokenDocument();
partyTokenData.push(data.toObject()); tokenData.push(data.toObject());
} }
const newTokens = await activeScene.createEmbeddedDocuments( const newTokens = await activeScene.createEmbeddedDocuments(
'Token', 'Token',
partyTokenData.map(tokenData => ({ tokenData.map(tokenData => ({
...tokenData, ...tokenData,
alpha: 0, alpha: 0,
x: actorX, x: actorX,

View file

@ -1,6 +1,6 @@
import BaseLevelUp from './levelup.mjs'; import BaseLevelUp from './levelup.mjs';
import { defaultCompanionTier, LevelOptionType } from '../../data/levelTier.mjs'; import { defaultCompanionTier, LevelOptionType } from '../../data/levelTier.mjs';
import { DhLevelup } from '../../data/levelup.mjs'; import { DhCompanionLevelup as DhLevelup } from '../../data/companionLevelup.mjs';
import { diceTypes, range } from '../../config/generalConfig.mjs'; import { diceTypes, range } from '../../config/generalConfig.mjs';
export default class DhCompanionLevelUp extends BaseLevelUp { export default class DhCompanionLevelUp extends BaseLevelUp {
@ -9,7 +9,9 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
this.levelTiers = this.addBonusChoices(defaultCompanionTier); this.levelTiers = this.addBonusChoices(defaultCompanionTier);
const playerLevelupData = actor.system.levelData; const playerLevelupData = actor.system.levelData;
this.levelup = new DhLevelup(DhLevelup.initializeData(this.levelTiers, playerLevelupData)); this.levelup = new DhLevelup(
DhLevelup.initializeData(this.levelTiers, playerLevelupData, actor.system.levelupChoicesLeft)
);
} }
async _preparePartContext(partId, context) { async _preparePartContext(partId, context) {

View file

@ -70,7 +70,10 @@ export default class DhlevelUpViewMode extends HandlebarsApplicationMixin(Applic
return checkbox; return checkbox;
}); });
let label = game.i18n.localize(option.label); let label =
optionKey === 'domainCard'
? game.i18n.format(option.label, { maxLevel: tier.levels.end })
: game.i18n.localize(option.label);
return { return {
label: label, label: label,
checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => { checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => {

View file

@ -125,6 +125,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options, 'action'); const context = await super._prepareContext(_options, 'action');
context.source = this.action.toObject(true); context.source = this.action.toObject(true);
context.action = this.action;
context.summons = []; context.summons = [];
for (const summon of context.source.summon ?? []) { for (const summon of context.source.summon ?? []) {

View file

@ -1,3 +1,4 @@
export * as actors from './actors/_module.mjs'; export * as actors from './actors/_module.mjs';
export * as api from './api/_modules.mjs'; export * as api from './api/_modules.mjs';
export * as items from './items/_module.mjs'; export * as items from './items/_module.mjs';
export * as rollTables from './rollTables/_module.mjs';

View file

@ -1,5 +1,5 @@
import DHBaseActorSheet from '../api/base-actor.mjs'; import DHBaseActorSheet from '../api/base-actor.mjs';
import DhpDeathMove from '../../dialogs/deathMove.mjs'; import DhDeathMove from '../../dialogs/deathMove.mjs';
import { abilities } from '../../../config/actorConfig.mjs'; import { abilities } from '../../../config/actorConfig.mjs';
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs'; import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
@ -27,6 +27,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
makeDeathMove: CharacterSheet.#makeDeathMove, makeDeathMove: CharacterSheet.#makeDeathMove,
levelManagement: CharacterSheet.#levelManagement, levelManagement: CharacterSheet.#levelManagement,
viewLevelups: CharacterSheet.#viewLevelups, viewLevelups: CharacterSheet.#viewLevelups,
resetCharacter: CharacterSheet.#resetCharacter,
toggleEquipItem: CharacterSheet.#toggleEquipItem, toggleEquipItem: CharacterSheet.#toggleEquipItem,
toggleResourceDice: CharacterSheet.#toggleResourceDice, toggleResourceDice: CharacterSheet.#toggleResourceDice,
handleResourceDice: CharacterSheet.#handleResourceDice, handleResourceDice: CharacterSheet.#handleResourceDice,
@ -42,6 +43,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
icon: 'fa-solid fa-angles-up', icon: 'fa-solid fa-angles-up',
label: 'DAGGERHEART.ACTORS.Character.viewLevelups', label: 'DAGGERHEART.ACTORS.Character.viewLevelups',
action: 'viewLevelups' action: 'viewLevelups'
},
{
icon: 'fa-solid fa-arrow-rotate-left',
label: 'DAGGERHEART.ACTORS.Character.resetCharacter',
action: 'resetCharacter'
} }
] ]
}, },
@ -220,13 +226,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
async _preparePartContext(partId, context, options) { async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options); context = await super._preparePartContext(partId, context, options);
switch (partId) { switch (partId) {
case 'header':
const { playerCanEditSheet, levelupAuto } = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Automation
);
context.showSettings = game.user.isGM || !levelupAuto || (levelupAuto && playerCanEditSheet);
break;
case 'loadout': case 'loadout':
await this._prepareLoadoutContext(context, options); await this._prepareLoadoutContext(context, options);
break; break;
@ -666,12 +665,19 @@ export default class CharacterSheet extends DHBaseActorSheet {
new LevelupViewMode(this.document).render({ force: true }); new LevelupViewMode(this.document).render({ force: true });
} }
/**
* Resets the character data and removes all embedded documents.
*/
static async #resetCharacter() {
new game.system.api.applications.dialogs.CharacterResetDialog(this.document).render({ force: true });
}
/** /**
* Opens the Death Move interface for the character. * Opens the Death Move interface for the character.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #makeDeathMove() { static async #makeDeathMove() {
await new DhpDeathMove(this.document).render({ force: true }); await new DhDeathMove(this.document).render({ force: true });
} }
/** /**
@ -728,9 +734,9 @@ export default class CharacterSheet extends DHBaseActorSheet {
if (!result) return; if (!result) return;
/* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */ /* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */
const costResources = result.costs const costResources =
.filter(x => x.enabled) result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) ||
.map(cost => ({ ...cost, value: -cost.value, total: -cost.total })); {};
config.resourceUpdates.addResources(costResources); config.resourceUpdates.addResources(costResources);
await config.resourceUpdates.updateResources(); await config.resourceUpdates.updateResources();
} }
@ -956,6 +962,18 @@ export default class CharacterSheet extends DHBaseActorSheet {
} }
async _onDropItem(event, item) { async _onDropItem(event, item) {
const setupCriticalItemTypes = ['class', 'subclass', 'ancestry', 'community'];
if (this.document.system.needsCharacterSetup && setupCriticalItemTypes.includes(item.type)) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.CharacterCreation.setupSkipTitle')
},
content: game.i18n.localize('DAGGERHEART.APPLICATIONS.CharacterCreation.setupSkipContent')
});
if (!confirmed) return;
}
if (this.document.uuid === item.parent?.uuid) { if (this.document.uuid === item.parent?.uuid) {
return super._onDropItem(event, item); return super._onDropItem(event, item);
} }

View file

@ -38,15 +38,6 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
} }
}; };
/** @inheritDoc */
async _onRender(context, options) {
await super._onRender(context, options);
this.element
.querySelector('.level-value')
?.addEventListener('change', event => this.document.updateLevel(Number(event.currentTarget.value)));
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Application Clicks Actions */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -71,10 +62,10 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
title: `${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}: ${this.actor.name}`, title: `${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}: ${this.actor.name}`,
headerTitle: `Companion ${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}`, headerTitle: `Companion ${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}`,
roll: { roll: {
trait: partner.system.spellcastModifierTrait?.key trait: partner.system.spellcastModifierTrait?.key,
companionRoll: true
}, },
hasRoll: true, hasRoll: true
data: partner.getRollData()
}; };
const result = await partner.diceRoll(config); const result = await partner.diceRoll(config);

View file

@ -600,7 +600,7 @@ export default function DHApplicationMixin(Base) {
{ {
relativeTo: isAction ? doc.parent : doc, relativeTo: isAction ? doc.parent : doc,
rollData: doc.getRollData?.(), rollData: doc.getRollData?.(),
secrets: isAction ? doc.parent.isOwner : doc.isOwner secrets: isAction ? doc.parent.parent.isOwner : doc.isOwner
} }
); );
} }

View file

@ -0,0 +1 @@
export { default as RollTableSheet } from './rollTable.mjs';

View file

@ -0,0 +1,191 @@
export default class DhRollTableSheet extends foundry.applications.sheets.RollTableSheet {
static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
actions: {
changeMode: DhRollTableSheet.#onChangeMode,
drawResult: DhRollTableSheet.#onDrawResult,
resetResults: DhRollTableSheet.#onResetResults,
addFormula: DhRollTableSheet.#addFormula,
removeFormula: DhRollTableSheet.#removeFormula
}
};
static buildParts() {
const { footer, header, sheet, results, ...parts } = super.PARTS;
return {
sheet: {
...sheet,
template: 'systems/daggerheart/templates/sheets/rollTable/sheet.hbs'
},
header: { template: 'systems/daggerheart/templates/sheets/rollTable/header.hbs' },
...parts,
results: {
template: 'systems/daggerheart/templates/sheets/rollTable/results.hbs',
templates: ['templates/sheets/roll-table/result-details.hbs'],
scrollable: ['table[data-results] tbody']
},
summary: { template: 'systems/daggerheart/templates/sheets/rollTable/summary.hbs' },
footer
};
}
static PARTS = DhRollTableSheet.buildParts();
async _preRender(context, options) {
await super._preRender(context, options);
if (!options.internalRefresh)
this.daggerheartFlag = new game.system.api.data.DhRollTable(this.document.flags.daggerheart);
}
/* root PART has a blank element on _attachPartListeners, so it cannot be used to set the eventListeners for the view mode */
async _onRender(context, options) {
super._onRender(context, options);
for (const element of this.element.querySelectorAll('.system-update-field'))
element.addEventListener('change', this.updateSystemField.bind(this));
}
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'sheet':
context.altFormula = this.daggerheartFlag.altFormula;
context.usesAltFormula = Object.keys(this.daggerheartFlag.altFormula).length > 0;
context.altFormulaOptions = {
'': { name: this.daggerheartFlag.formulaName },
...this.daggerheartFlag.altFormula
};
context.activeAltFormula = this.daggerheartFlag.activeAltFormula;
context.selectedFormula = this.daggerheartFlag.getActiveFormula(this.document.formula);
context.results = this.getExtendedResults(context.results);
break;
case 'header':
context.altFormula = this.daggerheartFlag.altFormula;
context.usesAltFormula = Object.keys(this.daggerheartFlag.altFormula).length > 0;
context.altFormulaOptions = {
'': { name: this.daggerheartFlag.formulaName },
...this.daggerheartFlag.altFormula
};
context.activeAltFormula = this.daggerheartFlag.activeAltFormula;
break;
case 'summary':
context.systemFields = this.daggerheartFlag.schema.fields;
context.altFormula = this.daggerheartFlag.altFormula;
context.formulaName = this.daggerheartFlag.formulaName;
break;
case 'results':
context.results = this.getExtendedResults(context.results);
break;
}
return context;
}
getExtendedResults(results) {
const bodyDarkMode = document.body.classList.contains('theme-dark');
const elementLightMode = this.element.classList.contains('theme-light');
const elementDarkMode = this.element.classList.contains('theme-dark');
const isDarkMode = elementDarkMode || (!elementLightMode && bodyDarkMode);
return results.map(x => ({
...x,
displayImg: isDarkMode && x.img === 'icons/svg/d20-black.svg' ? 'icons/svg/d20.svg' : x.img
}));
}
/* -------------------------------------------- */
/* Flag SystemData update methods */
/* -------------------------------------------- */
async updateSystemField(event) {
const { dataset, value } = event.target;
await this.daggerheartFlag.updateSource({ [dataset.path]: value });
this.render({ internalRefresh: true });
}
getSystemFlagUpdate() {
const deleteUpdate = Object.keys(this.document._source.flags.daggerheart?.altFormula ?? {}).reduce(
(acc, formulaKey) => {
if (!this.daggerheartFlag.altFormula[formulaKey]) acc.altFormula[`-=${formulaKey}`] = null;
return acc;
},
{ altFormula: {} }
);
return { ['flags.daggerheart']: foundry.utils.mergeObject(this.daggerheartFlag.toObject(), deleteUpdate) };
}
static async #addFormula() {
await this.daggerheartFlag.updateSource({
[`altFormula.${foundry.utils.randomID()}`]: game.system.api.data.DhRollTable.getDefaultFormula()
});
this.render({ internalRefresh: true });
}
static async #removeFormula(_event, target) {
await this.daggerheartFlag.updateSource({
[`altFormula.-=${target.dataset.key}`]: null
});
this.render({ internalRefresh: true });
}
/* -------------------------------------------- */
/* Extended RollTable methods */
/* -------------------------------------------- */
/**
* Alternate between view and edit modes.
* @this {RollTableSheet}
* @type {ApplicationClickAction}
*/
static async #onChangeMode() {
this.mode = this.isEditMode ? 'view' : 'edit';
await this.document.update(this.getSystemFlagUpdate());
await this.render({ internalRefresh: true });
}
/** @inheritdoc */
async _processSubmitData(event, form, submitData, options) {
/* RollTable sends an empty dummy event when swapping from view/edit first time */
if (Object.keys(submitData).length) {
if (!submitData.flags) submitData.flags = { daggerheart: {} };
submitData.flags.daggerheart = this.getSystemFlagUpdate();
}
super._processSubmitData(event, form, submitData, options);
}
/** @inheritdoc */
static async #onResetResults() {
await this.document.update(this.getSystemFlagUpdate());
await this.document.resetResults();
}
/**
* Roll and draw a TableResult.
* @this {RollTableSheet}
* @type {ApplicationClickAction}
*/
static async #onDrawResult(_event, button) {
if (this.form) await this.submit({ operation: { render: false } });
button.disabled = true;
const table = this.document;
await this.document.update(this.getSystemFlagUpdate());
/* Sending in the currently selectd activeFormula to table.roll to use as the formula */
const selectedFormula = this.daggerheartFlag.getActiveFormula(this.document.formula);
const tableRoll = await table.roll({ selectedFormula });
const draws = table.getResultsForRoll(tableRoll.roll.total);
if (draws.length > 0) {
if (game.settings.get('core', 'animateRollTable')) await this._animateRoll(draws);
await table.draw(tableRoll);
}
// Reenable the button if drawing with replacement since the draw won't trigger a sheet re-render
if (table.replacement) button.disabled = false;
}
}

View file

@ -81,6 +81,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
html.querySelectorAll('.group-roll-header-expand-section').forEach(element => html.querySelectorAll('.group-roll-header-expand-section').forEach(element =>
element.addEventListener('click', this.groupRollExpandSection) element.addEventListener('click', this.groupRollExpandSection)
); );
html.querySelectorAll('.risk-it-all-button').forEach(element =>
element.addEventListener('click', event => this.riskItAllClearStressAndHitPoints(event, data))
);
}; };
setupHooks() { setupHooks() {
@ -94,15 +97,17 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
/** Ensure the chat theme inherits the interface theme */ /** Ensure the chat theme inherits the interface theme */
_replaceHTML(result, content, options) { _replaceHTML(result, content, options) {
const themedElement = result.log?.querySelector(".chat-log"); const themedElement = result.log?.querySelector('.chat-log');
themedElement?.classList.remove("themed", "theme-light", "theme-dark"); themedElement?.classList.remove('themed', 'theme-light', 'theme-dark');
super._replaceHTML(result, content, options); super._replaceHTML(result, content, options);
} }
/** Remove chat log theme from notifications area */ /** Remove chat log theme from notifications area */
async _onFirstRender(result, content) { async _onFirstRender(result, content) {
await super._onFirstRender(result, content); await super._onFirstRender(result, content);
document.querySelector("#chat-notifications .chat-log")?.classList.remove("themed", "theme-light", "theme-dark") document
.querySelector('#chat-notifications .chat-log')
?.classList.remove('themed', 'theme-light', 'theme-dark');
} }
async onRollSimple(event, message) { async onRollSimple(event, message) {
@ -383,4 +388,10 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
}); });
event.target.closest('.group-roll-section').querySelector('.group-roll-content').classList.toggle('closed'); event.target.closest('.group-roll-section').querySelector('.group-roll-content').classList.toggle('closed');
} }
async riskItAllClearStressAndHitPoints(event, data) {
const resourceValue = event.target.dataset.resourceValue;
const actor = game.actors.get(event.target.dataset.actorId);
new game.system.api.applications.dialogs.RiskItAllDialog(actor, resourceValue).render({ force: true });
}
} }

View file

@ -171,7 +171,7 @@ export const defeatedConditions = () => {
acc[key] = { acc[key] = {
...choice, ...choice,
img: defeated[`${choice.id}Icon`], img: defeated[`${choice.id}Icon`],
description: `DAGGERHEART.CONFIG.Condition.${choice.id}.description` description: game.i18n.localize(`DAGGERHEART.CONFIG.Condition.${choice.id}.description`)
}; };
return acc; return acc;
@ -179,6 +179,10 @@ export const defeatedConditions = () => {
}; };
export const defeatedConditionChoices = { export const defeatedConditionChoices = {
deathMove: {
id: 'deathMove',
name: 'DAGGERHEART.CONFIG.Condition.deathMove.name'
},
defeated: { defeated: {
id: 'defeated', id: 'defeated',
name: 'DAGGERHEART.CONFIG.Condition.defeated.name' name: 'DAGGERHEART.CONFIG.Condition.defeated.name'

View file

@ -1,6 +1,7 @@
export { default as DhCombat } from './combat.mjs'; export { default as DhCombat } from './combat.mjs';
export { default as DhCombatant } from './combatant.mjs'; export { default as DhCombatant } from './combatant.mjs';
export { default as DhTagTeamRoll } from './tagTeamRoll.mjs'; export { default as DhTagTeamRoll } from './tagTeamRoll.mjs';
export { default as DhRollTable } from './rollTable.mjs';
export { default as RegisteredTriggers } from './registeredTriggers.mjs'; export { default as RegisteredTriggers } from './registeredTriggers.mjs';
export * as countdowns from './countdowns.mjs'; export * as countdowns from './countdowns.mjs';

View file

@ -377,14 +377,14 @@ export class ResourceUpdateMap extends Map {
if (!resource.key) continue; if (!resource.key) continue;
const existing = this.get(resource.key); const existing = this.get(resource.key);
if (existing) { if (!existing || resource.clear) {
this.set(resource.key, resource);
} else if (!existing?.clear) {
this.set(resource.key, { this.set(resource.key, {
...existing, ...existing,
value: existing.value + (resource.value ?? 0), value: existing.value + (resource.value ?? 0),
total: existing.total + (resource.total ?? 0) total: existing.total + (resource.total ?? 0)
}); });
} else {
this.set(resource.key, resource);
} }
} }
} }

View file

@ -27,7 +27,7 @@ const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) =>
}); });
/* Common rules applying to Characters and Adversaries */ /* Common rules applying to Characters and Adversaries */
export const commonActorRules = (extendedData = { damageReduction: {} }) => ({ export const commonActorRules = (extendedData = { damageReduction: {}, attack: { damage: {} } }) => ({
conditionImmunities: new fields.SchemaField({ conditionImmunities: new fields.SchemaField({
hidden: new fields.BooleanField({ initial: false }), hidden: new fields.BooleanField({ initial: false }),
restrained: new fields.BooleanField({ initial: false }), restrained: new fields.BooleanField({ initial: false }),
@ -41,7 +41,23 @@ export const commonActorRules = (extendedData = { damageReduction: {} }) => ({
magical: new fields.NumberField({ initial: 0, min: 0 }), magical: new fields.NumberField({ initial: 0, min: 0 }),
physical: new fields.NumberField({ initial: 0, min: 0 }) physical: new fields.NumberField({ initial: 0, min: 0 })
}), }),
...extendedData.damageReduction ...(extendedData.damageReduction ?? {})
}),
attack: new fields.SchemaField({
...extendedData.attack,
damage: new fields.SchemaField({
hpDamageMultiplier: new fields.NumberField({
required: true,
nullable: false,
initial: 1
}),
hpDamageTakenMultiplier: new fields.NumberField({
required: true,
nullable: false,
initial: 1
}),
...(extendedData.attack?.damage ?? {})
})
}) })
}); });

View file

@ -35,7 +35,14 @@ export default class DhCharacter extends BaseDataActor {
'DAGGERHEART.ACTORS.Character.maxHPBonus' 'DAGGERHEART.ACTORS.Character.maxHPBonus'
), ),
stress: resourceField(6, 0, 'DAGGERHEART.GENERAL.stress', true), stress: resourceField(6, 0, 'DAGGERHEART.GENERAL.stress', true),
hope: resourceField(6, 2, 'DAGGERHEART.GENERAL.hope') hope: new fields.SchemaField({
value: new fields.NumberField({
initial: 2,
min: 0,
integer: true,
label: 'DAGGERHEART.GENERAL.hope'
})
})
}), }),
traits: new fields.SchemaField({ traits: new fields.SchemaField({
agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'), agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'),
@ -78,12 +85,7 @@ export default class DhCharacter extends BaseDataActor {
bags: new fields.NumberField({ initial: 0, integer: true }), bags: new fields.NumberField({ initial: 0, integer: true }),
chests: new fields.NumberField({ initial: 0, integer: true }) chests: new fields.NumberField({ initial: 0, integer: true })
}), }),
scars: new fields.TypedObjectField( scars: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.scars' }),
new fields.SchemaField({
name: new fields.StringField({}),
description: new fields.StringField()
})
),
biography: new fields.SchemaField({ biography: new fields.SchemaField({
background: new fields.HTMLField(), background: new fields.HTMLField(),
connections: new fields.HTMLField(), connections: new fields.HTMLField(),
@ -251,35 +253,35 @@ export default class DhCharacter extends BaseDataActor {
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint' hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint'
}), }),
disabledArmor: new fields.BooleanField({ intial: false }) disabledArmor: new fields.BooleanField({ intial: false })
},
attack: {
damage: {
diceIndex: new fields.NumberField({
integer: true,
min: 0,
max: 5,
initial: 0,
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.label',
hint: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.hint'
}),
bonus: new fields.NumberField({
required: true,
initial: 0,
min: 0,
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.bonus.label'
})
},
roll: new fields.SchemaField({
trait: new fields.StringField({
required: true,
choices: CONFIG.DH.ACTOR.abilities,
nullable: true,
initial: null,
label: 'DAGGERHEART.GENERAL.Rules.attack.roll.trait.label'
})
})
} }
}), }),
attack: new fields.SchemaField({
damage: new fields.SchemaField({
diceIndex: new fields.NumberField({
integer: true,
min: 0,
max: 5,
initial: 0,
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.label',
hint: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.hint'
}),
bonus: new fields.NumberField({
required: true,
initial: 0,
min: 0,
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.bonus.label'
})
}),
roll: new fields.SchemaField({
trait: new fields.StringField({
required: true,
choices: CONFIG.DH.ACTOR.abilities,
nullable: true,
initial: null,
label: 'DAGGERHEART.GENERAL.Rules.attack.roll.trait.label'
})
})
}),
dualityRoll: new fields.SchemaField({ dualityRoll: new fields.SchemaField({
defaultHopeDice: new fields.NumberField({ defaultHopeDice: new fields.NumberField({
nullable: false, nullable: false,
@ -301,6 +303,9 @@ export default class DhCharacter extends BaseDataActor {
runeWard: new fields.BooleanField({ initial: false }), runeWard: new fields.BooleanField({ initial: false }),
burden: new fields.SchemaField({ burden: new fields.SchemaField({
ignore: new fields.BooleanField() ignore: new fields.BooleanField()
}),
roll: new fields.SchemaField({
guaranteedCritical: new fields.BooleanField()
}) })
}) })
}; };
@ -363,7 +368,7 @@ export default class DhCharacter extends BaseDataActor {
const modifiers = subClasses const modifiers = subClasses
?.map(sc => ({ ...this.traits[sc.system.spellcastingTrait], key: sc.system.spellcastingTrait })) ?.map(sc => ({ ...this.traits[sc.system.spellcastingTrait], key: sc.system.spellcastingTrait }))
.filter(x => x); .filter(x => x);
return modifiers.sort((a, b) => a.value - b.value)[0]; return modifiers.sort((a, b) => (b.value ?? 0) - (a.value ?? 0))[0];
} }
get spellcastModifier() { get spellcastModifier() {
@ -544,7 +549,18 @@ export default class DhCharacter extends BaseDataActor {
} }
get deathMoveViable() { get deathMoveViable() {
return this.resources.hitPoints.max > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.max; const { characterDefault } = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Automation
).defeated;
const deathMoveOutcomeStatuses = Object.keys(CONFIG.DH.GENERAL.defeatedConditionChoices).filter(
key => key !== characterDefault
);
const deathMoveNotResolved = this.parent.statuses.every(status => !deathMoveOutcomeStatuses.includes(status));
const allHitPointsMarked =
this.resources.hitPoints.max > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.max;
return deathMoveNotResolved && allHitPointsMarked;
} }
get armorApplicableDamageTypes() { get armorApplicableDamageTypes() {
@ -642,8 +658,15 @@ export default class DhCharacter extends BaseDataActor {
? armor.system.baseThresholds.severe + this.levelData.level.current ? armor.system.baseThresholds.severe + this.levelData.level.current
: this.levelData.level.current * 2 : this.levelData.level.current * 2
}; };
this.resources.hope.max -= Object.keys(this.scars).length;
const globalHopeMax = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxHope;
this.resources.hope.max = globalHopeMax - this.scars;
this.resources.hitPoints.max += this.class.value?.system?.hitPoints ?? 0; this.resources.hitPoints.max += this.class.value?.system?.hitPoints ?? 0;
/* Companion Related Data */
this.companionData = {
levelupChoices: this.levelData.level.current - 1
};
} }
prepareDerivedData() { prepareDerivedData() {
@ -659,6 +682,8 @@ export default class DhCharacter extends BaseDataActor {
} }
} }
} }
this.companion.system.attack.roll.bonus = this.traits.instinct.value;
} }
this.resources.hope.value = Math.min(baseHope, this.resources.hope.max); this.resources.hope.value = Math.min(baseHope, this.resources.hope.max);
@ -699,6 +724,30 @@ export default class DhCharacter extends BaseDataActor {
changes.system.experiences[experience].core = true; changes.system.experiences[experience].core = true;
} }
} }
/* Scars can alter the amount of current hope */
if (changes.system?.scars) {
const diff = this.system.scars - changes.system.scars;
const newHopeMax = this.system.resources.hope.max + diff;
const newHopeValue = Math.min(newHopeMax, this.system.resources.hope.value);
if (newHopeValue != this.system.resources.hope.value) {
if (!changes.system.resources) changes.system.resources = { hope: { value: 0 } };
changes.system.resources.hope = {
...changes.system.resources.hope,
value: changes.system.resources.hope.value + newHopeValue
};
}
}
/* Force companion data prep */
if (this.companion) {
if (
changes.system?.levelData?.level?.current !== undefined &&
changes.system.levelData.level.current !== this._source.levelData.level.current
) {
this.companion.update(this.companion.toObject(), { diff: false, recursive: false });
}
}
} }
async _preDelete() { async _preDelete() {
@ -714,4 +763,11 @@ export default class DhCharacter extends BaseDataActor {
t => !!t t => !!t
); );
} }
static migrateData(source) {
if (typeof source.scars === 'object') source.scars = 0;
if (source.resources?.hope?.max) source.scars = Math.max(6 - source.resources.hope.max, 0);
return super.migrateData(source);
}
} }

View file

@ -108,7 +108,11 @@ export default class DhCompanion extends BaseDataActor {
get proficiency() { get proficiency() {
return this.partner?.system?.proficiency ?? 1; return this.partner?.system?.proficiency ?? 1;
} }
get canLevelUp() {
return this.levelupChoicesLeft > 0;
}
isItemValid() { isItemValid() {
return false; return false;
} }
@ -147,6 +151,17 @@ export default class DhCompanion extends BaseDataActor {
} }
} }
prepareDerivedData() {
/* Partner Related Setup */
if (this.partner) {
this.levelData.level.changed = this.partner.system.levelData.level.current;
this.levelupChoicesLeft = Object.values(this.levelData.levelups).reduce((acc, curr) => {
acc = Math.max(acc - curr.selections.length, 0);
return acc;
}, this.partner.system.companionData.levelupChoices);
}
}
async _preUpdate(changes, options, userId) { async _preUpdate(changes, options, userId) {
const allowed = await super._preUpdate(changes, options, userId); const allowed = await super._preUpdate(changes, options, userId);
if (allowed === false) return; if (allowed === false) return;
@ -162,6 +177,16 @@ export default class DhCompanion extends BaseDataActor {
changes.system.experiences[experience].core = true; changes.system.experiences[experience].core = true;
} }
} }
/* Force partner data prep */
if (this.partner) {
if (
changes.system?.levelData?.level?.current !== undefined &&
changes.system.levelData.level.current !== this._source.levelData.level.current
) {
this.partner.update(this.partner.toObject(), { diff: false, recursive: false });
}
}
} }
async _preDelete() { async _preDelete() {

View file

@ -8,6 +8,7 @@ export const config = {
adversaryRoll: DHActorRoll, adversaryRoll: DHActorRoll,
damageRoll: DHActorRoll, damageRoll: DHActorRoll,
dualityRoll: DHActorRoll, dualityRoll: DHActorRoll,
fateRoll: DHActorRoll,
groupRoll: DHGroupRoll, groupRoll: DHGroupRoll,
systemMessage: DHSystemMessage systemMessage: DHSystemMessage
}; };

View file

@ -0,0 +1,370 @@
import { abilities } from '../config/actorConfig.mjs';
import { chunkify } from '../helpers/utils.mjs';
import { LevelOptionType } from './levelTier.mjs';
export class DhCompanionLevelup extends foundry.abstract.DataModel {
static initializeData(levelTierData, pcLevelData, origChoicesLeft) {
let choicesLeft = origChoicesLeft;
const { current, changed } = pcLevelData.level;
const bonusChoicesOnly = current === changed;
const startLevel = bonusChoicesOnly ? current : current + 1;
const endLevel = bonusChoicesOnly ? startLevel : changed;
const tiers = {};
const levels = {};
const tierKeys = Object.keys(levelTierData.tiers);
tierKeys.forEach(key => {
const tier = levelTierData.tiers[key];
const belongingLevels = [];
for (var i = tier.levels.start; i <= tier.levels.end; i++) {
if (i <= endLevel) {
const initialAchievements = i === tier.levels.start ? tier.initialAchievements : {};
const experiences = initialAchievements.experience
? [...Array(initialAchievements.experience.nr).keys()].reduce((acc, _) => {
acc[foundry.utils.randomID()] = {
name: '',
modifier: initialAchievements.experience.modifier
};
return acc;
}, {})
: {};
const currentChoices = pcLevelData.levelups[i]?.selections?.length;
const maxSelections =
i === endLevel
? choicesLeft + (currentChoices ?? 0)
: (currentChoices ?? tier.maxSelections[i]);
if (!pcLevelData.levelups[i]) choicesLeft -= maxSelections;
levels[i] = DhLevelupLevel.initializeData(pcLevelData.levelups[i], maxSelections, {
...initialAchievements,
experiences,
domainCards: {}
});
}
belongingLevels.push(i);
}
/* Improve. Temporary handling for Companion new experiences */
Object.keys(tier.extraAchievements ?? {}).forEach(key => {
const level = Number(key);
if (level >= startLevel && level <= endLevel) {
const levelExtras = tier.extraAchievements[level];
if (levelExtras.experience) {
levels[level].achievements.experiences[foundry.utils.randomID()] = {
name: '',
modifier: levelExtras.experience.modifier
};
}
}
});
tiers[key] = {
name: tier.name,
belongingLevels: belongingLevels,
options: Object.keys(tier.options).reduce((acc, key) => {
acc[key] = tier.options[key].toObject?.() ?? tier.options[key];
return acc;
}, {})
};
});
return {
tiers,
levels,
startLevel,
currentLevel: startLevel,
endLevel
};
}
static defineSchema() {
const fields = foundry.data.fields;
return {
tiers: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({ required: true }),
belongingLevels: new fields.ArrayField(new fields.NumberField({ required: true, integer: true })),
options: new fields.TypedObjectField(
new fields.SchemaField({
label: new fields.StringField({ required: true }),
checkboxSelections: new fields.NumberField({ required: true, integer: true }),
minCost: new fields.NumberField({ required: true, integer: true }),
type: new fields.StringField({ required: true, choices: LevelOptionType }),
value: new fields.NumberField({ integer: true }),
amount: new fields.NumberField({ integer: true })
})
)
})
),
levels: new fields.TypedObjectField(new fields.EmbeddedDataField(DhLevelupLevel)),
startLevel: new fields.NumberField({ required: true, integer: true }),
currentLevel: new fields.NumberField({ required: true, integer: true }),
endLevel: new fields.NumberField({ required: true, integer: true })
};
}
#levelFinished(levelKey) {
const allSelectionsMade = this.levels[levelKey].nrSelections.available === 0;
const allChoicesMade = Object.keys(this.levels[levelKey].choices).every(choiceKey => {
const choice = this.levels[levelKey].choices[choiceKey];
return Object.values(choice).every(checkbox => {
switch (choiceKey) {
case 'trait':
case 'experience':
case 'domainCard':
case 'subclass':
case 'vicious':
return checkbox.data.length === (checkbox.amount ?? 1);
case 'multiclass':
const classSelected = checkbox.data.length === 1;
const domainSelected = checkbox.secondaryData.domain;
const subclassSelected = checkbox.secondaryData.subclass;
return classSelected && domainSelected && subclassSelected;
default:
return true;
}
});
});
const experiencesSelected = !this.levels[levelKey].achievements.experiences
? true
: Object.values(this.levels[levelKey].achievements.experiences).every(exp => exp.name);
const domainCardsSelected = Object.values(this.levels[levelKey].achievements.domainCards)
.filter(x => x.level <= this.endLevel)
.every(card => card.uuid);
const allAchievementsSelected = experiencesSelected && domainCardsSelected;
return allSelectionsMade && allChoicesMade && allAchievementsSelected;
}
get currentLevelFinished() {
return this.#levelFinished(this.currentLevel);
}
get allLevelsFinished() {
return Object.keys(this.levels)
.filter(level => Number(level) >= this.startLevel)
.every(this.#levelFinished.bind(this));
}
get unmarkedTraits() {
const possibleLevels = Object.values(this.tiers).reduce((acc, tier) => {
if (tier.belongingLevels.includes(this.currentLevel)) acc = tier.belongingLevels;
return acc;
}, []);
return Object.keys(this.levels)
.filter(key => possibleLevels.some(x => x === Number(key)))
.reduce(
(acc, levelKey) => {
const level = this.levels[levelKey];
Object.values(level.choices).forEach(choice =>
Object.values(choice).forEach(checkbox => {
if (
checkbox.type === 'trait' &&
checkbox.data.length > 0 &&
Number(levelKey) !== this.currentLevel
) {
checkbox.data.forEach(data => delete acc[data]);
}
})
);
return acc;
},
{ ...abilities }
);
}
get classUpgradeChoices() {
let subclasses = [];
let multiclass = null;
Object.keys(this.levels).forEach(levelKey => {
const level = this.levels[levelKey];
Object.values(level.choices).forEach(choice => {
Object.values(choice).forEach(checkbox => {
if (checkbox.type === 'multiclass') {
multiclass = {
class: checkbox.data.length > 0 ? checkbox.data[0] : null,
domain: checkbox.secondaryData.domain ?? null,
subclass: checkbox.secondaryData.subclass ?? null,
tier: checkbox.tier,
level: levelKey
};
}
if (checkbox.type === 'subclass') {
subclasses.push({
tier: checkbox.tier,
level: levelKey
});
}
});
});
});
return { subclasses, multiclass };
}
get tiersForRendering() {
const tierKeys = Object.keys(this.tiers);
const selections = Object.keys(this.levels).reduce(
(acc, key) => {
const level = this.levels[key];
Object.keys(level.choices).forEach(optionKey => {
const choice = level.choices[optionKey];
Object.keys(choice).forEach(checkboxNr => {
const checkbox = choice[checkboxNr];
if (!acc[checkbox.tier][optionKey]) acc[checkbox.tier][optionKey] = {};
Object.keys(choice).forEach(checkboxNr => {
acc[checkbox.tier][optionKey][checkboxNr] = { ...checkbox, level: Number(key) };
});
});
});
return acc;
},
tierKeys.reduce((acc, key) => {
acc[key] = {};
return acc;
}, {})
);
const { multiclass, subclasses } = this.classUpgradeChoices;
return tierKeys.map((tierKey, tierIndex) => {
const tier = this.tiers[tierKey];
const multiclassInTier = multiclass?.tier === Number(tierKey);
const subclassInTier = subclasses.some(x => x.tier === Number(tierKey));
return {
name: game.i18n.localize(tier.name),
active: this.currentLevel >= Math.min(...tier.belongingLevels),
groups: Object.keys(tier.options).map(optionKey => {
const option = tier.options[optionKey];
const checkboxes = [...Array(option.checkboxSelections).keys()].flatMap(index => {
const checkboxNr = index + 1;
const checkboxData = selections[tierKey]?.[optionKey]?.[checkboxNr];
const checkbox = { ...option, checkboxNr, tier: tierKey };
if (checkboxData) {
checkbox.level = checkboxData.level;
checkbox.selected = true;
checkbox.disabled = checkbox.level !== this.currentLevel;
}
if (optionKey === 'multiclass') {
if ((multiclass && !multiclassInTier) || subclassInTier) {
checkbox.disabled = true;
}
}
if (optionKey === 'subclass' && multiclassInTier) {
checkbox.disabled = true;
}
return checkbox;
});
let label = game.i18n.localize(option.label);
if (optionKey === 'domainCard') {
const maxLevel = tier.belongingLevels[tier.belongingLevels.length - 1];
label = game.i18n.format(option.label, { maxLevel });
}
return {
label: label,
checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => {
const anySelected = chunkedBoxes.some(x => x.selected);
const anyDisabled = chunkedBoxes.some(x => x.disabled);
return {
multi: option.minCost > 1,
checkboxes: chunkedBoxes.map(x => ({
...x,
selected: anySelected,
disabled: anyDisabled
}))
};
})
};
})
};
});
}
}
export class DhLevelupLevel extends foundry.abstract.DataModel {
static initializeData(levelData = { selections: [] }, maxSelections, achievements) {
return {
maxSelections: maxSelections,
achievements: {
experiences: levelData.achievements?.experiences ?? achievements.experiences ?? {},
domainCards: levelData.achievements?.domainCards
? levelData.achievements.domainCards.reduce((acc, card, index) => {
acc[index] = { ...card };
return acc;
}, {})
: (achievements.domainCards ?? {}),
proficiency: levelData.achievements?.proficiency ?? achievements.proficiency ?? null
},
choices: levelData.selections.reduce((acc, data) => {
if (!acc[data.optionKey]) acc[data.optionKey] = {};
acc[data.optionKey][data.checkboxNr] = { ...data };
return acc;
}, {})
};
}
static defineSchema() {
const fields = foundry.data.fields;
return {
maxSelections: new fields.NumberField({ required: true, integer: true }),
achievements: new fields.SchemaField({
experiences: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({ required: true }),
modifier: new fields.NumberField({ required: true, integer: true })
})
),
domainCards: new fields.TypedObjectField(
new fields.SchemaField({
uuid: new fields.StringField({ required: true, nullable: true, initial: null }),
itemUuid: new fields.StringField({ required: true }),
level: new fields.NumberField({ required: true, integer: true })
})
),
proficiency: new fields.NumberField({ integer: true })
}),
choices: new fields.TypedObjectField(
new fields.TypedObjectField(
new fields.SchemaField({
tier: new fields.NumberField({ required: true, integer: true }),
minCost: new fields.NumberField({ required: true, integer: true }),
amount: new fields.NumberField({ integer: true }),
value: new fields.StringField(),
data: new fields.ArrayField(new fields.StringField()),
secondaryData: new fields.TypedObjectField(new fields.StringField()),
type: new fields.StringField({ required: true })
})
)
)
};
}
get nrSelections() {
const selections = Object.keys(this.choices).reduce((acc, choiceKey) => {
const choice = this.choices[choiceKey];
acc += Object.values(choice).reduce((acc, x) => acc + x.minCost, 0);
return acc;
}, 0);
return {
selections: selections,
available: this.maxSelections - selections
};
}
}

View file

@ -105,12 +105,22 @@ export default class DamageField extends fields.SchemaField {
damagePromises.push( damagePromises.push(
actor.takeHealing(config.damage).then(updates => targetDamage.push({ token, updates })) actor.takeHealing(config.damage).then(updates => targetDamage.push({ token, updates }))
); );
else else {
const configDamage = foundry.utils.deepClone(config.damage);
const hpDamageMultiplier = config.actionActor?.system.rules.attack.damage.hpDamageMultiplier ?? 1;
const hpDamageTakenMultiplier = actor.system.rules.attack.damage.hpDamageTakenMultiplier;
if (configDamage.hitPoints) {
for (const part of configDamage.hitPoints.parts) {
part.total = Math.ceil(part.total * hpDamageMultiplier * hpDamageTakenMultiplier);
}
}
damagePromises.push( damagePromises.push(
actor actor
.takeDamage(config.damage, config.isDirect) .takeDamage(configDamage, config.isDirect)
.then(updates => targetDamage.push({ token, updates })) .then(updates => targetDamage.push({ token, updates }))
); );
}
} }
Promise.all(damagePromises).then(async _ => { Promise.all(damagePromises).then(async _ => {

View file

@ -147,7 +147,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
return await foundry.applications.ux.TextEditor.implementation.enrichHTML(fullDescription, { return await foundry.applications.ux.TextEditor.implementation.enrichHTML(fullDescription, {
relativeTo: this, relativeTo: this,
rollData: this.getRollData(), rollData: this.getRollData(),
secrets: this.isOwner secrets: this.parent.isOwner
}); });
} }

View file

@ -20,6 +20,7 @@ export default class RegisteredTriggers extends Map {
} }
registerItemTriggers(item, registerOverride) { registerItemTriggers(item, registerOverride) {
if (!item.actor || !item._stats.createdTime) return;
for (const action of item.system.actions ?? []) { for (const action of item.system.actions ?? []) {
if (!action.actor) continue; if (!action.actor) continue;
@ -71,10 +72,21 @@ export default class RegisteredTriggers extends Map {
} }
} }
unregisterSceneEnvironmentTriggers(flagSystemData) {
const sceneData = new game.system.api.data.scenes.DHScene(flagSystemData);
for (const environment of sceneData.sceneEnvironments) {
if (environment.pack) continue;
this.unregisterItemTriggers(environment.system.features);
}
}
unregisterSceneTriggers(scene) { unregisterSceneTriggers(scene) {
this.unregisterSceneEnvironmentTriggers(scene.flags.daggerheart);
for (const triggerKey of Object.keys(CONFIG.DH.TRIGGER.triggers)) { for (const triggerKey of Object.keys(CONFIG.DH.TRIGGER.triggers)) {
const existingTrigger = this.get(triggerKey); const existingTrigger = this.get(triggerKey);
if (!existingTrigger) continue; if (!existingTrigger) continue;
const filtered = new Map(); const filtered = new Map();
for (const [uuid, data] of existingTrigger.entries()) { for (const [uuid, data] of existingTrigger.entries()) {
if (!uuid.startsWith(scene.uuid)) filtered.set(uuid, data); if (!uuid.startsWith(scene.uuid)) filtered.set(uuid, data);
@ -83,14 +95,17 @@ export default class RegisteredTriggers extends Map {
} }
} }
registerSceneEnvironmentTriggers(flagSystemData) {
const sceneData = new game.system.api.data.scenes.DHScene(flagSystemData);
for (const environment of sceneData.sceneEnvironments) {
for (const feature of environment.system.features) {
if (feature) this.registerItemTriggers(feature, true);
}
}
}
registerSceneTriggers(scene) { registerSceneTriggers(scene) {
/* TODO: Finish sceneEnvironment registration and unreg */ this.registerSceneEnvironmentTriggers(scene.flags.daggerheart);
// const systemData = new game.system.api.data.scenes.DHScene(scene.flags.daggerheart);
// for (const environment of systemData.sceneEnvironments) {
// for (const feature of environment.system.features) {
// if(feature) this.registerItemTriggers(feature, true);
// }
// }
for (const actor of scene.tokens.filter(x => x.actor).map(x => x.actor)) { for (const actor of scene.tokens.filter(x => x.actor).map(x => x.actor)) {
if (actor.prototypeToken.actorLink) continue; if (actor.prototypeToken.actorLink) continue;
@ -107,13 +122,11 @@ export default class RegisteredTriggers extends Map {
if (!triggerSettings.enabled) return updates; if (!triggerSettings.enabled) return updates;
const dualityTrigger = this.get(trigger); const dualityTrigger = this.get(trigger);
if (dualityTrigger) { if (dualityTrigger?.size) {
const tokenBoundActors = ['adversary', 'environment']; const triggerActors = ['character', 'adversary', 'environment'];
const triggerActors = ['character', ...tokenBoundActors];
for (let [itemUuid, { actor: actorUuid, triggeringActorType, commands }] of dualityTrigger.entries()) { for (let [itemUuid, { actor: actorUuid, triggeringActorType, commands }] of dualityTrigger.entries()) {
const actor = await foundry.utils.fromUuid(actorUuid); const actor = await foundry.utils.fromUuid(actorUuid);
if (!actor || !triggerActors.includes(actor.type)) continue; if (!actor || !triggerActors.includes(actor.type)) continue;
if (tokenBoundActors.includes(actor.type) && !actor.getActiveTokens().length) continue;
const triggerData = CONFIG.DH.TRIGGER.triggers[trigger]; const triggerData = CONFIG.DH.TRIGGER.triggers[trigger];
if (triggerData.usesActor && triggeringActorType !== 'any') { if (triggerData.usesActor && triggeringActorType !== 'any') {

38
module/data/rollTable.mjs Normal file
View file

@ -0,0 +1,38 @@
import FormulaField from './fields/formulaField.mjs';
//Extra definitions for RollTable
export default class DhRollTable extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
formulaName: new fields.StringField({
required: true,
nullable: false,
initial: 'Roll Formula',
label: 'DAGGERHEART.ROLLTABLES.FIELDS.formulaName.label'
}),
altFormula: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({
required: true,
nullable: false,
initial: 'Roll Formula',
label: 'DAGGERHEART.ROLLTABLES.FIELDS.formulaName.label'
}),
formula: new FormulaField({ label: 'Formula Roll', initial: '1d20' })
})
),
activeAltFormula: new fields.StringField({ nullable: true, initial: null })
};
}
getActiveFormula(baseFormula) {
return this.activeAltFormula ? (this.altFormula[this.activeAltFormula]?.formula ?? baseFormula) : baseFormula;
}
static getDefaultFormula = () => ({
name: game.i18n.localize('Roll Formula'),
formula: '1d20'
});
}

View file

@ -55,15 +55,10 @@ export default class DhAutomation extends foundry.abstract.DataModel {
initial: true, initial: true,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.resourceScrollTexts.label' label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.resourceScrollTexts.label'
}), }),
playerCanEditSheet: new fields.BooleanField({
required: true,
initial: false,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.playerCanEditSheet.label'
}),
defeated: new fields.SchemaField({ defeated: new fields.SchemaField({
enabled: new fields.BooleanField({ enabled: new fields.BooleanField({
required: true, required: true,
initial: false, initial: true,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.enabled.label' label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.enabled.label'
}), }),
overlay: new fields.BooleanField({ overlay: new fields.BooleanField({
@ -74,7 +69,7 @@ export default class DhAutomation extends foundry.abstract.DataModel {
characterDefault: new fields.StringField({ characterDefault: new fields.StringField({
required: true, required: true,
choices: CONFIG.DH.GENERAL.defeatedConditionChoices, choices: CONFIG.DH.GENERAL.defeatedConditionChoices,
initial: CONFIG.DH.GENERAL.defeatedConditionChoices.unconscious.id, initial: CONFIG.DH.GENERAL.defeatedConditionChoices.deathMove.id,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.characterDefault.label' label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.characterDefault.label'
}), }),
adversaryDefault: new fields.StringField({ adversaryDefault: new fields.StringField({
@ -89,23 +84,29 @@ export default class DhAutomation extends foundry.abstract.DataModel {
initial: CONFIG.DH.GENERAL.defeatedConditionChoices.defeated.id, initial: CONFIG.DH.GENERAL.defeatedConditionChoices.defeated.id,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.companionDefault.label' label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.companionDefault.label'
}), }),
deathMoveIcon: new fields.FilePathField({
initial: 'icons/magic/life/heart-cross-purple-orange.webp',
categories: ['IMAGE'],
base64: false,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.deathMove.label'
}),
deadIcon: new fields.FilePathField({ deadIcon: new fields.FilePathField({
initial: 'icons/magic/death/grave-tombstone-glow-teal.webp', initial: 'icons/magic/death/grave-tombstone-glow-teal.webp',
categories: ['IMAGE'], categories: ['IMAGE'],
base64: false, base64: false,
label: 'Dead' label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.dead.label'
}), }),
defeatedIcon: new fields.FilePathField({ defeatedIcon: new fields.FilePathField({
initial: 'icons/magic/control/fear-fright-mask-orange.webp', initial: 'icons/magic/control/fear-fright-mask-orange.webp',
categories: ['IMAGE'], categories: ['IMAGE'],
base64: false, base64: false,
label: 'Defeated' label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.defeated.label'
}), }),
unconsciousIcon: new fields.FilePathField({ unconsciousIcon: new fields.FilePathField({
initial: 'icons/magic/control/sleep-bubble-purple.webp', initial: 'icons/magic/control/sleep-bubble-purple.webp',
categories: ['IMAGE'], categories: ['IMAGE'],
base64: false, base64: false,
label: 'Unconcious' label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.unconscious.label'
}) })
}), }),
roll: new fields.SchemaField({ roll: new fields.SchemaField({

View file

@ -23,6 +23,13 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
initial: 12, initial: 12,
label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxFear.label' label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxFear.label'
}), }),
maxHope: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 6,
label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxHope.label'
}),
maxLoadout: new fields.NumberField({ maxLoadout: new fields.NumberField({
required: true, required: true,
integer: true, integer: true,

View file

@ -3,3 +3,4 @@ export { default as D20Roll } from './d20Roll.mjs';
export { default as DamageRoll } from './damageRoll.mjs'; export { default as DamageRoll } from './damageRoll.mjs';
export { default as DHRoll } from './dhRoll.mjs'; export { default as DHRoll } from './dhRoll.mjs';
export { default as DualityRoll } from './dualityRoll.mjs'; export { default as DualityRoll } from './dualityRoll.mjs';
export { default as FateRoll } from './fateRoll.mjs';

View file

@ -99,11 +99,14 @@ export default class D20Roll extends DHRoll {
this.options.roll.modifiers = this.applyBaseBonus(); this.options.roll.modifiers = this.applyBaseBonus();
const actorExperiences = this.options.roll.companionRoll
? (this.options.data?.companion?.system.experiences ?? {})
: (this.options.data.system?.experiences ?? {});
this.options.experiences?.forEach(m => { this.options.experiences?.forEach(m => {
if (this.options.data.system?.experiences?.[m]) if (actorExperiences[m])
this.options.roll.modifiers.push({ this.options.roll.modifiers.push({
label: this.options.data.system.experiences[m].name, label: actorExperiences[m].name,
value: this.options.data.system.experiences[m].value value: actorExperiences[m].value
}); });
}); });

View file

@ -12,6 +12,7 @@ export default class DualityRoll extends D20Roll {
constructor(formula, data = {}, options = {}) { constructor(formula, data = {}, options = {}) {
super(formula, data, options); super(formula, data, options);
this.rallyChoices = this.setRallyChoices(); this.rallyChoices = this.setRallyChoices();
this.guaranteedCritical = options.guaranteedCritical;
} }
static messageType = 'dualityRoll'; static messageType = 'dualityRoll';
@ -25,29 +26,23 @@ export default class DualityRoll extends D20Roll {
} }
get dHope() { get dHope() {
// if ( !(this.terms[0] instanceof foundry.dice.terms.Die) ) return;
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
return this.dice[0]; return this.dice[0];
// return this.#hopeDice;
} }
set dHope(faces) { set dHope(faces) {
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
this.terms[0].faces = this.getFaces(faces); this.dice[0].faces = this.getFaces(faces);
// this.#hopeDice = `d${face}`;
} }
get dFear() { get dFear() {
// if ( !(this.terms[1] instanceof foundry.dice.terms.Die) ) return;
if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice(); if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice();
return this.dice[1]; return this.dice[1];
// return this.#fearDice;
} }
set dFear(faces) { set dFear(faces) {
if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice(); if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice();
this.dice[1].faces = this.getFaces(faces); this.dice[1].faces = this.getFaces(faces);
// this.#fearDice = `d${face}`;
} }
get dAdvantage() { get dAdvantage() {
@ -90,26 +85,29 @@ export default class DualityRoll extends D20Roll {
} }
get isCritical() { get isCritical() {
if (this.guaranteedCritical) return true;
if (!this.dHope._evaluated || !this.dFear._evaluated) return; if (!this.dHope._evaluated || !this.dFear._evaluated) return;
return this.dHope.total === this.dFear.total; return this.dHope.total === this.dFear.total;
} }
get withHope() { get withHope() {
if (!this._evaluated) return; if (!this._evaluated || this.guaranteedCritical) return;
return this.dHope.total > this.dFear.total; return this.dHope.total > this.dFear.total;
} }
get withFear() { get withFear() {
if (!this._evaluated) return; if (!this._evaluated || this.guaranteedCritical) return;
return this.dHope.total < this.dFear.total; return this.dHope.total < this.dFear.total;
} }
get totalLabel() { get totalLabel() {
const label = this.withHope const label = this.guaranteedCritical
? 'DAGGERHEART.GENERAL.hope' ? 'DAGGERHEART.GENERAL.guaranteedCriticalSuccess'
: this.withFear : this.isCritical
? 'DAGGERHEART.GENERAL.fear' ? 'DAGGERHEART.GENERAL.criticalSuccess'
: 'DAGGERHEART.GENERAL.criticalSuccess'; : this.withHope
? 'DAGGERHEART.GENERAL.hope'
: 'DAGGERHEART.GENERAL.fear';
return game.i18n.localize(label); return game.i18n.localize(label);
} }
@ -178,6 +176,21 @@ export default class DualityRoll extends D20Roll {
return modifiers; return modifiers;
} }
static async buildConfigure(config = {}, message = {}) {
config.dialog ??= {};
config.guaranteedCritical = config.data?.parent?.appliedEffects.reduce((a, c) => {
const change = c.changes.find(ch => ch.key === 'system.rules.roll.guaranteedCritical');
if (change) a = true;
return a;
}, false);
if (config.guaranteedCritical) {
config.dialog.configure = false;
}
return super.buildConfigure(config, message);
}
getActionChangeKeys() { getActionChangeKeys() {
const changeKeys = new Set([`system.bonuses.roll.${this.options.actionType}`]); const changeKeys = new Set([`system.bonuses.roll.${this.options.actionType}`]);
@ -223,7 +236,7 @@ export default class DualityRoll extends D20Roll {
data.hope = { data.hope = {
dice: roll.dHope.denomination, dice: roll.dHope.denomination,
value: roll.dHope.total, value: this.guaranteedCritical ? 0 : roll.dHope.total,
rerolled: { rerolled: {
any: roll.dHope.results.some(x => x.rerolled), any: roll.dHope.results.some(x => x.rerolled),
rerolls: roll.dHope.results.filter(x => x.rerolled) rerolls: roll.dHope.results.filter(x => x.rerolled)
@ -231,7 +244,7 @@ export default class DualityRoll extends D20Roll {
}; };
data.fear = { data.fear = {
dice: roll.dFear.denomination, dice: roll.dFear.denomination,
value: roll.dFear.total, value: this.guaranteedCritical ? 0 : roll.dFear.total,
rerolled: { rerolled: {
any: roll.dFear.results.some(x => x.rerolled), any: roll.dFear.results.some(x => x.rerolled),
rerolls: roll.dFear.results.filter(x => x.rerolled) rerolls: roll.dFear.results.filter(x => x.rerolled)
@ -243,7 +256,7 @@ export default class DualityRoll extends D20Roll {
}; };
data.result = { data.result = {
duality: roll.withHope ? 1 : roll.withFear ? -1 : 0, duality: roll.withHope ? 1 : roll.withFear ? -1 : 0,
total: roll.dHope.total + roll.dFear.total, total: this.guaranteedCritical ? 0 : roll.dHope.total + roll.dFear.total,
label: roll.totalLabel label: roll.totalLabel
}; };
@ -261,7 +274,7 @@ export default class DualityRoll extends D20Roll {
} }
static async handleTriggers(roll, config) { static async handleTriggers(roll, config) {
if (!config.source?.actor) return; if (!config.source?.actor || config.skips?.triggers) return;
const updates = []; const updates = [];
const dualityUpdates = await game.system.registeredTriggers.runTrigger( const dualityUpdates = await game.system.registeredTriggers.runTrigger(

85
module/dice/fateRoll.mjs Normal file
View file

@ -0,0 +1,85 @@
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
import D20Roll from './d20Roll.mjs';
import { setDiceSoNiceForHopeFateRoll, setDiceSoNiceForFearFateRoll } from '../helpers/utils.mjs';
export default class FateRoll extends D20Roll {
constructor(formula, data = {}, options = {}) {
super(formula, data, options);
}
static messageType = 'fateRoll';
static DefaultDialog = D20RollDialog;
get title() {
return game.i18n.localize(`DAGGERHEART.GENERAL.fateRoll`);
}
get dHope() {
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
return this.dice[0];
}
set dHope(faces) {
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
this.dice[0].faces = this.getFaces(faces);
}
get dFear() {
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
return this.dice[0];
}
set dFear(faces) {
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
this.dice[0].faces = this.getFaces(faces);
}
get isCritical() {
return false;
}
get fateDie() {
return this.data.fateType;
}
static getHooks(hooks) {
return [...(hooks ?? []), 'Fate'];
}
/** @inheritDoc */
static fromData(data) {
data.terms[0].class = foundry.dice.terms.Die.name;
return super.fromData(data);
}
createBaseDice() {
if (this.dice[0] instanceof foundry.dice.terms.Die) {
this.terms = [this.terms[0]];
return;
}
this.terms[0] = new foundry.dice.terms.Die({ faces: 12 });
}
static async buildEvaluate(roll, config = {}, message = {}) {
await super.buildEvaluate(roll, config, message);
if (roll.fateDie === 'Hope') {
await setDiceSoNiceForHopeFateRoll(roll, config.roll.fate.dice);
} else {
await setDiceSoNiceForFearFateRoll(roll, config.roll.fate.dice);
}
}
static postEvaluate(roll, config = {}) {
const data = super.postEvaluate(roll, config);
data.fate = {
dice: roll.fateDie === 'Hope' ? roll.dHope.denomination : roll.dFear.denomination,
value: roll.fateDie === 'Hope' ? roll.dHope.total : roll.dFear.total,
fateDie: roll.fateDie
};
return data;
}
}

View file

@ -4,6 +4,7 @@ export { default as DhpCombat } from './combat.mjs';
export { default as DHCombatant } from './combatant.mjs'; export { default as DHCombatant } from './combatant.mjs';
export { default as DhActiveEffect } from './activeEffect.mjs'; export { default as DhActiveEffect } from './activeEffect.mjs';
export { default as DhChatMessage } from './chatMessage.mjs'; export { default as DhChatMessage } from './chatMessage.mjs';
export { default as DhRollTable } from './rollTable.mjs';
export { default as DhScene } from './scene.mjs'; export { default as DhScene } from './scene.mjs';
export { default as DhToken } from './token.mjs'; export { default as DhToken } from './token.mjs';
export { default as DhTooltipManager } from './tooltipManager.mjs'; export { default as DhTooltipManager } from './tooltipManager.mjs';

View file

@ -241,6 +241,11 @@ export default class DhpActor extends Actor {
} }
} }
}); });
if (this.system.companion) {
this.system.companion.updateLevel(usedLevel);
}
this.sheet.render(); this.sheet.render();
} }
} }
@ -764,16 +769,24 @@ export default class DhpActor extends Actor {
}; };
} }
} else { } else {
const valueFunc = (base, resource, baseMax) => {
if (resource.clear) return baseMax && base.inverted ? baseMax : 0;
return (base.value ?? base) + resource.value;
};
switch (r.key) { switch (r.key) {
case 'fear': case 'fear':
ui.resources.updateFear( ui.resources.updateFear(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) + r.value valueFunc(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
r
)
); );
break; break;
case 'armor': case 'armor':
if (this.system.armor?.system?.marks) { if (this.system.armor?.system?.marks) {
updates.armor.resources['system.marks.value'] = Math.max( updates.armor.resources['system.marks.value'] = Math.max(
Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore), Math.min(valueFunc(this.system.armor.system.marks, r), this.system.armorScore),
0 0
); );
} }
@ -782,7 +795,7 @@ export default class DhpActor extends Actor {
if (this.system.resources?.[r.key]) { if (this.system.resources?.[r.key]) {
updates.actor.resources[`system.resources.${r.key}.value`] = Math.max( updates.actor.resources[`system.resources.${r.key}.value`] = Math.max(
Math.min( Math.min(
this.system.resources[r.key].value + r.value, valueFunc(this.system.resources[r.key], r, this.system.resources[r.key].max),
this.system.resources[r.key].max this.system.resources[r.key].max
), ),
0 0
@ -841,8 +854,8 @@ export default class DhpActor extends Actor {
async toggleDefeated(defeatedState) { async toggleDefeated(defeatedState) {
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated; const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated;
const { unconscious, defeated, dead } = CONFIG.DH.GENERAL.conditions(); const { deathMove, unconscious, defeated, dead } = CONFIG.DH.GENERAL.conditions();
const defeatedConditions = new Set([unconscious.id, defeated.id, dead.id]); const defeatedConditions = new Set([deathMove.id, unconscious.id, defeated.id, dead.id]);
if (!defeatedState) { if (!defeatedState) {
for (let defeatedId of defeatedConditions) { for (let defeatedId of defeatedConditions) {
await this.toggleStatusEffect(defeatedId, { overlay: settings.overlay, active: defeatedState }); await this.toggleStatusEffect(defeatedId, { overlay: settings.overlay, active: defeatedState });
@ -856,6 +869,18 @@ export default class DhpActor extends Actor {
} }
} }
async setDeathMoveDefeated(defeatedIconId) {
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated;
const actorDefault = settings[`${this.type}Default`];
if (!settings.enabled || !settings.enabled || !actorDefault || actorDefault === defeatedIconId) return;
for (let defeatedId of Object.keys(CONFIG.DH.GENERAL.defeatedConditionChoices)) {
await this.toggleStatusEffect(defeatedId, { overlay: settings.overlay, active: false });
}
if (defeatedIconId) await this.toggleStatusEffect(defeatedIconId, { overlay: settings.overlay, active: true });
}
queueScrollText(scrollingTextData) { queueScrollText(scrollingTextData) {
this.#scrollTextQueue.push(...scrollingTextData.map(data => () => createScrollText(this, data))); this.#scrollTextQueue.push(...scrollingTextData.map(data => () => createScrollText(this, data)));
if (!this.#scrollTextInterval) { if (!this.#scrollTextInterval) {

View file

@ -87,6 +87,15 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
break; break;
} }
} }
if (this.type === 'fateRoll') {
html.classList.add('fate');
if (this.system.roll?.fate.fateDie == 'Hope') {
html.classList.add('hope');
}
if (this.system.roll?.fate.fateDie == 'Fear') {
html.classList.add('fear');
}
}
const autoExpandRoll = game.settings.get( const autoExpandRoll = game.settings.get(
CONFIG.DH.id, CONFIG.DH.id,

View file

@ -0,0 +1 @@
export { default as DhActorCollection } from './actorCollection.mjs';

View file

@ -0,0 +1,14 @@
export default class DhActorCollection extends foundry.documents.collections.Actors {
/** Ensure companions are initialized after all other subtypes. */
_initialize() {
super._initialize();
const companions = [];
for (const actor of this.values()) {
if (actor.type === 'companion') companions.push(actor);
}
for (const actor of companions) {
this.delete(actor.id);
this.set(actor.id, actor);
}
}
}

View file

@ -0,0 +1,122 @@
export default class DhRollTable extends foundry.documents.RollTable {
async roll({ selectedFormula, roll, recursive = true, _depth = 0 } = {}) {
// Prevent excessive recursion
if (_depth > 5) {
throw new Error(`Maximum recursion depth exceeded when attempting to draw from RollTable ${this.id}`);
}
const formula = selectedFormula ?? this.formula;
// If there is no formula, automatically calculate an even distribution
if (!this.formula) {
await this.normalize();
}
// Reference the provided roll formula
roll = roll instanceof Roll ? roll : Roll.create(formula);
let results = [];
// Ensure that at least one non-drawn result remains
const available = this.results.filter(r => !r.drawn);
if (!available.length) {
ui.notifications.warn(game.i18n.localize('TABLE.NoAvailableResults'));
return { roll, results };
}
// Ensure that results are available within the minimum/maximum range
const minRoll = (await roll.reroll({ minimize: true })).total;
const maxRoll = (await roll.reroll({ maximize: true })).total;
const availableRange = available.reduce(
(range, result) => {
const r = result.range;
if (!range[0] || r[0] < range[0]) range[0] = r[0];
if (!range[1] || r[1] > range[1]) range[1] = r[1];
return range;
},
[null, null]
);
if (availableRange[0] > maxRoll || availableRange[1] < minRoll) {
ui.notifications.warn('No results can possibly be drawn from this table and formula.');
return { roll, results };
}
// Continue rolling until one or more results are recovered
let iter = 0;
while (!results.length) {
if (iter >= 10000) {
ui.notifications.error(
`Failed to draw an available entry from Table ${this.name}, maximum iteration reached`
);
break;
}
roll = await roll.reroll();
results = this.getResultsForRoll(roll.total);
iter++;
}
// Draw results recursively from any inner Roll Tables
if (recursive) {
const inner = [];
for (const result of results) {
const { type, documentUuid } = result;
const documentName = foundry.utils.parseUuid(documentUuid)?.type;
if (type === 'document' && documentName === 'RollTable') {
const innerTable = await fromUuid(documentUuid);
if (innerTable) {
const innerRoll = await innerTable.roll({ _depth: _depth + 1 });
inner.push(...innerRoll.results);
}
} else inner.push(result);
}
results = inner;
}
// Return the Roll and the results
return { roll, results };
}
async toMessage(results, { roll, messageData = {}, messageOptions = {} } = {}) {
messageOptions.rollMode ??= game.settings.get('core', 'rollMode');
// Construct chat data
messageData = foundry.utils.mergeObject(
{
author: game.user.id,
speaker: foundry.documents.ChatMessage.implementation.getSpeaker(),
rolls: [],
sound: roll ? CONFIG.sounds.dice : null,
flags: { 'core.RollTable': this.id }
},
messageData
);
if (roll) messageData.rolls.push(roll);
// Render the chat card which combines the dice roll with the drawn results
const detailsPromises = await Promise.allSettled(results.map(r => r.getHTML()));
const flavorKey = `TABLE.DrawFlavor${results.length > 1 ? 'Plural' : ''}`;
const flavor = game.i18n.format(flavorKey, {
number: results.length,
name: foundry.utils.escapeHTML(this.name)
});
messageData.content = await foundry.applications.handlebars.renderTemplate(CONFIG.RollTable.resultTemplate, {
description: await TextEditor.implementation.enrichHTML(this.description, {
documents: true,
secrets: this.isOwner
}),
flavor: flavor,
results: results.map((result, i) => {
const r = result.toObject(false);
r.details = detailsPromises[i].value ?? '';
const useTableIcon =
result.icon === CONFIG.RollTable.resultIcon && this.img !== this.constructor.DEFAULT_ICON;
r.icon = useTableIcon ? this.img : result.icon;
return r;
}),
rollHTML: this.displayRoll && roll ? await roll.render() : null,
table: this
});
// Create the chat message
return foundry.documents.ChatMessage.implementation.create(messageData, messageOptions);
}
}

View file

@ -51,6 +51,27 @@ export default class DhScene extends Scene {
} }
} }
async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false;
if (changes.flags?.daggerheart) {
if (this._source.flags.daggerheart) {
const unregisterTriggerData = this._source.flags.daggerheart.sceneEnvironments.reduce(
(acc, env) => {
if (!changes.flags.daggerheart.sceneEnvironments.includes(env)) acc.sceneEnvironments.push(env);
return acc;
},
{ ...this._source.flags.daggerheart, sceneEnvironments: [] }
);
game.system.registeredTriggers.unregisterSceneEnvironmentTriggers(unregisterTriggerData);
}
game.system.registeredTriggers.registerSceneEnvironmentTriggers(changes.flags.daggerheart);
}
}
_onDelete(options, userId) { _onDelete(options, userId) {
super._onDelete(options, userId); super._onDelete(options, userId);

View file

@ -2,7 +2,7 @@ import { abilities } from '../config/actorConfig.mjs';
import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs'; import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs';
export default function DhDualityRollEnricher(match, _options) { export default function DhDualityRollEnricher(match, _options) {
const roll = rollCommandToJSON(match[1], match[0]); const roll = rollCommandToJSON(match[0]);
if (!roll) return match[0]; if (!roll) return match[0];
return getDualityMessage(roll.result, roll.flavor); return getDualityMessage(roll.result, roll.flavor);
@ -47,6 +47,7 @@ function getDualityMessage(roll, flavor) {
${roll?.trait && abilities[roll.trait] ? `data-trait="${roll.trait}"` : ''} ${roll?.trait && abilities[roll.trait] ? `data-trait="${roll.trait}"` : ''}
${roll?.advantage ? 'data-advantage="true"' : ''} ${roll?.advantage ? 'data-advantage="true"' : ''}
${roll?.disadvantage ? 'data-disadvantage="true"' : ''} ${roll?.disadvantage ? 'data-disadvantage="true"' : ''}
${roll?.grantResources ? 'data-grant-resources="true"' : ''}
> >
${roll?.reaction ? '<i class="fa-solid fa-reply"></i>' : '<i class="fa-solid fa-circle-half-stroke"></i>'} ${roll?.reaction ? '<i class="fa-solid fa-reply"></i>' : '<i class="fa-solid fa-circle-half-stroke"></i>'}
${label} ${label}
@ -63,7 +64,8 @@ export const renderDualityButton = async event => {
traitValue = button.dataset.trait?.toLowerCase(), traitValue = button.dataset.trait?.toLowerCase(),
target = getCommandTarget({ allowNull: true }), target = getCommandTarget({ allowNull: true }),
difficulty = button.dataset.difficulty, difficulty = button.dataset.difficulty,
advantage = button.dataset.advantage ? Number(button.dataset.advantage) : undefined; advantage = button.dataset.advantage ? Number(button.dataset.advantage) : undefined,
grantResources = Boolean(button.dataset?.grantResources);
await enrichedDualityRoll( await enrichedDualityRoll(
{ {
@ -73,14 +75,15 @@ export const renderDualityButton = async event => {
difficulty, difficulty,
title: button.dataset.title, title: button.dataset.title,
label: button.dataset.label, label: button.dataset.label,
advantage advantage,
grantResources
}, },
event event
); );
}; };
export const enrichedDualityRoll = async ( export const enrichedDualityRoll = async (
{ reaction, traitValue, target, difficulty, title, label, advantage }, { reaction, traitValue, target, difficulty, title, label, advantage, grantResources, customConfig },
event event
) => { ) => {
const config = { const config = {
@ -93,16 +96,24 @@ export const enrichedDualityRoll = async (
advantage, advantage,
type: reaction ? 'reaction' : null type: reaction ? 'reaction' : null
}, },
skips: {
resources: !grantResources,
triggers: !grantResources
},
type: 'trait', type: 'trait',
hasRoll: true hasRoll: true,
...(customConfig ?? {})
}; };
if (target) { if (target) {
await target.diceRoll(config); const result = await target.diceRoll(config);
if (!result) return;
result.resourceUpdates.updateResources();
} else { } else {
// For no target, call DualityRoll directly with basic data // For no target, call DualityRoll directly with basic data
config.data = { experiences: {}, traits: {}, rules: {} }; config.data = { experiences: {}, traits: {}, rules: {} };
config.source = { actor: null }; config.source = { actor: null };
await CONFIG.Dice.daggerheart.DualityRoll.build(config); await CONFIG.Dice.daggerheart.DualityRoll.build(config);
} }
return config;
}; };

View file

@ -0,0 +1,80 @@
import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs';
export default function DhFateRollEnricher(match, _options) {
const roll = rollCommandToJSON(match[0]);
if (!roll) return match[0];
return getFateMessage(roll.result, roll?.flavor);
}
export function getFateTypeData(fateTypeValue) {
const value = fateTypeValue ? fateTypeValue.capitalize() : 'Hope';
const lowercased = fateTypeValue?.toLowerCase?.() ?? 'hope';
switch (lowercased) {
case 'hope':
case 'fear':
return { value, label: game.i18n.localize(`DAGGERHEART.GENERAL.${lowercased}`) };
default:
return null;
}
}
function getFateMessage(roll, flavor) {
const fateTypeData = getFateTypeData(roll?.type);
if (!fateTypeData)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
const { value: fateType, label: fateTypeLabel } = fateTypeData;
const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll');
const fateElement = document.createElement('span');
fateElement.innerHTML = `
<button type="button" class="fate-roll-button${roll?.inline ? ' inline' : ''}"
data-title="${title}"
data-label="${fateTypeLabel}"
data-fateType="${fateType}"
>
${title}
</button>
`;
return fateElement;
}
export const renderFateButton = async event => {
const button = event.currentTarget,
target = getCommandTarget({ allowNull: true });
const fateTypeData = getFateTypeData(button.dataset?.fatetype);
if (!fateTypeData) ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
const { value: fateType, label: fateTypeLabel } = fateTypeData;
await enrichedFateRoll(
{
target,
title: button.dataset.title,
label: button.dataset.label,
fateType: fateType
},
event
);
};
export const enrichedFateRoll = async ({ target, title, label, fateType }, event) => {
const config = {
event: event ?? {},
title: title,
headerTitle: label,
roll: {},
hasRoll: true,
fateType: fateType,
skips: { reaction: true }
};
config.data = { experiences: {}, traits: {}, fateType: fateType };
config.source = { actor: target?.uuid };
await CONFIG.Dice.daggerheart.FateRoll.build(config);
return config;
};

View file

@ -1,10 +1,11 @@
import { default as DhDamageEnricher, renderDamageButton } from './DamageEnricher.mjs'; import { default as DhDamageEnricher, renderDamageButton } from './DamageEnricher.mjs';
import { default as DhDualityRollEnricher, renderDualityButton } from './DualityRollEnricher.mjs'; import { default as DhDualityRollEnricher, renderDualityButton } from './DualityRollEnricher.mjs';
import { default as DhFateRollEnricher, renderFateButton } from './FateRollEnricher.mjs';
import { default as DhEffectEnricher } from './EffectEnricher.mjs'; import { default as DhEffectEnricher } from './EffectEnricher.mjs';
import { default as DhTemplateEnricher, renderMeasuredTemplate } from './TemplateEnricher.mjs'; import { default as DhTemplateEnricher, renderMeasuredTemplate } from './TemplateEnricher.mjs';
import { default as DhLookupEnricher } from './LookupEnricher.mjs'; import { default as DhLookupEnricher } from './LookupEnricher.mjs';
export { DhDamageEnricher, DhDualityRollEnricher, DhEffectEnricher, DhTemplateEnricher }; export { DhDamageEnricher, DhDualityRollEnricher, DhEffectEnricher, DhTemplateEnricher, DhFateRollEnricher };
export const enricherConfig = [ export const enricherConfig = [
{ {
@ -15,6 +16,10 @@ export const enricherConfig = [
pattern: /\[\[\/dr\s?(.*?)\]\]({[^}]*})?/g, pattern: /\[\[\/dr\s?(.*?)\]\]({[^}]*})?/g,
enricher: DhDualityRollEnricher enricher: DhDualityRollEnricher
}, },
{
pattern: /\[\[\/fr\s?(.*?)\]\]({[^}]*})?/g,
enricher: DhFateRollEnricher
},
{ {
pattern: /@Effect\[([^\[\]]*)\]({[^}]*})?/g, pattern: /@Effect\[([^\[\]]*)\]({[^}]*})?/g,
enricher: DhEffectEnricher enricher: DhEffectEnricher
@ -38,6 +43,10 @@ export const enricherRenderSetup = element => {
.querySelectorAll('.duality-roll-button') .querySelectorAll('.duality-roll-button')
.forEach(element => element.addEventListener('click', renderDualityButton)); .forEach(element => element.addEventListener('click', renderDualityButton));
element
.querySelectorAll('.fate-roll-button')
.forEach(element => element.addEventListener('click', renderFateButton));
element element
.querySelectorAll('.measured-template-button') .querySelectorAll('.measured-template-button')
.forEach(element => element.addEventListener('click', renderMeasuredTemplate)); .forEach(element => element.addEventListener('click', renderMeasuredTemplate));

View file

@ -1,14 +1,14 @@
import { diceTypes, getDiceSoNicePresets, range } from '../config/generalConfig.mjs'; import { diceTypes, getDiceSoNicePresets, getDiceSoNicePreset, range } from '../config/generalConfig.mjs';
import Tagify from '@yaireo/tagify'; import Tagify from '@yaireo/tagify';
export const capitalize = string => { export const capitalize = string => {
return string.charAt(0).toUpperCase() + string.slice(1); return string.charAt(0).toUpperCase() + string.slice(1);
}; };
export function rollCommandToJSON(text, raw) { export function rollCommandToJSON(text) {
if (!text) return {}; if (!text) return {};
const flavorMatch = raw?.match(/{(.*)}$/); const flavorMatch = text?.match(/{(.*)}$/);
const flavor = flavorMatch ? flavorMatch[1] : null; const flavor = flavorMatch ? flavorMatch[1] : null;
// Match key="quoted string" OR key=unquotedValue // Match key="quoted string" OR key=unquotedValue
@ -31,7 +31,7 @@ export function rollCommandToJSON(text, raw) {
} }
result[key] = value; result[key] = value;
} }
return Object.keys(result).length > 0 ? { result, flavor } : null; return { result, flavor };
} }
export const getCommandTarget = (options = {}) => { export const getCommandTarget = (options = {}) => {
@ -69,6 +69,20 @@ export const setDiceSoNiceForDualityRoll = async (rollResult, advantageState, ho
} }
}; };
export const setDiceSoNiceForHopeFateRoll = async (rollResult, hopeFaces) => {
if (!game.modules.get('dice-so-nice')?.active) return;
const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
const diceSoNicePresets = await getDiceSoNicePreset(diceSoNice.hope, hopeFaces);
rollResult.dice[0].options = diceSoNicePresets;
};
export const setDiceSoNiceForFearFateRoll = async (rollResult, fearFaces) => {
if (!game.modules.get('dice-so-nice')?.active) return;
const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
const diceSoNicePresets = await getDiceSoNicePreset(diceSoNice.fear, fearFaces);
rollResult.dice[0].options = diceSoNicePresets;
};
export const chunkify = (array, chunkSize, mappingFunc) => { export const chunkify = (array, chunkSize, mappingFunc) => {
var chunkifiedArray = []; var chunkifiedArray = [];
for (let i = 0; i < array.length; i += chunkSize) { for (let i = 0; i < array.length; i += chunkSize) {

View file

@ -212,6 +212,7 @@ export async function runMigrations() {
} }
if (foundry.utils.isNewerVersion('1.5.5', lastMigrationVersion)) { if (foundry.utils.isNewerVersion('1.5.5', lastMigrationVersion)) {
/* Clear out Environments that were added directly from compendium */
for (const scene of game.scenes) { for (const scene of game.scenes) {
if (!scene.flags.daggerheart) continue; if (!scene.flags.daggerheart) continue;
const systemData = new game.system.api.data.scenes.DHScene(scene.flags.daggerheart); const systemData = new game.system.api.data.scenes.DHScene(scene.flags.daggerheart);
@ -226,6 +227,25 @@ export async function runMigrations() {
lastMigrationVersion = '1.5.5'; lastMigrationVersion = '1.5.5';
} }
if (foundry.utils.isNewerVersion('1.6.0', lastMigrationVersion)) {
/* Delevel any companions that are higher level than their partner character */
for (const companion of game.actors.filter(x => x.type === 'companion')) {
if (companion.system.levelData.level.current <= 1) continue;
if (!companion.system.partner) {
await companion.updateLevel(1);
} else {
const endLevel = companion.system.partner.system.levelData.level.current;
if (endLevel < companion.system.levelData.level.current) {
companion.system.levelData.level.changed = companion.system.levelData.level.current;
await companion.updateLevel(endLevel);
}
}
}
lastMigrationVersion = '1.6.0';
}
//#endregion //#endregion
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion);

View file

@ -235,7 +235,51 @@
}, },
"_id": "2ESeh4tPhr6DI5ty", "_id": "2ESeh4tPhr6DI5ty",
"img": "icons/magic/death/skull-horned-worn-fire-blue.webp", "img": "icons/magic/death/skull-horned-worn-fire-blue.webp",
"effects": [], "effects": [
{
"name": "Depths Of Despair",
"type": "base",
"system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
},
"_id": "nofxm1vGZ2TmceA2",
"img": "icons/magic/death/skull-horned-worn-fire-blue.webp",
"changes": [
{
"key": "system.rules.attack.damage.hpDamageMultiplier",
"mode": 5,
"value": "2",
"priority": null
}
],
"disabled": true,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
},
"description": "<p><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">The </span><span style=\"box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgb(93, 20, 43) rgba(0, 0, 0, 0);font-family:Montserrat, sans-serif;color:rgb(239, 230, 216);font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial\">Demon of Despair</span><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\"> deals double damage to PCs with 0 Hope.</span></p>",
"origin": null,
"tint": "#ffffff",
"transfer": true,
"statuses": [],
"sort": 0,
"flags": {},
"_stats": {
"compendiumSource": null
},
"_key": "!actors.items.effects!kE4dfhqmIQpNd44e.2ESeh4tPhr6DI5ty.nofxm1vGZ2TmceA2"
}
],
"folder": null, "folder": null,
"sort": 0, "sort": 0,
"ownership": { "ownership": {

View file

@ -304,7 +304,51 @@
}, },
"_id": "1fE6xo8yIOmZkGNE", "_id": "1fE6xo8yIOmZkGNE",
"img": "icons/skills/melee/strike-slashes-orange.webp", "img": "icons/skills/melee/strike-slashes-orange.webp",
"effects": [], "effects": [
{
"name": "Overwhelm",
"type": "base",
"system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
},
"_id": "eGB9G0ljYCcdGbOx",
"img": "icons/skills/melee/strike-slashes-orange.webp",
"changes": [
{
"key": "system.rules.attack.damage.hpDamageMultiplier",
"mode": 5,
"value": "2",
"priority": null
}
],
"disabled": true,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
},
"description": "<p><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">When a target the </span><span style=\"box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgb(93, 20, 43) rgba(0, 0, 0, 0);font-family:Montserrat, sans-serif;color:rgb(239, 230, 216);font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial\">Failed Experiment</span><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\"> attacks has other adversaries within Very Close range, the </span><span style=\"box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgb(93, 20, 43) rgba(0, 0, 0, 0);font-family:Montserrat, sans-serif;color:rgb(239, 230, 216);font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial\">Failed Experiment</span><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\"> deals double damage.</span></p>",
"origin": null,
"tint": "#ffffff",
"transfer": true,
"statuses": [],
"sort": 0,
"flags": {},
"_stats": {
"compendiumSource": null
},
"_key": "!actors.items.effects!ChwwVqowFw8hJQwT.1fE6xo8yIOmZkGNE.eGB9G0ljYCcdGbOx"
}
],
"folder": null, "folder": null,
"sort": 0, "sort": 0,
"ownership": { "ownership": {

View file

@ -229,7 +229,51 @@
}, },
"_id": "FGJTAeL38zTVd4fA", "_id": "FGJTAeL38zTVd4fA",
"img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp",
"effects": [], "effects": [
{
"name": "Punish the Guilty",
"type": "base",
"system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
},
"_id": "ID85zoIa5GfhNMti",
"img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp",
"changes": [
{
"key": "system.rules.attack.damage.hpDamageMultiplier",
"mode": 5,
"value": "2",
"priority": null
}
],
"disabled": true,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
},
"description": "<p><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">The </span><span style=\"box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgb(93, 20, 43) rgba(0, 0, 0, 0);font-family:Montserrat, sans-serif;color:rgb(239, 230, 216);font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial\">Hallowed Archer</span><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\"> deals double damage to targets marked Guilty by a High Seraph.</span></p>",
"origin": null,
"tint": "#ffffff",
"transfer": true,
"statuses": [],
"sort": 0,
"flags": {},
"_stats": {
"compendiumSource": null
},
"_key": "!actors.items.effects!kabueAo6BALApWqp.FGJTAeL38zTVd4fA.ID85zoIa5GfhNMti"
}
],
"folder": null, "folder": null,
"sort": 0, "sort": 0,
"ownership": { "ownership": {

View file

@ -336,7 +336,14 @@
"range": "melee" "range": "melee"
} }
}, },
"changes": [], "changes": [
{
"key": "system.rules.attack.damage.hpDamageTakenMultiplier",
"mode": 5,
"value": "2",
"priority": null
}
],
"disabled": false, "disabled": false,
"duration": { "duration": {
"startTime": null, "startTime": null,
@ -350,8 +357,8 @@
"description": "", "description": "",
"tint": "#ffffff", "tint": "#ffffff",
"statuses": [ "statuses": [
"restrained", "vulnerable",
"vulnerable" "restrained"
], ],
"sort": 0, "sort": 0,
"flags": {}, "flags": {},

View file

@ -230,7 +230,51 @@
"subType": null, "subType": null,
"originId": null "originId": null
}, },
"effects": [], "effects": [
{
"name": "Opportunist",
"type": "base",
"system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
},
"_id": "O03vYbyNLO3YPZGo",
"img": "icons/skills/targeting/crosshair-triple-strike-orange.webp",
"changes": [
{
"key": "system.rules.attack.damage.hpDamageMultiplier",
"mode": 5,
"value": "2",
"priority": null
}
],
"disabled": true,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
},
"description": "<p><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">When two or more adversaries are within Very Close range of a creature, all damage the </span><span style=\"box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgb(93, 20, 43) rgba(0, 0, 0, 0);font-family:Montserrat, sans-serif;color:rgb(239, 230, 216);font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial\">Skeleton Archer</span><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\"> deals to that creature is doubled.</span></p>",
"origin": null,
"tint": "#ffffff",
"transfer": true,
"statuses": [],
"sort": 0,
"flags": {},
"_stats": {
"compendiumSource": null
},
"_key": "!actors.items.effects!7X5q7a6ueeHs5oA9.6mL2FQ9pQdfoDNzG.O03vYbyNLO3YPZGo"
}
],
"folder": null, "folder": null,
"sort": 0, "sort": 0,
"ownership": { "ownership": {

View file

@ -9,13 +9,47 @@
"resource": { "resource": {
"type": "simple", "type": "simple",
"value": 0, "value": 0,
"max": "", "max": "@system.levelData.level.current",
"icon": "", "icon": "fa-solid fa-water",
"recovery": null, "recovery": "session",
"diceStates": {}, "diceStates": {},
"dieFaces": "d4" "dieFaces": "d4"
}, },
"actions": {}, "actions": {
"tFlus34KotJjHfTe": {
"type": "effect",
"_id": "tFlus34KotJjHfTe",
"systemPath": "actions",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"triggers": [
{
"trigger": "fearRoll",
"triggeringActorType": "self",
"command": "const { max, value } = this.item.system.resource;\nconst maxValue = actor.system.levelData.level.current;\nconst afterUpdate = value+1;\nif (afterUpdate > maxValue) return;\n\nui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.knowTheTide'));\nreturn { updates: [{\n key: 'resource',\n itemId: this.item.id,\n target: this.item,\n value: 1,\n}]};"
}
],
"cost": [],
"uses": {
"value": null,
"max": "",
"recovery": null,
"consumeOnSuccess": false
},
"effects": [],
"target": {
"type": "any",
"amount": null
},
"name": "Know The Tide",
"range": ""
}
},
"originItemType": null, "originItemType": null,
"subType": null, "subType": null,
"originId": null, "originId": null,

View file

@ -1,7 +1,7 @@
{ {
"name": "Consumables", "name": "Consumables",
"img": "icons/consumables/potions/bottle-corked-red.webp", "img": "icons/consumables/potions/bottle-corked-red.webp",
"description": "<p>To generate a random consumable, choose a rarity, roll the designated dice, and match the total to the item in the table:</p><ul><li><p>Common: 1d12 or 2d12</p></li><li><p>Uncommon: 2d12 or 3d12</p></li><li><p>Rare: 3d12 or 4d12</p></li><li><p>Legendary: 4d12 or 5d12</p></li></ul>", "description": "",
"results": [ "results": [
{ {
"type": "document", "type": "document",
@ -1511,8 +1511,27 @@
"default": 0, "default": 0,
"Bgvu4A6AMkRFOTGR": 3 "Bgvu4A6AMkRFOTGR": 3
}, },
"flags": {}, "flags": {
"formula": "1d60", "daggerheart": {
"activeAltFormula": "",
"formulaName": "Common",
"altFormula": {
"uoUn5fRTUkyg6U2G": {
"name": "Uncommon",
"formula": "3d12"
},
"FGxM2yoxUUUd9Eov": {
"name": "Rare",
"formula": "4d12"
},
"HZ2hRBxu0k8IW0jC": {
"name": "Legendary",
"formula": "5d12"
}
}
}
},
"formula": "2d12",
"_id": "tF04P02yVN1YDVel", "_id": "tF04P02yVN1YDVel",
"sort": 300000, "sort": 300000,
"_key": "!tables!tF04P02yVN1YDVel" "_key": "!tables!tF04P02yVN1YDVel"

View file

@ -1,7 +1,7 @@
{ {
"name": "Loot", "name": "Loot",
"img": "icons/commodities/treasure/brooch-gold-ruby.webp", "img": "icons/commodities/treasure/brooch-gold-ruby.webp",
"description": "<p>To generate a random item, choose a rarity, roll the designated dice, and match the total to the item in the table: </p><ul><li><p> Common: 1d12 or 2d12 </p></li><li><p>Uncommon: 2d12 or 3d12 </p></li><li><p>Rare: 3d12 or 4d12 </p></li><li><p>Legendary: 4d12 or 5d12</p></li></ul>", "description": "",
"results": [ "results": [
{ {
"type": "document", "type": "document",
@ -1511,8 +1511,27 @@
"default": 0, "default": 0,
"Bgvu4A6AMkRFOTGR": 3 "Bgvu4A6AMkRFOTGR": 3
}, },
"flags": {}, "flags": {
"formula": "1d60", "daggerheart": {
"activeAltFormula": "",
"formulaName": "Common",
"altFormula": {
"hJJtajaMk14bYM4X": {
"name": "Uncommon",
"formula": "3d12"
},
"yDVeXdKpG7LzjHWa": {
"name": "Rare",
"formula": "4d12"
},
"qPHNIuUgWAHauI6V": {
"name": "Legendary",
"formula": "5d12"
}
}
}
},
"formula": "2d12",
"_id": "S61Shlt2I5CbLRjz", "_id": "S61Shlt2I5CbLRjz",
"sort": 200000, "sort": 200000,
"_key": "!tables!S61Shlt2I5CbLRjz" "_key": "!tables!S61Shlt2I5CbLRjz"

View file

@ -1,7 +1,7 @@
{ {
"name": "Table of Random Objectives", "name": "Random Objectives",
"img": "icons/sundries/documents/document-torn-diagram-tan.webp", "img": "icons/sundries/documents/document-torn-diagram-tan.webp",
"description": "<p>Layering Goals Other than Attrition into Combat</p>", "description": "",
"results": [ "results": [
{ {
"type": "text", "type": "text",
@ -311,7 +311,20 @@
"default": 0, "default": 0,
"Bgvu4A6AMkRFOTGR": 3 "Bgvu4A6AMkRFOTGR": 3
}, },
"flags": {}, "flags": {
"daggerheart": {
"formulaName": "Roll Formula",
"altFormula": {},
"activeAltFormula": null,
"flags": {
"daggerheart": {
"formulaName": "Roll Formula",
"altFormula": {},
"activeAltFormula": null
}
}
}
},
"formula": "1d12", "formula": "1d12",
"_id": "I5L1dlgxXTNrCCkL", "_id": "I5L1dlgxXTNrCCkL",
"sort": 400000, "sort": 400000,

View file

@ -16,7 +16,51 @@
"artist": "" "artist": ""
} }
}, },
"effects": [], "effects": [
{
"name": "Advanced Training",
"type": "base",
"system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
},
"_id": "bKOuMxhB2Jth3j2T",
"img": "icons/creatures/mammals/wolf-howl-moon-gray.webp",
"changes": [
{
"key": "system.companionData.levelupChoices",
"mode": 2,
"value": "2",
"priority": null
}
],
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
},
"description": "<p><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">Choose two additional level-up options for your companion.</span></p>",
"origin": null,
"tint": "#ffffff",
"transfer": true,
"statuses": [],
"sort": 0,
"flags": {},
"_stats": {
"compendiumSource": null
},
"_key": "!items.effects!uGcs785h94RMtueH.bKOuMxhB2Jth3j2T"
}
],
"sort": 0, "sort": 0,
"ownership": { "ownership": {
"default": 0, "default": 0,

View file

@ -16,7 +16,51 @@
"artist": "" "artist": ""
} }
}, },
"effects": [], "effects": [
{
"name": "Expert Training",
"type": "base",
"system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
},
"_id": "rknTONvaUDZ2Yz1W",
"img": "icons/creatures/mammals/dog-husky-white-blue.webp",
"changes": [
{
"key": "system.companionData.levelupChoices",
"mode": 2,
"value": "1",
"priority": null
}
],
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
},
"description": "<p><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">Choose an additional level-up option for your companion.</span></p>",
"origin": null,
"tint": "#ffffff",
"transfer": true,
"statuses": [],
"sort": 0,
"flags": {},
"_stats": {
"compendiumSource": null
},
"_key": "!items.effects!iCXtOWBKv1FdKdWz.rknTONvaUDZ2Yz1W"
}
],
"sort": 0, "sort": 0,
"ownership": { "ownership": {
"default": 0, "default": 0,

View file

@ -0,0 +1,27 @@
.daggerheart.dh-style.dialog.views.character-reset {
.character-reset-container {
display: flex;
flex-direction: column;
gap: 8px;
legend {
padding: 0 4px;
}
.character-reset-header {
font-size: var(--font-size-18);
text-align: center;
}
.reset-data-container {
display: grid;
grid-template-columns: 3fr 2fr;
align-items: center;
gap: 4px;
label {
font-weight: bold;
}
}
}
}

View file

@ -1,55 +1,56 @@
@import '../../utils/spacing.less'; @import '../../utils/spacing.less';
@import '../../utils/colors.less'; @import '../../utils/colors.less';
@import '../../utils/fonts.less'; @import '../../utils/fonts.less';
.daggerheart.dh-style.dialog.death-move { .daggerheart.dh-style.dialog.death-move {
.death-move-container { .death-move-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 5px; gap: 5px;
.moves-list { .moves-list {
.move-item { .move-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
padding: 5px;
&:hover { border-radius: 5px;
background-color: light-dark(@soft-shadow, @soft-white-shadow); transition: background-color 0.3s ease-in-out;
cursor: pointer; height: 37px;
}
padding: 5px; &:hover {
border-radius: 5px; background-color: light-dark(@soft-shadow, @soft-white-shadow);
transition: background-color 0.3s ease-in-out; cursor: pointer;
}
.label {
display: flex; .label {
align-items: center; display: flex;
gap: 10px; align-items: center;
cursor: pointer; gap: 10px;
flex: 1; cursor: pointer;
i { flex: 1;
text-align: center; i {
width: 30px; text-align: center;
} width: 30px;
} }
}
input[type='radio'] {
margin-left: auto; input[type='radio'] {
} margin-left: auto;
} }
} }
} }
}
footer {
margin-top: 8px; footer {
display: flex; margin-top: 8px;
gap: 8px; display: flex;
gap: 8px;
button {
flex: 1; button {
height: 40px; flex: 1;
font-weight: 600; height: 40px;
} font-weight: 600;
} }
} }
}

View file

@ -38,4 +38,8 @@
@import './item-transfer/sheet.less'; @import './item-transfer/sheet.less';
@import './settings/change-currency-icon.less'; @import './settings/change-currency-icon.less';
@import './risk-it-all/sheet.less';
@import './character-reset/sheet.less';

View file

@ -0,0 +1,60 @@
.daggerheart.dialog.dh-style.views.risk-it-all {
.risk-it-all-container {
display: flex;
align-items: center;
flex-direction: column;
gap: 8px;
text-align: center;
header {
font-weight: bold;
font-size: var(--font-size-20);
}
.section-label {
font-size: var(--font-size-18);
text-decoration: underline;
}
.remaining-section {
display: flex;
flex-direction: column;
gap: 2px;
}
.resource-section {
width: 100%;
display: flex;
gap: 8px;
}
.final-section {
width: 100%;
display: flex;
flex-direction: column;
gap: 2px;
.final-section-values-container {
width: 100%;
display: flex;
align-items: center;
justify-content: space-evenly;
.final-section-value-container {
display: flex;
flex-direction: column;
gap: 2px;
}
}
}
}
footer {
width: 100%;
display: flex;
button {
flex: 1;
}
}
}

View file

@ -15,6 +15,14 @@
.message-header .message-header-main .message-sub-header-container h4 { .message-header .message-header-main .message-sub-header-container h4 {
color: @dark-blue; color: @dark-blue;
} }
.message-content {
.table-draw {
.table-description {
color: @dark;
}
}
}
} }
} }
@ -83,6 +91,7 @@
.message-content { .message-content {
padding-bottom: 8px; padding-bottom: 8px;
.flavor-text { .flavor-text {
font-size: var(--font-size-12); font-size: var(--font-size-12);
line-height: 20px; line-height: 20px;
@ -90,6 +99,33 @@
text-align: center; text-align: center;
display: block; display: block;
} }
.table-draw {
.table-flavor {
padding-top: 5px;
padding-bottom: 0.5rem;
font-size: var(--font-size-12);
}
.table-description {
color: @beige;
font-style: italic;
&.flavor-spaced {
padding-top: 0;
}
}
.table-results {
.description {
flex-basis: min-content;
> p:first-of-type {
margin-top: 0;
}
}
}
}
} }
} }
} }

View file

@ -1,5 +1,6 @@
.measured-template-button, .measured-template-button,
.enriched-damage-button, .enriched-damage-button,
.fate-roll-button,
.duality-roll-button { .duality-roll-button {
display: inline; display: inline;

View file

@ -24,13 +24,13 @@
font-weight: bold; font-weight: bold;
} }
} }
}
.clown-car img { .clown-car img {
transition: 0.5s; transition: 0.5s;
&.flipped { &.flipped {
transform: scaleX(-1); transform: scaleX(-1);
}
} }
} }

View file

@ -195,6 +195,11 @@
.hope-value { .hope-value {
display: flex; display: flex;
cursor: pointer; cursor: pointer;
&.scar {
cursor: initial;
opacity: 0.6;
}
} }
} }
} }

View file

@ -207,7 +207,7 @@
.input-section { .input-section {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; gap: 8px;
} }
} }

View file

@ -40,4 +40,5 @@
@import './items/heritage.less'; @import './items/heritage.less';
@import './items/item-sheet-shared.less'; @import './items/item-sheet-shared.less';
@import './rollTables/sheet.less';
@import './actions/actions.less'; @import './actions/actions.less';

View file

@ -0,0 +1,29 @@
.application.sheet.roll-table-sheet {
.formulas-section {
legend {
margin-left: auto;
margin-right: auto;
}
.formulas-container {
display: grid;
grid-template-columns: 1fr 1fr 40px;
gap: 10px;
text-align: center;
.formula-button {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.roll-table-view-formula-container {
width: fit-content;
display: flex;
align-items: center;
gap: 4px;
}
}

View file

@ -14,6 +14,7 @@
color: @dark; color: @dark;
} }
&.fate,
&.duality { &.duality {
background-image: url(../assets/parchments/dh-parchment-dark.png); background-image: url(../assets/parchments/dh-parchment-dark.png);
@ -66,6 +67,7 @@
} }
} }
&.fate,
&.critical { &.critical {
--text-color: @chat-purple; --text-color: @chat-purple;
--bg-color: @chat-purple-40; --bg-color: @chat-purple-40;
@ -80,7 +82,7 @@
} }
} }
&:not(.duality) { &:not(.duality .fate) {
.font-20 { .font-20 {
color: @dark; color: @dark;
} }
@ -173,6 +175,28 @@
} }
} }
&.fate {
&.hope {
--text-color: @golden;
--bg-color: @golden-40;
.message-header,
.message-content {
background-color: @golden-bg;
}
}
&.fear {
--text-color: @chat-blue;
--bg-color: @chat-blue-40;
.message-header,
.message-content {
background-color: @chat-blue-bg;
}
}
}
&.duality { &.duality {
&.hope { &.hope {
--text-color: @golden; --text-color: @golden;

View file

@ -0,0 +1,152 @@
@import '../../utils/colors.less';
@import '../../utils/fonts.less';
@import '../../utils/spacing.less';
#interface.theme-light {
.daggerheart.chat.death-moves {
.death-moves-list .death-move {
&:hover {
background: @dark-blue-10;
}
.death-label {
border-bottom: 1px solid @dark-blue;
.header-label .title {
color: @dark-blue;
}
.header-label .label {
color: @dark;
}
}
.fa-chevron-down {
color: @dark-blue;
}
}
.description {
color: @dark;
}
.result {
color: @dark;
}
.risk-it-all-button {
color: @dark;
}
}
}
.daggerheart.chat {
&.death-moves {
display: flex;
flex-direction: column;
align-items: center;
details[open] {
.fa-chevron-down {
transform: rotate(180deg);
transition: all 0.3s ease;
}
}
.death-moves-list {
display: flex;
flex-direction: column;
gap: 5px;
width: 100%;
.fa-chevron-down {
transition: all 0.3s ease;
margin-left: auto;
}
.death-move {
width: 100%;
.death-label {
display: flex;
align-items: center;
gap: 5px;
border-bottom: 1px solid @golden;
margin: 0 8px;
padding-bottom: 5px;
width: -webkit-fill-available;
&:hover {
background: light-dark(@dark-blue-10, @golden-10);
cursor: pointer;
transition: all 0.3s ease;
}
.death-image {
width: 40px;
height: 40px;
border-radius: 3px;
}
.header-label {
padding: 8px;
.title {
font-size: var(--font-size-16);
color: @golden;
font-weight: 700;
}
.label {
font-size: var(--font-size-12);
color: @beige;
margin: 0;
}
}
}
.description {
padding: 8px;
}
}
.action-use-button-parent {
width: 100%;
.action-use-target {
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
width: 100%;
padding: 4px 8px 10px 40px;
font-size: var(--font-size-12);
label {
font-weight: bold;
}
select {
flex: 1;
}
}
}
.action-use-button {
width: -webkit-fill-available;
margin: 0 8px;
font-weight: 600;
height: 40px;
}
}
.result {
padding: 8px;
font-weight: bold;
}
.risk-it-all-button {
width: -webkit-fill-available;
margin: 0 8px;
font-weight: 600;
height: 40px;
}
}
}

View file

@ -6,6 +6,7 @@
@import './chat/effect-summary.less'; @import './chat/effect-summary.less';
@import './chat/group-roll.less'; @import './chat/group-roll.less';
@import './chat/refresh-message.less'; @import './chat/refresh-message.less';
@import './chat/deathmoves.less';
@import './chat/sheet.less'; @import './chat/sheet.less';
@import './combat-sidebar/combat-sidebar.less'; @import './combat-sidebar/combat-sidebar.less';

View file

@ -2,7 +2,7 @@
"id": "daggerheart", "id": "daggerheart",
"title": "Daggerheart", "title": "Daggerheart",
"description": "An unofficial implementation of the Daggerheart system", "description": "An unofficial implementation of the Daggerheart system",
"version": "1.5.5", "version": "1.6.0",
"compatibility": { "compatibility": {
"minimum": "13.346", "minimum": "13.346",
"verified": "13.351", "verified": "13.351",
@ -285,6 +285,7 @@
}, },
"ChatMessage": { "ChatMessage": {
"dualityRoll": {}, "dualityRoll": {},
"fateRoll": {},
"adversaryRoll": {}, "adversaryRoll": {},
"damageRoll": {}, "damageRoll": {},
"abilityUse": {}, "abilityUse": {},

View file

@ -0,0 +1,33 @@
<div>
<div class="character-reset-container">
<div class="character-reset-header">{{localize "DAGGERHEART.APPLICATIONS.CharacterReset.headerTitle"}}</div>
<fieldset>
<legend>{{localize "DAGGERHEART.APPLICATIONS.CharacterReset.alwaysDeleteSection"}} <i class="fa-solid fa-lock"></i></legend>
<div class="reset-data-wrapper two-columns even">
{{#each this.data.delete as | data key|}}
<div class="reset-data-container">
<label>{{localize data.label}}</label>
<input type="checkbox" {{checked data.keep}} disabled />
</div>
{{/each}}
</div>
</fieldset>
<fieldset>
<legend>{{localize "DAGGERHEART.APPLICATIONS.CharacterReset.optionalDeleteSection"}}</legend>
<div class="reset-data-wrapper two-columns even">
{{#each this.data.optional as | data key|}}
<div class="reset-data-container">
<label>{{localize data.label}}</label>
<input type="checkbox" name="{{concat "data.optional." key ".keep"}}" {{checked data.keep}} />
</div>
{{/each}}
</div>
</fieldset>
<button type="button" data-action="finishSelection">{{localize "Reset"}}</button>
</div>
</div>

View file

@ -68,95 +68,127 @@
{{/if}} {{/if}}
{{/if}} {{/if}}
{{/if}} {{/if}}
{{#if (eq @root.rollType 'FateRoll')}}
{{#if (eq @root.roll.fateDie 'Hope')}}
<div class="dice-option">
<img class="dice-icon" src="{{concat 'systems/daggerheart/assets/icons/dice/hope/' @root.roll.dHope.denomination '.svg'}}" alt="">
<div class="dice-select">
<span class="label">{{localize "DAGGERHEART.GENERAL.hope"}}</span>
<select name="roll.dice.dHope">
{{selectOptions diceOptions selected=@root.roll.dHope.denomination}}
</select>
</div>
</div>
{{/if}}
{{#if (eq @root.roll.fateDie 'Fear')}}
<div class="dice-option">
<img class="dice-icon" src="{{concat 'systems/daggerheart/assets/icons/dice/fear/' @root.roll.dFear.denomination '.svg'}}" alt="">
<div class="dice-select">
<span class="label">{{localize "DAGGERHEART.GENERAL.fear"}}</span>
<select name="roll.dice.dFear">
{{selectOptions diceOptions selected=@root.roll.dFear.denomination}}
</select>
</div>
</div>
{{/if}}
{{/if}}
</div> </div>
{{#if hasSelectedEffects}} {{#if (ne @root.rollType 'FateRoll')}}
<fieldset class="experience-container"> {{#if hasSelectedEffects}}
<legend>{{localize "DAGGERHEART.GENERAL.Effect.plural"}}</legend> <fieldset class="experience-container">
<legend>{{localize "DAGGERHEART.GENERAL.Effect.plural"}}</legend>
{{#each selectedEffects as |effect id|}} {{#each selectedEffects as |effect id|}}
<div class="experience-chip {{#if effect.selected}}selected{{/if}}" data-action="toggleSelectedEffect" data-key="{{id}}" data-tooltip="{{this.description}}"> <div class="experience-chip {{#if effect.selected}}selected{{/if}}" data-action="toggleSelectedEffect" data-key="{{id}}" data-tooltip="{{this.description}}">
<span><i class="{{ifThen effect.selected "fa-solid" "fa-regular"}} fa-circle"></i></span> <span><i class="{{ifThen effect.selected "fa-solid" "fa-regular"}} fa-circle"></i></span>
<span class="label">{{effect.name}}</span> <span class="label">{{effect.name}}</span>
</div>
{{/each}}
</fieldset>
{{/if}}
{{#if experiences.length}}
<fieldset class="experience-container">
<legend>{{localize "DAGGERHEART.GENERAL.experience.plural"}}</legend>
{{#each experiences}}
{{#if name}}
<div class="experience-chip {{#if (includes ../selectedExperiences id)}}selected{{/if}}" data-action="selectExperience" data-key="{{id}}" data-tooltip="{{this.description}}">
<span><i class="{{ifThen (includes ../selectedExperiences id) "fa-solid" "fa-regular"}} fa-circle"></i></span>
<span class="label">{{name}} +{{value}}</span>
</div> </div>
{{/if}} {{/each}}
{{/each}} </fieldset>
</fieldset> {{/if}}
{{/if}}
<fieldset class="modifier-container {{#if (eq @root.rollType 'DualityRoll')}}two-columns{{else}}one-column{{/if}}"> {{#if experiences.length}}
<legend>{{localize "DAGGERHEART.GENERAL.Modifier.plural"}}</legend> <fieldset class="experience-container">
<div class="nest-inputs"> <legend>{{localize "DAGGERHEART.GENERAL.experience.plural"}}</legend>
<button class="advantage-chip flex1 {{#if (eq advantage 1)}}selected{{/if}}" data-action="updateIsAdvantage" data-advantage="1"> {{#each experiences}}
{{#if (eq advantage 1)}} {{#if name}}
<span><i class="fa-solid fa-circle"></i></span> <div class="experience-chip {{#if (includes ../selectedExperiences id)}}selected{{/if}}" data-action="selectExperience" data-key="{{id}}" data-tooltip="{{this.description}}">
{{else}} <span><i class="{{ifThen (includes ../selectedExperiences id) "fa-solid" "fa-regular"}} fa-circle"></i></span>
<span><i class="fa-regular fa-circle"></i></span> <span class="label">{{name}} +{{value}}</span>
{{/if}} </div>
<span class="label">{{localize "DAGGERHEART.GENERAL.Advantage.full"}}</span> {{/if}}
{{#if @root.rollConfig.data.advantageSources.length}} {{/each}}
<span class="advantage-chip-tooltip" data-tooltip="{{concat "#advantage#" @root.rollConfig.source.actor}}"><i class="fa-solid fa-circle-info"></i></span> </fieldset>
{{/if}} {{/if}}
</button> <fieldset class="modifier-container {{#if (eq @root.rollType 'DualityRoll')}}two-columns{{else}}one-column{{/if}}">
<button class="disadvantage-chip flex1 {{#if (eq advantage -1)}}selected{{/if}}" data-action="updateIsAdvantage" data-advantage="-1"> {{#if @root.advantage}}
{{#if (eq advantage -1)}} <legend>{{localize "DAGGERHEART.GENERAL.Modifier.plural"}}</legend>
<span><i class="fa-solid fa-circle"></i></span>
{{else}}
<span><i class="fa-regular fa-circle"></i></span>
{{/if}}
<span class="label">{{localize "DAGGERHEART.GENERAL.Disadvantage.full"}}</span>
{{#if @root.rollConfig.data.disadvantageSources.length}}
<span class="advantage-chip-tooltip" data-tooltip="{{concat "#disadvantage#" @root.rollConfig.source.actor}}"><i class="fa-solid fa-circle-info"></i></span>
{{/if}}
</button>
</div>
{{#unless (eq @root.rollType 'D20Roll')}}
<div class="nest-inputs"> <div class="nest-inputs">
<select name="roll.dice.advantageNumber"{{#unless advantage}} disabled{{/unless}}> <button class="advantage-chip flex1 {{#if (eq advantage 1)}}selected{{/if}}" data-action="updateIsAdvantage" data-advantage="1">
{{#times 10}} {{#if (eq advantage 1)}}
<option value="{{add this 1}}" {{#if (eq @root.roll.advantageNumber (add this 1))}} selected{{/if}}>{{add this 1}}</option> <span><i class="fa-solid fa-circle"></i></span>
{{/times}} {{else}}
</select> <span><i class="fa-regular fa-circle"></i></span>
<select name="roll.dice.advantageFaces"{{#unless advantage}} disabled{{/unless}}> {{/if}}
{{selectOptions diceOptions selected=@root.roll.dAdvantage.denomination}} <span class="label">{{localize "DAGGERHEART.GENERAL.Advantage.full"}}</span>
</select> {{#if @root.rollConfig.data.advantageSources.length}}
<span class="advantage-chip-tooltip" data-tooltip="{{concat "#advantage#" @root.rollConfig.source.actor}}"><i class="fa-solid fa-circle-info"></i></span>
{{/if}}
</button>
<button class="disadvantage-chip flex1 {{#if (eq advantage -1)}}selected{{/if}}" data-action="updateIsAdvantage" data-advantage="-1">
{{#if (eq advantage -1)}}
<span><i class="fa-solid fa-circle"></i></span>
{{else}}
<span><i class="fa-regular fa-circle"></i></span>
{{/if}}
<span class="label">{{localize "DAGGERHEART.GENERAL.Disadvantage.full"}}</span>
{{#if @root.rollConfig.data.disadvantageSources.length}}
<span class="advantage-chip-tooltip" data-tooltip="{{concat "#disadvantage#" @root.rollConfig.source.actor}}"><i class="fa-solid fa-circle-info"></i></span>
{{/if}}
</button>
</div> </div>
{{#if abilities}} {{#unless (eq @root.rollType 'D20Roll')}}
<span>{{localize "DAGGERHEART.GENERAL.traitModifier"}}</span> <div class="nest-inputs">
<select name="trait"> <select name="roll.dice.advantageNumber"{{#unless advantage}} disabled{{/unless}}>
{{selectOptions abilities selected=@root.rollConfig.roll.trait valueAttr="id" labelAttr="label" blank="" localize=true}} {{#times 10}}
<option value="{{add this 1}}" {{#if (eq @root.roll.advantageNumber (add this 1))}} selected{{/if}}>{{add this 1}}</option>
{{/times}}
</select>
<select name="roll.dice.advantageFaces"{{#unless advantage}} disabled{{/unless}}>
{{selectOptions diceOptions selected=@root.roll.dAdvantage.denomination}}
</select>
</div>
{{#if abilities}}
<span>{{localize "DAGGERHEART.GENERAL.traitModifier"}}</span>
<select name="trait">
{{selectOptions abilities selected=@root.rollConfig.roll.trait valueAttr="id" labelAttr="label" blank="" localize=true}}
</select>
{{/if}}
{{/unless}}
{{/if}}
{{#if @root.rallyDie.length}}
<span class="formula-label">{{localize "DAGGERHEART.CLASS.Feature.rallyDice"}}</span>
<select name="roll.dice._rallyIndex">
{{selectOptions @root.rallyDie blank="" selected=@root.roll._rallyIndex}}
</select> </select>
{{/if}} {{/if}}
{{/unless}} {{#if (eq @root.rollType 'DualityRoll')}}<span class="formula-label">{{localize "DAGGERHEART.GENERAL.situationalBonus"}}</span>{{/if}}
{{#if @root.rallyDie.length}} <input type="text" value="{{extraFormula}}" name="extraFormula" placeholder="{{#if (eq @root.rollType 'DualityRoll')}}Ex: 1d6 + 5{{else}}Situational Bonus{{/if}}">
<span class="formula-label">{{localize "DAGGERHEART.CLASS.Feature.rallyDice"}}</span> </fieldset>
<select name="roll.dice._rallyIndex"> {{/if}}
{{selectOptions @root.rallyDie blank="" selected=@root.roll._rallyIndex}}
</select>
{{/if}}
{{#if (eq @root.rollType 'DualityRoll')}}<span class="formula-label">{{localize "DAGGERHEART.GENERAL.situationalBonus"}}</span>{{/if}}
<input type="text" value="{{extraFormula}}" name="extraFormula" placeholder="{{#if (eq @root.rollType 'DualityRoll')}}Ex: 1d6 + 5{{else}}Situational Bonus{{/if}}">
</fieldset>
{{/unless}} {{/unless}}
{{#if (or costs uses)}} {{#if (or costs uses)}}
{{> 'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs'}} {{> 'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs'}}
{{/if}} {{/if}}
<span class="formula-label"><b>{{localize "DAGGERHEART.GENERAL.formula"}}:</b> {{@root.formula}}</span> {{#if (ne @root.rollType 'FateRoll')}}
<span class="formula-label"><b>{{localize "DAGGERHEART.GENERAL.formula"}}:</b> {{@root.formula}}</span>
{{/if}}
<div class="roll-dialog-controls"> <div class="roll-dialog-controls">
<select class="roll-mode-select" name="selectedRollMode"> <select class="roll-mode-select" name="selectedRollMode">

View file

@ -0,0 +1,39 @@
<div>
<div class="risk-it-all-container">
<header>{{localize "DAGGERHEART.APPLICATIONS.RiskItAllDialog.subtitle"}}</header>
<div class="remaining-section">
<label class="section-label">{{localize "DAGGERHEART.APPLICATIONS.RiskItAllDialog.remainingTitle"}}</label>
<div>{{this.remainingResource}}</div>
</div>
<div class="resource-section">
<div class="resource-container">
<label>{{localize "DAGGERHEART.APPLICATIONS.RiskItAllDialog.clearResource" resource=(localize "DAGGERHEART.GENERAL.HitPoints.short")}}: {{this.choices.hitPoints}}</label>
<input type="range" step="1" min="0" max="{{this.maxHitPointsValue}}" value="{{this.choices.hitPoints}}" name="choices.hitPoints" data-choice="hitPoints" />
</div>
<div class="resource-container">
<label>{{localize "DAGGERHEART.APPLICATIONS.RiskItAllDialog.clearResource" resource=(localize "DAGGERHEART.GENERAL.stress")}}: {{this.choices.stress}}</label>
<input type="range" step="1" min="0" max="{{this.maxStressValue}}" value="{{this.choices.stress}}" name="choices.stress" data-choice="stress" />
</div>
</div>
<div class="final-section">
<label class="section-label">{{localize "DAGGERHEART.APPLICATIONS.RiskItAllDialog.finalTitle"}}</label>
<div class="final-section-values-container">
<div class="final-section-value-container">
<label>{{localize "DAGGERHEART.GENERAL.HitPoints.plural"}}</label>
<span>{{this.final.hitPoints.value}}/{{this.final.hitPoints.max}}</span>
</div>
<div class="final-section-value-container">
<label>{{localize "DAGGERHEART.GENERAL.stress"}}</label>
<span>{{this.final.stress.value}}/{{this.final.stress.max}}</span>
</div>
</div>
</div>
<footer>
<button type="button" data-action="finish" {{disabled this.unfinished}}>{{localize "Submit"}}</button>
</footer>
</div>
</div>

View file

@ -11,6 +11,11 @@
<button type="button" class="control-icon" data-action="sort" data-direction="down" data-tooltip="HUD.ToBack"> <button type="button" class="control-icon" data-action="sort" data-direction="down" data-tooltip="HUD.ToBack">
<img src="{{icons.down}}"> <img src="{{icons.down}}">
</button> </button>
{{#if hasCompanion}}
<button type="button" class="control-icon clown-car" data-action="toggleCompanions" data-tooltip="{{#if companionOnCanvas}}{{localize "DAGGERHEART.APPLICATIONS.HUD.tokenHUD.retrieveCompanionTokens"}}{{else}}{{localize "DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositCompanionTokens"}}{{/if}}">
<img {{#if companionOnCanvas}}class="flipped"{{/if}} src="{{icons.toggleClowncar}}">
</button>
{{/if}}
{{#if canConfigure}} {{#if canConfigure}}
<button type="button" class="control-icon" data-action="config" data-tooltip="HUD.OpenConfig"> <button type="button" class="control-icon" data-action="config" data-tooltip="HUD.OpenConfig">
@ -76,7 +81,7 @@
{{#if (eq actorType 'party')}} {{#if (eq actorType 'party')}}
<button type="button" class="control-icon clown-car" data-action="togglePartyTokens" data-tooltip="{{#if partyOnCanvas}}{{localize "DAGGERHEART.APPLICATIONS.HUD.tokenHUD.retrievePartyTokens"}}{{else}}{{localize "DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositPartyTokens"}}{{/if}}"> <button type="button" class="control-icon clown-car" data-action="togglePartyTokens" data-tooltip="{{#if partyOnCanvas}}{{localize "DAGGERHEART.APPLICATIONS.HUD.tokenHUD.retrievePartyTokens"}}{{else}}{{localize "DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositPartyTokens"}}{{/if}}">
<img {{#if partyOnCanvas}}class="flipped"{{/if}} src="{{icons.toggleParty}}"> <img {{#if partyOnCanvas}}class="flipped"{{/if}} src="{{icons.toggleClowncar}}">
</button> </button>
{{/if}} {{/if}}

View file

@ -18,7 +18,6 @@
{{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}} {{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}}
{{formGroup settingFields.schema.fields.effects.fields.rangeDependent value=settingFields._source.effects.rangeDependent localize=true}} {{formGroup settingFields.schema.fields.effects.fields.rangeDependent value=settingFields._source.effects.rangeDependent localize=true}}
{{formGroup settingFields.schema.fields.levelupAuto value=settingFields._source.levelupAuto localize=true}} {{formGroup settingFields.schema.fields.levelupAuto value=settingFields._source.levelupAuto localize=true}}
{{formGroup settingFields.schema.fields.playerCanEditSheet value=settingFields._source.playerCanEditSheet localize=true}}
{{formGroup settingFields.schema.fields.damageReductionRulesDefault value=settingFields._source.damageReductionRulesDefault localize=true}} {{formGroup settingFields.schema.fields.damageReductionRulesDefault value=settingFields._source.damageReductionRulesDefault localize=true}}
{{formGroup settingFields.schema.fields.resourceScrollTexts value=settingFields._source.resourceScrollTexts localize=true}} {{formGroup settingFields.schema.fields.resourceScrollTexts value=settingFields._source.resourceScrollTexts localize=true}}
</section> </section>

View file

@ -9,11 +9,12 @@
</legend> </legend>
{{formGroup settingFields.schema.fields.defeated.fields.enabled value=settingFields._source.defeated.enabled localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.enabled value=settingFields._source.defeated.enabled localize=true}}
{{formGroup settingFields.schema.fields.defeated.fields.overlay value=settingFields._source.defeated.overlay localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.overlay value=settingFields._source.defeated.overlay localize=true}}
{{formGroup settingFields.schema.fields.defeated.fields.characterDefault value=settingFields._source.defeated.characterDefault labelAttr="name" localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.characterDefault value=settingFields._source.defeated.characterDefault labelAttr="name" localize=true}}
{{formGroup settingFields.schema.fields.defeated.fields.adversaryDefault value=settingFields._source.defeated.adversaryDefault labelAttr="name" localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.adversaryDefault value=settingFields._source.defeated.adversaryDefault labelAttr="name" localize=true}}
{{formGroup settingFields.schema.fields.defeated.fields.companionDefault value=settingFields._source.defeated.companionDefault labelAttr="name" localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.companionDefault value=settingFields._source.defeated.companionDefault labelAttr="name" localize=true}}
{{formGroup settingFields.schema.fields.defeated.fields.deathMoveIcon value=settingFields._source.defeated.deathMoveIcon localize=true}}
{{formGroup settingFields.schema.fields.defeated.fields.deadIcon value=settingFields._source.defeated.deadIcon localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.deadIcon value=settingFields._source.defeated.deadIcon localize=true}}
{{formGroup settingFields.schema.fields.defeated.fields.defeatedIcon value=settingFields._source.defeated.defeatedIcon localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.defeatedIcon value=settingFields._source.defeated.defeatedIcon localize=true}}
{{formGroup settingFields.schema.fields.defeated.fields.unconsciousIcon value=settingFields._source.defeated.unconsciousIcon localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.unconsciousIcon value=settingFields._source.defeated.unconsciousIcon localize=true}}

View file

@ -8,6 +8,7 @@
<h1>{{localize 'DAGGERHEART.SETTINGS.Menu.homebrew.name'}}</h1> <h1>{{localize 'DAGGERHEART.SETTINGS.Menu.homebrew.name'}}</h1>
</header> </header>
{{formGroup settingFields.schema.fields.maxFear value=settingFields._source.maxFear localize=true}} {{formGroup settingFields.schema.fields.maxFear value=settingFields._source.maxFear localize=true}}
{{formGroup settingFields.schema.fields.maxHope value=settingFields._source.maxHope localize=true}}
{{formGroup settingFields.schema.fields.maxDomains value=settingFields._source.maxDomains localize=true}} {{formGroup settingFields.schema.fields.maxDomains value=settingFields._source.maxDomains localize=true}}
{{formGroup settingFields.schema.fields.maxLoadout value=settingFields._source.maxLoadout localize=true}} {{formGroup settingFields.schema.fields.maxLoadout value=settingFields._source.maxLoadout localize=true}}
<div class="settings-hint"><label>{{localize "DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxLoadout.hint"}}</label></div> <div class="settings-hint"><label>{{localize "DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxLoadout.hint"}}</label></div>

View file

@ -11,6 +11,6 @@
</fieldset> </fieldset>
<fieldset class="action-category"> <fieldset class="action-category">
<legend>{{localize "DAGGERHEART.GENERAL.description"}}</legend> <legend>{{localize "DAGGERHEART.GENERAL.description"}}</legend>
{{formInput fields.description value=source.description enriched=source.description name="description" toggled=true }} {{formInput fields.description value=source.description enriched=action.description name="description" toggled=true }}
</fieldset> </fieldset>
</section> </section>

View file

@ -30,7 +30,7 @@
</div> </div>
<div class="code-mirror-wrapper {{#if trigger.revealed}}revealed{{/if}}"> <div class="code-mirror-wrapper {{#if trigger.revealed}}revealed{{/if}}">
{{formInput @root.fields.triggers.element.fields.command value=trigger.command elementType="code-mirror" name=(concat "triggers." index ".command") aria=(object label=(localize "Test")) }} {{formInput @root.fields.triggers.element.fields.command value=trigger.command elementType="code-mirror" name=(concat "triggers." index ".command") }}
</div> </div>
</fieldset> </fieldset>
{{/each}} {{/each}}

View file

@ -31,7 +31,7 @@
{{formGroup systemFields.resources.fields.stress.fields.max value=document._source.system.resources.stress.max localize=true}} {{formGroup systemFields.resources.fields.stress.fields.max value=document._source.system.resources.stress.max localize=true}}
{{formGroup systemFields.resources.fields.hope.fields.value value=document._source.system.resources.hope.value localize=true}} {{formGroup systemFields.resources.fields.hope.fields.value value=document._source.system.resources.hope.value localize=true}}
{{formGroup systemFields.resources.fields.hope.fields.max value=document._source.system.resources.hope.max localize=true}} {{formGroup systemFields.scars value=document._source.system.scars localize=true}}
{{formGroup systemFields.proficiency value=document._source.system.proficiency localize=true}} {{formGroup systemFields.proficiency value=document._source.system.proficiency localize=true}}
<span data-tooltip-text="{{localize "DAGGERHEART.UI.Tooltip.maxEvasionClassBound"}}"> <span data-tooltip-text="{{localize "DAGGERHEART.UI.Tooltip.maxEvasionClassBound"}}">

View file

@ -76,6 +76,11 @@
{{/if}} {{/if}}
</span> </span>
{{/times}} {{/times}}
{{#times document.system.scars}}
<span class='hope-value scar'>
<i class='fa-regular fa-ban'></i>
</span>
{{/times}}
</div> </div>
{{#if document.system.class.value}} {{#if document.system.class.value}}
<div class="domains-section"> <div class="domains-section">
@ -123,9 +128,7 @@
{{/each}} {{/each}}
</div> </div>
{{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' showSettings=showSettings }} {{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }}
{{#if ../showSettings}} <button type="button" data-action="openSettings" data-tooltip-text="{{localize "DAGGERHEART.UI.Tooltip.openSheetSettings"}}"><i class="fa-solid fa-wrench"></i></button>
<button type="button" data-action="openSettings" data-tooltip-text="{{localize "DAGGERHEART.UI.Tooltip.openSheetSettings"}}"><i class="fa-solid fa-wrench"></i></button>
{{/if}}
{{/'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}} {{/'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
</header> </header>

View file

@ -1,12 +1,12 @@
<aside class="character-sidebar-sheet"> <aside class="character-sidebar-sheet">
<div class="portrait {{#if isDeath}}death-roll{{/if}}"> <div class="portrait {{#if isDeath}}death-roll{{/if}}">
<img src="{{document.img}}" alt="{{document.name}}" data-action='editImage' data-edit="img"> <img src="{{document.img}}" alt="{{document.name}}" data-action='editImage' data-edit="img">
{{#if document.system.class.subclass.system.spellcastingTrait}} {{#if document.system.spellcastModifierTrait.key}}
<div class="icons-list"> <div class="icons-list">
<span class="spellcast-icon {{#if isDeath}}no-label{{/if}}"> <span class="spellcast-icon {{#if isDeath}}no-label{{/if}}">
<span class="spellcast-label"> <span class="spellcast-label">
{{localize "DAGGERHEART.ITEMS.Subclass.spellcastingTrait"}}: {{localize "DAGGERHEART.ITEMS.Subclass.spellcastingTrait"}}:
{{localize (concat 'DAGGERHEART.CONFIG.Traits.' document.system.class.subclass.system.spellcastingTrait '.short')}} {{localize (concat 'DAGGERHEART.CONFIG.Traits.' document.system.spellcastModifierTrait.key '.short')}}
</span> </span>
<i class="fa-solid fa-wand-sparkles"></i> <i class="fa-solid fa-wand-sparkles"></i>
</span> </span>

View file

@ -31,7 +31,7 @@
<h3 class='label'> <h3 class='label'>
{{localize 'DAGGERHEART.GENERAL.level'}} {{localize 'DAGGERHEART.GENERAL.level'}}
<div class="input-section"> <div class="input-section">
{{#if document.system.levelData.canLevelUp}} {{#if document.system.canLevelUp}}
<button <button
type="button" type="button"
class="level-button glow" data-tooltip="{{localize "DAGGERHEART.ACTORS.Character.levelUp"}}" class="level-button glow" data-tooltip="{{localize "DAGGERHEART.ACTORS.Character.levelUp"}}"
@ -41,7 +41,7 @@
</button> </button>
{{/if}} {{/if}}
<span {{#unless document.system.partner}}data-tooltip="{{localize "DAGGERHEART.UI.Tooltip.companionPartnerLevelBlock"}}"{{/unless}}> <span {{#unless document.system.partner}}data-tooltip="{{localize "DAGGERHEART.UI.Tooltip.companionPartnerLevelBlock"}}"{{/unless}}>
<input type="text" data-dtype="Number" class="level-value" value={{document.system.levelData.level.changed}} {{#if document.system.needsCharacterSetup}}disabled{{/if}} {{disabled (not document.system.partner)}} /> {{document.system.levelData.level.changed}}
</span> </span>
</div> </div>
</h3> </h3>

View file

@ -0,0 +1,20 @@
<header class="sheet-header img-name">
<img src="{{source.img}}" data-action="editImage" data-edit="img" alt="{{localize "DOCUMENT.FIELDS.img.label"}}">
<input type="text" name="name" value="{{source.name}}" placeholder="{{localize "DOCUMENT.FIELDS.name.label"}}" aria-label="{{localize "DOCUMENT.FIELDS.name.label"}}">
{{#if usesAltFormula}}
<div class="form-group">
<label>{{localize "Formula"}}</label>
<div class="form-fields">
<select class="system-update-field" data-path="activeAltFormula">
{{selectOptions this.altFormulaOptions selected=this.activeAltFormula labelAttr="name"}}
</select>
</div>
</div>
{{/if}}
<button data-action="changeMode">
<i class="fa-solid fa-eye" inert></i>
<span>{{localize "TABLE.ACTIONS.ChangeMode.View"}}</span>
</button>
</header>

View file

@ -0,0 +1,55 @@
<section class="tab{{#if tab.active}} active{{/if}}" data-group="{{tab.group}}" data-tab="{{tab.id}}">
<table class="flexcol" data-results>
<thead>
<tr class="flexrow">
<th class="image flexrow">
<button class="inline-control icon fa-solid fa-plus" data-action="createResult"
data-tooltip aria-label="{{localize "TABLE.ACTIONS.CreateResult"}}"></button>
</th>
<th class="details flexrow">{{localize "TABLE_RESULT.Details"}}</th>
<th class="weight flexrow">{{localize "TABLE_RESULT.FIELDS.weight.label"}}</th>
<th class="range flexrow">{{localize "TABLE_RESULT.FIELDS.range.label"}}</th>
<th class="controls flexrow">
<button class="inline-control icon fa-solid fa-scale-balanced" data-action="normalizeResults"
data-tooltip aria-label="{{localize "TABLE.ACTIONS.NormalizeResults"}}"></button>
</th>
</tr>
</thead>
<tbody class="scrollable">
{{#each results as |result i|}}
<tr class="flexrow{{#if result.drawn}} drawn{{/if}}" data-result-id="{{result.id}}">
<td class="image flexrow">
<img src="{{result.displayImg}}" data-action="editImage" data-edit="results.{{i}}.img"
alt="{{localize "TABLE_RESULT.FIELDS.img.label"}}" loading="lazy">
</td>
<td class="details">
{{> "templates/sheets/roll-table/result-details.hbs" result=result}}
</td>
<td class="weight flexrow">
<input type="number" name="results.{{i}}.weight" value="{{result.weight}}" placeholder="1">
</td>
<td class="range flexrow">
<input type="number" name="results.{{i}}.range.0" value="{{result.range.[0]}}" placeholder="L">
<span class="dash"></span>
<input type="number" name="results.{{i}}.range.1" value="{{result.range.[1]}}" placeholder="H">
</td>
<td class="controls flexrow">
<button class="inline-control icon fa-solid fa-file-pen" data-action="openResultSheet"
data-tooltip aria-label="{{localize "TABLE.ACTIONS.OpenResultConfig"}}"></button>
<button class="inline-control icon fa-solid fa-lock{{#unless result.drawn}}-open{{/unless}}"
data-action="lockResult"
data-tooltip aria-label="{{localize "TABLE.ACTIONS.ToggleDrawn"}}"></button>
<button class="inline-control icon fa-solid fa-trash" data-action="deleteResult"
data-tooltip aria-label="{{localize "TABLE.ACTIONS.DeleteResult"}}"></button>
</td>
<input type="hidden" name="results.{{i}}._id" value="{{result.id}}">
</tr>
{{/each}}
</tbody>
</table>
</section>

View file

@ -0,0 +1,49 @@
<header class="sheet-header flexrow">
<img src="{{document.img}}" alt="{{localize "DOCUMENT.FIELDS.img.label"}}">
<h1>{{document.name}}</h1>
<div class="roll-table-view-formula-container">
{{#if usesAltFormula}}
<select class="system-update-field" data-path="activeAltFormula">
{{selectOptions this.altFormulaOptions selected=this.activeAltFormula labelAttr="name"}}
</select>
{{/if}}
<h4>{{selectedFormula}}</h4>
</div>
<button data-action="changeMode">
<i class="fa-solid fa-pen" inert></i>
<span>{{localize "TABLE.ACTIONS.ChangeMode.Edit"}}</span>
</button>
</header>
{{{descriptionHTML}}}
<table class="flexcol" data-results>
<thead>
<tr class="flexrow">
<th class="image flexrow"></th>
<th class="range flexrow">{{localize "TABLE_RESULT.FIELDS.range.label"}}</th>
<th class="details flexrow">{{localize "TABLE_RESULT.Details"}}</th>
<th class="controls flexrow"></th>
</tr>
</thead>
<tbody class="scrollable">
{{#each results as |result i|}}
<tr class="flexrow{{#if result.drawn}} drawn{{/if}}" data-result-id="{{result.id}}">
<td class="image">
<img src="{{result.displayImg}}" alt="{{localize "TABLE_RESULT.FIELDS.img.label"}}" loading="lazy">
</td>
<td class="range">{{result.range}}</td>
<td class="details">
{{> "templates/sheets/roll-table/result-details.hbs" result=result}}
</td>
<td class="controls flexrow">
<button class="inline-control icon fa-solid fa-lock{{#unless result.drawn}}-open{{/unless}}"
data-action="lockResult"
data-tooltip aria-label="{{localize "TABLE.ACTIONS.ToggleDrawn"}}"></button>
<button class="inline-control icon fa-solid fa-up-from-bracket" data-action="drawSpecificResult"
data-tooltip aria-label="{{localize "TABLE.ACTIONS.DrawSpecificResult"}}"></button>
</td>
</tr>
{{/each}}
</tbody>
</table>

View file

@ -0,0 +1,22 @@
<section class="tab{{#if tab.active}} active{{/if}}" data-group="{{tab.group}}" data-tab="{{tab.id}}">
{{formGroup fields.description value=source.description rootId=rootId}}
<fieldset class="formulas-section">
<legend>{{localize "DAGGERHEART.ROLLTABLES.formula"}}</legend>
<div class="formulas-container">
<span>{{localize "DAGGERHEART.ROLLTABLES.FIELDS.formulaName.label"}}</span>
<span>{{localize "Formula Roll"}}</span>
<span></span>
<input type="text" value="{{@root.formulaName}}" class="system-update-field" data-path="formulaName" />
{{formInput fields.formula value=source.formula placeholder=formulaPlaceholder rootId=rootId}}
<button class="formula-button" data-action="addFormula"><i class="fa-solid fa-plus"></i></button>
{{#each @root.altFormula as | formula key |}}
<input type="text" value="{{formula.name}}" class="system-update-field" data-path="{{concat "altFormula." key ".name"}}" />
<input type="text" value="{{formula.formula}}" class="system-update-field" data-path="{{concat "altFormula." key ".formula"}}" />
<a class="formula-button" data-action="removeFormula" data-key="{{key}}"><i class="fa-solid fa-trash"></i></a>
{{/each}}
</div>
</fieldset>
{{formGroup fields.replacement value=source.replacement rootId=rootId}}
{{formGroup fields.displayRoll value=source.displayRoll rootId=rootId}}
</section>

View file

@ -1,17 +1,32 @@
<div class="daggerheart chat downtime"> <div class="daggerheart chat death-moves">
<ul class="downtime-moves-list"> <ul class="death-moves-list">
<details class="downtime-move"> <details class="death-move" {{this.open}}>
<summary class="downtime-label"> <summary class="death-label">
<img class="downtime-image" src="{{this.img}}" /> <img class="death-image" src="{{this.img}}" />
<div class="header-label"> <div class="header-label">
<h2 class="title">{{this.title}}</h2> <h2 class="title">{{this.title}}</h2>
<span class="label">{{localize 'DAGGERHEART.UI.Chat.deathMove.title'}}</span> <span class="label">{{localize 'DAGGERHEART.UI.Chat.deathMove.title'}}</span>
</div> </div>
<i class="fa-solid fa-chevron-down"></i> <i class="fa-solid {{this.chevron}}"></i>
</summary> </summary>
<div class="description"> <div class="description">
{{{this.description}}} {{{this.description}}}
</div> </div>
</details> </details>
</ul> </ul>
<div class="result">
{{{this.result}}}
</div>
{{#if this.showRiskItAllButton}}
<div>
<button class="risk-it-all-button" data-resource-value="{{this.riskItAllHope}}" data-actor-id="{{this.actorId}}">
<span>
{{this.riskItAllButtonLabel}}
</span>
</button>
<div>
{{/if}}
</div>
</div>
</div> </div>

View file

@ -29,64 +29,86 @@
<div class="dice-tooltip"> <div class="dice-tooltip">
<div class="wrapper"> <div class="wrapper">
<div class="roll-dice"> <div class="roll-dice">
{{#if roll.hope}} {{#if roll.fate}}
<div class="roll-die"> {{#if (eq roll.fate.fateDie "Hope")}}
<label>{{localize "DAGGERHEART.GENERAL.hope"}}</label> <div class="roll-die">
<div class="dice {{roll.hope.dice}} color-hope reroll-button" data-die-index="0" data-type="hope" data-tooltip="{{localize "DAGGERHEART.GENERAL.rerollThing" thing=(localize "DAGGERHEART.GENERAL.hope")}}"> <label>{{localize "DAGGERHEART.GENERAL.hope"}}</label>
{{#if roll.hope.rerolled.any}}<i class="fa-solid fa-dice dice-rerolled" data-tooltip="{{localize "DAGGERHEART.UI.Tooltip.diceIsRerolled" times=roll.hope.rerolled.rerolls.length}}"></i>{{/if}} <div class="dice {{roll.fate.dice}} color-hope" data-die-index="0" data-type="hope">
{{roll.hope.value}} {{roll.fate.value}}
</div>
</div>
<div class="roll-die has-plus">
<label>{{localize "DAGGERHEART.GENERAL.fear"}}</label>
<div class="dice {{roll.fear.dice}} color-fear reroll-button" data-die-index="2" data-type="fear" style="--svg-folder: 'fear';" data-tooltip="{{localize "DAGGERHEART.GENERAL.rerollThing" thing=(localize "DAGGERHEART.GENERAL.fear")}}">
{{#if roll.fear.rerolled.any}}<i class="fa-solid fa-dice dice-rerolled" data-tooltip="{{localize "DAGGERHEART.UI.Tooltip.diceIsRerolled" times=roll.fear.rerolled.rerolls.length}}"></i>{{/if}}
{{roll.fear.value}}
</div>
</div>
{{#if roll.advantage.type}}
<div class="roll-die has-plus">
{{#if (eq roll.advantage.type 1)}}
<label>{{localize "DAGGERHEART.GENERAL.Advantage.short"}}</label>
<div class="dice {{roll.advantage.dice}} color-adv">{{roll.advantage.value}}</div>
{{else}}
<label>{{localize "DAGGERHEART.GENERAL.Disadvantage.short"}}</label>
<div class="dice {{roll.advantage.dice}} color-dis">{{roll.advantage.value}}</div>
{{/if}}
</div>
{{/if}}
{{#if roll.rally.dice}}
<div class="roll-die has-plus">
<label>{{localize "DAGGERHEART.CLASS.Feature.short"}}</label>
<div class="dice {{roll.rally.dice}}">{{roll.rally.value}}</div>
</div>
{{/if}}
{{#each roll.extra}}
{{#each results}}
{{#unless discarded}}
<div class="roll-die has-plus">
<label></label>
<div class="dice {{../dice}}">{{result}}</div>
</div>
{{/unless}}
{{/each}}
{{/each}}
{{else}}
{{#each roll.dice}}
{{#each results}}
<div class="roll-die {{#unless (or @../first discarded)}} has-plus{{/unless}}">
<div class="dice {{../dice}}{{#if discarded}} discarded{{else}}{{#if (and @../first ../../roll.advantage.type)}}{{#if (eq ../../roll.advantage.type 1)}} color-adv{{else}} color-dis{{/if}}{{/if}}{{#if success}} color-adv{{/if}}{{/if}}">
{{result}}
</div>
</div> </div>
</div>
{{/if}}
{{#if (eq roll.fate.fateDie "Fear")}}
<div class="roll-die">
<label>{{localize "DAGGERHEART.GENERAL.fear"}}</label>
<div class="dice {{roll.fate.dice}} color-fear" data-die-index="0" data-type="fear">
{{roll.fate.value}}
</div>
</div>
{{/if}}
{{else}}
{{#if roll.hope}}
<div class="roll-die">
<label>{{localize "DAGGERHEART.GENERAL.hope"}}</label>
<div class="dice {{roll.hope.dice}} color-hope reroll-button" data-die-index="0" data-type="hope" data-tooltip="{{localize "DAGGERHEART.GENERAL.rerollThing" thing=(localize "DAGGERHEART.GENERAL.hope")}}">
{{#if roll.hope.rerolled.any}}<i class="fa-solid fa-dice dice-rerolled" data-tooltip="{{localize "DAGGERHEART.UI.Tooltip.diceIsRerolled" times=roll.hope.rerolled.rerolls.length}}"></i>{{/if}}
{{roll.hope.value}}
</div>
</div>
<div class="roll-die has-plus">
<label>{{localize "DAGGERHEART.GENERAL.fear"}}</label>
<div class="dice {{roll.fear.dice}} color-fear reroll-button" data-die-index="2" data-type="fear" style="--svg-folder: 'fear';" data-tooltip="{{localize "DAGGERHEART.GENERAL.rerollThing" thing=(localize "DAGGERHEART.GENERAL.fear")}}">
{{#if roll.fear.rerolled.any}}<i class="fa-solid fa-dice dice-rerolled" data-tooltip="{{localize "DAGGERHEART.UI.Tooltip.diceIsRerolled" times=roll.fear.rerolled.rerolls.length}}"></i>{{/if}}
{{roll.fear.value}}
</div>
</div>
{{#if roll.advantage.type}}
<div class="roll-die has-plus">
{{#if (eq roll.advantage.type 1)}}
<label>{{localize "DAGGERHEART.GENERAL.Advantage.short"}}</label>
<div class="dice {{roll.advantage.dice}} color-adv">{{roll.advantage.value}}</div>
{{else}}
<label>{{localize "DAGGERHEART.GENERAL.Disadvantage.short"}}</label>
<div class="dice {{roll.advantage.dice}} color-dis">{{roll.advantage.value}}</div>
{{/if}}
</div>
{{/if}}
{{#if roll.rally.dice}}
<div class="roll-die has-plus">
<label>{{localize "DAGGERHEART.CLASS.Feature.short"}}</label>
<div class="dice {{roll.rally.dice}}">{{roll.rally.value}}</div>
</div>
{{/if}}
{{#each roll.extra}}
{{#each results}}
{{#unless discarded}}
<div class="roll-die has-plus">
<label></label>
<div class="dice {{../dice}}">{{result}}</div>
</div>
{{/unless}}
{{/each}}
{{/each}} {{/each}}
{{/each}} {{else}}
{{#each roll.dice}}
{{#each results}}
<div class="roll-die {{#unless (or @../first discarded)}} has-plus{{/unless}}">
<div class="dice {{../dice}}{{#if discarded}} discarded{{else}}{{#if (and @../first ../../roll.advantage.type)}}{{#if (eq ../../roll.advantage.type 1)}} color-adv{{else}} color-dis{{/if}}{{/if}}{{#if success}} color-adv{{/if}}{{/if}}">
{{result}}
</div>
</div>
{{/each}}
{{/each}}
{{/if}}
{{/if}} {{/if}}
</div> </div>
</div> </div>
<div class="roll-formula">{{roll.formula}}</div> {{#if roll.fate}}
{{else}}
<div class="roll-formula">{{roll.formula}}</div>
{{/if}}
</div> </div>
</div> </div>
</div> </div>
{{/unless}} {{/unless}}
</div> </div>

View file

@ -0,0 +1,17 @@
<div class="table-draw" data-table-id="{{table.id}}">
{{#if flavor}}<div class="table-flavor">{{flavor}}</div>{{/if}}
{{#if description}}
<div class="table-description {{#if flavor}}flavor-spaced{{/if}}">{{{description}}}</div>
{{/if}}
{{{rollHTML}}}
<ul class="table-results">
{{#each results as |result|}}
<li class="flexrow" data-result-id="{{result.id}}">
<img src="{{result.icon}}">
{{{result.details}}}
</li>
{{/each}}
</ul>
</div>