diff --git a/.editorconfig b/.editorconfig index 6cfef2fc8..aa391e00b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,6 @@ [*] indent_size = 4 indent_style = spaces +end_of_line = lf [*.yml] indent_size = 2 diff --git a/.env.example b/.env.example index 730309d3e..3d62d699f 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,2 @@ FOUNDRY_MAIN_PATH=/path/to/foundry/resources/app/main.js -FOUNDRY_DATA_PATH=/path/to/foundry/data \ No newline at end of file +FOUNDRY_DATA_PATH=/path/to/foundry/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 6de9e5d07..000000000 --- a/.prettierrc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "trailingComma": "none", - "tabWidth": 4, - "useTabs": false, - "semi": true, - "singleQuote": true, - "quoteProps": "consistent", - "bracketSpacing": true, - "arrowParens": "avoid", - "printWidth": 120, - "endOfLine": "lf", - "bracketSameLine": true -} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..b472f3838 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "js/ts.preferGoToSourceDefinition": true +} \ No newline at end of file diff --git a/README.md b/README.md index f59143fd4..177636c7a 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,13 @@ You can find the documentation here: https://github.com/Foundryborne/daggerheart 3. **Configure your Foundry paths:** - ```bash - npm run setup:dev -- --foundry-path="/path/to/foundry/main.js" --data-path="/path/to/data" - ``` + Copy `.env.example` to `.env` and set up the foundry path and data path. The foundry path must point to main.js, and the data path must point the folder containing the Data folder itself (but not the `Data` folder itself). + +4. **Setup symlinks:** -4. **Start developing:** + In a shell with elevated privilages, run `npm run createSymlink`. It will add a foundry folder for types, and add a symlink to the daggerheart folder into your foundry data. + +5. **Start developing:** ```bash npm start ``` @@ -51,7 +53,7 @@ You can find the documentation here: https://github.com/Foundryborne/daggerheart - `npm start` - Start development with file watching and Foundry launching - `npm run build` - One-time build -- `npm run setup:dev -- --foundry-path="" --data-path=""` - Configure development environment +- `npm run createSymlink` - Setup types and output symlinks ### Notes @@ -66,6 +68,10 @@ You can find the documentation here: https://github.com/Foundryborne/daggerheart Looking to contribute to the project? Look no further, check out our [contributing guide](CONTRIBUTING.md), and keep the [Code of Conduct](coc.md) in mind when working on things. +## AI Policy + +The Foundryborne Daggerheart system does not make use of AI (generative or otherwise) for any area of its implementation. We expect all contributors to follow this same policy when contributing with a pull request; contributions made using AI will be rejected outright. + ## Disclaimer: **Daggerheart System** diff --git a/assets/icons/documents/actors/capybara.svg b/assets/icons/documents/actors/capybara.svg index 90deb64a3..8fbe2c06c 100644 --- a/assets/icons/documents/actors/capybara.svg +++ b/assets/icons/documents/actors/capybara.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/actors/dark-squad.svg b/assets/icons/documents/actors/dark-squad.svg index f21b4c5b6..39ca794ad 100644 --- a/assets/icons/documents/actors/dark-squad.svg +++ b/assets/icons/documents/actors/dark-squad.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/actors/dragon-head.svg b/assets/icons/documents/actors/dragon-head.svg index d9e008f50..324ffbbbb 100644 --- a/assets/icons/documents/actors/dragon-head.svg +++ b/assets/icons/documents/actors/dragon-head.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/actors/drama-masks.svg b/assets/icons/documents/actors/drama-masks.svg index 84307da0a..98b9b68b0 100644 --- a/assets/icons/documents/actors/drama-masks.svg +++ b/assets/icons/documents/actors/drama-masks.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/actors/forest.svg b/assets/icons/documents/actors/forest.svg index 8f7117e89..fcdb8a9de 100644 --- a/assets/icons/documents/actors/forest.svg +++ b/assets/icons/documents/actors/forest.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/battered-axe.svg b/assets/icons/documents/items/battered-axe.svg index 5d7be27d9..436884bd8 100644 --- a/assets/icons/documents/items/battered-axe.svg +++ b/assets/icons/documents/items/battered-axe.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/card-play.svg b/assets/icons/documents/items/card-play.svg index 587cb1c13..5c29fc437 100644 --- a/assets/icons/documents/items/card-play.svg +++ b/assets/icons/documents/items/card-play.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/chest-armor.svg b/assets/icons/documents/items/chest-armor.svg index 2cef80a63..03b82d48b 100644 --- a/assets/icons/documents/items/chest-armor.svg +++ b/assets/icons/documents/items/chest-armor.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/family-tree.svg b/assets/icons/documents/items/family-tree.svg index d95c935d7..031f23dad 100644 --- a/assets/icons/documents/items/family-tree.svg +++ b/assets/icons/documents/items/family-tree.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/laurel-crown.svg b/assets/icons/documents/items/laurel-crown.svg index 34a54d2ad..38dbcd041 100644 --- a/assets/icons/documents/items/laurel-crown.svg +++ b/assets/icons/documents/items/laurel-crown.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/laurels.svg b/assets/icons/documents/items/laurels.svg index 2c3cdf636..ae7398ec8 100644 --- a/assets/icons/documents/items/laurels.svg +++ b/assets/icons/documents/items/laurels.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/open-treasure-chest.svg b/assets/icons/documents/items/open-treasure-chest.svg index 172a80035..d66399f40 100644 --- a/assets/icons/documents/items/open-treasure-chest.svg +++ b/assets/icons/documents/items/open-treasure-chest.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/round-potion.svg b/assets/icons/documents/items/round-potion.svg index 7f9819149..0c2357329 100644 --- a/assets/icons/documents/items/round-potion.svg +++ b/assets/icons/documents/items/round-potion.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/stars-stack.svg b/assets/icons/documents/items/stars-stack.svg index 19c197f63..2d446683c 100644 --- a/assets/icons/documents/items/stars-stack.svg +++ b/assets/icons/documents/items/stars-stack.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/village.svg b/assets/icons/documents/items/village.svg index c28d742b6..b7c776a13 100644 --- a/assets/icons/documents/items/village.svg +++ b/assets/icons/documents/items/village.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/wolf-head.svg b/assets/icons/documents/items/wolf-head.svg index 2be500c1f..7e57bbad7 100644 --- a/assets/icons/documents/items/wolf-head.svg +++ b/assets/icons/documents/items/wolf-head.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/daggerheart.d.ts b/daggerheart.d.ts index ab754b17c..1641faa83 100644 --- a/daggerheart.d.ts +++ b/daggerheart.d.ts @@ -1,8 +1,12 @@ import '@client/global.mjs'; +import '@common/global.mjs'; +import '@common/primitives/global.mjs'; import Canvas from '@client/canvas/board.mjs'; +import { ResourceUpdateMap } from './module/data/action/baseAction.mjs'; // Foundry's use of `Object.assign(globalThis) means many globally available objects are not read as such // This declare global hopefully fixes that +// Note: eslint is not aware of these, whatever is added here should go in the eslint's globals list declare global { /** * A simple event framework used throughout Foundry Virtual Tabletop. @@ -12,9 +16,67 @@ declare global { class Hooks extends foundry.helpers.Hooks {} const fromUuid = foundry.utils.fromUuid; const fromUuidSync = foundry.utils.fromUuidSync; - + /** + * A representation of a color in hexadecimal format. + * This class provides methods for transformations and manipulations of colors. + */ + class Color extends foundry.utils.Color {} /** * The singleton game canvas */ const canvas: Canvas; + + const ActiveEffect: foundry.documents.ActiveEffect; + const Actor: foundry.documents.Actor; + const BaseScene: foundry.documents.BaseScene; + const ChatMessage: foundry.documents.ChatMessage; + const Combat: foundry.documents.Combat; + const Combatant: foundry.documents.Combatant; + const Item: foundry.documents.Item; + const Macro: foundry.documents.Macro; + const Scene: foundry.documents.Scene; + const TokenDocument: foundry.documents.TokenDocument; + + const Collection: foundry.utils.Collection; + const FormDataExtended: foundry.applications.ux.FormDataExtended; + const TextEditor: foundry.applications.ux.TextEditor; + + /** + * Data used to build rolls such as duality rolls. The definition is incomplete and likely incorrect. + * Objects will often accept a Partial and spit out a non-partial. Those that are not guaranteed should be marked optional. + */ + interface RollConfig { + // unverified, check which ones are used and optional/not optional + event: Event; + title: string; + roll: { + modifier: number; + simple: boolean; + type: string; + difficulty: number; + }; + hasDamage: boolean; + hasEffect: boolean; + hasRoll: boolean; + chatMessage: { + template: string; + mute: boolean; + }; + targets: object; + costs: object; + + // verified + source?: { + /** uuid of the actor this roll is coming from */ + actor: string; + }; + /** Roll data associated with the actor or item */ + data: object; + resourceUpdates: ResourceUpdateMap; + hooks: string[]; + dialog: { + configure: boolean; + }; + damageOptions: object; + } } diff --git a/daggerheart.mjs b/daggerheart.mjs index 25c41ced4..b92be2a28 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -77,6 +77,20 @@ CONFIG.Token.prototypeSheetClass = applications.sheetConfigs.DhPrototypeTokenCon CONFIG.Token.objectClass = placeables.DhTokenPlaceable; CONFIG.Token.rulerClass = placeables.DhTokenRuler; CONFIG.Token.hudClass = applications.hud.DHTokenHUD; +CONFIG.Token.barConfig = { + bar1: { + colors: { + full: Color.fromRGB([1, 0, 0]), + empty: Color.fromRGB([0, 0, 0]) + } + }, + bar2: { + colors: { + full: Color.fromString('#0032b1'), + empty: Color.fromRGB([0, 0, 0]) + } + } +}; CONFIG.ui.combat = applications.ui.DhCombatTracker; CONFIG.ui.nav = applications.ui.DhSceneNavigation; @@ -88,6 +102,7 @@ CONFIG.ui.actors = applications.sidebar.DhActorDirectory; CONFIG.ui.daggerheartMenu = applications.sidebar.DaggerheartMenu; CONFIG.ui.resources = applications.ui.DhFearTracker; CONFIG.ui.countdowns = applications.ui.DhCountdowns; +CONFIG.ui.pause = applications.ui.DhGamePause; CONFIG.ux.ContextMenu = applications.ux.DHContextMenu; CONFIG.ux.TooltipManager = documents.DhTooltipManager; CONFIG.ux.TokenManager = new TokenManager(); @@ -453,8 +468,8 @@ Hooks.on('renderDialogV2', (_dialog, html) => { const cls = html.classList.contains('item-create') ? documents.DHItem.implementation : html.classList.contains('actor-create') - ? documents.DhpActor.implementation - : null; + ? documents.DhpActor.implementation + : null; if (!cls) return; const form = html.querySelector('form'); diff --git a/eslint.config.mjs b/eslint.config.mjs index ce2bb86f7..74141cc82 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,14 +1,115 @@ import globals from 'globals'; -import { defineConfig } from 'eslint/config'; -import prettier from 'eslint-plugin-prettier'; +import { defineConfig, globalIgnores } from 'eslint/config'; +import tseslint from 'typescript-eslint'; +import js from '@eslint/js'; +import stylistic from '@stylistic/eslint-plugin'; + +/** @type {Partial} */ +export const stylisticRules = { + '@stylistic/indent': [ + 'error', + 4, + { + SwitchCase: 1 + } + ], + '@stylistic/max-len': ['error', { + code: 120, + ignoreComments: true, + ignoreStrings: true, + ignoreTemplateLiterals: true, + ignoreRegExpLiterals: true + }], + '@stylistic/quotes': ['error', 'single', { allowTemplateLiterals: 'always' }], + '@stylistic/arrow-parens': ['error', 'as-needed'], + '@stylistic/quote-props': ['error', 'as-needed'], + '@stylistic/array-bracket-newline': ['error', 'consistent'], + '@stylistic/key-spacing': 'error', + '@stylistic/comma-dangle': ['error', 'never'], + '@stylistic/space-in-parens': ['error', 'never'], + '@stylistic/space-infix-ops': 2, + '@stylistic/keyword-spacing': 2, + '@stylistic/semi-spacing': 2, + '@stylistic/no-multi-spaces': 2, + '@stylistic/no-extra-semi': 2, + '@stylistic/no-whitespace-before-property': 2, + '@stylistic/space-unary-ops': 2 +}; export default defineConfig([ - { files: ['**/*.{js,mjs,cjs}'], languageOptions: { globals: globals.browser } }, - { plugins: { prettier } }, + globalIgnores(['foundry/**/*', 'build/**/*']), + { + files: ['gulpfile.js', 'postcss.config.js'], + languageOptions: { globals: globals.node } + }, { files: ['**/*.{js,mjs,cjs}'], + plugins: { + '@stylistic': stylistic + }, + languageOptions: { + globals: { + ...globals.browser, + CONFIG: 'readonly', + CONST: 'readonly', + // Global classes + Color: 'readonly', + Handlebars: 'readonly', + Hooks: 'readonly', + PIXI: 'readonly', + ProseMirror: 'readonly', + Roll: 'readonly', + // global namespaces + canvas: 'readonly', + foundry: 'readonly', + game: 'readonly', + ui: 'readonly', + // global functions + fromUuid: 'readonly', + fromUuidSync: 'readonly', + getDocumentClass: 'readonly', + _del: 'readonly', + _replace: 'readonly', + _loc: 'readonly', + // Documents + ActiveEffect: 'readonly', + Actor: 'readonly', + BaseScene: 'readonly', + ChatMessage: 'readonly', + Combat: 'readonly', + Combatant: 'readonly', + Item: 'readonly', + Macro: 'readonly', + Scene: 'readonly', + TokenDocument: 'readonly', + // Other + Collection: 'readonly', + FormDataExtended: 'readonly', + TextEditor: 'readonly' + } + }, + rules: { + 'no-undef': 'error', + 'no-unused-vars': [ + 'error', + { + args: 'none', + destructuredArrayIgnorePattern: '^_', + varsIgnorePattern: '^_[A-Z]', + ignoreRestSiblings: true + } + ], + ...stylisticRules + } + }, + { + files: ['**/*.ts'], + extends: [js.configs.recommended, tseslint.configs.recommended], + plugins: { + '@stylistic': stylistic + }, rules: { - 'prettier/prettier': 'error' + ...stylisticRules } } ]); diff --git a/jsconfig.json b/jsconfig.json index 00bab1f53..42c4b4048 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,14 +1,14 @@ { "compilerOptions": { - "module": "ES6", - "target": "ES6", + "module": "es2022", + "target": "es2022", "paths": { "@client/*": ["./foundry/client/*"], "@common/*": ["./foundry/common/*"] } }, "exclude": ["node_modules", "**/node_modules/*"], - "include": ["daggerheart.mjs", "foundry/client/client.mjs", "daggerheart.d.ts"], + "include": ["daggerheart.mjs", "foundry/client/client.mjs", "daggerheart.d.ts", "module/**/*.d.ts"], "typeAcquisition": { "include": ["jquery"] } diff --git a/lang/en.json b/lang/en.json index 3a1340e0b..368dd54d8 100755 --- a/lang/en.json +++ b/lang/en.json @@ -484,7 +484,6 @@ "startFormula": "Randomized Start Value Formula", "currentCountdownCurrent": "Current: {value}", "currentCountdownStart": "Start: {value}", - "category": "Category", "progressionType": "Progression", "decreasing": "Decreasing", "looping": "Looping", @@ -1162,7 +1161,7 @@ "autoAppliedByLabel": "Max Stress" } }, - "CountdownType": { + "CountdownProgressType": { "actionRoll": "Action Roll", "characterAttack": "Character Attack", "characterSpotlight": "Character Spotlight", @@ -1170,6 +1169,10 @@ "fear": "Fear", "spotlight": "Spotlight" }, + "CountdownType": { + "encounter": { "label": "Short Term", "shortLabel": "Short" }, + "narrative": { "label": "Long Term", "shortLabel": "Long" } + }, "DaggerheartDiceAnimationEvents": { "critical": { "name": "Critical" }, "higher": { "name": "Highest Roll" } @@ -1281,6 +1284,13 @@ "diceValue": "Dice Value", "die": "Die" }, + "OutcomeType": { + "default": "Default Outcome", + "successHope": "Success/ Hope", + "successFear": "Success/ Fear", + "failureHope": "Failure/ Hope", + "failureFear": "Failure/ Fear" + }, "Range": { "self": { "name": "Self", @@ -1448,7 +1458,7 @@ }, "triggerType": "Trigger Type", "triggeringActorType": "Triggering Actor Type", - "triggerError": "{trigger} trigger failed for {actor}. It's probably configured wrong." + "triggerError": "{trigger} trigger on {item}({itemUuid}) failed for {actor}. It's probably configured wrong." }, "WeaponFeature": { "barrier": { @@ -2007,7 +2017,8 @@ "Attachments": { "attachHint": "Drop items here to attach them", "transferHint": "If checked, this effect will be applied to any actor that owns this Effect's parent Item. The effect is always applied if this Item is attached to another one." - } + }, + "OriginTag": "Origin: {name}" }, "GENERAL": { "Ability": { @@ -2353,7 +2364,9 @@ "triggers": "Triggers", "deathMoves": "Deathmoves", "sources": "Sources", - "packs": "Packs" + "packs": "Packs", + "range": "Range", + "outcomes": "Outcomes" }, "Tiers": { "singular": "Tier", @@ -2887,7 +2900,9 @@ "iconName": "Icon Name", "iconNameHint": "Icons are from fontawesome", "bagName": "Bag Name", - "chestName": "Chest Name" + "chestName": "Chest Name", + "denomination": "Denomination", + "initialAmount": "Starting Amount" }, "domains": { "domainsTitle": "Base Domains", diff --git a/module/applications/characterCreation/characterCreation.mjs b/module/applications/characterCreation/characterCreation.mjs index 82ca9ccbe..9d27fbb78 100644 --- a/module/applications/characterCreation/characterCreation.mjs +++ b/module/applications/characterCreation/characterCreation.mjs @@ -1,6 +1,5 @@ import { abilities } from '../../config/actorConfig.mjs'; import { burden } from '../../config/generalConfig.mjs'; -import { createEmbeddedItemsWithEffects, createEmbeddedItemWithEffects } from '../../helpers/utils.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -154,8 +153,8 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : this.tabGroups.primary !== 'equipment' - ? v.active - : false; + ? v.active + : false; v.cssClass = v.active ? 'active' : ''; switch (v.id) { @@ -211,9 +210,9 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl context.suggestedTraits = this.setup.class.system ? Object.keys(this.setup.class.system.characterGuide.suggestedTraits).map(traitKey => { - const trait = this.setup.class.system.characterGuide.suggestedTraits[traitKey]; - return `${game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${traitKey}.short`)} ${trait > 0 ? `+${trait}` : trait}`; - }) + const trait = this.setup.class.system.characterGuide.suggestedTraits[traitKey]; + return `${game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${traitKey}.short`)} ${trait > 0 ? `+${trait}` : trait}`; + }) : []; context.traits = { values: Object.keys(this.setup.traits).map(traitKey => { @@ -441,16 +440,15 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl if (type === 'subclasses') { const classItem = this.setup.class; - const uuid = classItem?._stats.compendiumSource ?? classItem?.uuid; presets.filter = { - 'system.linkedClass': { key: 'system.linkedClass', value: uuid } + 'system.linkedClass': { key: 'system.linkedClass', value: classItem?.sourceUuid } }; } if (equipment.includes(type)) presets.filter = { 'system.tier': { key: 'system.tier', value: 1 }, - 'type': { key: 'type', value: type } + type: { key: 'type', value: type } }; ui.compendiumBrowser.open(presets); @@ -524,37 +522,52 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl } }; - await createEmbeddedItemWithEffects(this.character, ancestry); - await createEmbeddedItemWithEffects(this.character, this.setup.community); - await createEmbeddedItemWithEffects(this.character, this.setup.class); - await createEmbeddedItemWithEffects(this.character, this.setup.subclass); - await createEmbeddedItemsWithEffects(this.character, Object.values(this.setup.domainCards)); + // Inner function to create the base item data + async function createEmbeddedItemData(baseData) { + const uuid = baseData.uuid ?? baseData._uuid + const data = baseData instanceof Item ? baseData : await foundry.utils.fromUuid(baseData.uuid) ?? baseData; + return { + ...baseData, + id: data.id, + uuid: uuid, + _uuid: uuid, + effects: data.effects?.map(effect => effect.toObject()), + flags: baseData.flags ?? data.flags, + _stats: { + ...data._stats, + compendiumSource: uuid.startsWith('Compendium.') ? uuid : null, + duplicateSource: uuid && !uuid.startsWith('Compendium.') ? uuid : null + } + }; + } + // Add the class first. All other items validate it during pre creation + await this.character.createEmbeddedDocuments('Item', [await createEmbeddedItemData(this.setup.class)]); + + // Add the remaining items + const newItems = [ + await createEmbeddedItemData(ancestry), + await createEmbeddedItemData(this.setup.community), + await createEmbeddedItemData(this.setup.subclass), + ...(await Promise.all( + Object.values(this.setup.domainCards).map(d => createEmbeddedItemData(d)) + )) + ]; if (this.equipment.armor.uuid) - await createEmbeddedItemWithEffects(this.character, this.equipment.armor, { - ...this.equipment.armor, - system: { ...this.equipment.armor.system, equipped: true } - }); + newItems.push(await createEmbeddedItemData(this.equipment.armor)); if (this.equipment.primaryWeapon.uuid) - await createEmbeddedItemWithEffects(this.character, this.equipment.primaryWeapon, { - ...this.equipment.primaryWeapon, - system: { ...this.equipment.primaryWeapon.system, equipped: true } - }); + newItems.push(await createEmbeddedItemData(this.equipment.primaryWeapon)); if (this.equipment.secondaryWeapon.uuid) - await createEmbeddedItemWithEffects(this.character, this.equipment.secondaryWeapon, { - ...this.equipment.secondaryWeapon, - system: { ...this.equipment.secondaryWeapon.system, equipped: true } - }); + newItems.push(await createEmbeddedItemData(this.equipment.secondaryWeapon)); if (this.equipment.inventory.choiceA.uuid) - await createEmbeddedItemWithEffects(this.character, this.equipment.inventory.choiceA); + newItems.push(await createEmbeddedItemData(this.equipment.inventory.choiceA)); if (this.equipment.inventory.choiceB.uuid) - await createEmbeddedItemWithEffects(this.character, this.equipment.inventory.choiceB); - - await createEmbeddedItemsWithEffects( - this.character, - this.setup.class.system.inventory.take.filter(x => x) - ); + newItems.push(await createEmbeddedItemData(this.equipment.inventory.choiceB)); + for (const item of this.setup.class.system.inventory.take.filter(x => x)) { + newItems.push(await createEmbeddedItemData(item)); + } + await this.character.createEmbeddedDocuments('Item', newItems); await this.character.update( { system: { @@ -587,26 +600,14 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl const item = await foundry.utils.fromUuid(data.uuid); if (item.type === 'ancestry' && event.target.closest('.primary-ancestry-card')) { this.setup.ancestryName.primary = item.name; - this.setup.primaryAncestry = { - ...item, - effects: Array.from(item.effects).map(x => x.toObject()), - uuid: item.uuid - }; + this.setup.primaryAncestry = item; } else if (item.type === 'ancestry' && event.target.closest('.secondary-ancestry-card')) { this.setup.ancestryName.secondary = item.name; - this.setup.secondaryAncestry = { - ...item, - effects: Array.from(item.effects).map(x => x.toObject()), - uuid: item.uuid - }; + this.setup.secondaryAncestry = item; } else if (item.type === 'community' && event.target.closest('.community-card')) { - this.setup.community = { - ...item, - effects: Array.from(item.effects).map(x => x.toObject()), - uuid: item.uuid - }; + this.setup.community = item; } else if (item.type === 'class' && event.target.closest('.class-card')) { - this.setup.class = { ...item, effects: Array.from(item.effects).map(x => x.toObject()), uuid: item.uuid }; + this.setup.class = item; this.setup.subclass = {}; this.setup.domainCards = { [foundry.utils.randomID()]: {}, @@ -619,11 +620,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl return; } - this.setup.subclass = { - ...item, - effects: Array.from(item.effects).map(x => x.toObject()), - uuid: item.uuid - }; + this.setup.subclass = item; } else if (item.type === 'domainCard' && event.target.closest('.domain-card')) { if (!this.setup.class.uuid) { ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.missingClass')); @@ -645,14 +642,14 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl return; } - this.setup.domainCards[event.target.closest('.domain-card').dataset.card] = { ...item, uuid: item.uuid }; + this.setup.domainCards[event.target.closest('.domain-card').dataset.card] = item; } else if (item.type === 'armor' && event.target.closest('.armor-card')) { if (item.system.tier > 1) { ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.itemTooHighTier')); return; } - this.equipment.armor = { ...item, uuid: item.uuid }; + this.equipment.armor = item; } else if (item.type === 'weapon' && event.target.closest('.primary-weapon-card')) { if (item.system.secondary) { ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.notPrimary')); @@ -668,7 +665,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl this.equipment.secondaryWeapon = {}; } - this.equipment.primaryWeapon = { ...item, uuid: item.uuid }; + this.equipment.primaryWeapon = item; } else if (item.type === 'weapon' && event.target.closest('.secondary-weapon-card')) { if (this.equipment.primaryWeapon?.system?.burden === burden.twoHanded.value) { ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.primaryIsTwoHanded')); @@ -685,7 +682,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl return; } - this.equipment.secondaryWeapon = { ...item, uuid: item.uuid }; + this.equipment.secondaryWeapon = item; } else { return; } diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index 76b2e751c..048797d8c 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -160,7 +160,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio } if (rest.hasOwnProperty('trait')) { this.config.roll.trait = rest.trait; - if (!this.config.source.item) + if (!this.config.source.item && this.config.roll.trait) this.config.title = game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { ability: game.i18n.localize(abilities[this.config.roll.trait]?.label) }); @@ -196,14 +196,14 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio 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, - { - extKey: button.dataset.key, - key: this.config?.data?.parent?.isNPC ? 'fear' : 'hope', - value: 1, - name: this.config.data?.system.experiences?.[button.dataset.key]?.name - } - ]; + ...this.config.costs, + { + extKey: button.dataset.key, + key: this.config?.data?.parent?.isNPC ? 'fear' : 'hope', + value: 1, + name: this.config.data?.system.experiences?.[button.dataset.key]?.name + } + ]; this.render(); } @@ -213,8 +213,8 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio this.config.actionType = this.reactionOverride ? 'reaction' : this.config.actionType === 'reaction' - ? 'action' - : this.config.actionType; + ? 'action' + : this.config.actionType; this.render(); } } @@ -224,7 +224,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio this.render(); } - static async submitRoll() { + static async submitRoll(event) { await this.close({ submitted: true }); } diff --git a/module/applications/dialogs/damageReductionDialog.mjs b/module/applications/dialogs/damageReductionDialog.mjs index b916a5de7..8cb026f05 100644 --- a/module/applications/dialogs/damageReductionDialog.mjs +++ b/module/applications/dialogs/damageReductionDialog.mjs @@ -138,13 +138,13 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap const stressReductionStress = this.availableStressReductions ? stressReductions.reduce((acc, red) => acc + red.cost, 0) : 0; + const stress = this.actor.system.resources.stress; context.stress = selectedStressMarks.length > 0 || this.availableStressReductions ? { - value: - this.actor.system.resources.stress.value + selectedStressMarks.length + stressReductionStress, - max: this.actor.system.resources.stress.max - } + value: stress.value + selectedStressMarks.length + stressReductionStress, + max: stress.max + } : null; context.maxArmorUsed = maxArmorUsed; @@ -176,7 +176,6 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap } static updateData(event, _, formData) { - const form = foundry.utils.expandObject(formData.object); this.render(true); } diff --git a/module/applications/dialogs/downtime.mjs b/module/applications/dialogs/downtime.mjs index 367540bf0..e209cc3b0 100644 --- a/module/applications/dialogs/downtime.mjs +++ b/module/applications/dialogs/downtime.mjs @@ -259,10 +259,10 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV const resetValue = increasing ? 0 : feature.system.resource.max - ? new Roll( + ? new Roll( Roll.replaceFormulaData(feature.system.resource.max, this.actor.getRollData()) ).evaluateSync().total - : 0; + : 0; await feature.update({ 'system.resource.value': resetValue }); } diff --git a/module/applications/dialogs/groupRollDialog.mjs b/module/applications/dialogs/groupRollDialog.mjs index dd504b4bf..ebc80d390 100644 --- a/module/applications/dialogs/groupRollDialog.mjs +++ b/module/applications/dialogs/groupRollDialog.mjs @@ -1,4 +1,5 @@ import { ResourceUpdateMap } from '../../data/action/baseAction.mjs'; +import { shouldUseHopeFearAutomation } from '../../helpers/utils.mjs'; import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import Party from '../sheets/actors/party.mjs'; @@ -167,8 +168,8 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat partContext.groupRoll = { totalLabel: leader?.rollData ? game.i18n.format('DAGGERHEART.GENERAL.withThing', { - thing: leader.roll.totalLabel - }) + thing: leader.roll.totalLabel + }) : null, totalDualityClass: leader?.roll?.isCritical ? 'critical' : leader?.roll?.withHope ? 'hope' : 'fear', total: leaderTotal + modifierTotal, @@ -480,19 +481,22 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat await cls.create(msgData); - const resourceMap = new ResourceUpdateMap(actor); - if (totalRoll.isCritical) { - resourceMap.addResources([ - { key: 'stress', value: -1, total: 1 }, - { key: 'hope', value: 1, total: 1 } - ]); - } else if (totalRoll.withHope) { - resourceMap.addResources([{ key: 'hope', value: 1, total: 1 }]); - } else { - resourceMap.addResources([{ key: 'fear', value: 1, total: 1 }]); - } + /* Handle resource updates for the finished GroupRoll */ + if (shouldUseHopeFearAutomation({ gmAsPlayer: true })) { + const resourceMap = new ResourceUpdateMap(actor); + if (totalRoll.isCritical) { + resourceMap.addResources([ + { key: 'stress', value: -1 }, + { key: 'hope', value: 1 } + ]); + } else if (totalRoll.withHope) { + resourceMap.addResources([{ key: 'hope', value: 1 }]); + } else { + resourceMap.addResources([{ key: 'fear', value: 1 }]); + } - resourceMap.updateResources(); + resourceMap.updateResources(); + } /* Fin */ this.cancelRoll({ confirm: false }); diff --git a/module/applications/dialogs/itemTransfer.mjs b/module/applications/dialogs/itemTransfer.mjs index 42e3a727a..46ceafbd3 100644 --- a/module/applications/dialogs/itemTransfer.mjs +++ b/module/applications/dialogs/itemTransfer.mjs @@ -39,6 +39,7 @@ export default class ItemTransferDialog extends HandlebarsApplicationMixin(Appli const homebrewKey = CONFIG.DH.SETTINGS.gameSettings.Homebrew; const currencySetting = game.settings.get(CONFIG.DH.id, homebrewKey).currency?.[currency] ?? null; const max = item?.system.quantity ?? originActor.system.gold[currency] ?? 0; + const isQuantifiable = targetActor.system.metadata.quantifiable?.includes(item?.type); return { originActor, @@ -46,7 +47,7 @@ export default class ItemTransferDialog extends HandlebarsApplicationMixin(Appli itemImage: item?.img, currencyIcon: currencySetting?.icon, max, - initial: targetActor.system.metadata.quantifiable?.includes(item.type) ? max : 1, + initial: isQuantifiable || !!currencySetting ? max : 1, title: item?.name ?? currencySetting?.label }; } diff --git a/module/applications/dialogs/tagTeamDialog.mjs b/module/applications/dialogs/tagTeamDialog.mjs index 3dc6b0fc4..19869a001 100644 --- a/module/applications/dialogs/tagTeamDialog.mjs +++ b/module/applications/dialogs/tagTeamDialog.mjs @@ -1,5 +1,6 @@ +import { ResourceUpdateMap } from '../../data/action/baseAction.mjs'; import { MemberData } from '../../data/tagTeamData.mjs'; -import { getCritDamageBonus } from '../../helpers/utils.mjs'; +import { getCritDamageBonus, shouldUseHopeFearAutomation } from '../../helpers/utils.mjs'; import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import Party from '../sheets/actors/party.mjs'; @@ -9,6 +10,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio constructor(party) { super({ id: `TagTeamDialog-${party.id}` }); + this.usesTagTeamHopeCost = true; this.party = party; this.partyMembers = party.system.partyMembers .filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type)) @@ -20,7 +22,9 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio owned: member.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER) })); - this.initiator = { cost: 3 }; + this.initiator = { cost: + this.party.system.schema.fields.tagTeam.fields.initiator.fields.cost.initial + }; this.openForAllPlayers = true; this.tabGroups.application = Object.keys(party.system.tagTeam.members).length @@ -94,9 +98,13 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio ?.addEventListener('input', this.updateInitiatorMemberField.bind(this)); htmlElement - .querySelector('.initiator-cost-field') + .querySelector('.initiator-cost-input') ?.addEventListener('input', this.updateInitiatorCostField.bind(this)); + htmlElement + .querySelector('.initiator-cost-enabled-checkbox') + ?.addEventListener('change', this.toggleInitiatorCostEnabled.bind(this)); + htmlElement .querySelector('.openforall-field') ?.addEventListener('change', this.updateOpenForAllField.bind(this)); @@ -156,6 +164,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio .map(x => ({ value: x.id, label: x.name })); partContext.initiatorDisabled = !selectedMembers.length; partContext.openForAllPlayers = this.openForAllPlayers; + partContext.usesTagTeamHopeCost = this.usesTagTeamHopeCost; break; case 'tagTeamRoll': @@ -397,6 +406,13 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio this.render(); } + toggleInitiatorCostEnabled(_event) { + this.usesTagTeamHopeCost = !this.usesTagTeamHopeCost; + this.initiator.cost = this.usesTagTeamHopeCost ? + this.party.system.schema.fields.tagTeam.fields.initiator.fields.cost.initial : 0; + this.render(); + } + updateOpenForAllField(event) { this.openForAllPlayers = event.target.checked; this.render(); @@ -653,8 +669,8 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio const baseSecondaryRoll = selectedRoll ? memberValues.find(x => !x.selected) : memberValues.length > 1 - ? memberValues[1] - : null; + ? memberValues[1] + : null; if (!baseMainRoll?.rollData || !baseSecondaryRoll) return null; @@ -752,32 +768,37 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio /* Handle resource updates from the finished TagTeamRoll */ const tagTeamData = this.party.system.tagTeam; - const fearUpdate = { key: 'fear', value: null, total: null, enabled: true }; - for (let memberId in tagTeamData.members) { - const resourceUpdates = []; - const rollGivesHope = finalRoll.isCritical || finalRoll.withHope; - if (memberId === tagTeamData.initiator.memberId) { - const value = tagTeamData.initiator.cost - ? rollGivesHope - ? 1 - tagTeamData.initiator.cost - : -tagTeamData.initiator.cost - : 1; - resourceUpdates.push({ key: 'hope', value: value, total: -value, enabled: true }); - } else if (rollGivesHope) { - resourceUpdates.push({ key: 'hope', value: 1, total: -1, enabled: true }); - } - if (finalRoll.isCritical) resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true }); - if (finalRoll.withFear) { - fearUpdate.value = fearUpdate.value === null ? 1 : fearUpdate.value + 1; - fearUpdate.total = fearUpdate.total === null ? -1 : fearUpdate.total - 1; + + const actorResourceMaps = Object.keys(tagTeamData.members).reduce((acc, key) => { + acc[key] = new ResourceUpdateMap(game.actors.get(key)); + return acc; + }, {}); + + if (shouldUseHopeFearAutomation({ gmAsPlayer: true })) { + const fearResourceMap = actorResourceMaps[tagTeamData.initiator.memberId]; + for (const memberId in tagTeamData.members) { + const resourceMap = actorResourceMaps[memberId]; + if (finalRoll.isCritical) { + resourceMap.addResources([ + { key: 'stress', value: -1, enabled: true }, + { key: 'hope', value: 1, enabled: true } + ]); + } else if (finalRoll.withHope) { + resourceMap.addResources([{ key: 'hope', value: 1, enabled: true }]); + } else if (finalRoll.withFear) { + fearResourceMap.addResources([{ key: 'fear', value: 1, enabled: true }]); + } } + } - game.actors.get(memberId).modifyResource(resourceUpdates); + /* Even with Hope/Fear automation off, the hope cost of performing the TagTeamRoll can still optionally be subtracted */ + if (tagTeamData.initiator.cost) { + const resourceMap = actorResourceMaps[tagTeamData.initiator.memberId]; + resourceMap.addResources([{ key: 'hope', value: -tagTeamData.initiator.cost, enabled: true }]); } - if (fearUpdate.value) { - mainActor.modifyResource([fearUpdate]); - } + for (const resourceMap of Object.values(actorResourceMaps)) + resourceMap.updateResources(); /* Fin */ this.cancelRoll({ confirm: false }); diff --git a/module/applications/hud/tokenHUD.mjs b/module/applications/hud/tokenHUD.mjs index 671b01a10..4805cd9e6 100644 --- a/module/applications/hud/tokenHUD.mjs +++ b/module/applications/hud/tokenHUD.mjs @@ -50,11 +50,11 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { ).showGenericStatusEffects; context.genericStatusEffects = useGeneric ? Object.keys(context.statusEffects).reduce((acc, key) => { - const effect = context.statusEffects[key]; - if (!effect.systemEffect) acc[key] = effect; + const effect = context.statusEffects[key]; + if (!effect.systemEffect) acc[key] = effect; - return acc; - }, {}) + return acc; + }, {}) : null; context.hasCompanion = this.actor.system.companion; @@ -68,11 +68,11 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { const warning = tokensWithoutActors.length === 1 ? game.i18n.format('DAGGERHEART.UI.Notifications.tokenActorMissing', { - name: tokensWithoutActors[0].name - }) + name: tokensWithoutActors[0].name + }) : game.i18n.format('DAGGERHEART.UI.Notifications.tokenActorsMissing', { - names: tokensWithoutActors.map(x => x.name).join(', ') - }); + names: tokensWithoutActors.map(x => x.name).join(', ') + }); const tokens = canvas.tokens.controlled .filter(t => t.actor && !DHTokenHUD.#nonCombatTypes.includes(t.actor.type)) @@ -174,8 +174,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { nonZeroIndex === sideMiddle ? 0 : nonZeroIndex < sideMiddle - ? -nonZeroIndex - : nonZeroIndex - sideMiddle; + ? -nonZeroIndex + : nonZeroIndex - sideMiddle; return { x: actorX - sizeX * distance, y: actorY - sizeY * distanceCoefficient }; } else if (index < side + inbetween) { const inbetweenIndex = nonZeroIndex - side; @@ -183,8 +183,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { inbetweenIndex === inbetweenMiddle ? 0 : inbetweenIndex < inbetweenMiddle - ? -inbetweenIndex - : 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; @@ -192,8 +192,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { sideIndex === sideMiddle ? 0 : sideIndex < sideMiddle - ? sideIndex - : -(sideIndex - sideMiddle); + ? sideIndex + : -(sideIndex - sideMiddle); return { x: actorX + sizeX * distance, y: actorY + sizeY * distanceCoefficient }; } else { const inbetweenIndex = nonZeroIndex - 2 * side - inbetween; @@ -201,8 +201,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { inbetweenIndex === inbetweenMiddle ? 0 : inbetweenIndex < inbetweenMiddle - ? inbetweenIndex - : -(inbetweenIndex - inbetweenMiddle); + ? inbetweenIndex + : -(inbetweenIndex - inbetweenMiddle); return { x: actorX - sizeX * distanceCoefficient, y: actorY + sizeY * distance }; } }) diff --git a/module/applications/levelup/characterLevelup.mjs b/module/applications/levelup/characterLevelup.mjs index e8d6cf1c4..a2df63c16 100644 --- a/module/applications/levelup/characterLevelup.mjs +++ b/module/applications/levelup/characterLevelup.mjs @@ -210,9 +210,9 @@ export default class DhCharacterLevelUp extends LevelUpBase { achievementExperiences = level.achievements.experiences ? Object.values(level.achievements.experiences).reduce((acc, experience) => { - if (experience.name) acc.push(experience); - return acc; - }, []) + if (experience.name) acc.push(experience); + return acc; + }, []) : []; } @@ -315,15 +315,15 @@ export default class DhCharacterLevelUp extends LevelUpBase { : null; advancement[choiceKey] = multiclassItem ? { - ...multiclassItem.toObject(), - domain: checkbox.secondaryData.domain - ? game.i18n.localize( - CONFIG.DH.DOMAIN.allDomains()[checkbox.secondaryData.domain] - .label - ) - : null, - subclass: subclass ? subclass.name : null - } + ...multiclassItem.toObject(), + domain: checkbox.secondaryData.domain + ? game.i18n.localize( + CONFIG.DH.DOMAIN.allDomains()[checkbox.secondaryData.domain] + .label + ) + : null, + subclass: subclass ? subclass.name : null + } : {}; break; } diff --git a/module/applications/levelup/companionLevelup.mjs b/module/applications/levelup/companionLevelup.mjs index d6bf2d789..92cf30500 100644 --- a/module/applications/levelup/companionLevelup.mjs +++ b/module/applications/levelup/companionLevelup.mjs @@ -77,9 +77,9 @@ export default class DhCompanionLevelUp extends BaseLevelUp { achievementExperiences = level.achievements.experiences ? Object.values(level.achievements.experiences).reduce((acc, experience) => { - if (experience.name) acc.push(experience); - return acc; - }, []) + if (experience.name) acc.push(experience); + return acc; + }, []) : []; } context.achievements = { @@ -155,15 +155,15 @@ export default class DhCompanionLevelUp extends BaseLevelUp { vicious: { damage: advancement.vicious?.damage ? { - old: actorDamageDice, - new: advancement.vicious.damage - } + old: actorDamageDice, + new: advancement.vicious.damage + } : null, range: advancement.vicious?.range ? { - old: game.i18n.localize(`DAGGERHEART.CONFIG.Range.${actorRange}.name`), - new: game.i18n.localize(advancement.vicious.range.label) - } + old: game.i18n.localize(`DAGGERHEART.CONFIG.Range.${actorRange}.name`), + new: game.i18n.localize(advancement.vicious.range.label) + } : null }, simple: advancement.simple ?? {} diff --git a/module/applications/levelup/levelup.mjs b/module/applications/levelup/levelup.mjs index c4616d9a1..f40609a47 100644 --- a/module/applications/levelup/levelup.mjs +++ b/module/applications/levelup/levelup.mjs @@ -1,4 +1,3 @@ -import { abilities, subclassFeatureLabels } from '../../config/actorConfig.mjs'; import { getDeleteKeys, tagifyElement } from '../../helpers/utils.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -135,192 +134,6 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) context.tabs.advancements.progress = { selected: selections, max: currentLevel.maxSelections }; context.showTabs = this.tabGroups.primary !== 'summary'; break; - - const actorArmor = this.actor.system.armor; - const levelKeys = Object.keys(this.levelup.levels); - let achivementProficiency = 0; - const achievementCards = []; - let achievementExperiences = []; - for (var levelKey of levelKeys) { - const level = this.levelup.levels[levelKey]; - if (Number(levelKey) < this.levelup.startLevel) continue; - - achivementProficiency += level.achievements.proficiency ?? 0; - const cards = level.achievements.domainCards ? Object.values(level.achievements.domainCards) : null; - if (cards) { - for (var card of cards) { - const itemCard = await foundry.utils.fromUuid(card.uuid); - achievementCards.push(itemCard); - } - } - - achievementExperiences = level.achievements.experiences - ? Object.values(level.achievements.experiences).reduce((acc, experience) => { - if (experience.name) acc.push(experience); - return acc; - }, []) - : []; - } - - context.achievements = { - proficiency: { - old: this.actor.system.proficiency, - new: this.actor.system.proficiency + achivementProficiency, - shown: achivementProficiency > 0 - }, - damageThresholds: { - major: { - old: this.actor.system.damageThresholds.major, - new: this.actor.system.damageThresholds.major + changedActorLevel - currentActorLevel - }, - severe: { - old: this.actor.system.damageThresholds.severe, - new: - this.actor.system.damageThresholds.severe + - (actorArmor - ? changedActorLevel - currentActorLevel - : (changedActorLevel - currentActorLevel) * 2) - }, - unarmored: !actorArmor - }, - domainCards: { - values: achievementCards, - shown: achievementCards.length > 0 - }, - experiences: { - values: achievementExperiences - } - }; - - const advancement = {}; - for (var levelKey of levelKeys) { - const level = this.levelup.levels[levelKey]; - if (Number(levelKey) < this.levelup.startLevel) continue; - - for (var choiceKey of Object.keys(level.choices)) { - const choice = level.choices[choiceKey]; - for (var checkbox of Object.values(choice)) { - switch (choiceKey) { - case 'proficiency': - case 'hitPoint': - case 'stress': - case 'evasion': - advancement[choiceKey] = advancement[choiceKey] - ? advancement[choiceKey] + Number(checkbox.value) - : Number(checkbox.value); - break; - case 'trait': - if (!advancement[choiceKey]) advancement[choiceKey] = {}; - for (var traitKey of checkbox.data) { - if (!advancement[choiceKey][traitKey]) advancement[choiceKey][traitKey] = 0; - advancement[choiceKey][traitKey] += 1; - } - break; - case 'domainCard': - if (!advancement[choiceKey]) advancement[choiceKey] = []; - if (checkbox.data.length === 1) { - const choiceItem = await foundry.utils.fromUuid(checkbox.data[0]); - advancement[choiceKey].push(choiceItem.toObject()); - } - break; - case 'experience': - if (!advancement[choiceKey]) advancement[choiceKey] = []; - const data = checkbox.data.map(data => { - const experience = Object.keys(this.actor.system.experiences).find( - x => x === data - ); - return this.actor.system.experiences[experience]?.description ?? ''; - }); - advancement[choiceKey].push({ data: data, value: checkbox.value }); - break; - case 'subclass': - if (checkbox.data[0]) { - const subclassItem = await foundry.utils.fromUuid(checkbox.data[0]); - if (!advancement[choiceKey]) advancement[choiceKey] = []; - advancement[choiceKey].push({ - ...subclassItem.toObject(), - featureLabel: game.i18n.localize( - subclassFeatureLabels[Number(checkbox.secondaryData.featureState)] - ) - }); - } - break; - case 'multiclass': - const multiclassItem = await foundry.utils.fromUuid(checkbox.data[0]); - const subclass = multiclassItem - ? await foundry.utils.fromUuid(checkbox.secondaryData.subclass) - : null; - advancement[choiceKey] = multiclassItem - ? { - ...multiclassItem.toObject(), - domain: checkbox.secondaryData.domain - ? game.i18n.localize( - CONFIG.DH.DOMAIN.allDomains()[checkbox.secondaryData.domain] - .label - ) - : null, - subclass: subclass ? subclass.name : null - } - : {}; - break; - } - } - } - } - - context.advancements = { - statistics: { - proficiency: { - old: context.achievements.proficiency.new, - new: context.achievements.proficiency.new + (advancement.proficiency ?? 0) - }, - hitPoints: { - old: this.actor.system.resources.hitPoints.max, - new: this.actor.system.resources.hitPoints.max + (advancement.hitPoint ?? 0) - }, - stress: { - old: this.actor.system.resources.stress.max, - new: this.actor.system.resources.stress.max + (advancement.stress ?? 0) - }, - evasion: { - old: this.actor.system.evasion, - new: this.actor.system.evasion + (advancement.evasion ?? 0) - } - }, - traits: Object.keys(this.actor.system.traits).reduce((acc, traitKey) => { - if (advancement.trait?.[traitKey]) { - if (!acc) acc = {}; - acc[traitKey] = { - label: game.i18n.localize(abilities[traitKey].label), - old: this.actor.system.traits[traitKey].value, - new: this.actor.system.traits[traitKey].value + advancement.trait[traitKey] - }; - } - return acc; - }, null), - domainCards: advancement.domainCard ?? [], - experiences: - advancement.experience?.flatMap(x => x.data.map(data => ({ name: data, modifier: x.value }))) ?? - [], - multiclass: advancement.multiclass, - subclass: advancement.subclass - }; - - context.advancements.statistics.proficiency.shown = - context.advancements.statistics.proficiency.new > context.advancements.statistics.proficiency.old; - context.advancements.statistics.hitPoints.shown = - context.advancements.statistics.hitPoints.new > context.advancements.statistics.hitPoints.old; - context.advancements.statistics.stress.shown = - context.advancements.statistics.stress.new > context.advancements.statistics.stress.old; - context.advancements.statistics.evasion.shown = - context.advancements.statistics.evasion.new > context.advancements.statistics.evasion.old; - context.advancements.statistics.shown = - context.advancements.statistics.proficiency.shown || - context.advancements.statistics.hitPoints.shown || - context.advancements.statistics.stress.shown || - context.advancements.statistics.evasion.shown; - - break; } return context; @@ -358,14 +171,14 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases'); if (experienceIncreaseTagify) { const allExperiences = { - ...this.actor.system.experiences, ...Object.values(this.levelup.levels).reduce((acc, level) => { for (const key of Object.keys(level.achievements.experiences)) { acc[key] = level.achievements.experiences[key]; } return acc; - }, {}) + }, {}), + ...this.actor.system.experiences }; tagifyElement( experienceIncreaseTagify, @@ -384,37 +197,35 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) this._dragDrop.forEach(d => d.bind(htmlElement)); } - tagifyUpdate = - type => - async (_, { option, removed }) => { - const updatePath = Object.keys(this.levelup.levels[this.levelup.currentLevel].choices).reduce( - (acc, choiceKey) => { - const choice = this.levelup.levels[this.levelup.currentLevel].choices[choiceKey]; - Object.keys(choice).forEach(checkboxNr => { - const checkbox = choice[checkboxNr]; - if ( - choiceKey === type && - (removed ? checkbox.data.includes(option) : checkbox.data.length < checkbox.amount) - ) { - acc = `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}.data`; - } - }); + tagifyUpdate = type => async (_, { option, removed }) => { + const updatePath = Object.keys(this.levelup.levels[this.levelup.currentLevel].choices).reduce( + (acc, choiceKey) => { + const choice = this.levelup.levels[this.levelup.currentLevel].choices[choiceKey]; + Object.keys(choice).forEach(checkboxNr => { + const checkbox = choice[checkboxNr]; + if ( + choiceKey === type && + (removed ? checkbox.data.includes(option) : checkbox.data.length < checkbox.amount) + ) { + acc = `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}.data`; + } + }); - return acc; - }, - null - ); + return acc; + }, + null + ); - if (!updatePath) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noSelectionsLeft')); - return; - } + if (!updatePath) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noSelectionsLeft')); + return; + } - const currentData = foundry.utils.getProperty(this.levelup, updatePath); - const updatedData = removed ? currentData.filter(x => x !== option) : [...currentData, option]; - await this.levelup.updateSource({ [updatePath]: updatedData }); - this.render(); - }; + const currentData = foundry.utils.getProperty(this.levelup, updatePath); + const updatedData = removed ? currentData.filter(x => x !== option) : [...currentData, option]; + await this.levelup.updateSource({ [updatePath]: updatedData }); + this.render(); + }; static async updateForm(event, _, formData) { const { levelup } = foundry.utils.expandObject(formData.object); @@ -593,10 +404,10 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) const domainCards = this.levelup.levels[this.levelup.currentLevel].achievements.domainCards; const illegalDomainCards = option.secondaryData.domain ? Object.keys(domainCards) - .map(key => ({ ...domainCards[key], key })) - .filter( - x => x.uuid && foundry.utils.fromUuidSync(x.uuid).system.domain === option.secondaryData.domain - ) + .map(key => ({ ...domainCards[key], key })) + .filter( + x => x.uuid && foundry.utils.fromUuidSync(x.uuid).system.domain === option.secondaryData.domain + ) : []; illegalDomainCards.forEach(card => { update[`levels.${this.levelup.currentLevel}.achievements.domainCards.${card.key}.uuid`] = null; diff --git a/module/applications/settings/appearanceSettings.mjs b/module/applications/settings/appearanceSettings.mjs index 9de9e7521..f2defffbc 100644 --- a/module/applications/settings/appearanceSettings.mjs +++ b/module/applications/settings/appearanceSettings.mjs @@ -1,12 +1,13 @@ -import DhAppearance from '../../data/settings/Appearance.mjs'; import { getDiceSoNicePreset } from '../../config/generalConfig.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; /** + * @import DhAppearance from '../../data/settings/Appearance.mjs'; * @import {ApplicationClickAction} from "@client/applications/_types.mjs" */ +/** Settings menu for appearance settings */ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(ApplicationV2) { /**@inheritdoc */ static DEFAULT_OPTIONS = { diff --git a/module/applications/settings/homebrewSettings.mjs b/module/applications/settings/homebrewSettings.mjs index 40ea0301b..c4dfc397d 100644 --- a/module/applications/settings/homebrewSettings.mjs +++ b/module/applications/settings/homebrewSettings.mjs @@ -111,7 +111,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli switch (partId) { case 'domains': - const selectedDomain = this.selected.domain ? this.settings.domains[this.selected.domain] : null; + const selectedDomain = this.settings.domains[this.selected.domain] ?? null; const enrichedDescription = selectedDomain ? await foundry.applications.ux.TextEditor.implementation.enrichHTML(selectedDomain.description) : null; @@ -251,8 +251,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli const configTitle = isDowntime ? game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.downtimeMove') : type === 'armorFeatures' - ? game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.armorFeature') - : game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.weaponFeature'); + ? game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.armorFeature') + : game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.weaponFeature'); const editedBase = await game.system.api.applications.sheetConfigs.SettingFeatureConfig.configure( configTitle, diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs index e83dfae47..958d502e1 100644 --- a/module/applications/sheets-configs/action-base-config.mjs +++ b/module/applications/sheets-configs/action-base-config.mjs @@ -1,3 +1,4 @@ +import { AltOutcome } from '../../data/action/altOutcome.mjs'; import { getUnusedDamageTypes } from '../../helpers/utils.mjs'; import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs'; @@ -25,14 +26,11 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) position: { width: 600, height: 'auto' }, actions: { toggleSection: this.toggleSection, - addEffect: this.addEffect, - removeEffect: this.removeEffect, addElement: this.addElement, removeElement: this.removeElement, removeTransformActor: this.removeTransformActor, - editEffect: this.editEffect, - addDamage: this.addDamage, - removeDamage: this.removeDamage, + addDamage: this.onAddDamage, + removeDamage: this.onRemoveDamage, editDoc: this.editDoc, addTrigger: this.addTrigger, removeTrigger: this.removeTrigger, @@ -62,9 +60,13 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) id: 'configuration', template: 'systems/daggerheart/templates/sheets-settings/action-settings/configuration.hbs' }, - effect: { - id: 'effect', - template: 'systems/daggerheart/templates/sheets-settings/action-settings/effect.hbs' + range: { + id: 'range', + template: 'systems/daggerheart/templates/sheets-settings/action-settings/range.hbs' + }, + outcomes: { + id: 'outcomes', + template: 'systems/daggerheart/templates/sheets-settings/action-settings/outcomes.hbs' }, trigger: { id: 'trigger', @@ -89,13 +91,21 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) icon: null, label: 'DAGGERHEART.GENERAL.Tabs.configuration' }, - effect: { + range: { + active: false, + cssClass: '', + group: 'primary', + id: 'range', + icon: null, + label: 'DAGGERHEART.GENERAL.Tabs.range' + }, + outcomes: { active: false, cssClass: '', group: 'primary', - id: 'effect', + id: 'outcomes', icon: null, - label: 'DAGGERHEART.GENERAL.Tabs.effects' + label: 'DAGGERHEART.GENERAL.Tabs.outcomes' }, trigger: { active: false, @@ -109,6 +119,78 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) static CLEAN_ARRAYS = ['cost', 'effects', 'summon']; + /* Needs to consider effect altOutcomes aswell */ + static getOutcomeTabs(action) { + const outcomeKeys = [ + 'default', + ...Object.keys(action.altOutcomes ?? {}).filter(key => action.altOutcomes[key]) + ]; + return outcomeKeys.reduce((acc, key, index) => { + acc[key] = { + active: index === 0, + cssClass: '', + group: 'outcomes', + id: key, + icon: null, + label: game.i18n.localize(CONFIG.DH.ACTIONS.outcomeTypes[key].label), + source: key === 'default' ? action._source : action._source.altOutcomes[key], + fields: key === 'default' ? action.schema.fields : action.schema.fields.altOutcomes.fields[key].fields, + getBasePath: path => (key === 'default' ? path : ['altOutcomes', key, path].join('.')) + }; + return acc; + }, {}); + } + + /* Needs to consider effect altOutcomes aswell */ + static selectOutcome(action, callback) { + const choices = Object.entries(CONFIG.DH.ACTIONS.outcomeTypes).reduce((acc, [key, value]) => { + if (action.altOutcomes[key] === null) acc.push({ id: key, label: game.i18n.localize(value.label) }); + + return acc; + }, []); + const content = new foundry.data.fields.StringField({ + label: game.i18n.localize('Outcome'), + choices, + required: true + }).toFormGroup( + {}, + { + name: 'outcome', + localize: true, + nameAttr: 'value', + labelAttr: 'label' + } + ).outerHTML; + + const callbackWrapper = (_, button) => { + const choiceIndex = button.form.elements.outcome.value; + callback(choices[choiceIndex]?.id); + }; + + const typeDialog = new foundry.applications.api.DialogV2({ + buttons: [ + foundry.utils.mergeObject( + { + action: 'ok', + label: 'Confirm', + icon: 'fas fa-check', + default: true + }, + { callback: callbackWrapper } + ) + ], + content: content, + rejectClose: false, + modal: false, + window: { + title: game.i18n.localize('Add Outcome') + }, + position: { width: 300 } + }); + + typeDialog.render(true); + } + _getTabs(tabs) { for (const v of Object.values(tabs)) { v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active; @@ -155,6 +237,10 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) context.openSection = this.openSection; context.tabs = this._getTabs(this.constructor.TABS); + + context.outcomeTabs = this._getTabs(DHActionBaseConfig.getOutcomeTabs(this.action)); + context.allOutcomesAssigned = Object.keys(context.outcomeTabs).length >= 4; + context.config = CONFIG.DH; if (this.action.damage) { context.allDamageTypesUsed = !getUnusedDamageTypes(this.action.damage.parts).length; @@ -204,7 +290,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) }; } - if (this.action.parent.metadata.isInventoryItem) { + if (this.action.parent.metadata?.isInventoryItem) { options.quantity = { label: 'DAGGERHEART.GENERAL.itemQuantity', group: 'Global' @@ -299,10 +385,12 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); } - static addDamage(_event) { + static onAddDamage(_event, button) { if (!this.action.damage.parts) return; - const choices = getUnusedDamageTypes(this.action._source.damage.parts); + const outcome = button.dataset.outcome; + const outcomeData = outcome === 'default' ? this.action._source : this.action._source.altOutcomes[outcome]; + const choices = getUnusedDamageTypes(outcomeData.damage.parts); const content = new foundry.data.fields.StringField({ label: game.i18n.localize('Damage Type'), choices, @@ -322,24 +410,23 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) const type = choices[button.form.elements.type.value].value; const part = this.action.schema.fields.damage.fields.parts.element.getInitialValue(); part.applyTo = type; - if (type === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) + if (type === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { part.type = this.action.schema.fields.damage.fields.parts.element.fields.type.element.initial; - - data.damage.parts[type] = part; + } + if (outcome !== 'default') data.altOutcomes[outcome] ??= new AltOutcome(); + (outcome === 'default' ? data : data.altOutcomes[outcome]).damage.parts[type] = part; this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); }; const typeDialog = new foundry.applications.api.DialogV2({ buttons: [ - foundry.utils.mergeObject( - { - action: 'ok', - label: 'Confirm', - icon: 'fas fa-check', - default: true - }, - { callback: callback } - ) + { + action: 'ok', + label: 'Confirm', + icon: 'fas fa-check', + default: true, + callback + } ], content: content, rejectClose: false, @@ -353,12 +440,11 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) typeDialog.render(true); } - static removeDamage(_event, button) { + static onRemoveDamage(_event, button) { if (!this.action.damage.parts) return; const data = this.action.toObject(); - const key = button.dataset.key; - delete data.damage.parts[key]; - data.damage.parts[`${key}`] = _del; + const { key, outcome } = button.dataset; + (outcome === 'default' ? data : data.altOutcomes[outcome]).damage.parts[key] = _del; this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); } @@ -448,11 +534,6 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); } - /** Specific implementation in extending classes **/ - static async addEffect(_event) {} - static removeEffect(_event, _button) {} - static editEffect(_event) {} - async close(options) { this.tabGroups.primary = 'base'; await super.close(options); diff --git a/module/applications/sheets-configs/action-config.mjs b/module/applications/sheets-configs/action-config.mjs index ef529e9b0..444a60fee 100644 --- a/module/applications/sheets-configs/action-config.mjs +++ b/module/applications/sheets-configs/action-config.mjs @@ -1,3 +1,4 @@ +import { AltOutcome } from '../../data/action/altOutcome.mjs'; import DHActionBaseConfig from './action-base-config.mjs'; export default class DHActionConfig extends DHActionBaseConfig { @@ -5,6 +6,8 @@ export default class DHActionConfig extends DHActionBaseConfig { ...DHActionBaseConfig.DEFAULT_OPTIONS, actions: { ...DHActionBaseConfig.DEFAULT_OPTIONS.actions, + addOutcome: this.onAddOutcome, + removeOutcome: this.onRemoveOutcome, addEffect: this.addEffect, removeEffect: this.removeEffect, editEffect: this.editEffect @@ -19,6 +22,23 @@ export default class DHActionConfig extends DHActionBaseConfig { return context; } + static onAddOutcome() { + const data = this.action.toObject(); + + DHActionBaseConfig.selectOutcome(this.action, key => { + if (!key) return; + data.altOutcomes[key] = new AltOutcome(); + this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); + }); + } + + static onRemoveOutcome(_event, button) { + const { outcome } = button.dataset; + const data = this.action.toObject(); + data.altOutcomes[outcome] = null; + this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); + } + static async addEffect(event) { const { areaIndex } = event.target.dataset; if (!this.action.effects) return; diff --git a/module/applications/sheets-configs/adversary-settings.mjs b/module/applications/sheets-configs/adversary-settings.mjs index 57405675e..ff3f3039b 100644 --- a/module/applications/sheets-configs/adversary-settings.mjs +++ b/module/applications/sheets-configs/adversary-settings.mjs @@ -10,11 +10,7 @@ export default class DHAdversarySettings extends DHBaseActorSettings { actions: { addExperience: DHAdversarySettings.#addExperience, removeExperience: DHAdversarySettings.#removeExperience - }, - dragDrop: [ - { dragSelector: null, dropSelector: '.tab.features' }, - { dragSelector: '.feature-item', dropSelector: null } - ] + } }; /**@override */ @@ -38,7 +34,8 @@ export default class DHAdversarySettings extends DHBaseActorSettings { }, features: { id: 'features', - template: 'systems/daggerheart/templates/sheets-settings/adversary-settings/features.hbs' + template: 'systems/daggerheart/templates/sheets-settings/adversary-settings/features.hbs', + scrollable: [''] } }; @@ -54,12 +51,16 @@ export default class DHAdversarySettings extends DHBaseActorSettings { async _prepareContext(options) { const context = await super._prepareContext(options); - const featureForms = ['passive', 'action', 'reaction']; - context.features = context.document.system.features.sort((a, b) => - a.system.featureForm !== b.system.featureForm - ? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm) - : a.sort - b.sort - ); + // Get feature groups. Uncategorized go to actions + const featureFormsTypes = ['passive', 'action', 'reaction']; + const features = this.document.system.features.sort((a, b) => a.sort - b.sort); + const featureGroups = featureFormsTypes.map(t => ({ + featureForm: t, + label: _loc(CONFIG.DH.ITEM.featureForm[t]), + features: features.filter(f => f.system.featureForm === t) + })); + featureGroups[1].features.push(...features.filter(f => !featureFormsTypes.includes(f.system.featureForm))); + context.featureGroups = featureGroups; return context; } @@ -97,32 +98,4 @@ export default class DHAdversarySettings extends DHBaseActorSettings { await this.actor.update({ [`system.experiences.${target.dataset.experience}`]: _del }); } - - async _onDragStart(event) { - const featureItem = event.currentTarget.closest('.feature-item'); - - if (featureItem) { - const feature = this.actor.items.get(featureItem.id); - const featureData = { type: 'Item', uuid: feature.uuid, fromInternal: true }; - event.dataTransfer.setData('text/plain', JSON.stringify(featureData)); - event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0); - } - } - - async _onDrop(event) { - event.stopPropagation(); - const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); - - const item = await fromUuid(data.uuid); - if (item?.type === 'feature') { - if (data.fromInternal && item.parent?.uuid === this.actor.uuid) { - return; - } - - const itemData = item.toObject(); - delete itemData._id; - - await this.actor.createEmbeddedDocuments('Item', [itemData]); - } - } } diff --git a/module/applications/sheets-configs/environment-settings.mjs b/module/applications/sheets-configs/environment-settings.mjs index 6d74f9c66..954d63816 100644 --- a/module/applications/sheets-configs/environment-settings.mjs +++ b/module/applications/sheets-configs/environment-settings.mjs @@ -15,7 +15,7 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings { dragDrop: [ { dragSelector: null, dropSelector: '.category-container' }, { dragSelector: null, dropSelector: '.tab.features' }, - { dragSelector: '.feature-item', dropSelector: null } + { dragSelector: '.feature-item, .inventory-item[data-type="adversary"]', dropSelector: null } ] }; @@ -32,11 +32,13 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings { }, features: { id: 'features', - template: 'systems/daggerheart/templates/sheets-settings/environment-settings/features.hbs' + template: 'systems/daggerheart/templates/sheets-settings/environment-settings/features.hbs', + scrollable: [''] }, adversaries: { id: 'adversaries', - template: 'systems/daggerheart/templates/sheets-settings/environment-settings/adversaries.hbs' + template: 'systems/daggerheart/templates/sheets-settings/environment-settings/adversaries.hbs', + scrollable: [''] } }; @@ -52,12 +54,16 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings { async _prepareContext(options) { const context = await super._prepareContext(options); - const featureForms = ['passive', 'action', 'reaction']; - context.features = context.document.system.features.sort((a, b) => - a.system.featureForm !== b.system.featureForm - ? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm) - : a.sort - b.sort - ); + // Get feature groups. Uncategorized go to actions + const featureFormsTypes = ['passive', 'action', 'reaction']; + const features = this.document.system.features.sort((a, b) => a.sort - b.sort); + const featureGroups = featureFormsTypes.map(t => ({ + featureForm: t, + label: _loc(CONFIG.DH.ITEM.featureForm[t]), + features: features.filter(f => f.system.featureForm === t) + })); + featureGroups[1].features.push(...features.filter(f => !featureFormsTypes.includes(f.system.featureForm))); + context.featureGroups = featureGroups; return context; } @@ -110,33 +116,30 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings { } async _onDragStart(event) { - const featureItem = event.currentTarget.closest('.feature-item'); - - if (featureItem) { - const feature = this.actor.items.get(featureItem.id); - const featureData = { type: 'Item', uuid: feature.uuid, fromInternal: true }; - event.dataTransfer.setData('text/plain', JSON.stringify(featureData)); - event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0); + const element = event.currentTarget.closest('.inventory-item[data-type=adversary]'); + if (element) { + const adversaryData = { type: 'Actor', uuid: element.dataset.itemUuid }; + event.dataTransfer.setData('text/plain', JSON.stringify(adversaryData)); + event.dataTransfer.setDragImage(element, 60, 0); + } else { + return super._onDragStart(event); } } async _onDrop(event) { event.stopPropagation(); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); - const item = await fromUuid(data.uuid); - if (data.fromInternal && item?.parent?.uuid === this.actor.uuid) return; - - if (item.type === 'adversary' && event.target.closest('.category-container')) { + const doc = await fromUuid(data.uuid); + if (doc?.type === 'adversary' && event.target.closest('.category-container')) { const target = event.target.closest('.category-container'); const path = `system.potentialAdversaries.${target.dataset.potentialAdversary}.adversaries`; const current = foundry.utils.getProperty(this.actor, path).map(x => x.uuid); - await this.actor.update({ - [path]: [...current, item.uuid] - }); - this.render(); - } else if (item.type === 'feature' && event.target.closest('.tab.features')) { - await this.actor.createEmbeddedDocuments('Item', [item]); - this.render(); + if (!current.includes(doc.uuid)) { + await this.actor.update({ [path]: [...current, doc.uuid] }); + } + return; } + + return super._onDrop(event); } } diff --git a/module/applications/sheets-configs/npc-settings.mjs b/module/applications/sheets-configs/npc-settings.mjs index c187877cd..33cbc270d 100644 --- a/module/applications/sheets-configs/npc-settings.mjs +++ b/module/applications/sheets-configs/npc-settings.mjs @@ -27,7 +27,8 @@ export default class DHNPCSettings extends DHBaseActorSettings { }, features: { id: 'features', - template: 'systems/daggerheart/templates/sheets-settings/npc-settings/features.hbs' + template: 'systems/daggerheart/templates/sheets-settings/npc-settings/features.hbs', + scrollable: [''] } }; @@ -43,43 +44,17 @@ export default class DHNPCSettings extends DHBaseActorSettings { async _prepareContext(options) { const context = await super._prepareContext(options); - const featureForms = ['passive', 'action', 'reaction']; - context.features = context.document.system.features.sort((a, b) => - a.system.featureForm !== b.system.featureForm - ? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm) - : a.sort - b.sort - ); + // Get feature groups. Uncategorized go to actions + const featureFormsTypes = ['passive', 'action', 'reaction']; + const features = this.document.system.features.sort((a, b) => a.sort - b.sort); + const featureGroups = featureFormsTypes.map(t => ({ + featureForm: t, + label: _loc(CONFIG.DH.ITEM.featureForm[t]), + features: features.filter(f => f.system.featureForm === t) + })); + featureGroups[1].features.push(...features.filter(f => !featureFormsTypes.includes(f.system.featureForm))); + context.featureGroups = featureGroups; return context; } - - /* -------------------------------------------- */ - - async _onDragStart(event) { - const featureItem = event.currentTarget.closest('.feature-item'); - - if (featureItem) { - const feature = this.actor.items.get(featureItem.id); - const featureData = { type: 'Item', uuid: feature.uuid, fromInternal: true }; - event.dataTransfer.setData('text/plain', JSON.stringify(featureData)); - event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0); - } - } - - async _onDrop(event) { - event.stopPropagation(); - const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); - - const item = await fromUuid(data.uuid); - if (item?.type === 'feature') { - if (data.fromInternal && item.parent?.uuid === this.actor.uuid) { - return; - } - - const itemData = item.toObject(); - delete itemData._id; - - await this.actor.createEmbeddedDocuments('Item', [itemData]); - } - } } diff --git a/module/applications/sheets-configs/setting-feature-config.mjs b/module/applications/sheets-configs/setting-feature-config.mjs index a5bcc4f98..531158cde 100644 --- a/module/applications/sheets-configs/setting-feature-config.mjs +++ b/module/applications/sheets-configs/setting-feature-config.mjs @@ -168,8 +168,8 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App updatedEffects = deleteEffect ? currentEffects.filter(x => x.id !== effectData.id) : existingEffectIndex === -1 - ? [...currentEffects, effectData] - : currentEffects.with(existingEffectIndex, effectData); + ? [...currentEffects, effectData] + : currentEffects.with(existingEffectIndex, effectData); await this.updateMove({ [`${this.movePath}.effects`]: updatedEffects }); @@ -235,9 +235,9 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App return this.hasEffects ? tabs : Object.keys(tabs).reduce((acc, key) => { - if (key !== 'effects') acc[key] = tabs[key]; - return acc; - }, {}); + if (key !== 'effects') acc[key] = tabs[key]; + return acc; + }, {}); } /** @override */ diff --git a/module/applications/sheets/actors/_types.d.ts b/module/applications/sheets/actors/_types.d.ts new file mode 100644 index 000000000..89adc57b6 --- /dev/null +++ b/module/applications/sheets/actors/_types.d.ts @@ -0,0 +1,8 @@ +import DhCompanion from '../../../data/actor/companion.mjs'; +import DhpActor from '../../../documents/actor.mjs'; + +declare module './companion.mjs' { + export default interface DhCompanionSheet { + actor: DhpActor; + } +} diff --git a/module/applications/sheets/actors/adversary.mjs b/module/applications/sheets/actors/adversary.mjs index 06dd4a0f0..853803927 100644 --- a/module/applications/sheets/actors/adversary.mjs +++ b/module/applications/sheets/actors/adversary.mjs @@ -103,7 +103,7 @@ export default class AdversarySheet extends DHBaseActorSheet { context.resources.stress.emptyPips = context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0; - const featureForms = ['passive', 'action', 'reaction']; + const featureForms = Object.keys(CONFIG.DH.ITEM.featureForm); context.features = this.document.system.features.sort((a, b) => a.system.featureForm !== b.system.featureForm ? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm) @@ -131,6 +131,15 @@ export default class AdversarySheet extends DHBaseActorSheet { return context; } + /** @inheritdoc */ + _prepareTabs(group) { + const result = super._prepareTabs(group); + if (group === 'primary') { + result.notes.empty = !this.document.system.notes?.trim(); + } + return result; + } + /**@inheritdoc */ _attachPartListeners(partId, htmlElement, options) { super._attachPartListeners(partId, htmlElement, options); @@ -186,15 +195,6 @@ export default class AdversarySheet extends DHBaseActorSheet { }); } - /** @inheritdoc */ - async _onDragStart(event) { - const inventoryItem = event.currentTarget.closest('.inventory-item'); - if (inventoryItem) { - event.dataTransfer.setDragImage(inventoryItem.querySelector('img'), 60, 0); - } - super._onDragStart(event); - } - /* -------------------------------------------- */ /* Application Clicks Actions */ /* -------------------------------------------- */ diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 5d0e7144d..3a60e7ca7 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -356,7 +356,7 @@ export default class CharacterSheet extends DHBaseActorSheet { const levelups = Object.values(actor.system.levelData?.levelups) ?? []; const uuid = item.uuid; - const sourceUuid = item._stats.compendiumSource; // on older characters this may be missing + const sourceUuid = item.sourceUuid; // on older characters this may be missing return levelups.some(data => { if (item.type === 'subclass') { const selectedSubclasses = data.selections.map(s => s.secondaryData?.subclass).filter(s => !!s); @@ -785,11 +785,11 @@ export default class CharacterSheet extends DHBaseActorSheet { filter: key === 'subclasses' ? { - 'system.linkedClass.uuid': { - key: 'system.linkedClass.uuid', - value: this.document.system.class.value?._stats.compendiumSource - } - } + 'system.linkedClass.uuid': { + key: 'system.linkedClass.uuid', + value: this.document.system.class.value?._stats.compendiumSource + } + } : undefined, render: { noFolder: true @@ -809,7 +809,7 @@ export default class CharacterSheet extends DHBaseActorSheet { /* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */ const costResources = - result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) || + result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value })) || {}; result.resourceUpdates.addResources(costResources); await result.resourceUpdates.updateResources(); @@ -1193,15 +1193,6 @@ export default class CharacterSheet extends DHBaseActorSheet { }); } - /** @inheritdoc */ - async _onDragStart(event) { - const inventoryItem = event.currentTarget.closest('.inventory-item'); - if (inventoryItem) { - event.dataTransfer.setDragImage(inventoryItem.querySelector('img'), 60, 0); - } - super._onDragStart(event); - } - async _onDropItem(event, item) { const setupCriticalItemTypes = ['class', 'subclass', 'ancestry', 'community']; if (this.document.system.needsCharacterSetup && setupCriticalItemTypes.includes(item.type)) { diff --git a/module/applications/sheets/actors/companion.mjs b/module/applications/sheets/actors/companion.mjs index a01b4a64f..4b5b5a56b 100644 --- a/module/applications/sheets/actors/companion.mjs +++ b/module/applications/sheets/actors/companion.mjs @@ -62,9 +62,7 @@ export default class DhCompanionSheet extends DHBaseActorSheet { await this.document.update({ 'system.resources.stress.value': newValue }); } - /** - * - */ + /** @this {DhCompanionSheet} **/ static async #actionRoll(event) { const partner = this.actor.system.partner; const config = { @@ -80,6 +78,7 @@ export default class DhCompanionSheet extends DHBaseActorSheet { const result = await partner.diceRoll(config); this.consumeResource(result?.costs); + result?.resourceUpdates.updateResources(); } // Remove when Action Refactor part #2 done diff --git a/module/applications/sheets/actors/environment.mjs b/module/applications/sheets/actors/environment.mjs index f8ff74a67..60fb9071a 100644 --- a/module/applications/sheets/actors/environment.mjs +++ b/module/applications/sheets/actors/environment.mjs @@ -72,13 +72,22 @@ export default class DhpEnvironment extends DHBaseActorSheet { return applicationOptions; } + /** @inheritdoc */ + _prepareTabs(group) { + const result = super._prepareTabs(group); + if (group === 'primary') { + result.potentialAdversaries.empty = foundry.utils.isEmpty(this.document.system.potentialAdversaries); + result.notes.empty = !this.document.system.notes?.trim(); + } + return result; + } + /**@inheritdoc */ async _preparePartContext(partId, context, options) { context = await super._preparePartContext(partId, context, options); switch (partId) { case 'header': await this._prepareHeaderContext(context, options); - break; case 'features': await this._prepareFeaturesContext(context, options); diff --git a/module/applications/sheets/actors/party.mjs b/module/applications/sheets/actors/party.mjs index 927a88109..3af8ea5f4 100644 --- a/module/applications/sheets/actors/party.mjs +++ b/module/applications/sheets/actors/party.mjs @@ -162,9 +162,9 @@ export default class Party extends DHBaseActorSheet { difficulty: actor.system.difficulty, traits: actor.system.traits ? traits.map(t => ({ - label: game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${t}.short`), - value: actor.system.traits[t].value - })) + label: game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${t}.short`), + value: actor.system.traits[t].value + })) : null, weapons }); @@ -306,7 +306,7 @@ export default class Party extends DHBaseActorSheet { static async downtimeMoveQuery({ actorId, downtimeType }) { const actor = await foundry.utils.fromUuid(actorId); - if (!actor || !actor?.isOwner) reject(); + if (!actor || !actor?.isOwner) return; new game.system.api.applications.dialogs.Downtime(actor, downtimeType === 'shortRest').render({ force: true }); diff --git a/module/applications/sheets/api/actor-setting.mjs b/module/applications/sheets/api/actor-setting.mjs index 738f70024..fa4340344 100644 --- a/module/applications/sheets/api/actor-setting.mjs +++ b/module/applications/sheets/api/actor-setting.mjs @@ -1,13 +1,15 @@ import DHApplicationMixin from './application-mixin.mjs'; -const { DocumentSheetV2 } = foundry.applications.api; +const { ActorSheetV2 } = foundry.applications.sheets; -/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ +/** + * @typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction + */ /** * Base settings sheet for Daggerheart actors. - * @extends {DHApplicationMixin} + * @extends {DHApplicationMixin} */ -export default class DHBaseActorSettings extends DHApplicationMixin(DocumentSheetV2) { +export default class DHBaseActorSettings extends DHApplicationMixin(ActorSheetV2) { /**@inheritdoc */ static DEFAULT_OPTIONS = { classes: ['dialog'], @@ -23,7 +25,7 @@ export default class DHBaseActorSettings extends DHApplicationMixin(DocumentShee }, dragDrop: [ { dragSelector: null, dropSelector: '.tab.features' }, - { dragSelector: '.feature-item', dropSelector: null } + { dragSelector: '.feature-item, .inventory-item[data-type="feature"]', dropSelector: null } ] }; @@ -34,7 +36,7 @@ export default class DHBaseActorSettings extends DHApplicationMixin(DocumentShee return options; } - /**@returns {foundry.documents.Actor} */ + /** @returns {foundry.documents.Actor} */ get actor() { return this.document; } @@ -73,4 +75,32 @@ export default class DHBaseActorSettings extends DHApplicationMixin(DocumentShee return context; } + + async _onDragStart(event) { + const featureItemEl = event.currentTarget.closest('.feature-item, .inventory-item[data-type="feature"]'); + const feature = this.actor.items.get(featureItemEl?.dataset.itemId); + if (feature && event.target.closest('.tab.features')) { + const featureData = { ...feature.toDragData(), fromInternal: true }; + event.dataTransfer.setData('text/plain', JSON.stringify(featureData)); + event.dataTransfer.setDragImage(featureItemEl.querySelector('img'), 60, 0); + } + } + + async _onDrop(event) { + event.stopPropagation(); + const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); + const item = await fromUuid(data.uuid); + if (item?.type === 'feature') { + if (data.fromInternal && item.parent?.uuid === this.actor.uuid) { + return super._onDrop(event); + } + + const itemData = item.toObject(); + delete itemData._id; + await this.actor.createEmbeddedDocuments('Item', [itemData]); + } + } + + /** Setting sheets do not auto extend */ + async _autoExpandDescriptions() {} } diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 2b0c3e55f..0168f46d6 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -4,6 +4,7 @@ import { getDocFromElement, getDocFromElementSync, tagifyElement } from '../../. const typeSettingsMap = { character: 'extendCharacterDescriptions', adversary: 'extendAdversaryDescriptions', + npc: 'extendAdversaryDescriptions', environment: 'extendEnvironmentDescriptions', ancestry: 'extendItemDescriptions', community: 'extendItemDescriptions', @@ -262,7 +263,7 @@ export default function DHApplicationMixin(Base) { if (!!this.options.contextMenus.length) this._createContextMenus(); - this.#autoExtendDescriptions(context); + this._autoExpandDescriptions(context); } /** @inheritDoc */ @@ -361,18 +362,17 @@ export default function DHApplicationMixin(Base) { */ async _onDragStart(event) { const inventoryItem = event.currentTarget.closest('.inventory-item'); - if (inventoryItem) { - const { type, itemUuid } = inventoryItem.dataset; - if (type === 'effect') { - const effect = await foundry.utils.fromUuid(itemUuid); - const effectData = { - type: 'ActiveEffect', - data: { ...effect.toObject(), _id: null }, - fromInternal: this.document.uuid - }; - event.dataTransfer.setData('text/plain', JSON.stringify(effectData)); - event.dataTransfer.setDragImage(inventoryItem.querySelector('img'), 60, 0); - } + if (!inventoryItem) return; + + const { type, itemUuid } = inventoryItem.dataset; + const effect = type === 'effect' ? await foundry.utils.fromUuid(itemUuid) : null; + if (effect) { + const effectData = { + ...effect.toDragData(), + fromInternal: this.document.uuid + }; + event.dataTransfer.setData('text/plain', JSON.stringify(effectData)); + event.dataTransfer.setDragImage(inventoryItem.querySelector('img'), 60, 0); } } @@ -382,14 +382,33 @@ export default function DHApplicationMixin(Base) { * @protected */ _onDrop(event) { + // Potentially handle subclasses that dont descend from actor/item sheet. event.stopPropagation(); - const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); - if (data.type === 'ActiveEffect' && data.fromInternal !== this.document.uuid) { - this.document.createEmbeddedDocuments('ActiveEffect', [data.data]); - } else { - // Fallback to super, but note that item sheets do not have this function - return super._onDrop?.(event); + return super._onDrop?.(event); + } + + /** @inheritdoc */ + _onSortItem(event, item) { + // If we are dragging a feature past its allowed feature form, put it in the front or in the back + const doc = this.actor.items.get(item.id); + const dropTargetEl = event.target.closest('[data-item-id]'); + const dropTarget = this.actor.items.get(dropTargetEl?.dataset.itemId); + if (doc?.type === 'feature' && dropTarget?.type === 'feature' && doc.system.featureForm !== dropTarget.system.featureForm) { + const siblings = this.actor.itemTypes.feature + .filter(f => f.system.featureForm === doc.system.featureForm) + .sort((a, b) => a.sort - b.sort); + if (siblings.length > 1) { + const featureForms = Object.keys(CONFIG.DH.ITEM.featureForm); + const thisFeatureIdx = featureForms.indexOf(doc.system.featureForm); + const targetFeatureIdx = featureForms.indexOf(dropTarget.system.featureForm); + const target = targetFeatureIdx < thisFeatureIdx ? siblings[0] : siblings.at(-1); + const sortUpdates = foundry.utils.performIntegerSort(doc, { target, siblings }); + const updateData = sortUpdates.map(u => ({ ...u.update, _id: u.target._id })); + return this.actor.updateEmbeddedDocuments('Item', updateData); + } } + + return super._onSortItem?.(event, item); } /* -------------------------------------------- */ @@ -499,7 +518,7 @@ export default function DHApplicationMixin(Base) { const doc = await getDocFromElement(target), action = doc?.system?.attack ?? doc; const config = action.prepareConfig(event); - config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects( + config.effects = await game.system.api.data.actions.actionsTypes.base.getActionRelevantEffects( this.document, doc ); @@ -584,7 +603,7 @@ export default function DHApplicationMixin(Base) { const doc = await fromUuid(itemUuid); //get inventory-item description element - const descriptionElement = el.querySelector('.invetory-description'); + const descriptionElement = el.querySelector('.inventory-description'); if (!doc || !descriptionElement) continue; // localize the description (idk if it's still necessary) @@ -612,8 +631,9 @@ export default function DHApplicationMixin(Base) { /** * Extend inventory description when enabled in settings. * @returns {Promise} + * @protected */ - async #autoExtendDescriptions(context) { + async _autoExpandDescriptions(context) { const inventoryItems = this.element.querySelectorAll('.inventory-item[data-item-uuid]'); for (const el of inventoryItems) { // Get the doc uuid from the element @@ -716,16 +736,16 @@ export default function DHApplicationMixin(Base) { * @type {ApplicationClickAction} */ static async #onCreateDoc(event, target) { - const { documentClass, type, inVault, disabled } = target.dataset; + const { documentClass, type, inVault, disabled, featureForm } = target.dataset; const parentIsItem = this.document.documentName === 'Item'; const featureOnCharacter = this.document.parent?.type === 'character' && type === 'feature'; const parent = featureOnCharacter ? this.document.parent : parentIsItem && documentClass === 'Item' - ? type === 'action' - ? this.document.system - : null - : this.document; + ? type === 'action' + ? this.document.system + : null + : this.document; let systemData = {}; if (featureOnCharacter) { @@ -734,6 +754,7 @@ export default function DHApplicationMixin(Base) { identifier: this.document.system.isMulticlass ? 'multiclass' : null }; } + if (featureForm) systemData.featureForm = featureForm; const cls = type === 'action' ? game.system.api.models.actions.actionsTypes.base : getDocumentClass(documentClass); diff --git a/module/applications/sheets/api/base-actor.mjs b/module/applications/sheets/api/base-actor.mjs index 7b8208227..007b641b1 100644 --- a/module/applications/sheets/api/base-actor.mjs +++ b/module/applications/sheets/api/base-actor.mjs @@ -1,10 +1,12 @@ import { getDocFromElement, itemIsIdentical } from '../../../helpers/utils.mjs'; -import DHBaseActorSettings from './actor-setting.mjs'; import DHApplicationMixin from './application-mixin.mjs'; const { ActorSheetV2 } = foundry.applications.sheets; -/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ +/** + * @import DHBaseActorSettings from './actor-setting.mjs'; + * @typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction + */ /** * A base actor sheet extending {@link ActorSheetV2} via {@link DHApplicationMixin} @@ -210,7 +212,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { const doc = await getDocFromElement(target), action = doc?.system?.attack ?? doc; const config = action.prepareConfig(event); - config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects( + config.effects = await game.system.api.data.actions.actionsTypes.base.getActionRelevantEffects( this.document, doc ); @@ -377,7 +379,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { action: 'update', documentName: 'Item', parent: targetActor, - updates: [{ '_id': existing.id, 'system.quantity': existing.system.quantity + quantity }] + updates: [{ _id: existing.id, 'system.quantity': existing.system.quantity + quantity }] }); } else { const itemsToCreate = []; @@ -410,7 +412,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { action: 'update', documentName: 'Item', parent: originActor, - updates: [{ '_id': item.id, 'system.quantity': item.system.quantity - quantity }] + updates: [{ _id: item.id, 'system.quantity': item.system.quantity - quantity }] }); } @@ -447,13 +449,15 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { const item = await getDocFromElement(event.target); if (item) { + const inventoryItem = event.currentTarget.closest('.inventory-item'); const dragData = { + ...item.toDragData(), originActor: this.document.uuid, - originId: item.id, - type: item.documentName, - uuid: item.uuid + originId: item.id }; event.dataTransfer.setData('text/plain', JSON.stringify(dragData)); + if (inventoryItem) event.dataTransfer.setDragImage(inventoryItem.querySelector('img'), 60, 0); + return; } super._onDragStart(event); diff --git a/module/applications/sheets/api/item-attachment-sheet.mjs b/module/applications/sheets/api/item-attachment-sheet.mjs index bcf2fc3c9..ea4d5352e 100644 --- a/module/applications/sheets/api/item-attachment-sheet.mjs +++ b/module/applications/sheets/api/item-attachment-sheet.mjs @@ -29,16 +29,6 @@ export default function ItemAttachmentSheet(Base) { } }; - async _preparePartContext(partId, context) { - await super._preparePartContext(partId, context); - - if (partId === 'attachments') { - context.attachedItems = await prepareAttachmentContext(this.document); - } - - return context; - } - async _onDrop(event) { const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); diff --git a/module/applications/sheets/items/subclass.mjs b/module/applications/sheets/items/subclass.mjs index e9d8370e9..650d20888 100644 --- a/module/applications/sheets/items/subclass.mjs +++ b/module/applications/sheets/items/subclass.mjs @@ -59,7 +59,7 @@ export default class SubclassSheet extends DHBaseItemSheet { const item = await fromUuid(data.uuid); const itemType = data.type === 'ActiveEffect' ? data.type : item.type; if (itemType === 'class') { - const uuid = item._stats.compendiumSource ?? item.uuid; + const uuid = item.sourceUuid; if (this.document.system.linkedClass !== uuid) { await this.document.update({ 'system.linkedClass': uuid }); // Re-render all class sheets for instant feedback diff --git a/module/applications/sidebar/tabs/actorDirectory.mjs b/module/applications/sidebar/tabs/actorDirectory.mjs index 89da14262..4d00afa2f 100644 --- a/module/applications/sidebar/tabs/actorDirectory.mjs +++ b/module/applications/sidebar/tabs/actorDirectory.mjs @@ -1,6 +1,6 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs.ActorDirectory { static DEFAULT_OPTIONS = { - renderUpdateKeys: ['system.levelData.level.current', 'system.partner', 'system.tier'] + renderUpdateKeys: ['system.levelData.level.current', 'system.partner', 'system.tier', 'system.type'] }; static _entryPartial = 'systems/daggerheart/templates/ui/sidebar/actor-document-partial.hbs'; @@ -13,8 +13,8 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs. return document.type === 'adversary' ? game.i18n.localize(adversaryTypes[document.system.type]?.label ?? 'TYPES.Actor.adversary') : document.type === 'environment' - ? game.i18n.localize(environmentTypes[document.system.type]?.label ?? 'TYPES.Actor.environment') - : null; + ? game.i18n.localize(environmentTypes[document.system.type]?.label ?? 'TYPES.Actor.environment') + : null; }; } diff --git a/module/applications/ui/_module.mjs b/module/applications/ui/_module.mjs index 80d3ebe45..e30950b53 100644 --- a/module/applications/ui/_module.mjs +++ b/module/applications/ui/_module.mjs @@ -8,3 +8,4 @@ export { default as DhHotbar } from './hotbar.mjs'; export { default as DhSceneNavigation } from './sceneNavigation.mjs'; export { ItemBrowser } from './itemBrowser.mjs'; export { default as DhProgress } from './progress.mjs'; +export { default as DhGamePause } from './gamePause.mjs'; diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index 7036a5dfc..199ee87d6 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -41,8 +41,8 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo const advantage = rollCommand.advantage ? CONFIG.DH.ACTIONS.advantageState.advantage.value : rollCommand.disadvantage - ? CONFIG.DH.ACTIONS.advantageState.disadvantage.value - : undefined; + ? CONFIG.DH.ACTIONS.advantageState.disadvantage.value + : undefined; const difficulty = rollCommand.difficulty; const grantResources = rollCommand.grantResources; @@ -50,8 +50,8 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo const title = (flavor ?? traitValue) ? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { - ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label) - }) + ability: game.i18n.localize(CONFIG.DH.ACTOR.abilities[traitValue].label) + }) : game.i18n.localize('DAGGERHEART.GENERAL.duality'); enrichedDualityRoll({ diff --git a/module/applications/ui/countdownEdit.mjs b/module/applications/ui/countdownEdit.mjs index b418107c9..1dbc56bea 100644 --- a/module/applications/ui/countdownEdit.mjs +++ b/module/applications/ui/countdownEdit.mjs @@ -35,7 +35,7 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio static PARTS = { countdowns: { - template: 'systems/daggerheart/templates/ui/countdown-edit.hbs', + template: 'systems/daggerheart/templates/ui/countdowns/countdown-edit.hbs', scrollable: ['.expanded-view', '.edit-content'] } }; @@ -45,7 +45,7 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio 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.countdownProgressionTypes = CONFIG.DH.GENERAL.countdownProgressionTypes; context.countdownLoopingTypes = CONFIG.DH.GENERAL.countdownLoopingTypes; context.hideNewCountdowns = this.hideNewCountdowns; @@ -56,13 +56,13 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio ? countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id ? 'DAGGERHEART.UI.Countdowns.increasingLoop' : countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id - ? 'DAGGERHEART.UI.Countdowns.decreasingLoop' - : 'DAGGERHEART.UI.Countdowns.loop' + ? 'DAGGERHEART.UI.Countdowns.decreasingLoop' + : 'DAGGERHEART.UI.Countdowns.loop' : null; const randomizeValid = !new Roll(countdown.progress.startFormula ?? '').isDeterministic; acc[key] = { ...countdown, - typeName: game.i18n.localize(CONFIG.DH.GENERAL.countdownBaseTypes[countdown.type].label), + typeName: game.i18n.localize(CONFIG.DH.GENERAL.countdownTypes[countdown.type].label), progress: { ...countdown.progress, typeName: game.i18n.localize( @@ -148,11 +148,11 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio } 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 } - }); + 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 }); } diff --git a/module/applications/ui/countdowns.mjs b/module/applications/ui/countdowns.mjs index 76e2b3993..5cf791003 100644 --- a/module/applications/ui/countdowns.mjs +++ b/module/applications/ui/countdowns.mjs @@ -11,9 +11,14 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; */ export default class DhCountdowns extends HandlebarsApplicationMixin(ApplicationV2) { + previousCountdownData = null; + changedCountdownsForAnimation = new Set(); + constructor(options = {}) { super(options); + this.previousCountdownData = + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns).countdowns; this.setupHooks(); } @@ -31,9 +36,10 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application minimizable: false }, actions: { - toggleViewMode: DhCountdowns.#toggleViewMode, - editCountdowns: DhCountdowns.#editCountdowns, - loopCountdown: DhCountdowns.#loopCountdown, + toggleViewMode: DhCountdowns.#onToggleViewMode, + toggleCountdownTypes: DhCountdowns.#onToggleCountdownTypes, + editCountdowns: DhCountdowns.#onEditCountdowns, + loopCountdown: DhCountdowns.#onLoopCountdown, decreaseCountdown: (_, target) => this.editCountdown(false, target), increaseCountdown: (_, target) => this.editCountdown(true, target) }, @@ -48,11 +54,20 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application static PARTS = { resources: { root: true, - template: 'systems/daggerheart/templates/ui/countdowns.hbs' + template: 'systems/daggerheart/templates/ui/countdowns/countdowns-view.hbs' } }; - /**@inheritdoc */ + /** + * Returns all visible countdown types + * @returns {string[]} + */ + get visibleCountdownTypes() { + const { encounter, narrative } = CONFIG.DH.GENERAL.countdownTypes; + return game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownTypeModes) + ?? [encounter.id, narrative.id]; + } + async _renderFrame(options) { const frame = await super._renderFrame(options); @@ -65,6 +80,57 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application return frame; } + + async _onRender(context, options) { + await super._onRender(context, options); + + /* Handle rendering/hiding/positioning of the countdown UI */ + this.element.hidden = !game.user.isGM && this.#getCountdowns().length === 0; + if (options?.force) { + document.getElementById('ui-right-column-1')?.appendChild(this.element); + } + + this.previousCountdownData = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns) + .countdowns; + + /* Handle animations to draw attention to countdown values changing */ + const typesToAnimate = new Set(); + for (const countdownKey of this.changedCountdownsForAnimation) { + const shimmerAnimation = [ + { backgroundPositionX: '98%' }, + { backgroundPositionX: '0%' } + ]; + const shimmerTiming = { + duration: 1000, + iterations: 1 + }; + + const element = this.element.querySelector(`.countdown-container[data-countdown="${countdownKey}"]`); + element?.animate(shimmerAnimation, shimmerTiming); + + const countdown = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns) + .countdowns[countdownKey]; + if (!this.visibleCountdownTypes.includes(countdown?.type)) + typesToAnimate.add(countdown.type); + } + + for (const type of typesToAnimate) { + const pulseAnimation = [ + { boxShadow: '0 0 1px 1px var(--golden)' }, + { boxShadow: '0 0 2px 2px var(--golden)' } + ]; + const pulseTiming = { + duration: 1000, + iterations: 3 + }; + + const element = this.element.querySelector(`.header-type-toggles .header-type[data-type="${type}"]`); + element?.animate(pulseAnimation, pulseTiming); + } + + this.changedCountdownsForAnimation.clear(); + } + /** Returns countdown data filtered by ownership */ #getCountdowns() { const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); @@ -76,16 +142,10 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application return values.filter(v => v.ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE); } - /** @override */ - async _prepareContext(options) { - const context = await super._prepareContext(options); - context.isGM = game.user.isGM; - - context.iconOnly = - game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) === - CONFIG.DH.GENERAL.countdownAppMode.iconOnly; + _getCountdownData() { const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); - context.countdowns = this.#getCountdowns().reduce((acc, { key, countdown, ownership }) => { + + return this.#getCountdowns().reduce((acc, { key, countdown, ownership }) => { const playersWithAccess = game.users.reduce((acc, user) => { const ownership = DhCountdowns.#getPlayerOwnership(user, setting, countdown); if (!user.isGM && ownership && ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) { @@ -101,14 +161,14 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application ? countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id ? 'DAGGERHEART.UI.Countdowns.increasingLoop' : countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id - ? 'DAGGERHEART.UI.Countdowns.decreasingLoop' - : 'DAGGERHEART.UI.Countdowns.loop' + ? 'DAGGERHEART.UI.Countdowns.decreasingLoop' + : 'DAGGERHEART.UI.Countdowns.loop' : null; const loopDisabled = !countdownEditable || (isLooping && (countdown.progress.current > 0 || countdown.progress.start === '0')); - acc[key] = { + acc[countdown.type][key] = { ...countdown, editable: countdownEditable, noPlayerAccess: nonGmPlayers.length && playersWithAccess.length === 0, @@ -117,7 +177,38 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application loopTooltip: isLooping && game.i18n.localize(loopTooltip) }; return acc; - }, {}); + }, Object.keys(CONFIG.DH.GENERAL.countdownTypes).reduce((acc, key) => { + acc[key] = {}; + return acc; + }, {})); + } + + /** @override */ + async _prepareContext(options) { + const context = await super._prepareContext(options); + context.isGM = game.user.isGM; + + context.iconOnly = + game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) + === CONFIG.DH.GENERAL.countdownAppMode.iconOnly; + + context.userCountdownTypes = this.visibleCountdownTypes; + + context.typeToggles = + Object.values(CONFIG.DH.GENERAL.countdownTypes).map(type => ({ + type: type.id, + label: game.i18n.localize(type.shortLabel), + active: context.userCountdownTypes.includes(type.id) + })); + + context.countdowns = this._getCountdownData(); + context.countdownTypesWithVisibleEntries = this.#getCountdowns().reduce((acc, data) => { + if (context.userCountdownTypes.includes(data.countdown.type) && !acc.includes(data.countdown.type)) + acc.push(data.countdown.type); + + return acc; + }, []); + return context; } @@ -147,7 +238,8 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application return true; } - static async #toggleViewMode() { + /** @this {DhCountdowns} */ + static async #onToggleViewMode() { const currentMode = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode); const appMode = CONFIG.DH.GENERAL.countdownAppMode; const newMode = currentMode === appMode.textIcon ? appMode.iconOnly : appMode.textIcon; @@ -158,15 +250,30 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application this.render(); } - static async #editCountdowns() { + /** @this {DhCountdowns} */ + static async #onToggleCountdownTypes(event, target) { + const currentTypes = this.visibleCountdownTypes; + const { type } = target.dataset; + const newTypes = event.shiftKey ? + [type] : + currentTypes.includes(type) ? currentTypes.filter(x => x !== type) : [...currentTypes, type]; + await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownTypeModes, newTypes); + + this.render(); + } + + /** @this {DhCountdowns} */ + static async #onEditCountdowns() { new game.system.api.applications.ui.CountdownEdit().render(true); } - static async #loopCountdown(_, target) { + /** @this {DhCountdowns} */ + static async #onLoopCountdown(_, 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 countdownId = target.closest('[data-countdown]').dataset.countdown; + const countdown = settings.countdowns[countdownId]; let progressMax = countdown.progress.start; let message = null; @@ -180,12 +287,12 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id ? Number(progressMax) + 1 : countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id - ? Math.max(Number(progressMax) - 1, 0) - : progressMax; + ? Math.max(Number(progressMax) - 1, 0) + : progressMax; await waitForDiceSoNice(message); await settings.updateSource({ - [`countdowns.${target.id}.progress`]: { + [`countdowns.${countdownId}.progress`]: { current: newMax, start: newMax } @@ -199,22 +306,23 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application if (!DhCountdowns.canPerformEdit()) return; const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); - const countdown = settings.countdowns[target.id]; + const countdownId = target.closest('[data-countdown]').dataset.countdown; + const countdown = settings.countdowns[countdownId]; const newCurrent = increase ? Math.min(countdown.progress.current + 1, countdown.progress.start) : Math.max(countdown.progress.current - 1, 0); - await settings.updateSource({ [`countdowns.${target.id}.progress.current`]: newCurrent }); + await settings.updateSource({ [`countdowns.${countdownId}.progress.current`]: newCurrent }); await emitGMUpdate(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 } - }); + 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 }); } @@ -234,29 +342,31 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application * Sends updates of the countdowns to the GM player. Since this is asynchronous, be sure to * update all the countdowns at the same time. * - * @param {...any} progressTypes Countdowns to be updated + * @param {...(string | { type: string; undo?: boolean })} progressTypes Countdowns to be updated */ static async updateCountdowns(...progressTypes) { + progressTypes = progressTypes.map(p => typeof p === 'string' ? { type: p } : p); const { countdownAutomation } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); if (!countdownAutomation) return; const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); const updatedCountdowns = Object.keys(countdownSetting.countdowns).reduce((acc, key) => { const countdown = countdownSetting.countdowns[key]; - if (progressTypes.indexOf(countdown.progress.type) !== -1 && countdown.progress.current > 0) { - acc.push(key); + const progressData = progressTypes.find(x => x.type === countdown.progress.type); + if (progressData && countdown.progress.current > 0) { + acc[key] = { value: progressData.undo ? 1 : -1 }; } return acc; - }, []); + }, {}); const countdownData = countdownSetting.toObject(); const settings = { ...countdownData, countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => { const countdown = foundry.utils.deepClone(countdownData.countdowns[key]); - if (updatedCountdowns.includes(key)) { - countdown.progress.current -= 1; + if (updatedCountdowns[key]) { + countdown.progress.current += updatedCountdowns[key].value; } acc[key] = countdown; @@ -267,12 +377,4 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application refreshType: RefreshType.Countdown }); } - - async _onRender(context, options) { - await super._onRender(context, options); - this.element.hidden = !game.user.isGM && this.#getCountdowns().length === 0; - if (options?.force) { - document.getElementById('ui-right-column-1')?.appendChild(this.element); - } - } } diff --git a/module/applications/ui/effectsDisplay.mjs b/module/applications/ui/effectsDisplay.mjs index 035041e17..a64b1b22f 100644 --- a/module/applications/ui/effectsDisplay.mjs +++ b/module/applications/ui/effectsDisplay.mjs @@ -67,10 +67,10 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica const actor = token ? token.actor : canvas.tokens.controlled.length === 0 - ? !game.user.isGM - ? game.user.character - : null - : canvas.tokens.controlled[0].actor; + ? !game.user.isGM + ? game.user.character + : null + : canvas.tokens.controlled[0].actor; return getIconVisibleActiveEffects(actor?.getActiveEffects() ?? []); }; diff --git a/module/applications/ui/gamePause.mjs b/module/applications/ui/gamePause.mjs new file mode 100644 index 000000000..8047f1dc0 --- /dev/null +++ b/module/applications/ui/gamePause.mjs @@ -0,0 +1,23 @@ +export default class DhGamePause extends foundry.applications.ui.GamePause { + async _onRender(context, options) { + await super._onRender(context, options); + + /* Avoid altering the styling if a module has subscribed to the renderGamePause hook */ + if (!Hooks.events.renderGamePause?.length) { + this.element.classList.add('dh-style'); + } + } + + /** @override */ + async _prepareContext(options) { + const context = await super._prepareContext(options); + + /* Avoid altering the gamepause context if a module has subscribed to the renderGamePause hook */ + if (!Hooks.events.renderGamePause?.length) { + context.spin = options.spin ?? false; + context.icon = options.icon ?? 'systems/daggerheart/assets/logos/compatible_with_DH_logos-10.png'; + } + + return context; + } +} \ No newline at end of file diff --git a/module/canvas/placeables/token.mjs b/module/canvas/placeables/token.mjs index 68e325c2e..7ae6dc37c 100644 --- a/module/canvas/placeables/token.mjs +++ b/module/canvas/placeables/token.mjs @@ -155,15 +155,15 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { const targetEdge = this.#getEdgeBoundary(targetBounds, originPoint, targetPoint); const adjustedOriginPoint = originEdge ? canvas.grid.getTopLeftPoint({ - x: originEdge.x + Math.sign(originPoint.x - originEdge.x), - y: originEdge.y + Math.sign(originPoint.y - originEdge.y) - }) + x: originEdge.x + Math.sign(originPoint.x - originEdge.x), + y: originEdge.y + Math.sign(originPoint.y - originEdge.y) + }) : originPoint; const adjustDestinationPoint = targetEdge ? canvas.grid.getTopLeftPoint({ - x: targetEdge.x + Math.sign(targetPoint.x - targetEdge.x), - y: targetEdge.y + Math.sign(targetPoint.y - targetEdge.y) - }) + x: targetEdge.x + Math.sign(targetPoint.x - targetEdge.x), + y: targetEdge.y + Math.sign(targetPoint.y - targetEdge.y) + }) : targetPoint; const distance = canvas.grid.measurePath([ { ...adjustedOriginPoint, elevation: 0 }, @@ -248,7 +248,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { } /** @inheritDoc */ - _drawBar(number, bar, data) { + _drawBar(index, bar, data) { // Determine sizing const { width, height } = this.document.getSize(); const s = canvas.dimensions.uiScale; @@ -256,9 +256,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { const bh = 8 * (this.document.height >= 2 ? 1.5 : 1) * s; // Determine the color to use - const Color = foundry.utils.Color; - const fillColor = number === 0 ? Color.fromRGB([1, 0, 0]) : Color.fromString('#0032b1'); - const emptyColor = Color.fromRGB([0, 0, 0]); + const { full: fullColor, empty: emptyColor } = this._getBarColors(index, data); // Draw the bar (accounting floating point numbers from bar animations) const widthUnit = bw / Math.ceil(data.max); @@ -268,7 +266,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { const x = mark * widthUnit; const marked = mark < Math.ceil(data.value); const remainder = mark === Math.ceil(data.value) - 1 ? data.value % 1 : 0; - const color = !marked ? emptyColor : remainder ? emptyColor.mix(fillColor, remainder) : fillColor; + const color = !marked ? emptyColor : remainder ? emptyColor.mix(fullColor, remainder) : fullColor; if (mark === 0 || mark === sections.length - 1) { bar.beginFill(color, marked ? 1.0 : 0.5).drawRect(x, 0, widthUnit, bh, 2 * s); // Would like drawRoundedRect, but it's very troublsome with the corners. Leaving for now. } else { @@ -277,7 +275,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { } // Set position - const posY = number === 0 ? height - bh : 0; + const posY = index === 0 ? height - bh : 0; bar.position.set(0, posY); return true; } diff --git a/module/config/actionConfig.mjs b/module/config/actionConfig.mjs index a6c04e180..83a2d6698 100644 --- a/module/config/actionConfig.mjs +++ b/module/config/actionConfig.mjs @@ -122,3 +122,26 @@ export const areaTypes = { label: 'Placed Area' } }; + +export const outcomeTypes = { + default: { + key: 'default', + label: 'DAGGERHEART.CONFIG.OutcomeType.default' + }, + successHope: { + key: 'successHope', + label: 'DAGGERHEART.CONFIG.OutcomeType.successHope' + }, + successFear: { + key: 'successFear', + label: 'DAGGERHEART.CONFIG.OutcomeType.successFear' + }, + failureHope: { + key: 'failureHope', + label: 'DAGGERHEART.CONFIG.OutcomeType.failureHope' + }, + failureFear: { + key: 'failureFear', + label: 'DAGGERHEART.CONFIG.OutcomeType.failureFear' + } +}; diff --git a/module/config/flagsConfig.mjs b/module/config/flagsConfig.mjs index 817ac89de..cb05bd729 100644 --- a/module/config/flagsConfig.mjs +++ b/module/config/flagsConfig.mjs @@ -24,7 +24,8 @@ export const itemAttachmentSource = 'attachmentSource'; export const userFlags = { welcomeMessage: 'welcome-message', - countdownMode: 'countdown-mode' + countdownMode: 'countdown-mode', + countdownTypeModes: 'countdown-type-modes' }; export const combatToggle = 'combat-toggle-origin'; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 6fc85ec55..188efafb2 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -867,27 +867,27 @@ export const abilityCosts = { export const countdownProgressionTypes = { actionRoll: { id: 'actionRoll', - label: 'DAGGERHEART.CONFIG.CountdownType.actionRoll' + label: 'DAGGERHEART.CONFIG.CountdownProgressType.actionRoll' }, characterAttack: { id: 'characterAttack', - label: 'DAGGERHEART.CONFIG.CountdownType.characterAttack' + label: 'DAGGERHEART.CONFIG.CountdownProgressType.characterAttack' }, characterSpotlight: { id: 'characterSpotlight', - label: 'DAGGERHEART.CONFIG.CountdownType.characterSpotlight' + label: 'DAGGERHEART.CONFIG.CountdownProgressType.characterSpotlight' }, custom: { id: 'custom', - label: 'DAGGERHEART.CONFIG.CountdownType.custom' + label: 'DAGGERHEART.CONFIG.CountdownProgressType.custom' }, fear: { id: 'fear', - label: 'DAGGERHEART.CONFIG.CountdownType.fear' + label: 'DAGGERHEART.CONFIG.CountdownProgressType.fear' }, spotlight: { id: 'spotlight', - label: 'DAGGERHEART.CONFIG.CountdownType.spotlight' + label: 'DAGGERHEART.CONFIG.CountdownProgressType.spotlight' } }; export const rollTypes = { @@ -935,17 +935,6 @@ export const simpleOwnershiplevels = { ...basicOwnershiplevels }; -export const countdownBaseTypes = { - narrative: { - id: 'narrative', - label: 'DAGGERHEART.APPLICATIONS.Countdown.types.narrative' - }, - encounter: { - id: 'encounter', - label: 'DAGGERHEART.APPLICATIONS.Countdown.types.encounter' - } -}; - export const countdownLoopingTypes = { noLooping: { id: 'noLooping', @@ -970,6 +959,19 @@ export const countdownAppMode = { iconOnly: 'icon-only' }; +export const countdownTypes = { + encounter: { + id: 'encounter', + label: 'DAGGERHEART.CONFIG.CountdownType.encounter.label', + shortLabel: 'DAGGERHEART.CONFIG.CountdownType.encounter.shortLabel' + }, + narrative: { + id: 'narrative', + label: 'DAGGERHEART.CONFIG.CountdownType.narrative.label', + shortLabel: 'DAGGERHEART.CONFIG.CountdownType.narrative.shortLabel' + } +}; + export const sceneRangeMeasurementSetting = { disable: { id: 'disable', diff --git a/module/config/itemBrowserConfig.mjs b/module/config/itemBrowserConfig.mjs index ae5fa71b3..83572dc0d 100644 --- a/module/config/itemBrowserConfig.mjs +++ b/module/config/itemBrowserConfig.mjs @@ -127,8 +127,8 @@ export const typeConfig = { isSecondary ? 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon.short' : isSecondary === false - ? 'DAGGERHEART.ITEMS.Weapon.primaryWeapon.short' - : '-' + ? 'DAGGERHEART.ITEMS.Weapon.primaryWeapon.short' + : '-' }, { key: 'system.tier', diff --git a/module/data/action/altOutcome.mjs b/module/data/action/altOutcome.mjs new file mode 100644 index 000000000..95f75f731 --- /dev/null +++ b/module/data/action/altOutcome.mjs @@ -0,0 +1,19 @@ +import { getDamageBaseFields } from '../fields/action/damageField.mjs'; + +const fields = foundry.data.fields; + +export class AltOutcome extends foundry.abstract.DataModel { + static defineSchema() { + return { + damage: new fields.SchemaField(getDamageBaseFields()) + // todo: add effects + }; + } + + get data() { + return { + ...this.parent, + ...this + }; + } +} diff --git a/module/data/action/attackAction.mjs b/module/data/action/attackAction.mjs index 1f7e1c924..1988b1d8a 100644 --- a/module/data/action/attackAction.mjs +++ b/module/data/action/attackAction.mjs @@ -79,8 +79,8 @@ export default class DHAttackAction extends DHDamageAction { const str = damageString ? damageString : game.i18n.format('DAGGERHEART.GENERAL.missingX', { - x: game.i18n.localize('DAGGERHEART.GENERAL.damage') - }); + x: game.i18n.localize('DAGGERHEART.GENERAL.damage') + }); const icons = Array.from(type) .map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon) diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index acd104a79..58be672b0 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -54,6 +54,10 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel return {}; } + get hasDescription() { + return Boolean(this.description); + } + /** * Create a Map containing each Action step based on fields define in schema. Ordered by Fields order property. * @@ -144,8 +148,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel return this.item instanceof DhpActor ? this.item : this.item?.parent instanceof DhpActor - ? this.item.parent - : null; + ? this.item.parent + : null; } /** @@ -223,12 +227,13 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel * @returns {object} */ async use(event, configOptions = {}) { - 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.'); let config = this.prepareConfig(event, configOptions); if (!config) return; - config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(this.actor, this.item); + config.effects = + await game.system.api.data.actions.actionsTypes.base.getActionRelevantEffects(this.actor, this.item); if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return; @@ -300,17 +305,17 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel const groupAttackTokens = this.damage.groupAttack ? game.system.api.fields.ActionFields.DamageField.getGroupAttackTokens( - this.actor.id, - this.damage.groupAttack - ) + this.actor.id, + this.damage.groupAttack + ) : null; config.damageOptions = { groupAttack: this.damage.groupAttack ? { - numAttackers: Math.max(groupAttackTokens.length, 1), - range: this.damage.groupAttack - } + numAttackers: Math.max(groupAttackTokens.length, 1), + range: this.damage.groupAttack + } : null }; } @@ -333,27 +338,45 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel } /** - * Get the all potentially applicable effects on the actor + * Get the all potentially applicable effects on the actor for the action's RollDialog * @param {DHActor} actor The actor performing the action * @param {DHItem|DhActor} effectParent The parent of the effect * @returns {DhActiveEffect[]} */ - static async getEffects(actor, effectParent) { + static async getActionRelevantEffects(actor, effectParent) { if (!actor) return []; - return Array.from(await actor.allApplicableEffects({ noTransferArmor: true, noSelfArmor: true })).filter( - effect => { - /* Effects on weapons only ever apply for the weapon itself */ - if (effect.parent.type === 'weapon') { - /* Unless they're secondary - then they apply only to other primary weapons */ - if (effect.parent.system.secondary) { - if (effectParent?.type !== 'weapon' || effectParent?.system.secondary) return false; - } else if (effectParent?.id !== effect.parent.id) return false; + // Changes on weapon effects are not typically only applicable to show in the roll dialog for the weapon itself + // The exemptions to this rule are listed below + const weaponTransferredEffectKeys = [ + 'system.bonuses.roll.spellcast.bonus' + ]; + + const results = []; + const applicableEffects = await actor.allApplicableEffects({ noTransferArmor: true, noSelfArmor: true }); + for (const effect of [...applicableEffects].filter(e => !e.isSuppressed)) { + if (effect.parent.type === 'weapon') { + // Effects on weapons only ever apply for the weapon itself (with a few exceptions) + const restricted = + effect.parent.system.secondary + // Secondary applies only to other primary weapons + ? effectParent?.type !== 'weapon' || effectParent?.system.secondary + // Primary only applies to itself + : effectParent?.id !== effect.parent.id; + if (restricted) { + const sourceChanges = effect._source.system.changes; + const changes = sourceChanges.filter(x => weaponTransferredEffectKeys.includes(x.key)); + if (changes.length) { + results.push(effect.clone({ 'system.changes': changes })); + } + continue; } - - return !effect.isSuppressed; } - ); + + results.push(effect); + } + + return results; } /** @@ -428,7 +451,15 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel static migrateData(source) { if (source.damage?.parts && Array.isArray(source.damage.parts)) { + let hitPointsExists = source.damage.parts.some(x => x.applyTo === 'hitPoints'); source.damage.parts = source.damage.parts.reduce((acc, part) => { + if (!part.applyTo && hitPointsExists) return acc; + + if (!part.applyTo) { + hitPointsExists = true; + part.applyTo = 'hitPoints'; + } + acc[part.applyTo] = part; return acc; }, {}); @@ -459,8 +490,7 @@ export class ResourceUpdateMap extends Map { } else if (!existing?.clear) { this.set(resource.key, { ...existing, - value: existing.value + (resource.value ?? 0), - total: existing.total + (resource.total ?? 0) + value: existing.value + (resource.value ?? 0) }); } } diff --git a/module/data/action/damageAction.mjs b/module/data/action/damageAction.mjs index 517355437..f78141816 100644 --- a/module/data/action/damageAction.mjs +++ b/module/data/action/damageAction.mjs @@ -1,8 +1,23 @@ +import { AltOutcome } from './altOutcome.mjs'; import DHBaseAction from './baseAction.mjs'; +const fields = foundry.data.fields; + export default class DHDamageAction extends DHBaseAction { static extraSchemas = [...super.extraSchemas, 'damage', 'target', 'effects']; + static defineSchema() { + return { + ...super.defineSchema(), + altOutcomes: new fields.SchemaField({ + successHope: new fields.EmbeddedDataField(AltOutcome, { nullable: true, initial: null }), + successFear: new fields.EmbeddedDataField(AltOutcome, { nullable: true, initial: null }), + failureHope: new fields.EmbeddedDataField(AltOutcome, { nullable: true, initial: null }), + failureFear: new fields.EmbeddedDataField(AltOutcome, { nullable: true, initial: null }) + }) + }; + } + /** * Return a display ready damage formula string * @returns Formula string diff --git a/module/data/activeEffect/beastformEffect.mjs b/module/data/activeEffect/beastformEffect.mjs index 0fbea1220..7e037f5ba 100644 --- a/module/data/activeEffect/beastformEffect.mjs +++ b/module/data/activeEffect/beastformEffect.mjs @@ -90,13 +90,13 @@ export default class BeastformEffect extends BaseEffect { ...baseUpdate, x, y, - 'texture': { + texture: { enabled: this.characterTokenData.usesDynamicToken, src: token.flags.daggerheart?.beastformTokenImg ?? this.characterTokenData.tokenImg, scaleX: this.characterTokenData.tokenSize.scale, scaleY: this.characterTokenData.tokenSize.scale }, - 'ring': { + ring: { subject: { texture: token.flags.daggerheart?.beastformSubjectTexture ?? this.characterTokenData.tokenRingImg diff --git a/module/data/activeEffect/changeTypes/armor.mjs b/module/data/activeEffect/changeTypes/armor.mjs index 217ff9dd3..0c226513d 100644 --- a/module/data/activeEffect/changeTypes/armor.mjs +++ b/module/data/activeEffect/changeTypes/armor.mjs @@ -166,10 +166,10 @@ export default class ArmorChange extends foundry.abstract.DataModel { value: change.type === 'armor' ? { - ...change.value, - current: Math.min(change.value.current, newMax), - max: newMax - } + ...change.value, + current: Math.min(change.value.current, newMax), + max: newMax + } : change.value })) ]; diff --git a/module/data/actor/_types.d.ts b/module/data/actor/_types.d.ts new file mode 100644 index 000000000..fec6e6383 --- /dev/null +++ b/module/data/actor/_types.d.ts @@ -0,0 +1,14 @@ +import DhpActor from '../../documents/actor.mjs' +import DhCharacter from './character.mjs'; + +declare module './base.mjs' { + export default interface BaseDataActor { + parent: DhpActor; + } +} + +declare module './companion.mjs' { + export default interface DhCompanion { + partner: DhpActor; + } +} diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs index d6d0dcdf0..ae17c128a 100644 --- a/module/data/actor/adversary.mjs +++ b/module/data/actor/adversary.mjs @@ -87,6 +87,7 @@ export default class DhpAdversary extends DhCreature { parts: { hitPoints: { type: ['physical'], + applyTo: 'hitPoints', value: { multiplier: 'flat' } diff --git a/module/data/actor/base.mjs b/module/data/actor/base.mjs index 9efbe7d7a..995708eba 100644 --- a/module/data/actor/base.mjs +++ b/module/data/actor/base.mjs @@ -1,9 +1,13 @@ -import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs'; -import DHItem from '../../documents/item.mjs'; import { createShallowProxy, getScrollTextData } from '../../helpers/utils.mjs'; const fields = foundry.data.fields; +/** + * @import DHItem from '../../documents/item.mjs'; + * @import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs'; + */ + +/** Function to generate resistance fields for damage types */ const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) => new fields.SchemaField({ resistance: new fields.BooleanField({ @@ -96,6 +100,8 @@ export const commonActorRules = (extendedData = { damageReduction: {}, attack: { * @property {Boolean} isNPC - This data model represents a NPC? * @property {typeof DHBaseActorSettings} settingSheet - The sheet class used to render the settings UI for this actor type. */ + +/** Base actor type data model for all actors in Daggerheart */ export default class BaseDataActor extends foundry.abstract.TypeDataModel { /** @returns {ActorDataModelMetadata}*/ static get metadata() { diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 10d53c13b..b39c64aa3 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -64,7 +64,18 @@ export default class DhCharacter extends DhCreature { core: new fields.BooleanField({ initial: false }) }) ), - gold: new GoldField(), + gold: new GoldField({ + initial: () => { + const homebrew = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew); + const { coins, handfuls, bags, chests } = homebrew.currency; + return { + coins: coins.enabled ? coins.initialAmount : 0, + handfuls: handfuls.enabled ? handfuls.initialAmount : 0, + bags: bags.enabled ? bags.initialAmount : 0, + chests: chests.enabled ? chests.initialAmount : 0 + }; + } + }), scars: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.scars' }), biography: new fields.SchemaField({ background: new fields.HTMLField(), @@ -96,6 +107,7 @@ export default class DhCharacter extends DhCreature { parts: { hitPoints: { type: ['physical'], + applyTo: 'hitPoints', value: { custom: { enabled: true, @@ -304,7 +316,12 @@ export default class DhCharacter extends DhCreature { label: 'DAGGERHEART.ACTORS.Character.defaultDisadvantageDice' }) }) - }) + }), + /** Accumulated armor score from all sources */ + armorScore: new fields.SchemaField({ + value: new fields.NumberField(), + max: new fields.NumberField() + }, { persisted: false }) }; } @@ -315,8 +332,8 @@ export default class DhCharacter extends DhCreature { return currentLevel === 1 ? 1 : Object.values(game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers).find( - tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end - ).tier; + tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end + ).tier; } get ancestry() { @@ -400,8 +417,8 @@ export default class DhCharacter extends DhCreature { get domainCards() { const domainCards = this.parent.items.filter(x => x.type === 'domainCard'); - const loadout = domainCards.filter(x => !x.system.inVault); - const vault = domainCards.filter(x => x.system.inVault); + const loadout = domainCards.filter(x => !x.system.inVault).sort((a, b) => a.sort - b.sort); + const vault = domainCards.filter(x => x.system.inVault).sort((a, b) => a.sort - b.sort); return { loadout: loadout, @@ -520,20 +537,20 @@ export default class DhCharacter extends DhCreature { if (armorSource.type === 'armor') { armorUpdates[armorSource.parent.id].updates.push({ - '_id': armorSource.id, + _id: armorSource.id, 'system.armor.current': armorSource.system.armor.current + usedArmorChange }); } else { effectUpdates[armorSource.parent.id].updates.push({ - '_id': armorSource.id, + _id: armorSource.id, 'system.changes': armorSource.system.changes.map(change => ({ ...change, value: change.type === 'armor' ? { - ...change.value, - current: armorSource.system.armorChange.value.current + usedArmorChange - } + ...change.value, + current: armorSource.system.armorChange.value.current + usedArmorChange + } : change.value })) }); @@ -621,21 +638,21 @@ export default class DhCharacter extends DhCreature { }, ...(multiclassFeatures.length ? { - multiclassFeatures: { - title: `${game.i18n.localize('DAGGERHEART.GENERAL.multiclass')} - ${this.multiclass.value?.name}`, - type: 'multiclass', - values: multiclassFeatures - } - } + multiclassFeatures: { + title: `${game.i18n.localize('DAGGERHEART.GENERAL.multiclass')} - ${this.multiclass.value?.name}`, + type: 'multiclass', + values: multiclassFeatures + } + } : {}), ...(multiclassSubclassFeatures.length ? { - multiclassSubclassFeatures: { - title: `${game.i18n.localize('DAGGERHEART.GENERAL.multiclass')} ${game.i18n.localize('TYPES.Item.subclass')} - ${this.multiclass.subclass?.name}`, - type: 'multiclassSubclass', - values: multiclassSubclassFeatures - } - } + multiclassSubclassFeatures: { + title: `${game.i18n.localize('DAGGERHEART.GENERAL.multiclass')} ${game.i18n.localize('TYPES.Item.subclass')} - ${this.multiclass.subclass?.name}`, + type: 'multiclassSubclass', + values: multiclassSubclassFeatures + } + } : {}), companionFeatures: { title: game.i18n.localize('DAGGERHEART.ACTORS.Character.companionFeatures'), @@ -659,8 +676,8 @@ export default class DhCharacter extends DhCreature { (this.primaryWeapon && this.secondaryWeapon) ? burden.twoHanded.value : this.primaryWeapon || this.secondaryWeapon - ? burden.oneHanded.value - : null; + ? burden.oneHanded.value + : null; } get deathMoveViable() { @@ -726,8 +743,8 @@ export default class DhCharacter extends DhCreature { currentLevel === 1 ? null : Object.values(game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers).find( - tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end - ).tier; + tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end + ).tier; if (game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto) { for (let levelKey in this.levelData.levelups) { const level = this.levelData.levelups[levelKey]; diff --git a/module/data/actor/companion.mjs b/module/data/actor/companion.mjs index 300bd698d..2ca7fd5bb 100644 --- a/module/data/actor/companion.mjs +++ b/module/data/actor/companion.mjs @@ -102,6 +102,7 @@ export default class DhCompanion extends DhCreature { parts: { hitPoints: { type: ['physical'], + applyTo: 'hitPoints', value: { dice: 'd6', multiplier: 'prof' diff --git a/module/data/actor/tierAdjustment.mjs b/module/data/actor/tierAdjustment.mjs index 785eec2bb..bc6ad1767 100644 --- a/module/data/actor/tierAdjustment.mjs +++ b/module/data/actor/tierAdjustment.mjs @@ -1,5 +1,6 @@ import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs'; import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs'; +import { parseInlineParams } from '../../enrichers/parser.mjs'; export function getTierAdjustedAdversary(source, tier) { const currentTier = source.tier ?? 1; @@ -60,8 +61,8 @@ export function getTierAdjustedAdversary(source, tier) { const descriptionFormulas = []; for (const withDescription of [item.system, ...Object.values(item.system.actions)]) { withDescription.description = withDescription.description.replace(damageRegex, (match, inner) => { - const { value: formula } = parseInlineParams(inner); - if (!formula || !type) return match; + const { value: formula } = parseInlineParams(inner, { first: 'value' }); + if (!formula) return match; try { const newFormula = calculateAdjustedDamage(formula, 'action', damageMeta)?.formula; diff --git a/module/data/companionLevelup.mjs b/module/data/companionLevelup.mjs index 7ab61210f..e24820de2 100644 --- a/module/data/companionLevelup.mjs +++ b/module/data/companionLevelup.mjs @@ -22,12 +22,12 @@ export class DhCompanionLevelup extends foundry.abstract.DataModel { const initialAchievements = i === tier.levels.start ? tier.initialAchievements : {}; const experiences = initialAchievements.experience ? [...Array(initialAchievements.experience.nr).keys()].reduce((acc, _) => { - acc[foundry.utils.randomID()] = { - name: '', - modifier: initialAchievements.experience.modifier - }; - return acc; - }, {}) + acc[foundry.utils.randomID()] = { + name: '', + modifier: initialAchievements.experience.modifier + }; + return acc; + }, {}) : {}; const currentChoices = pcLevelData.levelups[i]?.selections?.length; @@ -302,9 +302,9 @@ export class DhLevelupLevel extends foundry.abstract.DataModel { experiences: levelData.achievements?.experiences ?? achievements.experiences ?? {}, domainCards: levelData.achievements?.domainCards ? levelData.achievements.domainCards.reduce((acc, card, index) => { - acc[index] = { ...card }; - return acc; - }, {}) + acc[index] = { ...card }; + return acc; + }, {}) : (achievements.domainCards ?? {}), proficiency: levelData.achievements?.proficiency ?? achievements.proficiency ?? null }, diff --git a/module/data/countdowns.mjs b/module/data/countdowns.mjs index 7d27197d2..ffe4d26ba 100644 --- a/module/data/countdowns.mjs +++ b/module/data/countdowns.mjs @@ -13,6 +13,54 @@ export default class DhCountdowns extends foundry.abstract.DataModel { }) }; } + + handleChange() { + const previousCountdowns = foundry.ui.countdowns.previousCountdownData; + const changedCountdowns = Object.entries(this.countdowns).reduce((acc, [key, countdown]) => { + const previousCountdown = previousCountdowns[key]; + if (!previousCountdown || (previousCountdown.progress.current !== countdown.progress.current)) { + acc.push(key); + } + + return acc; + }, []); + + for (const countdownKey of changedCountdowns) + foundry.ui.countdowns.changedCountdownsForAnimation.add(countdownKey); + } + + static migrateData(source) { + const migrateOldCountdowns = (data, type) => { + for (const key of Object.keys(data.countdowns)) { + const countdown = data.countdowns[key]; + source.countdowns[key] = { + ...countdown, + type: type, + ownership: Object.keys(countdown.ownership.players).reduce((acc, key) => { + acc[key] = + countdown.ownership.players[key].type === 1 ? 2 : countdown.ownership.players[key].type; + return acc; + }, {}), + progress: { + ...countdown.progress, + type: countdown.progress.type.value + } + }; + } + + source[type] = null; + }; + + if (source.narrative) { + migrateOldCountdowns(source.narrative, 'narrative'); + } + + if (source.encounter) { + migrateOldCountdowns(source.encounter, 'encounter'); + } + + return super.migrateData(source); + } } export class DhCountdown extends foundry.abstract.DataModel { @@ -21,7 +69,8 @@ export class DhCountdown extends foundry.abstract.DataModel { return { type: new fields.StringField({ required: true, - choices: CONFIG.DH.GENERAL.countdownBaseTypes, + choices: CONFIG.DH.GENERAL.countdownTypes, + initial: CONFIG.DH.GENERAL.countdownTypes.encounter.id, label: 'DAGGERHEART.GENERAL.type' }), name: new fields.StringField({ @@ -77,15 +126,15 @@ export class DhCountdown extends foundry.abstract.DataModel { 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; - }, {}) + if (!user.isGM) { + acc[user.id] = CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE; + } + return acc; + }, {}) : undefined; return { - type: type ?? CONFIG.DH.GENERAL.countdownBaseTypes.narrative.id, + type: type ?? CONFIG.DH.GENERAL.countdownTypes.encounter.id, name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.newCountdown'), img: 'icons/magic/time/hourglass-yellow-green.webp', ownership: ownership, @@ -102,8 +151,8 @@ export class DhCountdown extends foundry.abstract.DataModel { 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, + ? this.ownership.players[user.id].type + : this.ownership.default, isGM: user.isGM }; diff --git a/module/data/fields/action/beastformField.mjs b/module/data/fields/action/beastformField.mjs index e3be99379..0eeb95c22 100644 --- a/module/data/fields/action/beastformField.mjs +++ b/module/data/fields/action/beastformField.mjs @@ -105,8 +105,8 @@ export default class BeastformField extends fields.SchemaField { baseSize === 'custom' ? 'custom' : (Object.keys(CONFIG.DH.ACTOR.tokenSize).find( - x => CONFIG.DH.ACTOR.tokenSize[x].value === CONFIG.DH.ACTOR.tokenSize[baseSize].value + 1 - ) ?? baseSize); + x => CONFIG.DH.ACTOR.tokenSize[x].value === CONFIG.DH.ACTOR.tokenSize[baseSize].value + 1 + ) ?? baseSize); formData.system.tokenSize = { ...evolvedData.form.system.tokenSize, size: evolvedSize diff --git a/module/data/fields/action/costField.mjs b/module/data/fields/action/costField.mjs index 1928af413..82cfcd234 100644 --- a/module/data/fields/action/costField.mjs +++ b/module/data/fields/action/costField.mjs @@ -116,8 +116,8 @@ export default class CostField extends fields.ArrayField { c.key === 'fear' ? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) : resources[c.key].isReversed - ? resources[c.key].max - resources[c.key].value - : resources[c.key].value; + ? resources[c.key].max - resources[c.key].value + : resources[c.key].value; if (c.scalable) c.maxStep = Math.floor((c.max - c.value) / c.step); return c; }); @@ -149,8 +149,8 @@ export default class CostField extends fields.ArrayField { !resources[c.key] ? a : a && resources[c.key].isReversed - ? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max - : resources[c.key]?.value >= (c.total ?? c.value), + ? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max + : resources[c.key]?.value >= (c.total ?? c.value), true ); } diff --git a/module/data/fields/action/countdownField.mjs b/module/data/fields/action/countdownField.mjs index 990f8ef11..53e132c93 100644 --- a/module/data/fields/action/countdownField.mjs +++ b/module/data/fields/action/countdownField.mjs @@ -8,8 +8,8 @@ export default class CountdownField extends fields.ArrayField { ...game.system.api.data.countdowns.DhCountdown.defineSchema(), type: new fields.StringField({ required: true, - choices: CONFIG.DH.GENERAL.countdownBaseTypes, - initial: CONFIG.DH.GENERAL.countdownBaseTypes.encounter.id, + choices: CONFIG.DH.GENERAL.countdownTypes, + initial: CONFIG.DH.GENERAL.countdownTypes.encounter.id, label: 'DAGGERHEART.GENERAL.type' }), name: new fields.StringField({ @@ -87,11 +87,11 @@ export default class CountdownField extends fields.ArrayField { CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSetting.toObject() - ), - game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.Refresh, - data: { refreshType: RefreshType.Countdown } - }); + ); + game.socket.emit(`system.${CONFIG.DH.id}`, { + action: socketEvent.Refresh, + data: { refreshType: RefreshType.Countdown } + }); Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown }); }, data, diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index 9b21d3ba9..b7bd23326 100644 --- a/module/data/fields/action/damageField.mjs +++ b/module/data/fields/action/damageField.mjs @@ -4,6 +4,15 @@ import IterableTypedObjectField from '../iterableTypedObjectField.mjs'; const fields = foundry.data.fields; +export const getDamageBaseFields = () => ({ + parts: new IterableTypedObjectField(DHDamageData), + includeBase: new fields.BooleanField({ + initial: false, + label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label' + }), + direct: new fields.BooleanField({ initial: false, label: 'DAGGERHEART.CONFIG.DamageType.direct.name' }) +}); + export default class DamageField extends fields.SchemaField { /** * Action Workflow order @@ -13,12 +22,7 @@ export default class DamageField extends fields.SchemaField { /** @inheritDoc */ constructor(options, context = {}) { const damageFields = { - parts: new IterableTypedObjectField(DHDamageData), - includeBase: new fields.BooleanField({ - initial: false, - label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label' - }), - direct: new fields.BooleanField({ initial: false, label: 'DAGGERHEART.CONFIG.DamageType.direct.name' }), + ...getDamageBaseFields(), groupAttack: new fields.StringField({ choices: CONFIG.DH.GENERAL.groupAttackRange, blank: true, @@ -28,6 +32,12 @@ export default class DamageField extends fields.SchemaField { super(damageFields, options, context); } + getDamageData(outcome) { + if (outcome === 'successHope') return this; + + return this.altOutcomes[outcome].data; + } + /** * Roll Damage/Healing Action Workflow part. * Must be called within Action context or similar. diff --git a/module/data/fields/action/effectsField.mjs b/module/data/fields/action/effectsField.mjs index d2ee16820..e943d63dd 100644 --- a/module/data/fields/action/effectsField.mjs +++ b/module/data/fields/action/effectsField.mjs @@ -61,11 +61,11 @@ export default class EffectsField extends fields.ArrayField { token: messageToken, conditionImmunities: Object.values(conditionImmunities).some(x => x) ? game.i18n.format('DAGGERHEART.UI.Chat.effectSummary.immunityTo', { - immunities: Object.keys(conditionImmunities) - .filter(x => conditionImmunities[x]) - .map(x => game.i18n.localize(conditions[x].name)) - .join(', ') - }) + immunities: Object.keys(conditionImmunities) + .filter(x => conditionImmunities[x]) + .map(x => game.i18n.localize(conditions[x].name)) + .join(', ') + }) : null }); diff --git a/module/data/fields/action/saveField.mjs b/module/data/fields/action/saveField.mjs index 0629353e7..7343ab850 100644 --- a/module/data/fields/action/saveField.mjs +++ b/module/data/fields/action/saveField.mjs @@ -69,11 +69,11 @@ export default class SaveField extends fields.SchemaField { game.user === actor.owner ? SaveField.rollSave.call(this, actor, event) : actor.owner.query('reactionRoll', { - actionId: this.uuid, - actorId: actor.uuid, - event, - message - }); + actionId: this.uuid, + actorId: actor.uuid, + event, + message + }); const result = await rollSave; await SaveField.updateSaveMessage.call(this, result, message, target.id); subResolve(); @@ -97,8 +97,8 @@ export default class SaveField extends fields.SchemaField { 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) - }), + ability: game.i18n.localize(abilities[this.save.trait]?.label) + }), rollConfig = { event, title, diff --git a/module/data/fields/action/summonField.mjs b/module/data/fields/action/summonField.mjs index a2275fa53..6845d2baf 100644 --- a/module/data/fields/action/summonField.mjs +++ b/module/data/fields/action/summonField.mjs @@ -1,4 +1,4 @@ -import { itemAbleRollParse, triggerChatRollFx } from '../../../helpers/utils.mjs'; +import { getWorldActor, itemAbleRollParse, triggerChatRollFx } from '../../../helpers/utils.mjs'; import FormulaField from '../formulaField.mjs'; const fields = foundry.data.fields; @@ -42,7 +42,7 @@ export default class DHSummonField extends fields.ArrayField { const count = roll.total; if (!roll.isDeterministic) rolls.push(roll); - const actor = await DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID)); + const actor = await getWorldActor(await foundry.utils.fromUuid(summon.actorUUID)); /* Extending summon data in memory so it's available in actionField.toChat. Think it's harmless, but ugly. Could maybe find a better way. */ summon.actor = actor.toObject(); @@ -62,19 +62,6 @@ export default class DHSummonField extends fields.ArrayField { DHSummonField.handleSummon(summonData, this.actor); } - /* Check for any available instances of the actor present in the world if we're missing artwork in the compendium. If none exists, create one. */ - static async getWorldActor(baseActor) { - const dataType = game.system.api.data.actors[`Dh${baseActor.type.capitalize()}`]; - if (baseActor.inCompendium && dataType && baseActor.img === dataType.DEFAULT_ICON) { - const worldActorCopy = game.actors.find(x => x.name === baseActor.name); - if (worldActorCopy) return worldActorCopy; - - return await game.system.api.documents.DhpActor.create(baseActor.toObject()); - } - - return baseActor; - } - static async handleSummon(summonData, actionActor) { await CONFIG.ux.TokenManager.createTokensWithPreview(summonData, { elevation: actionActor.token?.elevation }); diff --git a/module/data/fields/actorField.mjs b/module/data/fields/actorField.mjs index a5f6f379b..e165f5cc0 100644 --- a/module/data/fields/actorField.mjs +++ b/module/data/fields/actorField.mjs @@ -116,13 +116,16 @@ class ResourcesField extends fields.TypedObjectField { } class GoldField extends fields.SchemaField { - constructor() { - super({ - 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 }) - }); + constructor(options = {}) { + super( + { + 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 }) + }, + options + ); } } diff --git a/module/data/fields/formulaField.mjs b/module/data/fields/formulaField.mjs index 85922f1f0..c465bec35 100644 --- a/module/data/fields/formulaField.mjs +++ b/module/data/fields/formulaField.mjs @@ -39,7 +39,7 @@ export default class FormulaField extends foundry.data.fields.StringField { let roll = null; try { roll = new Roll(value.replace(/@([a-z.0-9_-]+)/gi, '1')); - } catch (_) { + } catch { roll = new Roll(value.replace(/@([a-z.0-9_-]+)/gi, 'd6')); } roll.evaluateSync({ strict: false }); diff --git a/module/data/item/_types.d.ts b/module/data/item/_types.d.ts new file mode 100644 index 000000000..6004e13b0 --- /dev/null +++ b/module/data/item/_types.d.ts @@ -0,0 +1,8 @@ +import DHItem from '../../documents/item.mjs'; + +declare module './base.mjs' { + export default interface BaseDataItem { + parent: DHItem; + actor: DhpActor; + } +} \ No newline at end of file diff --git a/module/data/item/ancestry.mjs b/module/data/item/ancestry.mjs index b9253a3cb..eae1136c3 100644 --- a/module/data/item/ancestry.mjs +++ b/module/data/item/ancestry.mjs @@ -1,6 +1,6 @@ import BaseDataItem from './base.mjs'; import ItemLinkFields from '../../data/fields/itemLinkFields.mjs'; -import { getFeaturesHTMLData } from '../../helpers/utils.mjs'; +import { fromUuids, getFeaturesHTMLData } from '../../helpers/utils.mjs'; export default class DHAncestry extends BaseDataItem { /** @inheritDoc */ @@ -45,6 +45,10 @@ export default class DHAncestry extends BaseDataItem { /**@inheritdoc */ async getDescriptionData() { + // Preload all ancestry features for acquisition from the cache + // todo: make feature acquisition async and replace feature helpers for methods + await fromUuids(this._source.features.map(f => f.item)); + const baseDescription = this.description; const features = await getFeaturesHTMLData(this.features); diff --git a/module/data/item/armor.mjs b/module/data/item/armor.mjs index 21c56f9aa..15bb620db 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -52,6 +52,10 @@ export default class DHArmor extends AttachableItem { ); } + get itemFeatures() { + return this.armorFeatures; + } + /**@inheritdoc */ async getDescriptionData() { const baseDescription = this.description; @@ -169,8 +173,4 @@ export default class DHArmor extends AttachableItem { const labels = [`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.armor.max}`]; return labels; } - - get itemFeatures() { - return this.armorFeatures; - } } diff --git a/module/data/item/beastform.mjs b/module/data/item/beastform.mjs index ee9d98397..ba274cc73 100644 --- a/module/data/item/beastform.mjs +++ b/module/data/item/beastform.mjs @@ -208,8 +208,8 @@ export default class DHBeastform extends BaseDataItem { const autoTokenSize = this.tokenSize.size !== 'custom' ? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes[ - this.tokenSize.size - ] + this.tokenSize.size + ] : null; const width = autoTokenSize ?? this.tokenSize.width; const height = autoTokenSize ?? this.tokenSize.height; diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs index 470a1e3c7..b9cda2647 100644 --- a/module/data/item/class.mjs +++ b/module/data/item/class.mjs @@ -70,14 +70,14 @@ export default class DHClass extends BaseDataItem { } async fetchSubclasses() { - const uuids = [this.parent.uuid, this.parent._stats?.compendiumSource].filter(u => !!u); - const subclasses = game.items.filter(x => x.type === 'subclass' && uuids.includes(x.system.linkedClass)); + const sourceUuid = this.parent.sourceUuid; + const subclasses = game.items.filter(x => x.type === 'subclass' && x.system.linkedClass === sourceUuid); for (const pack of game.packs) { const packIds = []; const indexes = await pack.getIndex({ fields: ['system.linkedClass'] }); for (const index of indexes) { if (index.type !== 'subclass') continue; - if (!uuids.includes(index.system?.linkedClass)) continue; + if (index.system?.linkedClass !== sourceUuid) continue; if (subclasses.find(x => x.uuid === index.uuid)) continue; packIds.push(index._id); } diff --git a/module/data/item/community.mjs b/module/data/item/community.mjs index 6d0549762..6f4470b84 100644 --- a/module/data/item/community.mjs +++ b/module/data/item/community.mjs @@ -1,4 +1,4 @@ -import { getFeaturesHTMLData } from '../../helpers/utils.mjs'; +import { fromUuids, getFeaturesHTMLData } from '../../helpers/utils.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; import BaseDataItem from './base.mjs'; @@ -27,6 +27,10 @@ export default class DHCommunity extends BaseDataItem { /**@inheritdoc */ async getDescriptionData() { + // Preload all community features for acquisition from the cache + // todo: make feature acquisition async and replace feature helpers for methods + await fromUuids(this._source.features); + const baseDescription = this.description; const features = await getFeaturesHTMLData(this.features); diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs index 55b078c22..cbf73bc37 100644 --- a/module/data/item/subclass.mjs +++ b/module/data/item/subclass.mjs @@ -70,9 +70,7 @@ export default class DHSubclass extends BaseDataItem { return false; } - const match = [multiclass, actorClass].find( - c => c && (c._stats.compendiumSource ?? c.uuid) === this.linkedClass - ); + const match = [multiclass, actorClass].find(c => c && c.sourceUuid === this.linkedClass); if (!match) { const key = multiclass ? 'subclassNotInMulticlass' : 'subclassNotInClass'; ui.notifications.warn(`DAGGERHEART.UI.Notifications.${key}`, { localize: true }); @@ -91,7 +89,7 @@ export default class DHSubclass extends BaseDataItem { ? game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.spellcastingTrait].label) : null; - // Preload all class features for acquisition from the cache + // Preload all subclass features for acquisition from the cache // todo: make feature acquisition async and replace feature helpers for methods await fromUuids(this._source.features.map(f => f.item)); diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 84e4de7fe..39c0fc8e5 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -113,6 +113,10 @@ export default class DHWeapon extends AttachableItem { ); } + get itemFeatures() { + return this.weaponFeatures; + } + /**@inheritdoc */ async getDescriptionData() { const baseDescription = this.description; @@ -269,8 +273,4 @@ export default class DHWeapon extends AttachableItem { return labels; } - - get itemFeatures() { - return this.weaponFeatures; - } } diff --git a/module/data/levelup.mjs b/module/data/levelup.mjs index 4dc1c058b..e30bf52d5 100644 --- a/module/data/levelup.mjs +++ b/module/data/levelup.mjs @@ -19,12 +19,12 @@ export class DhLevelup extends foundry.abstract.DataModel { const initialAchievements = i === tier.levels.start ? tier.initialAchievements : {}; const experiences = initialAchievements.experience ? [...Array(initialAchievements.experience.nr).keys()].reduce((acc, _) => { - acc[foundry.utils.randomID()] = { - name: '', - modifier: initialAchievements.experience.modifier - }; - return acc; - }, {}) + acc[foundry.utils.randomID()] = { + name: '', + modifier: initialAchievements.experience.modifier + }; + return acc; + }, {}) : {}; const domainCards = [...Array(tier.domainCardByLevel).keys()].reduce((acc, _) => { @@ -298,9 +298,9 @@ export class DhLevelupLevel extends foundry.abstract.DataModel { experiences: levelData.achievements?.experiences ?? achievements.experiences ?? {}, domainCards: levelData.achievements?.domainCards ? levelData.achievements.domainCards.reduce((acc, card, index) => { - acc[index] = { ...card }; - return acc; - }, {}) + acc[index] = { ...card }; + return acc; + }, {}) : (achievements.domainCards ?? {}), proficiency: levelData.achievements?.proficiency ?? achievements.proficiency ?? null }, diff --git a/module/data/registeredTriggers.mjs b/module/data/registeredTriggers.mjs index ab86351c7..64e7f93e8 100644 --- a/module/data/registeredTriggers.mjs +++ b/module/data/registeredTriggers.mjs @@ -149,12 +149,15 @@ export default class RegisteredTriggers extends Map { const result = await command(...args); if (result?.updates?.length) updates.push(...result.updates); - } catch (_) { + } catch { + const item = await foundry.utils.fromUuid(itemUuid); const triggerName = game.i18n.localize(triggerData.label); - ui.notifications.error( + console.error( game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerError', { trigger: triggerName, - actor: currentActor?.name + actor: currentActor?.name, + item: item?.name ?? '', + itemUuid: item?.uuid ?? '' }) ); } diff --git a/module/data/settings/Homebrew.mjs b/module/data/settings/Homebrew.mjs index 312474588..a0761e20f 100644 --- a/module/data/settings/Homebrew.mjs +++ b/module/data/settings/Homebrew.mjs @@ -2,7 +2,7 @@ import { defaultRestOptions } from '../../config/generalConfig.mjs'; import { resetAndRerenderActors } from '../../helpers/utils.mjs'; import { ActionsField } from '../fields/actionField.mjs'; -const currencyField = (initial, label, icon) => +const currencyField = (initial, label, icon, initialAmount = 0) => new foundry.data.fields.SchemaField({ enabled: new foundry.data.fields.BooleanField({ required: true, initial: true }), label: new foundry.data.fields.StringField({ @@ -10,7 +10,14 @@ const currencyField = (initial, label, icon) => initial, label }), - icon: new foundry.data.fields.StringField({ required: true, nullable: false, blank: true, initial: icon }) + icon: new foundry.data.fields.StringField({ required: true, nullable: false, blank: true, initial: icon }), + initialAmount: new foundry.data.fields.NumberField({ + required: true, + integer: true, + min: 0, + initial: initialAmount, + label: 'DAGGERHEART.SETTINGS.Homebrew.currency.initialAmount' + }) }); const restMoveField = () => @@ -108,7 +115,8 @@ export default class DhHomebrew extends foundry.abstract.DataModel { handfuls: currencyField( 'Handfuls', 'DAGGERHEART.SETTINGS.Homebrew.currency.handfulName', - 'fa-solid fa-coins' + 'fa-solid fa-coins', + 1 ), bags: currencyField('Bags', 'DAGGERHEART.SETTINGS.Homebrew.currency.bagName', 'fa-solid fa-sack'), chests: currencyField( @@ -193,6 +201,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel { for (const type of ['coins', 'handfuls', 'bags', 'chests']) { const initial = this.schema.fields.currency.fields[type].getInitialValue(); source.currency[type] = foundry.utils.mergeObject(initial, source.currency[type], { inplace: false }); + source.currency[type].initialAmount ??= 0; } return source; } diff --git a/module/dice/d20Roll.mjs b/module/dice/d20Roll.mjs index b1d3bd0b7..e9c447e4f 100644 --- a/module/dice/d20Roll.mjs +++ b/module/dice/d20Roll.mjs @@ -100,16 +100,19 @@ export default class D20Roll extends DHRoll { this.options.roll.modifiers = this.applyBaseBonus(); - const actorExperiences = this.options.roll.companionRoll - ? (this.options.data?.companion?.system.experiences ?? {}) - : (this.options.data.system?.experiences ?? {}); - this.options.experiences?.forEach(m => { - if (actorExperiences[m]) - this.options.roll.modifiers.push({ - label: actorExperiences[m].name, - value: actorExperiences[m].value - }); - }); + let actorExperiences = this.options.data.system?.experiences ?? {}; + if (this.options.roll.companionRoll) { + const companion = typeof this.options.data.companion === 'string' ? + foundry.utils.fromUuidSync(this.options.data.companion) : + this.options.data.companion; + actorExperiences = companion?.system?.experiences ?? {}; + } + for (const m of this.options.experiences?.filter(m => !!actorExperiences[m]) ?? []) { + this.options.roll.modifiers.push({ + label: actorExperiences[m].name, + value: actorExperiences[m].value + }); + } this.addModifiers(); if (this.options.extraFormula) { diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index 02c4ab242..1c6ec829f 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -23,6 +23,10 @@ export default class DHRoll extends Roll { static DefaultDialog = D20RollDialog; + /** + * @param {Partial} config + * @returns {Promise} + */ static async build(config = {}, message = {}) { const roll = await this.buildConfigure(config, message); if (!roll) return; @@ -34,9 +38,14 @@ export default class DHRoll extends Roll { return config; } + /** + * @param {Partial} config + * @returns {Promise} + */ static async buildConfigure(config = {}, message = {}) { config.hooks = [...this.getHooks(), '']; config.dialog ??= {}; + config.damageOptions ??= {}; for (const hook of config.hooks) { if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null; @@ -103,9 +112,9 @@ export default class DHRoll extends Roll { if (action?.chatDisplay) { actionDescription = action ? await foundry.applications.ux.TextEditor.implementation.enrichHTML(action.description, { - relativeTo: config.data, - rollData: config.data.getRollData?.() ?? {} - }) + relativeTo: config.data, + rollData: config.data.getRollData?.() ?? {} + }) : null; config.actionChatMessageHandled = true; } diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index 1d2d556a2..e2d967a32 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -1,6 +1,6 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; import D20Roll from './d20Roll.mjs'; -import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; +import { parseRallyDice, setDiceSoNiceForDualityRoll, shouldUseHopeFearAutomation } from '../helpers/utils.mjs'; import { getDiceSoNicePresets } from '../config/generalConfig.mjs'; import { updateResourcesForDualityReroll } from './helpers.mjs'; @@ -109,10 +109,10 @@ export default class DualityRoll extends D20Roll { const label = this.guaranteedCritical ? 'DAGGERHEART.GENERAL.guaranteedCriticalSuccess' : this.isCritical - ? 'DAGGERHEART.GENERAL.criticalSuccess' - : this.withHope - ? 'DAGGERHEART.GENERAL.hope' - : 'DAGGERHEART.GENERAL.fear'; + ? 'DAGGERHEART.GENERAL.criticalSuccess' + : this.withHope + ? 'DAGGERHEART.GENERAL.hope' + : 'DAGGERHEART.GENERAL.fear'; return game.i18n.localize(label); } @@ -147,8 +147,8 @@ export default class DualityRoll extends D20Roll { const advDieClass = this.hasAdvantage ? game.system.api.dice.diceTypes.AdvantageDie : this.hasDisadvantage - ? game.system.api.dice.diceTypes.DisadvantageDie - : null; + ? game.system.api.dice.diceTypes.DisadvantageDie + : null; if (advDieClass) { const advDie = new advDieClass({ faces: this.advantageFaces, number: this.advantageNumber }); if (this.terms.length < 4) { @@ -312,11 +312,9 @@ export default class DualityRoll extends D20Roll { } static async addDualityResourceUpdates(config) { - const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); - const hopeFearAutomation = automationSettings.hopeFear; if ( !config.source?.actor || - (game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) || + !shouldUseHopeFearAutomation() || config.actionType === 'reaction' || config.skips?.resources ) @@ -334,15 +332,15 @@ export default class DualityRoll extends D20Roll { const fear = (config.roll.result.duality === -1 ? 1 : 0) - (config.rerolledRoll.result.duality === -1 ? 1 : 0); - if (hope !== 0) updates.push({ key: 'hope', value: hope, total: -1 * hope, enabled: true }); - if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, total: stress, enabled: true }); - if (fear !== 0) updates.push({ key: 'fear', value: fear, total: -1 * fear, enabled: true }); + if (hope !== 0) updates.push({ key: 'hope', value: hope, enabled: true }); + if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, enabled: true }); + if (fear !== 0) updates.push({ key: 'fear', value: fear, enabled: true }); } } else { if (config.roll.isCritical || config.roll.result.duality === 1) - updates.push({ key: 'hope', value: 1, total: -1, enabled: true }); - if (config.roll.isCritical) updates.push({ key: 'stress', value: -1, total: 1, enabled: true }); - if (config.roll.result.duality === -1) updates.push({ key: 'fear', value: 1, total: -1, enabled: true }); + updates.push({ key: 'hope', value: 1, enabled: true }); + if (config.roll.isCritical) updates.push({ key: 'stress', value: -1, enabled: true }); + if (config.roll.result.duality === -1) updates.push({ key: 'fear', value: 1, enabled: true }); } if (updates.length) { diff --git a/module/dice/helpers.mjs b/module/dice/helpers.mjs index 335199494..d03970d0f 100644 --- a/module/dice/helpers.mjs +++ b/module/dice/helpers.mjs @@ -1,17 +1,28 @@ -export function updateResourcesForDualityReroll(oldDuality, newDuality, actor) { - const { hopeFear } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); - if (game.user.isGM ? !hopeFear.gm : !hopeFear.players) return; +import { ResourceUpdateMap } from '../data/action/baseAction.mjs'; - const updates = []; +export function updateResourcesForDualityReroll(oldDuality, newDuality, actor) { const hope = (newDuality >= 0 ? 1 : 0) - (oldDuality >= 0 ? 1 : 0); const stress = (newDuality === 0 ? 1 : 0) - (oldDuality === 0 ? 1 : 0); const fear = (newDuality === -1 ? 1 : 0) - (oldDuality === -1 ? 1 : 0); - if (hope !== 0) updates.push({ key: 'hope', value: hope, total: -1 * hope, enabled: true }); - if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, total: stress, enabled: true }); - if (fear !== 0) updates.push({ key: 'fear', value: fear, total: -1 * fear, enabled: true }); + const { hopeFear, countdownAutomation } = + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); + + if (game.user.isGM ? hopeFear.gm : hopeFear.players) { + const updates = []; + if (hope !== 0) updates.push({ key: 'hope', value: hope, enabled: true }); + if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, enabled: true }); + if (fear !== 0) updates.push({ key: 'fear', value: fear, enabled: true }) + + const resourceUpdates = new ResourceUpdateMap(actor); + resourceUpdates.addResources(updates); + resourceUpdates.updateResources(); + } - const resourceUpdates = new ResourceUpdateMap(actor); - resourceUpdates.addResources(updates); - resourceUpdates.updateResources(); + if (countdownAutomation && fear !== 0) { + game.system.api.applications.ui.DhCountdowns.updateCountdowns({ + type: CONFIG.DH.GENERAL.countdownProgressionTypes.fear.id, + undo: fear === 1 ? false : true + }); + } } diff --git a/module/documents/_types.d.ts b/module/documents/_types.d.ts new file mode 100644 index 000000000..a94d63956 --- /dev/null +++ b/module/documents/_types.d.ts @@ -0,0 +1,22 @@ +import BaseDataActor from '../data/actor/base.mjs' +import DHItem from './item.mjs'; +import BaseDataItem from '../data/item/base.mjs'; +import DhActiveEffect from './activeEffect.mjs'; +import EmbeddedCollection from '@common/abstract/embedded-collection.mjs'; + +declare module './actor.mjs' { + export default interface DhpActor { + system: T; + items: EmbeddedCollection; + effects: EmbeddedCollection; + } +} + +declare module './item.mjs' { + export default interface DHItem { + parent: DhpActor; + actor: DhpActor; + system: T; + effects: EmbeddedCollection; + } +} \ No newline at end of file diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index f9239a902..4a9f3cc4b 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -65,6 +65,10 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { ); } + get hasDescription() { + return Boolean(this.description); + } + /* -------------------------------------------- */ /* Event Handlers */ /* -------------------------------------------- */ @@ -175,8 +179,8 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { return model instanceof documentClass ? model : model.parent - ? this.#resolveParentDocument(model.parent, documentClass) - : null; + ? this.#resolveParentDocument(model.parent, documentClass) + : null; } static getChangeValue(model, change, effect) { @@ -212,7 +216,7 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { try { const evl = new Function('sandbox', `with (sandbox) { return ${expression}}`); result = evl(Roll.MATH_PROXY); - } catch (err) { + } catch { return expression; } @@ -224,12 +228,13 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { * @returns {string[]} An array of localized tag strings. */ _getTags() { - const tags = [ - `${game.i18n.localize(this.parent.system.metadata.label)}: ${this.parent.name}`, - game.i18n.localize( - this.isTemporary ? 'DAGGERHEART.EFFECTS.Duration.temporary' : 'DAGGERHEART.EFFECTS.Duration.passive' - ) - ]; + const tags = []; + const originActor = DhActiveEffect.#resolveParentDocument(fromUuidSync(this.origin, { strict: false }), Actor); + if (originActor && originActor !== this.actor) { + tags.push(_loc('DAGGERHEART.EFFECTS.OriginTag', { name: originActor.name })); + } else if (!(this.parent instanceof Actor)) { + tags.push(`${_loc(this.parent.system.metadata.label)}: ${this.parent.name}`); + } for (const statusId of this.statuses) { const status = CONFIG.statusEffects.find(s => s.id === statusId); diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index fb10435f1..8ef64f655 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -531,21 +531,7 @@ export default class DhpActor extends Actor { } /** - * @param {object} config - * @param {Event} config.event - * @param {string} config.title - * @param {object} config.roll - * @param {number} config.roll.modifier - * @param {boolean} [config.roll.simple=false] - * @param {string} [config.roll.type] - * @param {number} [config.roll.difficulty] - * @param {boolean} [config.hasDamage] - * @param {boolean} [config.hasEffect] - * @param {object} [config.chatMessage] - * @param {string} config.chatMessage.template - * @param {boolean} [config.chatMessage.mute] - * @param {object} [config.targets] - * @param {object} [config.costs] + * @param {Partial} config */ async diceRoll(config) { config.source = { ...(config.source ?? {}), actor: this.uuid }; @@ -563,7 +549,7 @@ export default class DhpActor extends Actor { headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { ability: abilityLabel }), - effects: await game.system.api.data.actions.actionsTypes.base.getEffects(this), + effects: await game.system.api.data.actions.actionsTypes.base.getActionRelevantEffects(this), roll: { trait: trait, type: 'trait' diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 893e6e5ca..b555dfca5 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -9,9 +9,9 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { actor && this.isContentVisible ? actor : { - img: this.author.avatar ? this.author.avatar : 'icons/svg/mystery-man.svg', - name: '' - }; + img: this.author.avatar ? this.author.avatar : 'icons/svg/mystery-man.svg', + name: '' + }; /* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */ const html = await super.renderHTML({ actor: actorData, author: this.author }); @@ -167,7 +167,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { if (this.system.action) { const actor = await foundry.utils.fromUuid(config.source.actor); const item = actor?.items.get(config.source.item) ?? null; - config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(actor, item); + config.effects = await game.system.api.data.actions.actionsTypes.base.getActionRelevantEffects(actor, item); await this.system.action.workflow.get('damage')?.execute(config, this._id, true); } } @@ -290,14 +290,14 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { behaviors: effects.length > 0 ? [ - { - name: game.i18n.localize('TYPES.RegionBehavior.applyActiveEffect'), - type: 'applyActiveEffect', - system: { - effects: effects - } - } - ] + { + name: game.i18n.localize('TYPES.RegionBehavior.applyActiveEffect'), + type: 'applyActiveEffect', + system: { + effects: effects + } + } + ] : [], displayMeasurements: true, locked: false, diff --git a/module/documents/combat.mjs b/module/documents/combat.mjs index 20996b77b..e74127e97 100644 --- a/module/documents/combat.mjs +++ b/module/documents/combat.mjs @@ -46,7 +46,9 @@ export default class DhpCombat extends Combat { for (let actor of actors) { await actor.createEmbeddedDocuments( 'ActiveEffect', - effects.filter(x => x.effectTargetTypes.includes(actor.type)) + effects + .filter(x => x.effectTargetTypes.includes(actor.type)) + .map(x => foundry.utils.deepClone(x)) ); } } else { diff --git a/module/documents/item.mjs b/module/documents/item.mjs index f46e24e6b..147175382 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -5,6 +5,16 @@ import ActionSelectionDialog from '../applications/dialogs/actionSelectionDialog * @extends {foundry.documents.Item} */ export default class DHItem extends foundry.documents.Item { + /** + * Returns the uuid of the original item this item was derived from, + * or its own uuid if its a compendium item or not derived from a source item. + * @returns {string} + */ + get sourceUuid() { + const isCompendium = this._id && this.pack && !this.isEmbedded; + return isCompendium ? this.uuid : this._stats.duplicateSource ?? this._stats.compendiumSource ?? this.uuid; + } + /** @inheritDoc */ prepareEmbeddedDocuments() { super.prepareEmbeddedDocuments(); @@ -79,6 +89,10 @@ export default class DHItem extends foundry.documents.Item { return !pack?.locked && this.isOwner && isValidType && hasActions; } + get hasDescription() { + return Boolean(this.system.description) || Boolean(this.system.itemFeatures?.length); + } + /** @inheritdoc */ static async createDialog(data = {}, createOptions = {}, options = {}) { const { folders, types, template, context = {}, ...dialogOptions } = options; @@ -98,8 +112,8 @@ export default class DHItem extends foundry.documents.Item { isInventoryItem === true ? 'Inventory Items' //TODO localize : isInventoryItem === false - ? 'Character Items' //TODO localize - : 'Other'; //TODO localize + ? 'Character Items' //TODO localize + : 'Other'; //TODO localize return { value: type, label, group }; } diff --git a/module/documents/token.mjs b/module/documents/token.mjs index 308627246..8e91d4f0f 100644 --- a/module/documents/token.mjs +++ b/module/documents/token.mjs @@ -324,7 +324,7 @@ export default class DHToken extends CONFIG.Token.documentClass { } let x = 0.5 * bottom; let y = 0.25; - for (let k = width - bottom; k--; ) { + for (let k = width - bottom; k--;) { points.push(x, y); x += 0.5; y -= 0.25; @@ -333,7 +333,7 @@ export default class DHToken extends CONFIG.Token.documentClass { y += 0.25; } points.push(x, y); - for (let k = bottom; k--; ) { + for (let k = bottom; k--;) { y += 0.5; points.push(x, y); x += 0.5; @@ -341,14 +341,14 @@ export default class DHToken extends CONFIG.Token.documentClass { points.push(x, y); } y += 0.5; - for (let k = top; k--; ) { + for (let k = top; k--;) { points.push(x, y); x -= 0.5; y += 0.25; points.push(x, y); y += 0.5; } - for (let k = width - top; k--; ) { + for (let k = width - top; k--;) { points.push(x, y); x -= 0.5; y += 0.25; @@ -357,7 +357,7 @@ export default class DHToken extends CONFIG.Token.documentClass { y -= 0.25; } points.push(x, y); - for (let k = top; k--; ) { + for (let k = top; k--;) { y -= 0.5; points.push(x, y); x -= 0.5; @@ -365,7 +365,7 @@ export default class DHToken extends CONFIG.Token.documentClass { points.push(x, y); } y -= 0.5; - for (let k = bottom; k--; ) { + for (let k = bottom; k--;) { points.push(x, y); x += 0.5; y -= 0.25; diff --git a/module/enrichers/DamageEnricher.mjs b/module/enrichers/DamageEnricher.mjs index e3f9c42af..db0e87298 100644 --- a/module/enrichers/DamageEnricher.mjs +++ b/module/enrichers/DamageEnricher.mjs @@ -1,7 +1,7 @@ import { parseInlineParams } from './parser.mjs'; export default function DhDamageEnricher(match, _options) { - const { value, type, inline } = parseInlineParams(match[1]); + const { value, type, inline } = parseInlineParams(match[1], { first: 'value' }); if (!value || !type) return match[0]; return getDamageMessage(value, type, inline, match[0]); } @@ -59,7 +59,7 @@ export const renderDamageButton = async event => { { formula: value, applyTo: CONFIG.DH.GENERAL.healingTypes.hitPoints.id, - type: type + damageTypes: type } ] }; diff --git a/module/enrichers/DualityRollEnricher.mjs b/module/enrichers/DualityRollEnricher.mjs index 5b66179fd..a7db01a49 100644 --- a/module/enrichers/DualityRollEnricher.mjs +++ b/module/enrichers/DualityRollEnricher.mjs @@ -15,8 +15,8 @@ function getDualityMessage(roll, flavor) { (roll?.trait ? game.i18n.format('DAGGERHEART.GENERAL.rollWith', { roll: trait }) : roll?.reaction - ? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll') - : game.i18n.localize('DAGGERHEART.GENERAL.duality')); + ? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll') + : game.i18n.localize('DAGGERHEART.GENERAL.duality')); const dataLabel = trait ? game.i18n.localize(abilities[roll.trait].label) @@ -25,14 +25,14 @@ function getDualityMessage(roll, flavor) { const advantage = roll?.advantage ? CONFIG.DH.ACTIONS.advantageState.advantage.value : roll?.disadvantage - ? CONFIG.DH.ACTIONS.advantageState.disadvantage.value - : undefined; + ? CONFIG.DH.ACTIONS.advantageState.disadvantage.value + : undefined; const advantageLabel = advantage === CONFIG.DH.ACTIONS.advantageState.advantage.value ? 'Advantage' : advantage === CONFIG.DH.ACTIONS.advantageState.disadvantage.value - ? 'Disadvantage' - : undefined; + ? 'Disadvantage' + : undefined; const dualityElement = document.createElement('span'); dualityElement.innerHTML = ` diff --git a/module/enrichers/FateRollEnricher.mjs b/module/enrichers/FateRollEnricher.mjs index c82bbcb2f..8513ed947 100644 --- a/module/enrichers/FateRollEnricher.mjs +++ b/module/enrichers/FateRollEnricher.mjs @@ -49,7 +49,7 @@ export const renderFateButton = async event => { const fateTypeData = getFateTypeData(button.dataset?.fatetype); if (!fateTypeData) ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing')); - const { value: fateType, label: fateTypeLabel } = fateTypeData; + const { value: fateType } = fateTypeData; await enrichedFateRoll( { diff --git a/module/enrichers/TemplateEnricher.mjs b/module/enrichers/TemplateEnricher.mjs index 8db3ec14f..bbe93b179 100644 --- a/module/enrichers/TemplateEnricher.mjs +++ b/module/enrichers/TemplateEnricher.mjs @@ -8,8 +8,8 @@ export default function DhTemplateEnricher(match, _options) { const range = params.range && Number.isNaN(Number(params.range)) ? Object.values(CONFIG.DH.GENERAL.templateRanges).find( - x => x.id.toLowerCase() === params.range || x.short === params.range - )?.id + x => x.id.toLowerCase() === params.range || x.short === params.range + )?.id : params.range; if (!CONFIG.DH.GENERAL.templateTypes[type] || !range) return match[0]; diff --git a/module/enrichers/parser.mjs b/module/enrichers/parser.mjs index 365caec98..76ea0b735 100644 --- a/module/enrichers/parser.mjs +++ b/module/enrichers/parser.mjs @@ -8,7 +8,7 @@ export function parseInlineParams(paramString, { first } = {}) { const parts = paramString.split('|').map(x => x.trim()); const params = {}; for (const [idx, param] of parts.entries()) { - if (first && idx === 0) { + if (first && idx === 0 && !param.includes(':')) { params[first] = param; } else { const parts = param.split(':'); diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 2f20175ba..cb79a76b8 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -108,9 +108,9 @@ export const tagifyElement = (element, baseOptions, onChange, tagifyOptions = {} const options = Array.isArray(baseOptions) ? baseOptions : Object.keys(baseOptions).map(optionKey => ({ - ...baseOptions[optionKey], - id: optionKey - })); + ...baseOptions[optionKey], + id: optionKey + })); const tagifyElement = new Tagify(element, { tagTextProp: 'name', @@ -318,7 +318,7 @@ export function getDocFromElementSync(element) { const target = element.closest('[data-item-uuid]'); try { return foundry.utils.fromUuidSync(target.dataset.itemUuid) ?? null; - } catch (_) { + } catch { return null; } } @@ -377,7 +377,7 @@ export const itemAbleRollParse = (value, actor, item) => { try { return Roll.replaceFormulaData(slicedValue, rollData); - } catch (_) { + } catch { return ''; } }; @@ -418,40 +418,6 @@ export function createScrollText(actor, data) { } } -export async function createEmbeddedItemWithEffects(actor, baseData, update) { - const data = baseData.uuid.startsWith('Compendium') ? await foundry.utils.fromUuid(baseData.uuid) : baseData; - const [doc] = await actor.createEmbeddedDocuments('Item', [ - { - ...(update ?? data), - ...baseData, - id: data.id, - uuid: data.uuid, - _uuid: data.uuid, - effects: data.effects?.map(effect => effect.toObject()), - _stats: { - ...data._stats, - compendiumSource: data.pack ? `Compendium.${data.pack}.Item.${data.id}` : null - } - } - ]); - - return doc; -} - -export async function createEmbeddedItemsWithEffects(actor, baseData) { - const effectData = []; - for (let d of baseData) { - const data = d.uuid.startsWith('Compendium') ? await foundry.utils.fromUuid(d.uuid) : d; - effectData.push({ - ...data, - id: data.id, - uuid: data.uuid, - effects: data.effects?.map(effect => effect.toObject()) - }); - } - await actor.createEmbeddedDocuments('Item', effectData); -} - export function shuffleArray(array) { let currentIndex = array.length; while (currentIndex != 0) { @@ -605,8 +571,8 @@ export function calculateExpectedValue(formulaOrTerms) { const terms = Array.isArray(formulaOrTerms) ? formulaOrTerms : typeof formulaOrTerms === 'string' - ? parseTermsFromSimpleFormula(formulaOrTerms) - : [formulaOrTerms]; + ? parseTermsFromSimpleFormula(formulaOrTerms) + : [formulaOrTerms]; return terms.reduce((r, t) => r + (t.bonus ?? 0) + (t.diceQuantity ? (t.diceQuantity * (t.faces + 1)) / 2 : 0), 0); } @@ -656,8 +622,8 @@ export async function RefreshFeatures( 'resource.value': increasing ? 0 : game.system.api.documents.DhActiveEffect.effectSafeEval( - Roll.replaceFormulaData(item.system.resource.max, actor.getRollData()) - ) + Roll.replaceFormulaData(item.system.resource.max, actor.getRollData()) + ) }; } if (item.system.metadata?.hasActions) { @@ -879,6 +845,7 @@ export async function fromUuids(uuids) { const packEmbeddedEntries = entries.filter( e => !(e.value instanceof Document) && + e.parsed && e.parsed.collection instanceof foundry.documents.collections.CompendiumCollection && e.parsed.embedded.length > 0 ); @@ -895,7 +862,7 @@ export async function fromUuids(uuids) { const pack = game.packs.get(packGroup[0].value.pack); if (!pack) continue; - const ids = packGroup.map(p => p.parsed.id); + const ids = packGroup.map(p => p.parsed?.id).filter(id => !!id); const documents = await pack.getDocuments({ _id__in: ids }); for (const p of packGroup) { p.value = documents.find(d => d.id === p.parsed.id) ?? p.value; @@ -918,3 +885,31 @@ export async function triggerChatRollFx(rolls, options = { whisper: false, blind foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); } } + +export function shouldUseHopeFearAutomation(options = { gmAsPlayer: true }) { + const { hopeFear } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); + return (!game.user.isGM || options.gmAsPlayer) ? hopeFear.players : hopeFear.gm; +} + +export async function getWorldActor(baseActor) { + if (baseActor.inCompendium) { + const worldActorCopy = game.actors.find(x => + x._stats.compendiumSource === baseActor.uuid && + (!x.prototypeToken.actorLink || x.name === baseActor.name) + ); + + if (worldActorCopy) + return worldActorCopy; + + const baseActorData = baseActor; + return await game.system.api.documents.DhpActor.create({ + ...baseActorData, + _stats: { + ...baseActorData._stats, + compendiumSource: baseActor.uuid + } + }); + } + + return baseActor; +} \ No newline at end of file diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index 1b08e0ad3..bf3153589 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -50,6 +50,7 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/ui/chat/parts/target-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/button-part.hbs', 'systems/daggerheart/templates/ui/itemBrowser/itemContainer.hbs', + 'systems/daggerheart/templates/ui/countdowns/parts/countdowns.hbs', 'systems/daggerheart/templates/scene/dh-config.hbs', 'systems/daggerheart/templates/settings/appearance-settings/diceSoNiceTab.hbs', 'systems/daggerheart/templates/sheets/activeEffect/typeChanges/armorChange.hbs' diff --git a/module/systemRegistration/migrations.mjs b/module/systemRegistration/migrations.mjs index b4c446b25..6971c34c0 100644 --- a/module/systemRegistration/migrations.mjs +++ b/module/systemRegistration/migrations.mjs @@ -1,5 +1,4 @@ import { defaultRestOptions } from '../config/generalConfig.mjs'; -import { RefreshType, socketEvent } from './socket.mjs'; export async function runMigrations() { let lastMigrationVersion = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion); @@ -24,8 +23,8 @@ export async function runMigrations() { const { originItemType, isMulticlass, identifier } = item.system; const base = originItemType ? actor.items.find( - x => x.type === originItemType && Boolean(isMulticlass) === Boolean(x.system.isMulticlass) - ) + x => x.type === originItemType && Boolean(isMulticlass) === Boolean(x.system.isMulticlass) + ) : null; if (base) { const feature = base.system.features.find(x => x.item && x.item.uuid === item.uuid); @@ -153,61 +152,26 @@ export async function runMigrations() { await pack.configure({ locked: true }); } - /* Migrate old countdown structure */ - const countdownSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); - const getCountdowns = (data, type) => { - return Object.keys(data.countdowns).reduce((acc, key) => { - const countdown = data.countdowns[key]; - acc[key] = { - ...countdown, - type: type, - ownership: Object.keys(countdown.ownership.players).reduce((acc, key) => { - acc[key] = - countdown.ownership.players[key].type === 1 ? 2 : countdown.ownership.players[key].type; - return acc; - }, {}), - progress: { - ...countdown.progress, - type: countdown.progress.type.value - } - }; - - return acc; - }, {}); - }; - - await countdownSettings.updateSource({ - countdowns: { - ...getCountdowns(countdownSettings.narrative, CONFIG.DH.GENERAL.countdownBaseTypes.narrative.id), - ...getCountdowns(countdownSettings.encounter, CONFIG.DH.GENERAL.countdownBaseTypes.encounter.id) - } - }); - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSettings); - - game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.Refresh, - data: { refreshType: RefreshType.Countdown } - }); - Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown }); - lastMigrationVersion = '1.2.0'; } if (foundry.utils.isNewerVersion('1.2.7', lastMigrationVersion)) { - const tagTeam = game.settings.get(CONFIG.DH.id, 'TagTeamRoll'); - const initatorMissing = tagTeam.initiator && !game.actors.some(actor => actor.id === tagTeam.initiator); - const missingMembers = Object.keys(tagTeam.members).reduce((acc, id) => { - if (!game.actors.some(actor => actor.id === id)) { - acc[id] = _del; - } - return acc; - }, {}); + try { + const tagTeam = game.settings.get(CONFIG.DH.id, 'TagTeamRoll'); + const initatorMissing = tagTeam.initiator && !game.actors.some(actor => actor.id === tagTeam.initiator); + const missingMembers = Object.keys(tagTeam.members).reduce((acc, id) => { + if (!game.actors.some(actor => actor.id === id)) { + acc[id] = _del; + } + return acc; + }, {}); - await tagTeam.updateSource({ - initiator: initatorMissing ? null : tagTeam.initiator, - members: missingMembers - }); - await game.settings.set(CONFIG.DH.id, 'TagTeamRoll', tagTeam); + await tagTeam.updateSource({ + initiator: initatorMissing ? null : tagTeam.initiator, + members: missingMembers + }); + await game.settings.set(CONFIG.DH.id, 'TagTeamRoll', tagTeam); + } catch { } lastMigrationVersion = '1.2.7'; } @@ -303,6 +267,8 @@ export async function runMigrations() { /* Migrate existing effects modifying armor, creating new Armor Effects instead */ const migrateEffects = async entity => { + if (!entity?.effects) return; + for (const effect of entity.effects) { if (effect.system.changes.every(x => x.key !== 'system.armorScore')) continue; diff --git a/module/systemRegistration/settings.mjs b/module/systemRegistration/settings.mjs index a66323c79..1a9852743 100644 --- a/module/systemRegistration/settings.mjs +++ b/module/systemRegistration/settings.mjs @@ -199,7 +199,10 @@ const registerNonConfigSettings = () => { game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, { scope: 'world', config: false, - type: DhCountdowns + type: DhCountdowns, + onChange: value => { + value.handleChange(); + } }); game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings, { diff --git a/package-lock.json b/package-lock.json index 282230326..dee096eb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,18 +13,20 @@ "rollup": "^4.40.0" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@foundryvtt/foundryvtt-cli": "^1.0.2", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", + "@stylistic/eslint-plugin": "^5.10.0", "concurrently": "^8.2.2", "eslint": "^10.2.1", - "eslint-plugin-prettier": "^5.5.5", "globals": "^17.5.0", "husky": "^9.1.7", "lint-staged": "^16.4.0", "postcss": "^8.4.32", - "prettier": "^3.5.3", - "rollup-plugin-postcss": "^4.0.2" + "rollup-plugin-postcss": "^4.0.2", + "typescript": "^6.0.3", + "typescript-eslint": "^8.60.1" } }, "node_modules/@babel/runtime": { @@ -158,6 +160,27 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, "node_modules/@eslint/object-schema": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", @@ -420,19 +443,6 @@ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - } - }, "node_modules/@rollup/plugin-commonjs": { "version": "25.0.8", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz", @@ -761,6 +771,58 @@ "util": "^0.12.4" } }, + "node_modules/@stylistic/eslint-plugin": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz", + "integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/types": "^8.56.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.0.0 || ^10.0.0" + } + }, + "node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@stylistic/eslint-plugin/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -795,6 +857,288 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", + "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/type-utils": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.60.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", + "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", + "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.60.1", + "@typescript-eslint/types": "^8.60.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", + "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", + "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", + "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", + "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", + "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.60.1", + "@typescript-eslint/tsconfig-utils": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", + "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", + "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@yaireo/tagify": { "version": "4.35.1", "resolved": "https://registry.npmjs.org/@yaireo/tagify/-/tagify-4.35.1.tgz", @@ -1853,10 +2197,11 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -2241,37 +2586,6 @@ } } }, - "node_modules/eslint-plugin-prettier": { - "version": "5.5.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", - "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.1", - "synckit": "^0.11.12" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, "node_modules/eslint-scope": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", @@ -2511,13 +2825,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", @@ -2554,6 +2861,24 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -5217,34 +5542,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", - "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", - "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -6053,22 +6350,6 @@ "node": ">= 10" } }, - "node_modules/synckit": { - "version": "0.11.12", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", - "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.2.9" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" - } - }, "node_modules/teex": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", @@ -6116,6 +6397,23 @@ "node": ">=18" } }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6147,6 +6445,19 @@ "tree-kill": "cli.js" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -6171,6 +6482,44 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.1.tgz", + "integrity": "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.60.1", + "@typescript-eslint/parser": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", diff --git a/package.json b/package.json index 73a7fe993..533d250a5 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ }, "scripts": { "start": "node ./tools/run-start.mjs", - "start-test": "node ./resources/app/main.js --dataPath=./ && rollup -c --watch && gulp", "build": "npm run rollup && npm run gulp", "rollup": "rollup -c", "gulp": "gulp less", @@ -18,24 +17,25 @@ "pullYMLtoLDB": "node ./tools/pullYMLtoLDB.mjs", "pullYMLtoLDBBuild": "node ./tools/pullYMLtoLDB.mjs --build", "createSymlink": "node ./tools/create-symlink.mjs", - "setup:dev": "node ./tools/dev-setup.mjs", "lint": "eslint", "lint:fix": "eslint --fix", "prepare": "husky" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@foundryvtt/foundryvtt-cli": "^1.0.2", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", + "@stylistic/eslint-plugin": "^5.10.0", "concurrently": "^8.2.2", "eslint": "^10.2.1", - "eslint-plugin-prettier": "^5.5.5", "globals": "^17.5.0", "husky": "^9.1.7", "lint-staged": "^16.4.0", "postcss": "^8.4.32", - "prettier": "^3.5.3", - "rollup-plugin-postcss": "^4.0.2" + "rollup-plugin-postcss": "^4.0.2", + "typescript": "^6.0.3", + "typescript-eslint": "^8.60.1" }, "lint-staged": { "**/*": "eslint --fix" diff --git a/styles/daggerheart.less b/styles/daggerheart.less index 187402fb7..4da2e0434 100755 --- a/styles/daggerheart.less +++ b/styles/daggerheart.less @@ -1,19 +1,11 @@ @import './less/sheets/index.less'; @import './less/sheets-settings/index.less'; - @import './less/dialog/index.less'; - -@import './less//hud/index.less'; - -@import './less/utils/colors.less'; -@import './less/utils/fonts.less'; - +@import './less/hud/index.less'; +@import './less/utils/index.less'; @import './less/global/index.less'; - @import './less/ui/index.less'; - @import './less/ux/index.less'; @import '../build/tagify.css'; -@import './less/utils/mixin.less'; diff --git a/styles/less/dialog/actions/index.less b/styles/less/dialog/actions/index.less new file mode 100644 index 000000000..e9cc04019 --- /dev/null +++ b/styles/less/dialog/actions/index.less @@ -0,0 +1 @@ +@import "./action-list.less"; \ No newline at end of file diff --git a/styles/less/dialog/attribution/index.less b/styles/less/dialog/attribution/index.less new file mode 100644 index 000000000..2f8eaf459 --- /dev/null +++ b/styles/less/dialog/attribution/index.less @@ -0,0 +1 @@ +@import "./sheet.less"; \ No newline at end of file diff --git a/styles/less/dialog/beastform/index.less b/styles/less/dialog/beastform/index.less new file mode 100644 index 000000000..2f8eaf459 --- /dev/null +++ b/styles/less/dialog/beastform/index.less @@ -0,0 +1 @@ +@import "./sheet.less"; \ No newline at end of file diff --git a/styles/less/dialog/character-creation/index.less b/styles/less/dialog/character-creation/index.less new file mode 100644 index 000000000..adf8d57a5 --- /dev/null +++ b/styles/less/dialog/character-creation/index.less @@ -0,0 +1,4 @@ +@import "./sheet.less"; +@import "./creation-action-footer.less"; +@import "./selections-container.less"; +@import "./tab-navigation.less"; \ No newline at end of file diff --git a/styles/less/dialog/character-reset/index.less b/styles/less/dialog/character-reset/index.less new file mode 100644 index 000000000..1c574e817 --- /dev/null +++ b/styles/less/dialog/character-reset/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/dialog/compendiumBrowserPackDialog/index.less b/styles/less/dialog/compendiumBrowserPackDialog/index.less new file mode 100644 index 000000000..1c574e817 --- /dev/null +++ b/styles/less/dialog/compendiumBrowserPackDialog/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/dialog/damage-reduction/index.less b/styles/less/dialog/damage-reduction/index.less new file mode 100644 index 000000000..0b8e94a8f --- /dev/null +++ b/styles/less/dialog/damage-reduction/index.less @@ -0,0 +1,2 @@ +@import './sheets.less'; +@import './damage-reduction-container.less'; \ No newline at end of file diff --git a/styles/less/dialog/damage-selection/index.less b/styles/less/dialog/damage-selection/index.less new file mode 100644 index 000000000..1c574e817 --- /dev/null +++ b/styles/less/dialog/damage-selection/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/dialog/death-move/index.less b/styles/less/dialog/death-move/index.less new file mode 100644 index 000000000..8a8a16c46 --- /dev/null +++ b/styles/less/dialog/death-move/index.less @@ -0,0 +1 @@ +@import './death-move-container.less'; \ No newline at end of file diff --git a/styles/less/dialog/dice-roll/index.less b/styles/less/dialog/dice-roll/index.less new file mode 100644 index 000000000..8e0af6e06 --- /dev/null +++ b/styles/less/dialog/dice-roll/index.less @@ -0,0 +1 @@ +@import './roll-selection.less'; \ No newline at end of file diff --git a/styles/less/dialog/downtime/index.less b/styles/less/dialog/downtime/index.less new file mode 100644 index 000000000..09cc2dfe8 --- /dev/null +++ b/styles/less/dialog/downtime/index.less @@ -0,0 +1 @@ +@import './downtime-container.less'; \ No newline at end of file diff --git a/styles/less/dialog/group-roll-dialog/_common.less b/styles/less/dialog/group-roll-dialog/_common.less deleted file mode 100644 index f74ab8a06..000000000 --- a/styles/less/dialog/group-roll-dialog/_common.less +++ /dev/null @@ -1,44 +0,0 @@ -h1 { - color: @color-text-emphatic; - font: 700 var(--font-size-24) var(--dh-font-subtitle); - text-align: center; -} - -header { - --bar-color: light-dark(@dark-blue, @golden); - color: light-dark(@dark, @beige); - display: flex; - justify-content: center; - align-items: center; - - &:not(:first-child) { - margin-top: var(--spacer-8); - } - - span { - padding: 0 10px; - } - - &:before { - content: ' '; - flex: 1; - height: 1px; - background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, var(--bar-color) 100%); - } - - &:after { - content: ' '; - flex: 1; - height: 1px; - background: linear-gradient(90deg, var(--bar-color) 0%, rgba(0, 0, 0, 0) 100%); - } -} - -img.portrait { - border-radius: 50%; - border: none; - object-fit: cover; - object-position: center top; - width: 2.5rem; - height: 2.5rem; -} diff --git a/styles/less/dialog/group-roll-dialog/index.less b/styles/less/dialog/group-roll-dialog/index.less index 27925fa2c..f90b57dc5 100644 --- a/styles/less/dialog/group-roll-dialog/index.less +++ b/styles/less/dialog/group-roll-dialog/index.less @@ -1,8 +1,3 @@ -.daggerheart.dialog.dh-style.views.group-roll-dialog { - .window-content { - @import "./_common.less"; - } -} - -@import "./initialization.less"; -@import "./main.less"; +@import './sheet.less'; +@import './initialization.less'; +@import './main.less'; diff --git a/styles/less/dialog/group-roll-dialog/sheet.less b/styles/less/dialog/group-roll-dialog/sheet.less new file mode 100644 index 000000000..938710c9c --- /dev/null +++ b/styles/less/dialog/group-roll-dialog/sheet.less @@ -0,0 +1,48 @@ +.daggerheart.dialog.dh-style.views.group-roll-dialog { + .window-content { + h1 { + color: @color-text-emphatic; + font: 700 var(--font-size-24) var(--dh-font-subtitle); + text-align: center; + } + + header { + --bar-color: light-dark(@dark-blue, @golden); + color: light-dark(@dark, @beige); + display: flex; + justify-content: center; + align-items: center; + + &:not(:first-child) { + margin-top: var(--spacer-8); + } + + span { + padding: 0 10px; + } + + &:before { + content: ' '; + flex: 1; + height: 1px; + background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, var(--bar-color) 100%); + } + + &:after { + content: ' '; + flex: 1; + height: 1px; + background: linear-gradient(90deg, var(--bar-color) 0%, rgba(0, 0, 0, 0) 100%); + } + } + + img.portrait { + border-radius: 50%; + border: none; + object-fit: cover; + object-position: center top; + width: 2.5rem; + height: 2.5rem; + } + } +} \ No newline at end of file diff --git a/styles/less/dialog/image-select/index.less b/styles/less/dialog/image-select/index.less new file mode 100644 index 000000000..1c574e817 --- /dev/null +++ b/styles/less/dialog/image-select/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/dialog/index.less b/styles/less/dialog/index.less index e8f61318e..4ce4834e3 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -1,42 +1,20 @@ -@import './attribution/sheet.less'; -@import './level-up/index.less'; - -@import './resource-dice/sheet.less'; - -@import './actions/action-list.less'; - -@import './damage-selection/sheet.less'; - -@import './downtime/downtime-container.less'; - -@import './death-move/death-move-container.less'; - -@import './beastform/sheet.less'; - -@import './character-creation/creation-action-footer.less'; -@import './character-creation/selections-container.less'; -@import './character-creation/sheet.less'; -@import './character-creation/tab-navigation.less'; - -@import './dice-roll/roll-selection.less'; -@import './damage-reduction/damage-reduction-container.less'; -@import './damage-reduction/sheets.less'; - -@import './multiclass-choice/sheet.less'; - -@import './tag-team-dialog/initialization.less'; -@import './tag-team-dialog/sheet.less'; - +@import './actions/index.less'; +@import './attribution/index.less'; +@import './beastform/index.less'; +@import './character-creation/index.less'; +@import './character-reset/index.less'; +@import './compendiumBrowserPackDialog/index.less'; +@import './damage-reduction/index.less'; +@import './damage-selection/index.less'; +@import './death-move/index.less'; +@import './dice-roll/index.less'; +@import './downtime/index.less'; @import './group-roll-dialog/index.less'; - -@import './image-select/sheet.less'; - -@import './item-transfer/sheet.less'; - -@import './settings/change-currency-icon.less'; - -@import './risk-it-all/sheet.less'; - -@import './character-reset/sheet.less'; - -@import './compendiumBrowserPackDialog/sheet.less'; +@import './level-up/index.less'; +@import './resource-dice/index.less'; +@import './multiclass-choice/index.less'; +@import './tag-team-dialog/index.less'; +@import './image-select/index.less'; +@import './item-transfer/index.less'; +@import './settings/index.less'; +@import './risk-it-all/index.less'; \ No newline at end of file diff --git a/styles/less/dialog/item-transfer/index.less b/styles/less/dialog/item-transfer/index.less new file mode 100644 index 000000000..1c574e817 --- /dev/null +++ b/styles/less/dialog/item-transfer/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/dialog/multiclass-choice/index.less b/styles/less/dialog/multiclass-choice/index.less new file mode 100644 index 000000000..1c574e817 --- /dev/null +++ b/styles/less/dialog/multiclass-choice/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/dialog/resource-dice/index.less b/styles/less/dialog/resource-dice/index.less new file mode 100644 index 000000000..1c574e817 --- /dev/null +++ b/styles/less/dialog/resource-dice/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/dialog/risk-it-all/index.less b/styles/less/dialog/risk-it-all/index.less new file mode 100644 index 000000000..1c574e817 --- /dev/null +++ b/styles/less/dialog/risk-it-all/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/dialog/settings/index.less b/styles/less/dialog/settings/index.less new file mode 100644 index 000000000..235f3b9c8 --- /dev/null +++ b/styles/less/dialog/settings/index.less @@ -0,0 +1 @@ +@import './change-currency-icon.less'; \ No newline at end of file diff --git a/styles/less/dialog/tag-team-dialog/index.less b/styles/less/dialog/tag-team-dialog/index.less new file mode 100644 index 000000000..8bf568242 --- /dev/null +++ b/styles/less/dialog/tag-team-dialog/index.less @@ -0,0 +1,2 @@ +@import './sheet.less'; +@import './initialization.less'; \ No newline at end of file diff --git a/styles/less/dialog/tag-team-dialog/initialization.less b/styles/less/dialog/tag-team-dialog/initialization.less index d6f7ad294..e79ed65c1 100644 --- a/styles/less/dialog/tag-team-dialog/initialization.less +++ b/styles/less/dialog/tag-team-dialog/initialization.less @@ -88,9 +88,21 @@ grid-template-columns: 1fr 1fr; gap: 8px; - &.inactive { + .inactive { opacity: 0.4; } + + .initiator-cost-fields { + display: flex; + flex-direction: column; + align-items: flex-start; + + .initiator-cost-inputs { + display: grid; + grid-template-columns: auto 1fr; + align-items: center; + } + } } footer { diff --git a/styles/less/global/global.less b/styles/less/global/global.less index c0e7f3fcf..19a9e5193 100644 --- a/styles/less/global/global.less +++ b/styles/less/global/global.less @@ -12,6 +12,9 @@ } .daggerheart.dh-style { + /** Not an actual scrollbar width (it can't be configured on all browsers) but actually a compensation value for scrollbar gutter purposes */ + --scrollbar-width: 10px; + * { scrollbar-width: thin; scrollbar-color: light-dark(@dark-blue, @golden) transparent; diff --git a/styles/less/global/inventory-item.less b/styles/less/global/inventory-item.less index 3a5a93216..fc73ba958 100644 --- a/styles/less/global/inventory-item.less +++ b/styles/less/global/inventory-item.less @@ -43,16 +43,19 @@ } } + .item-main { + border-radius: 5px; + padding: 2px; + margin: -2px; + } + &:hover { .inventory-item-header .item-label .item-name .expanded-icon { margin-left: 10px; display: inline-block; } - &:has(.inventory-item-content.extensible) { - .inventory-item-header, - .inventory-item-content { - background: light-dark(@dark-blue-40, @golden-40); - } + .item-main { + background: light-dark(@dark-blue-40, @golden-40); } &:has(.inventory-item-content.extended) { .inventory-item-header .item-label .item-name .expanded-icon { @@ -60,19 +63,6 @@ } } } - - &:has(.inventory-item-content.extensible) { - .inventory-item-header { - border-radius: 5px 5px 0 0; - } - .inventory-item-content { - border-radius: 0 0 5px 5px; - } - } - - &:not(:has(.inventory-item-content.extensible)) .inventory-item-header { - border-radius: 5px; - } } .inventory-item-header, @@ -171,7 +161,7 @@ grid-template-rows: 1fr; padding-top: 4px; } - .invetory-description { + .inventory-description { overflow: hidden; h1 { diff --git a/styles/less/global/prose-mirror.less b/styles/less/global/prose-mirror.less index e4b1249f7..27048ddf1 100644 --- a/styles/less/global/prose-mirror.less +++ b/styles/less/global/prose-mirror.less @@ -1,8 +1,11 @@ @import '../utils/colors.less'; @import '../utils/fonts.less'; +@import '../utils/mixin.less'; .application.daggerheart { prose-mirror { + --menu-padding: 4px 0px; + --menu-height: calc(var(--menu-button-height) + 8px); height: 100% !important; width: 100%; @@ -10,6 +13,7 @@ background-color: transparent; } .editor-content { + .with-scroll-shadows(); h1 { font-size: var(--font-size-32); } @@ -40,7 +44,7 @@ ul { list-style: disc; } - } + } // Fixes centering and makes it not render over scrollbar &:hover button.toggle:enabled { display: flex; diff --git a/styles/less/hud/index.less b/styles/less/hud/index.less index 459f8fd74..f1f4602e9 100644 --- a/styles/less/hud/index.less +++ b/styles/less/hud/index.less @@ -1 +1 @@ -@import './token-hud/token-hud.less'; +@import './token-hud/index.less'; diff --git a/styles/less/hud/token-hud/index.less b/styles/less/hud/token-hud/index.less new file mode 100644 index 000000000..c86d09398 --- /dev/null +++ b/styles/less/hud/token-hud/index.less @@ -0,0 +1 @@ +@import './token-hud.less'; \ No newline at end of file diff --git a/styles/less/sheets-settings/adversary-settings/index.less b/styles/less/sheets-settings/adversary-settings/index.less new file mode 100644 index 000000000..5968577d3 --- /dev/null +++ b/styles/less/sheets-settings/adversary-settings/index.less @@ -0,0 +1,3 @@ +@import './sheet.less'; +@import './experiences.less'; +@import './features.less'; \ No newline at end of file diff --git a/styles/less/sheets-settings/character-settings/index.less b/styles/less/sheets-settings/character-settings/index.less new file mode 100644 index 000000000..1c574e817 --- /dev/null +++ b/styles/less/sheets-settings/character-settings/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/sheets-settings/environment-settings/index.less b/styles/less/sheets-settings/environment-settings/index.less new file mode 100644 index 000000000..1e6ee34da --- /dev/null +++ b/styles/less/sheets-settings/environment-settings/index.less @@ -0,0 +1,2 @@ +@import './adversaries.less'; +@import './features.less'; \ No newline at end of file diff --git a/styles/less/sheets-settings/index.less b/styles/less/sheets-settings/index.less index f575f848a..53a03868d 100644 --- a/styles/less/sheets-settings/index.less +++ b/styles/less/sheets-settings/index.less @@ -1,8 +1,4 @@ @import './header.less'; -@import './adversary-settings/sheet.less'; -@import './adversary-settings/experiences.less'; -@import './adversary-settings/features.less'; -@import './character-settings/sheet.less'; - -@import './environment-settings/features.less'; -@import './environment-settings/adversaries.less'; +@import './adversary-settings/index.less'; +@import './character-settings/index.less'; +@import './environment-settings/index.less'; diff --git a/styles/less/sheets/actions/actions.less b/styles/less/sheets/actions/actions.less index 485f8e91b..19912a985 100644 --- a/styles/less/sheets/actions/actions.less +++ b/styles/less/sheets/actions/actions.less @@ -153,4 +153,23 @@ align-items: center; gap: 4px; } + + .inner-tabs { + justify-content: left; + + .inner-tab { + line-height: 1.5; + border: 1px solid light-dark(@beige, @beige); + border-radius: 6px; + padding: 0 4px; + background: light-dark(@beige, @dark-blue); + color: light-dark(@dark-blue, @beige); + + a { + &.active { + + } + } + } + } } diff --git a/styles/less/sheets/actions/index.less b/styles/less/sheets/actions/index.less new file mode 100644 index 000000000..29ef86454 --- /dev/null +++ b/styles/less/sheets/actions/index.less @@ -0,0 +1 @@ +@import './actions.less'; \ No newline at end of file diff --git a/styles/less/sheets/activeEffects/index.less b/styles/less/sheets/activeEffects/index.less new file mode 100644 index 000000000..19f8a3a77 --- /dev/null +++ b/styles/less/sheets/activeEffects/index.less @@ -0,0 +1 @@ +@import './activeEffects.less'; \ No newline at end of file diff --git a/styles/less/sheets/actors/actor-sheet-shared.less b/styles/less/sheets/actors/actor-sheet-shared.less index b3eb0469b..5eb5b43c0 100644 --- a/styles/less/sheets/actors/actor-sheet-shared.less +++ b/styles/less/sheets/actors/actor-sheet-shared.less @@ -37,6 +37,10 @@ color: @color-text-subtle; } + .window-header > .attribution-header-label { + margin-right: var(--spacer-4); + } + .tab.inventory { .gold-section { display: grid; @@ -50,6 +54,37 @@ } } + .tab.notes.active { + padding: 0; + margin: 0; + margin-top: -10px; // will be removed once tab-navigation bottom margin is removed on all actor sheets + scrollbar-gutter: unset; + + // Add padding around top level level prosemirrors used for note tabs + > prose-mirror { + @right-padding: calc(16px - var(--scrollbar-width)); + .editor-content { + scrollbar-gutter: stable; + padding-right: @right-padding; + } + &.inactive { + button.toggle { + top: 16px; + } + .editor-content { + padding: 16px @right-padding 4px 16px; + } + } + &.active { + padding: 8px 0 4px 16px; + } + } + + .artist-attribution { + padding-left: 16px; + } + } + .search-section { display: flex; gap: 10px; diff --git a/styles/less/sheets/actors/adversary/header.less b/styles/less/sheets/actors/adversary/header.less index 1e5e4fa57..549a219df 100644 --- a/styles/less/sheets/actors/adversary/header.less +++ b/styles/less/sheets/actors/adversary/header.less @@ -3,16 +3,21 @@ .application.sheet.daggerheart.actor.dh-style.adversary { .adversary-header-sheet { - padding: 0 15px; padding-top: var(--header-height); width: 100%; + > *:not(line-div, .tab-navigation) { + padding-left: 15px; + padding-right: 15px; + } + .name-row { display: flex; gap: 5px; align-items: center; justify-content: space-between; - padding: 8px 0; + padding-top: var(--spacer-4); + padding-bottom: var(--spacer-4); flex: 1; h1 { @@ -34,8 +39,8 @@ .tags { display: flex; - gap: 10px; - padding-bottom: 8px; + gap: 8px; + padding-bottom: var(--spacer-12); .tag { display: flex; @@ -44,8 +49,7 @@ justify-content: center; align-items: center; padding: 3px 5px; - font-size: var(--font-size-12); - font: @font-body; + font: var(--font-size-12) @font-body; background: light-dark(@dark-15, @beige-15); border: 1px solid light-dark(@dark, @beige); @@ -64,8 +68,16 @@ .adversary-info { display: flex; flex-direction: column; - gap: 12px; - padding: 16px 0; + gap: var(--spacer-8); + padding-top: var(--spacer-12); + padding-bottom: var(--spacer-12); + } + + .tab-navigation { + margin-top: 0; + button[data-action="openSettings"] { + margin-right: 12px; + } } } } diff --git a/styles/less/sheets/actors/index.less b/styles/less/sheets/actors/index.less new file mode 100644 index 000000000..959bc0f59 --- /dev/null +++ b/styles/less/sheets/actors/index.less @@ -0,0 +1,7 @@ +@import './actor-sheet-shared.less'; +@import './adversary/index.less'; +@import './character/index.less'; +@import './companion/index.less'; +@import './environment/index.less'; +@import './npc/index.less'; +@import './party/index.less'; \ No newline at end of file diff --git a/styles/less/sheets/index.less b/styles/less/sheets/index.less index 4312f755a..451ae03af 100644 --- a/styles/less/sheets/index.less +++ b/styles/less/sheets/index.less @@ -1,22 +1,5 @@ -@import './actions/actions.less'; - -@import './actors/actor-sheet-shared.less'; - -@import './actors/adversary/index.less'; -@import './actors/character/index.less'; -@import './actors/companion/index.less'; -@import './actors/environment/index.less'; -@import './actors/npc/index.less'; -@import './actors/party/index.less'; - -@import './items/beastform.less'; -@import './items/class.less'; -@import './items/domain-card.less'; -@import './items/feature.less'; -@import './items/heritage.less'; -@import './items/item-sheet-shared.less'; - -@import './rollTables/sheet.less'; -@import './actions/actions.less'; - -@import './activeEffects/activeEffects.less'; +@import './activeEffects/index.less'; +@import './actions/index.less'; +@import './actors/index.less'; +@import './items/index.less'; +@import './rollTables/index.less'; \ No newline at end of file diff --git a/styles/less/sheets/items/index.less b/styles/less/sheets/items/index.less new file mode 100644 index 000000000..7c40a2e35 --- /dev/null +++ b/styles/less/sheets/items/index.less @@ -0,0 +1,6 @@ +@import './beastform.less'; +@import './class.less'; +@import './domain-card.less'; +@import './feature.less'; +@import './heritage.less'; +@import './item-sheet-shared.less'; \ No newline at end of file diff --git a/styles/less/sheets/rollTables/index.less b/styles/less/sheets/rollTables/index.less new file mode 100644 index 000000000..1c574e817 --- /dev/null +++ b/styles/less/sheets/rollTables/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/ui/chat/index.less b/styles/less/ui/chat/index.less new file mode 100644 index 000000000..9cadbd04f --- /dev/null +++ b/styles/less/ui/chat/index.less @@ -0,0 +1,10 @@ +@import './sheet.less'; +@import './ability-use.less'; +@import './action.less'; +@import './chat.less'; +@import './damage-summary.less'; +@import './deathmoves.less'; +@import './downtime.less'; +@import './effect-summary.less'; +@import './group-roll.less'; +@import './refresh-message.less'; \ No newline at end of file diff --git a/styles/less/ui/combat-sidebar/index.less b/styles/less/ui/combat-sidebar/index.less new file mode 100644 index 000000000..786815efd --- /dev/null +++ b/styles/less/ui/combat-sidebar/index.less @@ -0,0 +1,5 @@ +@import './combat-sidebar.less'; +@import './combatant-controls.less'; +@import './encounter-controls.less'; +@import './spotlight-control.less'; +@import './token-actions.less'; \ No newline at end of file diff --git a/styles/less/ui/countdown/countdown.less b/styles/less/ui/countdown/countdown.less index 66a6c88a7..96e01ffdc 100644 --- a/styles/less/ui/countdown/countdown.less +++ b/styles/less/ui/countdown/countdown.less @@ -18,11 +18,10 @@ border: 0; box-shadow: none; color: @color-text-primary; - width: 300px; + width: 18.75rem; pointer-events: all; align-self: flex-end; transition: 0.3s right ease-in-out; - display: flex; flex-direction: column; @@ -36,7 +35,7 @@ transition: opacity var(--ui-fade-duration); } - :not(.performance-low, .noblur) { + &:not(.performance-low, .noblur) { backdrop-filter: blur(5px); } @@ -49,8 +48,7 @@ } &.icon-only { - width: 180px; - min-width: 180px; + width: 12rem; } .countdowns-header, @@ -66,12 +64,31 @@ padding: 0 0.5rem; overflow: hidden; font-size: var(--font-size-13); + .window-title { font-family: @font-body; - flex: 1; } - .header-control + .header-control { - margin-left: 8px; + + + .header-type-toggles { + flex: 1; + display: flex; + align-items: center; + gap: 4px; + + .header-type { + flex: 1; + border: 1px solid @beige; + border-radius: 12px; + padding: 2px 4px; + text-align: center; + background-color: @dark-blue; + color: @beige; + + &.inactive { + opacity: 0.4; + } + } } } @@ -83,9 +100,23 @@ overflow: auto; max-height: 312px; + .countdown-category-container { + display: flex; + flex-direction: column; + gap: 8px; + + &.hidden { + display: none; + } + } + .countdown-container { display: flex; width: 100%; + border-radius: 6px; + background: linear-gradient(-45deg, transparent 30%, light-dark(@dark-blue-40, @golden-40) 35%, transparent 40%); + background-size: 300%; + background-position-x: 100%; &.icon-only { gap: 8px; @@ -106,10 +137,11 @@ display: flex; align-items: center; gap: 16px; + border-radius: 6px; img { - width: 44px; - height: 44px; + width: 2.75rem; + height: 2.75rem; border-radius: 6px; } @@ -127,7 +159,7 @@ .countdown-tool-controls { display: flex; align-items: center; - gap: 16px; + gap: var(--spacer-12); } .progress-tag { diff --git a/styles/less/ui/countdown/index.less b/styles/less/ui/countdown/index.less new file mode 100644 index 000000000..45ecda260 --- /dev/null +++ b/styles/less/ui/countdown/index.less @@ -0,0 +1,3 @@ +@import './sheet.less'; +@import './countdown-edit.less'; +@import './countdown.less'; \ No newline at end of file diff --git a/styles/less/ui/effects-display/index.less b/styles/less/ui/effects-display/index.less new file mode 100644 index 000000000..1c574e817 --- /dev/null +++ b/styles/less/ui/effects-display/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/ui/game-pause/game-pause.less b/styles/less/ui/game-pause/game-pause.less new file mode 100644 index 000000000..daf9a1e43 --- /dev/null +++ b/styles/less/ui/game-pause/game-pause.less @@ -0,0 +1,34 @@ +@import '../../utils/mixin.less'; + +#pause.dh-style { + animation: none; + + img { + width: 125px; + height: 125px; + } + + figcaption { + --base-shadow: drop-shadow(2px 2px 2px black); + position: absolute; + margin-top: 24px; + animation: pause-pulse 3s ease-in-out infinite; + font-size: var(--font-size-30); + letter-spacing: 0.18em; + filter: var(--base-shadow); + } + + @keyframes pause-pulse { + 0% { + filter: var(--base-shadow) drop-shadow(0 0 5px @secondary-blue); + } + + 50% { + filter: var(--base-shadow) drop-shadow(0 0 5px @golden-secondary); + } + + 100% { + filter: var(--base-shadow) drop-shadow(0 0 5px @secondary-blue); + } + } +} \ No newline at end of file diff --git a/styles/less/ui/game-pause/index.less b/styles/less/ui/game-pause/index.less new file mode 100644 index 000000000..ad28855cd --- /dev/null +++ b/styles/less/ui/game-pause/index.less @@ -0,0 +1 @@ +@import './game-pause.less'; \ No newline at end of file diff --git a/styles/less/ui/index.less b/styles/less/ui/index.less index 31ea89553..af1614e4b 100644 --- a/styles/less/ui/index.less +++ b/styles/less/ui/index.less @@ -1,40 +1,12 @@ -@import './chat/ability-use.less'; -@import './chat/action.less'; -@import './chat/chat.less'; -@import './chat/damage-summary.less'; -@import './chat/downtime.less'; -@import './chat/effect-summary.less'; -@import './chat/group-roll.less'; -@import './chat/refresh-message.less'; -@import './chat/deathmoves.less'; -@import './chat/sheet.less'; - -@import './combat-sidebar/combat-sidebar.less'; -@import './combat-sidebar/combatant-controls.less'; -@import './combat-sidebar/encounter-controls.less'; -@import './combat-sidebar/spotlight-control.less'; -@import './combat-sidebar/token-actions.less'; -@import './item-browser/item-browser.less'; - -@import './countdown/countdown.less'; -@import './countdown/countdown-edit.less'; -@import './countdown/sheet.less'; - -@import './ownership-selection/ownership-selection.less'; - -@import './resources/resources.less'; - -@import './settings/settings.less'; -@import './settings/homebrew-settings/domains.less'; -@import './settings/homebrew-settings/types.less'; -@import './settings/homebrew-settings/resources.less'; -@import './settings/appearance-settings/diceSoNice.less'; - -@import './sidebar/tabs.less'; -@import './sidebar/daggerheartMenu.less'; - -@import './scene-config/scene-config.less'; - -@import './effects-display/sheet.less'; - -@import './scene-navigation/scene-navigation.less'; +@import './chat/index.less'; +@import './combat-sidebar/index.less'; +@import './countdown/index.less'; +@import './effects-display/index.less'; +@import './game-pause/index.less'; +@import './item-browser/index.less'; +@import './ownership-selection/index.less'; +@import './resources/index.less'; +@import './scene-config/index.less'; +@import './scene-navigation/index.less'; +@import './settings/index.less'; +@import './sidebar/index.less'; \ No newline at end of file diff --git a/styles/less/ui/item-browser/index.less b/styles/less/ui/item-browser/index.less new file mode 100644 index 000000000..842f716ba --- /dev/null +++ b/styles/less/ui/item-browser/index.less @@ -0,0 +1 @@ +@import './item-browser.less'; \ No newline at end of file diff --git a/styles/less/ui/ownership-selection/index.less b/styles/less/ui/ownership-selection/index.less new file mode 100644 index 000000000..9646670a6 --- /dev/null +++ b/styles/less/ui/ownership-selection/index.less @@ -0,0 +1 @@ +@import './ownership-selection.less'; \ No newline at end of file diff --git a/styles/less/ui/resources/index.less b/styles/less/ui/resources/index.less new file mode 100644 index 000000000..a7d087853 --- /dev/null +++ b/styles/less/ui/resources/index.less @@ -0,0 +1 @@ +@import './resources.less'; \ No newline at end of file diff --git a/styles/less/ui/scene-config/index.less b/styles/less/ui/scene-config/index.less new file mode 100644 index 000000000..4e3af3636 --- /dev/null +++ b/styles/less/ui/scene-config/index.less @@ -0,0 +1 @@ +@import './scene-config.less'; \ No newline at end of file diff --git a/styles/less/ui/scene-navigation/index.less b/styles/less/ui/scene-navigation/index.less new file mode 100644 index 000000000..c0765ae70 --- /dev/null +++ b/styles/less/ui/scene-navigation/index.less @@ -0,0 +1 @@ +@import './scene-navigation.less'; \ No newline at end of file diff --git a/styles/less/ui/settings/appearance-settings/index.less b/styles/less/ui/settings/appearance-settings/index.less new file mode 100644 index 000000000..8b1c109a2 --- /dev/null +++ b/styles/less/ui/settings/appearance-settings/index.less @@ -0,0 +1 @@ +@import './diceSoNice.less'; \ No newline at end of file diff --git a/styles/less/ui/settings/homebrew-settings/index.less b/styles/less/ui/settings/homebrew-settings/index.less new file mode 100644 index 000000000..f0a8bfc14 --- /dev/null +++ b/styles/less/ui/settings/homebrew-settings/index.less @@ -0,0 +1,3 @@ +@import './domains.less'; +@import './resources.less'; +@import './types.less'; \ No newline at end of file diff --git a/styles/less/ui/settings/index.less b/styles/less/ui/settings/index.less new file mode 100644 index 000000000..4e1aa7983 --- /dev/null +++ b/styles/less/ui/settings/index.less @@ -0,0 +1,3 @@ +@import './settings.less'; +@import './appearance-settings/index.less'; +@import './homebrew-settings/index.less'; \ No newline at end of file diff --git a/styles/less/ui/settings/settings.less b/styles/less/ui/settings/settings.less index 35c484805..f2ab99e7e 100644 --- a/styles/less/ui/settings/settings.less +++ b/styles/less/ui/settings/settings.less @@ -201,6 +201,23 @@ } } + .currency-rows { + display: grid; + grid-template-columns: auto 1fr 1fr auto; + gap: 4px; + align-items: center; + width: 100%; + + .currency-header-label { + text-align: center; + font-size: var(--font-size-14); + } + + input[type='checkbox'] { + justify-self: center; + } + } + .settings-hint { width: 100%; display: flex; diff --git a/styles/less/ui/sidebar/index.less b/styles/less/ui/sidebar/index.less new file mode 100644 index 000000000..b4961b419 --- /dev/null +++ b/styles/less/ui/sidebar/index.less @@ -0,0 +1,2 @@ +@import './daggerheartMenu.less'; +@import './tabs.less'; \ No newline at end of file diff --git a/styles/less/utils/fonts.less b/styles/less/utils/fonts.less index 201ea356b..72ee1a853 100755 --- a/styles/less/utils/fonts.less +++ b/styles/less/utils/fonts.less @@ -1,25 +1,22 @@ -@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=Cinzel+Decorative:wght@700&family=Montserrat:wght@400;600&display=swap'); -@import './mixin.less'; - -:root { - --dh-font-title: 'Cinzel Decorative'; - --dh-font-subtitle: 'Cinzel'; - --dh-font-body: 'Montserrat'; - - /* Include missing font sizes */ - --font-size-8: 0.5rem; - --font-size-9: 0.5625rem; - --font-size-22: 1.375rem; -} - -@font-title: ~"var(--dh-font-title, 'Cinzel Decorative'), serif"; -@font-subtitle: ~"var(--dh-font-subtitle, 'Cinzel'), serif"; -@font-body: ~"var(--dh-font-body, 'Montserrat'), sans-serif"; - -.dh-style { - .dh-typography(); -} - -.dh-style fieldset { - .dh-typography(); -} +@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=Cinzel+Decorative:wght@700&family=Montserrat:wght@400;600&display=swap'); +@import './mixin.less'; + +:root { + --dh-font-title: 'Cinzel Decorative'; + --dh-font-subtitle: 'Cinzel'; + --dh-font-body: 'Montserrat'; + + /* Include missing font sizes */ + --font-size-8: 0.5rem; + --font-size-9: 0.5625rem; + --font-size-22: 1.375rem; + --font-size-30: 1.875rem; +} + +@font-title: ~"var(--dh-font-title, 'Cinzel Decorative'), serif"; +@font-subtitle: ~"var(--dh-font-subtitle, 'Cinzel'), serif"; +@font-body: ~"var(--dh-font-body, 'Montserrat'), sans-serif"; + +.dh-style { + .dh-typography(); +} diff --git a/styles/less/utils/index.less b/styles/less/utils/index.less new file mode 100644 index 000000000..37b096d31 --- /dev/null +++ b/styles/less/utils/index.less @@ -0,0 +1,4 @@ +@import './colors.less'; +@import './fonts.less'; +@import './mixin.less'; +@import './spacing.less'; \ No newline at end of file diff --git a/styles/less/utils/mixin.less b/styles/less/utils/mixin.less index 18b1f9a61..e2ef85ef1 100644 --- a/styles/less/utils/mixin.less +++ b/styles/less/utils/mixin.less @@ -49,16 +49,15 @@ * Apply default typography styles. */ .dh-typography() { + --font-body: @font-body; + font-family: @font-body; + h1 { --dh-input-color-text: @color-text-emphatic; font-family: @font-title; margin: 0; border: none; font-weight: normal; - input[type='text'], - .input[contenteditable] { - font-family: @font-title; - } } h2, @@ -86,22 +85,17 @@ font-weight: normal; } - p, - span, - textarea, - label, - select, + // todo: determine if we can remove these or replace with font-family: inherit multi-select .tags .tag, table, - fieldset legend, - input[type='text'], - input[type='number'], - input[type='search'], - .tagify__dropdown, - li { + .tagify__dropdown { font-family: @font-body; } + textarea { + font-family: inherit; + } + button span { font-weight: bold; } diff --git a/styles/less/ux/autocomplete/index.less b/styles/less/ux/autocomplete/index.less new file mode 100644 index 000000000..910071460 --- /dev/null +++ b/styles/less/ux/autocomplete/index.less @@ -0,0 +1 @@ +@import './autocomplete.less'; \ No newline at end of file diff --git a/styles/less/ux/index.less b/styles/less/ux/index.less index a73f2d1cd..b6c9a2e7b 100644 --- a/styles/less/ux/index.less +++ b/styles/less/ux/index.less @@ -1,10 +1,2 @@ -@import './tooltip/sheet.less'; -@import './tooltip/tooltip.less'; -@import './tooltip/armorManagement.less'; -@import './tooltip/battlepoints.less'; -@import './tooltip/bordered-tooltip.less'; -@import './tooltip/domain-cards.less'; - -@import './autocomplete/autocomplete.less'; - -@import './tooltip/resource-management.less'; +@import './autocomplete/index.less'; +@import './tooltip/index.less'; \ No newline at end of file diff --git a/styles/less/ux/tooltip/index.less b/styles/less/ux/tooltip/index.less new file mode 100644 index 000000000..eeec93541 --- /dev/null +++ b/styles/less/ux/tooltip/index.less @@ -0,0 +1,7 @@ +@import './sheet.less'; +@import './armorManagement.less'; +@import './battlepoints.less'; +@import './bordered-tooltip.less'; +@import './domain-cards.less'; +@import './resource-management.less'; +@import './tooltip.less'; \ No newline at end of file diff --git a/system.json b/system.json index 588ceafe8..0ecc2e75d 100644 --- a/system.json +++ b/system.json @@ -2,15 +2,15 @@ "id": "daggerheart", "title": "Daggerheart", "description": "An unofficial implementation of the Daggerheart system", - "version": "2.3.1", + "version": "2.4.2", "compatibility": { - "minimum": "14.361", - "verified": "14.363", + "minimum": "14.364", + "verified": "14.364", "maximum": "14" }, "url": "https://github.com/Foundryborne/daggerheart", "manifest": "https://raw.githubusercontent.com/Foundryborne/daggerheart/v14/system.json", - "download": "https://github.com/Foundryborne/daggerheart/releases/download/2.3.1/system.zip", + "download": "https://github.com/Foundryborne/daggerheart/releases/download/2.4.2/system.zip", "authors": [ { "name": "WBHarry" diff --git a/templates/actionTypes/damage.hbs b/templates/actionTypes/damage.hbs index 033008402..837155640 100644 --- a/templates/actionTypes/damage.hbs +++ b/templates/actionTypes/damage.hbs @@ -6,20 +6,20 @@ {{else}} {{localize "DAGGERHEART.GENERAL.damage"}} {{/if}} - {{#unless (eq path 'system.attack.')}}{{/unless}} + {{#unless (eq path 'system.attack.')}}{{/unless}}
{{#if @root.hasBaseDamage}} - {{formField @root.fields.damage.fields.includeBase value=@root.source.damage.includeBase name="damage.includeBase" classes="checkbox" localize=true }} + {{formField @root.fields.damage.fields.includeBase value=@root.source.damage.includeBase name=(concat basePath ".includeBase") classes="checkbox" localize=true }} {{/if}} {{#unless (eq @root.source.type 'healing')}} - {{formField baseFields.direct value=source.direct name=(concat path "damage.direct") localize=true classes="checkbox"}} + {{formField baseFields.direct value=source.direct name=(concat path basePath ".direct") localize=true classes="checkbox"}} {{/unless}} {{#if (and @root.isNPC (not (eq path 'system.attack.')))}} - {{formField baseFields.groupAttack value=source.groupAttack name=(concat path "damage.groupAttack") localize=true classes="select"}} + {{formField baseFields.groupAttack value=source.groupAttack name=(concat path basePath ".groupAttack") localize=true classes="select"}} {{/if}}
- + {{!-- Handlebars uses Symbol.Iterator to produce index|key. This isn't compatible with our parts object, so we instead use applyTo, which is the same value --}} {{#each source.parts as |dmg key|}}
@@ -27,44 +27,44 @@ {{localize (concat "DAGGERHEART.CONFIG.HealingType." dmg.applyTo ".name")}} {{#unless (or dmg.base ../path)}} - + {{/unless}} {{#if (and (not @root.isNPC) @root.hasRoll (not dmg.base))}} - {{formField ../fields.resultBased value=dmg.resultBased name=(concat "damage.parts." dmg.applyTo ".resultBased") localize=true classes="checkbox"}} + {{formField ../fields.resultBased value=dmg.resultBased name=(concat ../basePath ".parts." dmg.applyTo ".resultBased") localize=true classes="checkbox"}} {{/if}} {{#if (and (not @root.isNPC) @root.hasRoll (not dmg.base) dmg.resultBased)}}
{{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.hope")}} - {{> formula fields=../fields.value.fields type=../fields.type dmg=dmg source=dmg.value target="value" key=dmg.applyTo path=../path}} + {{> formula fields=../fields.value.fields type=../fields.type dmg=dmg source=dmg.value target="value" key=dmg.applyTo path=../path basePath=../basePath}}
{{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.fear")}} - {{> formula fields=../fields.valueAlt.fields type=../fields.type dmg=dmg source=dmg.valueAlt target="valueAlt" key=dmg.applyTo path=../path}} + {{> formula fields=../fields.valueAlt.fields type=../fields.type dmg=dmg source=dmg.valueAlt target="valueAlt" key=dmg.applyTo path=../path basePath=../basePath}}
{{else}} - {{> formula fields=../fields.value.fields type=../fields.type dmg=dmg source=dmg.value target="value" key=dmg.applyTo path=../path}} + {{> formula fields=../fields.value.fields type=../fields.type dmg=dmg source=dmg.value target="value" key=dmg.applyTo path=../path basePath=../basePath}} {{/if}} {{#if (and (eq dmg.applyTo 'hitPoints') (ne @root.source.type 'healing'))}} - {{formField ../fields.type value=dmg.type name=(concat ../path "damage.parts." dmg.applyTo ".type") localize=true}} + {{formField ../fields.type value=dmg.type name=(concat ../path ../basePath ".parts." dmg.applyTo ".type") localize=true}} {{/if}} {{#if ../horde}}
{{localize "DAGGERHEART.ACTORS.Adversary.hordeDamage"}}
- - {{formField ../fields.valueAlt.fields.flatMultiplier value=dmg.valueAlt.flatMultiplier name=(concat ../path "damage.parts." dmg.applyTo ".valueAlt.flatMultiplier") label="DAGGERHEART.ACTIONS.Settings.multiplier" classes="inline-child" localize=true }} - {{formField ../fields.valueAlt.fields.dice value=dmg.valueAlt.dice name=(concat ../path "damage.parts." dmg.applyTo ".valueAlt.dice") classes="inline-child" localize=true}} - {{formField ../fields.valueAlt.fields.bonus value=dmg.valueAlt.bonus name=(concat ../path "damage.parts." dmg.applyTo ".valueAlt.bonus") localize=true classes="inline-child"}} + + {{formField ../fields.valueAlt.fields.flatMultiplier value=dmg.valueAlt.flatMultiplier name=(concat ../path ../basePath ".parts." dmg.applyTo ".valueAlt.flatMultiplier") label="DAGGERHEART.ACTIONS.Settings.multiplier" classes="inline-child" localize=true }} + {{formField ../fields.valueAlt.fields.dice value=dmg.valueAlt.dice name=(concat ../path ../basePath ".parts." dmg.applyTo ".valueAlt.dice") classes="inline-child" localize=true}} + {{formField ../fields.valueAlt.fields.bonus value=dmg.valueAlt.bonus name=(concat ../path ../basePath ".parts." dmg.applyTo ".valueAlt.bonus") localize=true classes="inline-child"}}
{{/if}} - +
{{/each}} @@ -72,21 +72,21 @@ {{#*inline "formula"}} {{#unless dmg.base}} - {{formField fields.custom.fields.enabled value=source.custom.enabled name=(concat path "damage.parts." key "." target ".custom.enabled") classes="checkbox" localize=true}} + {{formField fields.custom.fields.enabled value=source.custom.enabled name=(concat path basePath ".parts." key "." target ".custom.enabled") classes="checkbox" localize=true}} {{/unless}} {{#if source.custom.enabled}} - {{formField fields.custom.fields.formula value=source.custom.formula name=(concat path "damage.parts." key "." target ".custom.formula") localize=true}} + {{formField fields.custom.fields.formula value=source.custom.formula name=(concat path basePath ".parts." key "." target ".custom.formula") localize=true}} {{else}}
{{#unless @root.isNPC}} - {{formField fields.multiplier value=source.multiplier name=(concat path "damage.parts." key "." target ".multiplier") localize=true}} + {{formField fields.multiplier value=source.multiplier name=(concat path basePath ".parts." key "." target ".multiplier") localize=true}} {{/unless}} - {{#if (eq source.multiplier 'flat')}}{{formField fields.flatMultiplier value=source.flatMultiplier name=(concat path "damage.parts." key "." target ".flatMultiplier") localize=true }}{{/if}} - {{formField fields.dice value=source.dice name=(concat path "damage.parts." key "." target ".dice") localize=true}} - {{formField fields.bonus value=source.bonus name=(concat path "damage.parts." key "." target ".bonus") localize=true}} + {{#if (eq source.multiplier 'flat')}}{{formField fields.flatMultiplier value=source.flatMultiplier name=(concat path basePath ".parts." key "." target ".flatMultiplier") localize=true }}{{/if}} + {{formField fields.dice value=source.dice name=(concat path basePath ".parts." key "." target ".dice") localize=true}} + {{formField fields.bonus value=source.bonus name=(concat path basePath ".parts." key "." target ".bonus") localize=true}}
{{/if}} {{#if @root.isNPC}} - + {{/if}} {{/inline}} \ No newline at end of file diff --git a/templates/dialogs/tagTeamDialog/initialization.hbs b/templates/dialogs/tagTeamDialog/initialization.hbs index 0b92e68e2..40491e1b3 100644 --- a/templates/dialogs/tagTeamDialog/initialization.hbs +++ b/templates/dialogs/tagTeamDialog/initialization.hbs @@ -26,11 +26,16 @@ -
+
-
- -
+
+ + +
diff --git a/templates/hud/tokenHUD.hbs b/templates/hud/tokenHUD.hbs index cf43e15d3..91c82f136 100644 --- a/templates/hud/tokenHUD.hbs +++ b/templates/hud/tokenHUD.hbs @@ -103,7 +103,7 @@ {{/if}} -
+
{{#each movementActions as |action|}} - + {{#if action.img}} {{ action.label }} diff --git a/templates/settings/homebrew-settings/settings.hbs b/templates/settings/homebrew-settings/settings.hbs index 4b6e7d85f..1fd449a94 100644 --- a/templates/settings/homebrew-settings/settings.hbs +++ b/templates/settings/homebrew-settings/settings.hbs @@ -42,29 +42,35 @@
- {{localize "DAGGERHEART.SETTINGS.Homebrew.currency.title"}} + {{localize "DAGGERHEART.SETTINGS.Homebrew.currency.title"}}
{{formGroup settingFields.schema.fields.currency.fields.title value=settingFields._source.currency.title localize=true}}
-
+
+ + {{localize "DAGGERHEART.SETTINGS.Homebrew.currency.denomination"}} + {{localize "DAGGERHEART.SETTINGS.Homebrew.currency.initialAmount"}} + + - {{formGroup settingFields.schema.fields.currency.fields.coins.fields.label value=settingFields._source.currency.coins.label localize=true}} + {{formInput settingFields.schema.fields.currency.fields.coins.fields.label value=settingFields._source.currency.coins.label name="currency.coins.label"}} + -
-
+ - {{formGroup settingFields.schema.fields.currency.fields.handfuls.fields.label value=settingFields._source.currency.handfuls.label localize=true}} + {{formInput settingFields.schema.fields.currency.fields.handfuls.fields.label value=settingFields._source.currency.handfuls.label name="currency.handfuls.label"}} + -
-
+ - {{formGroup settingFields.schema.fields.currency.fields.bags.fields.label value=settingFields._source.currency.bags.label localize=true}} + {{formInput settingFields.schema.fields.currency.fields.bags.fields.label value=settingFields._source.currency.bags.label name="currency.bags.label"}} + -
-
+ - {{formGroup settingFields.schema.fields.currency.fields.chests.fields.label value=settingFields._source.currency.chests.label localize=true}} + {{formInput settingFields.schema.fields.currency.fields.chests.fields.label value=settingFields._source.currency.chests.label name="currency.chests.label"}} +
diff --git a/templates/sheets-settings/action-settings/configuration.hbs b/templates/sheets-settings/action-settings/configuration.hbs index 8df528c9d..9fb54cc8b 100644 --- a/templates/sheets-settings/action-settings/configuration.hbs +++ b/templates/sheets-settings/action-settings/configuration.hbs @@ -3,8 +3,8 @@ data-group="primary" data-tab="config" > + {{#if fields.roll}}{{> 'systems/daggerheart/templates/actionTypes/roll.hbs' fields=fields.roll.fields source=source.roll}}{{/if}} + {{#if fields.save}}{{> 'systems/daggerheart/templates/actionTypes/save.hbs' fields=fields.save.fields source=source.save}}{{/if}} {{> 'systems/daggerheart/templates/actionTypes/uses.hbs' fields=fields.uses.fields source=source.uses}} {{> 'systems/daggerheart/templates/actionTypes/cost.hbs' fields=fields.cost.element.fields source=source.cost costOptions=costOptions}} - {{> 'systems/daggerheart/templates/actionTypes/range-target.hbs' fields=(object range=fields.range target=fields.target.fields) source=(object target=source.target range=source.range)}} - {{> 'systems/daggerheart/templates/actionTypes/areas.hbs' fields=fields.areas.element.fields source=source.areas}} \ No newline at end of file diff --git a/templates/sheets-settings/action-settings/effect.hbs b/templates/sheets-settings/action-settings/effect.hbs deleted file mode 100644 index 567cb81c4..000000000 --- a/templates/sheets-settings/action-settings/effect.hbs +++ /dev/null @@ -1,15 +0,0 @@ -
- {{#if fields.roll}}{{> 'systems/daggerheart/templates/actionTypes/roll.hbs' fields=fields.roll.fields source=source.roll}}{{/if}} - {{#if fields.save}}{{> 'systems/daggerheart/templates/actionTypes/save.hbs' fields=fields.save.fields source=source.save}}{{/if}} - {{#if fields.damage}}{{> 'systems/daggerheart/templates/actionTypes/damage.hbs' fields=fields.damage.fields.parts.element.fields source=source.damage baseFields=fields.damage.fields }}{{/if}} - {{#if fields.macro}}{{> 'systems/daggerheart/templates/actionTypes/macro.hbs' fields=fields.macro source=source.macro}}{{/if}} - {{#if fields.effects}}{{> 'systems/daggerheart/templates/actionTypes/effect.hbs' fields=fields.effects.element.fields source=source.effects}}{{/if}} - {{#if fields.beastform}}{{> 'systems/daggerheart/templates/actionTypes/beastform.hbs' fields=fields.beastform.fields source=source.beastform}}{{/if}} - {{#if fields.summon}}{{> 'systems/daggerheart/templates/actionTypes/summon.hbs' fields=fields.summon.element.fields source=source.summon}}{{/if}} - {{#if fields.countdown}}{{> 'systems/daggerheart/templates/actionTypes/countdown.hbs' fields=fields.countdown.element.fields source=source.countdown}}{{/if}} - {{#if fields.transform}}{{> 'systems/daggerheart/templates/actionTypes/transform.hbs' fields=fields.transform.fields source=source.transform}}{{/if}} -
\ No newline at end of file diff --git a/templates/sheets-settings/action-settings/outcomes.hbs b/templates/sheets-settings/action-settings/outcomes.hbs new file mode 100644 index 000000000..fb15bd06d --- /dev/null +++ b/templates/sheets-settings/action-settings/outcomes.hbs @@ -0,0 +1,44 @@ +
+ + + {{#each outcomeTabs as |tab|}} +
+ {{#if tab.fields.damage}} + {{> 'systems/daggerheart/templates/actionTypes/damage.hbs' + outcome=tab.id + fields=tab.fields.damage.fields.parts.element.fields + source=tab.source.damage + baseFields=tab.fields.damage.fields + isDefaultDamage="true" + basePath=(tab.getBasePath "damage") + }} + {{/if}} + +
+ {{/each}} + + {{#if fields.macro}}{{> 'systems/daggerheart/templates/actionTypes/macro.hbs' fields=fields.macro source=source.macro}}{{/if}} + {{#if fields.effects}}{{> 'systems/daggerheart/templates/actionTypes/effect.hbs' fields=fields.effects.element.fields source=source.effects}}{{/if}} + {{#if fields.beastform}}{{> 'systems/daggerheart/templates/actionTypes/beastform.hbs' fields=fields.beastform.fields source=source.beastform}}{{/if}} + {{#if fields.summon}}{{> 'systems/daggerheart/templates/actionTypes/summon.hbs' fields=fields.summon.element.fields source=source.summon}}{{/if}} + {{#if fields.countdown}}{{> 'systems/daggerheart/templates/actionTypes/countdown.hbs' fields=fields.countdown.element.fields source=source.countdown}}{{/if}} + {{#if fields.transform}}{{> 'systems/daggerheart/templates/actionTypes/transform.hbs' fields=fields.transform.fields source=source.transform}}{{/if}} +
\ No newline at end of file diff --git a/templates/sheets-settings/action-settings/range.hbs b/templates/sheets-settings/action-settings/range.hbs new file mode 100644 index 000000000..273e89c9b --- /dev/null +++ b/templates/sheets-settings/action-settings/range.hbs @@ -0,0 +1,8 @@ +
+ {{> 'systems/daggerheart/templates/actionTypes/range-target.hbs' fields=(object range=fields.range target=fields.target.fields) source=(object target=source.target range=source.range)}} + {{> 'systems/daggerheart/templates/actionTypes/areas.hbs' fields=fields.areas.element.fields source=source.areas}} +
\ No newline at end of file diff --git a/templates/sheets-settings/adversary-settings/features.hbs b/templates/sheets-settings/adversary-settings/features.hbs index 2f2f5f475..1cab8cfe9 100644 --- a/templates/sheets-settings/adversary-settings/features.hbs +++ b/templates/sheets-settings/adversary-settings/features.hbs @@ -3,27 +3,31 @@ data-tab='{{tabs.features.id}}' data-group='{{tabs.features.group}}' > - -
- {{localize tabs.features.label}} -
    - {{#each @root.features as |feature|}} -
  • - -
    - {{feature.name}} -
    -
    - - -
    -
  • - {{/each}} -
-
- {{localize "DAGGERHEART.GENERAL.dropFeaturesHere"}} -
-
+ {{#each featureGroups as |group|}} +
+ + {{group.label}} + + + + +
    + {{#each group.features as |feature|}} + {{> 'daggerheart.inventory-item' + item=feature + type='feature' + actorType=@root.document.type + hideTags=true + hideContextMenu=true + hideResources=true + showActions=false + hideTooltip=true + }} + {{/each}} +
+
+ {{/each}} +
+ {{localize "DAGGERHEART.GENERAL.dropFeaturesHere"}} +
\ No newline at end of file diff --git a/templates/sheets-settings/environment-settings/features.hbs b/templates/sheets-settings/environment-settings/features.hbs index 579fe74e0..1cab8cfe9 100644 --- a/templates/sheets-settings/environment-settings/features.hbs +++ b/templates/sheets-settings/environment-settings/features.hbs @@ -3,24 +3,31 @@ data-tab='{{tabs.features.id}}' data-group='{{tabs.features.group}}' > - -
- {{localize tabs.features.label}} -
    - {{#each @root.features as |feature|}} -
  • - -
    - {{feature.name}} -
    -
    - - -
    -
  • - {{/each}} -
-
+ {{#each featureGroups as |group|}} +
+ + {{group.label}} + + + + +
    + {{#each group.features as |feature|}} + {{> 'daggerheart.inventory-item' + item=feature + type='feature' + actorType=@root.document.type + hideTags=true + hideContextMenu=true + hideResources=true + showActions=false + hideTooltip=true + }} + {{/each}} +
+
+ {{/each}} +
+ {{localize "DAGGERHEART.GENERAL.dropFeaturesHere"}} +
\ No newline at end of file diff --git a/templates/sheets-settings/npc-settings/features.hbs b/templates/sheets-settings/npc-settings/features.hbs index 2f2f5f475..1cab8cfe9 100644 --- a/templates/sheets-settings/npc-settings/features.hbs +++ b/templates/sheets-settings/npc-settings/features.hbs @@ -3,27 +3,31 @@ data-tab='{{tabs.features.id}}' data-group='{{tabs.features.group}}' > - -
- {{localize tabs.features.label}} -
    - {{#each @root.features as |feature|}} -
  • - -
    - {{feature.name}} -
    -
    - - -
    -
  • - {{/each}} -
-
- {{localize "DAGGERHEART.GENERAL.dropFeaturesHere"}} -
-
+ {{#each featureGroups as |group|}} +
+ + {{group.label}} + + + + +
    + {{#each group.features as |feature|}} + {{> 'daggerheart.inventory-item' + item=feature + type='feature' + actorType=@root.document.type + hideTags=true + hideContextMenu=true + hideResources=true + showActions=false + hideTooltip=true + }} + {{/each}} +
+
+ {{/each}} +
+ {{localize "DAGGERHEART.GENERAL.dropFeaturesHere"}} +
\ No newline at end of file diff --git a/templates/sheets/actors/adversary/notes.hbs b/templates/sheets/actors/adversary/notes.hbs index a5c3f706d..d329d3180 100644 --- a/templates/sheets/actors/adversary/notes.hbs +++ b/templates/sheets/actors/adversary/notes.hbs @@ -1,13 +1,9 @@
-
- {{localize tabs.notes.label}} - {{formInput notes.field value=notes.value enriched=notes.enriched toggled=true}} -
- + {{formInput notes.field value=notes.value enriched=notes.enriched toggled=true}} {{#if (and showAttribution document.system.attribution.artist)}} {{/if}} diff --git a/templates/sheets/actors/environment/notes.hbs b/templates/sheets/actors/environment/notes.hbs index 4f6b131e2..1acf0e930 100644 --- a/templates/sheets/actors/environment/notes.hbs +++ b/templates/sheets/actors/environment/notes.hbs @@ -3,10 +3,7 @@ data-tab='{{tabs.notes.id}}' data-group='{{tabs.notes.group}}' > -
- {{localize tabs.notes.label}} - {{formInput notes.field value=notes.value enriched=notes.value toggled=true}} -
+ {{formInput notes.field value=notes.value enriched=notes.value toggled=true}} {{#if (and showAttribution document.system.attribution.artist)}} diff --git a/templates/sheets/actors/npc/notes.hbs b/templates/sheets/actors/npc/notes.hbs index bc9ac3cf1..7dd5432d6 100644 --- a/templates/sheets/actors/npc/notes.hbs +++ b/templates/sheets/actors/npc/notes.hbs @@ -1,7 +1,7 @@
{{formInput notes.field value=notes.value enriched=notes.enriched toggled=true}} diff --git a/templates/sheets/actors/party/notes.hbs b/templates/sheets/actors/party/notes.hbs index 663a484a8..af21b29fb 100644 --- a/templates/sheets/actors/party/notes.hbs +++ b/templates/sheets/actors/party/notes.hbs @@ -1,10 +1,7 @@
-
- {{localize tabs.notes.label}} - {{formInput notes.field value=notes.value enriched=notes.value toggled=true}} -
+ {{formInput notes.field value=notes.value enriched=notes.value toggled=true}}
\ No newline at end of file diff --git a/templates/sheets/global/partials/domain-card-item.hbs b/templates/sheets/global/partials/domain-card-item.hbs index 54e44e645..ca584e577 100644 --- a/templates/sheets/global/partials/domain-card-item.hbs +++ b/templates/sheets/global/partials/domain-card-item.hbs @@ -1,4 +1,4 @@ -
  • +
  • {{item.system.recallCost}} diff --git a/templates/sheets/global/partials/inventory-item-V2.hbs b/templates/sheets/global/partials/inventory-item-V2.hbs index 523e93042..775690d49 100644 --- a/templates/sheets/global/partials/inventory-item-V2.hbs +++ b/templates/sheets/global/partials/inventory-item-V2.hbs @@ -25,146 +25,146 @@ Parameters: data-type="{{type}}" data-item-type="{{item.type}}" data-item-uuid="{{item.uuid}}" data-no-compendium-edit="{{noCompendiumEdit}}" > -
    - {{!-- Image --}} -
    - - {{#if item.usable}} - {{#if @root.isNPC}} - d20 - {{else}} - 2d12 - {{/if}} - {{/if}} -
    - - {{!-- Name & Tags --}} -
    - {{!-- Item Name --}} - {{localize item.name}} {{#unless (or noExtensible (not item.system.description))}}{{/unless}} - - {{!-- Tags Start --}} - {{#if (not ../hideTags)}} - {{#> "systems/daggerheart/templates/sheets/global/partials/item-tags.hbs" item}} - {{#if (eq ../type 'feature')}} - {{#if (and system.featureForm (or (eq @root.document.type "adversary") (eq @root.document.type "environment")))}} -
    - {{localize (concat "DAGGERHEART.CONFIG.FeatureForm." system.featureForm)}} -
    +
    +
    + {{!-- Image --}} +
    + + {{#if (and item.usable (ne showActions false))}} + {{#if @root.isNPC}} + d20 + {{else}} + 2d12 {{/if}} {{/if}} - {{/ "systems/daggerheart/templates/sheets/global/partials/item-tags.hbs"}} - {{/if}} +
    - {{!--Tags End --}} -
    + {{!-- Name & Tags --}} +
    + {{!-- Item Name --}} + {{localize item.name}} {{#unless (or noExtensible (not item.hasDescription))}}{{/unless}} - {{!-- Simple Resource --}} - {{#if (and (not hideResources) (not (eq item.system.resource.type 'diceValue')))}} - {{> "systems/daggerheart/templates/sheets/global/partials/item-resource.hbs"}} - {{/if}} - {{#if (or isQuantifiable (or (eq item.system.quantity 0) (gt item.system.quantity 1)))}} -
    - -
    - {{/if}} - - {{!-- Controls --}} - {{#unless hideControls}} -
    - {{!-- Toggle/Equip buttons --}} - {{#if @root.editable}} - {{#if (and (eq actorType 'character') (eq type 'weapon'))}} - - - - {{/if}} - {{#if (and (eq actorType 'character') (eq type 'armor'))}} - - - - {{/if}} - {{#if (and (eq type 'domainCard'))}} - - - + {{!-- Tags Start --}} + {{#if (not hideTags)}} + {{#> "systems/daggerheart/templates/sheets/global/partials/item-tags.hbs" item}} + {{#if (and (eq ../type 'feature') system.featureForm (ne @root.document.type "character"))}} +
    + {{localize (concat "DAGGERHEART.CONFIG.FeatureForm." system.featureForm)}} +
    + {{/if}} + {{/ "systems/daggerheart/templates/sheets/global/partials/item-tags.hbs"}} {{/if}} - {{#if (and (and (eq type 'effect') (not (eq item.type 'beastform'))))}} - - - - {{/if}} - {{/if}} - {{!-- Send to Chat --}} - {{#if (hasProperty item "toChat")}} - - - + {{!--Tags End --}} +
    + + {{!-- Simple Resource --}} + {{#if (and (not hideResources) (not (eq item.system.resource.type 'diceValue')))}} + {{> "systems/daggerheart/templates/sheets/global/partials/item-resource.hbs"}} + {{/if}} + {{#if (or isQuantifiable (or (eq item.system.quantity 0) (gt item.system.quantity 1)))}} +
    + +
    {{/if}} - {{!-- Document management buttons or context menu --}} - {{#if (and (not isActor) (not hideContextMenu))}} - - - - {{else if (and @root.editable (not hideModifyControls))}} - - - - {{#if (not isActor)}} - - - - {{else if (eq type 'adversary')}} - - - - {{/if}} + {{!-- Controls --}} + {{#unless hideControls}} +
    + {{!-- Toggle/Equip buttons --}} + {{#if @root.editable}} + {{#if (and (eq actorType 'character') (eq type 'weapon'))}} + + + + {{/if}} + {{#if (and (eq actorType 'character') (eq type 'armor'))}} + + + + {{/if}} + {{#if (and (eq type 'domainCard'))}} + + + + {{/if}} + {{#if (and (and (eq type 'effect') (not (eq item.type 'beastform'))))}} + + + + {{/if}} + {{/if}} + + {{!-- Send to Chat --}} + {{#if (hasProperty item "toChat")}} + + + + {{/if}} + + {{!-- Document management buttons or context menu --}} + {{#if (and (not isActor) (not hideContextMenu))}} + + + + {{else if (and @root.editable (not hideModifyControls))}} + + + + {{#if (not isActor)}} + + + + {{else if (eq type 'adversary')}} + + + + {{/if}} + {{/if}} +
    + {{/unless}} +
    + {{#unless hideDescription}} +
    + {{!-- Description --}} +
    +
    + {{/unless}} +
    + {{!-- Dice Resource --}} + {{#if (and (not hideResources) (eq item.system.resource.type 'diceValue'))}} + {{> "systems/daggerheart/templates/sheets/global/partials/item-resource.hbs"}} + {{/if}} + {{!-- Actions Buttons --}} + {{#if (and showActions item.system.actions.size)}} +
    + {{#each item.system.actions as | action |}} +
    + {{#if (and (eq action.type 'beastform') @root.beastformActive)}} + + {{else}} + + {{/if}} + {{#if action.uses.max}} +
    + {{/if}}
    - {{/unless}} -
    -
    - {{!-- Description --}} - {{#unless hideDescription}} -
    - {{/unless}} -
    - {{!-- Dice Resource --}} - {{#if (and (not hideResources) (eq item.system.resource.type 'diceValue'))}} - {{> "systems/daggerheart/templates/sheets/global/partials/item-resource.hbs"}} - {{/if}} - {{!-- Actions Buttons --}} - {{#if (and showActions item.system.actions.size)}} -
    - {{#each item.system.actions as | action |}} -
    - {{#if (and (eq action.type 'beastform') @root.beastformActive)}} - - {{else}} - - {{/if}} - {{#if action.uses.max}} -
    - - {{/if}} + {{/each}}
    - {{/each}} -
    - {{/if}} -
  • \ No newline at end of file + {{/if}} + diff --git a/templates/ui/countdown-edit.hbs b/templates/ui/countdowns/countdown-edit.hbs similarity index 97% rename from templates/ui/countdown-edit.hbs rename to templates/ui/countdowns/countdown-edit.hbs index 6b7d22d4d..eebefe11e 100644 --- a/templates/ui/countdown-edit.hbs +++ b/templates/ui/countdowns/countdown-edit.hbs @@ -78,9 +78,9 @@
    - +
    diff --git a/templates/ui/countdowns/countdowns-view.hbs b/templates/ui/countdowns/countdowns-view.hbs new file mode 100644 index 000000000..7727c4b6a --- /dev/null +++ b/templates/ui/countdowns/countdowns-view.hbs @@ -0,0 +1,24 @@ +
    +
    + + {{#unless iconOnly}} + {{localize "DAGGERHEART.UI.Countdowns.title"}} + {{/unless}} +
    + {{#each typeToggles as |type|}} + {{localize type.label}} + {{/each}} +
    + {{#if isGM}} + + + + {{/if}} + + + +
    +
    + {{> "systems/daggerheart/templates/ui/countdowns/parts/countdowns.hbs" }} +
    +
    \ No newline at end of file diff --git a/templates/ui/countdowns.hbs b/templates/ui/countdowns/parts/countdowns.hbs similarity index 65% rename from templates/ui/countdowns.hbs rename to templates/ui/countdowns/parts/countdowns.hbs index 950678269..4f0ee0865 100644 --- a/templates/ui/countdowns.hbs +++ b/templates/ui/countdowns/parts/countdowns.hbs @@ -1,37 +1,29 @@ -
    -
    - - {{localize "DAGGERHEART.UI.Countdowns.title"}} - - - - - - -
    -
    - {{#each countdowns as | countdown id |}} -
    +{{#each countdowns as | category key |}} +
    + {{#each category as |countdown id|}} +
    - +
    - {{#unless ../iconOnly}}{{/unless}} + {{#unless @root.iconOnly}} +
    {{countdown.name}}
    + {{/unless}}
    - {{#if countdown.editable}}{{/if}} + {{#if countdown.editable}}{{/if}}
    {{countdown.progress.current}}/{{countdown.progress.start}}
    - {{#if countdown.editable}}{{/if}} + {{#if countdown.editable}}{{/if}}
    - {{#if (not ../iconOnly)}} + {{#if (not @root.iconOnly)}} {{#if (and countdown.noPlayerAccess @root.isGM)}} {{/if}} {{#unless (eq countdown.progress.looping "noLooping")}} - + {{#if (eq countdown.progress.looping "increasing")}} @@ -49,4 +41,4 @@
    {{/each}}
    -
    \ No newline at end of file +{{/each}} \ No newline at end of file diff --git a/templates/ui/itemBrowser/filterContainer.hbs b/templates/ui/itemBrowser/filterContainer.hbs index da8e8cfe9..64784bf1d 100644 --- a/templates/ui/itemBrowser/filterContainer.hbs +++ b/templates/ui/itemBrowser/filterContainer.hbs @@ -9,13 +9,15 @@
    {{else}} - {{#if filtered }} - {{formField field localize=true blank="" name=name choices=(@root.formatChoices this) valueAttr="value" dataset=(object key=key) value=value}} - {{else}} - {{#if field.label}} - {{formField field localize=true blank="" name=name dataset=(object key=key) value=value}} + {{#if field}} + {{#if filtered }} + {{formField field localize=true blank="" name=name choices=(@root.formatChoices this) valueAttr="value" dataset=(object key=key) value=value}} {{else}} - {{formField field localize=true blank="" name=name dataset=(object key=key) label=label value=value}} + {{#if field.label}} + {{formField field localize=true blank="" name=name dataset=(object key=key) value=value}} + {{else}} + {{formField field localize=true blank="" name=name dataset=(object key=key) label=label value=value}} + {{/if}} {{/if}} {{/if}} {{/if}} diff --git a/tools/analyze-damage.mjs b/tools/analyze-damage.mjs index 7b3fb9e51..31c254ce7 100644 --- a/tools/analyze-damage.mjs +++ b/tools/analyze-damage.mjs @@ -82,7 +82,7 @@ function getMean(numbers) { } function getMedianAverageDeviation(numbers, { median }) { - const residuals = allDamage.map(d => Math.abs(d - median)); + const residuals = numbers.map(d => Math.abs(d - median)); return getMedian(residuals); } @@ -98,8 +98,8 @@ function parseDamage(damage) { p.value.custom.enabled ? p.value.custom.formula : [p.value.flatMultiplier ? `${p.value.flatMultiplier}${p.value.dice}` : 0, p.value.bonus ?? 0] - .filter(p => !!p) - .join('+') + .filter(p => !!p) + .join('+') ) .join('+'); return getExpectedDamage(formula); diff --git a/tools/create-symlink.mjs b/tools/create-symlink.mjs index fd828c73e..a9a121b04 100644 --- a/tools/create-symlink.mjs +++ b/tools/create-symlink.mjs @@ -1,45 +1,70 @@ import fs from 'fs'; import path from 'path'; import readline from 'readline'; +import { isContainedPath, readEnvFile } from './util.mjs'; -const askQuestion = question => { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - - return new Promise(resolve => - rl.question(question, answer => { - rl.close(); - resolve(answer); - }) - ); -}; - -const installPath = await askQuestion('Enter your Foundry install path: '); - -// Determine if it's an Electron install (nested structure) -const nested = fs.existsSync(path.join(installPath, 'resources', 'app')); -const fileRoot = nested ? path.join(installPath, 'resources', 'app') : installPath; - -try { - await fs.promises.mkdir('foundry'); -} catch (e) { - if (e.code !== 'EEXIST') throw e; -} +const projectRoot = path.resolve(import.meta.dirname, '../') +const { foundryRoot, dataPath } = readEnvFile(); + +async function createFoundrySymlink() { + // If foundry already exists, exit and inform the user. This operation can't complete correctly otherwise. + // If the folder is empty, its fine. It may have failed due to perms + const foundryDestPath = path.join(projectRoot, 'foundry'); + if (fs.existsSync(foundryDestPath) && fs.readdirSync(foundryDestPath).length) { + console.log('"foundry" folder already exists in this project'); + return; + } + + console.log('Creating "foundry" symlinks for types'); + try { + await fs.promises.mkdir(foundryDestPath); + console.log('Root foundry folder created'); + } catch (e) { + if (e.code !== 'EEXIST') throw e; + } + + // JavaScript files + for (const p of ['client', 'common', 'tsconfig.json']) { + try { + await fs.promises.symlink(path.join(foundryRoot, p), path.join(foundryDestPath, p)); + console.log(`${p} folder created`); + } catch (e) { + if (e.code !== 'EEXIST') throw e; + } + } -// JavaScript files -for (const p of ['client', 'common', 'tsconfig.json']) { + // Language files try { - await fs.promises.symlink(path.join(fileRoot, p), path.join('foundry', p)); + await fs.promises.symlink(path.join(foundryRoot, 'public', 'lang'), path.join(foundryDestPath, 'lang')); + console.log(`lang folder created`); } catch (e) { if (e.code !== 'EEXIST') throw e; + console.log(`lang folder already exists`); } } -// Language files -try { - await fs.promises.symlink(path.join(fileRoot, 'public', 'lang'), path.join('foundry', 'lang')); -} catch (e) { - if (e.code !== 'EEXIST') throw e; +async function createDaggerheartSymlink() { + if (isContainedPath(dataPath, projectRoot)) { + console.log('The Daggerheart project repo is in foundry data, so a symlink won\'t be created'); + return; + } + + const destination = path.join(dataPath, 'Data', 'systems', 'daggerheart'); + if (fs.existsSync(destination)) { + console.log('A Daggerheart folder already exists in Foundry data'); + return; + } + + console.log('Creating Daggerheart symlink in the foundry systems folder') + try { + await fs.promises.symlink(projectRoot, destination); + console.log('Daggerheart system folder symlink created'); + } catch (e) { + if (e.code !== 'EEXIST') throw e; + console.log(`Daggerheart system folder already exists`); + } } + +await createFoundrySymlink(); +console.log(); // Add empty newline +await createDaggerheartSymlink(); diff --git a/tools/dev-setup.mjs b/tools/dev-setup.mjs deleted file mode 100644 index f232f5a82..000000000 --- a/tools/dev-setup.mjs +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env node -import fs from 'fs'; - -const args = process.argv.slice(2); -const foundryPath = args.find(arg => arg.startsWith('--foundry-path='))?.split('=')[1]; -const dataPath = args.find(arg => arg.startsWith('--data-path='))?.split('=')[1]; - -if (!foundryPath || !dataPath) { - console.log('Usage: npm run setup:dev -- --foundry-path="/path/to/foundry/main.js" --data-path="/path/to/data"'); - process.exit(1); -} - -const envContent = `FOUNDRY_MAIN_PATH=${foundryPath} -FOUNDRY_DATA_PATH=${dataPath} -`; - -fs.writeFileSync('.env', envContent); -console.log(`✅ Development environment configured: ${foundryPath}, ${dataPath}`); diff --git a/tools/eslint.config.mjs b/tools/eslint.config.mjs new file mode 100644 index 000000000..36a5174aa --- /dev/null +++ b/tools/eslint.config.mjs @@ -0,0 +1,20 @@ +import globals from 'globals'; +import { defineConfig, globalIgnores } from 'eslint/config'; +import { stylisticRules } from '../eslint.config.mjs'; +import stylistic from '@stylistic/eslint-plugin'; + +export default defineConfig([ + globalIgnores(['foundry/**/*']), + { + files: ['**/*.{js,mjs,cjs}'], + plugins: { + '@stylistic': stylistic + }, + languageOptions: { globals: globals.node }, + rules: { + 'no-undef': 'error', + 'no-unused-vars': 0, + ...stylisticRules + } + } +]); diff --git a/tools/run-start.mjs b/tools/run-start.mjs index 3f6b25cb4..379e174e1 100644 --- a/tools/run-start.mjs +++ b/tools/run-start.mjs @@ -1,24 +1,13 @@ #!/usr/bin/env node import { spawn } from 'child_process'; +import { readEnvFile } from './util.mjs'; import fs from 'fs'; -// Load .env file if it exists -if (fs.existsSync('.env')) { - const envFile = fs.readFileSync('.env', 'utf8'); - envFile.split('\n').forEach(line => { - const [key, value] = line.split('='); - if (key && value) { - process.env[key] = value; - } - }); -} - -// Set defaults if not in environment -const foundryPath = process.env.FOUNDRY_MAIN_PATH || '../../../../FoundryDev/main.js'; -const dataPath = process.env.FOUNDRY_DATA_PATH || '../../../'; +// Load .env file params +const { foundryPath, dataPath } = readEnvFile(); // Run the original command with proper environment -const args = ['rollup -c --watch', `node "\"${foundryPath}\"" --dataPath="${dataPath}" --noupnp`, 'gulp']; +const args = ['rollup -c --watch', 'gulp', `node "\"${foundryPath}\"" --dataPath="${dataPath}" --noupnp`]; spawn('npx', ['concurrently', ...args.map(arg => `"${arg}"`)], { stdio: 'inherit', diff --git a/tools/util.mjs b/tools/util.mjs new file mode 100644 index 000000000..ae3fedc5e --- /dev/null +++ b/tools/util.mjs @@ -0,0 +1,40 @@ +import fs from 'fs'; +import path from 'path'; + +export function readEnvFile() { + if (!fs.existsSync('.env')) { + console.error('No configured .env file. Copy .env.example to .env and configure it.'); + process.exit(); + } + + const envFile = fs.readFileSync('.env', 'utf8'); + envFile.split('\n').forEach(line => { + const [key, value] = line.split('='); + if (key && value) { + process.env[key] = value; + } + }); + + // Determine foundry path, handling if its an electron install (nested structure) + const foundryPath = path.normalize(process.env.FOUNDRY_MAIN_PATH).trimEnd(); + const dataPath = path.normalize(process.env.FOUNDRY_DATA_PATH).trimEnd(); + if (!foundryPath.endsWith('main.js')) { + console.error('Configured FOUNDRY_MAIN_PATH is invalid, it must end with main.js'); + process.exit(); + } + if (path.basename(dataPath) === 'Data' || !fs.existsSync(path.join(dataPath, 'Data'))) { + console.error('Configured FOUNDRY_DATA_PATH is incorrect. This must be a folder that contains "Data"'); + process.exit(); + } + + return { + foundryPath, + foundryRoot: path.dirname(foundryPath), + dataPath + }; +} + +export function isContainedPath(parent, child) { + const relative = path.relative(parent, child); + return relative && !relative.startsWith('..') && !path.isAbsolute(relative); +} \ No newline at end of file