diff --git a/.editorconfig b/.editorconfig index 6cfef2fc..aa391e00 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 730309d3..3d62d699 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 6de9e5d0..00000000 --- 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 00000000..b472f383 --- /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 f59143fd..177636c7 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. **Start developing:** +4. **Setup symlinks:** + + 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 90deb64a..8fbe2c06 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 f21b4c5b..39ca794a 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 d9e008f5..324ffbbb 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 84307da0..98b9b68b 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 8f7117e8..fcdb8a9d 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 5d7be27d..436884bd 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 587cb1c1..5c29fc43 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 2cef80a6..03b82d48 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 d95c935d..031f23da 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 34a54d2a..38dbcd04 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 2c3cdf63..ae7398ec 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 172a8003..d66399f4 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 7f981914..0c235732 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 19c197f6..2d446683 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 c28d742b..b7c776a1 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 2be500c1..7e57bbad 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 ab754b17..1641faa8 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 25c41ced..b92be2a2 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 ce2bb86f..74141cc8 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: { - 'prettier/prettier': 'error' + '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: { + ...stylisticRules } } ]); diff --git a/jsconfig.json b/jsconfig.json index 00bab1f5..42c4b404 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 a853d5f9..368dd54d 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" } @@ -1455,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": { @@ -2014,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": { @@ -2896,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 82ca9ccb..9d27fbb7 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 76b2e751..048797d8 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 b916a5de..8cb026f0 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 367540bf..e209cc3b 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 dd504b4b..ebc80d39 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 42e3a727..46ceafbd 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 3dc6b0fc..19869a00 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; - } - game.actors.get(memberId).modifyResource(resourceUpdates); + 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 }]); + } + } + } + + /* 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 671b01a1..4805cd9e 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 e8d6cf1c..a2df63c1 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 d6bf2d78..92cf3050 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 c4616d9a..f40609a4 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 9de9e752..f2defffb 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 40ea0301..c4dfc397 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 5a92435c..958d502e 100644 --- a/module/applications/sheets-configs/action-base-config.mjs +++ b/module/applications/sheets-configs/action-base-config.mjs @@ -290,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' diff --git a/module/applications/sheets-configs/adversary-settings.mjs b/module/applications/sheets-configs/adversary-settings.mjs index 57405675..ff3f3039 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 6d74f9c6..954d6381 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 c187877c..33cbc270 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 a5bcc4f9..531158cd 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 00000000..89adc57b --- /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 06dd4a0f..85380392 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 5d0e7144..3a60e7ca 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 a01b4a64..4b5b5a56 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 f8ff74a6..60fb9071 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 927a8810..3af8ea5f 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 738f7002..fa434034 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 2b0c3e55..0168f46d 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 7b820822..007b641b 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 bcf2fc3c..ea4d5352 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 e9d8370e..650d2088 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 89da1426..4d00afa2 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 80d3ebe4..e30950b5 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 7036a5df..199ee87d 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 b418107c..1dbc56be 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 76e2b399..5cf79100 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 035041e1..a64b1b22 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 00000000..8047f1dc --- /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 68e325c2..7ae6dc37 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/flagsConfig.mjs b/module/config/flagsConfig.mjs index 817ac89d..cb05bd72 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 6fc85ec5..188efafb 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 ae5fa71b..83572dc0 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/attackAction.mjs b/module/data/action/attackAction.mjs index 1f7e1c92..1988b1d8 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 acd104a7..58be672b 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' + ]; - return !effect.isSuppressed; + 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; + } } - ); + + 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/activeEffect/beastformEffect.mjs b/module/data/activeEffect/beastformEffect.mjs index 0fbea122..7e037f5b 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 217ff9dd..0c226513 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 00000000..fec6e638 --- /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 d6d0dcdf..ae17c128 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 9efbe7d7..995708eb 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 10d53c13..b39c64aa 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 300bd698..2ca7fd5b 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 785eec2b..bc6ad176 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 7ab61210..e24820de 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 7d27197d..ffe4d26b 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 e3be9937..0eeb95c2 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 1928af41..82cfcd23 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 990f8ef1..53e132c9 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/effectsField.mjs b/module/data/fields/action/effectsField.mjs index d2ee1682..e943d63d 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 0629353e..7343ab85 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 a2275fa5..6845d2ba 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 a5f6f379..e165f5cc 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 85922f1f..c465bec3 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 00000000..6004e13b --- /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 b9253a3c..eae1136c 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 21c56f9a..15bb620d 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 ee9d9839..ba274cc7 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 470a1e3c..b9cda264 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 6d054976..6f4470b8 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 55b078c2..cbf73bc3 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 84e4de7f..39c0fc8e 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 4dc1c058..e30bf52d 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 ab86351c..64e7f93e 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 31247458..a0761e20 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 b1d3bd0b..e9c447e4 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 02c4ab24..1c6ec829 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 1d2d556a..e2d967a3 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 33519949..d03970d0 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); - const resourceUpdates = new ResourceUpdateMap(actor); - resourceUpdates.addResources(updates); - resourceUpdates.updateResources(); + 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(); + } + + 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 00000000..a94d6395 --- /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 f9239a90..4a9f3cc4 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 fb10435f..8ef64f65 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 893e6e5c..b555dfca 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 20996b77..e74127e9 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 f46e24e6..14717538 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 30862724..8e91d4f0 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 e3f9c42a..db0e8729 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 5b66179f..a7db01a4 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 c82bbcb2..8513ed94 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 8db3ec14..bbe93b17 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 365caec9..76ea0b73 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 2f20175b..cb79a76b 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 1b08e0ad..bf315358 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 b4c446b2..6971c34c 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 a66323c7..1a985274 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 28223032..dee096eb 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 73a7fe99..533d250a 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 187402fb..4da2e043 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 00000000..e9cc0401 --- /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 00000000..2f8eaf45 --- /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 00000000..2f8eaf45 --- /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 00000000..adf8d57a --- /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 00000000..1c574e81 --- /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 00000000..1c574e81 --- /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 00000000..0b8e94a8 --- /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 00000000..1c574e81 --- /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 00000000..8a8a16c4 --- /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 00000000..8e0af6e0 --- /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 00000000..09cc2dfe --- /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 f74ab8a0..00000000 --- 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 27925fa2..f90b57dc 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 00000000..938710c9 --- /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 00000000..1c574e81 --- /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 e8f61318..4ce4834e 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 00000000..1c574e81 --- /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 00000000..1c574e81 --- /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 00000000..1c574e81 --- /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 00000000..1c574e81 --- /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 00000000..235f3b9c --- /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 00000000..8bf56824 --- /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 d6f7ad29..e79ed65c 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 c0e7f3fc..19a9e519 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 3a5a9321..fc73ba95 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 e4b1249f..27048ddf 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 459f8fd7..f1f4602e 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 00000000..c86d0939 --- /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 00000000..5968577d --- /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 00000000..1c574e81 --- /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 00000000..1e6ee34d --- /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 f575f848..53a03868 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/index.less b/styles/less/sheets/actions/index.less new file mode 100644 index 00000000..29ef8645 --- /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 00000000..19f8a3a7 --- /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 b3eb0469..5eb5b43c 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 1e5e4fa5..549a219d 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 00000000..959bc0f5 --- /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 4312f755..451ae03a 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 00000000..7c40a2e3 --- /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 00000000..1c574e81 --- /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 00000000..9cadbd04 --- /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 00000000..786815ef --- /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 66a6c88a..96e01ffd 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 00000000..45ecda26 --- /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 00000000..1c574e81 --- /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 00000000..daf9a1e4 --- /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 00000000..ad28855c --- /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 31ea8955..af1614e4 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 00000000..842f716b --- /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 00000000..9646670a --- /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 00000000..a7d08785 --- /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 00000000..4e3af363 --- /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 00000000..c0765ae7 --- /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 00000000..8b1c109a --- /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 00000000..f0a8bfc1 --- /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 00000000..4e1aa798 --- /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 35c48480..f2ab99e7 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 00000000..b4961b41 --- /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 201ea356..72ee1a85 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 00000000..37b096d3 --- /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 18b1f9a6..e2ef85ef 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 00000000..91007146 --- /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 a73f2d1c..b6c9a2e7 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 00000000..eeec9354 --- /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 588ceafe..0ecc2e75 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/dialogs/tagTeamDialog/initialization.hbs b/templates/dialogs/tagTeamDialog/initialization.hbs index 0b92e68e..40491e1b 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 cf43e15d..91c82f13 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 4b6e7d85..1fd449a9 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/adversary-settings/features.hbs b/templates/sheets-settings/adversary-settings/features.hbs index 2f2f5f47..1cab8cfe 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}} - -
- {{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 579fe74e..1cab8cfe 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 2f2f5f47..1cab8cfe 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 a5c3f706..d329d318 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 4f6b131e..1acf0e93 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 bc9ac3cf..7dd5432d 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 663a484a..af21b29f 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 54e44e64..ca584e57 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 523e9304..775690d4 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)))}} -
    - + {{!-- 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}} + + {{!--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}} + + {{!-- 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}} - - {{!-- 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}} + {{!-- 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}} - - {{!-- 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 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 6b7d22d4..eebefe11 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 00000000..7727c4b6 --- /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 95067826..4f0ee086 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 da8e8cfe..64784bf1 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 7b3fb9e5..31c254ce 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 fd828c73..a9a121b0 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 - }); +const projectRoot = path.resolve(import.meta.dirname, '../') +const { foundryRoot, dataPath } = readEnvFile(); - return new Promise(resolve => - rl.question(question, answer => { - rl.close(); - resolve(answer); - }) - ); -}; +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; + } -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; -} - -// JavaScript files -for (const p of ['client', 'common', 'tsconfig.json']) { + console.log('Creating "foundry" symlinks for types'); try { - await fs.promises.symlink(path.join(fileRoot, p), path.join('foundry', p)); + 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; + } + } + + // Language files + try { + 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 f232f5a8..00000000 --- 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 00000000..36a5174a --- /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 3f6b25cb..379e174e 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 00000000..ae3fedc5 --- /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