Merge branch 'main' into release

This commit is contained in:
WBHarry 2025-11-11 16:06:03 +01:00
commit 395820513b
1285 changed files with 25675 additions and 8850 deletions

View file

@ -44,12 +44,14 @@ We encourage contributors to leave comments or open Discussions when proposing s
## 🧾 Issue & PR Best Practices ## 🧾 Issue & PR Best Practices
**For Issues:** **For Issues:**
- Use clear, descriptive titles - Use clear, descriptive titles
- Provide a concise explanation of the problem or idea - Provide a concise explanation of the problem or idea
- Include reproduction steps or example scenarios if it's a bug - Include reproduction steps or example scenarios if it's a bug
- Add screenshots or logs if helpful - Add screenshots or logs if helpful
**For Pull Requests:** **For Pull Requests:**
- Use a clear title summarizing the change - Use a clear title summarizing the change
- Provide a brief description of what your code does and why - Provide a brief description of what your code does and why
- Link to any related Issues - Link to any related Issues
@ -71,6 +73,6 @@ Discussions are currently happening on GitHub — in Issues, PRs, and [GitHub Di
## 🤗 Thank You! ## 🤗 Thank You!
Whether you're fixing a typo or designing entire mechanics — every contribution matters. Thank you for helping bring *Daggerheart* to life in FoundryVTT through **Foundryborne**! Whether you're fixing a typo or designing entire mechanics — every contribution matters. Thank you for helping bring _Daggerheart_ to life in FoundryVTT through **Foundryborne**!
🐸🛠️ 🐸🛠️

BIN
assets/icons/arrow-dunk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg"
width="42.0746mm" height="50.8071mm"
viewBox="0 0 159 192">
<path id="Fear"
fill="#808080" stroke="black" stroke-width="1"
d="M 107.14,62.84
C 107.14,62.84 52.70,137.55 52.70,137.55
52.70,137.55 110.33,137.55 110.33,137.55
110.33,137.55 127.99,79.63 127.99,79.63
127.99,79.63 107.14,62.84 107.14,62.84 Z
M 48.07,144.21
C 48.07,144.21 13.61,192.28 13.61,192.28
14.01,191.90 30.68,177.55 47.81,176.06
47.81,176.06 64.48,176.30 64.48,176.30
64.69,176.22 79.31,180.77 79.31,180.77
79.31,180.77 126.04,165.15 126.04,165.15
126.04,165.15 110.91,143.92 110.91,143.92
110.91,143.92 48.07,144.21 48.07,144.21 Z
M 134.24,81.37
C 134.24,81.37 115.74,140.33 115.74,140.33
115.74,140.33 131.41,161.00 131.41,161.00
131.41,161.00 158.98,122.82 158.98,122.82
158.98,122.82 158.91,72.83 158.91,72.83
158.91,72.83 134.24,81.37 134.24,81.37 Z
M 130.43,30.45
C 130.43,30.45 110.41,59.27 110.41,59.27
110.41,59.27 131.77,75.21 131.77,75.21
131.77,75.21 157.51,67.23 157.51,67.23
157.25,67.32 130.42,30.45 130.43,30.45 Z" />
<path id="Hope"
fill="white" stroke="black" stroke-width="1"
d="M 143.84,1.80
C 143.84,1.80 105.59,54.92 105.59,54.92
105.59,54.92 83.04,39.72 83.04,39.72
83.04,39.72 82.87,13.08 82.87,13.08
82.23,14.32 111.40,18.59 112.13,17.16
113.68,18.32 145.11,2.74 143.84,1.80 Z
M 76.01,13.40
C 76.01,13.40 76.01,40.05 76.01,40.05
76.01,40.05 25.99,75.35 25.99,75.35
25.99,75.35 1.96,68.16 1.96,68.16
1.96,68.16 30.08,27.62 30.08,27.62
30.08,27.62 76.01,13.40 76.01,13.40 Z
M 79.11,44.62
C 79.11,44.62 101.34,60.81 101.34,60.81
101.34,60.81 48.38,133.54 48.38,133.54
48.38,133.54 30.57,80.09 30.57,80.09
30.57,80.09 79.11,44.62 79.11,44.62 Z
M 24.36,81.07
C 24.36,81.07 43.64,139.59 43.64,139.59
43.64,139.59 28.11,162.31 28.11,162.31
28.11,162.31 0.33,123.90 0.33,123.90
0.33,123.90 0.33,74.05 0.33,74.05
0.33,74.05 24.36,81.07 24.36,81.07 Z" />
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="height: 512px; width: 512px;"><path d="M0 0h512v512H0z" fill="#000" fill-opacity="1"></path><g class="" transform="translate(0,0)" style=""><path d="M369.1 21.22c-19.2 0-36.2 10.63-47.9 26.47-11.7 15.84-18.6 37.03-18.6 60.31 0 21.1 5.7 40.5 15.5 55.7-5.7 1.6-11 3.9-15.9 6.6-10.2-8.5-22.6-13.6-35.9-13.6-19.3 0-36.3 10.6-48 26.4-4.7 6.4-8.6 13.6-11.6 21.5-4.8-2.4-9.9-4.3-15.5-5.6 9.4-15.1 14.8-34.1 14.8-54.7 0-23.2-6.9-44.43-18.6-60.27-11.7-15.84-28.7-26.5-47.9-26.5s-36.2 10.66-47.94 26.5C79.87 99.87 73 121.1 73 144.3c0 21.1 5.69 40.5 15.47 55.8-32.07 9.1-50.29 37.1-59.44 70-9.79 35.2-10.87 77.3-10.87 115.6v9.4h45.5l6.78 99.3h18.75l-7.28-106.5-4.1-80-18.65 1 3.47 67.5H36.97c.24-35.2 1.97-72.1 10.09-101.2 8.78-31.6 23.32-52.8 51.25-58.2l4.69-.1c10.3 8.8 22.9 14.2 36.5 14.2 14.1 0 26.9-5.7 37.4-15h4.6c7.8 1.2 14.4 3.5 20.1 6.7-1.2 6.6-1.9 13.5-1.9 20.6 0 21.1 5.7 40.5 15.5 55.8-32.1 9.1-50.3 37.2-59.4 70-9.8 35.2-10.9 77.3-10.9 115.6v9.4c21.7-.3 42.8.2 64.3.2l-.5-7.3-4.1-80-18.7.9 3.4 67.5h-25.6c.3-35.2 2-72.1 10.1-101.2 8.7-31.6 23.3-52.7 51.1-58.2l4.9-.1c10.3 8.8 22.8 14.2 36.4 14.2 14.1 0 27-5.7 37.5-15h4.4c15.4 2.4 26.1 8.9 34.5 18.6 8.5 9.7 14.5 23.2 18.5 39.2 7.3 29.5 7.7 66.9 7.7 102.5h-23.4l3.5-67.5-18.7-.9-4.2 82-.3 5.3c20.8 0 43.3-.3 61.9-.2v-9.4c0-38.1.5-80.6-8.4-116.3-4.4-17.8-11.3-34.1-22.4-47-9.7-11.1-22.7-19.4-38.8-23.4 9.4-15.1 14.7-34.1 14.7-54.7 0-22.5-6.4-43.2-17.5-58.8 3.9-1.8 8.1-3.1 12.7-4h4.7c10.3 8.8 22.9 14.2 36.5 14.2 14.1 0 27-5.8 37.4-15l4.6-.1c15.4 2.5 26 8.9 34.4 18.6 8.5 9.8 14.5 23.3 18.5 39.3 7.3 29.4 7.7 66.8 7.7 102.4h-23.4l3.5-67.4-18.7-1-4.1 79.7-8.6 143.1h18.7l8.2-135.7h43.1v-9.3c0-38.2.6-80.7-8.3-116.3-4.5-17.9-11.4-34.2-22.5-47-9.6-11.2-22.6-19.5-38.8-23.5 9.4-15.1 14.8-34 14.8-54.6 0-23.28-6.9-44.47-18.6-60.31-11.6-15.29-31.5-26.13-47.9-26.47zm0 18.69c12.4 0 23.9 6.69 32.9 18.87 9 12.19 14.9 29.67 14.9 49.22 0 19.5-5.9 37-14.9 49.2-9 12.2-20.5 18.9-32.9 18.9-12.3 0-23.9-6.7-32.9-18.9s-14.9-29.7-14.9-49.2c0-19.55 5.9-37.03 14.9-49.22 9-12.18 20.6-18.87 32.9-18.87zM139.5 76.22c12.4 0 23.9 6.72 32.9 18.9s14.9 29.68 14.9 49.18-5.9 37-14.9 49.2c-9 12.2-20.5 18.9-32.9 18.9-12.4 0-23.9-6.7-32.9-18.9-8.97-12.2-14.91-29.7-14.91-49.2 0-19.5 5.94-37 14.91-49.17 9-12.19 20.5-18.91 32.9-18.91zm197.8 22.34v18.64h22.5V98.56h-22.5zm41.1 0v18.64h22.5V98.56h-22.5zM107.7 134.9v18.7h22.5v-18.7h-22.5zm41.1 0v18.7h22.5v-18.7h-22.5zm117.5 40.4c12.3 0 23.8 6.7 32.8 18.9 9 12.2 15 29.7 15 49.2 0 19.6-6 37-15 49.2-9 12.2-20.5 18.9-32.8 18.9-12.4 0-24-6.7-33-18.9-8.9-12.2-14.9-29.6-14.9-49.2 0-19.5 6-37 14.9-49.2 9-12.2 20.6-18.9 33-18.9zM234.5 234v18.7h22.4V234zm41.1 0v18.7H298V234h-22.4z" fill="#fff" fill-opacity="1"></path></g></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -1,5 +1,6 @@
import { SYSTEM } from './module/config/system.mjs'; import { SYSTEM } from './module/config/system.mjs';
import * as applications from './module/applications/_module.mjs'; import * as applications from './module/applications/_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 dice from './module/dice/_module.mjs'; import * as dice from './module/dice/_module.mjs';
@ -7,10 +8,8 @@ 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 { NarrativeCountdowns } from './module/applications/ui/countdowns.mjs';
import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll } from './module/dice/_module.mjs'; import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll } from './module/dice/_module.mjs';
import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs'; import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs';
import { registerCountdownHooks } from './module/data/countdowns.mjs';
import { import {
handlebarsRegistration, handlebarsRegistration,
runMigrations, runMigrations,
@ -26,6 +25,7 @@ Hooks.once('init', () => {
CONFIG.DH = SYSTEM; CONFIG.DH = SYSTEM;
game.system.api = { game.system.api = {
applications, applications,
data,
models, models,
documents, documents,
dice, dice,
@ -34,15 +34,6 @@ Hooks.once('init', () => {
CONFIG.TextEditor.enrichers.push(...enricherConfig); CONFIG.TextEditor.enrichers.push(...enricherConfig);
CONFIG.statusEffects = [
...CONFIG.statusEffects.filter(x => !['dead', 'unconscious'].includes(x.id)),
...Object.values(SYSTEM.GENERAL.conditions).map(x => ({
...x,
name: game.i18n.localize(x.name),
systemEffect: true
}))
];
CONFIG.Dice.daggerheart = { CONFIG.Dice.daggerheart = {
DHRoll: DHRoll, DHRoll: DHRoll,
DualityRoll: DualityRoll, DualityRoll: DualityRoll,
@ -94,6 +85,10 @@ Hooks.once('init', () => {
types: ['environment'], types: ['environment'],
makeDefault: true makeDefault: true
}); });
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Party, {
types: ['party'],
makeDefault: true
});
CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect; CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect;
CONFIG.ActiveEffect.dataModels = models.activeEffects.config; CONFIG.ActiveEffect.dataModels = models.activeEffects.config;
@ -133,9 +128,12 @@ Hooks.once('init', () => {
CONFIG.ui.combat = applications.ui.DhCombatTracker; CONFIG.ui.combat = applications.ui.DhCombatTracker;
CONFIG.ui.chat = applications.ui.DhChatLog; CONFIG.ui.chat = applications.ui.DhChatLog;
CONFIG.ui.hotbar = applications.ui.DhHotbar; CONFIG.ui.hotbar = applications.ui.DhHotbar;
CONFIG.ui.sidebar = applications.sidebar.DhSidebar;
CONFIG.ui.daggerheartMenu = applications.sidebar.DaggerheartMenu;
CONFIG.Token.rulerClass = placeables.DhTokenRuler; CONFIG.Token.rulerClass = placeables.DhTokenRuler;
CONFIG.ui.resources = applications.ui.DhFearTracker; CONFIG.ui.resources = applications.ui.DhFearTracker;
CONFIG.ui.countdowns = applications.ui.DhCountdowns;
CONFIG.ux.ContextMenu = applications.ux.DHContextMenu; CONFIG.ux.ContextMenu = applications.ux.DHContextMenu;
CONFIG.ux.TooltipManager = documents.DhTooltipManager; CONFIG.ux.TooltipManager = documents.DhTooltipManager;
@ -146,18 +144,39 @@ Hooks.once('init', () => {
// Make Compendium Dialog resizable // Make Compendium Dialog resizable
foundry.applications.sidebar.apps.Compendium.DEFAULT_OPTIONS.window.resizable = true; foundry.applications.sidebar.apps.Compendium.DEFAULT_OPTIONS.window.resizable = true;
DocumentSheetConfig.registerSheet(foundry.documents.Scene, SYSTEM.id, applications.scene.DhSceneConfigSettings, {
makeDefault: true,
label: 'Daggerheart'
});
settingsRegistration.registerDHSettings(); settingsRegistration.registerDHSettings();
RegisterHandlebarsHelpers.registerHelpers(); RegisterHandlebarsHelpers.registerHelpers();
return handlebarsRegistration(); return handlebarsRegistration();
}); });
Hooks.on('setup', () => {
CONFIG.statusEffects = [
...CONFIG.statusEffects.filter(x => !['dead', 'unconscious'].includes(x.id)),
...Object.values(SYSTEM.GENERAL.conditions()).map(x => ({
...x,
name: game.i18n.localize(x.name),
systemEffect: true
}))
];
});
Hooks.on('ready', async () => { Hooks.on('ready', async () => {
ui.resources = new CONFIG.ui.resources(); ui.resources = new CONFIG.ui.resources();
if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear !== 'hide') if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear !== 'hide')
ui.resources.render({ force: true }); ui.resources.render({ force: true });
registerCountdownHooks(); ui.countdowns = new CONFIG.ui.countdowns();
ui.countdowns.render({ force: true });
if (!(ui.compendiumBrowser instanceof applications.ui.ItemBrowser))
ui.compendiumBrowser = new applications.ui.ItemBrowser();
socketRegistration.registerSocketHooks(); socketRegistration.registerSocketHooks();
registerRollDiceHooks(); registerRollDiceHooks();
socketRegistration.registerUserQueries(); socketRegistration.registerUserQueries();
@ -230,29 +249,6 @@ Hooks.on('chatMessage', (_, message) => {
} }
}); });
Hooks.on('renderJournalDirectory', async (tab, html, _, options) => {
if (tab.id === 'journal') {
if (options.parts && !options.parts.includes('footer')) return;
const buttons = tab.element.querySelector('.directory-footer.action-buttons');
const title = game.i18n.format('DAGGERHEART.APPLICATIONS.Countdown.title', {
type: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.types.narrative')
});
buttons.insertAdjacentHTML(
'afterbegin',
`
<button id="narrative-countdown-button">
<i class="fa-solid fa-stopwatch"></i>
<span style="font-weight: 400; font-family: var(--font-sans);">${title}</span>
</button>`
);
buttons.querySelector('#narrative-countdown-button').onclick = async () => {
new NarrativeCountdowns().open();
};
}
});
Hooks.on('moveToken', async (movedToken, data) => { Hooks.on('moveToken', async (movedToken, data) => {
const effectsAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).effects; const effectsAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).effects;
if (!effectsAutomation.rangeDependent) return; if (!effectsAutomation.rangeDependent) return;
@ -300,3 +296,6 @@ Hooks.on('moveToken', async (movedToken, data) => {
await effect.value.update({ disabled: effect.disabled }); await effect.value.update({ disabled: effect.disabled });
} }
}); });
Hooks.on('renderCompendiumDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html));
Hooks.on('renderDocumentDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html));

View file

@ -20,12 +20,21 @@
"character": "Character", "character": "Character",
"companion": "Companion", "companion": "Companion",
"adversary": "Adversary", "adversary": "Adversary",
"environment": "Environment" "environment": "Environment",
"party": "Party"
} }
}, },
"CONTROLS": { "CONTROLS": {
"inFront": "In Front" "inFront": "In Front"
}, },
"SCENE": {
"TABS": {
"SHEET": {
"dh": "Daggerheart"
}
}
},
"DAGGERHEART": { "DAGGERHEART": {
"ACTIONS": { "ACTIONS": {
"TYPES": { "TYPES": {
@ -64,6 +73,14 @@
"exactHint": "The Character's Tier is used if empty", "exactHint": "The Character's Tier is used if empty",
"label": "Beastform" "label": "Beastform"
}, },
"damage": {
"multiplier": "Multiplier",
"flatMultiplier": "Flat Multiplier"
},
"general": {
"customFormula": "Custom Formula",
"formula": "Formula"
},
"displayInChat": "Display in chat" "displayInChat": "Display in chat"
}, },
"RollField": { "RollField": {
@ -80,6 +97,7 @@
"attackName": "Attack Name", "attackName": "Attack Name",
"includeBase": { "label": "Include Item Damage" }, "includeBase": { "label": "Include Item Damage" },
"multiplier": "Multiplier", "multiplier": "Multiplier",
"saveHint": "Set a default Trait to enable Reaction Roll. It can be changed later in Reaction Roll Dialog.",
"resultBased": { "resultBased": {
"label": "Formula based on Hope/Fear result." "label": "Formula based on Hope/Fear result."
}, },
@ -153,7 +171,9 @@
"hint": "Add single words or short text as reminders and hints of what a character has advantage on." "hint": "Add single words or short text as reminders and hints of what a character has advantage on."
}, },
"age": "Age", "age": "Age",
"backgroundQuestions": "Backgrounds",
"companionFeatures": "Companion Features", "companionFeatures": "Companion Features",
"connections": "Connections",
"contextMenu": { "contextMenu": {
"consume": "Consume Item", "consume": "Consume Item",
"equip": "Equip", "equip": "Equip",
@ -194,6 +214,7 @@
"confirmTitle": "Companion Levelup", "confirmTitle": "Companion Levelup",
"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",
"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?"
}, },
@ -238,6 +259,9 @@
} }
}, },
"APPLICATIONS": { "APPLICATIONS": {
"Attribution": {
"title": "Attribution"
},
"CharacterCreation": { "CharacterCreation": {
"tabs": { "tabs": {
"ancestry": "Ancestry", "ancestry": "Ancestry",
@ -246,7 +270,8 @@
"experience": "Experience", "experience": "Experience",
"traits": "Traits", "traits": "Traits",
"domainCards": "Domain Cards", "domainCards": "Domain Cards",
"equipment": "Equipment" "equipment": "Equipment",
"story": "Story"
}, },
"ancestryNamePlaceholder": "Your ancestry's name", "ancestryNamePlaceholder": "Your ancestry's name",
"buttonTitle": "Character Setup", "buttonTitle": "Character Setup",
@ -267,6 +292,7 @@
"selectSubclass": "Select Subclass", "selectSubclass": "Select Subclass",
"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.",
"suggestedArmor": "Suggested Armor", "suggestedArmor": "Suggested Armor",
"suggestedPrimaryWeapon": "Suggested Primary Weapon", "suggestedPrimaryWeapon": "Suggested Primary Weapon",
"suggestedSecondaryWeapon": "Suggested Secondary Weapon", "suggestedSecondaryWeapon": "Suggested Secondary Weapon",
@ -290,7 +316,8 @@
"toLoadout": "Send to Loadout", "toLoadout": "Send to Loadout",
"toVault": "Send to Vault", "toVault": "Send to Vault",
"unequip": "Unequip", "unequip": "Unequip",
"useItem": "Use Item" "useItem": "Use Item",
"cancelBeastform": "Cancel Beastform"
}, },
"Countdown": { "Countdown": {
"addCountdown": "Add Countdown", "addCountdown": "Add Countdown",
@ -305,7 +332,8 @@
"label": { "label": "Label", "hint": "Used for custom" }, "label": { "label": "Label", "hint": "Used for custom" },
"value": { "label": "Value" } "value": { "label": "Value" }
} }
} },
"type": { "label": "Countdown Type" }
} }
} }
}, },
@ -320,6 +348,25 @@
"encounter": "Encounter" "encounter": "Encounter"
} }
}, },
"CountdownEdit": {
"title": "Countdown Edit",
"viewTitle": "Countdowns",
"editTitle": "Edit Countdowns",
"newCountdown": "New Countdown",
"removeCountdownTitle": "Remove Countdown",
"removeCountdownText": "Are you sure you want to remove the countdown: {name}?",
"current": "Current",
"max": "Max",
"currentCountdownValue": "Current: {value}",
"currentCountdownMax": "Max: {value}",
"category": "Category",
"type": "Type",
"defaultOwnershipTooltip": "The default player ownership of countdowns",
"hideNewCountdowns": "Hide New Countdowns"
},
"DaggerheartMenu": {
"title": "GM Tools"
},
"DeleteConfirmation": { "DeleteConfirmation": {
"title": "Delete {type} - {name}", "title": "Delete {type} - {name}",
"text": "Are you sure you want to delete {name}?" "text": "Are you sure you want to delete {name}?"
@ -391,9 +438,15 @@
}, },
"HUD": { "HUD": {
"tokenHUD": { "tokenHUD": {
"genericEffects": "Foundry Effects" "genericEffects": "Foundry Effects",
"depositPartyTokens": "Deposit Party Tokens",
"retrievePartyTokens": "Retrieve Party Tokens"
} }
}, },
"ImageSelect": {
"title": "Select Image",
"selectImage": "Select Image"
},
"Levelup": { "Levelup": {
"actions": { "actions": {
"creatureComfort": { "creatureComfort": {
@ -475,24 +528,28 @@
}, },
"takeLevelUp": "Finish Level Up", "takeLevelUp": "Finish Level Up",
"tier2": { "tier2": {
"name": "Tier 2",
"label": "Levels 2-4", "label": "Levels 2-4",
"infoLabel": "At Level 2, gain an additional Experience at +2 and gain a +1 bonus to your Proficiency.", "infoLabel": "At Level 2, gain an additional Experience at +2 and gain a +1 bonus to your Proficiency.",
"pretext": "Choose two options from the list below", "pretext": "Choose two options from the list below",
"posttext": "Take an additional domain card of your level or lower from a domain you have access to." "posttext": "Take an additional domain card of your level or lower from a domain you have access to."
}, },
"tier3": { "tier3": {
"name": "Tier 3",
"label": "Levels 5-7", "label": "Levels 5-7",
"infoLabel": "At Level 5, take an additional Experience and clear all marks on Character Traits.", "infoLabel": "At Level 5, take an additional Experience and clear all marks on Character Traits.",
"pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.", "pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.",
"posttext": "Take an additional domain card of your level or lower from a domain you have access to." "posttext": "Take an additional domain card of your level or lower from a domain you have access to."
}, },
"tier4": { "tier4": {
"name": "Tier 4",
"label": "Levels 8-10", "label": "Levels 8-10",
"infoLabel": "At Level 8, take an additional Experience and clear all marks on Character Traits.", "infoLabel": "At Level 8, take an additional Experience and clear all marks on Character Traits.",
"pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.", "pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.",
"posttext": "Take an additional domain card of your level or lower from a domain you have access to." "posttext": "Take an additional domain card of your level or lower from a domain you have access to."
}, },
"title": "{actor} Level Up" "title": "{actor} Level Up",
"viewModeTitle": "{actor} Level Up (View Mode)"
}, },
"MulticlassChoice": { "MulticlassChoice": {
"title": "Multiclassing - {actor}", "title": "Multiclassing - {actor}",
@ -514,6 +571,19 @@
"ResourceDice": { "ResourceDice": {
"title": "{name} Resource", "title": "{name} Resource",
"rerollDice": "Reroll Dice" "rerollDice": "Reroll Dice"
},
"TagTeamSelect": {
"title": "Tag Team Roll",
"leaderTitle": "Initiating Character",
"membersTitle": "Participants",
"partyTeam": "Party Team",
"hopeCost": "Hope Cost",
"initiatingCharacter": "Initiating Character",
"linkMessageHint": "Make a roll from your character sheet to link it to the Tag Team Roll",
"damageNotRolled": "Damage not rolled in chat message yet",
"insufficientHope": "The initiating character doesn't have enough hope",
"createTagTeam": "Create TagTeam Roll",
"chatMessageRollTitle": "Roll"
} }
}, },
"CLASS": { "CLASS": {
@ -857,6 +927,11 @@
"magical": { "magical": {
"name": "Magical", "name": "Magical",
"abbreviation": "Mag" "abbreviation": "Mag"
},
"direct": {
"name": "Direct Damage",
"short": "Direct",
"abbreviation": "Dir"
} }
}, },
"DeathMoves": { "DeathMoves": {
@ -906,23 +981,28 @@
"HealingType": { "HealingType": {
"hitPoints": { "hitPoints": {
"name": "Hit Points", "name": "Hit Points",
"abbreviation": "HP" "abbreviation": "HP",
"inChatRoll": "Damage"
}, },
"stress": { "stress": {
"name": "Stress", "name": "Stress",
"abbreviation": "STR" "abbreviation": "STR",
"inChatRoll": "Stress"
}, },
"hope": { "hope": {
"name": "Hope", "name": "Hope",
"abbreviation": "HO" "abbreviation": "HO",
"inChatRoll": "Hope"
}, },
"armor": { "armor": {
"name": "Armor Slot", "name": "Armor Slot",
"abbreviation": "AS" "abbreviation": "AS",
"inChatRoll": "Armor Slot"
}, },
"fear": { "fear": {
"name": "Fear", "name": "Fear",
"abbreviation": "FR" "abbreviation": "FR",
"inChatRoll": "Fear"
} }
}, },
"ItemResourceProgression": { "ItemResourceProgression": {
@ -989,6 +1069,12 @@
"selectType": "Select Action Type", "selectType": "Select Action Type",
"selectAction": "Action Selection" "selectAction": "Action Selection"
}, },
"TargetTypes": {
"any": "Any",
"friendly": "Friendly",
"hostile": "Hostile",
"self": "Self"
},
"TemplateTypes": { "TemplateTypes": {
"circle": "Circle", "circle": "Circle",
"cone": "Cone", "cone": "Cone",
@ -1838,6 +1924,10 @@
} }
} }
}, },
"SpotlightRequests": {
"singular": "Spotlight Request",
"plural": "Spotlight Requests"
},
"Tabs": { "Tabs": {
"details": "Details", "details": "Details",
"attack": "Attack", "attack": "Attack",
@ -1862,6 +1952,7 @@
"story": "Story", "story": "Story",
"biography": "Biography", "biography": "Biography",
"general": "General", "general": "General",
"resources": "Resources",
"foundation": "Foundation", "foundation": "Foundation",
"specialization": "Specialization", "specialization": "Specialization",
"mastery": "Mastery", "mastery": "Mastery",
@ -1877,7 +1968,15 @@
"tier4": "tier 4", "tier4": "tier 4",
"domains": "Domains", "domains": "Domains",
"downtime": "Downtime", "downtime": "Downtime",
"rules": "Rules" "roll": "Roll",
"rules": "Rules",
"partyMembers": "Party Members",
"projects": "Projects",
"types": "Types",
"itemFeatures": "Item Features",
"questions": "Questions",
"configuration": "Configuration",
"base": "Base"
}, },
"Tiers": { "Tiers": {
"singular": "Tier", "singular": "Tier",
@ -1894,10 +1993,12 @@
"amount": "Amount", "amount": "Amount",
"any": "Any", "any": "Any",
"armor": "Armor", "armor": "Armor",
"armorFeatures": "Armor Features",
"armors": "Armors", "armors": "Armors",
"armorScore": "Armor Score", "armorScore": "Armor Score",
"activeEffects": "Active Effects", "activeEffects": "Active Effects",
"armorSlots": "Armor Slots", "armorSlots": "Armor Slots",
"artistAttribution": "Artwork By: {artist}",
"attack": "Attack", "attack": "Attack",
"basics": "Basics", "basics": "Basics",
"bonus": "Bonus", "bonus": "Bonus",
@ -1905,6 +2006,7 @@
"continue": "Continue", "continue": "Continue",
"criticalSuccess": "Critical Success", "criticalSuccess": "Critical Success",
"criticalShort": "Critical", "criticalShort": "Critical",
"custom": "Custom",
"d20Roll": "D20 Roll", "d20Roll": "D20 Roll",
"damage": "Damage", "damage": "Damage",
"damageRoll": "Damage Roll", "damageRoll": "Damage Roll",
@ -1927,6 +2029,7 @@
"fear": "Fear", "fear": "Fear",
"features": "Features", "features": "Features",
"formula": "Formula", "formula": "Formula",
"gm": "GM",
"healing": "Healing", "healing": "Healing",
"healingRoll": "Healing Roll", "healingRoll": "Healing Roll",
"hit": { "hit": {
@ -1945,6 +2048,7 @@
"inactiveEffects": "Inactive Effects", "inactiveEffects": "Inactive Effects",
"inventory": "Inventory", "inventory": "Inventory",
"itemResource": "Item Resource", "itemResource": "Item Resource",
"itemQuantity": "Item Quantity",
"items": "Items", "items": "Items",
"label": "Label", "label": "Label",
"level": "Level", "level": "Level",
@ -1963,11 +2067,16 @@
"none": "None", "none": "None",
"noTarget": "No current target", "noTarget": "No current target",
"partner": "Partner", "partner": "Partner",
"player": {
"single": "Player",
"plurial": "Players"
},
"proficiency": "Proficiency", "proficiency": "Proficiency",
"quantity": "Quantity", "quantity": "Quantity",
"range": "Range", "range": "Range",
"reactionRoll": "Reaction Roll", "reactionRoll": "Reaction Roll",
"recovery": "Recovery", "recovery": "Recovery",
"refresh": "Refresh",
"reroll": "Reroll", "reroll": "Reroll",
"rerollThing": "Reroll {thing}", "rerollThing": "Reroll {thing}",
"resource": "Resource", "resource": "Resource",
@ -1978,6 +2087,8 @@
"save": "Save", "save": "Save",
"scalable": "Scalable", "scalable": "Scalable",
"situationalBonus": "Situational Bonus", "situationalBonus": "Situational Bonus",
"spent": "Spent",
"step": "Step",
"stress": "Stress", "stress": "Stress",
"subclasses": "Subclasses", "subclasses": "Subclasses",
"success": "Success", "success": "Success",
@ -1988,6 +2099,7 @@
}, },
"title": "Title", "title": "Title",
"total": "Total", "total": "Total",
"traitModifier": "Trait Modifier",
"true": "True", "true": "True",
"type": "Type", "type": "Type",
"unarmed": "Unarmed", "unarmed": "Unarmed",
@ -1997,11 +2109,17 @@
"used": "Used", "used": "Used",
"uses": "Uses", "uses": "Uses",
"value": "Value", "value": "Value",
"weaponFeatures": "Weapon Features",
"weapons": "Weapons", "weapons": "Weapons",
"withThing": "With {thing}" "withThing": "With {thing}"
}, },
"ITEMS": { "ITEMS": {
"FIELDS": { "FIELDS": {
"attribution": {
"source": { "label": "Source" },
"page": { "label": "Page" },
"artist": { "label": "Artist" }
},
"resource": { "resource": {
"amount": { "label": "Amount" }, "amount": { "label": "Amount" },
"dieFaces": { "label": "Die Faces" }, "dieFaces": { "label": "Die Faces" },
@ -2076,7 +2194,8 @@
} }
}, },
"Consumable": { "Consumable": {
"consumeOnUse": "Consume On Use" "consumeOnUse": "Consume On Use",
"destroyOnEmpty": "Destroy On Empty"
}, },
"DomainCard": { "DomainCard": {
"type": "Type", "type": "Type",
@ -2097,14 +2216,44 @@
"SETTINGS": { "SETTINGS": {
"Appearance": { "Appearance": {
"FIELDS": { "FIELDS": {
"displayFear": { "label": "Fear Display" }, "displayFear": {
"dualityColorScheme": { "label": "Chat Style" }, "label": "Display Fear"
"showGenericStatusEffects": { "label": "Show Foundry Status Effects" }, },
"showGenericStatusEffects": {
"label": "Show Foundry Status Effects"
},
"hideAttribution": {
"label": "Hide Attribution"
},
"expandedTitle": "Auto-expand Descriptions", "expandedTitle": "Auto-expand Descriptions",
"extendCharacterDescriptions": { "label": "Characters" }, "extendCharacterDescriptions": {
"extendAdversaryDescriptions": { "label": "Adversaries" }, "label": "Characters"
"extendEnvironmentDescriptions": { "label": "Environments" }, },
"extendItemDescriptions": { "label": "Items" } "extendAdversaryDescriptions": {
"label": "Adversaries"
},
"extendEnvironmentDescriptions": {
"label": "Environments"
},
"extendItemDescriptions": {
"label": "Items"
},
"expandRollMessage": {
"title": "Auto-expand Message Sections",
"desc": {
"label": "Description"
},
"roll": {
"label": "Formula"
},
"damage": {
"label": "Damage/Healing"
},
"target": {
"label": "Target"
}
},
"useResourcePips": { "label": "Pip Display For Resources" }
}, },
"fearDisplay": { "fearDisplay": {
"token": "Tokens", "token": "Tokens",
@ -2159,15 +2308,45 @@
"playerCanEditSheet": { "playerCanEditSheet": {
"label": "Players Can Manually Edit Character Settings", "label": "Players Can Manually Edit Character Settings",
"hint": "Players are allowed to access the manual Character Settings and change their statistics beyond the rules." "hint": "Players are allowed to access the manual Character Settings and change their statistics beyond the rules."
},
"roll": {
"roll": {
"label": "Roll",
"hint": "Auto behavior for rolls like Attack, Spellcast, etc."
},
"damage": {
"label": "Damage/Healing Roll",
"hint": "Auto behavior for Damage & Healing rolls after the Attack/Spellcast."
},
"save": {
"label": "Reaction Roll",
"hint": "Auto behavior if a Reaction Roll is needed. Targets must be selected before the action is made"
},
"damageApply": {
"label": "Apply Damage/Healing",
"hint": "Automatically apply damages & healings. Targets must be selected before the action is made and Reaction Roll Automation must be different than Never. Bypass users permissions."
},
"effect": {
"label": "Apply Effects",
"hint": "Automatically apply effects. Targets must be selected before the action is made and Reaction Roll Automation must be different than Never. Bypass users permissions."
}
},
"summaryMessages": {
"label": "Summary Messages"
} }
}, },
"defeated": { "defeated": {
"title": "Defeated Handling" "title": "Defeated Handling"
},
"roll": {
"title": "Actions"
} }
}, },
"Homebrew": { "Homebrew": {
"newDowntimeMove": "Downtime Move", "newDowntimeMove": "Downtime Move",
"newFeature": "New ItemFeature",
"downtimeMoves": "Downtime Moves", "downtimeMoves": "Downtime Moves",
"itemFeatures": "Item Features",
"nrChoices": "# Moves Per Rest", "nrChoices": "# Moves Per Rest",
"resetMovesTitle": "Reset {type} Downtime Moves", "resetMovesTitle": "Reset {type} Downtime Moves",
"resetMovesText": "Are you sure you want to reset?", "resetMovesText": "Are you sure you want to reset?",
@ -2200,6 +2379,10 @@
"deleteDomain": "Delete Domain", "deleteDomain": "Delete Domain",
"deleteDomainText": "Are you sure you want to delete the {name} domain? It will be immediately removed from all Actors in this world where it's currently used. Compendiums are not cleared.", "deleteDomainText": "Are you sure you want to delete the {name} domain? It will be immediately removed from all Actors in this world where it's currently used. Compendiums are not cleared.",
"duplicateDomain": "There is already a domain with this identification." "duplicateDomain": "There is already a domain with this identification."
},
"adversaryType": {
"title": "Custom Adversary Types",
"newType": "Adversary Type"
} }
}, },
"Menu": { "Menu": {
@ -2220,10 +2403,8 @@
"hint": "System ruler setup for displaying ranges in Daggerheart" "hint": "System ruler setup for displaying ranges in Daggerheart"
}, },
"appearance": { "appearance": {
"title": "Appearance Settings",
"label": "Appearance Settings", "label": "Appearance Settings",
"hint": "Modify the look of various parts of the system", "hint": "Modify the look of various parts of the system",
"name": "Appearance Settings",
"duality": "Duality Rolls", "duality": "Duality Rolls",
"diceSoNice": { "diceSoNice": {
"title": "Dice So Nice", "title": "Dice So Nice",
@ -2263,6 +2444,15 @@
"ResetSettings": { "ResetSettings": {
"resetConfirmationTitle": "Reset Settings", "resetConfirmationTitle": "Reset Settings",
"resetConfirmationText": "Are you sure you want to reset the {settings}?" "resetConfirmationText": "Are you sure you want to reset the {settings}?"
},
"Scene": {
"FIELDS": {
"rangeMeasurement": {
"setting": { "label": "Setting" }
}
},
"disabledText": "Daggerheart Measurements are disabled in System Settings - Variant Rules",
"rangeMeasurement": "Range Measurement"
} }
}, },
"UI": { "UI": {
@ -2270,6 +2460,7 @@
"action": { "action": {
"title": "Action" "title": "Action"
}, },
"appliedTo": "Applied To",
"applyEffect": { "applyEffect": {
"title": "Apply Effects - {name}" "title": "Apply Effects - {name}"
}, },
@ -2279,6 +2470,11 @@
"rollHealing": "Roll Healing", "rollHealing": "Roll Healing",
"applyEffect": "Apply Effects" "applyEffect": "Apply Effects"
}, },
"clearResource": "Clear {quantity} {resource}",
"damageSummary": {
"title": "Damage Applied",
"healingTitle": "Healing Applied"
},
"damageRoll": { "damageRoll": {
"title": "Damage - {damage}", "title": "Damage - {damage}",
"dealDamageToTargets": "Damage Hit Targets", "dealDamageToTargets": "Damage Hit Targets",
@ -2300,18 +2496,91 @@
"dualityRoll": { "dualityRoll": {
"abilityCheckTitle": "{ability} Check" "abilityCheckTitle": "{ability} Check"
}, },
"effectSummary": {
"title": "Effects Applied"
},
"featureTitle": "Class Feature", "featureTitle": "Class Feature",
"groupRoll": {
"title": "Group Roll",
"leader": "Leader",
"partyTeam": "Party Team",
"team": "Team",
"selectLeader": "Select a Leader",
"selectMember": "Select a Member",
"rerollTitle": "Reroll Group Roll",
"rerollContent": "Are you sure you want to reroll your {trait} check?",
"rerollTooltip": "Reroll"
},
"healingRoll": { "healingRoll": {
"title": "Heal - {damage}", "title": "Heal - {damage}",
"heal": "Heal", "heal": "Heal",
"applyHealing": "Apply Healing" "applyHealing": "Apply Healing"
}, },
"markResource": "Mark {quantity} {resource}",
"refreshMessage": {
"title": "Feature Refresh",
"header": "Refreshed"
},
"reroll": { "reroll": {
"confirmTitle": "Reroll Dice", "confirmTitle": "Reroll Dice",
"confirmText": "Are you sure you want to reroll?" "confirmText": "Are you sure you want to reroll?"
}, },
"resourceRoll": { "resourceRoll": {
"playerMessage": "{user} rerolled their {name}" "playerMessage": "{user} rerolled their {name}"
},
"tagTeam": {
"title": "Tag Team",
"membersTitle": "Members"
}
},
"ChatLog": {
"rerollDamage": "Reroll Damage",
"assignTagRoll": "Assign as Tag Roll"
},
"Countdowns": {
"title": "Countdowns",
"toggleIconMode": "Toggle Icon Only",
"noPlayerAccess": "This countdown isn't visible to any players"
},
"ItemBrowser": {
"title": "Daggerheart Compendium Browser",
"hint": "Select a Folder in sidebar to start browsing through the compendium",
"searchPlaceholder": "Search...",
"columnName": "Name",
"tooltipFilters": "Filters",
"tooltipErase": "Erase",
"difficultyMin": "Difficulty (Min)",
"difficultyMax": "Difficulty (Max)",
"hitPointsMin": "Hit Points (Min)",
"hitPointsMax": "Hit Points (Max)",
"stressMin": "Stress (Min)",
"stressMax": "Stress (Max)",
"armorScoreMin": "Armor Score (Min)",
"armorScoreMax": "Armor Score (Max)",
"levelMin": "Level (Min)",
"levelMax": "Level (Max)",
"recallCostMin": "Recall Cost (Min)",
"recallCostMax": "Recall Cost (Max)",
"evasionMin": "Evasion (Min)",
"evasionMax": "Evasion (Max)",
"subtype": "Subtype",
"folders": {
"characters": "Characters",
"adversaries": "Adversaries",
"ancestries": "Ancestries",
"equipment": "Equipment",
"classes": "Classes",
"subclasses": "Subclasses",
"domainCards": "Domain Cards",
"communities": "Communities",
"environments": "Environments",
"beastforms": "Beastforms",
"features": "Features",
"items": "Items",
"weapons": "Weapons",
"armors": "Armors",
"consumables": "Consumables",
"loots": "Loots"
} }
}, },
"Notifications": { "Notifications": {
@ -2319,6 +2588,7 @@
"beastformInapplicable": "A beastform can only be applied to a Character.", "beastformInapplicable": "A beastform can only be applied to a Character.",
"beastformAlreadyApplied": "The character already has a beastform applied!", "beastformAlreadyApplied": "The character already has a beastform applied!",
"noTargetsSelected": "No targets are selected.", "noTargetsSelected": "No targets are selected.",
"noTargetsSelectedOrPerm": "No targets are selected or with the update permission.",
"attackTargetDoesNotExist": "The target token no longer exists", "attackTargetDoesNotExist": "The target token no longer exists",
"insufficentAdvancements": "You don't have enough advancements left.", "insufficentAdvancements": "You don't have enough advancements left.",
"noAssignedPlayerCharacter": "You have no assigned character.", "noAssignedPlayerCharacter": "You have no assigned character.",
@ -2346,6 +2616,8 @@
"wrongDomain": "The card isn't from one of your class domains.", "wrongDomain": "The card isn't from one of your class domains.",
"cardTooHighLevel": "The card is too high level!", "cardTooHighLevel": "The card is too high level!",
"duplicateCard": "You cannot select the same card more than once.", "duplicateCard": "You cannot select the same card more than once.",
"duplicateCharacter": "This actor is already registered in the party members list.",
"onlyCharactersInPartySheet": "You can drag only characters to a party sheet.",
"notPrimary": "The weapon is not a primary weapon!", "notPrimary": "The weapon is not a primary weapon!",
"notSecondary": "The weapon is not a secondary weapon!", "notSecondary": "The weapon is not a secondary weapon!",
"itemTooHighTier": "The item must be from Tier1", "itemTooHighTier": "The item must be from Tier1",
@ -2373,11 +2645,23 @@
"beastformEquipWeapon": "You cannot use weapons while in a Beastform.", "beastformEquipWeapon": "You cannot use weapons while in a Beastform.",
"loadoutMaxReached": "You've reached maximum loadout. Move atleast one domain card to the vault, or increase the limit in homebrew settings if desired.", "loadoutMaxReached": "You've reached maximum loadout. Move atleast one domain card to the vault, or increase the limit in homebrew settings if desired.",
"domainMaxReached": "You've reached the maximum domains for the class. Increase the limit in homebrew settings if desired.", "domainMaxReached": "You've reached the maximum domains for the class. Increase the limit in homebrew settings if desired.",
"insufficientResources": "You have insufficient resources", "insufficientResources": "You don't have enough resources to use that action.",
"actionNoUsesRemaining": "That action doesn't have remaining uses.",
"multiclassAlreadyPresent": "You already have a class and multiclass", "multiclassAlreadyPresent": "You already have a class and multiclass",
"subclassesAlreadyPresent": "You already have a class and multiclass subclass", "subclassesAlreadyPresent": "You already have a class and multiclass subclass",
"noDiceSystem": "Your selected dice {system} does not have a {faces} dice", "noDiceSystem": "Your selected dice {system} does not have a {faces} dice",
"subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class." "gmMenuRefresh": "You refreshed all actions and resources {types}",
"subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class.",
"gmRequired": "This action requires an online GM",
"gmOnly": "This can only be accessed by the GM",
"noActorOwnership": "You do not have permissions for this character"
},
"Sidebar": {
"daggerheartMenu": {
"title": "Daggerheart Menu",
"startSession": "Start Session",
"startScene": "Start Scene"
}
}, },
"Tooltip": { "Tooltip": {
"disableEffect": "Disable Effect", "disableEffect": "Disable Effect",
@ -2404,7 +2688,9 @@
"rulesOff": "Rules Off", "rulesOff": "Rules Off",
"remainingUses": "Uses refresh on {type}", "remainingUses": "Uses refresh on {type}",
"rightClickExtand": "Right-Click to extand", "rightClickExtand": "Right-Click to extand",
"companionPartnerLevelBlock": "The companion needs an assigned partner to level up." "companionPartnerLevelBlock": "The companion needs an assigned partner to level up.",
"configureAttribution": "Configure Attribution",
"deleteItem": "Delete Item"
} }
} }
} }

View file

@ -2,8 +2,10 @@ export * as characterCreation from './characterCreation/_module.mjs';
export * as dialogs from './dialogs/_module.mjs'; export * as dialogs from './dialogs/_module.mjs';
export * as hud from './hud/_module.mjs'; export * as hud from './hud/_module.mjs';
export * as levelup from './levelup/_module.mjs'; export * as levelup from './levelup/_module.mjs';
export * as scene from './scene/_module.mjs';
export * as settings from './settings/_module.mjs'; export * as settings from './settings/_module.mjs';
export * as sheets from './sheets/_module.mjs'; export * as sheets from './sheets/_module.mjs';
export * as sheetConfigs from './sheets-configs/_module.mjs'; export * as sheetConfigs from './sheets-configs/_module.mjs';
export * as sidebar from './sidebar/_module.mjs';
export * as ui from './ui/_module.mjs'; export * as ui from './ui/_module.mjs';
export * as ux from './ux/_module.mjs'; export * as ux from './ux/_module.mjs';

View file

@ -1,6 +1,5 @@
import { abilities } from '../../config/actorConfig.mjs'; import { abilities } from '../../config/actorConfig.mjs';
import { burden } from '../../config/generalConfig.mjs'; import { burden } from '../../config/generalConfig.mjs';
import { ItemBrowser } from '../ui/itemBrowser.mjs';
import { createEmbeddedItemsWithEffects, createEmbeddedItemWithEffects } from '../../helpers/utils.mjs'; import { createEmbeddedItemsWithEffects, createEmbeddedItemWithEffects } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -46,8 +45,6 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
}; };
this._dragDrop = this._createDragDropHandlers(); this._dragDrop = this._createDragDropHandlers();
this.itemBrowser = null;
} }
get title() { get title() {
@ -86,9 +83,9 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
static PARTS = { static PARTS = {
tabs: { template: 'systems/daggerheart/templates/characterCreation/tabs.hbs' }, tabs: { template: 'systems/daggerheart/templates/characterCreation/tabs.hbs' },
class: { template: 'systems/daggerheart/templates/characterCreation/tabs/class.hbs' },
ancestry: { template: 'systems/daggerheart/templates/characterCreation/tabs/ancestry.hbs' }, ancestry: { template: 'systems/daggerheart/templates/characterCreation/tabs/ancestry.hbs' },
community: { template: 'systems/daggerheart/templates/characterCreation/tabs/community.hbs' }, community: { template: 'systems/daggerheart/templates/characterCreation/tabs/community.hbs' },
class: { template: 'systems/daggerheart/templates/characterCreation/tabs/class.hbs' },
traits: { template: 'systems/daggerheart/templates/characterCreation/tabs/traits.hbs' }, traits: { template: 'systems/daggerheart/templates/characterCreation/tabs/traits.hbs' },
experience: { template: 'systems/daggerheart/templates/characterCreation/tabs/experience.hbs' }, experience: { template: 'systems/daggerheart/templates/characterCreation/tabs/experience.hbs' },
domainCards: { template: 'systems/daggerheart/templates/characterCreation/tabs/domainCards.hbs' }, domainCards: { template: 'systems/daggerheart/templates/characterCreation/tabs/domainCards.hbs' },
@ -98,6 +95,13 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
}; };
static TABS = { static TABS = {
class: {
active: false,
cssClass: '',
group: 'setup',
id: 'class',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.class'
},
ancestry: { ancestry: {
active: true, active: true,
cssClass: '', cssClass: '',
@ -112,13 +116,6 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
id: 'community', id: 'community',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.community' label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.community'
}, },
class: {
active: false,
cssClass: '',
group: 'setup',
id: 'class',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.class'
},
traits: { traits: {
active: false, active: false,
cssClass: '', cssClass: '',
@ -159,10 +156,10 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
v.cssClass = v.active ? 'active' : ''; v.cssClass = v.active ? 'active' : '';
switch (v.id) { switch (v.id) {
case 'community': case 'ancestry':
v.disabled = this.setup.visibility < 2; v.disabled = this.setup.visibility < 2;
break; break;
case 'class': case 'community':
v.disabled = this.setup.visibility < 3; v.disabled = this.setup.visibility < 3;
break; break;
case 'traits': case 'traits':
@ -195,7 +192,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
} }
async _prepareContext(_options) { async _prepareContext(_options) {
this.tabGroups.setup = this.tabGroups.setup ?? 'ancestry'; this.tabGroups.setup = this.tabGroups.setup ?? 'class';
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.tabs = this._getTabs(this.constructor.TABS); context.tabs = this._getTabs(this.constructor.TABS);
@ -269,13 +266,13 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
context.isLastTab = this.tabGroups.setup === 'equipment'; context.isLastTab = this.tabGroups.setup === 'equipment';
switch (this.tabGroups.setup) { switch (this.tabGroups.setup) {
case null: case null:
case 'ancestry': case 'class':
context.nextDisabled = this.setup.visibility === 1; context.nextDisabled = this.setup.visibility === 1;
break; break;
case 'community': case 'ancestry':
context.nextDisabled = this.setup.visibility === 2; context.nextDisabled = this.setup.visibility === 2;
break; break;
case 'class': case 'community':
context.nextDisabled = this.setup.visibility === 3; context.nextDisabled = this.setup.visibility === 3;
break; break;
case 'traits': case 'traits':
@ -366,11 +363,11 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
case 4: case 4:
return this.getNrSelectedTrait() === 6 ? 5 : 4; return this.getNrSelectedTrait() === 6 ? 5 : 4;
case 3: case 3:
return this.setup.class.uuid && this.setup.subclass.uuid ? 4 : 3; return this.setup.community.uuid ? 4 : 3;
case 2: case 2:
return this.setup.community.uuid ? 3 : 2; return this.setup.primaryAncestry.uuid ? 3 : 2;
case 1: case 1:
return this.setup.primaryAncestry.uuid ? 2 : 1; return this.setup.class.uuid && this.setup.subclass.uuid ? 2 : 1;
} }
} }
@ -425,8 +422,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
equipment = ['armor', 'weapon']; equipment = ['armor', 'weapon'];
const presets = { const presets = {
compendium: 'daggerheart', folder: equipment.includes(type) ? `equipments.folders.${type}s` : type,
folder: equipment.includes(type) ? 'equipments' : type,
render: { render: {
noFolder: true noFolder: true
} }
@ -442,14 +438,14 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
presets.filter = { presets.filter = {
'system.linkedClass.uuid': { key: 'system.linkedClass.uuid', value: this.setup.class?.uuid } 'system.linkedClass.uuid': { key: 'system.linkedClass.uuid', value: this.setup.class?.uuid }
}; };
if (equipment.includes(type)) if (equipment.includes(type))
presets.filter = { presets.filter = {
'system.tier': { key: 'system.tier', value: 1 }, 'system.tier': { key: 'system.tier', value: 1 },
'type': { key: 'type', value: type } 'type': { key: 'type', value: type }
}; };
return (this.itemBrowser = await new ItemBrowser({ presets }).render({ force: true })); ui.compendiumBrowser.open(presets);
} }
static async viewItem(_, target) { static async viewItem(_, target) {
@ -477,10 +473,10 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
static setupGoNext() { static setupGoNext() {
switch (this.setup.visibility) { switch (this.setup.visibility) {
case 2: case 2:
this.tabGroups.setup = 'community'; this.tabGroups.setup = 'ancestry';
break; break;
case 3: case 3:
this.tabGroups.setup = 'class'; this.tabGroups.setup = 'community';
break; break;
case 4: case 4:
this.tabGroups.setup = 'traits'; this.tabGroups.setup = 'traits';
@ -567,7 +563,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
{ overwrite: true } { overwrite: true }
); );
if (this.itemBrowser) this.itemBrowser.close(); if (ui.compendiumBrowser) ui.compendiumBrowser.close();
this.close(); this.close();
} }

View file

@ -1,11 +1,15 @@
export { default as AttributionDialog } from './attributionDialog.mjs';
export { default as BeastformDialog } from './beastformDialog.mjs'; export { default as BeastformDialog } from './beastformDialog.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';
export { default as DeathMove } from './deathMove.mjs'; export { default as DeathMove } from './deathMove.mjs';
export { default as Downtime } from './downtime.mjs'; export { default as Downtime } from './downtime.mjs';
export { default as ImageSelectDialog } from './imageSelectDialog.mjs';
export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.mjs'; export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.mjs';
export { default as OwnershipSelection } from './ownershipSelection.mjs'; export { default as OwnershipSelection } from './ownershipSelection.mjs';
export { default as RerollDamageDialog } from './rerollDamageDialog.mjs'; export { default as RerollDamageDialog } from './rerollDamageDialog.mjs';
export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs'; 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 TagTeamDialog } from './tagTeamDialog.mjs';

View file

@ -0,0 +1,93 @@
import autocomplete from 'autocompleter';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class AttributionDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(item) {
super({});
this.item = item;
this.sources = Object.keys(CONFIG.DH.GENERAL.attributionSources).flatMap(groupKey => {
const group = CONFIG.DH.GENERAL.attributionSources[groupKey];
return group.values.map(x => ({ group: group.label, ...x }));
});
}
get title() {
return game.i18n.localize('DAGGERHEART.APPLICATIONS.Attribution.title');
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dh-style', 'dialog', 'views', 'attribution'],
position: { width: 'auto', height: 'auto' },
window: { icon: 'fa-solid fa-signature' },
form: { handler: this.updateData, submitOnChange: false, closeOnSubmit: true }
};
static PARTS = {
main: { template: 'systems/daggerheart/templates/dialogs/attribution.hbs' }
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
const sources = this.sources;
htmlElement.querySelectorAll('.attribution-input').forEach(element => {
autocomplete({
input: element,
fetch: function (text, update) {
if (!text) {
update(sources);
} else {
text = text.toLowerCase();
var suggestions = sources.filter(n => n.label.toLowerCase().includes(text));
update(suggestions);
}
},
render: function (item, search) {
const label = game.i18n.localize(item.label);
const matchIndex = label.toLowerCase().indexOf(search);
const beforeText = label.slice(0, matchIndex);
const matchText = label.slice(matchIndex, matchIndex + search.length);
const after = label.slice(matchIndex + search.length, label.length);
const element = document.createElement('li');
element.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
if (item.hint) {
element.dataset.tooltip = game.i18n.localize(item.hint);
}
return element;
},
renderGroup: function (label) {
const itemElement = document.createElement('div');
itemElement.textContent = game.i18n.localize(label);
return itemElement;
},
onSelect: function (item) {
element.value = item.label;
},
click: e => e.fetch(),
customize: function (_input, _inputRect, container) {
container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ;
},
minLength: 0
});
});
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.item = this.item;
context.data = this.item.system.attribution;
return context;
}
static async updateData(_event, _element, formData) {
await this.item.update({ 'system.attribution': formData.object });
this.item.sheet.refreshFrame();
}
}

View file

@ -276,7 +276,22 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
const featureItem = item; const featureItem = item;
app.addEventListener( app.addEventListener(
'close', 'close',
() => resolve({ selected: app.selected, evolved: app.evolved, hybrid: app.hybrid, item: featureItem }), async () => {
const selected = app.selected.toObject();
const data = await game.system.api.data.items.DHBeastform.getWildcardImage(
app.configData.data.parent,
app.selected
);
if (data) {
if (!data.selectedImage) selected = null;
else {
if (data.usesDynamicToken) selected.system.tokenRingImg = data.selectedImage;
else selected.system.tokenImg = data.selectedImage;
}
}
resolve({ selected: selected, evolved: app.evolved, hybrid: app.hybrid, item: featureItem });
},
{ once: true } { once: true }
); );
app.render({ force: true }); app.render({ force: true });

View file

@ -1,3 +1,5 @@
import { abilities } from '../../config/actorConfig.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class D20RollDialog extends HandlebarsApplicationMixin(ApplicationV2) { export default class D20RollDialog extends HandlebarsApplicationMixin(ApplicationV2) {
@ -7,20 +9,20 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.roll = roll; this.roll = roll;
this.config = config; this.config = config;
this.config.experiences = []; this.config.experiences = [];
this.reactionOverride = config.roll?.type === 'reaction'; this.reactionOverride = config.actionType === 'reaction';
if (config.source?.action) { if (config.source?.action) {
this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent; this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent;
this.action = this.action =
config.data.attack?._id == config.source.action config.data.attack?._id == config.source.action
? config.data.attack ? config.data.attack
: this.item.system.actions.get(config.source.action); : this.item.system.actionsList?.find(a => a.id === config.source.action);
} }
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
id: 'roll-selection', // id: 'roll-selection',
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'roll-selection'], classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'roll-selection'],
position: { position: {
width: 'auto' width: 'auto'
@ -32,6 +34,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
updateIsAdvantage: this.updateIsAdvantage, updateIsAdvantage: this.updateIsAdvantage,
selectExperience: this.selectExperience, selectExperience: this.selectExperience,
toggleReaction: this.toggleReaction, toggleReaction: this.toggleReaction,
toggleTagTeamRoll: this.toggleTagTeamRoll,
submitRoll: this.submitRoll submitRoll: this.submitRoll
}, },
form: { form: {
@ -42,7 +45,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
}; };
get title() { get title() {
return this.config.title; return `${this.config.title}${this.actor ? `: ${this.actor.name}` : ''}`;
} }
get actor() { get actor() {
@ -66,7 +69,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.rollConfig = this.config; context.rollConfig = this.config;
context.hasRoll = !!this.config.roll; context.hasRoll = !!this.config.roll;
context.canRoll = true; context.canRoll = true;
context.selectedRollMode = this.config.selectedRollMode; context.selectedRollMode = this.config.selectedRollMode ?? game.settings.get('core', 'rollMode');
context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({ context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({
action, action,
label, label,
@ -81,7 +84,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
); );
context.costs = updatedCosts.map(x => ({ context.costs = updatedCosts.map(x => ({
...x, ...x,
label: x.keyIsID label: x.itemId
? this.action.parent.parent.name ? this.action.parent.parent.name
: game.i18n.localize(CONFIG.DH.GENERAL.abilityCosts[x.key].label) : game.i18n.localize(CONFIG.DH.GENERAL.abilityCosts[x.key].label)
})); }));
@ -113,15 +116,31 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.isLite = this.config.roll?.lite; context.isLite = this.config.roll?.lite;
context.extraFormula = this.config.extraFormula; context.extraFormula = this.config.extraFormula;
context.formula = this.roll.constructFormula(this.config); context.formula = this.roll.constructFormula(this.config);
if (this.actor.system.traits) context.abilities = this.getTraitModifiers();
context.showReaction = !context.rollConfig.type && context.rollType === 'DualityRoll'; context.showReaction = !this.config.roll?.type && context.rollType === 'DualityRoll';
context.reactionOverride = this.reactionOverride; context.reactionOverride = this.reactionOverride;
} }
const tagTeamSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
if (tagTeamSetting.members[this.actor.id] && !this.config.skips?.createMessage) {
context.activeTagTeamRoll = true;
context.tagTeamSelected = this.config.tagTeamSelected;
}
return context; return context;
} }
getTraitModifiers() {
return Object.values(abilities).map(a => ({
id: a.id,
label: `${game.i18n.localize(a.label)} (${this.actor.system.traits[a.id]?.value.signedString() ?? 0})`
}));
}
static updateRollConfiguration(event, _, formData) { static updateRollConfiguration(event, _, formData) {
const { ...rest } = foundry.utils.expandObject(formData.object); const { ...rest } = foundry.utils.expandObject(formData.object);
this.config.selectedRollMode = rest.selectedRollMode; this.config.selectedRollMode = rest.selectedRollMode;
if (this.config.costs) { if (this.config.costs) {
@ -133,6 +152,12 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.roll[key] = value; this.roll[key] = value;
}); });
} }
if (rest.hasOwnProperty('trait')) {
this.config.roll.trait = rest.trait;
this.config.title = game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(abilities[this.config.roll.trait]?.label)
});
}
this.config.extraFormula = rest.extraFormula; this.config.extraFormula = rest.extraFormula;
this.render(); this.render();
} }
@ -151,35 +176,38 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.config.experiences.indexOf(button.dataset.key) > -1 this.config.experiences.indexOf(button.dataset.key) > -1
? this.config.experiences.filter(x => x !== button.dataset.key) ? this.config.experiences.filter(x => x !== button.dataset.key)
: [...this.config.experiences, button.dataset.key]; : [...this.config.experiences, button.dataset.key];
if (this.config?.data?.parent?.type === 'character' || this.config?.data?.parent?.type === 'companion') { this.config.costs =
this.config.costs = this.config.costs.indexOf(this.config.costs.find(c => c.extKey === button.dataset.key)) > -1
this.config.costs.indexOf(this.config.costs.find(c => c.extKey === button.dataset.key)) > -1 ? this.config.costs.filter(x => x.extKey !== button.dataset.key)
? this.config.costs.filter(x => x.extKey !== button.dataset.key) : [
: [ ...this.config.costs,
...this.config.costs, {
{ extKey: button.dataset.key,
extKey: button.dataset.key, key: this.config?.data?.parent?.isNPC ? 'fear' : 'hope',
key: 'hope', value: 1,
value: 1, name: this.config.data?.experiences?.[button.dataset.key]?.name
name: this.config.data?.experiences?.[button.dataset.key]?.name }
} ];
];
}
this.render(); this.render();
} }
static toggleReaction() { static toggleReaction() {
if (this.config.roll) { if (this.config.roll) {
this.reactionOverride = !this.reactionOverride; this.reactionOverride = !this.reactionOverride;
this.config.roll.type = this.reactionOverride this.config.actionType = this.reactionOverride
? CONFIG.DH.ITEM.actionTypes.reaction.id ? CONFIG.DH.ITEM.actionTypes.reaction.id
: this.config.roll.type === CONFIG.DH.ITEM.actionTypes.reaction.id : this.config.actionType === CONFIG.DH.ITEM.actionTypes.reaction.id
? null ? null
: this.config.roll.type; : this.config.actionType;
this.render(); this.render();
} }
} }
static toggleTagTeamRoll() {
this.config.tagTeamSelected = !this.config.tagTeamSelected;
this.render();
}
static async submitRoll() { static async submitRoll() {
await this.close({ submitted: true }); await this.close({ submitted: true });
} }

View file

@ -0,0 +1,196 @@
import autocomplete from 'autocompleter';
import { abilities } from '../../config/actorConfig.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class GroupRollDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actors) {
super();
this.actors = actors;
this.actorLeader = {};
this.actorsMembers = [];
}
get title() {
return 'Group Roll';
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'group-roll'],
position: { width: 'auto', height: 'auto' },
window: {
title: 'DAGGERHEART.UI.Chat.groupRoll.title'
},
actions: {
roll: GroupRollDialog.#roll,
removeLeader: GroupRollDialog.#removeLeader,
removeMember: GroupRollDialog.#removeMember
},
form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false }
};
static PARTS = {
application: {
id: 'group-roll',
template: 'systems/daggerheart/templates/dialogs/group-roll/group-roll.hbs'
}
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
const leaderChoices = this.actors.filter(x => this.actorsMembers.every(member => member.actor?.id !== x.id));
const memberChoices = this.actors.filter(
x => this.actorLeader?.actor?.id !== x.id && this.actorsMembers.every(member => member.actor?.id !== x.id)
);
htmlElement.querySelectorAll('.leader-change-input').forEach(element => {
autocomplete({
input: element,
fetch: function (text, update) {
if (!text) {
update(leaderChoices);
} else {
text = text.toLowerCase();
var suggestions = leaderChoices.filter(n => n.name.toLowerCase().includes(text));
update(suggestions);
}
},
render: function (actor, search) {
const actorName = game.i18n.localize(actor.name);
const matchIndex = actorName.toLowerCase().indexOf(search);
const beforeText = actorName.slice(0, matchIndex);
const matchText = actorName.slice(matchIndex, matchIndex + search.length);
const after = actorName.slice(matchIndex + search.length, actorName.length);
const img = document.createElement('img');
img.src = actor.img;
const element = document.createElement('li');
element.appendChild(img);
const label = document.createElement('span');
label.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
element.appendChild(label);
return element;
},
renderGroup: function (label) {
const itemElement = document.createElement('div');
itemElement.textContent = game.i18n.localize(label);
return itemElement;
},
onSelect: actor => {
element.value = actor.uuid;
this.actorLeader = { actor: actor, trait: 'agility', difficulty: 0 };
this.render();
},
click: e => e.fetch(),
customize: function (_input, _inputRect, container) {
container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ;
},
minLength: 0
});
});
htmlElement.querySelectorAll('.team-push-input').forEach(element => {
autocomplete({
input: element,
fetch: function (text, update) {
if (!text) {
update(memberChoices);
} else {
text = text.toLowerCase();
var suggestions = memberChoices.filter(n => n.name.toLowerCase().includes(text));
update(suggestions);
}
},
render: function (actor, search) {
const actorName = game.i18n.localize(actor.name);
const matchIndex = actorName.toLowerCase().indexOf(search);
const beforeText = actorName.slice(0, matchIndex);
const matchText = actorName.slice(matchIndex, matchIndex + search.length);
const after = actorName.slice(matchIndex + search.length, actorName.length);
const img = document.createElement('img');
img.src = actor.img;
const element = document.createElement('li');
element.appendChild(img);
const label = document.createElement('span');
label.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
element.appendChild(label);
return element;
},
renderGroup: function (label) {
const itemElement = document.createElement('div');
itemElement.textContent = game.i18n.localize(label);
return itemElement;
},
onSelect: actor => {
element.value = actor.uuid;
this.actorsMembers.push({ actor: actor, trait: 'agility', difficulty: 0 });
this.render({ force: true });
},
click: e => e.fetch(),
customize: function (_input, _inputRect, container) {
container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ;
},
minLength: 0
});
});
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.leader = this.actorLeader;
context.members = this.actorsMembers;
context.traitList = abilities;
context.allSelected = this.actorsMembers.length + (this.actorLeader?.actor ? 1 : 0) === this.actors.length;
context.rollDisabled = context.members.length === 0 || !this.actorLeader?.actor;
return context;
}
static updateData(event, _, formData) {
const { actorLeader, actorsMembers } = foundry.utils.expandObject(formData.object);
this.actorLeader = foundry.utils.mergeObject(this.actorLeader, actorLeader);
this.actorsMembers = foundry.utils.mergeObject(this.actorsMembers, actorsMembers);
this.render(true);
}
static async #removeLeader(_, button) {
this.actorLeader = null;
this.render();
}
static async #removeMember(_, button) {
this.actorsMembers = this.actorsMembers.filter(m => m.actor.uuid !== button.dataset.memberUuid);
this.render();
}
static async #roll() {
const cls = getDocumentClass('ChatMessage');
const systemData = {
leader: this.actorLeader,
members: this.actorsMembers
};
const msg = {
type: 'groupRoll',
user: game.user.id,
speaker: cls.getSpeaker(),
title: game.i18n.localize('DAGGERHEART.UI.Chat.groupRoll.title'),
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/groupRoll.hbs',
{ system: systemData }
)
};
cls.create(msg);
this.close();
}
}

View file

@ -0,0 +1,67 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class ImageSelectDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(titleName, images) {
super();
this.titleName = titleName;
this.images = images;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dialog', 'dh-style', 'image-select'],
position: {
width: 600,
height: 'auto'
},
window: {
icon: 'fa-solid fa-paw'
},
actions: {
selectImage: ImageSelectDialog.#selectImage,
finishSelection: ImageSelectDialog.#finishSelection
}
};
get title() {
return this.titleName;
}
/** @override */
static PARTS = {
main: { template: 'systems/daggerheart/templates/dialogs/image-select/main.hbs' },
footer: { template: 'systems/daggerheart/templates/dialogs/image-select/footer.hbs' }
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.images = this.images;
context.selectedImage = this.selectedImage;
return context;
}
static #selectImage(_event, button) {
this.selectedImage = button.dataset.image ?? button.querySelector('img').dataset.image;
this.render();
}
static #finishSelection() {
this.close({ submitted: true });
}
async close(options = {}) {
if (!options.submitted) this.selectedImage = null;
await super.close();
}
static async configure(title, images) {
return new Promise(resolve => {
const app = new this(title, images);
app.addEventListener('close', () => resolve(app.selectedImage), { once: true });
app.render({ force: true });
});
}
}

View file

@ -1,18 +1,20 @@
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class OwnershipSelection extends HandlebarsApplicationMixin(ApplicationV2) { export default class OwnershipSelection extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(resolve, reject, name, ownership) { constructor(name, ownership, defaultOwnership) {
super({}); super({});
this.resolve = resolve;
this.reject = reject;
this.name = name; this.name = name;
this.ownership = ownership; this.ownership = foundry.utils.deepClone(ownership);
this.defaultOwnership = defaultOwnership;
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
classes: ['daggerheart', 'views', 'ownership-selection'], classes: ['daggerheart', 'views', 'dialog', 'dh-style', 'ownership-selection'],
window: {
icon: 'fa-solid fa-users'
},
position: { position: {
width: 600, width: 600,
height: 'auto' height: 'auto'
@ -30,43 +32,48 @@ export default class OwnershipSelection extends HandlebarsApplicationMixin(Appli
return game.i18n.format('DAGGERHEART.APPLICATIONS.OwnershipSelection.title', { name: this.name }); return game.i18n.format('DAGGERHEART.APPLICATIONS.OwnershipSelection.title', { name: this.name });
} }
getOwnershipData(id) {
return this.ownership[id] ?? CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT;
}
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.ownershipOptions = Object.keys(CONST.DOCUMENT_OWNERSHIP_LEVELS).map(level => ({ context.ownershipDefaultOptions = CONFIG.DH.GENERAL.basicOwnershiplevels;
value: CONST.DOCUMENT_OWNERSHIP_LEVELS[level], context.ownershipOptions = CONFIG.DH.GENERAL.simpleOwnershiplevels;
label: game.i18n.localize(`OWNERSHIP.${level}`) context.defaultOwnership = this.defaultOwnership;
})); context.ownership = game.users.reduce((acc, user) => {
context.ownership = { if (!user.isGM) {
default: this.ownership.default, acc[user.id] = {
players: Object.keys(this.ownership.players).reduce((acc, x) => { ...user,
const user = game.users.get(x); img: user.character?.img ?? 'icons/svg/cowled.svg',
if (!user.isGM) { ownership: this.getOwnershipData(user.id)
acc[x] = { };
img: user.character?.img ?? 'icons/svg/cowled.svg', }
name: user.name,
ownership: this.ownership.players[x].value
};
}
return acc; return acc;
}, {}) }, {});
};
return context; return context;
} }
static async updateData(event, _, formData) { static async updateData(event, _, formData) {
const { ownership } = foundry.utils.expandObject(formData.object); const data = foundry.utils.expandObject(formData.object);
this.close(data);
this.resolve(ownership);
this.close(true);
} }
async close(fromSave) { async close(data) {
if (!fromSave) { if (data) {
this.reject(); this.saveData = data;
} }
await super.close(); await super.close();
} }
static async configure(name, ownership, defaultOwnership) {
return new Promise(resolve => {
const app = new this(name, ownership, defaultOwnership);
app.addEventListener('close', () => resolve(app.saveData), { once: true });
app.render({ force: true });
});
}
} }

View file

@ -1,3 +1,5 @@
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) { export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) {
@ -122,6 +124,15 @@ export default class RerollDamageDialog extends HandlebarsApplicationMixin(Appli
}, {}) }, {})
}; };
await this.message.update(update); await this.message.update(update);
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
await this.close(); await this.close();
} }

View file

@ -0,0 +1,315 @@
import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class TagTeamDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(party) {
super();
this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
this.party = party;
this.setupHooks = Hooks.on(socketEvent.Refresh, ({ refreshType }) => {
if (refreshType === RefreshType.TagTeamRoll) {
this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
this.render();
}
});
}
get title() {
return game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title');
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'tag-team-dialog'],
position: { width: 550, height: 'auto' },
actions: {
removeMember: TagTeamDialog.#removeMember,
unlinkMessage: TagTeamDialog.#unlinkMessage,
selectMessage: TagTeamDialog.#selectMessage,
createTagTeam: TagTeamDialog.#createTagTeam
},
form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false }
};
static PARTS = {
application: {
id: 'tag-team-dialog',
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog.hbs'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.hopeCost = this.hopeCost;
context.data = this.data;
context.memberOptions = this.party.filter(c => !this.data.members[c.id]);
context.selectedCharacterOptions = this.party.filter(c => this.data.members[c.id]);
context.members = Object.keys(this.data.members).map(id => {
const roll = this.data.members[id].messageId ? game.messages.get(this.data.members[id].messageId) : null;
context.usesDamage =
context.usesDamage === undefined
? roll?.system.hasDamage
: context.usesDamage && roll?.system.hasDamage;
return {
character: this.party.find(x => x.id === id),
selected: this.data.members[id].selected,
roll: roll,
damageValues: roll
? Object.keys(roll.system.damage).map(key => ({
key: key,
name: game.i18n.localize(CONFIG.DH.GENERAL.healingTypes[key].label),
total: roll.system.damage[key].total
}))
: null
};
});
const initiatorChar = this.party.find(x => x.id === this.data.initiator.id);
context.initiator = {
character: initiatorChar,
cost: this.data.initiator.cost
};
context.selectedData = Object.values(context.members).reduce(
(acc, member) => {
if (!member.roll) return acc;
if (member.selected) {
acc.result = `${member.roll.system.roll.total} ${member.roll.system.roll.result.label}`;
}
if (context.usesDamage) {
if (!acc.damageValues) acc.damageValues = {};
for (let damage of member.damageValues) {
if (acc.damageValues[damage.key]) {
acc.damageValues[damage.key].total += damage.total;
} else {
acc.damageValues[damage.key] = foundry.utils.deepClone(damage);
}
}
}
return acc;
},
{ result: null, damageValues: null }
);
context.showResult = Object.values(context.members).reduce((enabled, member) => {
if (!member.roll) return enabled;
if (context.usesDamage) {
enabled = enabled === null ? member.damageValues.length > 0 : enabled && member.damageValues.length > 0;
} else {
enabled = enabled === null ? Boolean(member.roll) : enabled && Boolean(member.roll);
}
return enabled;
}, null);
context.createDisabled =
!context.selectedData.result ||
!this.data.initiator.id ||
Object.keys(this.data.members).length === 0 ||
Object.values(context.members).some(x =>
context.usesDamage ? !x.damageValues || x.damageValues.length === 0 : !x.roll
);
return context;
}
async updateSource(update) {
await this.data.updateSource(update);
if (game.user.isGM) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, this.data.toObject());
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
} else {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll,
update: this.data.toObject(),
refresh: { refreshType: RefreshType.TagTeamRoll }
}
});
}
}
static async updateData(_event, _element, formData) {
const { selectedAddMember, initiator } = foundry.utils.expandObject(formData.object);
const update = { initiator: initiator };
if (selectedAddMember) {
const member = await foundry.utils.fromUuid(selectedAddMember);
update[`members.${member.id}`] = { messageId: null };
}
await this.updateSource(update);
this.render();
}
static async #removeMember(_, button) {
const update = { [`members.-=${button.dataset.characterId}`]: null };
if (this.data.initiator.id === button.dataset.characterId) {
update.iniator = { id: null };
}
await this.updateSource(update);
}
static async #unlinkMessage(_, button) {
await this.updateSource({ [`members.${button.id}.messageId`]: null });
}
static async #selectMessage(_, button) {
const member = this.data.members[button.id];
const currentSelected = Object.keys(this.data.members).find(key => this.data.members[key].selected);
const curretSelectedUpdate =
currentSelected && currentSelected !== button.id ? { [`${currentSelected}`]: { selected: false } } : {};
await this.updateSource({
members: {
[`${button.id}`]: { selected: !member.selected },
...curretSelectedUpdate
}
});
}
static async #createTagTeam() {
const mainRollId = Object.keys(this.data.members).find(key => this.data.members[key].selected);
const mainRoll = game.messages.get(this.data.members[mainRollId].messageId);
if (this.data.initiator.cost) {
const initiator = this.party.find(x => x.id === this.data.initiator.id);
if (initiator.system.resources.hope.value < this.data.initiator.cost) {
return ui.notifications.warn(
game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.insufficientHope')
);
}
}
const secondaryRolls = Object.keys(this.data.members)
.filter(key => key !== mainRollId)
.map(key => game.messages.get(this.data.members[key].messageId));
const systemData = foundry.utils.deepClone(mainRoll).system.toObject();
for (let roll of secondaryRolls) {
if (roll.system.hasDamage) {
for (let key in roll.system.damage) {
var damage = roll.system.damage[key];
if (systemData.damage[key]) {
systemData.damage[key].total += damage.total;
systemData.damage[key].parts = [...systemData.damage[key].parts, ...damage.parts];
} else {
systemData.damage[key] = damage;
}
}
}
}
systemData.title = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.chatMessageRollTitle');
const cls = getDocumentClass('ChatMessage'),
msgData = {
type: 'dualityRoll',
user: game.user.id,
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title'),
speaker: cls.getSpeaker({ actor: this.party.find(x => x.id === mainRollId) }),
system: systemData,
rolls: mainRoll.rolls,
sound: null,
flags: { core: { RollTable: true } }
};
await cls.create(msgData);
const fearUpdate = { key: 'fear', value: null, total: null, enabled: true };
for (let memberId of Object.keys(this.data.members)) {
const resourceUpdates = [];
if (systemData.roll.isCritical || systemData.roll.result.duality === 1) {
const value =
memberId !== this.data.initiator.id
? 1
: this.data.initiator.cost
? 1 - this.data.initiator.cost
: 1;
resourceUpdates.push({ key: 'hope', value: value, total: -value, enabled: true });
}
if (systemData.roll.isCritical) resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true });
if (systemData.roll.result.duality === -1) {
fearUpdate.value = fearUpdate.value === null ? 1 : fearUpdate.value + 1;
fearUpdate.total = fearUpdate.total === null ? -1 : fearUpdate.total - 1;
}
this.party.find(x => x.id === memberId).modifyResource(resourceUpdates);
}
if (fearUpdate.value) {
this.party.find(x => x.id === mainRollId).modifyResource([fearUpdate]);
}
/* Improve by fetching default from schema */
const update = { members: [], initiator: { id: null, cost: 3 } };
if (game.user.isGM) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, update);
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
} else {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll,
update: update,
refresh: { refreshType: RefreshType.TagTeamRoll }
}
});
}
}
static async assignRoll(char, message) {
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
const character = settings.members[char.id];
if (!character) return;
await settings.updateSource({ [`members.${char.id}.messageId`]: message.id });
if (game.user.isGM) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, settings);
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
} else {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll,
update: settings,
refresh: { refreshType: RefreshType.TagTeamRoll }
}
});
}
}
async close(options = {}) {
Hooks.off(socketEvent.Refresh, this.setupHooks);
await super.close(options);
}
}

View file

@ -1,6 +1,12 @@
import { shuffleArray } from '../../helpers/utils.mjs';
export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['daggerheart'] classes: ['daggerheart'],
actions: {
combat: DHTokenHUD.#onToggleCombat,
togglePartyTokens: DHTokenHUD.#togglePartyTokens
}
}; };
/** @override */ /** @override */
@ -11,8 +17,20 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
} }
}; };
static #nonCombatTypes = ['environment', 'companion'];
async _prepareContext(options) { async _prepareContext(options) {
const context = await super._prepareContext(options); const context = await super._prepareContext(options);
context.partyOnCanvas =
this.actor.type === 'party' &&
this.actor.system.partyMembers.some(member => member.getActiveTokens().length > 0);
context.icons.toggleParty = 'systems/daggerheart/assets/icons/arrow-dunk.png';
context.actorType = this.actor.type;
context.usesEffects = this.actor.type !== 'party';
context.canToggleCombat = DHTokenHUD.#nonCombatTypes.includes(this.actor.type)
? false
: context.canToggleCombat;
context.systemStatusEffects = Object.keys(context.statusEffects).reduce((acc, key) => { context.systemStatusEffects = Object.keys(context.statusEffects).reduce((acc, key) => {
const effect = context.statusEffects[key]; const effect = context.statusEffects[key];
if (effect.systemEffect) acc[key] = effect; if (effect.systemEffect) acc[key] = effect;
@ -36,6 +54,119 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
return context; return context;
} }
static async #onToggleCombat() {
const tokens = canvas.tokens.controlled
.filter(t => !t.actor || !DHTokenHUD.#nonCombatTypes.includes(t.actor.type))
.map(t => t.document);
if (!this.object.controlled) tokens.push(this.document);
try {
if (this.document.inCombat) await TokenDocument.implementation.deleteCombatants(tokens);
else await TokenDocument.implementation.createCombatants(tokens);
} catch (err) {
ui.notifications.warn(err.message);
}
}
static async #togglePartyTokens(_, button) {
const icon = button.querySelector('img');
icon.classList.toggle('flipped');
button.dataset.tooltip = game.i18n.localize(
icon.classList.contains('flipped')
? 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.retrievePartyTokens'
: 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositPartyTokens'
);
const animationDuration = 500;
const activeTokens = this.actor.system.partyMembers.flatMap(member => member.getActiveTokens());
const { x: actorX, y: actorY } = this.document;
if (activeTokens.length > 0) {
for (let token of activeTokens) {
await token.document.update(
{ x: actorX, y: actorY, alpha: 0 },
{ animation: { duration: animationDuration } }
);
setTimeout(() => token.document.delete(), animationDuration);
}
} else {
const activeScene = game.scenes.find(x => x.active);
const partyTokenData = [];
for (let member of this.actor.system.partyMembers) {
const data = await member.getTokenDocument();
partyTokenData.push(data.toObject());
}
const newTokens = await activeScene.createEmbeddedDocuments(
'Token',
partyTokenData.map(tokenData => ({
...tokenData,
alpha: 0,
x: actorX,
y: actorY
}))
);
const { sizeX, sizeY } = activeScene.grid;
const nrRandomPositions = Math.ceil(newTokens.length / 8) * 8;
/* This is an overcomplicated mess, but I'm stupid */
const positions = shuffleArray(
[...Array(nrRandomPositions).keys()].map((_, index) => {
const nonZeroIndex = index + 1;
const indexFloor = Math.floor(index / 8);
const distanceCoefficient = indexFloor + 1;
const side = 3 + indexFloor * 2;
const sideMiddle = Math.ceil(side / 2);
const inbetween = 1 + indexFloor * 2;
const inbetweenMiddle = Math.ceil(inbetween / 2);
if (index < side) {
const distance =
nonZeroIndex === sideMiddle
? 0
: nonZeroIndex < sideMiddle
? -nonZeroIndex
: nonZeroIndex - sideMiddle;
return { x: actorX - sizeX * distance, y: actorY - sizeY * distanceCoefficient };
} else if (index < side + inbetween) {
const inbetweenIndex = nonZeroIndex - side;
const distance =
inbetweenIndex === inbetweenMiddle
? 0
: inbetweenIndex < inbetweenMiddle
? -inbetweenIndex
: inbetweenIndex - inbetweenMiddle;
return { x: actorX + sizeX * distanceCoefficient, y: actorY + sizeY * distance };
} else if (index < 2 * side + inbetween) {
const sideIndex = nonZeroIndex - side - inbetween;
const distance =
sideIndex === sideMiddle
? 0
: sideIndex < sideMiddle
? sideIndex
: -(sideIndex - sideMiddle);
return { x: actorX + sizeX * distance, y: actorY + sizeY * distanceCoefficient };
} else {
const inbetweenIndex = nonZeroIndex - 2 * side - inbetween;
const distance =
inbetweenIndex === inbetweenMiddle
? 0
: inbetweenIndex < inbetweenMiddle
? inbetweenIndex
: -(inbetweenIndex - inbetweenMiddle);
return { x: actorX - sizeX * distanceCoefficient, y: actorY + sizeY * distance };
}
})
);
for (let token of newTokens) {
const position = positions.pop();
token.update(
{ x: position.x, y: position.y, alpha: 1 },
{ animation: { duration: animationDuration } }
);
}
}
}
_getStatusEffectChoices() { _getStatusEffectChoices() {
// Include all HUD-enabled status effects // Include all HUD-enabled status effects
const choices = {}; const choices = {};

View file

@ -1,3 +1,4 @@
export { default as CharacterLevelup } from './characterLevelup.mjs'; export { default as CharacterLevelup } from './characterLevelup.mjs';
export { default as CompanionLevelup } from './companionLevelup.mjs'; export { default as CompanionLevelup } from './companionLevelup.mjs';
export { default as Levelup } from './levelup.mjs'; export { default as Levelup } from './levelup.mjs';
export { default as LevelupViewMode } from './levelupViewMode.mjs';

View file

@ -1,6 +1,5 @@
import { abilities, subclassFeatureLabels } from '../../config/actorConfig.mjs'; import { abilities, subclassFeatureLabels } from '../../config/actorConfig.mjs';
import { getDeleteKeys, tagifyElement } from '../../helpers/utils.mjs'; import { getDeleteKeys, tagifyElement } from '../../helpers/utils.mjs';
import { ItemBrowser } from '../ui/itemBrowser.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -12,8 +11,6 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
this._dragDrop = this._createDragDropHandlers(); this._dragDrop = this._createDragDropHandlers();
this.tabGroups.primary = 'advancements'; this.tabGroups.primary = 'advancements';
this.itemBrowser = null;
} }
get title() { get title() {
@ -540,7 +537,6 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const type = target.dataset.compendium ?? target.dataset.type; const type = target.dataset.compendium ?? target.dataset.type;
const presets = { const presets = {
compendium: 'daggerheart',
folder: type, folder: type,
render: { render: {
noFolder: true noFolder: true
@ -559,7 +555,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
}; };
} }
return (this.itemBrowser = await new ItemBrowser({ presets }).render({ force: true })); ui.compendiumBrowser.open(presets);
} }
static async selectPreview(_, button) { static async selectPreview(_, button) {
@ -662,7 +658,8 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
}, {}); }, {});
await this.actor.levelUp(levelupData); await this.actor.levelUp(levelupData);
if (this.itemBrowser) this.itemBrowser.close();
if (ui.compendiumBrowser) ui.compendiumBrowser.close();
this.close(); this.close();
} }
} }

View file

@ -0,0 +1,95 @@
import { chunkify } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhlevelUpViewMode extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actor) {
super({});
this.actor = actor;
}
get title() {
return game.i18n.format('DAGGERHEART.APPLICATIONS.Levelup.viewModeTitle', { actor: this.actor.name });
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'dialog', 'dh-style', 'levelup'],
position: { width: 1000, height: 'auto' },
window: {
resizable: true,
icon: 'fa-solid fa-arrow-turn-up'
}
};
static PARTS = {
main: { template: 'systems/daggerheart/templates/levelup/tabs/viewMode.hbs' }
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
const { tiers } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers);
const tierKeys = Object.keys(tiers);
const selections = Object.keys(this.actor.system.levelData.levelups).reduce(
(acc, key) => {
const level = this.actor.system.levelData.levelups[key];
Object.keys(level.selections).forEach(optionKey => {
const choice = level.selections[optionKey];
if (!acc[choice.tier][choice.optionKey]) acc[choice.tier][choice.optionKey] = {};
acc[choice.tier][choice.optionKey][choice.checkboxNr] = choice;
});
return acc;
},
tierKeys.reduce((acc, key) => {
acc[key] = {};
return acc;
}, {})
);
context.tiers = tierKeys.map((tierKey, tierIndex) => {
const tier = tiers[tierKey];
return {
name: tier.name,
active: true,
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, disabled: true };
if (checkboxData) {
checkbox.level = checkboxData.level;
checkbox.selected = true;
}
return checkbox;
});
let label = game.i18n.localize(option.label);
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
}))
};
})
};
})
};
});
return context;
}
}

View file

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

View file

@ -0,0 +1,63 @@
export default class DhSceneConfigSettings extends foundry.applications.sheets.SceneConfig {
// static DEFAULT_OPTIONS = {
// ...super.DEFAULT_OPTIONS,
// form: {
// handler: this.updateData,
// closeOnSubmit: true
// }
// };
static buildParts() {
const { footer, tabs, ...parts } = super.PARTS;
const tmpParts = {
// tabs,
tabs: { template: 'systems/daggerheart/templates/scene/tabs.hbs' },
...parts,
dh: { template: 'systems/daggerheart/templates/scene/dh-config.hbs' },
footer
};
return tmpParts;
}
static PARTS = DhSceneConfigSettings.buildParts();
static buildTabs() {
super.TABS.sheet.tabs.push({ id: 'dh', src: 'systems/daggerheart/assets/logos/FoundryBorneLogoWhite.svg' });
return super.TABS;
}
static TABS = DhSceneConfigSettings.buildTabs();
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
switch (partId) {
case 'dh':
htmlElement.querySelector('#rangeMeasurementSetting')?.addEventListener('change', async event => {
const flagData = foundry.utils.mergeObject(this.document.flags.daggerheart, {
rangeMeasurement: { setting: event.target.value }
});
this.document.flags.daggerheart = flagData;
this.render();
});
break;
}
}
/** @inheritDoc */
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'dh':
context.data = new game.system.api.data.scenes.DHScene(canvas.scene.flags.daggerheart);
context.variantRules = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules);
break;
}
return context;
}
// static async updateData(event, _, formData) {
// const data = foundry.utils.expandObject(formData.object);
// this.close(data);
// }
}

View file

@ -3,43 +3,48 @@ import { getDiceSoNicePreset } from '../../config/generalConfig.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
/**
* @import {ApplicationClickAction} from "@client/applications/_types.mjs"
*/
export default class DHAppearanceSettings extends HandlebarsApplicationMixin(ApplicationV2) { export default class DHAppearanceSettings extends HandlebarsApplicationMixin(ApplicationV2) {
constructor() { /**@inheritdoc */
super({});
this.settings = new DhAppearance(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).toObject()
);
}
get title() {
return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title');
}
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
id: 'daggerheart-appearance-settings', id: 'daggerheart-appearance-settings',
classes: ['daggerheart', 'dialog', 'dh-style', 'setting'], classes: ['daggerheart', 'dialog', 'dh-style', 'setting'],
position: { width: '600', height: 'auto' }, position: { width: '600', height: 'auto' },
window: { window: {
title: 'DAGGERHEART.SETTINGS.Menu.title',
icon: 'fa-solid fa-gears' icon: 'fa-solid fa-gears'
}, },
actions: { actions: {
reset: this.reset, reset: DHAppearanceSettings.#onReset,
save: this.save, preview: DHAppearanceSettings.#onPreview
preview: this.preview
}, },
form: { handler: this.updateData, submitOnChange: true } form: {
closeOnSubmit: true,
handler: DHAppearanceSettings.#onSubmit
}
}; };
static PARTS = { static PARTS = {
main: { header: { template: 'systems/daggerheart/templates/settings/appearance-settings/header.hbs' },
template: 'systems/daggerheart/templates/settings/appearance-settings.hbs' tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
} main: { template: 'systems/daggerheart/templates/settings/appearance-settings/main.hbs' },
diceSoNice: { template: 'systems/daggerheart/templates/settings/appearance-settings/diceSoNice.hbs' },
footer: { template: 'templates/generic/form-footer.hbs' }
}; };
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
general: {
tabs: [
{ id: 'main', label: 'DAGGERHEART.GENERAL.Tabs.general' },
{ id: 'diceSoNice', label: 'DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.title' }
],
initial: 'main'
},
diceSoNice: { diceSoNice: {
tabs: [ tabs: [
{ id: 'hope', label: 'DAGGERHEART.GENERAL.hope' }, { id: 'hope', label: 'DAGGERHEART.GENERAL.hope' },
@ -51,79 +56,149 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
} }
}; };
changeTab(tab, group, options) { /**@type {DhAppearance}*/
super.changeTab(tab, group, options); setting;
this.render(); static #localized = false;
/** @inheritDoc */
async _preFirstRender(_context, _options) {
await super._preFirstRender(_context, _options);
if (!DHAppearanceSettings.#localized) {
foundry.helpers.Localization.localizeDataModel(this.setting.constructor);
DHAppearanceSettings.#localized = true;
}
} }
async _prepareContext(_options) { /** @inheritdoc */
const context = await super._prepareContext(_options); _configureRenderParts(options) {
context.settingFields = this.settings; const parts = super._configureRenderParts(options);
if (!game.modules.get('dice-so-nice')?.active) {
context.showDiceSoNice = game.modules.get('dice-so-nice')?.active; delete parts.diceSoNice;
if (game.dice3d) { delete parts.tabs;
context.diceSoNiceTextures = game.dice3d.exports.TEXTURELIST;
context.diceSoNiceColorsets = game.dice3d.exports.COLORSETS;
context.diceSoNiceMaterials = Object.keys(game.dice3d.DiceFactory.material_options).map(key => ({
key: key,
name: `DICESONICE.Material${key.capitalize()}`
}));
context.diceSoNiceSystems = [];
for (const [key, system] of game.dice3d.DiceFactory.systems.entries()) {
context.diceSoNiceSystems.push({ key, name: system.name });
}
} }
return parts;
}
context.diceTab = { /**@inheritdoc */
key: this.tabGroups.diceSoNice, async _prepareContext(options) {
source: this.settings._source.diceSoNice[this.tabGroups.diceSoNice], const context = await super._prepareContext(options);
fields: this.settings.schema.fields.diceSoNice.fields[this.tabGroups.diceSoNice].fields if (options.isFirstRender)
}; this.setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
context.setting = this.setting;
context.fields = this.setting.schema.fields;
context.tabs = this._prepareTabs('general');
context.dsnTabs = this._prepareTabs('diceSoNice');
return context; return context;
} }
static async updateData(event, element, formData) { /**@inheritdoc */
const updatedSettings = foundry.utils.expandObject(formData.object); async _preparePartContext(partId, context, options) {
const partContext = await super._preparePartContext(partId, context, options);
await this.settings.updateSource(updatedSettings); if (partId in context.tabs) partContext.tab = partContext.tabs[partId];
this.render(); switch (partId) {
case 'diceSoNice':
await this.prepareDiceSoNiceContext(partContext);
break;
case 'footer':
partContext.buttons = [
{ type: 'button', action: 'reset', icon: 'fa-solid fa-arrow-rotate-left', label: 'Reset' },
{ type: 'submit', icon: 'fa-solid fa-floppy-disk', label: 'Save Changes' }
];
break;
}
return partContext;
} }
static async preview() { /**
const source = this.settings._source.diceSoNice[this.tabGroups.diceSoNice]; * Prepare render context for the DSN part.
let faces = 'd12'; * @param {ApplicationRenderContext} context
switch (this.tabGroups.diceSoNice) { * @returns {Promise<void>}
case 'advantage': * @protected
case 'disadvantage': */
faces = 'd6'; async prepareDiceSoNiceContext(context) {
} context.diceSoNiceTextures = Object.entries(game.dice3d.exports.TEXTURELIST).reduce(
const preset = await getDiceSoNicePreset(source, faces); (acc, [k, v]) => ({
const diceSoNiceRoll = await new Roll(`1${faces}`).evaluate(); ...acc,
[k]: v.name
}),
{}
);
context.diceSoNiceColorsets = Object.values(game.dice3d.exports.COLORSETS).reduce(
(acc, v) => ({
...acc,
[v.id]: v.description
}),
{}
);
context.diceSoNiceMaterials = Object.keys(game.dice3d.DiceFactory.material_options).reduce(
(acc, key) => ({
...acc,
[key]: `DICESONICE.Material${key.capitalize()}`
}),
{}
);
context.diceSoNiceSystems = Object.fromEntries(
[...game.dice3d.DiceFactory.systems].map(([k, v]) => [k, v.name])
);
foundry.utils.mergeObject(
context.dsnTabs,
['hope', 'fear', 'advantage', 'disadvantage'].reduce(
(acc, key) => ({
...acc,
[key]: {
values: this.setting.diceSoNice[key],
fields: this.setting.schema.getField(`diceSoNice.${key}`).fields
}
}),
{}
)
);
}
/**
* Submit the configuration form.
* @this {DHAppearanceSettings}
* @param {SubmitEvent} event
* @param {HTMLFormElement} form
* @param {foundry.applications.ux.FormDataExtended} formData
* @returns {Promise<void>}
*/
static async #onSubmit(event, form, formData) {
const data = this.setting.schema.clean(foundry.utils.expandObject(formData.object));
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, data);
}
/* -------------------------------------------- */
/**
* Submit the configuration form.
* @this {DHAppearanceSettings}
* @type {ApplicationClickAction}
*/
static async #onPreview(_, target) {
const formData = new foundry.applications.ux.FormDataExtended(target.closest('form'));
const { diceSoNice } = foundry.utils.expandObject(formData.object);
const { key } = target.dataset;
const faces = ['advantage', 'disadvantage'].includes(key) ? 'd6' : 'd12';
const preset = await getDiceSoNicePreset(diceSoNice[key], faces);
const diceSoNiceRoll = await new foundry.dice.Roll(`1${faces}`).evaluate();
diceSoNiceRoll.dice[0].options.appearance = preset.appearance; diceSoNiceRoll.dice[0].options.appearance = preset.appearance;
diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile; diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile;
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, false); await game.dice3d.showForRoll(diceSoNiceRoll, game.user, false);
} }
static async reset() { /**
this.settings = new DhAppearance(); * Reset the form back to default values.
this.render(); * @this {DHAppearanceSettings}
} * @type {ApplicationClickAction}
*/
static async save() { static async #onReset() {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, this.settings.toObject()); this.setting = new this.setting.constructor();
this.render({ force: false });
this.close();
}
_getTabs(tabs) {
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active;
v.cssClass = v.active ? 'active' : '';
}
return tabs;
} }
} }

View file

@ -35,13 +35,14 @@ export default class DhAutomationSettings extends HandlebarsApplicationMixin(App
header: { template: 'systems/daggerheart/templates/settings/automation-settings/header.hbs' }, header: { template: 'systems/daggerheart/templates/settings/automation-settings/header.hbs' },
general: { template: 'systems/daggerheart/templates/settings/automation-settings/general.hbs' }, general: { template: 'systems/daggerheart/templates/settings/automation-settings/general.hbs' },
rules: { template: 'systems/daggerheart/templates/settings/automation-settings/rules.hbs' }, rules: { template: 'systems/daggerheart/templates/settings/automation-settings/rules.hbs' },
roll: { template: 'systems/daggerheart/templates/settings/automation-settings/roll.hbs' },
footer: { template: 'systems/daggerheart/templates/settings/automation-settings/footer.hbs' } footer: { template: 'systems/daggerheart/templates/settings/automation-settings/footer.hbs' }
}; };
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
main: { main: {
tabs: [{ id: 'general' }, { id: 'rules' }], tabs: [{ id: 'general' }, { id: 'rules' }, { id: 'roll' }],
initial: 'general', initial: 'general',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }

View file

@ -1,5 +1,6 @@
import { DhHomebrew } from '../../data/settings/_module.mjs'; import { DhHomebrew } from '../../data/settings/_module.mjs';
import { slugify } from '../../helpers/utils.mjs'; import { slugify } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhHomebrewSettings extends HandlebarsApplicationMixin(ApplicationV2) { export default class DhHomebrewSettings extends HandlebarsApplicationMixin(ApplicationV2) {
@ -10,11 +11,14 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).toObject() game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).toObject()
); );
this.selected = { this.selected = this.#getDefaultAdversaryType();
domain: null
};
} }
#getDefaultAdversaryType = () => ({
domain: null,
adversaryType: null
});
get title() { get title() {
return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title'); return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title');
} }
@ -35,6 +39,9 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
addDomain: this.addDomain, addDomain: this.addDomain,
toggleSelectedDomain: this.toggleSelectedDomain, toggleSelectedDomain: this.toggleSelectedDomain,
deleteDomain: this.deleteDomain, deleteDomain: this.deleteDomain,
addAdversaryType: this.addAdversaryType,
deleteAdversaryType: this.deleteAdversaryType,
selectAdversaryType: this.selectAdversaryType,
save: this.save, save: this.save,
reset: this.reset reset: this.reset
}, },
@ -45,6 +52,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
settings: { template: 'systems/daggerheart/templates/settings/homebrew-settings/settings.hbs' }, settings: { template: 'systems/daggerheart/templates/settings/homebrew-settings/settings.hbs' },
domains: { template: 'systems/daggerheart/templates/settings/homebrew-settings/domains.hbs' }, domains: { template: 'systems/daggerheart/templates/settings/homebrew-settings/domains.hbs' },
types: { template: 'systems/daggerheart/templates/settings/homebrew-settings/types.hbs' },
itemTypes: { template: 'systems/daggerheart/templates/settings/homebrew-settings/itemFeatures.hbs' },
downtime: { template: 'systems/daggerheart/templates/settings/homebrew-settings/downtime.hbs' }, downtime: { template: 'systems/daggerheart/templates/settings/homebrew-settings/downtime.hbs' },
footer: { template: 'systems/daggerheart/templates/settings/homebrew-settings/footer.hbs' } footer: { template: 'systems/daggerheart/templates/settings/homebrew-settings/footer.hbs' }
}; };
@ -52,12 +61,19 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
main: { main: {
tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'downtime' }], tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'types' }, { id: 'itemFeatures' }, { id: 'downtime' }],
initial: 'settings', initial: 'settings',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }
}; };
changeTab(tab, group, options) {
super.changeTab(tab, group, options);
this.selected = this.#getDefaultAdversaryType();
this.render();
}
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.settingFields = this.settings; context.settingFields = this.settings;
@ -79,6 +95,11 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
context.configDomains = CONFIG.DH.DOMAIN.domains; context.configDomains = CONFIG.DH.DOMAIN.domains;
context.homebrewDomains = this.settings.domains; context.homebrewDomains = this.settings.domains;
break; break;
case 'types':
context.selectedAdversaryType = this.selected.adversaryType
? { id: this.selected.adversaryType, ...this.settings.adversaryTypes[this.selected.adversaryType] }
: null;
break;
} }
return context; return context;
@ -95,33 +116,53 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
} }
static async addItem(_, target) { static async addItem(_, target) {
await this.settings.updateSource({ const { type } = target.dataset;
[`restMoves.${target.dataset.type}.moves.${foundry.utils.randomID()}`]: { if (['shortRest', 'longRest'].includes(type)) {
name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'), await this.settings.updateSource({
img: 'icons/magic/life/cross-worn-green.webp', [`restMoves.${type}.moves.${foundry.utils.randomID()}`]: {
description: '', name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'),
actions: [] img: 'icons/magic/life/cross-worn-green.webp',
} description: '',
}); actions: []
}
});
} else if (['armorFeatures', 'weaponFeatures'].includes(type)) {
await this.settings.updateSource({
[`itemFeatures.${type}.${foundry.utils.randomID()}`]: {
name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newFeature'),
img: 'icons/magic/life/cross-worn-green.webp',
description: '',
actions: [],
effects: []
}
});
}
this.render(); this.render();
} }
static async editItem(_, target) { static async editItem(_, target) {
const move = this.settings.restMoves[target.dataset.type].moves[target.dataset.id]; const { type, id } = target.dataset;
const path = `restMoves.${target.dataset.type}.moves.${target.dataset.id}`; const isDowntime = ['shortRest', 'longRest'].includes(type);
const editedMove = await game.system.api.applications.sheetConfigs.DowntimeConfig.configure( const path = isDowntime ? `restMoves.${type}.moves.${id}` : `itemFeatures.${type}.${id}`;
move, const featureBase = isDowntime ? this.settings.restMoves[type].moves[id] : this.settings.itemFeatures[type][id];
path,
this.settings
);
if (!editedMove) return;
await this.updateAction.bind(this)(editedMove, target.dataset.type, target.dataset.id); const editedBase = await game.system.api.applications.sheetConfigs.SettingFeatureConfig.configure(
featureBase,
path,
this.settings,
{ hasIcon: isDowntime, hasEffects: !isDowntime }
);
if (!editedBase) return;
await this.updateAction.bind(this)(editedBase, target.dataset.type, target.dataset.id);
} }
async updateAction(data, type, id) { async updateAction(data, type, id) {
const isDowntime = ['shortRest', 'longRest'].includes(type);
const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`;
await this.settings.updateSource({ await this.settings.updateSource({
[`restMoves.${type}.moves.${id}`]: { [`${path}.${id}`]: {
actions: data.actions, actions: data.actions,
name: data.name, name: data.name,
icon: data.icon, icon: data.icon,
@ -129,12 +170,16 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
description: data.description description: data.description
} }
}); });
this.render(); this.render();
} }
static async removeItem(_, target) { static async removeItem(_, target) {
const { type, id } = target.dataset;
const isDowntime = ['shortRest', 'longRest'].includes(type);
const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`;
await this.settings.updateSource({ await this.settings.updateSource({
[`restMoves.${target.dataset.type}.moves.-=${target.dataset.id}`]: null [`${path}.-=${id}`]: null
}); });
this.render(); this.render();
} }
@ -301,6 +346,32 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render(); this.render();
} }
static async addAdversaryType(_, target) {
const newId = foundry.utils.randomID();
await this.settings.updateSource({
[`adversaryTypes.${newId}`]: {
id: newId,
label: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.adversaryType.newType')
}
});
this.selected.adversaryType = newId;
this.render();
}
static async deleteAdversaryType(_, target) {
const { key } = target.dataset;
await this.settings.updateSource({ [`adversaryTypes.-=${key}`]: null });
this.selected.adversaryType = this.selected.adversaryType === key ? null : this.selected.adversaryType;
this.render();
}
static async selectAdversaryType(_, target) {
this.selected.adversaryType = this.selected.adversaryType === target.dataset.type ? null : target.dataset.type;
this.render();
}
static async save() { static async save() {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.close(); this.close();

View file

@ -2,7 +2,8 @@ export { default as ActionConfig } from './action-config.mjs';
export { default as CharacterSettings } from './character-settings.mjs'; export { default as CharacterSettings } from './character-settings.mjs';
export { default as AdversarySettings } from './adversary-settings.mjs'; export { default as AdversarySettings } from './adversary-settings.mjs';
export { default as CompanionSettings } from './companion-settings.mjs'; export { default as CompanionSettings } from './companion-settings.mjs';
export { default as DowntimeConfig } from './downtimeConfig.mjs'; export { default as SettingActiveEffectConfig } from './setting-active-effect-config.mjs';
export { default as SettingFeatureConfig } from './setting-feature-config.mjs';
export { default as EnvironmentSettings } from './environment-settings.mjs'; export { default as EnvironmentSettings } from './environment-settings.mjs';
export { default as ActiveEffectConfig } from './activeEffectConfig.mjs'; export { default as ActiveEffectConfig } from './activeEffectConfig.mjs';
export { default as DhTokenConfig } from './token-config.mjs'; export { default as DhTokenConfig } from './token-config.mjs';

View file

@ -66,7 +66,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
group: 'primary', group: 'primary',
id: 'base', id: 'base',
icon: null, icon: null,
label: 'Base' label: 'DAGGERHEART.GENERAL.Tabs.base'
}, },
config: { config: {
active: false, active: false,
@ -74,7 +74,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
group: 'primary', group: 'primary',
id: 'config', id: 'config',
icon: null, icon: null,
label: 'Configuration' label: 'DAGGERHEART.GENERAL.Tabs.configuration'
}, },
effect: { effect: {
active: false, active: false,
@ -82,7 +82,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
group: 'primary', group: 'primary',
id: 'effect', id: 'effect',
icon: null, icon: null,
label: 'Effect' label: 'DAGGERHEART.GENERAL.Tabs.effects'
} }
}; };
@ -132,12 +132,19 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
const options = foundry.utils.deepClone(CONFIG.DH.GENERAL.abilityCosts); const options = foundry.utils.deepClone(CONFIG.DH.GENERAL.abilityCosts);
const resource = this.action.parent.resource; const resource = this.action.parent.resource;
if (resource) { if (resource) {
options[this.action.parent.parent.id] = { options.resource = {
label: 'DAGGERHEART.GENERAL.itemResource', label: 'DAGGERHEART.GENERAL.itemResource',
group: 'Global' group: 'Global'
}; };
} }
if (this.action.parent.metadata?.isQuantifiable) {
options.quantity = {
label: 'DAGGERHEART.GENERAL.itemQuantity',
group: 'Global'
};
}
return options; return options;
} }
@ -164,13 +171,14 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
_prepareSubmitData(_event, formData) { _prepareSubmitData(_event, formData) {
const submitData = foundry.utils.expandObject(formData.object); const submitData = foundry.utils.expandObject(formData.object);
const itemAbilityCostKeys = Object.keys(CONFIG.DH.GENERAL.itemAbilityCosts);
for (const keyPath of this.constructor.CLEAN_ARRAYS) { for (const keyPath of this.constructor.CLEAN_ARRAYS) {
const data = foundry.utils.getProperty(submitData, keyPath); const data = foundry.utils.getProperty(submitData, keyPath);
const dataValues = data ? Object.values(data) : []; const dataValues = data ? Object.values(data) : [];
if (keyPath === 'cost') { if (keyPath === 'cost') {
for (var value of dataValues) { for (var value of dataValues) {
const item = this.action.parent.parent.id === value.key; value.itemId = itemAbilityCostKeys.includes(value.key) ? this.action.parent.parent.id : null;
value.keyIsID = Boolean(item);
} }
} }

View file

@ -96,6 +96,13 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
}); });
} }
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.systemFields = context.document.system.schema.fields;
return context;
}
async _preparePartContext(partId, context) { async _preparePartContext(partId, context) {
const partContext = await super._preparePartContext(partId, context); const partContext = await super._preparePartContext(partId, context);
switch (partId) { switch (partId) {

View file

@ -0,0 +1,227 @@
import autocomplete from 'autocompleter';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class SettingActiveEffectConfig extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(effect) {
super({});
this.effect = foundry.utils.deepClone(effect);
const ignoredActorKeys = ['config', 'DhEnvironment'];
this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => {
if (!ignoredActorKeys.includes(key)) {
const model = game.system.api.models.actors[key];
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model);
const group = game.i18n.localize(model.metadata.label);
const choices = CONFIG.Token.documentClass
.getTrackedAttributeChoices(attributes, model)
.map(x => ({ ...x, group: group }));
acc.push(...choices);
}
return acc;
}, []);
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'sheet', 'dh-style', 'active-effect-config'],
tag: 'form',
position: {
width: 560
},
form: {
submitOnChange: false,
closeOnSubmit: false,
handler: SettingActiveEffectConfig.#onSubmit
},
actions: {
editImage: SettingActiveEffectConfig.#editImage,
addChange: SettingActiveEffectConfig.#addChange,
deleteChange: SettingActiveEffectConfig.#deleteChange
}
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/activeEffect/header.hbs' },
tabs: { template: 'templates/generic/tab-navigation.hbs' },
details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] },
settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' },
changes: {
template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs',
scrollable: ['ol[data-changes]']
},
footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' }
};
static TABS = {
sheet: {
tabs: [
{ id: 'details', icon: 'fa-solid fa-book' },
{ id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' },
{ id: 'changes', icon: 'fa-solid fa-gears' }
],
initial: 'details',
labelPrefix: 'EFFECT.TABS'
}
};
/**@inheritdoc */
async _onFirstRender(context, options) {
await super._onFirstRender(context, options);
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.source = this.effect;
context.fields = game.system.api.documents.DhActiveEffect.schema.fields;
context.systemFields = game.system.api.data.activeEffects.BaseEffect._schema.fields;
return context;
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
const changeChoices = this.changeChoices;
htmlElement.querySelectorAll('.effect-change-input').forEach(element => {
autocomplete({
input: element,
fetch: function (text, update) {
if (!text) {
update(changeChoices);
} else {
text = text.toLowerCase();
var suggestions = changeChoices.filter(n => n.label.toLowerCase().includes(text));
update(suggestions);
}
},
render: function (item, search) {
const label = game.i18n.localize(item.label);
const matchIndex = label.toLowerCase().indexOf(search);
const beforeText = label.slice(0, matchIndex);
const matchText = label.slice(matchIndex, matchIndex + search.length);
const after = label.slice(matchIndex + search.length, label.length);
const element = document.createElement('li');
element.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
if (item.hint) {
element.dataset.tooltip = game.i18n.localize(item.hint);
}
return element;
},
renderGroup: function (label) {
const itemElement = document.createElement('div');
itemElement.textContent = game.i18n.localize(label);
return itemElement;
},
onSelect: function (item) {
element.value = `system.${item.value}`;
},
click: e => e.fetch(),
customize: function (_input, _inputRect, container) {
container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ;
},
minLength: 0
});
});
}
async _preparePartContext(partId, context) {
if (partId in context.tabs) context.tab = context.tabs[partId];
switch (partId) {
case 'details':
context.isActorEffect = false;
context.isItemEffect = true;
const useGeneric = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance
).showGenericStatusEffects;
if (!useGeneric) {
context.statuses = Object.values(CONFIG.DH.GENERAL.conditions).map(status => ({
value: status.id,
label: game.i18n.localize(status.name)
}));
}
break;
case 'changes':
context.modes = Object.entries(CONST.ACTIVE_EFFECT_MODES).reduce((modes, [key, value]) => {
modes[value] = game.i18n.localize(`EFFECT.MODE_${key}`);
return modes;
}, {});
context.priorities = ActiveEffectConfig.DEFAULT_PRIORITIES;
break;
}
return context;
}
static async #onSubmit(event, form, formData) {
this.data = foundry.utils.expandObject(formData.object);
this.close();
}
/**
* Edit a Document image.
* @this {DocumentSheetV2}
* @type {ApplicationClickAction}
*/
static async #editImage(_event, target) {
if (target.nodeName !== 'IMG') {
throw new Error('The editImage action is available only for IMG elements.');
}
const attr = target.dataset.edit;
const current = foundry.utils.getProperty(this.effect, attr);
const fp = new FilePicker.implementation({
current,
type: 'image',
callback: path => (target.src = path),
position: {
top: this.position.top + 40,
left: this.position.left + 10
}
});
await fp.browse();
}
/**
* Add a new change to the effect's changes array.
* @this {ActiveEffectConfig}
* @type {ApplicationClickAction}
*/
static async #addChange() {
const submitData = foundry.utils.expandObject(new FormDataExtended(this.form).object);
const changes = Object.values(submitData.changes ?? {});
changes.push({});
this.effect.changes = changes;
this.render();
}
/**
* Delete a change from the effect's changes array.
* @this {ActiveEffectConfig}
* @type {ApplicationClickAction}
*/
static async #deleteChange(event) {
const submitData = foundry.utils.expandObject(new FormDataExtended(this.form).object);
const changes = Object.values(submitData.changes);
const row = event.target.closest('li');
const index = Number(row.dataset.index) || 0;
changes.splice(index, 1);
this.effect.changes = changes;
this.render();
}
static async configure(effect, options = {}) {
return new Promise(resolve => {
const app = new this(effect, options);
app.addEventListener('close', () => resolve(app.data), { once: true });
app.render({ force: true });
});
}
}

View file

@ -3,8 +3,8 @@ import DHActionConfig from './action-config.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DowntimeConfig extends HandlebarsApplicationMixin(ApplicationV2) { export default class SettingFeatureConfig extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(move, movePath, settings, options) { constructor(move, movePath, settings, optionalParts, options) {
super(options); super(options);
this.move = move; this.move = move;
@ -12,6 +12,10 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
this.movePath = movePath; this.movePath = movePath;
this.actionsPath = `${movePath}.actions`; this.actionsPath = `${movePath}.actions`;
this.settings = settings; this.settings = settings;
const { hasIcon, hasEffects } = optionalParts;
this.hasIcon = hasIcon;
this.hasEffects = hasEffects;
} }
get title() { get title() {
@ -30,6 +34,7 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
addItem: this.addItem, addItem: this.addItem,
editItem: this.editItem, editItem: this.editItem,
removeItem: this.removeItem, removeItem: this.removeItem,
addEffect: this.addEffect,
resetMoves: this.resetMoves, resetMoves: this.resetMoves,
saveForm: this.saveForm saveForm: this.saveForm
}, },
@ -41,13 +46,14 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
main: { template: 'systems/daggerheart/templates/settings/downtime-config/main.hbs' }, main: { template: 'systems/daggerheart/templates/settings/downtime-config/main.hbs' },
actions: { template: 'systems/daggerheart/templates/settings/downtime-config/actions.hbs' }, actions: { template: 'systems/daggerheart/templates/settings/downtime-config/actions.hbs' },
effects: { template: 'systems/daggerheart/templates/settings/downtime-config/effects.hbs' },
footer: { template: 'systems/daggerheart/templates/settings/downtime-config/footer.hbs' } footer: { template: 'systems/daggerheart/templates/settings/downtime-config/footer.hbs' }
}; };
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
primary: { primary: {
tabs: [{ id: 'main' }, { id: 'actions' }], tabs: [{ id: 'main' }, { id: 'actions' }, { id: 'effects' }],
initial: 'main', initial: 'main',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }
@ -55,6 +61,9 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.tabs = this._filterTabs(context.tabs);
context.hasIcon = this.hasIcon;
context.hasEffects = this.hasEffects;
context.move = this.move; context.move = this.move;
context.move.enrichedDescription = await foundry.applications.ux.TextEditor.enrichHTML( context.move.enrichedDescription = await foundry.applications.ux.TextEditor.enrichHTML(
context.move.description context.move.description
@ -130,13 +139,30 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
} }
static async editItem(_, target) { static async editItem(_, target) {
const actionId = target.dataset.id; const { type, id } = target.dataset;
const action = this.move.actions.get(actionId); if (type === 'effect') {
await new DHActionConfig(action, async updatedMove => { const effectIndex = this.move.effects.findIndex(x => x.id === id);
await this.settings.updateSource({ [`${this.actionsPath}.${actionId}`]: updatedMove }); const effect = this.move.effects[effectIndex];
const updatedEffect =
await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect);
if (!updatedEffect) return;
await this.settings.updateSource({
[`${this.movePath}.effects`]: this.move.effects.reduce((acc, effect, index) => {
acc.push(index === effectIndex ? { ...updatedEffect, id: effect.id } : effect);
return acc;
}, [])
});
this.move = foundry.utils.getProperty(this.settings, this.movePath); this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render(); this.render();
}).render(true); } else {
const action = this.move.actions.get(id);
await new DHActionConfig(action, async updatedMove => {
await this.settings.updateSource({ [`${this.actionsPath}.${id}`]: updatedMove });
this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render();
}).render(true);
}
} }
static async removeItem(_, target) { static async removeItem(_, target) {
@ -145,16 +171,38 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
this.render(); this.render();
} }
static async addEffect(_, target) {
const currentEffects = foundry.utils.getProperty(this.settings, `${this.movePath}.effects`);
await this.settings.updateSource({
[`${this.movePath}.effects`]: [
...currentEffects,
game.system.api.data.activeEffects.BaseEffect.getDefaultObject()
]
});
this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render();
}
static resetMoves() {} static resetMoves() {}
_filterTabs(tabs) {
return this.hasEffects
? tabs
: Object.keys(tabs).reduce((acc, key) => {
if (key !== 'effects') acc[key] = tabs[key];
return acc;
}, {});
}
/** @override */ /** @override */
_onClose(options = {}) { _onClose(options = {}) {
if (!options.submitted) this.move = null; if (!options.submitted) this.move = null;
} }
static async configure(move, movePath, settings, options = {}) { static async configure(move, movePath, settings, optionalParts, options = {}) {
return new Promise(resolve => { return new Promise(resolve => {
const app = new this(move, movePath, settings, options); const app = new this(move, movePath, settings, optionalParts, options);
app.addEventListener('close', () => resolve(app.move), { once: true }); app.addEventListener('close', () => resolve(app.move), { once: true });
app.render({ force: true }); app.render({ force: true });
}); });

View file

@ -2,3 +2,4 @@ export { default as Adversary } from './adversary.mjs';
export { default as Character } from './character.mjs'; export { default as Character } from './character.mjs';
export { default as Companion } from './companion.mjs'; export { default as Companion } from './companion.mjs';
export { default as Environment } from './environment.mjs'; export { default as Environment } from './environment.mjs';
export { default as Party } from './party.mjs';

View file

@ -4,24 +4,51 @@ import DHBaseActorSheet from '../api/base-actor.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
export default class AdversarySheet extends DHBaseActorSheet { export default class AdversarySheet extends DHBaseActorSheet {
/** @inheritDoc */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['adversary'], classes: ['adversary'],
position: { width: 660, height: 766 }, position: { width: 660, height: 766 },
window: { resizable: true }, window: { resizable: true },
actions: { actions: {
reactionRoll: AdversarySheet.#reactionRoll toggleHitPoints: AdversarySheet.#toggleHitPoints,
toggleStress: AdversarySheet.#toggleStress,
reactionRoll: AdversarySheet.#reactionRoll,
toggleResourceDice: AdversarySheet.#toggleResourceDice,
handleResourceDice: AdversarySheet.#handleResourceDice
}, },
window: { window: {
resizable: true resizable: true,
controls: [
{
icon: 'fa-solid fa-signature',
label: 'DAGGERHEART.UI.Tooltip.configureAttribution',
action: 'editAttribution'
}
]
} }
}; };
static PARTS = { static PARTS = {
sidebar: { template: 'systems/daggerheart/templates/sheets/actors/adversary/sidebar.hbs' }, limited: {
template: 'systems/daggerheart/templates/sheets/actors/adversary/limited.hbs',
scrollable: ['.limited-container']
},
sidebar: {
template: 'systems/daggerheart/templates/sheets/actors/adversary/sidebar.hbs',
scrollable: ['.shortcut-items-section']
},
header: { template: 'systems/daggerheart/templates/sheets/actors/adversary/header.hbs' }, header: { template: 'systems/daggerheart/templates/sheets/actors/adversary/header.hbs' },
features: { template: 'systems/daggerheart/templates/sheets/actors/adversary/features.hbs' }, features: {
notes: { template: 'systems/daggerheart/templates/sheets/actors/adversary/notes.hbs' }, template: 'systems/daggerheart/templates/sheets/actors/adversary/features.hbs',
effects: { template: 'systems/daggerheart/templates/sheets/actors/adversary/effects.hbs' } scrollable: ['.feature-section']
},
notes: {
template: 'systems/daggerheart/templates/sheets/actors/adversary/notes.hbs'
},
effects: {
template: 'systems/daggerheart/templates/sheets/actors/adversary/effects.hbs',
scrollable: ['.effects-sections']
}
}; };
/** @inheritdoc */ /** @inheritdoc */
@ -33,10 +60,33 @@ export default class AdversarySheet extends DHBaseActorSheet {
} }
}; };
/** @inheritdoc */
_initializeApplicationOptions(options) {
const applicationOptions = super._initializeApplicationOptions(options);
if (applicationOptions.document.testUserPermission(game.user, 'LIMITED', { exact: true })) {
applicationOptions.position.width = 360;
applicationOptions.position.height = 'auto';
}
return applicationOptions;
}
/**@inheritdoc */ /**@inheritdoc */
async _prepareContext(options) { async _prepareContext(options) {
const context = await super._prepareContext(options); const context = await super._prepareContext(options);
context.systemFields.attack.fields = this.document.system.attack.schema.fields; context.systemFields.attack.fields = this.document.system.attack.schema.fields;
context.resources = Object.keys(this.document.system.resources).reduce((acc, key) => {
acc[key] = this.document.system.resources[key];
return acc;
}, {});
const maxResource = Math.max(context.resources.hitPoints.max, context.resources.stress.max);
context.resources.hitPoints.emptyPips =
context.resources.hitPoints.max < maxResource ? maxResource - context.resources.hitPoints.max : 0;
context.resources.stress.emptyPips =
context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0;
return context; return context;
} }
@ -45,7 +95,11 @@ export default class AdversarySheet extends DHBaseActorSheet {
context = await super._preparePartContext(partId, context, options); context = await super._preparePartContext(partId, context, options);
switch (partId) { switch (partId) {
case 'header': case 'header':
case 'limited':
await this._prepareHeaderContext(context, options); await this._prepareHeaderContext(context, options);
const adversaryTypes = CONFIG.DH.ACTOR.allAdversaryTypes();
context.adversaryType = game.i18n.localize(adversaryTypes[this.document.system.type].label);
break; break;
case 'notes': case 'notes':
await this._prepareNotesContext(context, options); await this._prepareNotesContext(context, options);
@ -113,6 +167,27 @@ export default class AdversarySheet extends DHBaseActorSheet {
/* Application Clicks Actions */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
/**
* Toggles hitpoint resource value.
* @type {ApplicationClickAction}
*/
static async #toggleHitPoints(_, button) {
const hitPointsValue = Number.parseInt(button.dataset.value);
const newValue =
this.document.system.resources.hitPoints.value >= hitPointsValue ? hitPointsValue - 1 : hitPointsValue;
await this.document.update({ 'system.resources.hitPoints.value': newValue });
}
/**
* Toggles stress resource value.
* @type {ApplicationClickAction}
*/
static async #toggleStress(_, button) {
const StressValue = Number.parseInt(button.dataset.value);
const newValue = this.document.system.resources.stress.value >= StressValue ? StressValue - 1 : StressValue;
await this.document.update({ 'system.resources.stress.value': newValue });
}
/** /**
* Performs a reaction roll for an Adversary. * Performs a reaction roll for an Adversary.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
@ -123,9 +198,9 @@ export default class AdversarySheet extends DHBaseActorSheet {
title: `Reaction Roll: ${this.actor.name}`, title: `Reaction Roll: ${this.actor.name}`,
headerTitle: 'Adversary Reaction Roll', headerTitle: 'Adversary Reaction Roll',
roll: { roll: {
type: 'reaction' type: 'trait'
}, },
type: 'trait', actionType: 'reaction',
hasRoll: true, hasRoll: true,
data: this.actor.getRollData() data: this.actor.getRollData()
}; };
@ -133,6 +208,40 @@ export default class AdversarySheet extends DHBaseActorSheet {
this.actor.diceRoll(config); this.actor.diceRoll(config);
} }
/**
* Toggle the used state of a resource dice.
* @type {ApplicationClickAction}
*/
static async #toggleResourceDice(event, target) {
const item = await getDocFromElement(target);
const { dice } = event.target.closest('.item-resource').dataset;
const diceState = item.system.resource.diceStates[dice];
await item.update({
[`system.resource.diceStates.${dice}.used`]: diceState ? !diceState.used : true
});
}
/**
* Handle the roll values of resource dice.
* @type {ApplicationClickAction}
*/
static async #handleResourceDice(_, target) {
const item = await getDocFromElement(target);
if (!item) return;
const rollValues = await game.system.api.applications.dialogs.ResourceDiceDialog.create(item, this.document);
if (!rollValues) return;
await item.update({
'system.resource.diceStates': rollValues.reduce((acc, state, index) => {
acc[index] = { value: state.value, used: state.used };
return acc;
}, {})
});
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Application Listener Actions */ /* Application Listener Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */

View file

@ -1,11 +1,10 @@
import DHBaseActorSheet from '../api/base-actor.mjs'; import DHBaseActorSheet from '../api/base-actor.mjs';
import DhpDeathMove from '../../dialogs/deathMove.mjs'; import DhpDeathMove from '../../dialogs/deathMove.mjs';
import { abilities } from '../../../config/actorConfig.mjs'; import { abilities } from '../../../config/actorConfig.mjs';
import DhCharacterlevelUp from '../../levelup/characterLevelup.mjs'; import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
import FilterMenu from '../../ux/filter-menu.mjs'; import FilterMenu from '../../ux/filter-menu.mjs';
import { getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs'; import { getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs';
import { ItemBrowser } from '../../ui/itemBrowser.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
@ -15,22 +14,34 @@ export default class CharacterSheet extends DHBaseActorSheet {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['character'], classes: ['character'],
position: { width: 850, height: 800 }, position: { width: 850, height: 800 },
/* Foundry adds disabled to all buttons and inputs if editPermission is missing. This is not desired. */
editPermission: CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER,
actions: { actions: {
toggleVault: CharacterSheet.#toggleVault, toggleVault: CharacterSheet.#toggleVault,
rollAttribute: CharacterSheet.#rollAttribute, rollAttribute: CharacterSheet.#rollAttribute,
toggleHitPoints: CharacterSheet.#toggleHitPoints,
toggleStress: CharacterSheet.#toggleStress,
toggleArmor: CharacterSheet.#toggleArmor,
toggleHope: CharacterSheet.#toggleHope, toggleHope: CharacterSheet.#toggleHope,
toggleLoadoutView: CharacterSheet.#toggleLoadoutView, toggleLoadoutView: CharacterSheet.#toggleLoadoutView,
openPack: CharacterSheet.#openPack, openPack: CharacterSheet.#openPack,
makeDeathMove: CharacterSheet.#makeDeathMove, makeDeathMove: CharacterSheet.#makeDeathMove,
levelManagement: CharacterSheet.#levelManagement, levelManagement: CharacterSheet.#levelManagement,
viewLevelups: CharacterSheet.#viewLevelups,
toggleEquipItem: CharacterSheet.#toggleEquipItem, toggleEquipItem: CharacterSheet.#toggleEquipItem,
toggleResourceDice: CharacterSheet.#toggleResourceDice, toggleResourceDice: CharacterSheet.#toggleResourceDice,
handleResourceDice: CharacterSheet.#handleResourceDice, handleResourceDice: CharacterSheet.#handleResourceDice,
useDowntime: this.useDowntime, useDowntime: this.useDowntime
tempBrowser: CharacterSheet.#tempBrowser
}, },
window: { window: {
resizable: true resizable: true,
controls: [
{
icon: 'fa-solid fa-angles-up',
label: 'DAGGERHEART.ACTORS.Character.viewLevelups',
action: 'viewLevelups'
}
]
}, },
dragDrop: [ dragDrop: [
{ {
@ -68,8 +79,14 @@ export default class CharacterSheet extends DHBaseActorSheet {
/**@override */ /**@override */
static PARTS = { static PARTS = {
limited: {
id: 'limited',
scrollable: ['.limited-container'],
template: 'systems/daggerheart/templates/sheets/actors/character/limited.hbs'
},
sidebar: { sidebar: {
id: 'sidebar', id: 'sidebar',
scrollable: ['.shortcut-items-section'],
template: 'systems/daggerheart/templates/sheets/actors/character/sidebar.hbs' template: 'systems/daggerheart/templates/sheets/actors/character/sidebar.hbs'
}, },
header: { header: {
@ -78,22 +95,27 @@ export default class CharacterSheet extends DHBaseActorSheet {
}, },
features: { features: {
id: 'features', id: 'features',
scrollable: ['.features-sections'],
template: 'systems/daggerheart/templates/sheets/actors/character/features.hbs' template: 'systems/daggerheart/templates/sheets/actors/character/features.hbs'
}, },
loadout: { loadout: {
id: 'loadout', id: 'loadout',
scrollable: ['.items-section'],
template: 'systems/daggerheart/templates/sheets/actors/character/loadout.hbs' template: 'systems/daggerheart/templates/sheets/actors/character/loadout.hbs'
}, },
inventory: { inventory: {
id: 'inventory', id: 'inventory',
scrollable: ['.items-section'],
template: 'systems/daggerheart/templates/sheets/actors/character/inventory.hbs' template: 'systems/daggerheart/templates/sheets/actors/character/inventory.hbs'
}, },
biography: { biography: {
id: 'biography', id: 'biography',
scrollable: ['.items-section'],
template: 'systems/daggerheart/templates/sheets/actors/character/biography.hbs' template: 'systems/daggerheart/templates/sheets/actors/character/biography.hbs'
}, },
effects: { effects: {
id: 'effects', id: 'effects',
scrollable: ['.effects-sections'],
template: 'systems/daggerheart/templates/sheets/actors/character/effects.hbs' template: 'systems/daggerheart/templates/sheets/actors/character/effects.hbs'
} }
}; };
@ -118,6 +140,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
}); });
htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => { htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => {
element.addEventListener('change', this.updateItemQuantity.bind(this)); element.addEventListener('change', this.updateItemQuantity.bind(this));
element.addEventListener('click', e => e.stopPropagation());
}); });
// Add listener for armor marks input // Add listener for armor marks input
@ -126,16 +149,37 @@ export default class CharacterSheet extends DHBaseActorSheet {
}); });
} }
/** @inheritdoc */
_initializeApplicationOptions(options) {
const applicationOptions = super._initializeApplicationOptions(options);
if (applicationOptions.document.testUserPermission(game.user, 'LIMITED', { exact: true })) {
applicationOptions.position.width = 360;
applicationOptions.position.height = 'auto';
}
return applicationOptions;
}
/** @inheritDoc */ /** @inheritDoc */
async _onRender(context, options) { async _onRender(context, options) {
await super._onRender(context, options); await super._onRender(context, options);
this.element if (!this.document.testUserPermission(game.user, 'LIMITED', { exact: true })) {
.querySelector('.level-value') this.element
?.addEventListener('change', event => this.document.updateLevel(Number(event.currentTarget.value))); .querySelector('.level-value')
?.addEventListener('change', event => this.document.updateLevel(Number(event.currentTarget.value)));
this._createFilterMenus(); const observer = this.document.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER, {
this._createSearchFilter(); exact: true
});
if (observer) {
this.element.querySelector('.window-content').classList.add('viewMode');
}
this._createFilterMenus();
this._createSearchFilter();
}
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -155,6 +199,16 @@ export default class CharacterSheet extends DHBaseActorSheet {
return acc; return acc;
}, {}); }, {});
context.resources = Object.keys(this.document.system.resources).reduce((acc, key) => {
acc[key] = this.document.system.resources[key];
return acc;
}, {});
const maxResource = Math.max(context.resources.hitPoints.max, context.resources.stress.max);
context.resources.hitPoints.emptyPips =
context.resources.hitPoints.max < maxResource ? maxResource - context.resources.hitPoints.max : 0;
context.resources.stress.emptyPips =
context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0;
context.inventory = { context.inventory = {
currency: { currency: {
title: game.i18n.localize('DAGGERHEART.CONFIG.Gold.title'), title: game.i18n.localize('DAGGERHEART.CONFIG.Gold.title'),
@ -210,7 +264,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @protected * @protected
*/ */
async _prepareLoadoutContext(context, _options) { async _prepareLoadoutContext(context, _options) {
context.cardView = !game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsList); context.cardView = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsCard);
} }
/** /**
@ -586,7 +640,14 @@ export default class CharacterSheet extends DHBaseActorSheet {
if (!value || !subclass) if (!value || !subclass)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.missingClassOrSubclass')); return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.missingClassOrSubclass'));
new DhCharacterlevelUp(this.document).render({ force: true }); new CharacterLevelup(this.document).render({ force: true });
}
/**
* Opens the charater level management window in viewMode.
*/
static #viewLevelups() {
new LevelupViewMode(this.document).render({ force: true });
} }
/** /**
@ -605,7 +666,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
const { key } = button.dataset; const { key } = button.dataset;
const presets = { const presets = {
compendium: 'daggerheart',
folder: key, folder: key,
filter: filter:
key === 'subclasses' key === 'subclasses'
@ -621,7 +681,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
} }
}; };
return new ItemBrowser({ presets }).render({ force: true }); ui.compendiumBrowser.open(presets);
} }
/** /**
@ -649,31 +709,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
}) })
}); });
this.consumeResource(result?.costs); if (result) game.system.api.fields.ActionFields.CostField.execute.call(this, result);
}
// Remove when Action Refactor part #2 done
async consumeResource(costs) {
if (!costs?.length) return;
const usefulResources = {
...foundry.utils.deepClone(this.actor.system.resources),
fear: {
value: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
max: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear,
reversed: false
}
};
const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(costs).map(c => {
const resource = usefulResources[c.key];
return {
key: c.key,
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
target: resource.target,
keyIsID: resource.keyIsID
};
});
await this.actor.modifyResource(resources);
} }
//TODO: redo toggleEquipItem method //TODO: redo toggleEquipItem method
@ -718,11 +754,42 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #toggleLoadoutView(_, button) { static async #toggleLoadoutView(_, button) {
const newAbilityView = button.dataset.value !== 'true'; const newAbilityView = button.dataset.value === 'true';
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsList, newAbilityView); await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsCard, newAbilityView);
this.render(); this.render();
} }
/**
* Toggles hitpoint resource value.
* @type {ApplicationClickAction}
*/
static async #toggleHitPoints(_, button) {
const hitPointsValue = Number.parseInt(button.dataset.value);
const newValue =
this.document.system.resources.hitPoints.value >= hitPointsValue ? hitPointsValue - 1 : hitPointsValue;
await this.document.update({ 'system.resources.hitPoints.value': newValue });
}
/**
* Toggles stress resource value.
* @type {ApplicationClickAction}
*/
static async #toggleStress(_, button) {
const StressValue = Number.parseInt(button.dataset.value);
const newValue = this.document.system.resources.stress.value >= StressValue ? StressValue - 1 : StressValue;
await this.document.update({ 'system.resources.stress.value': newValue });
}
/**
* Toggles ArmorScore resource value.
* @type {ApplicationClickAction}
*/
static async #toggleArmor(_, button, element) {
const ArmorValue = Number.parseInt(button.dataset.value);
const newValue = this.document.system.armor.system.marks.value >= ArmorValue ? ArmorValue - 1 : ArmorValue;
await this.document.system.armor.update({ 'system.marks.value': newValue });
}
/** /**
* Toggles a hope resource value. * Toggles a hope resource value.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
@ -762,13 +829,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
}); });
} }
/**
* Temp
*/
static async #tempBrowser(_, target) {
new ItemBrowser().render({ force: true });
}
/** /**
* Handle the roll values of resource dice. * Handle the roll values of resource dice.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
@ -828,6 +888,23 @@ export default class CharacterSheet extends DHBaseActorSheet {
itemData.system.inVault = true; itemData.system.inVault = true;
} }
if (item.type === 'beastform') {
if (this.document.effects.find(x => x.type === 'beastform')) {
return ui.notifications.warn(
game.i18n.localize('DAGGERHEART.UI.Notifications.beastformAlreadyApplied')
);
}
const data = await game.system.api.data.items.DHBeastform.getWildcardImage(this.document, itemData);
if (data) {
if (!data.selectedImage) return;
else {
if (data.usesDynamicToken) itemData.system.tokenRingImg = data.selectedImage;
else itemData.system.tokenImg = data.selectedImage;
}
}
}
if (this.document.uuid === item.parent?.uuid) return this._onSortItem(event, itemData); if (this.document.uuid === item.parent?.uuid) return this._onSortItem(event, itemData);
const createdItem = await this._onDropItemCreate(itemData); const createdItem = await this._onDropItemCreate(itemData);

View file

@ -8,14 +8,23 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
classes: ['actor', 'companion'], classes: ['actor', 'companion'],
position: { width: 340 }, position: { width: 340 },
actions: { actions: {
toggleStress: DhCompanionSheet.#toggleStress,
actionRoll: DhCompanionSheet.#actionRoll,
levelManagement: DhCompanionSheet.#levelManagement levelManagement: DhCompanionSheet.#levelManagement
} }
}; };
static PARTS = { static PARTS = {
limited: {
template: 'systems/daggerheart/templates/sheets/actors/companion/limited.hbs',
scrollable: ['.limited-container']
},
header: { template: 'systems/daggerheart/templates/sheets/actors/companion/header.hbs' }, header: { template: 'systems/daggerheart/templates/sheets/actors/companion/header.hbs' },
details: { template: 'systems/daggerheart/templates/sheets/actors/companion/details.hbs' }, details: { template: 'systems/daggerheart/templates/sheets/actors/companion/details.hbs' },
effects: { template: 'systems/daggerheart/templates/sheets/actors/companion/effects.hbs' } effects: {
template: 'systems/daggerheart/templates/sheets/actors/companion/effects.hbs',
scrollable: ['.effects-sections']
}
}; };
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -42,6 +51,61 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
/* Application Clicks Actions */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
/**
* Toggles stress resource value.
* @type {ApplicationClickAction}
*/
static async #toggleStress(_, button) {
const StressValue = Number.parseInt(button.dataset.value);
const newValue = this.document.system.resources.stress.value >= StressValue ? StressValue - 1 : StressValue;
await this.document.update({ 'system.resources.stress.value': newValue });
}
/**
*
*/
static async #actionRoll(event) {
const partner = this.actor.system.partner;
const config = {
event,
title: `${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}: ${this.actor.name}`,
headerTitle: `Companion ${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}`,
roll: {
trait: partner.system.spellcastModifierTrait?.key
},
hasRoll: true,
data: partner.getRollData()
};
const result = await partner.diceRoll(config);
this.consumeResource(result?.costs);
}
// Remove when Action Refactor part #2 done
async consumeResource(costs) {
if (!costs?.length) return;
const partner = this.actor.system.partner;
const usefulResources = {
...foundry.utils.deepClone(partner.system.resources),
fear: {
value: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
max: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear,
reversed: false
}
};
const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(costs).map(c => {
const resource = usefulResources[c.key];
return {
key: c.key,
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
target: resource.target
};
});
await partner.modifyResource(resources);
}
/** /**
* Opens the companions level management window. * Opens the companions level management window.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}

View file

@ -1,3 +1,4 @@
import { getDocFromElement } from '../../../helpers/utils.mjs';
import DHBaseActorSheet from '../api/base-actor.mjs'; import DHBaseActorSheet from '../api/base-actor.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
@ -8,21 +9,39 @@ export default class DhpEnvironment extends DHBaseActorSheet {
classes: ['environment'], classes: ['environment'],
position: { position: {
width: 500, width: 500,
height: 725 height: 740
}, },
window: { window: {
resizable: true resizable: true,
controls: [
{
icon: 'fa-solid fa-signature',
label: 'DAGGERHEART.UI.Tooltip.configureAttribution',
action: 'editAttribution'
}
]
},
actions: {
toggleResourceDice: DhpEnvironment.#toggleResourceDice,
handleResourceDice: DhpEnvironment.#handleResourceDice
}, },
actions: {},
dragDrop: [{ dragSelector: '.action-section .inventory-item', dropSelector: null }] dragDrop: [{ dragSelector: '.action-section .inventory-item', dropSelector: null }]
}; };
/**@override */ /**@override */
static PARTS = { static PARTS = {
limited: {
template: 'systems/daggerheart/templates/sheets/actors/environment/limited.hbs',
scrollable: ['.limited-container']
},
header: { template: 'systems/daggerheart/templates/sheets/actors/environment/header.hbs' }, header: { template: 'systems/daggerheart/templates/sheets/actors/environment/header.hbs' },
features: { template: 'systems/daggerheart/templates/sheets/actors/environment/features.hbs' }, features: {
template: 'systems/daggerheart/templates/sheets/actors/environment/features.hbs',
scrollable: ['feature-section']
},
potentialAdversaries: { potentialAdversaries: {
template: 'systems/daggerheart/templates/sheets/actors/environment/potentialAdversaries.hbs' template: 'systems/daggerheart/templates/sheets/actors/environment/potentialAdversaries.hbs',
scrollable: ['items-sections']
}, },
notes: { template: 'systems/daggerheart/templates/sheets/actors/environment/notes.hbs' } notes: { template: 'systems/daggerheart/templates/sheets/actors/environment/notes.hbs' }
}; };
@ -36,12 +55,25 @@ export default class DhpEnvironment extends DHBaseActorSheet {
} }
}; };
/** @inheritdoc */
_initializeApplicationOptions(options) {
const applicationOptions = super._initializeApplicationOptions(options);
if (applicationOptions.document.testUserPermission(game.user, 'LIMITED', { exact: true })) {
applicationOptions.position.width = 360;
applicationOptions.position.height = 'auto';
}
return applicationOptions;
}
/**@inheritdoc */ /**@inheritdoc */
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': case 'header':
await this._prepareHeaderContext(context, options); await this._prepareHeaderContext(context, options);
break; break;
case 'notes': case 'notes':
await this._prepareNotesContext(context, options); await this._prepareNotesContext(context, options);
@ -106,4 +138,42 @@ export default class DhpEnvironment extends DHBaseActorSheet {
event.dataTransfer.setDragImage(item, 60, 0); event.dataTransfer.setDragImage(item, 60, 0);
} }
} }
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
/**
* Toggle the used state of a resource dice.
* @type {ApplicationClickAction}
*/
static async #toggleResourceDice(event, target) {
const item = await getDocFromElement(target);
const { dice } = event.target.closest('.item-resource').dataset;
const diceState = item.system.resource.diceStates[dice];
await item.update({
[`system.resource.diceStates.${dice}.used`]: diceState ? !diceState.used : true
});
}
/**
* Handle the roll values of resource dice.
* @type {ApplicationClickAction}
*/
static async #handleResourceDice(_, target) {
const item = await getDocFromElement(target);
if (!item) return;
const rollValues = await game.system.api.applications.dialogs.ResourceDiceDialog.create(item, this.document);
if (!rollValues) return;
await item.update({
'system.resource.diceStates': rollValues.reduce((acc, state, index) => {
acc[index] = { value: state.value, used: state.used };
return acc;
}, {})
});
}
} }

View file

@ -0,0 +1,512 @@
import DHBaseActorSheet from '../api/base-actor.mjs';
import { getDocFromElement } from '../../../helpers/utils.mjs';
import { ItemBrowser } from '../../ui/itemBrowser.mjs';
import FilterMenu from '../../ux/filter-menu.mjs';
import DaggerheartMenu from '../../sidebar/tabs/daggerheartMenu.mjs';
import { socketEvent } from '../../../systemRegistration/socket.mjs';
import GroupRollDialog from '../../dialogs/group-roll-dialog.mjs';
import DhpActor from '../../../documents/actor.mjs';
import DHItem from '../../../documents/item.mjs';
export default class Party extends DHBaseActorSheet {
constructor(options) {
super(options);
this.refreshSelections = DaggerheartMenu.defaultRefreshSelections();
}
/**@inheritdoc */
static DEFAULT_OPTIONS = {
classes: ['party'],
position: {
width: 550
},
window: {
resizable: true
},
actions: {
deletePartyMember: Party.#deletePartyMember,
deleteItem: Party.#deleteItem,
toggleHope: Party.#toggleHope,
toggleHitPoints: Party.#toggleHitPoints,
toggleStress: Party.#toggleStress,
toggleArmorSlot: Party.#toggleArmorSlot,
tempBrowser: Party.#tempBrowser,
refeshActions: Party.#refeshActions,
triggerRest: Party.#triggerRest,
tagTeamRoll: Party.#tagTeamRoll,
groupRoll: Party.#groupRoll,
selectRefreshable: DaggerheartMenu.selectRefreshable,
refreshActors: DaggerheartMenu.refreshActors
},
dragDrop: [{ dragSelector: '.actors-section .inventory-item', dropSelector: null }]
};
/**@override */
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/actors/party/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
partyMembers: { template: 'systems/daggerheart/templates/sheets/actors/party/party-members.hbs' },
resources: {
template: 'systems/daggerheart/templates/sheets/actors/party/resources.hbs',
scrollable: ['']
},
/* NOT YET IMPLEMENTED */
// projects: {
// template: 'systems/daggerheart/templates/sheets/actors/party/projects.hbs',
// scrollable: ['']
// },
inventory: {
template: 'systems/daggerheart/templates/sheets/actors/party/inventory.hbs',
scrollable: ['.tab.inventory .items-section']
},
notes: { template: 'systems/daggerheart/templates/sheets/actors/party/notes.hbs' }
};
/** @inheritdoc */
static TABS = {
primary: {
tabs: [
{ id: 'partyMembers' },
{ id: 'resources' },
/* NOT YET IMPLEMENTED */
// { id: 'projects' },
{ id: 'inventory' },
{ id: 'notes' }
],
initial: 'partyMembers',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
};
async _onRender(context, options) {
await super._onRender(context, options);
this._createFilterMenus();
this._createSearchFilter();
}
/* -------------------------------------------- */
/* Prepare Context */
/* -------------------------------------------- */
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.inventory = {
currency: {
title: game.i18n.localize('DAGGERHEART.CONFIG.Gold.title'),
coins: game.i18n.localize('DAGGERHEART.CONFIG.Gold.coins'),
handfuls: game.i18n.localize('DAGGERHEART.CONFIG.Gold.handfuls'),
bags: game.i18n.localize('DAGGERHEART.CONFIG.Gold.bags'),
chests: game.i18n.localize('DAGGERHEART.CONFIG.Gold.chests')
}
};
const homebrewCurrency = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).currency;
if (homebrewCurrency.enabled) {
context.inventory.currency = homebrewCurrency;
}
if (context.inventory.length === 0) {
context.inventory = Array(1).fill(Array(5).fill([]));
}
return context;
}
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'header':
await this._prepareHeaderContext(context, options);
break;
case 'notes':
await this._prepareNotesContext(context, options);
break;
}
return context;
}
/**
* Prepare render context for the Header part.
* @param {ApplicationRenderContext} context
* @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/
async _prepareHeaderContext(context, _options) {
const { system } = this.document;
const { TextEditor } = foundry.applications.ux;
context.description = await TextEditor.implementation.enrichHTML(system.description, {
secrets: this.document.isOwner,
relativeTo: this.document
});
}
/**
* Prepare render context for the Biography part.
* @param {ApplicationRenderContext} context
* @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/
async _prepareNotesContext(context, _options) {
const { system } = this.document;
const { TextEditor } = foundry.applications.ux;
const paths = {
notes: 'notes'
};
for (const [key, path] of Object.entries(paths)) {
const value = foundry.utils.getProperty(system, path);
context[key] = {
field: system.schema.getField(path),
value,
enriched: await TextEditor.implementation.enrichHTML(value, {
secrets: this.document.isOwner,
relativeTo: this.document
})
};
}
}
/**
* Toggles a hope resource value.
* @type {ApplicationClickAction}
*/
static async #toggleHope(_, target) {
const hopeValue = Number.parseInt(target.dataset.value);
const actor = await foundry.utils.fromUuid(target.dataset.actorId);
const newValue = actor.system.resources.hope.value >= hopeValue ? hopeValue - 1 : hopeValue;
await actor.update({ 'system.resources.hope.value': newValue });
this.render();
}
/**
* Toggles a hp resource value.
* @type {ApplicationClickAction}
*/
static async #toggleHitPoints(_, target) {
const hitPointsValue = Number.parseInt(target.dataset.value);
const actor = await foundry.utils.fromUuid(target.dataset.actorId);
const newValue = actor.system.resources.hitPoints.value >= hitPointsValue ? hitPointsValue - 1 : hitPointsValue;
await actor.update({ 'system.resources.hitPoints.value': newValue });
this.render();
}
/**
* Toggles a stress resource value.
* @type {ApplicationClickAction}
*/
static async #toggleStress(_, target) {
const stressValue = Number.parseInt(target.dataset.value);
const actor = await foundry.utils.fromUuid(target.dataset.actorId);
const newValue = actor.system.resources.stress.value >= stressValue ? stressValue - 1 : stressValue;
await actor.update({ 'system.resources.stress.value': newValue });
this.render();
}
/**
* Toggles a armor slot resource value.
* @type {ApplicationClickAction}
*/
static async #toggleArmorSlot(_, target, element) {
const armorItem = await foundry.utils.fromUuid(target.dataset.itemUuid);
const armorValue = Number.parseInt(target.dataset.value);
const newValue = armorItem.system.marks.value >= armorValue ? armorValue - 1 : armorValue;
await armorItem.update({ 'system.marks.value': newValue });
this.render();
}
/**
* Opens Compedium Browser
*/
static async #tempBrowser(_, target) {
new ItemBrowser().render({ force: true });
}
static async #refeshActions() {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: 'New Section',
icon: 'fa-solid fa-campground'
},
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sidebar/daggerheart-menu/main.hbs',
{
refreshables: DaggerheartMenu.defaultRefreshSelections()
}
),
classes: ['daggerheart', 'dialog', 'dh-style', 'tab', 'sidebar-tab', 'daggerheartMenu-sidebar']
});
if (!confirmed) return;
}
static async #triggerRest(_, button) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize(`DAGGERHEART.APPLICATIONS.Downtime.${button.dataset.type}.title`),
icon: button.dataset.type === 'shortRest' ? 'fa-solid fa-utensils' : 'fa-solid fa-bed'
},
content: 'This will trigger a dialog to players make their downtime moves, are you sure?',
classes: ['daggerheart', 'dialog', 'dh-style']
});
if (!confirmed) return;
this.document.system.partyMembers.forEach(actor => {
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.DowntimeTrigger,
data: {
actorId: actor.uuid,
downtimeType: button.dataset.type
}
});
});
}
static async downtimeMoveQuery({ actorId, downtimeType }) {
const actor = await foundry.utils.fromUuid(actorId);
if (!actor || !actor?.isOwner) reject();
new game.system.api.applications.dialogs.Downtime(actor, downtimeType === 'shortRest').render({
force: true
});
}
static async #tagTeamRoll() {
new game.system.api.applications.dialogs.TagTeamDialog(this.document.system.partyMembers).render({
force: true
});
}
static async #groupRoll(params) {
new GroupRollDialog(this.document.system.partyMembers).render({ force: true });
}
/**
* Get the set of ContextMenu options for Consumable and Loot.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
* @this {CharacterSheet}
* @protected
*/
static #getItemContextOptions() {
return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true });
}
/* -------------------------------------------- */
/* Filter Tracking */
/* -------------------------------------------- */
/**
* The currently active search filter.
* @type {foundry.applications.ux.SearchFilter}
*/
#search = {};
/**
* The currently active search filter.
* @type {FilterMenu}
*/
#menu = {};
/**
* Tracks which item IDs are currently displayed, organized by filter type and section.
* @type {{
* inventory: {
* search: Set<string>,
* menu: Set<string>
* },
* loadout: {
* search: Set<string>,
* menu: Set<string>
* },
* }}
*/
#filteredItems = {
inventory: {
search: new Set(),
menu: new Set()
},
loadout: {
search: new Set(),
menu: new Set()
}
};
/* -------------------------------------------- */
/* Search Inputs */
/* -------------------------------------------- */
/**
* Create and initialize search filter instances for the inventory and loadout sections.
*
* Sets up two {@link foundry.applications.ux.SearchFilter} instances:
* - One for the inventory, which filters items in the inventory grid.
* - One for the loadout, which filters items in the loadout/card grid.
* @private
*/
_createSearchFilter() {
//Filters could be a application option if needed
const filters = [
{
key: 'inventory',
input: 'input[type="search"].search-inventory',
content: '[data-application-part="inventory"] .items-section',
callback: this._onSearchFilterInventory.bind(this)
}
];
for (const { key, input, content, callback } of filters) {
const filter = new foundry.applications.ux.SearchFilter({
inputSelector: input,
contentSelector: content,
callback
});
filter.bind(this.element);
this.#search[key] = filter;
}
}
/**
* Handle invetory items search and filtering.
* @param {KeyboardEvent} event The keyboard input event.
* @param {string} query The input search string.
* @param {RegExp} rgx The regular expression query that should be matched against.
* @param {HTMLElement} html The container to filter items from.
* @protected
*/
async _onSearchFilterInventory(_event, query, rgx, html) {
this.#filteredItems.inventory.search.clear();
for (const li of html.querySelectorAll('.inventory-item')) {
const item = await getDocFromElement(li);
const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name);
if (matchesSearch) this.#filteredItems.inventory.search.add(item.id);
const { menu } = this.#filteredItems.inventory;
li.hidden = !(menu.has(item.id) && matchesSearch);
}
}
/* -------------------------------------------- */
/* Filter Menus */
/* -------------------------------------------- */
_createFilterMenus() {
//Menus could be a application option if needed
const menus = [
{
key: 'inventory',
container: '[data-application-part="inventory"]',
content: '.items-section',
callback: this._onMenuFilterInventory.bind(this),
target: '.filter-button',
filters: FilterMenu.invetoryFilters
}
];
menus.forEach(m => {
const container = this.element.querySelector(m.container);
this.#menu[m.key] = new FilterMenu(container, m.target, m.filters, m.callback, {
contentSelector: m.content
});
});
}
/**
* Callback when filters change
* @param {PointerEvent} event
* @param {HTMLElement} html
* @param {import('../ux/filter-menu.mjs').FilterItem[]} filters
*/
async _onMenuFilterInventory(_event, html, filters) {
this.#filteredItems.inventory.menu.clear();
for (const li of html.querySelectorAll('.inventory-item')) {
const item = await getDocFromElement(li);
const matchesMenu =
filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f));
if (matchesMenu) this.#filteredItems.inventory.menu.add(item.id);
const { search } = this.#filteredItems.inventory;
li.hidden = !(search.has(item.id) && matchesMenu);
}
}
/* -------------------------------------------- */
async _onDragStart(event) {
const item = event.currentTarget.closest('.inventory-item');
if (item) {
const adversaryData = { type: 'Actor', uuid: item.dataset.itemUuid };
event.dataTransfer.setData('text/plain', JSON.stringify(adversaryData));
event.dataTransfer.setDragImage(item, 60, 0);
}
}
async _onDrop(event) {
// Prevent event bubbling to avoid duplicate handling
event.preventDefault();
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await foundry.utils.fromUuid(data.uuid);
if (item instanceof DhpActor) {
const currentMembers = this.document.system.partyMembers.map(x => x.uuid);
if (currentMembers.includes(data.uuid)) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.duplicateCharacter'));
}
await this.document.update({ 'system.partyMembers': [...currentMembers, item.uuid] });
} else if (item instanceof DHItem) {
this.document.createEmbeddedDocuments('Item', [item.toObject()]);
} else {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.onlyCharactersInPartySheet'));
}
}
static async #deletePartyMember(event, target) {
const doc = await getDocFromElement(target.closest('.inventory-item'));
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize('TYPES.Actor.adversary'),
name: doc.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: doc.name })
});
if (!confirmed) return;
}
const currentMembers = this.document.system.partyMembers.map(x => x.uuid);
const newMemberdList = currentMembers.filter(uuid => uuid !== doc.uuid);
await this.document.update({ 'system.partyMembers': newMemberdList });
}
static async #deleteItem(event, target) {
const doc = await getDocFromElement(target.closest('.inventory-item'));
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize('TYPES.Actor.party'),
name: doc.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: doc.name })
});
if (!confirmed) return;
}
this.document.deleteEmbeddedDocuments('Item', [doc.id]);
}
}

View file

@ -44,9 +44,8 @@ export default class DHBaseActorSettings extends DHApplicationMixin(DocumentShee
const context = await super._prepareContext(options); const context = await super._prepareContext(options);
context.isNPC = this.actor.isNPC; context.isNPC = this.actor.isNPC;
if (context.systemFields.attack) { if (context.systemFields.attack)
context.systemFields.attack.fields = this.actor.system.attack.schema.fields; context.systemFields.attack.fields = this.actor.system.attack.schema.fields;
}
return context; return context;
} }

View file

@ -1,6 +1,5 @@
const { HandlebarsApplicationMixin } = foundry.applications.api; const { HandlebarsApplicationMixin } = foundry.applications.api;
import { getDocFromElement, getDocFromElementSync, tagifyElement } from '../../../helpers/utils.mjs'; import { getDocFromElement, getDocFromElementSync, tagifyElement } from '../../../helpers/utils.mjs';
import { ItemBrowser } from '../../ui/itemBrowser.mjs';
const typeSettingsMap = { const typeSettingsMap = {
character: 'extendCharacterDescriptions', character: 'extendCharacterDescriptions',
@ -85,6 +84,8 @@ export default function DHApplicationMixin(Base) {
this._dragDrop = this._createDragDropHandlers(); this._dragDrop = this._createDragDropHandlers();
} }
#nonHeaderAttribution = ['environment', 'ancestry', 'community', 'domainCard'];
/** /**
* The default options for the sheet. * The default options for the sheet.
* @type {DHSheetV2Configuration} * @type {DHSheetV2Configuration}
@ -98,10 +99,12 @@ export default function DHApplicationMixin(Base) {
deleteDoc: DHSheetV2.#deleteDoc, deleteDoc: DHSheetV2.#deleteDoc,
toChat: DHSheetV2.#toChat, toChat: DHSheetV2.#toChat,
useItem: DHSheetV2.#useItem, useItem: DHSheetV2.#useItem,
viewItem: DHSheetV2.#viewItem,
toggleEffect: DHSheetV2.#toggleEffect, toggleEffect: DHSheetV2.#toggleEffect,
toggleExtended: DHSheetV2.#toggleExtended, toggleExtended: DHSheetV2.#toggleExtended,
addNewItem: DHSheetV2.#addNewItem, addNewItem: DHSheetV2.#addNewItem,
browseItem: DHSheetV2.#browseItem browseItem: DHSheetV2.#browseItem,
editAttribution: DHSheetV2.#editAttribution
}, },
contextMenus: [ contextMenus: [
{ {
@ -125,6 +128,43 @@ export default function DHApplicationMixin(Base) {
tagifyConfigs: [] tagifyConfigs: []
}; };
/**@inheritdoc */
async _renderFrame(options) {
const frame = await super._renderFrame(options);
const hideAttribution = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance
).hideAttribution;
const headerAttribution = !this.#nonHeaderAttribution.includes(this.document.type);
if (!hideAttribution && this.document.system.metadata.hasAttribution && headerAttribution) {
const { source, page } = this.document.system.attribution;
const attribution = [source, page ? `pg ${page}.` : null].filter(x => x).join('. ');
const element = `<label class="attribution-header-label">${attribution}</label>`;
this.window.controls.insertAdjacentHTML('beforebegin', element);
}
return frame;
}
/**
* Refresh the custom parts of the application frame
*/
refreshFrame() {
const hideAttribution = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance
).hideAttribution;
const headerAttribution = !this.#nonHeaderAttribution.includes(this.document.type);
if (!hideAttribution && this.document.system.metadata.hasAttribution && headerAttribution) {
const { source, page } = this.document.system.attribution;
const attribution = [source, page ? `pg ${page}.` : null].filter(x => x).join('. ');
const label = this.window.header.querySelector('.attribution-header-label');
label.innerHTML = attribution;
}
}
/** /**
* Related documents that should cause a rerender of this application when updated. * Related documents that should cause a rerender of this application when updated.
*/ */
@ -164,11 +204,19 @@ export default function DHApplicationMixin(Base) {
this.relatedDocs.filter(doc => doc).map(doc => delete doc.apps[this.id]); this.relatedDocs.filter(doc => doc).map(doc => delete doc.apps[this.id]);
} }
/** @inheritdoc */
async _renderHTML(context, options) {
const rendered = await super._renderHTML(context, options);
for (const result of Object.values(rendered)) {
await this.#prepareInventoryDescription(result);
}
return rendered;
}
/**@inheritdoc */ /**@inheritdoc */
async _onRender(context, options) { async _onRender(context, options) {
await super._onRender(context, options); await super._onRender(context, options);
this._createTagifyElements(this.options.tagifyConfigs); this._createTagifyElements(this.options.tagifyConfigs);
await this.#prepareInventoryDescription(context);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -176,8 +224,8 @@ export default function DHApplicationMixin(Base) {
/* -------------------------------------------- */ /* -------------------------------------------- */
/**@inheritdoc */ /**@inheritdoc */
_syncPartState(partId, newElement, priorElement, state) { _preSyncPartState(partId, newElement, priorElement, state) {
super._syncPartState(partId, newElement, priorElement, state); super._preSyncPartState(partId, newElement, priorElement, state);
for (const el of priorElement.querySelectorAll('.extensible.extended')) { for (const el of priorElement.querySelectorAll('.extensible.extended')) {
const { actionId, itemUuid } = el.parentElement.dataset; const { actionId, itemUuid } = el.parentElement.dataset;
const selector = `${actionId ? `[data-action-id="${actionId}"]` : `[data-item-uuid="${itemUuid}"]`} .extensible`; const selector = `${actionId ? `[data-action-id="${actionId}"]` : `[data-item-uuid="${itemUuid}"]`} .extensible`;
@ -372,6 +420,19 @@ export default function DHApplicationMixin(Base) {
]; ];
if (usable) { if (usable) {
options.unshift({
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.cancelBeastform',
icon: 'fa-solid fa-ban',
condition: target => {
const doc = getDocFromElementSync(target);
return doc && doc.system?.actions?.some(a => a.type === 'beastform');
},
callback: async target =>
game.system.api.fields.ActionFields.BeastformField.handleActiveTransformations.call(
await getDocFromElement(target)
)
});
options.unshift({ options.unshift({
name: 'DAGGERHEART.GENERAL.damage', name: 'DAGGERHEART.GENERAL.damage',
icon: 'fa-solid fa-explosion', icon: 'fa-solid fa-explosion',
@ -382,7 +443,9 @@ export default function DHApplicationMixin(Base) {
callback: async (target, event) => { callback: async (target, event) => {
const doc = await getDocFromElement(target), const doc = await getDocFromElement(target),
action = doc?.system?.attack ?? doc; action = doc?.system?.attack ?? doc;
return action && action.use(event, { byPassRoll: true }); const config = action.prepareConfig(event);
config.hasRoll = false;
return action && action.workflow.get('damage').execute(config, null, true);
} }
}); });
@ -401,7 +464,7 @@ export default function DHApplicationMixin(Base) {
options.push({ options.push({
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat', name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
icon: 'fa-solid fa-message', icon: 'fa-solid fa-message',
callback: async target => (await getDocFromElement(target)).toChat(this.document.id) callback: async target => (await getDocFromElement(target)).toChat(this.document.uuid)
}); });
if (deletable) if (deletable)
@ -442,11 +505,12 @@ export default function DHApplicationMixin(Base) {
/** /**
* Prepares and enriches an inventory item or action description for display. * Prepares and enriches an inventory item or action description for display.
* @param {HTMLElement} element the element to enrich the inventory items of
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async #prepareInventoryDescription(context) { async #prepareInventoryDescription(element) {
// Get all inventory item elements with a data-item-uuid attribute // Get all inventory item elements with a data-item-uuid attribute
const inventoryItems = this.element.querySelectorAll('.inventory-item[data-item-uuid]'); const inventoryItems = element.querySelectorAll('.inventory-item[data-item-uuid]');
for (const el of inventoryItems) { for (const el of inventoryItems) {
// Get the doc uuid from the element // Get the doc uuid from the element
const { itemUuid } = el?.dataset || {}; const { itemUuid } = el?.dataset || {};
@ -537,28 +601,27 @@ export default function DHApplicationMixin(Base) {
static async #browseItem(event, target) { static async #browseItem(event, target) {
const type = target.dataset.compendium ?? target.dataset.type; const type = target.dataset.compendium ?? target.dataset.type;
const presets = {}; const presets = {
render: {
noFolder: true
}
};
switch (type) { switch (type) {
case 'loot': case 'loot':
presets.folder = 'equipments.folders.loots';
break;
case 'consumable': case 'consumable':
presets.folder = 'equipments.folders.consumables';
break;
case 'armor': case 'armor':
presets.folder = 'equipments.folders.armors';
break;
case 'weapon': case 'weapon':
presets.compendium = 'daggerheart'; presets.folder = 'equipments.folders.weapons';
presets.folder = 'equipments';
presets.render = {
noFolder: true
};
presets.filter = {
type: { key: 'type', value: type, forced: true }
};
break; break;
case 'domainCard': case 'domainCard':
presets.compendium = 'daggerheart';
presets.folder = 'domains'; presets.folder = 'domains';
presets.render = {
noFolder: true
};
presets.filter = { presets.filter = {
'level.max': { key: 'level.max', value: this.document.system.levelData.level.current }, 'level.max': { key: 'level.max', value: this.document.system.levelData.level.current },
'system.domain': { key: 'system.domain', value: this.document.system.domains } 'system.domain': { key: 'system.domain', value: this.document.system.domains }
@ -568,7 +631,15 @@ export default function DHApplicationMixin(Base) {
return; return;
} }
return new ItemBrowser({ presets }).render({ force: true }); ui.compendiumBrowser.open(presets);
}
/**
* Open the attribution dialog
* @type {ApplicationClickAction}
*/
static async #editAttribution() {
new game.system.api.applications.dialogs.AttributionDialog(this.document).render({ force: true });
} }
/** /**
@ -640,7 +711,7 @@ export default function DHApplicationMixin(Base) {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #toChat(_event, target) { static async #toChat(_event, target) {
let doc = await getDocFromElement(target); const doc = await getDocFromElement(target);
return doc.toChat(doc.uuid); return doc.toChat(doc.uuid);
} }
@ -649,10 +720,19 @@ export default function DHApplicationMixin(Base) {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #useItem(event, target) { static async #useItem(event, target) {
let doc = await getDocFromElement(target); const doc = await getDocFromElement(target);
await doc.use(event); await doc.use(event);
} }
/**
* View an item by opening its sheet
* @type {ApplicationClickAction}
*/
static async #viewItem(_, target) {
const doc = await getDocFromElement(target);
await doc.sheet.render({ force: true });
}
/** /**
* Toggle a ActiveEffect * Toggle a ActiveEffect
* @type {ApplicationClickAction} * @type {ApplicationClickAction}

View file

@ -47,6 +47,12 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
return (this.#settingSheet ??= SheetClass ? new SheetClass({ document: this.document }) : null); return (this.#settingSheet ??= SheetClass ? new SheetClass({ document: this.document }) : null);
} }
get isVisible() {
const viewPermission = this.document.testUserPermission(game.user, this.options.viewPermission);
const limitedOnly = this.document.testUserPermission(game.user, this.options.viewPermission, { exact: true });
return limitedOnly ? this.document.system.metadata.hasLimitedView : viewPermission;
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Prepare Context */ /* Prepare Context */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -55,6 +61,13 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.isNPC = this.document.isNPC; context.isNPC = this.document.isNPC;
context.useResourcePips = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance
).useResourcePips;
context.showAttribution = !game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
.hideAttribution;
return context; return context;
} }
@ -69,6 +82,31 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
return context; return context;
} }
_configureRenderParts(options) {
const parts = super._configureRenderParts(options);
if (!this.document.system.metadata.hasLimitedView) return parts;
if (this.document.testUserPermission(game.user, 'LIMITED', { exact: true })) return { limited: parts.limited };
return Object.keys(parts).reduce((acc, key) => {
if (key !== 'limited') acc[key] = parts[key];
return acc;
}, {});
}
/** @inheritDoc */
async _onRender(context, options) {
await super._onRender(context, options);
if (
this.document.system.metadata.hasLimitedView &&
this.document.testUserPermission(game.user, 'LIMITED', { exact: true })
) {
this.element.classList = `${this.element.classList} limited`;
}
}
/**@inheritdoc */ /**@inheritdoc */
_attachPartListeners(partId, htmlElement, options) { _attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options); super._attachPartListeners(partId, htmlElement, options);

View file

@ -13,7 +13,16 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['item'], classes: ['item'],
position: { width: 600 }, position: { width: 600 },
window: { resizable: true }, window: {
resizable: true,
controls: [
{
icon: 'fa-solid fa-signature',
label: 'DAGGERHEART.UI.Tooltip.configureAttribution',
action: 'editAttribution'
}
]
},
form: { form: {
submitOnChange: true submitOnChange: true
}, },
@ -55,6 +64,15 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
/* Prepare Context */ /* Prepare Context */
/* -------------------------------------------- */ /* -------------------------------------------- */
/**@inheritdoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.showAttribution = !game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
.hideAttribution;
return context;
}
/**@inheritdoc */ /**@inheritdoc */
async _preparePartContext(partId, context, options) { async _preparePartContext(partId, context, options) {
await super._preparePartContext(partId, context, options); await super._preparePartContext(partId, context, options);

View file

@ -8,7 +8,7 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
tagifyConfigs: [ tagifyConfigs: [
{ {
selector: '.features-input', selector: '.features-input',
options: () => CONFIG.DH.ITEM.armorFeatures, options: () => CONFIG.DH.ITEM.orderedArmorFeatures(),
callback: ArmorSheet.#onFeatureSelect callback: ArmorSheet.#onFeatureSelect
} }
] ]

View file

@ -46,6 +46,10 @@ export default class ClassSheet extends DHBaseItemSheet {
template: 'systems/daggerheart/templates/sheets/items/class/settings.hbs', template: 'systems/daggerheart/templates/sheets/items/class/settings.hbs',
scrollable: ['.settings'] scrollable: ['.settings']
}, },
questions: {
template: 'systems/daggerheart/templates/sheets/items/class/questions.hbs',
scrollable: ['.questions']
},
effects: { effects: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs', template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs',
scrollable: ['.effects'] scrollable: ['.effects']
@ -55,7 +59,13 @@ export default class ClassSheet extends DHBaseItemSheet {
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
primary: { primary: {
tabs: [{ id: 'description' }, { id: 'features' }, { id: 'settings' }, { id: 'effects' }], tabs: [
{ id: 'description' },
{ id: 'features' },
{ id: 'settings' },
{ id: 'questions' },
{ id: 'effects' }
],
initial: 'description', initial: 'description',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }

View file

@ -8,7 +8,7 @@ export default class WeaponSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
tagifyConfigs: [ tagifyConfigs: [
{ {
selector: '.features-input', selector: '.features-input',
options: () => CONFIG.DH.ITEM.weaponFeatures, options: () => CONFIG.DH.ITEM.orderedWeaponFeatures(),
callback: WeaponSheet.#onFeatureSelect callback: WeaponSheet.#onFeatureSelect
} }
] ]

View file

@ -0,0 +1,2 @@
export { default as DaggerheartMenu } from './tabs/daggerheartMenu.mjs';
export { default as DhSidebar } from './sidebar.mjs';

View file

@ -0,0 +1,73 @@
export default class DhSidebar extends foundry.applications.sidebar.Sidebar {
/** @override */
static TABS = {
chat: {
documentName: 'ChatMessage'
},
combat: {
documentName: 'Combat'
},
scenes: {
documentName: 'Scene',
gmOnly: true
},
actors: {
documentName: 'Actor'
},
items: {
documentName: 'Item'
},
journal: {
documentName: 'JournalEntry',
tooltip: 'SIDEBAR.TabJournal'
},
tables: {
documentName: 'RollTable'
},
cards: {
documentName: 'Cards'
},
macros: {
documentName: 'Macro'
},
playlists: {
documentName: 'Playlist'
},
compendium: {
tooltip: 'SIDEBAR.TabCompendium',
icon: 'fa-solid fa-book-atlas'
},
daggerheartMenu: {
tooltip: 'DAGGERHEART.UI.Sidebar.daggerheartMenu.title',
img: 'systems/daggerheart/assets/logos/FoundryBorneLogoWhite.svg',
gmOnly: true
},
settings: {
tooltip: 'SIDEBAR.TabSettings',
icon: 'fa-solid fa-gears'
}
};
/** @override */
static PARTS = {
tabs: {
id: 'tabs',
template: 'systems/daggerheart/templates/sidebar/tabs.hbs'
}
};
/** @override */
async _prepareTabContext(context, options) {
context.tabs = Object.entries(this.constructor.TABS).reduce((obj, [k, v]) => {
let { documentName, gmOnly, tooltip, icon, img } = v;
if (gmOnly && !game.user.isGM) return obj;
if (documentName) {
tooltip ??= getDocumentClass(documentName).metadata.labelPlural;
icon ??= CONFIG[documentName]?.sidebarIcon;
}
obj[k] = { tooltip, icon, img };
obj[k].active = this.tabGroups.primary === k;
return obj;
}, {});
}
}

View file

@ -0,0 +1,160 @@
const { HandlebarsApplicationMixin } = foundry.applications.api;
const { AbstractSidebarTab } = foundry.applications.sidebar;
/**
* The daggerheart menu tab.
* @extends {AbstractSidebarTab}
* @mixes HandlebarsApplication
*/
export default class DaggerheartMenu extends HandlebarsApplicationMixin(AbstractSidebarTab) {
constructor(options) {
super(options);
this.refreshSelections = DaggerheartMenu.defaultRefreshSelections();
}
static defaultRefreshSelections() {
return {
session: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.session') },
scene: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.scene') },
longRest: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.longrest') },
shortRest: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.shortrest') }
};
}
/** @override */
static DEFAULT_OPTIONS = {
classes: ['dh-style'],
window: {
title: 'SIDEBAR.TabSettings'
},
actions: {
selectRefreshable: DaggerheartMenu.#selectRefreshable,
refreshActors: DaggerheartMenu.#refreshActors
}
};
/** @override */
static tabName = 'daggerheartMenu';
/** @override */
static PARTS = {
main: { template: 'systems/daggerheart/templates/sidebar/daggerheart-menu/main.hbs' }
};
/* -------------------------------------------- */
/** @inheritDoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.refreshables = this.refreshSelections;
context.disableRefresh = Object.values(this.refreshSelections).every(x => !x.selected);
return context;
}
async getRefreshables(types) {
const refreshedActors = {};
for (let actor of game.actors) {
if (['character', 'adversary'].includes(actor.type) && actor.prototypeToken.actorLink) {
const updates = {};
for (let item of actor.items) {
if (item.system.metadata.hasResource && types.includes(item.system.resource?.recovery)) {
if (!refreshedActors[actor.id])
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
refreshedActors[actor.id].refreshed.add(
game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[item.system.resource.recovery].label)
);
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
const increasing =
item.system.resource.progression === CONFIG.DH.ITEM.itemResourceProgression.increasing.id;
updates[item.id].system = {
...updates[item.id].system,
'resource.value': increasing
? 0
: Roll.replaceFormulaData(item.system.resource.max, actor.getRollData())
};
}
if (item.system.metadata.hasActions) {
const refreshTypes = new Set();
const actions = item.system.actions.filter(action => {
if (types.includes(action.uses.recovery)) {
refreshTypes.add(action.uses.recovery);
return true;
}
return false;
});
if (actions.length === 0) continue;
if (!refreshedActors[actor.id])
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
refreshedActors[actor.id].refreshed.add(
...refreshTypes.map(type => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[type].label))
);
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
updates[item.id].system = {
...updates[item.id].system,
...actions.reduce(
(acc, action) => {
acc.actions[action.id] = { 'uses.value': 0 };
return acc;
},
{ actions: updates[item.id].system.actions ?? {} }
)
};
}
}
for (let key in updates) {
const update = updates[key];
await actor.items.get(key).update(update);
}
}
}
return refreshedActors;
}
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
static async #selectRefreshable(_event, button) {
const { type } = button.dataset;
this.refreshSelections[type].selected = !this.refreshSelections[type].selected;
this.render();
}
static async #refreshActors() {
const refreshKeys = Object.keys(this.refreshSelections).filter(key => this.refreshSelections[key].selected);
await this.getRefreshables(refreshKeys);
const types = refreshKeys.map(x => this.refreshSelections[x].label).join(', ');
ui.notifications.info(
game.i18n.format('DAGGERHEART.UI.Notifications.gmMenuRefresh', {
types: `[${types}]`
})
);
this.refreshSelections = DaggerheartMenu.defaultRefreshSelections();
const cls = getDocumentClass('ChatMessage');
const msg = {
user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/refreshMessage.hbs',
{
types: types
}
),
title: game.i18n.localize('DAGGERHEART.UI.Chat.refreshMessage.title'),
speaker: cls.getSpeaker()
};
cls.create(msg);
this.render();
}
}

View file

@ -1,5 +1,7 @@
export { default as CountdownEdit } from './countdownEdit.mjs';
export { default as DhCountdowns } from './countdowns.mjs';
export { default as DhChatLog } from './chatLog.mjs'; export { default as DhChatLog } from './chatLog.mjs';
export { default as DhCombatTracker } from './combatTracker.mjs'; export { default as DhCombatTracker } from './combatTracker.mjs';
export * as DhCountdowns from './countdowns.mjs';
export { default as DhFearTracker } from './fearTracker.mjs'; export { default as DhFearTracker } from './fearTracker.mjs';
export { default as DhHotbar } from './hotbar.mjs'; export { default as DhHotbar } from './hotbar.mjs';
export { ItemBrowser } from './itemBrowser.mjs';

View file

@ -1,4 +1,5 @@
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; import { abilities } from '../../config/actorConfig.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog { export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
constructor(options) { constructor(options) {
@ -37,7 +38,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
// } // }
// }, // },
{ {
name: 'Reroll Damage', name: game.i18n.localize('DAGGERHEART.UI.ChatLog.rerollDamage'),
icon: '<i class="fa-solid fa-dice"></i>', icon: '<i class="fa-solid fa-dice"></i>',
condition: li => { condition: li => {
const message = game.messages.get(li.dataset.messageId); const message = game.messages.get(li.dataset.messageId);
@ -55,21 +56,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
} }
addChatListeners = async (app, html, data) => { addChatListeners = async (app, html, data) => {
html.querySelectorAll('.duality-action-damage').forEach(element =>
element.addEventListener('click', event => this.onRollDamage(event, data.message))
);
html.querySelectorAll('.target-save').forEach(element =>
element.addEventListener('click', event => this.onRollSave(event, data.message))
);
html.querySelectorAll('.roll-all-save-button').forEach(element =>
element.addEventListener('click', event => this.onRollAllSave(event, data.message))
);
html.querySelectorAll('.simple-roll-button').forEach(element => html.querySelectorAll('.simple-roll-button').forEach(element =>
element.addEventListener('click', event => this.onRollSimple(event, data.message)) element.addEventListener('click', event => this.onRollSimple(event, data.message))
); );
html.querySelectorAll('.healing-button').forEach(element =>
element.addEventListener('click', event => this.onHealing(event, data.message))
);
html.querySelectorAll('.ability-use-button').forEach(element => html.querySelectorAll('.ability-use-button').forEach(element =>
element.addEventListener('click', event => this.abilityUseButton(event, data.message)) element.addEventListener('click', event => this.abilityUseButton(event, data.message))
); );
@ -79,6 +68,18 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
html.querySelectorAll('.reroll-button').forEach(element => html.querySelectorAll('.reroll-button').forEach(element =>
element.addEventListener('click', event => this.rerollEvent(event, data.message)) element.addEventListener('click', event => this.rerollEvent(event, data.message))
); );
html.querySelectorAll('.group-roll-button').forEach(element =>
element.addEventListener('click', event => this.groupRollButton(event, data.message))
);
html.querySelectorAll('.group-roll-reroll').forEach(element =>
element.addEventListener('click', event => this.groupRollReroll(event, data.message))
);
html.querySelectorAll('.group-roll-success').forEach(element =>
element.addEventListener('click', event => this.groupRollSuccessEvent(event, data.message))
);
html.querySelectorAll('.group-roll-header-expand-section').forEach(element =>
element.addEventListener('click', this.groupRollExpandSection)
);
}; };
setupHooks() { setupHooks() {
@ -90,80 +91,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
super.close(options); super.close(options);
} }
async getActor(uuid) {
return await foundry.utils.fromUuid(uuid);
}
getAction(actor, itemId, actionId) {
const item = actor.items.get(itemId),
action =
actor.system.attack?._id === actionId
? actor.system.attack
: item.system.attack?._id === actionId
? item.system.attack
: item?.system?.actions?.get(actionId);
return action;
}
async onRollDamage(event, message) {
event.stopPropagation();
const actor = await this.getActor(message.system.source.actor);
if(!actor.isOwner) return true;
if (message.system.source.item && message.system.source.action) {
const action = this.getAction(actor, message.system.source.item, message.system.source.action);
if (!action || !action?.rollDamage) return;
await action.rollDamage(event, message);
}
}
async onRollSave(event, message) {
event.stopPropagation();
const actor = await this.getActor(message.system.source.actor),
tokenId = event.target.closest('[data-token]')?.dataset.token,
token = game.canvas.tokens.get(tokenId);
if (!token?.actor || !token.isOwner) return true;
if (message.system.source.item && message.system.source.action) {
const action = this.getAction(actor, message.system.source.item, message.system.source.action);
if (!action || !action?.hasSave) return;
action.rollSave(token.actor, event, message).then(result =>
emitAsGM(
GMUpdateEvent.UpdateSaveMessage,
action.updateSaveMessage.bind(action, result, message, token.id),
{
action: action.uuid,
message: message._id,
token: token.id,
result
}
)
);
}
}
async onRollAllSave(event, message) {
event.stopPropagation();
if (!game.user.isGM) return;
const targets = event.target.parentElement.querySelectorAll('[data-token] .target-save');
const actor = await this.getActor(message.system.source.actor),
action = this.getAction(actor, message.system.source.item, message.system.source.action);
targets.forEach(async el => {
const tokenId = el.closest('[data-token]')?.dataset.token,
token = game.canvas.tokens.get(tokenId);
if (!token.actor) return;
if (game.user === token.actor.owner) el.dispatchEvent(new PointerEvent('click', { shiftKey: true }));
else {
token.actor.owner
.query('reactionRoll', {
actionId: action.uuid,
actorId: token.actor.uuid,
event,
message
})
.then(result => action.updateSaveMessage(result, message, token.id));
}
});
}
async onRollSimple(event, message) { async onRollSimple(event, message) {
const buttonType = event.target.dataset.type ?? 'damage', const buttonType = event.target.dataset.type ?? 'damage',
total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0), total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0),
@ -197,8 +124,11 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
item.system.attack?.id === event.currentTarget.id item.system.attack?.id === event.currentTarget.id
? item.system.attack ? item.system.attack
: item.system.actions.get(event.currentTarget.id); : item.system.actions.get(event.currentTarget.id);
if (event.currentTarget.dataset.directDamage) action.use(event, { byPassRoll: true }); if (event.currentTarget.dataset.directDamage) {
else action.use(event); const config = action.prepareConfig(event);
config.hasRoll = false;
action.workflow.get('damage').execute(config, null, true);
} else action.use(event);
} }
async actionUseButton(event, message) { async actionUseButton(event, message) {
@ -249,6 +179,169 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
'system.roll': newRoll, 'system.roll': newRoll,
'rolls': [parsedRoll] 'rolls': [parsedRoll]
}); });
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
} }
} }
async groupRollButton(event, message) {
const path = event.currentTarget.dataset.path;
const { actor: actorData, trait } = foundry.utils.getProperty(message.system, path);
const actor = game.actors.get(actorData._id);
if (!actor.testUserPermission(game.user, 'OWNER')) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership'));
}
const traitLabel = game.i18n.localize(abilities[trait].label);
const config = {
event: event,
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
}),
roll: {
trait: trait,
advantage: 0,
modifiers: [{ label: traitLabel, value: actor.system.traits[trait].value }]
},
hasRoll: true,
skips: {
createMessage: true,
resources: true
}
};
const result = await actor.diceRoll({
...config,
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
})
});
const newMessageData = foundry.utils.deepClone(message.system);
foundry.utils.setProperty(newMessageData, `${path}.result`, result.roll);
const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) };
const updatedContent = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/groupRoll.hbs',
{ ...renderData, user: game.user }
);
const mess = game.messages.get(message._id);
await emitAsGM(
GMUpdateEvent.UpdateDocument,
mess.update.bind(mess),
{
...renderData,
content: updatedContent
},
mess.uuid
);
}
async groupRollReroll(event, message) {
const path = event.currentTarget.dataset.path;
const { actor: actorData, trait } = foundry.utils.getProperty(message.system, path);
const actor = game.actors.get(actorData._id);
if (!actor.testUserPermission(game.user, 'OWNER')) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership'));
}
const traitLabel = game.i18n.localize(abilities[trait].label);
const config = {
event: event,
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
}),
roll: {
trait: trait,
advantage: 0,
modifiers: [{ label: traitLabel, value: actor.system.traits[trait].value }]
},
hasRoll: true,
skips: {
createMessage: true
}
};
const result = await actor.diceRoll({
...config,
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
})
});
const newMessageData = foundry.utils.deepClone(message.system);
foundry.utils.setProperty(newMessageData, `${path}.result`, { ...result.roll, rerolled: true });
const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) };
const updatedContent = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/groupRoll.hbs',
{ ...renderData, user: game.user }
);
const mess = game.messages.get(message._id);
await emitAsGM(
GMUpdateEvent.UpdateDocument,
mess.update.bind(mess),
{
...renderData,
content: updatedContent
},
mess.uuid
);
}
async groupRollSuccessEvent(event, message) {
if (!game.user.isGM) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmOnly'));
}
const { path, success } = event.currentTarget.dataset;
const { actor: actorData } = foundry.utils.getProperty(message.system, path);
const actor = game.actors.get(actorData._id);
if (!actor.testUserPermission(game.user, 'OWNER')) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership'));
}
const newMessageData = foundry.utils.deepClone(message.system);
foundry.utils.setProperty(newMessageData, `${path}.manualSuccess`, Boolean(success));
const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) };
const updatedContent = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/groupRoll.hbs',
{ ...renderData, user: game.user }
);
const mess = game.messages.get(message._id);
await emitAsGM(
GMUpdateEvent.UpdateDocument,
mess.update.bind(mess),
{
...renderData,
content: updatedContent
},
mess.uuid
);
}
async groupRollExpandSection(event) {
event.target
.closest('.group-roll-header-expand-section')
.querySelectorAll('i')
.forEach(element => {
element.classList.toggle('fa-angle-up');
element.classList.toggle('fa-angle-down');
});
event.target.closest('.group-roll-section').querySelector('.group-roll-content').classList.toggle('closed');
}
} }

View file

@ -1,5 +1,3 @@
import { EncounterCountdowns } from '../ui/countdowns.mjs';
export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
actions: { actions: {
@ -36,10 +34,21 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
const adversaries = context.turns?.filter(x => x.isNPC) ?? []; const adversaries = context.turns?.filter(x => x.isNPC) ?? [];
const characters = context.turns?.filter(x => !x.isNPC) ?? []; const characters = context.turns?.filter(x => !x.isNPC) ?? [];
const spotlightRequests = characters
?.filter(x => !x.isNPC)
.filter(x => x.system.spotlight.requestOrderIndex > 0)
.sort((a, b) => {
const valueA = a.system.spotlight.requestOrderIndex;
const valueB = b.system.spotlight.requestOrderIndex;
return valueA - valueB;
});
Object.assign(context, { Object.assign(context, {
actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens, actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens,
adversaries, adversaries,
characters characters: characters?.filter(x => !x.isNPC).filter(x => x.system.spotlight.requestOrderIndex == 0),
spotlightRequests
}); });
} }
@ -114,7 +123,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
async setCombatantSpotlight(combatantId) { async setCombatantSpotlight(combatantId) {
const update = { const update = {
system: { system: {
'spotlight.requesting': false 'spotlight.requesting': false,
'spotlight.requestOrderIndex': 0
} }
}; };
const combatant = this.viewed.combatants.get(combatantId); const combatant = this.viewed.combatants.get(combatantId);
@ -142,11 +152,15 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
} }
static async requestSpotlight(_, target) { static async requestSpotlight(_, target) {
const characters = this.viewed.turns?.filter(x => !x.isNPC) ?? [];
const orderValues = characters.map(character => character.system.spotlight.requestOrderIndex);
const maxRequestIndex = Math.max(...orderValues);
const { combatantId } = target.closest('[data-combatant-id]')?.dataset ?? {}; const { combatantId } = target.closest('[data-combatant-id]')?.dataset ?? {};
const combatant = this.viewed.combatants.get(combatantId); const combatant = this.viewed.combatants.get(combatantId);
await combatant.update({ await combatant.update({
'system.spotlight': { 'system.spotlight': {
requesting: !combatant.system.spotlight.requesting requesting: !combatant.system.spotlight.requesting,
requestOrderIndex: !combatant.system.spotlight.requesting ? maxRequestIndex + 1 : 0
} }
}); });
@ -168,8 +182,4 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
await combatant.update({ 'system.actionTokens': newIndex }); await combatant.update({ 'system.actionTokens': newIndex });
this.render(); this.render();
} }
static openCountdowns() {
new EncounterCountdowns().open();
}
} }

View file

@ -0,0 +1,199 @@
import { DhCountdown } from '../../data/countdowns.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class CountdownEdit extends HandlebarsApplicationMixin(ApplicationV2) {
constructor() {
super();
this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
this.editingCountdowns = new Set();
this.currentEditCountdown = null;
this.hideNewCountdowns = false;
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'dialog', 'dh-style', 'countdown-edit'],
tag: 'form',
position: { width: 600 },
window: {
title: 'DAGGERHEART.APPLICATIONS.CountdownEdit.title',
icon: 'fa-solid fa-clock-rotate-left'
},
actions: {
addCountdown: CountdownEdit.#addCountdown,
toggleCountdownEdit: CountdownEdit.#toggleCountdownEdit,
editCountdownImage: CountdownEdit.#editCountdownImage,
editCountdownOwnership: CountdownEdit.#editCountdownOwnership,
removeCountdown: CountdownEdit.#removeCountdown
},
form: { handler: this.updateData, submitOnChange: true }
};
static PARTS = {
countdowns: {
template: 'systems/daggerheart/templates/ui/countdown-edit.hbs',
scrollable: ['.expanded-view', '.edit-content']
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.isGM = game.user.isGM;
context.ownershipDefaultOptions = CONFIG.DH.GENERAL.basicOwnershiplevels;
context.defaultOwnership = this.data.defaultOwnership;
context.countdownBaseTypes = CONFIG.DH.GENERAL.countdownBaseTypes;
context.countdownTypes = CONFIG.DH.GENERAL.countdownTypes;
context.hideNewCountdowns = this.hideNewCountdowns;
context.countdowns = Object.keys(this.data.countdowns).reduce((acc, key) => {
const countdown = this.data.countdowns[key];
acc[key] = {
...countdown,
typeName: game.i18n.localize(CONFIG.DH.GENERAL.countdownBaseTypes[countdown.type].name),
progress: {
...countdown.progress,
typeName: game.i18n.localize(CONFIG.DH.GENERAL.countdownTypes[countdown.progress.type].label)
},
editing: this.editingCountdowns.has(key)
};
return acc;
}, {});
return context;
}
/** @override */
async _postRender(_context, _options) {
if (this.currentEditCountdown) {
setTimeout(() => {
const input = this.element.querySelector(
`.countdown-edit-container[data-id="${this.currentEditCountdown}"] input`
);
if (input) {
input.select();
this.currentEditCountdown = null;
}
}, 100);
}
}
canPerformEdit() {
if (game.user.isGM) return true;
if (!game.users.activeGM) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired'));
return false;
}
return true;
}
async updateSetting(update) {
const noGM = !game.users.find(x => x.isGM && x.active);
if (noGM) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired'));
return;
}
await this.data.updateSource(update);
await emitAsGM(GMUpdateEvent.UpdateCountdowns, this.gmSetSetting.bind(this.data), this.data, null, {
refreshType: RefreshType.Countdown
});
this.render();
}
static async updateData(_event, _, formData) {
const { hideNewCountdowns, ...settingsData } = foundry.utils.expandObject(formData.object);
// Sync current and max if max is changing and they were equal before
for (const [id, countdown] of Object.entries(settingsData.countdowns ?? {})) {
const existing = this.data.countdowns[id];
const wasEqual = existing && existing.progress.current === existing.progress.max;
if (wasEqual && countdown.progress.max !== existing.progress.max) {
countdown.progress.current = countdown.progress.max;
} else {
countdown.progress.current = Math.min(countdown.progress.current, countdown.progress.max);
}
}
this.hideNewCountdowns = hideNewCountdowns;
this.updateSetting(settingsData);
}
async gmSetSetting(data) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data),
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: { refreshType: RefreshType.Countdown }
});
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
}
static #addCountdown() {
const id = foundry.utils.randomID();
this.editingCountdowns.add(id);
this.currentEditCountdown = id;
this.updateSetting({
[`countdowns.${id}`]: DhCountdown.defaultCountdown(null, this.hideNewCountdowns)
});
}
static #editCountdownImage(_, target) {
const countdown = this.data.countdowns[target.id];
const fp = new foundry.applications.apps.FilePicker.implementation({
current: countdown.img,
type: 'image',
callback: async path => this.updateSetting({ [`countdowns.${target.id}.img`]: path }),
top: this.position.top + 40,
left: this.position.left + 10
});
return fp.browse();
}
static #toggleCountdownEdit(_, button) {
const { countdownId } = button.dataset;
const isEditing = this.editingCountdowns.has(countdownId);
if (isEditing) this.editingCountdowns.delete(countdownId);
else {
this.editingCountdowns.add(countdownId);
this.currentEditCountdown = countdownId;
}
this.render();
}
static async #editCountdownOwnership(_, button) {
const countdown = this.data.countdowns[button.dataset.countdownId];
const data = await game.system.api.applications.dialogs.OwnershipSelection.configure(
countdown.name,
countdown.ownership,
this.data.defaultOwnership
);
if (!data) return;
this.updateSetting({ [`countdowns.${button.dataset.countdownId}`]: data });
}
static async #removeCountdown(event, button) {
const { countdownId } = button.dataset;
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.removeCountdownTitle')
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.CountdownEdit.removeCountdownText', {
name: this.data.countdowns[countdownId].name
})
});
if (!confirmed) return;
}
if (this.editingCountdowns.has(countdownId)) this.editingCountdowns.delete(countdownId);
this.updateSetting({ [`countdowns.-=${countdownId}`]: null });
}
}

View file

@ -1,355 +1,229 @@
import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import constructHTMLButton from '../../helpers/utils.mjs';
import OwnershipSelection from '../dialogs/ownershipSelection.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) { /**
constructor(basePath) { * A UI element which displays the countdowns in this world.
super({}); *
* @extends ApplicationV2
* @mixes HandlebarsApplication
*/
this.basePath = basePath; export default class DhCountdowns extends HandlebarsApplicationMixin(ApplicationV2) {
} constructor(options = {}) {
super(options);
get title() {
return game.i18n.format('DAGGERHEART.APPLICATIONS.Countdown.title', { this.sidebarCollapsed = true;
type: game.i18n.localize(`DAGGERHEART.APPLICATIONS.Countdown.types.${this.basePath}`) this.setupHooks();
});
} }
/** @inheritDoc */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'dh-style', 'countdown'], id: 'countdowns',
tag: 'form', tag: 'div',
position: { width: 740, height: 700 }, classes: ['daggerheart', 'dh-style', 'countdowns', 'faded-ui'],
window: { window: {
icon: 'fa-solid fa-clock-rotate-left',
frame: true, frame: true,
title: 'Countdowns', title: 'DAGGERHEART.UI.Countdowns.title',
resizable: true, positioned: false,
resizable: false,
minimizable: false minimizable: false
}, },
actions: { actions: {
addCountdown: this.addCountdown, toggleViewMode: DhCountdowns.#toggleViewMode,
removeCountdown: this.removeCountdown, editCountdowns: DhCountdowns.#editCountdowns,
editImage: this.onEditImage, decreaseCountdown: (_, target) => this.editCountdown(false, target),
openOwnership: this.openOwnership, increaseCountdown: (_, target) => this.editCountdown(true, target)
openCountdownOwnership: this.openCountdownOwnership,
toggleSimpleView: this.toggleSimpleView
}, },
form: { handler: this.updateData, submitOnChange: true } position: {
}; width: 400,
height: 222,
static PARTS = { top: 50
countdowns: {
template: 'systems/daggerheart/templates/ui/countdowns.hbs',
scrollable: ['.expanded-view']
} }
}; };
_attachPartListeners(partId, htmlElement, options) { /** @override */
super._attachPartListeners(partId, htmlElement, options); static PARTS = {
resources: {
root: true,
template: 'systems/daggerheart/templates/ui/countdowns.hbs'
}
};
htmlElement.querySelectorAll('.mini-countdown-container').forEach(element => { get element() {
element.addEventListener('click', event => this.updateCountdownValue.bind(this)(event, false)); return document.body.querySelector('.daggerheart.dh-style.countdowns');
element.addEventListener('contextmenu', event => this.updateCountdownValue.bind(this)(event, true));
});
}
async _preFirstRender(context, options) {
options.position =
game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].position) ??
Countdowns.DEFAULT_OPTIONS.position;
const viewSetting =
game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].simple) ?? !game.user.isGM;
this.simpleView =
game.user.isGM || !this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER) ? viewSetting : true;
context.simple = this.simpleView;
}
_onPosition(position) {
game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].position, position);
} }
/**@inheritdoc */
async _renderFrame(options) { async _renderFrame(options) {
const frame = await super._renderFrame(options); const frame = await super._renderFrame(options);
if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER)) { const iconOnly =
const button = constructHTMLButton({ game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) ===
label: '', CONFIG.DH.GENERAL.countdownAppMode.iconOnly;
classes: ['header-control', 'icon', 'fa-solid', 'fa-wrench'], if (iconOnly) frame.classList.add('icon-only');
dataset: { action: 'toggleSimpleView', tooltip: 'DAGGERHEART.APPLICATIONS.Countdown.toggleSimple' } else frame.classList.remove('icon-only');
});
this.window.controls.after(button); const header = frame.querySelector('.window-header');
header.querySelector('button[data-action="close"]').remove();
if (game.user.isGM) {
const editTooltip = game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.editTitle');
const editButton = `<a style="margin-right: 8px;" class="header-control" data-tooltip="${editTooltip}" aria-label="${editTooltip}" data-action="editCountdowns"><i class="fa-solid fa-wrench"></i></a>`;
header.insertAdjacentHTML('beforeEnd', editButton);
} }
const minimizeTooltip = game.i18n.localize('DAGGERHEART.UI.Countdowns.toggleIconMode');
const minimizeButton = `<a class="header-control" data-tooltip="${minimizeTooltip}" aria-label="${minimizeTooltip}" data-action="toggleViewMode"><i class="fa-solid fa-down-left-and-up-right-to-center"></i></a>`;
header.insertAdjacentHTML('beforeEnd', minimizeButton);
return frame; return frame;
} }
testUserPermission(level, exact, altSettings) { /** @override */
if (game.user.isGM) return true; async _prepareContext(options) {
const context = await super._prepareContext(options);
const settings =
altSettings ?? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath];
const defaultAllowed = exact ? settings.ownership.default === level : settings.ownership.default >= level;
const userAllowed = exact
? settings.playerOwnership[game.user.id]?.value === level
: settings.playerOwnership[game.user.id]?.value >= level;
return defaultAllowed || userAllowed;
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
const countdownData = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[
this.basePath
];
context.isGM = game.user.isGM; context.isGM = game.user.isGM;
context.base = this.basePath; context.sidebarCollapsed = this.sidebarCollapsed;
context.iconOnly =
game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) ===
CONFIG.DH.GENERAL.countdownAppMode.iconOnly;
context.canCreate = this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true); const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
context.source = { context.countdowns = Object.keys(setting.countdowns).reduce((acc, key) => {
...countdownData, const countdown = setting.countdowns[key];
countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => { const ownership = DhCountdowns.#getPlayerOwnership(game.user, setting, countdown);
const countdown = countdownData.countdowns[key]; if (ownership === CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) return acc;
if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.LIMITED, false, countdown)) { const playersWithAccess = game.users.reduce((acc, user) => {
acc[key] = { const ownership = DhCountdowns.#getPlayerOwnership(user, setting, countdown);
...countdown, if (!user.isGM && ownership && ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) {
canEdit: this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true, countdown) acc.push(user);
};
} }
return acc; return acc;
}, {}) }, []);
}; const nonGmPlayers = game.users.filter(x => !x.isGM);
context.systemFields = countdownData.schema.fields; acc[key] = {
context.countdownFields = context.systemFields.countdowns.element.fields; ...countdown,
context.simple = this.simpleView; editable: game.user.isGM || ownership === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER,
playerAccess: playersWithAccess.length !== nonGmPlayers.length ? playersWithAccess : [],
noPlayerAccess: nonGmPlayers.length && playersWithAccess.length === 0
};
return acc;
}, {});
return context; return context;
} }
static async updateData(event, _, formData) { static #getPlayerOwnership(user, setting, countdown) {
const data = foundry.utils.expandObject(formData.object); const playerOwnership = countdown.ownership[user.id];
const newSetting = foundry.utils.mergeObject( return playerOwnership === undefined || playerOwnership === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns).toObject(), ? setting.defaultOwnership
data : playerOwnership;
); }
if (game.user.isGM) { toggleCollapsedPosition = async (_, collapsed) => {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, newSetting); this.sidebarCollapsed = collapsed;
this.render(); if (!collapsed) this.element.classList.add('expanded');
} else { else this.element.classList.remove('expanded');
await game.socket.emit(`system.${CONFIG.DH.id}`, { };
action: socketEvent.GMUpdate,
data: { cooldownRefresh = ({ refreshType }) => {
action: GMUpdateEvent.UpdateSetting, if (refreshType === RefreshType.Countdown) this.render();
uuid: CONFIG.DH.SETTINGS.gameSettings.Countdowns, };
update: newSetting
} static canPerformEdit() {
}); if (game.user.isGM) return true;
const noGM = !game.users.find(x => x.isGM && x.active);
if (noGM) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired'));
return false;
} }
return true;
} }
async updateSetting(update) { static async #toggleViewMode() {
if (game.user.isGM) { const currentMode = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode);
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, update); const appMode = CONFIG.DH.GENERAL.countdownAppMode;
await game.socket.emit(`system.${CONFIG.DH.id}`, { const newMode = currentMode === appMode.textIcon ? appMode.iconOnly : appMode.textIcon;
action: socketEvent.Refresh, await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode, newMode);
data: {
refreshType: RefreshType.Countdown,
application: `${this.basePath}-countdowns`
}
});
this.render(); if (newMode === appMode.iconOnly) this.element.classList.add('icon-only');
} else { else this.element.classList.remove('icon-only');
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: CONFIG.DH.SETTINGS.gameSettings.Countdowns,
update: update,
refresh: { refreshType: RefreshType.Countdown, application: `${this.basePath}-countdowns` }
}
});
}
}
static onEditImage(_, target) {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath];
const current = setting.countdowns[target.dataset.countdown].img;
const fp = new foundry.applications.apps.FilePicker.implementation({
current,
type: 'image',
callback: async path => this.updateImage.bind(this)(path, target.dataset.countdown),
top: this.position.top + 40,
left: this.position.left + 10
});
return fp.browse();
}
async updateImage(path, countdown) {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
await setting.updateSource({
[`${this.basePath}.countdowns.${countdown}.img`]: path
});
await this.updateSetting(setting);
}
static openOwnership(_, target) {
new Promise((resolve, reject) => {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath];
const ownership = { default: setting.ownership.default, players: setting.playerOwnership };
new OwnershipSelection(resolve, reject, this.title, ownership).render(true);
}).then(async ownership => {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
await setting.updateSource({
[`${this.basePath}.ownership`]: ownership
});
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, setting.toObject());
this.render();
});
}
static openCountdownOwnership(_, target) {
const countdownId = target.dataset.countdown;
new Promise((resolve, reject) => {
const countdown = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath]
.countdowns[countdownId];
const ownership = { default: countdown.ownership.default, players: countdown.playerOwnership };
new OwnershipSelection(resolve, reject, countdown.name, ownership).render(true);
}).then(async ownership => {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
await setting.updateSource({
[`${this.basePath}.countdowns.${countdownId}.ownership`]: ownership
});
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, setting);
this.render();
});
}
static async toggleSimpleView() {
this.simpleView = !this.simpleView;
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].simple, this.simpleView);
this.render(); this.render();
} }
async updateCountdownValue(event, increase) { static async #editCountdowns() {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); new game.system.api.applications.ui.CountdownEdit().render(true);
const countdown = countdownSetting[this.basePath].countdowns[event.currentTarget.dataset.countdown];
if (!this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)) {
return;
}
const currentValue = countdown.progress.current;
if (increase && currentValue === countdown.progress.max) return;
if (!increase && currentValue === 0) return;
await countdownSetting.updateSource({
[`${this.basePath}.countdowns.${event.currentTarget.dataset.countdown}.progress.current`]: increase
? currentValue + 1
: currentValue - 1
});
await this.updateSetting(countdownSetting.toObject());
} }
static async addCountdown() { static async editCountdown(increase, target) {
if (!DhCountdowns.canPerformEdit()) return;
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const countdown = settings.countdowns[target.id];
const newCurrent = increase
? Math.min(countdown.progress.current + 1, countdown.progress.max)
: Math.max(countdown.progress.current - 1, 0);
await settings.updateSource({ [`countdowns.${target.id}.progress.current`]: newCurrent });
await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
refreshType: RefreshType.Countdown
});
}
static async gmSetSetting(data) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data),
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: { refreshType: RefreshType.Countdown }
});
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
}
setupHooks() {
Hooks.on('collapseSidebar', this.toggleCollapsedPosition.bind());
Hooks.on(socketEvent.Refresh, this.cooldownRefresh.bind());
}
close(options) {
Hooks.off('collapseSidebar', this.toggleCollapsedPosition);
Hooks.off(socketEvent.Refresh, this.cooldownRefresh);
super.close(options);
}
static async updateCountdowns(progressType) {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
await countdownSetting.updateSource({ const updatedCountdowns = Object.keys(countdownSetting.countdowns).reduce((acc, key) => {
[`${this.basePath}.countdowns.${foundry.utils.randomID()}`]: { const countdown = countdownSetting.countdowns[key];
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.newCountdown'), if (countdown.progress.type === progressType && countdown.progress.current > 0) {
ownership: game.user.isGM acc.push(key);
? {}
: {
players: {
[game.user.id]: { type: CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER }
}
}
} }
});
await this.updateSetting(countdownSetting.toObject()); return acc;
} }, []);
static async removeCountdown(_, target) { const countdownData = countdownSetting.toObject();
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, {
const countdownName = countdownSetting[this.basePath].countdowns[target.dataset.countdown].name; ...countdownData,
countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => {
const confirmed = await foundry.applications.api.DialogV2.confirm({ const countdown = foundry.utils.deepClone(countdownData.countdowns[key]);
window: { if (updatedCountdowns.includes(key)) {
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.removeCountdownTitle') countdown.progress.current -= 1;
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.Countdown.removeCountdownText', { name: countdownName })
});
if (!confirmed) return;
await countdownSetting.updateSource({ [`${this.basePath}.countdowns.-=${target.dataset.countdown}`]: null });
await this.updateSetting(countdownSetting.toObject());
}
async open() {
await this.render(true);
if (
Object.keys(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath].countdowns
).length > 0
) {
this.minimize();
}
}
}
export class NarrativeCountdowns extends Countdowns {
constructor() {
super('narrative');
}
static DEFAULT_OPTIONS = {
id: 'narrative-countdowns'
};
}
export class EncounterCountdowns extends Countdowns {
constructor() {
super('encounter');
}
static DEFAULT_OPTIONS = {
id: 'encounter-countdowns'
};
}
export async function updateCountdowns(progressType) {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const update = Object.keys(countdownSetting).reduce((update, typeKey) => {
return foundry.utils.mergeObject(
update,
Object.keys(countdownSetting[typeKey].countdowns).reduce((acc, countdownKey) => {
const countdown = countdownSetting[typeKey].countdowns[countdownKey];
if (countdown.progress.current > 0 && countdown.progress.type.value === progressType) {
acc[`${typeKey}.countdowns.${countdownKey}.progress.current`] = countdown.progress.current - 1;
} }
acc[key] = countdown;
return acc; return acc;
}, {}) }, {})
); });
}, {});
await countdownSetting.updateSource(update); const data = { refreshType: RefreshType.Countdown };
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSetting); await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
const data = { refreshType: RefreshType.Countdown }; data
await game.socket.emit(`system.${CONFIG.DH.id}`, { });
action: socketEvent.Refresh, Hooks.callAll(socketEvent.Refresh, data);
data }
});
Hooks.callAll(socketEvent.Refresh, data);
} }

View file

@ -1,4 +1,4 @@
import { emitAsGM, GMUpdateEvent, socketEvent } from "../../systemRegistration/socket.mjs"; import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -78,7 +78,7 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
/** @override */ /** @override */
async _preRender(context, options) { async _preRender(context, options) {
if (this.currentFear > this.maxFear) if (this.currentFear > this.maxFear && game.user.isGM)
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, this.maxFear); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, this.maxFear);
} }
@ -106,19 +106,10 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
} }
async updateFear(value) { async updateFear(value) {
return emitAsGM(GMUpdateEvent.UpdateFear, game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), value); return emitAsGM(
/* if(!game.user.isGM) GMUpdateEvent.UpdateFear,
await game.socket.emit(`system.${CONFIG.DH.id}`, { game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
action: socketEvent.GMUpdate, value
data: { );
action: GMUpdateEvent.UpdateFear,
update: value
}
});
else
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, value); */
/* if (!game.user.isGM) return;
value = Math.max(0, Math.min(this.maxFear, value));
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, value); */
} }
} }

View file

@ -15,16 +15,14 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
this.fieldFilter = []; this.fieldFilter = [];
this.selectedMenu = { path: [], data: null }; this.selectedMenu = { path: [], data: null };
this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig; this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig;
this.presets = options.presets; this.presets = {};
this.compendiumBrowserTypeKey = 'compendiumBrowserDefault';
if (this.presets?.compendium && this.presets?.folder)
ItemBrowser.selectFolder.call(this, null, null, this.presets.compendium, this.presets.folder);
} }
/** @inheritDoc */ /** @inheritDoc */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
id: 'itemBrowser', id: 'itemBrowser',
classes: ['daggerheart', 'dh-style', 'dialog', 'compendium-browser'], classes: ['daggerheart', 'dh-style', 'dialog', 'compendium-browser', 'loader'],
tag: 'div', tag: 'div',
window: { window: {
frame: true, frame: true,
@ -84,17 +82,29 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
} }
}; };
/** @inheritDoc */
async _preFirstRender(context, options) {
if (context.presets?.render?.noFolder || context.presets?.render?.lite) options.position.width = 600;
await super._preFirstRender(context, options);
}
/** @inheritDoc */ /** @inheritDoc */
async _preRender(context, options) { async _preRender(context, options) {
if (context.presets?.render?.noFolder || context.presets?.render?.lite) this.presets = options.presets ?? {};
options.parts.splice(options.parts.indexOf('sidebar'), 1); const noFolder = this.presets?.render?.noFolder;
if (noFolder === true) {
this.compendiumBrowserTypeKey = 'compendiumBrowserNoFolder';
}
const lite = this.presets?.render?.lite;
if (lite === true) {
this.compendiumBrowserTypeKey = 'compendiumBrowserLite';
}
const userPresetPosition = game.user.getFlag(
CONFIG.DH.id,
CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position
);
options.position = userPresetPosition ?? ItemBrowser.DEFAULT_OPTIONS.position;
if (!userPresetPosition) {
const width = noFolder === true || lite === true ? 600 : 850;
if (this.rendered) this.setPosition({ width });
else options.position.width = width;
}
await super._preRender(context, options); await super._preRender(context, options);
} }
@ -103,32 +113,35 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
async _onRender(context, options) { async _onRender(context, options) {
await super._onRender(context, options); await super._onRender(context, options);
this._createSearchFilter(); this.element
this._createFilterInputs(); .querySelectorAll('[data-action="selectFolder"]')
this._createDragProcess(); .forEach(element =>
element.classList.toggle('is-selected', element.dataset.folderId === this.selectedMenu.path.join('.'))
if (context.presets?.render?.lite) this.element.classList.add('lite');
if (context.presets?.render?.noFolder) this.element.classList.add('no-folder');
if (context.presets?.render?.noFilter) this.element.classList.add('no-filter');
if (this.presets?.filter) {
Object.entries(this.presets.filter).forEach(
([k, v]) => (this.fieldFilter.find(c => c.name === k).value = v.value)
); );
await this._onInputFilterBrowser();
} this._createSearchFilter();
this.element.classList.toggle('lite', this.presets?.render?.lite === true);
this.element.classList.toggle('no-folder', this.presets?.render?.noFolder === true);
this.element.classList.toggle('no-filter', this.presets?.render?.noFilter === true);
this.element.querySelectorAll('.folder-list > [data-action="selectFolder"]').forEach(element => {
element.hidden =
this.presets.render?.folders?.length && !this.presets.render.folders.includes(element.dataset.folderId);
});
}
_onPosition(position) {
game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position, position);
} }
_attachPartListeners(partId, htmlElement, options) { _attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options); super._attachPartListeners(partId, htmlElement, options);
htmlElement htmlElement.querySelectorAll('[data-action="selectFolder"]').forEach(element =>
.querySelectorAll('[data-action="selectFolder"]') element.addEventListener('contextmenu', event => {
.forEach(element => element.addEventListener("contextmenu", (event) => {
event.target.classList.toggle('expanded'); event.target.classList.toggle('expanded');
})) })
);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -139,22 +152,26 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
async _prepareContext(options) { async _prepareContext(options) {
const context = await super._prepareContext(options); const context = await super._prepareContext(options);
context.compendiums = this.getCompendiumFolders(foundry.utils.deepClone(this.config)); context.compendiums = this.getCompendiumFolders(foundry.utils.deepClone(this.config));
// context.pathTitle = this.pathTile;
context.menu = this.selectedMenu; context.menu = this.selectedMenu;
context.formatLabel = this.formatLabel; context.formatLabel = this.formatLabel;
context.formatChoices = this.formatChoices; context.formatChoices = this.formatChoices;
context.fieldFilter = this.fieldFilter = this._createFieldFilter();
context.items = this.items; context.items = this.items;
context.presets = this.presets; context.presets = this.presets;
return context; return context;
} }
open(presets = {}) {
this.presets = presets;
ItemBrowser.selectFolder.call(this);
}
getCompendiumFolders(config, parent = null, depth = 0) { getCompendiumFolders(config, parent = null, depth = 0) {
let folders = []; let folders = [];
Object.values(config).forEach(c => { Object.values(config).forEach(c => {
// if(this.presets.render?.folders?.length && !this.presets.render.folders.includes(c.id)) return;
const folder = { const folder = {
id: c.id, id: c.id,
label: c.label, label: game.i18n.localize(c.label),
selected: (!parent || parent.selected) && this.selectedMenu.path[depth] === c.id selected: (!parent || parent.selected) && this.selectedMenu.path[depth] === c.id
}; };
folder.folders = c.folders folder.folders = c.folders
@ -162,47 +179,108 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
: []; : [];
folders.push(folder); folders.push(folder);
}); });
folders.sort((a, b) => a.label.localeCompare(b.label));
return folders; return folders;
} }
static async selectFolder(_, target, compend, folder) { static async selectFolder(_, target) {
const config = foundry.utils.deepClone(this.config), const folderId = target?.dataset?.folderId ?? this.presets.folder,
compendium = compend ?? target.closest('[data-compendium-id]').dataset.compendiumId, folderData = foundry.utils.getProperty(this.config, folderId) ?? {};
folderId = folder ?? target.dataset.folderId,
folderPath = `${compendium}.folders.${folderId}`, const columns = ItemBrowser.getFolderConfig(folderData).map(col => ({
folderData = foundry.utils.getProperty(config, folderPath); ...col,
label: game.i18n.localize(col.label)
}));
this.selectedMenu = { this.selectedMenu = {
path: folderPath.split('.'), path: folderId?.split('.') ?? [],
data: { data: {
...folderData, ...folderData,
columns: ItemBrowser.getFolderConfig(folderData) columns: columns
} }
}; };
let items = []; await this.render({ force: true, presets: this.presets });
for (const key of folderData.keys) {
const comp = game.packs.get(`${compendium}.${key}`);
if (!comp) return;
items = items.concat(await comp.getDocuments({ type__in: folderData.type }));
}
this.items = ItemBrowser.sortBy(items, 'name'); if (this.selectedMenu?.data?.type?.length) this.loadItems();
if(target) {
target.closest('.compendium-sidebar').querySelectorAll('[data-action="selectFolder"]').forEach(element => element.classList.remove("is-selected"))
target.classList.add('is-selected');
}
this.render({ force: true });
} }
_replaceHTML(result, content, options) { _replaceHTML(result, content, options) {
if(!options.isFirstRender) delete result.sidebar; if (!options.isFirstRender) delete result.sidebar;
super._replaceHTML(result, content, options); super._replaceHTML(result, content, options);
} }
loadItems() {
let loadTimeout = this.toggleLoader(true);
const promises = [];
game.packs.forEach(pack => {
promises.push(
new Promise(async resolve => {
const items = await pack.getDocuments({ type__in: this.selectedMenu?.data?.type });
resolve(items);
})
);
});
Promise.all(promises).then(async result => {
this.items = ItemBrowser.sortBy(
result.flatMap(r => r),
'name'
);
this.fieldFilter = this._createFieldFilter();
if (this.presets?.filter) {
Object.entries(this.presets.filter).forEach(([k, v]) => {
const filter = this.fieldFilter.find(c => c.name === k);
if (filter) filter.value = v.value;
});
// await this._onInputFilterBrowser();
}
const filterList = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/itemBrowser/filterContainer.hbs',
{
fieldFilter: this.fieldFilter,
presets: this.presets,
formatChoices: this.formatChoices
}
);
this.element.querySelector('.filter-content .wrapper').innerHTML = filterList;
const filterContainer = this.element.querySelector('.filter-header > [data-action="expandContent"]');
if (this.fieldFilter.length === 0) filterContainer.setAttribute('disabled', '');
else filterContainer.removeAttribute('disabled');
const itemList = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/itemBrowser/itemContainer.hbs',
{
items: this.items,
menu: this.selectedMenu,
formatLabel: this.formatLabel
}
);
this.element.querySelector('.item-list').innerHTML = itemList;
this._createFilterInputs();
await this._onInputFilterBrowser();
this._createDragProcess();
clearTimeout(loadTimeout);
this.toggleLoader(false);
});
}
toggleLoader(state) {
const container = this.element.querySelector('.item-list');
return setTimeout(() => {
container.classList.toggle('loader', state);
}, 100);
}
static expandContent(_, target) { static expandContent(_, target) {
const parent = target.parentElement; const parent = target.parentElement;
parent.classList.toggle('expanded'); parent.classList.toggle('expanded');
@ -237,6 +315,12 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
else if (typeof f.choices === 'function') { else if (typeof f.choices === 'function') {
f.choices = f.choices(this.items); f.choices = f.choices(this.items);
} }
// Clear field label so template uses our custom label parameter
if (f.field && f.label) {
f.field.label = undefined;
}
f.name ??= f.key; f.name ??= f.key;
f.value = this.presets?.filter?.[f.name]?.value ?? null; f.value = this.presets?.filter?.[f.name]?.value ?? null;
}); });
@ -314,6 +398,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
for (const li of html.querySelectorAll('.item-container')) { for (const li of html.querySelectorAll('.item-container')) {
const itemUUID = li.dataset.itemUuid, const itemUUID = li.dataset.itemUuid,
item = this.items.find(i => i.uuid === itemUUID); item = this.items.find(i => i.uuid === itemUUID);
if (!item) continue;
const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name);
if (matchesSearch) this.#filteredItems.browser.search.add(item.id); if (matchesSearch) this.#filteredItems.browser.search.add(item.id);
const { input } = this.#filteredItems.browser; const { input } = this.#filteredItems.browser;
@ -336,7 +421,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
item = this.items.find(i => i.uuid === itemUUID); item = this.items.find(i => i.uuid === itemUUID);
if (!item) continue; if (!item) continue;
const matchesMenu = const matchesMenu =
this.fieldFilter.length === 0 || this.fieldFilter.length === 0 ||
this.fieldFilter.every( this.fieldFilter.every(
@ -405,11 +490,13 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
const newOrder = [...itemList].reverse().sort((a, b) => { const newOrder = [...itemList].reverse().sort((a, b) => {
const aProp = a.querySelector(`[data-item-key="${key}"]`), const aProp = a.querySelector(`[data-item-key="${key}"]`),
bProp = b.querySelector(`[data-item-key="${key}"]`); bProp = b.querySelector(`[data-item-key="${key}"]`),
aValue = isNaN(aProp.innerText) ? aProp.innerText : Number(aProp.innerText),
bValue = isNaN(bProp.innerText) ? bProp.innerText : Number(bProp.innerText);
if (type === 'DESC') { if (type === 'DESC') {
return aProp.innerText < bProp.innerText ? 1 : -1; return aValue < bValue ? 1 : -1;
} else { } else {
return aProp.innerText > bProp.innerText ? 1 : -1; return aValue > bValue ? 1 : -1;
} }
}); });
@ -438,4 +525,41 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
_canDragStart() { _canDragStart() {
return true; return true;
} }
static injectSidebarButton(html) {
if (!game.user.isGM) return;
const sectionId = html.dataset.tab,
menus = {
actors: {
folder: 'adversaries',
render: {
folders: ['adversaries', 'characters', 'environments']
}
},
items: {
folder: 'equipments',
render: {
noFolder: true
}
},
compendium: {}
};
if (Object.keys(menus).includes(sectionId)) {
const headerActions = html.querySelector('.header-actions');
const button = document.createElement('button');
button.type = 'button';
button.classList.add('open-compendium-browser');
button.innerHTML = `
<i class="fa-solid fa-book-atlas"></i>
${game.i18n.localize('DAGGERHEART.UI.Tooltip.compendiumBrowser')}
`;
button.addEventListener('click', event => {
ui.compendiumBrowser.open(menus[sectionId]);
});
headerActions.append(button);
}
}
} }

View file

@ -10,29 +10,48 @@ export default class DhMeasuredTemplate extends foundry.canvas.placeables.Measur
const splitRulerText = this.ruler.text.split(' '); const splitRulerText = this.ruler.text.split(' ');
if (splitRulerText.length > 0) { if (splitRulerText.length > 0) {
const rulerValue = Number(splitRulerText[0]); const rulerValue = Number(splitRulerText[0]);
const vagueLabel = this.constructor.getDistanceLabel(rulerValue, rangeMeasurementSettings); const result = DhMeasuredTemplate.getRangeLabels(rulerValue, rangeMeasurementSettings);
this.ruler.text = vagueLabel; this.ruler.text = result.distance + (result.units ? ' ' + result.units : '');
} }
} }
} }
static getDistanceLabel(distance, settings) { static getRangeLabels(distanceValue, settings) {
if (distance <= settings.melee) { let result = { distance: distanceValue, units: '' };
return game.i18n.localize('DAGGERHEART.CONFIG.Range.melee.name'); const sceneRangeMeasurement = canvas.scene.flags.daggerheart?.rangeMeasurement;
}
if (distance <= settings.veryClose) { const { disable, custom } = CONFIG.DH.GENERAL.sceneRangeMeasurementSetting;
return game.i18n.localize('DAGGERHEART.CONFIG.Range.veryClose.name'); if (sceneRangeMeasurement.setting === disable.id) {
} result.distance = distanceValue;
if (distance <= settings.close) { result.units = canvas.scene?.grid?.units;
return game.i18n.localize('DAGGERHEART.CONFIG.Range.close.name'); return result;
}
if (distance <= settings.far) {
return game.i18n.localize('DAGGERHEART.CONFIG.Range.far.name');
}
if (distance > settings.far) {
return game.i18n.localize('DAGGERHEART.CONFIG.Range.veryFar.name');
} }
return ''; const melee = sceneRangeMeasurement.setting === custom.id ? sceneRangeMeasurement.melee : settings.melee;
const veryClose =
sceneRangeMeasurement.setting === custom.id ? sceneRangeMeasurement.veryClose : settings.veryClose;
const close = sceneRangeMeasurement.setting === custom.id ? sceneRangeMeasurement.close : settings.close;
const far = sceneRangeMeasurement.setting === custom.id ? sceneRangeMeasurement.far : settings.far;
if (distanceValue <= melee) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.melee.name');
return result;
}
if (distanceValue <= veryClose) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.veryClose.name');
return result;
}
if (distanceValue <= close) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.close.name');
return result;
}
if (distanceValue <= far) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.far.name');
return result;
}
if (distanceValue > far) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.veryFar.name');
}
return result;
} }
} }

View file

@ -8,9 +8,9 @@ export default class DhpRuler extends foundry.canvas.interaction.Ruler {
const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement; const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement;
if (range.enabled) { if (range.enabled) {
const distance = DhMeasuredTemplate.getDistanceLabel(waypoint.measurement.distance.toNearest(0.01), range); const result = DhMeasuredTemplate.getRangeLabels(waypoint.measurement.distance.toNearest(0.01), range);
context.cost = { total: distance, units: null }; context.cost = { total: result.distance, units: result.units };
context.distance = { total: distance, units: null }; context.distance = { total: result.distance, units: result.units };
} }
return context; return context;

View file

@ -8,9 +8,9 @@ export default class DhpTokenRuler extends foundry.canvas.placeables.tokens.Toke
const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement; const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement;
if (range.enabled) { if (range.enabled) {
const distance = DhMeasuredTemplate.getDistanceLabel(waypoint.measurement.distance.toNearest(0.01), range); const result = DhMeasuredTemplate.getRangeLabels(waypoint.measurement.distance.toNearest(0.01), range);
context.cost = { total: distance, units: null }; context.cost = { total: result.distance, units: result.units };
context.distance = { total: distance, units: null }; context.distance = { total: result.distance, units: result.units };
} }
return context; return context;

View file

@ -157,6 +157,11 @@ export const adversaryTypes = {
} }
}; };
export const allAdversaryTypes = () => ({
...adversaryTypes,
...game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).adversaryTypes
});
export const environmentTypes = { export const environmentTypes = {
exploration: { exploration: {
label: 'DAGGERHEART.CONFIG.EnvironmentType.exploration.label', label: 'DAGGERHEART.CONFIG.EnvironmentType.exploration.label',

View file

@ -1,4 +1,4 @@
export const displayDomainCardsAsList = 'displayDomainCardsAsList'; export const displayDomainCardsAsCard = 'displayDomainCardsAsCard';
export const narrativeCountdown = { export const narrativeCountdown = {
simple: 'countdown-narrative-simple', simple: 'countdown-narrative-simple',
position: 'countdown-narrative-position' position: 'countdown-narrative-position'
@ -8,8 +8,21 @@ export const encounterCountdown = {
position: 'countdown-encounter-position' position: 'countdown-encounter-position'
}; };
export const compendiumBrowserDefault = {
position: 'compendium-browser-default-position'
};
export const compendiumBrowserNoFolder = {
position: 'compendium-browser-no-folder-position'
};
export const compendiumBrowserLite = {
position: 'compendium-browser-lite-position'
};
export const itemAttachmentSource = 'attachmentSource'; export const itemAttachmentSource = 'attachmentSource';
export const userFlags = { export const userFlags = {
welcomeMessage: 'welcome-message' welcomeMessage: 'welcome-message',
countdownMode: 'countdown-mode'
}; };

View file

@ -90,22 +90,22 @@ export const rangeInclusion = {
export const otherTargetTypes = { export const otherTargetTypes = {
friendly: { friendly: {
id: 'friendly', id: 'friendly',
label: 'Friendly' label: 'DAGGERHEART.CONFIG.TargetTypes.friendly'
}, },
hostile: { hostile: {
id: 'hostile', id: 'hostile',
label: 'Hostile' label: 'DAGGERHEART.CONFIG.TargetTypes.hostile'
}, },
any: { any: {
id: 'any', id: 'any',
label: 'Any' label: 'DAGGERHEART.CONFIG.TargetTypes.any'
} }
}; };
export const targetTypes = { export const targetTypes = {
self: { self: {
id: 'self', id: 'self',
label: 'Self' label: 'DAGGERHEART.CONFIG.TargetTypes.self'
}, },
...otherTargetTypes ...otherTargetTypes
}; };
@ -164,28 +164,36 @@ export const healingTypes = {
} }
}; };
export const defeatedConditions = { export const defeatedConditions = () => {
const defeated = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated;
return Object.keys(defeatedConditionChoices).reduce((acc, key) => {
const choice = defeatedConditionChoices[key];
acc[key] = {
...choice,
img: defeated[`${choice.id}Icon`],
description: `DAGGERHEART.CONFIG.Condition.${choice.id}.description`
};
return acc;
}, {});
};
export const defeatedConditionChoices = {
defeated: { defeated: {
id: 'defeated', id: 'defeated',
name: 'DAGGERHEART.CONFIG.Condition.defeated.name', name: 'DAGGERHEART.CONFIG.Condition.defeated.name'
img: 'icons/magic/control/fear-fright-mask-orange.webp',
description: 'DAGGERHEART.CONFIG.Condition.defeated.description'
}, },
unconscious: { unconscious: {
id: 'unconscious', id: 'unconscious',
name: 'DAGGERHEART.CONFIG.Condition.unconscious.name', name: 'DAGGERHEART.CONFIG.Condition.unconscious.name'
img: 'icons/magic/control/sleep-bubble-purple.webp',
description: 'DAGGERHEART.CONFIG.Condition.unconscious.description'
}, },
dead: { dead: {
id: 'dead', id: 'dead',
name: 'DAGGERHEART.CONFIG.Condition.dead.name', name: 'DAGGERHEART.CONFIG.Condition.dead.name'
img: 'icons/magic/death/grave-tombstone-glow-teal.webp',
description: 'DAGGERHEART.CONFIG.Condition.dead.description'
} }
}; };
export const conditions = { export const conditions = () => ({
vulnerable: { vulnerable: {
id: 'vulnerable', id: 'vulnerable',
name: 'DAGGERHEART.CONFIG.Condition.vulnerable.name', name: 'DAGGERHEART.CONFIG.Condition.vulnerable.name',
@ -204,8 +212,8 @@ export const conditions = {
img: 'icons/magic/control/debuff-chains-shackle-movement-red.webp', img: 'icons/magic/control/debuff-chains-shackle-movement-red.webp',
description: 'DAGGERHEART.CONFIG.Condition.restrained.description' description: 'DAGGERHEART.CONFIG.Condition.restrained.description'
}, },
...defeatedConditions ...defeatedConditions()
}; });
export const defaultRestOptions = { export const defaultRestOptions = {
shortRest: () => ({ shortRest: () => ({
@ -561,6 +569,19 @@ export const refreshTypes = {
} }
}; };
export const itemAbilityCosts = {
resource: {
id: 'resource',
label: 'DAGGERHEART.GENERAL.resource',
group: 'Global'
},
quantity: {
id: 'quantity',
label: 'DAGGERHEART.GENERAL.quantity',
group: 'Global'
}
};
export const abilityCosts = { export const abilityCosts = {
hitPoints: { hitPoints: {
id: 'hitPoints', id: 'hitPoints',
@ -574,19 +595,20 @@ export const abilityCosts = {
}, },
hope: { hope: {
id: 'hope', id: 'hope',
label: 'Hope', label: 'DAGGERHEART.CONFIG.HealingType.hope.name',
group: 'TYPES.Actor.character' group: 'TYPES.Actor.character'
}, },
armor: { armor: {
id: 'armor', id: 'armor',
label: 'Armor Slot', label: 'DAGGERHEART.CONFIG.HealingType.armor.name',
group: 'TYPES.Actor.character' group: 'TYPES.Actor.character'
}, },
fear: { fear: {
id: 'fear', id: 'fear',
label: 'Fear', label: 'DAGGERHEART.CONFIG.HealingType.fear.name',
group: 'TYPES.Actor.adversary' group: 'TYPES.Actor.adversary'
} },
resource: itemAbilityCosts.resource
}; };
export const countdownTypes = { export const countdownTypes = {
@ -624,8 +646,57 @@ export const rollTypes = {
} }
}; };
export const attributionSources = {
daggerheart: {
label: 'Daggerheart',
values: [{ label: 'Daggerheart SRD' }]
}
};
export const fearDisplay = { export const fearDisplay = {
token: { value: 'token', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.token' }, token: { value: 'token', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.token' },
bar: { value: 'bar', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.bar' }, bar: { value: 'bar', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.bar' },
hide: { value: 'hide', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.hide' } hide: { value: 'hide', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.hide' }
}; };
export const basicOwnershiplevels = {
0: { value: 0, label: 'OWNERSHIP.NONE' },
2: { value: 2, label: 'OWNERSHIP.OBSERVER' },
3: { value: 3, label: 'OWNERSHIP.OWNER' }
};
export const simpleOwnershiplevels = {
[-1]: { value: -1, label: 'OWNERSHIP.INHERIT' },
...basicOwnershiplevels
};
export const countdownBaseTypes = {
narrative: {
id: 'narrative',
name: 'DAGGERHEART.APPLICATIONS.Countdown.types.narrative'
},
encounter: {
id: 'encounter',
name: 'DAGGERHEART.APPLICATIONS.Countdown.types.encounter'
}
};
export const countdownAppMode = {
textIcon: 'text-icon',
iconOnly: 'icon-only'
};
export const sceneRangeMeasurementSetting = {
disable: {
id: 'disable',
label: 'Disable Daggerheart Range Measurement'
},
default: {
id: 'default',
label: 'Default'
},
custom: {
id: 'custom',
label: 'Custom'
}
};

View file

@ -3,63 +3,63 @@ export const typeConfig = {
columns: [ columns: [
{ {
key: 'system.tier', key: 'system.tier',
label: 'Tier' label: 'DAGGERHEART.GENERAL.Tiers.singular'
}, },
{ {
key: 'system.type', key: 'system.type',
label: 'Type' label: 'DAGGERHEART.GENERAL.type'
} }
], ],
filters: [ filters: [
{ {
key: 'system.tier', key: 'system.tier',
label: 'Tier', label: 'DAGGERHEART.GENERAL.Tiers.singular',
field: 'system.api.models.actors.DhAdversary.schema.fields.tier' field: 'system.api.models.actors.DhAdversary.schema.fields.tier'
}, },
{ {
key: 'system.type', key: 'system.type',
label: 'Type', label: 'DAGGERHEART.GENERAL.type',
field: 'system.api.models.actors.DhAdversary.schema.fields.type' field: 'system.api.models.actors.DhAdversary.schema.fields.type'
}, },
{ {
key: 'system.difficulty', key: 'system.difficulty',
name: 'difficulty.min', name: 'difficulty.min',
label: 'Difficulty (Min)', label: 'DAGGERHEART.UI.ItemBrowser.difficultyMin',
field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty', field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty',
operator: 'gte' operator: 'gte'
}, },
{ {
key: 'system.difficulty', key: 'system.difficulty',
name: 'difficulty.max', name: 'difficulty.max',
label: 'Difficulty (Max)', label: 'DAGGERHEART.UI.ItemBrowser.difficultyMax',
field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty', field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty',
operator: 'lte' operator: 'lte'
}, },
{ {
key: 'system.resources.hitPoints.max', key: 'system.resources.hitPoints.max',
name: 'hp.min', name: 'hp.min',
label: 'Hit Points (Min)', label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMin',
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max', field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max',
operator: 'gte' operator: 'gte'
}, },
{ {
key: 'system.resources.hitPoints.max', key: 'system.resources.hitPoints.max',
name: 'hp.max', name: 'hp.max',
label: 'Hit Points (Max)', label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMax',
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max', field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max',
operator: 'lte' operator: 'lte'
}, },
{ {
key: 'system.resources.stress.max', key: 'system.resources.stress.max',
name: 'stress.min', name: 'stress.min',
label: 'Stress (Min)', label: 'DAGGERHEART.UI.ItemBrowser.stressMin',
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max', field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max',
operator: 'gte' operator: 'gte'
}, },
{ {
key: 'system.resources.stress.max', key: 'system.resources.stress.max',
name: 'stress.max', name: 'stress.max',
label: 'Stress (Max)', label: 'DAGGERHEART.UI.ItemBrowser.stressMax',
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max', field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max',
operator: 'lte' operator: 'lte'
} }
@ -69,22 +69,22 @@ export const typeConfig = {
columns: [ columns: [
{ {
key: 'type', key: 'type',
label: 'Type' label: 'DAGGERHEART.GENERAL.type'
}, },
{ {
key: 'system.secondary', key: 'system.secondary',
label: 'Subtype', label: 'DAGGERHEART.UI.ItemBrowser.subtype',
format: isSecondary => (isSecondary ? 'secondary' : isSecondary === false ? 'primary' : '-') format: isSecondary => (isSecondary ? 'secondary' : isSecondary === false ? 'primary' : '-')
}, },
{ {
key: 'system.tier', key: 'system.tier',
label: 'Tier' label: 'DAGGERHEART.GENERAL.Tiers.singular'
} }
], ],
filters: [ filters: [
{ {
key: 'type', key: 'type',
label: 'Type', label: 'DAGGERHEART.GENERAL.type',
choices: () => choices: () =>
CONFIG.Item.documentClass.TYPES.filter(t => CONFIG.Item.documentClass.TYPES.filter(t =>
['armor', 'weapon', 'consumable', 'loot'].includes(t) ['armor', 'weapon', 'consumable', 'loot'].includes(t)
@ -92,15 +92,15 @@ export const typeConfig = {
}, },
{ {
key: 'system.secondary', key: 'system.secondary',
label: 'Subtype', label: 'DAGGERHEART.UI.ItemBrowser.subtype',
choices: [ choices: [
{ value: false, label: 'Primary Weapon' }, { value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon' },
{ value: true, label: 'Secondary Weapon' } { value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon' }
] ]
}, },
{ {
key: 'system.tier', key: 'system.tier',
label: 'Tier', label: 'DAGGERHEART.GENERAL.Tiers.singular',
choices: [ choices: [
{ value: '1', label: '1' }, { value: '1', label: '1' },
{ value: '2', label: '2' }, { value: '2', label: '2' },
@ -110,36 +110,36 @@ export const typeConfig = {
}, },
{ {
key: 'system.burden', key: 'system.burden',
label: 'Burden', label: 'DAGGERHEART.GENERAL.burden',
field: 'system.api.models.items.DHWeapon.schema.fields.burden' field: 'system.api.models.items.DHWeapon.schema.fields.burden'
}, },
{ {
key: 'system.attack.roll.trait', key: 'system.attack.roll.trait',
label: 'Trait', label: 'DAGGERHEART.GENERAL.Trait.single',
field: 'system.api.models.actions.actionsTypes.attack.schema.fields.roll.fields.trait' field: 'system.api.models.actions.actionsTypes.attack.schema.fields.roll.fields.trait'
}, },
{ {
key: 'system.attack.range', key: 'system.attack.range',
label: 'Range', label: 'DAGGERHEART.GENERAL.range',
field: 'system.api.models.actions.actionsTypes.attack.schema.fields.range' field: 'system.api.models.actions.actionsTypes.attack.schema.fields.range'
}, },
{ {
key: 'system.baseScore', key: 'system.baseScore',
name: 'armor.min', name: 'armor.min',
label: 'Armor Score (Min)', label: 'DAGGERHEART.UI.ItemBrowser.armorScoreMin',
field: 'system.api.models.items.DHArmor.schema.fields.baseScore', field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
operator: 'gte' operator: 'gte'
}, },
{ {
key: 'system.baseScore', key: 'system.baseScore',
name: 'armor.max', name: 'armor.max',
label: 'Armor Score (Max)', label: 'DAGGERHEART.UI.ItemBrowser.armorScoreMax',
field: 'system.api.models.items.DHArmor.schema.fields.baseScore', field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
operator: 'lte' operator: 'lte'
}, },
{ {
key: 'system.itemFeatures', key: 'system.itemFeatures',
label: 'Features', label: 'DAGGERHEART.GENERAL.features',
choices: () => choices: () =>
[ [
...Object.entries(CONFIG.DH.ITEM.weaponFeatures), ...Object.entries(CONFIG.DH.ITEM.weaponFeatures),
@ -149,6 +149,102 @@ export const typeConfig = {
} }
] ]
}, },
weapons: {
columns: [
{
key: 'system.secondary',
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
format: isSecondary => (isSecondary ? 'secondary' : isSecondary === false ? 'primary' : '-')
},
{
key: 'system.tier',
label: 'DAGGERHEART.GENERAL.Tiers.singular'
}
],
filters: [
{
key: 'system.secondary',
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
choices: [
{ value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon' },
{ value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon' }
]
},
{
key: 'system.tier',
label: 'DAGGERHEART.GENERAL.Tiers.singular',
choices: [
{ value: '1', label: '1' },
{ value: '2', label: '2' },
{ value: '3', label: '3' },
{ value: '4', label: '4' }
]
},
{
key: 'system.burden',
label: 'DAGGERHEART.GENERAL.burden',
field: 'system.api.models.items.DHWeapon.schema.fields.burden'
},
{
key: 'system.attack.roll.trait',
label: 'DAGGERHEART.GENERAL.Trait.single',
field: 'system.api.models.actions.actionsTypes.attack.schema.fields.roll.fields.trait'
},
{
key: 'system.attack.range',
label: 'DAGGERHEART.GENERAL.range',
field: 'system.api.models.actions.actionsTypes.attack.schema.fields.range'
},
{
key: 'system.itemFeatures',
label: 'DAGGERHEART.GENERAL.features',
choices: () =>
Object.entries(CONFIG.DH.ITEM.weaponFeatures).map(([k, v]) => ({ value: k, label: v.label })),
operator: 'contains3'
}
]
},
armors: {
columns: [
{
key: 'system.tier',
label: 'DAGGERHEART.GENERAL.Tiers.singular'
}
],
filters: [
{
key: 'system.tier',
label: 'DAGGERHEART.GENERAL.Tiers.singular',
choices: [
{ value: '1', label: '1' },
{ value: '2', label: '2' },
{ value: '3', label: '3' },
{ value: '4', label: '4' }
]
},
{
key: 'system.baseScore',
name: 'armor.min',
label: 'DAGGERHEART.UI.ItemBrowser.armorScoreMin',
field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
operator: 'gte'
},
{
key: 'system.baseScore',
name: 'armor.max',
label: 'DAGGERHEART.UI.ItemBrowser.armorScoreMax',
field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
operator: 'lte'
},
{
key: 'system.itemFeatures',
label: 'DAGGERHEART.GENERAL.features',
choices: () =>
Object.entries(CONFIG.DH.ITEM.armorFeatures).map(([k, v]) => ({ value: k, label: v.label })),
operator: 'contains3'
}
]
},
features: { features: {
columns: [], columns: [],
filters: [] filters: []
@ -157,54 +253,54 @@ export const typeConfig = {
columns: [ columns: [
{ {
key: 'system.type', key: 'system.type',
label: 'Type' label: 'DAGGERHEART.GENERAL.type'
}, },
{ {
key: 'system.domain', key: 'system.domain',
label: 'Domain' label: 'DAGGERHEART.GENERAL.Domain.single'
}, },
{ {
key: 'system.level', key: 'system.level',
label: 'Level' label: 'DAGGERHEART.GENERAL.level'
} }
], ],
filters: [ filters: [
{ {
key: 'system.type', key: 'system.type',
label: 'Type', label: 'DAGGERHEART.GENERAL.type',
field: 'system.api.models.items.DHDomainCard.schema.fields.type' field: 'system.api.models.items.DHDomainCard.schema.fields.type'
}, },
{ {
key: 'system.domain', key: 'system.domain',
label: 'Domain', label: 'DAGGERHEART.GENERAL.Domain.single',
field: 'system.api.models.items.DHDomainCard.schema.fields.domain', field: 'system.api.models.items.DHDomainCard.schema.fields.domain',
operator: 'contains2' operator: 'contains2'
}, },
{ {
key: 'system.level', key: 'system.level',
name: 'level.min', name: 'level.min',
label: 'Level (Min)', label: 'DAGGERHEART.UI.ItemBrowser.levelMin',
field: 'system.api.models.items.DHDomainCard.schema.fields.level', field: 'system.api.models.items.DHDomainCard.schema.fields.level',
operator: 'gte' operator: 'gte'
}, },
{ {
key: 'system.level', key: 'system.level',
name: 'level.max', name: 'level.max',
label: 'Level (Max)', label: 'DAGGERHEART.UI.ItemBrowser.levelMax',
field: 'system.api.models.items.DHDomainCard.schema.fields.level', field: 'system.api.models.items.DHDomainCard.schema.fields.level',
operator: 'lte' operator: 'lte'
}, },
{ {
key: 'system.recallCost', key: 'system.recallCost',
name: 'recall.min', name: 'recall.min',
label: 'Recall Cost (Min)', label: 'DAGGERHEART.UI.ItemBrowser.recallCostMin',
field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost', field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost',
operator: 'gte' operator: 'gte'
}, },
{ {
key: 'system.recallCost', key: 'system.recallCost',
name: 'recall.max', name: 'recall.max',
label: 'Recall Cost (Max)', label: 'DAGGERHEART.UI.ItemBrowser.recallCostMax',
field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost', field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost',
operator: 'lte' operator: 'lte'
} }
@ -214,50 +310,50 @@ export const typeConfig = {
columns: [ columns: [
{ {
key: 'system.evasion', key: 'system.evasion',
label: 'Evasion' label: 'DAGGERHEART.GENERAL.evasion'
}, },
{ {
key: 'system.hitPoints', key: 'system.hitPoints',
label: 'Hit Points' label: 'DAGGERHEART.GENERAL.HitPoints.plural'
}, },
{ {
key: 'system.domains', key: 'system.domains',
label: 'Domains' label: 'DAGGERHEART.GENERAL.Domain.plural'
} }
], ],
filters: [ filters: [
{ {
key: 'system.evasion', key: 'system.evasion',
name: 'evasion.min', name: 'evasion.min',
label: 'Evasion (Min)', label: 'DAGGERHEART.UI.ItemBrowser.evasionMin',
field: 'system.api.models.items.DHClass.schema.fields.evasion', field: 'system.api.models.items.DHClass.schema.fields.evasion',
operator: 'gte' operator: 'gte'
}, },
{ {
key: 'system.evasion', key: 'system.evasion',
name: 'evasion.max', name: 'evasion.max',
label: 'Evasion (Max)', label: 'DAGGERHEART.UI.ItemBrowser.evasionMax',
field: 'system.api.models.items.DHClass.schema.fields.evasion', field: 'system.api.models.items.DHClass.schema.fields.evasion',
operator: 'lte' operator: 'lte'
}, },
{ {
key: 'system.hitPoints', key: 'system.hitPoints',
name: 'hp.min', name: 'hp.min',
label: 'Hit Points (Min)', label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMin',
field: 'system.api.models.items.DHClass.schema.fields.hitPoints', field: 'system.api.models.items.DHClass.schema.fields.hitPoints',
operator: 'gte' operator: 'gte'
}, },
{ {
key: 'system.hitPoints', key: 'system.hitPoints',
name: 'hp.max', name: 'hp.max',
label: 'Hit Points (Max)', label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMax',
field: 'system.api.models.items.DHClass.schema.fields.hitPoints', field: 'system.api.models.items.DHClass.schema.fields.hitPoints',
operator: 'lte' operator: 'lte'
}, },
{ {
key: 'system.domains', key: 'system.domains',
label: 'Domains', label: 'DAGGERHEART.GENERAL.Domain.plural',
choices: () => Object.values(CONFIG.DH.DOMAIN.domains).map(d => ({ value: d.id, label: d.label })), choices: () => Object.values(CONFIG.DH.DOMAIN.allDomains()).map(d => ({ value: d.id, label: d.label })),
operator: 'contains2' operator: 'contains2'
} }
] ]
@ -271,17 +367,20 @@ export const typeConfig = {
}, },
{ {
key: 'system.spellcastingTrait', key: 'system.spellcastingTrait',
label: 'Spellcasting Trait' label: 'DAGGERHEART.ITEMS.Subclass.spellcastingTrait'
} }
], ],
filters: [ filters: [
{ {
key: 'system.linkedClass.uuid', key: 'system.linkedClass.uuid',
label: 'Class', label: 'Class',
choices: (items) => { choices: items => {
const list = items.map(item => ({ value: item.system.linkedClass.uuid, label: item.system.linkedClass.name })); const list = items.map(item => ({
return list.reduce((a,c) => { value: item.system.linkedClass.uuid,
if(!(a.find(i => i.value === c.value))) a.push(c); label: item.system.linkedClass.name
}));
return list.reduce((a, c) => {
if (!a.find(i => i.value === c.value)) a.push(c);
return a; return a;
}, []); }, []);
} }
@ -292,22 +391,22 @@ export const typeConfig = {
columns: [ columns: [
{ {
key: 'system.tier', key: 'system.tier',
label: 'Tier' label: 'DAGGERHEART.GENERAL.Tiers.singular'
}, },
{ {
key: 'system.mainTrait', key: 'system.mainTrait',
label: 'Main Trait' label: 'DAGGERHEART.GENERAL.Trait.single'
} }
], ],
filters: [ filters: [
{ {
key: 'system.tier', key: 'system.tier',
label: 'Tier', label: 'DAGGERHEART.GENERAL.Tiers.singular',
field: 'system.api.models.items.DHBeastform.schema.fields.tier' field: 'system.api.models.items.DHBeastform.schema.fields.tier'
}, },
{ {
key: 'system.mainTrait', key: 'system.mainTrait',
label: 'Main Trait', label: 'DAGGERHEART.GENERAL.Trait.single',
field: 'system.api.models.items.DHBeastform.schema.fields.mainTrait' field: 'system.api.models.items.DHBeastform.schema.fields.mainTrait'
} }
] ]
@ -315,109 +414,144 @@ export const typeConfig = {
}; };
export const compendiumConfig = { export const compendiumConfig = {
daggerheart: { characters: {
id: 'daggerheart', id: 'characters',
label: 'DAGGERHEART', keys: ['characters'],
folders: { label: 'DAGGERHEART.UI.ItemBrowser.folders.characters',
adversaries: { type: ['character']
id: 'adversaries', // listType: 'characters'
keys: ['adversaries'], },
label: 'Adversaries', adversaries: {
type: ['adversary'], id: 'adversaries',
listType: 'adversaries' keys: ['adversaries'],
}, label: 'DAGGERHEART.UI.ItemBrowser.folders.adversaries',
ancestries: { type: ['adversary'],
id: 'ancestries', listType: 'adversaries'
},
ancestries: {
id: 'ancestries',
keys: ['ancestries'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.ancestries',
type: ['ancestry']
/* folders: {
features: {
id: 'features',
keys: ['ancestries'], keys: ['ancestries'],
label: 'Ancestries', label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
type: ['ancestry'], type: ['feature']
folders: { }
features: { } */
id: 'features', },
keys: ['ancestries'], equipments: {
label: 'Features', id: 'equipments',
type: ['feature'] keys: ['armors', 'weapons', 'consumables', 'loot'],
} label: 'DAGGERHEART.UI.ItemBrowser.folders.equipment',
} type: ['armor', 'weapon', 'consumable', 'loot'],
listType: 'items',
folders: {
weapons: {
id: 'weapons',
keys: ['weapons'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.weapons',
type: ['weapon'],
listType: 'weapons'
}, },
equipments: { armors: {
id: 'equipments', id: 'armors',
keys: ['armors', 'weapons', 'consumables', 'loot'], keys: ['armors'],
label: 'Equipment', label: 'DAGGERHEART.UI.ItemBrowser.folders.armors',
type: ['armor', 'weapon', 'consumable', 'loot'], type: ['armor'],
listType: 'items' listType: 'armors'
}, },
classes: { consumables: {
id: 'classes', id: 'consumables',
keys: ['classes'], keys: ['consumables'],
label: 'Classes', label: 'DAGGERHEART.UI.ItemBrowser.folders.consumables',
type: ['class'], type: ['consumable']
folders: {
features: {
id: 'features',
keys: ['classes'],
label: 'Features',
type: ['feature']
},
items: {
id: 'items',
keys: ['classes'],
label: 'Items',
type: ['armor', 'weapon', 'consumable', 'loot'],
listType: 'items'
}
},
listType: 'classes'
}, },
subclasses: { loots: {
id: 'subclasses', id: 'loots',
keys: ['subclasses'], keys: ['loots'],
label: 'Subclasses', label: 'DAGGERHEART.UI.ItemBrowser.folders.loots',
type: ['subclass'], type: ['loot']
listType: 'subclasses'
},
domains: {
id: 'domains',
keys: ['domains'],
label: 'Domain Cards',
type: ['domainCard'],
listType: 'cards'
},
communities: {
id: 'communities',
keys: ['communities'],
label: 'Communities',
type: ['community'],
folders: {
features: {
id: 'features',
keys: ['communities'],
label: 'Features',
type: ['feature']
}
}
},
environments: {
id: 'environments',
keys: ['environments'],
label: 'Environments',
type: ['environment']
},
beastforms: {
id: 'beastforms',
keys: ['beastforms'],
label: 'Beastforms',
type: ['beastform'],
listType: 'beastforms',
folders: {
features: {
id: 'features',
keys: ['beastforms'],
label: 'Features',
type: ['feature']
}
}
} }
} }
},
classes: {
id: 'classes',
keys: ['classes'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.classes',
type: ['class'],
/* folders: {
features: {
id: 'features',
keys: ['classes'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
type: ['feature']
},
items: {
id: 'items',
keys: ['classes'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.items',
type: ['armor', 'weapon', 'consumable', 'loot'],
listType: 'items'
}
}, */
listType: 'classes'
},
subclasses: {
id: 'subclasses',
keys: ['subclasses'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.subclasses',
type: ['subclass'],
listType: 'subclasses'
},
domains: {
id: 'domains',
keys: ['domains'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.domainCards',
type: ['domainCard'],
listType: 'cards'
},
communities: {
id: 'communities',
keys: ['communities'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.communities',
type: ['community']
/* folders: {
features: {
id: 'features',
keys: ['communities'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
type: ['feature']
}
} */
},
environments: {
id: 'environments',
keys: ['environments'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.environments',
type: ['environment']
},
beastforms: {
id: 'beastforms',
keys: ['beastforms'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.beastforms',
type: ['beastform'],
listType: 'beastforms'
/* folders: {
features: {
id: 'features',
keys: ['beastforms'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
type: ['feature']
}
} */
},
features: {
id: 'features',
keys: ['features'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
type: ['feature']
} }
}; };

View file

@ -452,6 +452,34 @@ export const armorFeatures = {
} }
}; };
export const allArmorFeatures = () => {
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures
.armorFeatures;
return {
...armorFeatures,
...Object.keys(homebrewFeatures).reduce((acc, key) => {
const feature = homebrewFeatures[key];
acc[key] = { ...feature, label: feature.name };
return acc;
}, {})
};
};
export const orderedArmorFeatures = () => {
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures
.armorFeatures;
const allFeatures = { ...armorFeatures, ...homebrewFeatures };
const all = Object.keys(allFeatures).map(key => {
const feature = allFeatures[key];
return {
...feature,
id: key,
label: feature.label ?? feature.name
};
});
return Object.values(all).sort((a, b) => game.i18n.localize(a.label).localeCompare(game.i18n.localize(b.label)));
};
export const weaponFeatures = { export const weaponFeatures = {
barrier: { barrier: {
label: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.name', label: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.name',
@ -865,6 +893,9 @@ export const weaponFeatures = {
name: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.description',
img: 'icons/commodities/currency/coins-crown-stack-gold.webp', img: 'icons/commodities/currency/coins-crown-stack-gold.webp',
target: {
type: 'self'
},
// Should cost handful of gold, // Should cost handful of gold,
effects: [ effects: [
{ {
@ -1380,6 +1411,34 @@ export const weaponFeatures = {
} }
}; };
export const allWeaponFeatures = () => {
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures
.weaponFeatures;
return {
...weaponFeatures,
...Object.keys(homebrewFeatures).reduce((acc, key) => {
const feature = homebrewFeatures[key];
acc[key] = { ...feature, label: feature.name };
return acc;
}, {})
};
};
export const orderedWeaponFeatures = () => {
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures
.weaponFeatures;
const allFeatures = { ...weaponFeatures, ...homebrewFeatures };
const all = Object.keys(allFeatures).map(key => {
const feature = allFeatures[key];
return {
...feature,
id: key,
label: feature.label ?? feature.name
};
});
return Object.values(all).sort((a, b) => game.i18n.localize(a.label).localeCompare(game.i18n.localize(b.label)));
};
export const featureTypes = { export const featureTypes = {
ancestry: { ancestry: {
id: 'ancestry', id: 'ancestry',

View file

@ -27,5 +27,25 @@ export const gameSettings = {
}, },
LevelTiers: 'LevelTiers', LevelTiers: 'LevelTiers',
Countdowns: 'Countdowns', Countdowns: 'Countdowns',
LastMigrationVersion: 'LastMigrationVersion' LastMigrationVersion: 'LastMigrationVersion',
TagTeamRoll: 'TagTeamRoll'
};
export const actionAutomationChoices = {
never: {
id: 'never',
label: 'Never'
},
showDialog: {
id: 'showDialog',
label: 'Show Dialog only'
},
// npcOnly: {
// id: "npcOnly",
// label: "Always for non-characters"
// },
always: {
id: 'always',
label: 'Always'
}
}; };

View file

@ -1,5 +1,6 @@
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 * as actions from './action/_module.mjs'; export * as actions from './action/_module.mjs';
export * as activeEffects from './activeEffect/_module.mjs'; export * as activeEffects from './activeEffect/_module.mjs';
@ -7,3 +8,4 @@ export * as actors from './actor/_module.mjs';
export * as chatMessages from './chat-message/_modules.mjs'; export * as chatMessages from './chat-message/_modules.mjs';
export * as fields from './fields/_module.mjs'; export * as fields from './fields/_module.mjs';
export * as items from './item/_module.mjs'; export * as items from './item/_module.mjs';
export * as scenes from './scene/_module.mjs';

View file

@ -1,14 +1,9 @@
import DhpActor from '../../documents/actor.mjs'; import DhpActor from '../../documents/actor.mjs';
import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs'; import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs';
import { ActionMixin } from '../fields/actionField.mjs'; import { ActionMixin } from '../fields/actionField.mjs';
import { abilities } from '../../config/actorConfig.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
/*
!!! I'm currently refactoring the whole Action thing, it's a WIP !!!
*/
/* /*
ToDo ToDo
- Target Check / Target Picker - Target Check / Target Picker
@ -20,6 +15,7 @@ const fields = foundry.data.fields;
export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel) { export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel) {
static extraSchemas = ['cost', 'uses', 'range']; static extraSchemas = ['cost', 'uses', 'range'];
/** @inheritDoc */
static defineSchema() { static defineSchema() {
const schemaFields = { const schemaFields = {
_id: new fields.DocumentIdField({ initial: () => foundry.utils.randomID() }), _id: new fields.DocumentIdField({ initial: () => foundry.utils.randomID() }),
@ -32,36 +28,82 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
actionType: new fields.StringField({ actionType: new fields.StringField({
choices: CONFIG.DH.ITEM.actionTypes, choices: CONFIG.DH.ITEM.actionTypes,
initial: 'action', initial: 'action',
nullable: true nullable: false,
required: true
}) })
}; };
this.extraSchemas.forEach(s => { this.extraSchemas.forEach(s => {
let clsField; let clsField = this.getActionField(s);
if ((clsField = this.getActionField(s))) schemaFields[s] = new clsField(); if (clsField) schemaFields[s] = new clsField();
}); });
return schemaFields; return schemaFields;
} }
/**
* Create a Map containing each Action step based on fields define in schema. Ordered by Fields order property.
*
* Each step can be called individually as long as needed config is provided.
* Ex: <action>.workflow.get("damage").execute(config)
* @returns {Map}
*/
defineWorkflow() {
const workflow = new Map();
this.constructor.extraSchemas.forEach(s => {
let clsField = this.constructor.getActionField(s);
if (clsField?.execute) {
workflow.set(s, { order: clsField.order, execute: clsField.execute.bind(this) });
if (s === 'damage')
workflow.set('applyDamage', { order: 75, execute: clsField.applyDamage.bind(this) });
}
});
return new Map([...workflow.entries()].sort(([aKey, aValue], [bKey, bValue]) => aValue.order - bValue.order));
}
/**
* Getter returning the workflow property or creating it the first time the property is called
*/
get workflow() {
if (this.hasOwnProperty('_workflow')) return this._workflow;
const workflow = Object.freeze(this.defineWorkflow());
Object.defineProperty(this, '_workflow', { value: workflow, writable: false });
return workflow;
}
/**
* Get the Field class from ActionFields global config
* @param {string} name Field short name, equal to Action property
* @returns Action Field
*/
static getActionField(name) { static getActionField(name) {
const field = game.system.api.fields.ActionFields[`${name.capitalize()}Field`]; const field = game.system.api.fields.ActionFields[`${name.capitalize()}Field`];
return fields.DataField.isPrototypeOf(field) && field; return fields.DataField.isPrototypeOf(field) && field;
} }
/** @inheritDoc */
prepareData() { prepareData() {
this.name = this.name || game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[this.type].name); this.name = this.name || game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[this.type].name);
this.img = this.img ?? this.parent?.parent?.img; this.img = this.img ?? this.parent?.parent?.img;
} }
/**
* Get Action ID
*/
get id() { get id() {
return this._id; return this._id;
} }
/**
* Return Item the action is attached too.
*/
get item() { get item() {
return this.parent.parent; return this.parent.parent;
} }
/**
* Return the first Actor parent found.
*/
get actor() { get actor() {
return this.item instanceof DhpActor return this.item instanceof DhpActor
? this.item ? this.item
@ -74,6 +116,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return 'trait'; return 'trait';
} }
/**
* Prepare base data based on Action Type & Parent Type
* @param {object} parent
* @returns {object}
*/
static getSourceConfig(parent) { static getSourceConfig(parent) {
const updateSource = {}; const updateSource = {};
if (parent?.parent?.type === 'weapon' && this === game.system.api.models.actions.actionsTypes.attack) { if (parent?.parent?.type === 'weapon' && this === game.system.api.models.actions.actionsTypes.attack) {
@ -96,6 +143,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return updateSource; return updateSource;
} }
/**
* Obtain a data object used to evaluate any dice rolls associated with this particular Action
* @param {object} [data ={}] Optional data object from previous configuration/rolls
* @returns {object}
*/
getRollData(data = {}) { getRollData(data = {}) {
if (!this.actor) return null; if (!this.actor) return null;
const actorData = this.actor.getRollData(false); const actorData = this.actor.getRollData(false);
@ -111,19 +163,30 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return actorData; return actorData;
} }
async use(event, options = {}) { /**
* Execute each part of the Action workflow in order, calling a specific event before and after each part.
* @param {object} config Config object usually created from prepareConfig method
*/
async executeWorkflow(config) {
for (const [key, part] of this.workflow) {
if (Hooks.call(`${CONFIG.DH.id}.pre${key.capitalize()}Action`, this, config) === false) return;
if ((await part.execute(config)) === false) return;
if (Hooks.call(`${CONFIG.DH.id}.post${key.capitalize()}Action`, this, config) === false) return;
}
}
/**
* Main method to use the Action
* @param {Event} event Event from the button used to trigger the Action
* @returns {object}
*/
async use(event) {
if (!this.actor) throw new Error("An Action can't be used outside of an Actor context."); if (!this.actor) throw new Error("An Action can't be used outside of an Actor context.");
if (this.chatDisplay) await this.toChat(); if (this.chatDisplay) await this.toChat();
let { byPassRoll } = options,
config = this.prepareConfig(event, byPassRoll); let config = this.prepareConfig(event);
for (let i = 0; i < this.constructor.extraSchemas.length; i++) { if (!config) return;
let clsField = this.constructor.getActionField(this.constructor.extraSchemas[i]);
if (clsField?.prepareConfig) {
const keep = clsField.prepareConfig.call(this, config);
if (config.isFastForward && !keep) return;
}
}
if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return; if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return;
@ -133,273 +196,116 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
if (!config) return; if (!config) return;
} }
if (config.hasRoll) { // Execute the Action Worflow in order based of schema fields
const rollConfig = this.prepareRoll(config); await this.executeWorkflow(config);
config.roll = rollConfig;
config = await this.actor.diceRoll(config);
if (!config) return;
}
if (this.doFollowUp(config)) {
if (this.rollDamage && this.damage.parts.length) await this.rollDamage(event, config);
else if (this.trigger) await this.trigger(event, config);
else if (this.hasSave || this.hasEffect) {
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
roll._evaluated = true;
await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
}
}
// Consume resources
await this.consume(config);
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return; if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;
return config; return config;
} }
/* */ /**
prepareConfig(event, byPass = false) { * Create the basic config common to every action type
const hasRoll = this.getUseHasRoll(byPass); * @param {Event} event Event from the button used to trigger the Action
return { * @returns {object}
*/
prepareBaseConfig(event) {
const config = {
event, event,
title: `${this.item.name}: ${game.i18n.localize(this.name)}`, title: `${this.item instanceof CONFIG.Actor.documentClass ? '' : `${this.item.name}: `}${game.i18n.localize(this.name)}`,
source: { source: {
item: this.item._id, item: this.item._id,
action: this._id, action: this._id,
actor: this.actor.uuid actor: this.actor.uuid
}, },
dialog: { dialog: {},
configure: hasRoll actionType: this.actionType,
}, hasRoll: this.hasRoll,
type: this.roll?.type ?? this.type, hasDamage: this.hasDamage,
hasRoll: hasRoll, hasHealing: this.hasHealing,
hasDamage: this.damage?.parts?.length && this.type !== 'healing', hasEffect: this.hasEffect,
hasHealing: this.damage?.parts?.length && this.type === 'healing',
hasEffect: !!this.effects?.length,
hasSave: this.hasSave, hasSave: this.hasSave,
isDirect: !!this.damage?.direct,
selectedRollMode: game.settings.get('core', 'rollMode'), selectedRollMode: game.settings.get('core', 'rollMode'),
isFastForward: event.shiftKey,
data: this.getRollData(), data: this.getRollData(),
evaluate: hasRoll evaluate: this.hasRoll
}; };
DHBaseAction.applyKeybindings(config);
return config;
} }
/**
* Create the config for that action used for its workflow
* @param {Event} event Event from the button used to trigger the Action
* @returns {object}
*/
prepareConfig(event) {
const config = this.prepareBaseConfig(event);
for (const clsField of Object.values(this.schema.fields)) {
if (clsField?.prepareConfig) if (clsField.prepareConfig.call(this, config) === false) return false;
}
return config;
}
/**
* Method used to know if a configuration dialog must be shown or not when there is no roll.
* @param {*} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @returns {boolean}
*/
requireConfigurationDialog(config) { requireConfigurationDialog(config) {
return !config.event.shiftKey && !config.hasRoll && (config.costs?.length || config.uses); return !config.event.shiftKey && !config.hasRoll && (config.costs?.length || config.uses);
} }
prepareRoll() { /**
const roll = { * Consume Action configured resources & uses.
baseModifiers: this.roll.getModifier(), * That method is only used when we want those resources to be consumed outside of the use method workflow.
label: 'Attack', * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
type: this.actionType, * @param {boolean} successCost
difficulty: this.roll?.difficulty, */
formula: this.roll.getFormula(),
advantage: CONFIG.DH.ACTIONS.advantageState[this.roll.advState].value
};
if (this.roll?.type === 'diceSet' || !this.hasRoll) roll.lite = true;
return roll;
}
doFollowUp(config) {
return !config.hasRoll;
}
async consume(config, successCost = false) { async consume(config, successCost = false) {
const actor = this.actor.system.partner ?? this.actor, await this.workflow.get('cost')?.execute(config, successCost);
usefulResources = { await this.workflow.get('uses')?.execute(config, successCost);
...foundry.utils.deepClone(actor.system.resources),
fear: {
value: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
max: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear,
reversed: false
}
};
for (var cost of config.costs) { if (config.roll && !config.roll.success && successCost) {
if (cost.keyIsID) {
usefulResources[cost.key] = {
value: cost.value,
target: this.parent.parent,
keyIsID: true
};
}
}
const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(config.costs)
.filter(
c =>
(!successCost && (!c.consumeOnSuccess || config.roll?.success)) ||
(successCost && c.consumeOnSuccess)
)
.reduce((a, c) => {
const resource = usefulResources[c.key];
if (resource) {
a.push({
key: c.key,
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
target: resource.target,
keyIsID: resource.keyIsID
});
return a;
}
}, []);
await actor.modifyResource(resources);
if (
config.uses?.enabled &&
((!successCost && (!config.uses?.consumeOnSuccess || config.roll?.success)) ||
(successCost && config.uses?.consumeOnSuccess))
)
this.update({ 'uses.value': this.uses.value + 1 });
if (config.roll?.success || successCost) {
setTimeout(() => { setTimeout(() => {
(config.message ?? config.parent).update({ 'system.successConsumed': true }); (config.message ?? config.parent).update({ 'system.successConsumed': true });
}, 50); }, 50);
} }
} }
/* */
/* ROLL */ /**
getUseHasRoll(byPass = false) { * Set if a configuration dialog must be shown or not if a special keyboard key is pressed.
return this.hasRoll && !byPass; * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
*/
static applyKeybindings(config) {
config.dialog.configure ??= !(config.event.shiftKey || config.event.altKey || config.event.ctrlKey);
} }
/**
* Getters to know which parts the action is composed of. A field can exist but configured to not be used.
* @returns {boolean} If that part is in the action.
*/
get hasRoll() { get hasRoll() {
return !!this.roll?.type; return !!this.roll?.type;
} }
get modifiers() { get hasDamage() {
if (!this.actor) return []; return this.damage?.parts?.length && this.type !== 'healing';
const modifiers = []; }
/** Placeholder for specific bonuses **/
return modifiers; get hasHealing() {
return this.damage?.parts?.length && this.type === 'healing';
} }
/* ROLL */
/* SAVE */
get hasSave() { get hasSave() {
return !!this.save?.trait; return !!this.save?.trait;
} }
/* SAVE */
/* EFFECTS */
get hasEffect() { get hasEffect() {
return this.effects?.length > 0; return this.effects?.length > 0;
} }
async applyEffects(event, data, targets) {
targets ??= data.system.targets;
const force = true; /* Where should this come from? */
if (!this.effects?.length || !targets.length) return;
let effects = this.effects;
targets.forEach(async token => {
if (!token.hit && !force) return;
if (this.hasSave && token.saved.success === true) {
effects = this.effects.filter(e => e.onSave === true);
}
if (!effects.length) return;
effects.forEach(async e => {
const actor = canvas.tokens.get(token.id)?.actor,
effect = this.item.effects.get(e._id);
if (!actor || !effect) return;
await this.applyEffect(effect, actor);
});
});
}
async applyEffect(effect, actor) {
const existingEffect = actor.effects.find(e => e.origin === effect.uuid);
if (existingEffect) {
return effect.update(
foundry.utils.mergeObject({
...effect.constructor.getInitialDuration(),
disabled: false
})
);
}
// Otherwise, create a new effect on the target
const effectData = foundry.utils.mergeObject({
...effect.toObject(),
disabled: false,
transfer: false,
origin: effect.uuid
});
await ActiveEffect.implementation.create(effectData, { parent: actor });
}
/* EFFECTS */
/* SAVE */
async rollSave(actor, event, message) {
if (!actor) return;
const title = actor.isNPC
? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll')
: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(abilities[this.save.trait]?.label)
});
return actor.diceRoll({
event,
title,
roll: {
trait: this.save.trait,
difficulty: this.save.difficulty ?? this.actor?.baseSaveDifficulty,
type: 'reaction'
},
type: 'trait',
hasRoll: true,
data: actor.getRollData()
});
}
updateSaveMessage(result, message, targetId) {
if (!result) return;
const updateMsg = this.updateChatMessage.bind(this, message, targetId, {
result: result.roll.total,
success: result.roll.success
});
if (game.modules.get('dice-so-nice')?.active)
game.dice3d.waitFor3DAnimationByMessageID(result.message.id ?? result.message._id).then(() => updateMsg());
else updateMsg();
}
static rollSaveQuery({ actionId, actorId, event, message }) {
return new Promise(async (resolve, reject) => {
const actor = await fromUuid(actorId),
action = await fromUuid(actionId);
if (!actor || !actor?.isOwner) reject();
action.rollSave(actor, event, message).then(result => resolve(result));
});
}
/* SAVE */
async updateChatMessage(message, targetId, changes, chain = true) {
setTimeout(async () => {
const chatMessage = ui.chat.collection.get(message._id);
await chatMessage.update({
flags: {
[game.system.id]: {
reactionRolls: {
[targetId]: changes
}
}
}
});
}, 100);
if (chain) {
if (message.system.source.message)
this.updateChatMessage(ui.chat.collection.get(message.system.source.message), targetId, changes, false);
const relatedChatMessages = ui.chat.collection.filter(c => c.system.source?.message === message._id);
relatedChatMessages.forEach(c => {
this.updateChatMessage(c, targetId, changes, false);
});
}
}
/** /**
* Generates a list of localized tags for this action. * Generates a list of localized tags for this action.
* @returns {string[]} An array of localized tag strings. * @returns {string[]} An array of localized tag strings.

View file

@ -1,10 +1,9 @@
import BeastformDialog from '../../applications/dialogs/beastformDialog.mjs';
import DHBaseAction from './baseAction.mjs'; import DHBaseAction from './baseAction.mjs';
export default class DhBeastformAction extends DHBaseAction { export default class DhBeastformAction extends DHBaseAction {
static extraSchemas = [...super.extraSchemas, 'beastform']; static extraSchemas = [...super.extraSchemas, 'beastform'];
async use(event, options) { /* async use(event, options) {
const beastformConfig = this.prepareBeastformConfig(); const beastformConfig = this.prepareBeastformConfig();
const abort = await this.handleActiveTransformations(); const abort = await this.handleActiveTransformations();
@ -82,5 +81,5 @@ export default class DhBeastformAction extends DHBaseAction {
beastformEffects.map(x => x.id) beastformEffects.map(x => x.id)
); );
return existingEffects; return existingEffects;
} } */
} }

View file

@ -1,65 +1,5 @@
import { setsEqual } from '../../helpers/utils.mjs';
import DHBaseAction from './baseAction.mjs'; import DHBaseAction from './baseAction.mjs';
export default class DHDamageAction extends DHBaseAction { export default class DHDamageAction extends DHBaseAction {
static extraSchemas = [...super.extraSchemas, 'damage', 'target', 'effects']; static extraSchemas = [...super.extraSchemas, 'damage', 'target', 'effects'];
getFormulaValue(part, data) {
let formulaValue = part.value;
if (data.hasRoll && part.resultBased && data.roll.result.duality === -1) return part.valueAlt;
const isAdversary = this.actor.type === 'adversary';
if (isAdversary && this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) {
const hasHordeDamage = this.actor.effects.find(x => x.type === 'horde');
if (hasHordeDamage && !hasHordeDamage.disabled) return part.valueAlt;
}
return formulaValue;
}
formatFormulas(formulas, systemData) {
const formattedFormulas = [];
formulas.forEach(formula => {
if (isNaN(formula.formula))
formula.formula = Roll.replaceFormulaData(formula.formula, this.getRollData(systemData));
const same = formattedFormulas.find(
f => setsEqual(f.damageTypes, formula.damageTypes) && f.applyTo === formula.applyTo
);
if (same) same.formula += ` + ${formula.formula}`;
else formattedFormulas.push(formula);
});
return formattedFormulas;
}
async rollDamage(event, data) {
const systemData = data.system ?? data;
let formulas = this.damage.parts.map(p => ({
formula: this.getFormulaValue(p, systemData).getFormula(this.actor),
damageTypes: p.applyTo === 'hitPoints' && !p.type.size ? new Set(['physical']) : p.type,
applyTo: p.applyTo
}));
if (!formulas.length) return;
formulas = this.formatFormulas(formulas, systemData);
delete systemData.evaluate;
const config = {
...systemData,
roll: formulas,
dialog: {},
data: this.getRollData()
};
if (this.hasSave) config.onSave = this.save.damageMod;
if (data.system) {
config.source.message = data._id;
config.directDamage = false;
} else {
config.directDamage = true;
}
return CONFIG.Dice.daggerheart.DamageRoll.build(config);
}
} }

View file

@ -2,15 +2,4 @@ import DHBaseAction from './baseAction.mjs';
export default class DHMacroAction extends DHBaseAction { export default class DHMacroAction extends DHBaseAction {
static extraSchemas = [...super.extraSchemas, 'macro']; static extraSchemas = [...super.extraSchemas, 'macro'];
async trigger(event, ...args) {
const fixUUID = !this.macro.includes('Macro.') ? `Macro.${this.macro}` : this.macro,
macro = await fromUuid(fixUUID);
try {
if (!macro) throw new Error(`No macro found for the UUID: ${this.macro}.`);
macro.execute();
} catch (error) {
ui.notifications.error(error);
}
}
} }

View file

@ -30,4 +30,24 @@ export default class BaseEffect extends foundry.abstract.TypeDataModel {
}) })
}; };
} }
static getDefaultObject() {
return {
name: 'New Effect',
id: foundry.utils.randomID(),
disabled: false,
img: 'icons/magic/life/heart-cross-blue.webp',
description: '',
statuses: [],
changes: [],
system: {
rangeDependence: {
enabled: false,
type: CONFIG.DH.GENERAL.rangeInclusion.withinRange.id,
target: CONFIG.DH.GENERAL.otherTargetTypes.hostile.id,
range: CONFIG.DH.GENERAL.range.melee.id
}
}
};
}
} }

View file

@ -2,12 +2,14 @@ import DhCharacter from './character.mjs';
import DhCompanion from './companion.mjs'; import DhCompanion from './companion.mjs';
import DhAdversary from './adversary.mjs'; import DhAdversary from './adversary.mjs';
import DhEnvironment from './environment.mjs'; import DhEnvironment from './environment.mjs';
import DhParty from './party.mjs';
export { DhCharacter, DhCompanion, DhAdversary, DhEnvironment }; export { DhCharacter, DhCompanion, DhAdversary, DhEnvironment, DhParty };
export const config = { export const config = {
character: DhCharacter, character: DhCharacter,
companion: DhCompanion, companion: DhCompanion,
adversary: DhAdversary, adversary: DhAdversary,
environment: DhEnvironment environment: DhEnvironment,
party: DhParty
}; };

View file

@ -10,7 +10,8 @@ export default class DhpAdversary extends BaseDataActor {
return foundry.utils.mergeObject(super.metadata, { return foundry.utils.mergeObject(super.metadata, {
label: 'TYPES.Actor.adversary', label: 'TYPES.Actor.adversary',
type: 'adversary', type: 'adversary',
settingSheet: DHAdversarySettings settingSheet: DHAdversarySettings,
hasAttribution: true
}); });
} }
@ -26,7 +27,7 @@ export default class DhpAdversary extends BaseDataActor {
}), }),
type: new fields.StringField({ type: new fields.StringField({
required: true, required: true,
choices: CONFIG.DH.ACTOR.adversaryTypes, choices: CONFIG.DH.ACTOR.allAdversaryTypes,
initial: CONFIG.DH.ACTOR.adversaryTypes.standard.id initial: CONFIG.DH.ACTOR.adversaryTypes.standard.id
}), }),
motivesAndTactics: new fields.StringField(), motivesAndTactics: new fields.StringField(),
@ -169,4 +170,13 @@ export default class DhpAdversary extends BaseDataActor {
} }
} }
} }
_getTags() {
const tags = [
game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${this.tier}`),
`${game.i18n.localize(`DAGGERHEART.CONFIG.AdversaryType.${this.type}.label`)}`,
`${game.i18n.localize('DAGGERHEART.GENERAL.difficulty')}: ${this.difficulty}`
];
return tags;
}
} }

View file

@ -1,5 +1,5 @@
import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs'; import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs';
import { createScrollText, getScrollTextData } from '../../helpers/utils.mjs'; import { getScrollTextData } from '../../helpers/utils.mjs';
const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) => const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) =>
new foundry.data.fields.SchemaField({ new foundry.data.fields.SchemaField({
@ -39,7 +39,9 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
type: 'base', type: 'base',
isNPC: true, isNPC: true,
settingSheet: null, settingSheet: null,
hasResistances: true hasResistances: true,
hasAttribution: false,
hasLimitedView: true
}; };
} }
@ -53,6 +55,13 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
const fields = foundry.data.fields; const fields = foundry.data.fields;
const schema = {}; const schema = {};
if (this.metadata.hasAttribution) {
schema.attribution = new fields.SchemaField({
source: new fields.StringField(),
page: new fields.NumberField(),
artist: new fields.StringField()
});
}
if (this.metadata.isNPC) schema.description = new fields.HTMLField({ required: true, nullable: true }); if (this.metadata.isNPC) schema.description = new fields.HTMLField({ required: true, nullable: true });
if (this.metadata.hasResistances) if (this.metadata.hasResistances)
schema.resistance = new fields.SchemaField({ schema.resistance = new fields.SchemaField({
@ -78,6 +87,13 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
*/ */
static DEFAULT_ICON = null; static DEFAULT_ICON = null;
get attributionLabel() {
if (!this.attribution) return;
const { source, page } = this.attribution;
return [source, page ? `pg ${page}.` : null].filter(x => x).join('. ');
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
@ -138,6 +154,6 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
_onUpdate(changes, options, userId) { _onUpdate(changes, options, userId) {
super._onUpdate(changes, options, userId); super._onUpdate(changes, options, userId);
createScrollText(this.parent, options.scrollingTextData); if (options.scrollingTextData) this.parent.queueScrollText(options.scrollingTextData);
} }
} }

View file

@ -673,4 +673,8 @@ export default class DhCharacter extends BaseDataActor {
this.companion.updateLevel(1); this.companion.updateLevel(1);
} }
} }
_getTags() {
return [this.class.value?.name, this.class.subclass?.name, this.community?.name, this.ancestry?.name].filter((t) => !!t);
}
} }

View file

@ -12,7 +12,8 @@ export default class DhEnvironment extends BaseDataActor {
label: 'TYPES.Actor.environment', label: 'TYPES.Actor.environment',
type: 'environment', type: 'environment',
settingSheet: DHEnvironmentSettings, settingSheet: DHEnvironmentSettings,
hasResistances: false hasResistances: false,
hasAttribution: true
}); });
} }

View file

@ -0,0 +1,48 @@
import BaseDataActor from './base.mjs';
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
export default class DhParty extends BaseDataActor {
/**@inheritdoc */
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
partyMembers: new ForeignDocumentUUIDArrayField({ type: 'Actor' }),
notes: new fields.HTMLField(),
gold: new fields.SchemaField({
coins: new fields.NumberField({ initial: 0, integer: true }),
handfuls: new fields.NumberField({ initial: 1, integer: true }),
bags: new fields.NumberField({ initial: 0, integer: true }),
chests: new fields.NumberField({ initial: 0, integer: true })
})
};
}
/* -------------------------------------------- */
/**@inheritdoc */
static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/actors/dark-squad.svg';
/* -------------------------------------------- */
prepareBaseData() {
super.prepareBaseData();
this.partyMembers = this.partyMembers.filter(p => !!p);
// Register this party to all members
if (game.actors.get(this.parent.id) === this.parent) {
for (const member of this.partyMembers) {
member.parties?.add(this.parent);
}
}
}
_onDelete(options, userId) {
super._onDelete(options, userId);
// Clear this party from all members that aren't deleted
for (const member of this.partyMembers) {
member.parties?.delete(this.parent);
}
}
}

View file

@ -1,9 +1,13 @@
import DHAbilityUse from "./abilityUse.mjs"; import DHAbilityUse from './abilityUse.mjs';
import DHActorRoll from "./adversaryRoll.mjs"; import DHActorRoll from './actorRoll.mjs';
import DHGroupRoll from './groupRoll.mjs';
import DHSystemMessage from './systemMessage.mjs';
export const config = { export const config = {
abilityUse: DHAbilityUse, abilityUse: DHAbilityUse,
adversaryRoll: DHActorRoll, adversaryRoll: DHActorRoll,
damageRoll: DHActorRoll, damageRoll: DHActorRoll,
dualityRoll: DHActorRoll dualityRoll: DHActorRoll,
groupRoll: DHGroupRoll,
systemMessage: DHSystemMessage
}; };

View file

@ -29,6 +29,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
hasEffect: new fields.BooleanField({ initial: false }), hasEffect: new fields.BooleanField({ initial: false }),
hasSave: new fields.BooleanField({ initial: false }), hasSave: new fields.BooleanField({ initial: false }),
hasTarget: new fields.BooleanField({ initial: false }), hasTarget: new fields.BooleanField({ initial: false }),
isDirect: new fields.BooleanField({ initial: false }),
isCritical: new fields.BooleanField({ initial: false }), isCritical: new fields.BooleanField({ initial: false }),
onSave: new fields.StringField(), onSave: new fields.StringField(),
source: new fields.SchemaField({ source: new fields.SchemaField({
@ -54,9 +55,12 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
} }
get action() { get action() {
const actionItem = this.actionItem; const actionActor = this.actionActor,
if (!actionItem || !this.source.action) return null; actionItem = this.actionItem;
return actionItem.system.actionsList?.find(a => a.id === this.source.action); if (!this.source.action) return null;
if (actionItem) return actionItem.system.actionsList?.find(a => a.id === this.source.action);
else if (actionActor?.system.attack?._id === this.source.action) return actionActor.system.attack;
return null;
} }
get targetMode() { get targetMode() {
@ -94,7 +98,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
} }
registerTargetHook() { registerTargetHook() {
if (!this.parent.isAuthor) return; if (!this.parent.isAuthor || !this.hasTarget) return;
if (this.targetMode && this.parent.targetHook !== null) { if (this.targetMode && this.parent.targetHook !== null) {
Hooks.off('targetToken', this.parent.targetHook); Hooks.off('targetToken', this.parent.targetHook);
return (this.parent.targetHook = null); return (this.parent.targetHook = null);
@ -126,7 +130,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
} }
this.canViewSecret = this.parent.speakerActor?.testUserPermission(game.user, 'OBSERVER'); this.canViewSecret = this.parent.speakerActor?.testUserPermission(game.user, 'OBSERVER');
this.canButtonApply = game.user.isGM; //temp this.canButtonApply = game.user.isGM; //temp
this.isGM = game.user.isGM; //temp this.isGM = game.user.isGM; //temp
} }

View file

@ -0,0 +1,39 @@
import { abilities } from '../../config/actorConfig.mjs';
export default class DHGroupRoll extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
leader: new fields.EmbeddedDataField(GroupRollMemberField),
members: new fields.ArrayField(new fields.EmbeddedDataField(GroupRollMemberField))
};
}
get totalModifier() {
return this.members.reduce((acc, m) => {
if (m.manualSuccess === null) return acc;
return acc + (m.manualSuccess ? 1 : -1);
}, 0);
}
}
class GroupRollMemberField extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
actor: new fields.ObjectField(),
trait: new fields.StringField({ choices: abilities }),
difficulty: new fields.StringField(),
result: new fields.ObjectField({ nullable: true, initial: null }),
manualSuccess: new fields.BooleanField({ nullable: true, initial: null })
};
}
/* Can be expanded if we handle automation of success/failure */
get success() {
return manualSuccess;
}
}

View file

@ -0,0 +1,9 @@
export default class DHSystemMessage extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
useTitle: new fields.BooleanField({ initial: true })
};
}
}

View file

@ -3,7 +3,8 @@ export default class DhCombatant extends foundry.abstract.TypeDataModel {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
spotlight: new fields.SchemaField({ spotlight: new fields.SchemaField({
requesting: new fields.BooleanField({ required: true, initial: false }) requesting: new fields.BooleanField({ required: true, initial: false }),
requestOrderIndex: new fields.NumberField({ required: true, integer: true, initial: 0 })
}), }),
actionTokens: new fields.NumberField({ required: true, integer: true, initial: 3 }) actionTokens: new fields.NumberField({ required: true, integer: true, initial: 3 })
}; };

View file

@ -1,25 +1,28 @@
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
export default class DhCountdowns extends foundry.abstract.DataModel { export default class DhCountdowns extends foundry.abstract.DataModel {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
/* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */
narrative: new fields.EmbeddedDataField(DhCountdownData), narrative: new fields.EmbeddedDataField(DhCountdownData),
encounter: new fields.EmbeddedDataField(DhCountdownData) encounter: new fields.EmbeddedDataField(DhCountdownData),
/**/
countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhCountdown)),
defaultOwnership: new fields.NumberField({
required: true,
choices: CONFIG.DH.GENERAL.basicOwnershiplevels,
initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER
})
}; };
} }
static CountdownCategories = { narrative: 'narrative', combat: 'combat' };
} }
/* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */
class DhCountdownData extends foundry.abstract.DataModel { class DhCountdownData extends foundry.abstract.DataModel {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.APPLICATIONS.Countdown']; // Nots ure why this won't work. Setting labels manually for now
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhCountdown)), countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhOldCountdown)),
ownership: new fields.SchemaField({ ownership: new fields.SchemaField({
default: new fields.NumberField({ default: new fields.NumberField({
required: true, required: true,
@ -56,7 +59,8 @@ class DhCountdownData extends foundry.abstract.DataModel {
} }
} }
class DhCountdown extends foundry.abstract.DataModel { /* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */
class DhOldCountdown extends foundry.abstract.DataModel {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
@ -129,17 +133,88 @@ class DhCountdown extends foundry.abstract.DataModel {
} }
} }
export const registerCountdownHooks = () => { export class DhCountdown extends foundry.abstract.DataModel {
Hooks.on(socketEvent.Refresh, ({ refreshType, application }) => { static defineSchema() {
if (refreshType === RefreshType.Countdown) { const fields = foundry.data.fields;
if (application) { return {
foundry.applications.instances.get(application)?.render(); type: new fields.StringField({
} else { required: true,
foundry.applications.instances.get('narrative-countdowns')?.render(); choices: CONFIG.DH.GENERAL.countdownBaseTypes,
foundry.applications.instances.get('encounter-countdowns')?.render(); label: 'DAGGERHEART.GENERAL.type'
} }),
name: new fields.StringField({
required: true,
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.name.label'
}),
img: new fields.FilePathField({
categories: ['IMAGE'],
base64: false,
initial: 'icons/magic/time/hourglass-yellow-green.webp'
}),
ownership: new fields.TypedObjectField(
new fields.NumberField({
required: true,
choices: CONFIG.DH.GENERAL.simpleOwnershiplevels,
initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
})
),
progress: new fields.SchemaField({
current: new fields.NumberField({
required: true,
integer: true,
initial: 1,
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.current.label'
}),
max: new fields.NumberField({
required: true,
integer: true,
initial: 1,
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.max.label'
}),
type: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.countdownTypes,
initial: CONFIG.DH.GENERAL.countdownTypes.custom.id,
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.type.label'
})
})
};
}
return false; static defaultCountdown(type, playerHidden) {
} const ownership = playerHidden
}); ? game.users.reduce((acc, user) => {
}; if (!user.isGM) {
acc[user.id] = CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE;
}
return acc;
}, {})
: undefined;
return {
type: type ?? CONFIG.DH.GENERAL.countdownBaseTypes.narrative.id,
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.newCountdown'),
img: 'icons/magic/time/hourglass-yellow-green.webp',
ownership: ownership,
progress: {
current: 1,
max: 1
}
};
}
get playerOwnership() {
return Array.from(game.users).reduce((acc, user) => {
acc[user.id] = {
value: user.isGM
? CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER
: this.ownership.players[user.id] && this.ownership.players[user.id].type !== -1
? this.ownership.players[user.id].type
: this.ownership.default,
isGM: user.isGM
};
return acc;
}, {});
}
}

View file

@ -6,6 +6,5 @@ export { default as EffectsField } from './effectsField.mjs';
export { default as SaveField } from './saveField.mjs'; export { default as SaveField } from './saveField.mjs';
export { default as BeastformField } from './beastformField.mjs'; export { default as BeastformField } from './beastformField.mjs';
export { default as DamageField } from './damageField.mjs'; export { default as DamageField } from './damageField.mjs';
export { default as HealingField } from './healingField.mjs';
export { default as RollField } from './rollField.mjs'; export { default as RollField } from './rollField.mjs';
export { default as MacroField } from './macroField.mjs'; export { default as MacroField } from './macroField.mjs';

View file

@ -1,6 +1,13 @@
import BeastformDialog from '../../../applications/dialogs/beastformDialog.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
export default class BeastformField extends fields.SchemaField { export default class BeastformField extends fields.SchemaField {
/**
* Action Workflow order
*/
static order = 90;
constructor(options = {}, context = {}) { constructor(options = {}, context = {}) {
const beastformFields = { const beastformFields = {
tierAccess: new fields.SchemaField({ tierAccess: new fields.SchemaField({
@ -27,4 +34,96 @@ export default class BeastformField extends fields.SchemaField {
}; };
super(beastformFields, options, context); super(beastformFields, options, context);
} }
/**
* Beastform Transformation Action Workflow part.
* Must be called within Action context or similar.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
*/
static async execute(config) {
// Should not be useful anymore here
await BeastformField.handleActiveTransformations.call(this);
const { selected, evolved, hybrid } = await BeastformDialog.configure(config, this.item);
if (!selected) return false;
return await BeastformField.transform.call(this, selected, evolved, hybrid);
}
/**
* Update Action Workflow config object.
* Must be called within Action context.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
*/
prepareConfig(config) {
if (this.actor.effects.find(x => x.type === 'beastform')) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.beastformAlreadyApplied'));
return false;
}
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
const actorLevel = this.actor.system.levelData.level.current;
const actorTier =
Object.values(settingsTiers).find(
tier => actorLevel >= tier.levels.start && actorLevel <= tier.levels.end
) ?? 1;
config.tierLimit = this.beastform.tierAccess.exact ?? actorTier;
}
/**
* TODO by Harry
* @param {*} selectedForm
* @param {*} evolvedData
* @param {*} hybridData
* @returns
*/
static async transform(selectedForm, evolvedData, hybridData) {
const formData = evolvedData?.form ? evolvedData.form.toObject() : selectedForm;
const beastformEffect = formData.effects.find(x => x.type === 'beastform');
if (!beastformEffect) {
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
return false;
}
if (evolvedData?.form) {
const evolvedForm = selectedForm.effects.find(x => x.type === 'beastform');
if (!evolvedForm) {
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
return false;
}
beastformEffect.changes = [...beastformEffect.changes, ...evolvedForm.changes];
formData.system.features = [...formData.system.features, ...selectedForm.system.features.map(x => x.uuid)];
}
if (selectedForm.system.beastformType === CONFIG.DH.ITEM.beastformTypes.hybrid.id) {
formData.system.advantageOn = Object.values(hybridData.advantages).reduce((advantages, formCategory) => {
Object.keys(formCategory).forEach(advantageKey => {
advantages[advantageKey] = formCategory[advantageKey];
});
return advantages;
}, {});
formData.system.features = [
...formData.system.features,
...Object.values(hybridData.features).flatMap(x => Object.keys(x))
];
}
this.actor.createEmbeddedDocuments('Item', [formData]);
}
/**
* Remove existing beastform effect and return true if there was one
* @returns {boolean}
*/
static async handleActiveTransformations() {
const beastformEffects = this.actor.effects.filter(x => x.type === 'beastform');
const existingEffects = beastformEffects.length > 0;
await this.actor.deleteEmbeddedDocuments(
'ActiveEffect',
beastformEffects.map(x => x.id)
);
return existingEffects;
}
} }

View file

@ -1,6 +1,12 @@
const fields = foundry.data.fields; const fields = foundry.data.fields;
export default class CostField extends fields.ArrayField { export default class CostField extends fields.ArrayField {
/**
* Action Workflow order
*/
static order = 150;
/** @inheritDoc */
constructor(options = {}, context = {}) { constructor(options = {}, context = {}) {
const element = new fields.SchemaField({ const element = new fields.SchemaField({
key: new fields.StringField({ key: new fields.StringField({
@ -8,7 +14,7 @@ export default class CostField extends fields.ArrayField {
required: true, required: true,
initial: 'hope' initial: 'hope'
}), }),
keyIsID: new fields.BooleanField(), itemId: new fields.StringField({ nullable: true, initial: null }),
value: new fields.NumberField({ nullable: true, initial: 1, min: 0 }), value: new fields.NumberField({ nullable: true, initial: 1, min: 0 }),
scalable: new fields.BooleanField({ initial: false }), scalable: new fields.BooleanField({ initial: false }),
step: new fields.NumberField({ nullable: true, initial: null }), step: new fields.NumberField({ nullable: true, initial: null }),
@ -20,18 +26,88 @@ export default class CostField extends fields.ArrayField {
super(element, options, context); super(element, options, context);
} }
static prepareConfig(config) { /**
* Cost Consumption Action Workflow part.
* Consume configured action resources.
* Must be called within Action context or similar.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @param {boolean} [successCost=false] Consume only resources configured as "On Success only" if not already consumed.
*/
static async execute(config, successCost = false) {
const actor = this.actor.system.partner ?? this.actor,
usefulResources = {
...foundry.utils.deepClone(actor.system.resources),
fear: {
value: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
max: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear,
reversed: false
}
};
if (this.parent?.parent) {
for (var cost of config.costs) {
if (cost.itemId) {
usefulResources[cost.key] = {
value: cost.value,
target: this.parent.parent,
itemId: cost.itemId
};
}
}
}
const resources = CostField.getRealCosts(config.costs)
.filter(
c =>
(!successCost && (!c.consumeOnSuccess || config.roll?.success)) ||
(successCost && c.consumeOnSuccess)
)
.reduce((a, c) => {
const resource = usefulResources[c.key];
if (resource) {
a.push({
key: c.key,
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
target: resource.target,
itemId: resource.itemId
});
return a;
}
}, []);
await actor.modifyResource(resources);
}
/**
* Update Action Workflow config object.
* Must be called within Action context or similar.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @returns {boolean} Return false if fast-forwarded and no more uses.
*/
prepareConfig(config) {
const costs = this.cost?.length ? foundry.utils.deepClone(this.cost) : []; const costs = this.cost?.length ? foundry.utils.deepClone(this.cost) : [];
config.costs = CostField.calcCosts.call(this, costs); config.costs = CostField.calcCosts.call(this, costs);
const hasCost = CostField.hasCost.call(this, config.costs); const hasCost = CostField.hasCost.call(this, config.costs);
if (config.isFastForward && !hasCost) if (config.dialog.configure === false && !hasCost) {
return ui.notifications.warn("You don't have the resources to use that action."); ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.insufficientResources'));
return hasCost; return hasCost;
}
} }
/**
*
* Must be called within Action context.
* @param {*} costs
* @returns
*/
static calcCosts(costs) { static calcCosts(costs) {
const resources = CostField.getResources.call(this, costs); const resources = CostField.getResources.call(this, costs);
return costs.map(c => { let filteredCosts = costs;
if (this.parent?.metadata.isQuantifiable && this.parent.consumeOnUse === false) {
filteredCosts = filteredCosts.filter(c => c.key !== 'quantity');
}
return filteredCosts.map(c => {
c.scale = c.scale ?? 0; c.scale = c.scale ?? 0;
c.step = c.step ?? 1; c.step = c.step ?? 1;
c.total = c.value + c.scale * c.step; c.total = c.value + c.scale * c.step;
@ -40,13 +116,19 @@ export default class CostField extends fields.ArrayField {
c.key === 'fear' c.key === 'fear'
? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) ? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear)
: resources[c.key].isReversed : resources[c.key].isReversed
? resources[c.key].max ? resources[c.key].max - resources[c.key].value
: resources[c.key].value; : resources[c.key].value;
if (c.scalable) c.maxStep = Math.floor((c.max - c.value) / c.step); if (c.scalable) c.maxStep = Math.floor((c.max - c.value) / c.step);
return c; return c;
}); });
} }
/**
* Check if the current Actor currently has all needed resources.
* Must be called within Action context.
* @param {*} costs
* @returns {boolean}
*/
static hasCost(costs) { static hasCost(costs) {
const realCosts = CostField.getRealCosts.call(this, costs), const realCosts = CostField.getRealCosts.call(this, costs),
hasFearCost = realCosts.findIndex(c => c.key === 'fear'); hasFearCost = realCosts.findIndex(c => c.key === 'fear');
@ -73,17 +155,20 @@ export default class CostField extends fields.ArrayField {
); );
} }
/**
* Get all Actor resources + parent Item potential one.
* Must be called within Action context.
* @param {*} costs
* @returns
*/
static getResources(costs) { static getResources(costs) {
const actorResources = foundry.utils.deepClone(this.actor.system.resources); const actorResources = foundry.utils.deepClone(this.actor.system.resources);
if (this.actor.system.partner) if (this.actor.system.partner)
actorResources.hope = foundry.utils.deepClone(this.actor.system.partner.system.resources.hope); actorResources.hope = foundry.utils.deepClone(this.actor.system.partner.system.resources.hope);
const itemResources = {}; const itemResources = {};
for (let itemResource of costs) { for (let itemResource of costs) {
if (itemResource.keyIsID) { if (itemResource.itemId) {
itemResources[itemResource.key] = { itemResources[itemResource.key] = CostField.getItemIdCostResource.bind(this)(itemResource);
value: this.parent.resource.value ?? 0,
max: CostField.formatMax.call(this, this.parent?.resource?.max)
};
} }
} }
@ -93,8 +178,48 @@ export default class CostField extends fields.ArrayField {
}; };
} }
static getItemIdCostResource(itemResource) {
switch (itemResource.key) {
case CONFIG.DH.GENERAL.itemAbilityCosts.resource.id:
return {
value: this.parent.resource.value ?? 0,
max: CostField.formatMax.call(this, this.parent?.resource?.max)
};
case CONFIG.DH.GENERAL.itemAbilityCosts.quantity.id:
return {
value: this.parent.quantity ?? 0,
max: this.parent.quantity ?? 0
};
default:
return { value: 0, max: 0 };
}
}
static getItemIdCostUpdate(r) {
switch (r.key) {
case CONFIG.DH.GENERAL.itemAbilityCosts.resource.id:
return {
path: 'system.resource.value',
value: r.target.system.resource.value + r.value
};
case CONFIG.DH.GENERAL.itemAbilityCosts.quantity.id:
return {
path: 'system.quantity',
value: r.target.system.quantity + r.value
};
default:
return { path: '', value: undefined };
}
}
/**
*
* @param {*} costs
* @returns
*/
static getRealCosts(costs) { static getRealCosts(costs) {
const realCosts = costs?.length ? costs.filter(c => c.enabled) : []; const cloneCosts = foundry.utils.deepClone(costs),
realCosts = cloneCosts?.length ? cloneCosts.filter(c => c.enabled) : [];
let mergedCosts = []; let mergedCosts = [];
realCosts.forEach(c => { realCosts.forEach(c => {
const getCost = Object.values(mergedCosts).find(gc => gc.key === c.key); const getCost = Object.values(mergedCosts).find(gc => gc.key === c.key);
@ -104,6 +229,12 @@ export default class CostField extends fields.ArrayField {
return mergedCosts; return mergedCosts;
} }
/**
* Format scalable max cost, inject Action datas if it's a formula.
* Must be called within Action context.
* @param {number|string} max Configured maximum for that resource.
* @returns {number} The max cost value.
*/
static formatMax(max) { static formatMax(max) {
max ??= 0; max ??= 0;
if (isNaN(max)) { if (isNaN(max)) {

View file

@ -1,18 +1,210 @@
import FormulaField from '../formulaField.mjs'; import FormulaField from '../formulaField.mjs';
import { setsEqual } from '../../../helpers/utils.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
export default class DamageField extends fields.SchemaField { export default class DamageField extends fields.SchemaField {
/**
* Action Workflow order
*/
static order = 20;
/** @inheritDoc */
constructor(options, context = {}) { constructor(options, context = {}) {
const damageFields = { const damageFields = {
parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)), parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)),
includeBase: new fields.BooleanField({ includeBase: new fields.BooleanField({
initial: false, initial: false,
label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label' label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label'
}) }),
direct: new fields.BooleanField({ initial: false, label: 'DAGGERHEART.CONFIG.DamageType.direct.name' })
}; };
super(damageFields, options, context); super(damageFields, options, context);
} }
/**
* Roll Damage/Healing Action Workflow part.
* Must be called within Action context or similar.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @param {string} [messageId=null] ChatMessage Id where the clicked button belong.
* @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example.
*/
static async execute(config, messageId = null, force = false) {
if (!this.hasDamage && !this.hasHealing) return;
if (
this.hasRoll &&
DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.never.id &&
!force
)
return;
let formulas = this.damage.parts.map(p => ({
formula: DamageField.getFormulaValue.call(this, p, config).getFormula(this.actor),
damageTypes: p.applyTo === 'hitPoints' && !p.type.size ? new Set(['physical']) : p.type,
applyTo: p.applyTo
}));
if (!formulas.length) return false;
formulas = DamageField.formatFormulas.call(this, formulas, config);
const damageConfig = {
...config,
roll: formulas,
dialog: {},
data: this.getRollData()
};
delete damageConfig.evaluate;
if (DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id)
damageConfig.dialog.configure = false;
if (config.hasSave) config.onSave = damageConfig.onSave = this.save.damageMod;
damageConfig.source.message = config.message?._id ?? messageId;
damageConfig.directDamage = !!damageConfig.source?.message;
// if(damageConfig.source?.message && game.modules.get('dice-so-nice')?.active)
// await game.dice3d.waitFor3DAnimationByMessageID(damageConfig.source.message);
const damageResult = await CONFIG.Dice.daggerheart.DamageRoll.build(damageConfig);
if (!damageResult) return false;
config.damage = damageResult.damage;
config.message ??= damageConfig.message;
}
/**
* Apply Damage/Healing Action Worflow part.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @param {*[]} targets Arrays of targets to bypass pre-selected ones.
* @param {boolean} force If the method should be executed outside of Action workflow, for ChatMessage button for example.
*/
static async applyDamage(config, targets = null, force = false) {
targets ??= config.targets.filter(target => target.hit);
if (!config.damage || !targets?.length || (!DamageField.getApplyAutomation() && !force)) return;
const targetDamage = [];
const damagePromises = [];
for (let target of targets) {
const actor = foundry.utils.fromUuidSync(target.actorId);
if (!actor) continue;
if (!config.hasHealing && config.onSave && target.saved?.success === true) {
const mod = CONFIG.DH.ACTIONS.damageOnSave[config.onSave]?.mod ?? 1;
Object.entries(config.damage).forEach(([k, v]) => {
v.total = 0;
v.parts.forEach(part => {
part.total = Math.ceil(part.total * mod);
v.total += part.total;
});
});
}
const token = game.scenes.find(x => x.active).tokens.find(x => x.id === target.id);
if (config.hasHealing)
damagePromises.push(
actor.takeHealing(config.damage).then(updates => targetDamage.push({ token, updates }))
);
else
damagePromises.push(
actor
.takeDamage(config.damage, config.isDirect)
.then(updates => targetDamage.push({ token, updates }))
);
}
Promise.all(damagePromises).then(async _ => {
const summaryMessageSettings = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Automation
).summaryMessages;
if (!summaryMessageSettings.damage) return;
const cls = getDocumentClass('ChatMessage');
const msg = {
type: 'systemMessage',
user: game.user.id,
speaker: cls.getSpeaker(),
title: game.i18n.localize(
`DAGGERHEART.UI.Chat.damageSummary.${config.hasHealing ? 'healingTitle' : 'title'}`
),
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/damageSummary.hbs',
{
targets: targetDamage
}
)
};
cls.create(msg);
});
}
/**
* Return value or valueAlt from damage part
* Must be called within Action context or similar.
* @param {object} part Damage Part
* @param {object} data Action getRollData
* @returns Formula value object
*/
static getFormulaValue(part, data) {
let formulaValue = part.value;
if (data.hasRoll && part.resultBased && data.roll.result.duality === -1) return part.valueAlt;
const isAdversary = this.actor.type === 'adversary';
if (isAdversary && this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) {
const hasHordeDamage = this.actor.effects.find(x => x.type === 'horde');
if (hasHordeDamage && !hasHordeDamage.disabled) return part.valueAlt;
}
return formulaValue;
}
/**
* Prepare formulas for Damage Roll
* Must be called within Action context or similar.
* @param {object[]} formulas Array of formatted formulas object
* @param {object} data Action getRollData
* @returns
*/
static formatFormulas(formulas, data) {
const formattedFormulas = [];
formulas.forEach(formula => {
if (isNaN(formula.formula))
formula.formula = Roll.replaceFormulaData(formula.formula, this.getRollData(data));
const same = formattedFormulas.find(
f => setsEqual(f.damageTypes, formula.damageTypes) && f.applyTo === formula.applyTo
);
if (same) same.formula += ` + ${formula.formula}`;
else formattedFormulas.push(formula);
});
return formattedFormulas;
}
/**
* Return the automation setting for execute method for current user role
* @returns {string} Id from settingsConfig.mjs actionAutomationChoices
*/
static getAutomation() {
return (
(game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.gm) ||
(!game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.players)
);
}
/**
* Return the automation setting for applyDamage method for current user role
* @returns {boolean} If applyDamage should be triggered automatically
*/
static getApplyAutomation() {
return (
(game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.gm) ||
(!game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.players)
);
}
} }
export class DHActionDiceData extends foundry.abstract.DataModel { export class DHActionDiceData extends foundry.abstract.DataModel {
@ -22,14 +214,26 @@ export class DHActionDiceData extends foundry.abstract.DataModel {
multiplier: new fields.StringField({ multiplier: new fields.StringField({
choices: CONFIG.DH.GENERAL.multiplierTypes, choices: CONFIG.DH.GENERAL.multiplierTypes,
initial: 'prof', initial: 'prof',
label: 'Multiplier' label: 'DAGGERHEART.ACTIONS.Config.damage.multiplier',
nullable: false,
required: true
}), }),
flatMultiplier: new fields.NumberField({ nullable: true, initial: 1, label: 'Flat Multiplier' }), flatMultiplier: new fields.NumberField({
dice: new fields.StringField({ choices: CONFIG.DH.GENERAL.diceTypes, initial: 'd6', label: 'Dice' }), nullable: true,
bonus: new fields.NumberField({ nullable: true, initial: null, label: 'Bonus' }), initial: 1,
label: 'DAGGERHEART.ACTIONS.Config.damage.flatMultiplier'
}),
dice: new fields.StringField({
choices: CONFIG.DH.GENERAL.diceTypes,
initial: 'd6',
label: 'DAGGERHEART.GENERAL.Dice.single',
nullable: false,
required: true
}),
bonus: new fields.NumberField({ nullable: true, initial: null, label: 'DAGGERHEART.GENERAL.bonus' }),
custom: new fields.SchemaField({ custom: new fields.SchemaField({
enabled: new fields.BooleanField({ label: 'Custom Formula' }), enabled: new fields.BooleanField({ label: 'DAGGERHEART.ACTIONS.Config.general.customFormula' }),
formula: new FormulaField({ label: 'Formula', initial: "" }) formula: new FormulaField({ label: 'DAGGERHEART.ACTIONS.Config.general.formula', initial: '' })
}) })
}; };
} }

View file

@ -1,6 +1,14 @@
import { emitAsGM, GMUpdateEvent } from '../../../systemRegistration/socket.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
export default class EffectsField extends fields.ArrayField { export default class EffectsField extends fields.ArrayField {
/**
* Action Workflow order
*/
static order = 100;
/** @inheritDoc */
constructor(options = {}, context = {}) { constructor(options = {}, context = {}) {
const element = new fields.SchemaField({ const element = new fields.SchemaField({
_id: new fields.DocumentIdField(), _id: new fields.DocumentIdField(),
@ -8,4 +16,116 @@ export default class EffectsField extends fields.ArrayField {
}); });
super(element, options, context); super(element, options, context);
} }
/**
* Apply Effects Action Workflow part.
* Must be called within Action context or similar.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @param {object[]} [targets=null] Array of targets to override pre-selected ones.
* @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example.
*/
static async execute(config, targets = null, force = false) {
if (!config.hasEffect) return;
let message = config.message ?? ui.chat.collection.get(config.parent?._id);
if (!message) {
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
roll._evaluated = true;
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
}
if (EffectsField.getAutomation() || force) {
targets ??= (message.system?.targets ?? config.targets).filter(t => !config.hasRoll || t.hit);
await emitAsGM(GMUpdateEvent.UpdateEffect, EffectsField.applyEffects.bind(this), targets, this.uuid);
// EffectsField.applyEffects.call(this, config.targets.filter(t => !config.hasRoll || t.hit));
}
}
/**
* Apply Action Effects to a list of Targets
* Must be called within Action context or similar.
* @param {object[]} targets Array of formatted targets
*/
static async applyEffects(targets) {
if (!this.effects?.length || !targets?.length) return;
let effects = this.effects;
const messageTargets = [];
targets.forEach(async baseToken => {
if (this.hasSave && token.saved.success === true) effects = this.effects.filter(e => e.onSave === true);
if (!effects.length) return;
const token = canvas.tokens.get(baseToken.id);
if (!token) return;
messageTargets.push(token.document);
effects.forEach(async e => {
const effect = this.item.effects.get(e._id);
if (!token.actor || !effect) return;
await EffectsField.applyEffect(effect, token.actor);
});
});
if (messageTargets.length === 0) return;
const summaryMessageSettings = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Automation
).summaryMessages;
if (!summaryMessageSettings.effects) return;
const cls = getDocumentClass('ChatMessage');
const msg = {
type: 'systemMessage',
user: game.user.id,
speaker: cls.getSpeaker(),
title: game.i18n.localize('DAGGERHEART.UI.Chat.effectSummary.title'),
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/effectSummary.hbs',
{
effects: this.effects.map(e => this.item.effects.get(e._id)),
targets: messageTargets
}
)
};
cls.create(msg);
}
/**
* Apply an Effect to a target or enable it if already on it
* @param {object} effect Effect object containing ActiveEffect UUID
* @param {object} actor Actor Document
*/
static async applyEffect(effect, actor) {
const existingEffect = actor.effects.find(e => e.origin === effect.uuid);
if (existingEffect) {
return effect.update(
foundry.utils.mergeObject({
...effect.constructor.getInitialDuration(),
disabled: false
})
);
}
// Otherwise, create a new effect on the target
const effectData = foundry.utils.mergeObject({
...effect.toObject(),
disabled: false,
transfer: false,
origin: effect.uuid
});
await ActiveEffect.implementation.create(effectData, { parent: actor });
}
/**
* Return the automation setting for execute method for current user role
* @returns {boolean} If execute should be triggered automatically
*/
static getAutomation() {
return (
(game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.gm) ||
(!game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.players)
);
}
} }

View file

@ -1,12 +0,0 @@
import { DHDamageData } from './damageField.mjs';
const fields = foundry.data.fields;
export default class HealingField extends fields.SchemaField {
constructor(options, context = {}) {
const healingFields = {
parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData))
};
super(healingFields, options, context);
}
}

View file

@ -1,7 +1,29 @@
const fields = foundry.data.fields; const fields = foundry.data.fields;
export default class MacroField extends fields.DocumentUUIDField { export default class MacroField extends fields.DocumentUUIDField {
/**
* Action Workflow order
*/
static order = 70;
/** @inheritDoc */
constructor(context = {}) { constructor(context = {}) {
super({ type: "Macro" }, context); super({ type: 'Macro' }, context);
}
/**
* Macro Action Workflow part.
* Must be called within Action context or similar or similar.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. Currently not used.
*/
static async execute(config) {
const fixUUID = !this.macro.includes('Macro.') ? `Macro.${this.macro}` : this.macro,
macro = await fromUuid(fixUUID);
try {
if (!macro) throw new Error(`No macro found for the UUID: ${this.macro}.`);
macro.execute();
} catch (error) {
ui.notifications.error(error);
}
} }
} }

View file

@ -1,17 +1,23 @@
const fields = foundry.data.fields; const fields = foundry.data.fields;
export default class RangeField extends fields.StringField { export default class RangeField extends fields.StringField {
/** @inheritDoc */
constructor(context = {}) { constructor(context = {}) {
const options = { const options = {
choices: CONFIG.DH.GENERAL.range, choices: CONFIG.DH.GENERAL.range,
required: false, required: false,
blank: true, blank: true,
label: "DAGGERHEART.GENERAL.range" label: 'DAGGERHEART.GENERAL.range'
}; };
super(options, context); super(options, context);
} }
static prepareConfig(config) { /**
return true; * Update Action Workflow config object.
* NOT YET IMPLEMENTED.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
*/
prepareConfig(config) {
return;
} }
} }

View file

@ -5,18 +5,27 @@ export class DHActionRollData extends foundry.abstract.DataModel {
static defineSchema() { static defineSchema() {
return { return {
type: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.GENERAL.rollTypes }), type: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.GENERAL.rollTypes }),
trait: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.ACTOR.abilities, label: "DAGGERHEART.GENERAL.Trait.single" }), trait: new fields.StringField({
nullable: true,
initial: null,
choices: CONFIG.DH.ACTOR.abilities,
label: 'DAGGERHEART.GENERAL.Trait.single'
}),
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }), difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
bonus: new fields.NumberField({ nullable: true, initial: null, integer: true }), bonus: new fields.NumberField({ nullable: true, initial: null, integer: true }),
advState: new fields.StringField({ advState: new fields.StringField({
choices: CONFIG.DH.ACTIONS.advantageState, choices: CONFIG.DH.ACTIONS.advantageState,
initial: 'neutral' initial: 'neutral',
nullable: false,
required: true
}), }),
diceRolling: new fields.SchemaField({ diceRolling: new fields.SchemaField({
multiplier: new fields.StringField({ multiplier: new fields.StringField({
choices: CONFIG.DH.GENERAL.diceSetNumbers, choices: CONFIG.DH.GENERAL.diceSetNumbers,
initial: 'prof', initial: 'prof',
label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.multiplier' label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.multiplier',
nullable: false,
required: true
}), }),
flatMultiplier: new fields.NumberField({ flatMultiplier: new fields.NumberField({
nullable: true, nullable: true,
@ -26,7 +35,9 @@ export class DHActionRollData extends foundry.abstract.DataModel {
dice: new fields.StringField({ dice: new fields.StringField({
choices: CONFIG.DH.GENERAL.diceTypes, choices: CONFIG.DH.GENERAL.diceTypes,
initial: CONFIG.DH.GENERAL.diceTypes.d6, initial: CONFIG.DH.GENERAL.diceTypes.d6,
label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.dice' label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.dice',
nullable: false,
required: true
}), }),
compare: new fields.StringField({ compare: new fields.StringField({
choices: CONFIG.DH.ACTIONS.diceCompare, choices: CONFIG.DH.ACTIONS.diceCompare,
@ -71,29 +82,6 @@ export class DHActionRollData extends foundry.abstract.DataModel {
const modifiers = []; const modifiers = [];
if (!this.parent?.actor) return modifiers; if (!this.parent?.actor) return modifiers;
switch (this.parent.actor.type) { switch (this.parent.actor.type) {
case 'character':
const spellcastingTrait =
this.type === 'spellcast'
? (this.parent.actor?.system?.spellcastModifierTrait?.key ?? 'agility')
: null;
const trait =
this.useDefault || !this.trait
? (spellcastingTrait ?? this.parent.item.system.attack?.roll?.trait ?? 'agility')
: this.trait;
if (
this.type === CONFIG.DH.GENERAL.rollTypes.attack.id ||
this.type === CONFIG.DH.GENERAL.rollTypes.trait.id
)
modifiers.push({
label: `DAGGERHEART.CONFIG.Traits.${trait}.name`,
value: this.parent.actor.system.traits[trait].value
});
else if (this.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id)
modifiers.push({
label: `DAGGERHEART.CONFIG.RollTypes.spellcast.name`,
value: this.parent.actor.system.spellcastModifier
});
break;
case 'companion': case 'companion':
case 'adversary': case 'adversary':
if (this.type === CONFIG.DH.GENERAL.rollTypes.attack.id) if (this.type === CONFIG.DH.GENERAL.rollTypes.attack.id)
@ -107,10 +95,79 @@ export class DHActionRollData extends foundry.abstract.DataModel {
} }
return modifiers; return modifiers;
} }
get rollTrait() {
if (this.parent?.actor?.type !== 'character') return null;
switch (this.type) {
case CONFIG.DH.GENERAL.rollTypes.spellcast.id:
return this.parent.actor?.system?.spellcastModifierTrait?.key ?? 'agility';
case CONFIG.DH.GENERAL.rollTypes.attack.id:
case CONFIG.DH.GENERAL.rollTypes.trait.id:
return this.useDefault || !this.trait
? (this.parent.item.system.attack?.roll?.trait ?? 'agility')
: this.trait;
default:
return null;
}
}
} }
export default class RollField extends fields.EmbeddedDataField { export default class RollField extends fields.EmbeddedDataField {
/**
* Action Workflow order
*/
static order = 10;
/** @inheritDoc */
constructor(options, context = {}) { constructor(options, context = {}) {
super(DHActionRollData, options, context); super(DHActionRollData, options, context);
} }
/**
* Roll Action Workflow part.
* Must be called within Action context or similar.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
*/
static async execute(config) {
if (!config.hasRoll) return;
config = await this.actor.diceRoll(config);
if (!config) return false;
}
/**
* Update Action Workflow config object.
* Must be called within Action context.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
*/
prepareConfig(config) {
if (!config.hasRoll) return;
config.dialog.configure = RollField.getAutomation() ? !config.dialog.configure : config.dialog.configure;
const roll = {
baseModifiers: this.roll.getModifier(),
label: 'Attack',
type: this.roll?.type,
trait: this.roll?.rollTrait,
difficulty: this.roll?.difficulty,
formula: this.roll.getFormula(),
advantage: CONFIG.DH.ACTIONS.advantageState[this.roll.advState].value
};
if (this.roll.type === 'diceSet' || !this.hasRoll) roll.lite = true;
config.roll = roll;
}
/**
* Return the automation setting for execute method for current user role
* @returns {boolean} If execute should be triggered automatically
*/
static getAutomation() {
return (
(game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.gm) ||
(!game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.players)
);
}
} }

View file

@ -1,6 +1,14 @@
import { abilities } from '../../../config/actorConfig.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
export default class SaveField extends fields.SchemaField { export default class SaveField extends fields.SchemaField {
/**
* Action Workflow order
*/
static order = 50;
/** @inheritDoc */
constructor(options = {}, context = {}) { constructor(options = {}, context = {}) {
const saveFields = { const saveFields = {
trait: new fields.StringField({ trait: new fields.StringField({
@ -11,9 +19,164 @@ export default class SaveField extends fields.SchemaField {
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }), difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
damageMod: new fields.StringField({ damageMod: new fields.StringField({
initial: CONFIG.DH.ACTIONS.damageOnSave.none.id, initial: CONFIG.DH.ACTIONS.damageOnSave.none.id,
choices: CONFIG.DH.ACTIONS.damageOnSave choices: CONFIG.DH.ACTIONS.damageOnSave,
nullable: false,
required: true
}) })
}; };
super(saveFields, options, context); super(saveFields, options, context);
} }
/**
* Reaction Roll Action Workflow part.
* Must be called within Action context or similar.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @param {object[]} [targets=null] Array of targets to override pre-selected ones.
* @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example.
*/
static async execute(config, targets = null, force = false) {
if (!config.hasSave) return;
let message = config.message ?? ui.chat.collection.get(config.parent?._id);
if (!message) {
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
roll._evaluated = true;
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
}
if (SaveField.getAutomation() !== CONFIG.DH.SETTINGS.actionAutomationChoices.never.id || force) {
targets ??= config.targets.filter(t => !config.hasRoll || t.hit);
await SaveField.rollAllSave.call(this, targets, config.event, message);
} else return false;
}
/**
* Roll a Reaction Roll for all targets. Send a query to the owner if the User is not.
* Must be called within Action context.
* @param {object[]} targets Array of formatted targets.
* @param {Event} event Triggering event
* @param {ChatMessage} message The ChatMessage the triggered button comes from.
*/
static async rollAllSave(targets, event, message) {
if (!targets) return;
return new Promise(resolve => {
const aPromise = [];
targets.forEach(target => {
aPromise.push(
new Promise(async subResolve => {
const actor = fromUuidSync(target.actorId);
if (actor) {
const rollSave =
game.user === actor.owner
? SaveField.rollSave.call(this, actor, event)
: actor.owner.query('reactionRoll', {
actionId: this.uuid,
actorId: actor.uuid,
event,
message
});
const result = await rollSave;
await SaveField.updateSaveMessage.call(this, result, message, target.id);
subResolve();
} else subResolve();
})
);
});
Promise.all(aPromise).then(result => resolve());
});
}
/**
* Roll a Reaction Roll for the specified Actor against the Action difficulty.
* Must be called within Action context.
* @param {*} actor Actor document
* @param {Event} event Triggering event
* @returns {object} Actor diceRoll config result.
*/
static async rollSave(actor, event) {
if (!actor) return;
const title = actor.isNPC
? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll')
: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(abilities[this.save.trait]?.label)
}),
rollConfig = {
event,
title,
roll: {
trait: this.save.trait,
difficulty: this.save.difficulty ?? this.actor?.baseSaveDifficulty,
type: 'trait'
},
actionType: 'reaction',
hasRoll: true,
data: actor.getRollData()
};
if (SaveField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id)
rollConfig.dialog = { configure: false };
return actor.diceRoll(rollConfig);
}
/**
* Update a Roll ChatMessage for a token according to his Reaction Roll result.
* @param {object} result Result from the Reaction Roll
* @param {object} message ChatMessage to update
* @param {string} targetId Token ID
*/
static async updateSaveMessage(result, message, targetId) {
if (!result) return;
const updateMsg = async function (message, targetId, result) {
// setTimeout(async () => {
const chatMessage = ui.chat.collection.get(message._id),
changes = {
flags: {
[game.system.id]: {
reactionRolls: {
[targetId]: {
result: result.roll.total,
success: result.roll.success
}
}
}
}
};
await chatMessage.update(changes);
// }, 100);
};
if (game.modules.get('dice-so-nice')?.active)
game.dice3d
.waitFor3DAnimationByMessageID(result.message.id ?? result.message._id)
.then(async () => await updateMsg(message, targetId, result));
else await updateMsg(message, targetId, result);
}
/**
* Return the automation setting for execute method for current user role
* @returns {string} Id from settingsConfig.mjs actionAutomationChoices
*/
static getAutomation() {
return (
(game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.gm) ||
(!game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.players)
);
}
/**
* Send a query to an Actor owner to roll a Reaction Roll then send back the result.
* @param {object} param0
* @param {string} param0.actionId Action ID
* @param {string} param0.actorId Actor ID
* @param {Event} param0.event Triggering event
* @param {ChatMessage} param0.message Chat Message to update
* @returns
*/
static rollSaveQuery({ actionId, actorId, event, message }) {
return new Promise(async (resolve, reject) => {
const actor = await fromUuid(actorId),
action = await fromUuid(actionId);
if (!actor || !actor?.isOwner) reject();
SaveField.rollSave.call(action, actor, event, message).then(result => resolve(result));
});
}
} }

View file

@ -1,56 +1,80 @@
const fields = foundry.data.fields; const fields = foundry.data.fields;
export default class TargetField extends fields.SchemaField { export default class TargetField extends fields.SchemaField {
/** @inheritDoc */
constructor(options = {}, context = {}) { constructor(options = {}, context = {}) {
const targetFields = { const targetFields = {
type: new fields.StringField({ type: new fields.StringField({
choices: CONFIG.DH.GENERAL.targetTypes, choices: CONFIG.DH.GENERAL.targetTypes,
initial: CONFIG.DH.GENERAL.targetTypes.any.id, initial: CONFIG.DH.GENERAL.targetTypes.any.id,
nullable: true nullable: true,
blank: true
}), }),
amount: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }) amount: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 })
}; };
super(targetFields, options, context); super(targetFields, options, context);
} }
static prepareConfig(config) { /**
if (!this.target?.type) return []; * Update Action Workflow config object.
* Must be called within Action context.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
*/
prepareConfig(config) {
if (!this.target?.type) return (config.targets = []);
config.hasTarget = true; config.hasTarget = true;
let targets; let targets;
// If the Action is configured as self-targeted, set targets as the owner.
if (this.target?.type === CONFIG.DH.GENERAL.targetTypes.self.id) if (this.target?.type === CONFIG.DH.GENERAL.targetTypes.self.id)
targets = [this.actor.token ?? this.actor.prototypeToken]; targets = [this.actor.token ?? this.actor.prototypeToken];
else { else {
targets = Array.from(game.user.targets); targets = Array.from(game.user.targets);
if (this.target.type !== CONFIG.DH.GENERAL.targetTypes.any.id) { if (this.target.type !== CONFIG.DH.GENERAL.targetTypes.any.id) {
targets = targets.filter(t => TargetField.isTargetFriendly.call(this, t)); targets = targets.filter(target => TargetField.isTargetFriendly(this.actor, target, this.target.type));
if (this.target.amount && targets.length > this.target.amount) targets = []; if (this.target.amount && targets.length > this.target.amount) targets = [];
} }
} }
config.targets = targets.map(t => TargetField.formatTarget.call(this, t)); config.targets = targets.map(t => TargetField.formatTarget.call(this, t));
const hasTargets = TargetField.checkTargets.call(this, this.target.amount, config.targets); const hasTargets = TargetField.checkTargets.call(this, this.target.amount, config.targets);
if (config.isFastForward && !hasTargets) if (config.dialog.configure === false && !hasTargets) {
return ui.notifications.warn('Too many targets selected for that actions.'); ui.notifications.warn('Too many targets selected for that actions.');
return hasTargets; return hasTargets;
}
} }
/**
* Check if the number of selected targets respect the amount set in the Action.
* NOT YET IMPLEMENTED. Will be with Target Picker.
* @param {number} amount Max amount of targets configured in the action.
* @param {*[]} targets Array of targeted tokens.
* @returns {boolean} If the amount of targeted tokens does not exceed action configured one.
*/
static checkTargets(amount, targets) { static checkTargets(amount, targets) {
return true; return true;
// return !amount || (targets.length > amount); // return !amount || (targets.length > amount);
} }
static isTargetFriendly(target) { /**
const actorDisposition = this.actor.token * Compare 2 Actors disposition between each other
? this.actor.token.disposition * @param {*} actor First actor document.
: this.actor.prototypeToken.disposition, * @param {*} target Second actor document.
* @param {string} type Disposition id to compare (friendly/hostile).
* @returns {boolean} If both actors respect the provided type.
*/
static isTargetFriendly(actor, target, type) {
const actorDisposition = actor.token ? actor.token.disposition : actor.prototypeToken.disposition,
targetDisposition = target.document.disposition; targetDisposition = target.document.disposition;
return ( return (
(this.target.type === CONFIG.DH.GENERAL.targetTypes.friendly.id && (type === CONFIG.DH.GENERAL.targetTypes.friendly.id && actorDisposition === targetDisposition) ||
actorDisposition === targetDisposition) || (type === CONFIG.DH.GENERAL.targetTypes.hostile.id && actorDisposition + targetDisposition === 0)
(this.target.type === CONFIG.DH.GENERAL.targetTypes.hostile.id &&
actorDisposition + targetDisposition === 0)
); );
} }
/**
* Format actor to useful datas for Action roll workflow.
* @param {*} actor Actor object to format.
* @returns {*} Formatted Actor.
*/
static formatTarget(actor) { static formatTarget(actor) {
return { return {
id: actor.id, id: actor.id,

View file

@ -3,6 +3,12 @@ import FormulaField from '../formulaField.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
export default class UsesField extends fields.SchemaField { export default class UsesField extends fields.SchemaField {
/**
* Action Workflow order
*/
static order = 160;
/** @inheritDoc */
constructor(options = {}, context = {}) { constructor(options = {}, context = {}) {
const usesFields = { const usesFields = {
value: new fields.NumberField({ nullable: true, initial: null }), value: new fields.NumberField({ nullable: true, initial: null }),
@ -20,15 +26,45 @@ export default class UsesField extends fields.SchemaField {
super(usesFields, options, context); super(usesFields, options, context);
} }
static prepareConfig(config) { /**
* Uses Consumption Action Workflow part.
* Increment Action spent uses by 1.
* Must be called within Action context or similar or similar.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @param {boolean} [successCost=false] Consume only resources configured as "On Success only" if not already consumed.
*/
static async execute(config, successCost = false) {
if (
config.uses?.enabled &&
((!successCost && (!config.uses?.consumeOnSuccess || config.roll?.success)) ||
(successCost && config.uses?.consumeOnSuccess))
)
this.update({ 'uses.value': this.uses.value + 1 });
}
/**
* Update Action Workflow config object.
* Must be called within Action context.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @returns {boolean} Return false if fast-forwarded and no more uses.
*/
prepareConfig(config) {
const uses = this.uses?.max ? foundry.utils.deepClone(this.uses) : null; const uses = this.uses?.max ? foundry.utils.deepClone(this.uses) : null;
if (uses && !uses.value) uses.value = 0; if (uses && !uses.value) uses.value = 0;
config.uses = uses; config.uses = uses;
const hasUses = UsesField.hasUses.call(this, config.uses); const hasUses = UsesField.hasUses.call(this, config.uses);
if (config.isFastForward && !hasUses) return ui.notifications.warn("That action doesn't have remaining uses."); if (config.dialog.configure === false && !hasUses) {
return hasUses; ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.actionNoUsesRemaining'));
return hasUses;
}
} }
/**
* Prepare Uses object for Action Workflow
* Must be called within Action context.
* @param {object} uses
* @returns {object}
*/
static calcUses(uses) { static calcUses(uses) {
if (!uses) return null; if (!uses) return null;
return { return {
@ -38,6 +74,12 @@ export default class UsesField extends fields.SchemaField {
}; };
} }
/**
* Check if the Action still get atleast one unspent uses.
* Must be called within Action context.
* @param {*} uses
* @returns {boolean}
*/
static hasUses(uses) { static hasUses(uses) {
if (!uses) return true; if (!uses) return true;
let max = uses.max ?? 0; let max = uses.max ?? 0;

View file

@ -38,4 +38,13 @@ export default class ForeignDocumentUUIDField extends foundry.data.fields.Docume
toObject(value) { toObject(value) {
return value?.uuid ?? value; return value?.uuid ?? value;
} }
/** @override */
_cast(value) {
if (typeof value === 'string') return value;
if (value instanceof foundry.abstract.Document) return value.uuid;
throw new Error(
`The value provided to a ForeignDocumentUUIDField must be a ${foundry.abstract.Document.name} instance or a UUID string.`
);
}
} }

View file

@ -1,5 +1,4 @@
import AttachableItem from './attachableItem.mjs'; import AttachableItem from './attachableItem.mjs';
import { armorFeatures } from '../../config/itemConfig.mjs';
export default class DHArmor extends AttachableItem { export default class DHArmor extends AttachableItem {
/** @inheritDoc */ /** @inheritDoc */
@ -25,7 +24,7 @@ export default class DHArmor extends AttachableItem {
new fields.SchemaField({ new fields.SchemaField({
value: new fields.StringField({ value: new fields.StringField({
required: true, required: true,
choices: CONFIG.DH.ITEM.armorFeatures, choices: CONFIG.DH.ITEM.allArmorFeatures,
blank: true blank: true
}), }),
effectIds: new fields.ArrayField(new fields.StringField({ required: true })), effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
@ -60,13 +59,14 @@ export default class DHArmor extends AttachableItem {
const allowed = await super._preUpdate(changes, options, user); const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false; if (allowed === false) return false;
const changedArmorFeatures = changes.system?.armorFeatures ?? [];
const removedFeatures = this.armorFeatures.filter(x => changedArmorFeatures.every(y => y.value !== x.value));
if (changes.system?.armorFeatures) { if (changes.system?.armorFeatures) {
const removed = this.armorFeatures.filter(x => !changes.system.armorFeatures.includes(x)); const added = changedArmorFeatures.filter(x => this.armorFeatures.every(y => y.value !== x.value));
const added = changes.system.armorFeatures.filter(x => !this.armorFeatures.includes(x));
const effectIds = []; const effectIds = [];
const actionIds = []; const actionIds = [];
for (var feature of removed) { for (var feature of removedFeatures) {
effectIds.push(...feature.effectIds); effectIds.push(...feature.effectIds);
actionIds.push(...feature.actionIds); actionIds.push(...feature.actionIds);
} }
@ -76,8 +76,9 @@ export default class DHArmor extends AttachableItem {
return acc; return acc;
}, {}); }, {});
const allFeatures = CONFIG.DH.ITEM.allArmorFeatures();
for (const feature of added) { for (const feature of added) {
const featureData = armorFeatures[feature.value]; const featureData = allFeatures[feature.value];
if (featureData.effects?.length > 0) { if (featureData.effects?.length > 0) {
const embeddedItems = await this.parent.createEmbeddedDocuments( const embeddedItems = await this.parent.createEmbeddedDocuments(
'ActiveEffect', 'ActiveEffect',
@ -91,7 +92,7 @@ export default class DHArmor extends AttachableItem {
} }
const newActions = {}; const newActions = {};
if (featureData.actions?.length > 0) { if (featureData.actions?.length > 0 || featureData.actions?.size > 0) {
for (let action of featureData.actions) { for (let action of featureData.actions) {
const embeddedEffects = await this.parent.createEmbeddedDocuments( const embeddedEffects = await this.parent.createEmbeddedDocuments(
'ActiveEffect', 'ActiveEffect',
@ -110,10 +111,12 @@ export default class DHArmor extends AttachableItem {
{ {
...cls.getSourceConfig(this), ...cls.getSourceConfig(this),
...action, ...action,
type: action.type,
_id: actionId, _id: actionId,
name: game.i18n.localize(action.name), name: game.i18n.localize(action.name),
description: game.i18n.localize(action.description), description: game.i18n.localize(action.description),
effects: embeddedEffects.map(x => ({ _id: x.id })) effects: embeddedEffects.map(x => ({ _id: x.id })),
systemPath: 'actions'
}, },
{ parent: this } { parent: this }
); );
@ -126,6 +129,16 @@ export default class DHArmor extends AttachableItem {
} }
} }
_onUpdate(a, b, c) {
super._onUpdate(a, b, c);
if (this.actor?.type === 'character') {
for (const party of this.actor.parties) {
party.render();
}
}
}
/** /**
* Generates a list of localized tags based on this item's type-specific properties. * Generates a list of localized tags based on this item's type-specific properties.
* @returns {string[]} An array of localized tag strings. * @returns {string[]} An array of localized tag strings.
@ -145,7 +158,8 @@ export default class DHArmor extends AttachableItem {
*/ */
_getLabels() { _getLabels() {
const labels = []; const labels = [];
if(this.baseScore) labels.push(`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`) if (this.baseScore)
labels.push(`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`);
return labels; return labels;
} }

View file

@ -26,7 +26,8 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
hasResource: false, hasResource: false,
isQuantifiable: false, isQuantifiable: false,
isInventoryItem: false, isInventoryItem: false,
hasActions: false hasActions: false,
hasAttribution: true
}; };
} }
@ -37,7 +38,13 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
/** @inheritDoc */ /** @inheritDoc */
static defineSchema() { static defineSchema() {
const schema = {}; const schema = {
attribution: new fields.SchemaField({
source: new fields.StringField(),
page: new fields.NumberField(),
artist: new fields.StringField()
})
};
if (this.metadata.hasDescription) schema.description = new fields.HTMLField({ required: true, nullable: true }); if (this.metadata.hasDescription) schema.description = new fields.HTMLField({ required: true, nullable: true });
@ -110,6 +117,13 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
return []; return [];
} }
get attributionLabel() {
if (!this.attribution) return;
const { source, page } = this.attribution;
return [source, page ? `pg ${page}.` : null].filter(x => x).join('. ');
}
/** /**
* Obtain a data object used to evaluate any dice rolls associated with this Item Type * Obtain a data object used to evaluate any dice rolls associated with this Item Type
* @param {object} [options] - Options which modify the getRollData method. * @param {object} [options] - Options which modify the getRollData method.
@ -187,6 +201,8 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
super._onUpdate(changed, options, userId); super._onUpdate(changed, options, userId);
updateLinkedItemApps(options, this.parent.sheet); updateLinkedItemApps(options, this.parent.sheet);
createScrollText(this.parent?.parent, options.scrollingTextData);
if (this.parent?.parent && options.scrollingTextData)
this.parent.parent.queueScrollText(options.scrollingTextData);
} }
} }

View file

@ -33,11 +33,13 @@ export default class DHBeastform extends BaseDataItem {
tokenImg: new fields.FilePathField({ tokenImg: new fields.FilePathField({
initial: 'icons/svg/mystery-man.svg', initial: 'icons/svg/mystery-man.svg',
categories: ['IMAGE'], categories: ['IMAGE'],
wildcard: true,
base64: false base64: false
}), }),
tokenRingImg: new fields.FilePathField({ tokenRingImg: new fields.FilePathField({
initial: 'icons/svg/mystery-man.svg', initial: 'icons/svg/mystery-man.svg',
categories: ['IMAGE'], categories: ['IMAGE'],
wildcard: true,
base64: false base64: false
}), }),
tokenSize: new fields.SchemaField({ tokenSize: new fields.SchemaField({
@ -108,6 +110,30 @@ export default class DHBeastform extends BaseDataItem {
}; };
} }
static async getWildcardImage(actor, beastform) {
const usesDynamicToken = actor.prototypeToken.ring.enabled && beastform.system.tokenRingImg;
const tokenPath = usesDynamicToken ? beastform.system.tokenRingImg : beastform.system.tokenImg;
const usesWildcard = tokenPath.includes('*');
if (usesWildcard) {
const filePicker = new foundry.applications.apps.FilePicker.implementation(tokenPath);
const { files } = await foundry.applications.apps.FilePicker.implementation.browse(
filePicker.activeSource,
tokenPath,
{
wildcard: true,
type: 'image'
}
);
const selectedImage = await game.system.api.applications.dialogs.ImageSelectDialog.configure(
game.i18n.localize('DAGGERHEART.APPLICATIONS.ImageSelect.title'),
files
);
return { usesDynamicToken, selectedImage };
}
return null;
}
async _preCreate() { async _preCreate() {
if (!this.actor) return; if (!this.actor) return;

View file

@ -49,6 +49,8 @@ export default class DHClass extends BaseDataItem {
suggestedSecondaryWeapon: new ForeignDocumentUUIDField({ type: 'Item' }), suggestedSecondaryWeapon: new ForeignDocumentUUIDField({ type: 'Item' }),
suggestedArmor: new ForeignDocumentUUIDField({ type: 'Item' }) suggestedArmor: new ForeignDocumentUUIDField({ type: 'Item' })
}), }),
backgroundQuestions: new fields.ArrayField(new fields.StringField(), { initial: ['', '', ''] }),
connections: new fields.ArrayField(new fields.StringField(), { initial: ['', '', ''] }),
isMulticlass: new fields.BooleanField({ initial: false }) isMulticlass: new fields.BooleanField({ initial: false })
}; };
} }
@ -96,6 +98,20 @@ export default class DHClass extends BaseDataItem {
} }
} }
} }
if (!data.system.isMulticlass) {
const addQuestions = (base, questions) => {
return `${base}${questions.map(q => `<p><strong>${q}</strong></p>`).join('<br/>')}`;
};
const backgroundQuestions = data.system.backgroundQuestions.filter(x => x);
const connections = data.system.connections.filter(x => x);
await this.actor.update({
'system.biography': {
background: addQuestions(this.actor.system.biography.background, backgroundQuestions),
connections: addQuestions(this.actor.system.biography.connections, connections)
}
});
}
} }
const allowed = await super._preCreate(data, options, user); const allowed = await super._preCreate(data, options, user);

View file

@ -1,5 +1,4 @@
import BaseDataItem from './base.mjs'; import BaseDataItem from './base.mjs';
import { ActionField } from '../fields/actionField.mjs';
export default class DHConsumable extends BaseDataItem { export default class DHConsumable extends BaseDataItem {
/** @inheritDoc */ /** @inheritDoc */
@ -19,7 +18,8 @@ export default class DHConsumable extends BaseDataItem {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
...super.defineSchema(), ...super.defineSchema(),
consumeOnUse: new fields.BooleanField({ initial: false }) consumeOnUse: new fields.BooleanField({ initial: true }),
destroyOnEmpty: new fields.BooleanField({ initial: true })
}; };
} }
@ -27,5 +27,4 @@ export default class DHConsumable extends BaseDataItem {
/**@override */ /**@override */
static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/items/round-potion.svg'; static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/items/round-potion.svg';
} }

Some files were not shown because too many files have changed in this diff Show more