diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..56ce8818 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto eol=lf +*.json text eol=lf diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 553a1a17..4ffcc64d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -37,6 +37,7 @@ jobs: url: https://github.com/${{github.repository}} manifest: https://raw.githubusercontent.com/${{github.repository}}/v14/system.json download: https://github.com/${{github.repository}}/releases/download/${{github.event.release.tag_name}}/system.zip + flags.hotReload: false # Create a zip file with all files required by the module to add to the release - run: zip -r ./system.zip system.json README.md LICENSE build/daggerheart.js build/tagify.css styles/daggerheart.css assets/ templates/ packs/ lang/ diff --git a/.gitignore b/.gitignore index 9a22c0ce..e41a67d3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ Build build foundry styles/daggerheart.css +styles/daggerheart.css.map diff --git a/README.md b/README.md index 0c2dabc3..ac3666b3 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,11 @@ You can find the documentation here: https://github.com/Foundryborne/daggerheart ## Contributing -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. +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: diff --git a/assets/icons/documents/actors/drama-masks.svg b/assets/icons/documents/actors/drama-masks.svg new file mode 100644 index 00000000..84307da0 --- /dev/null +++ b/assets/icons/documents/actors/drama-masks.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/daggerheart.mjs b/daggerheart.mjs index e25f1b09..25c41ced 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -196,6 +196,11 @@ Hooks.once('init', () => { makeDefault: true, label: sheetLabel('TYPES.Actor.environment') }); + Actors.registerSheet(SYSTEM.id, applications.sheets.actors.NPC, { + types: ['npc'], + makeDefault: true, + label: sheetLabel('TYPES.Actor.npc') + }); Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Party, { types: ['party'], makeDefault: true, @@ -342,7 +347,8 @@ Hooks.on(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, async data => { const party = game.actors.get(data.partyId); if (!party) return; - const dialog = new game.system.api.applications.dialogs.TagTeamDialog(party); + const TagTeamDialog = game.system.api.applications.dialogs.TagTeamDialog; + const dialog = foundry.applications.instances.get(`TagTeamDialog-${party.id}`) ?? new TagTeamDialog(party); dialog.tabGroups.application = 'tagTeamRoll'; await dialog.render({ force: true }); } @@ -353,7 +359,8 @@ Hooks.on(CONFIG.DH.HOOKS.hooksConfig.groupRollStart, async data => { const party = game.actors.get(data.partyId); if (!party) return; - const dialog = new game.system.api.applications.dialogs.GroupRollDialog(party); + const GroupRollDialog = game.system.api.applications.dialogs.GroupRollDialog; + const dialog = foundry.applications.instances.get(`GroupRollDialog-${party.id}`) ?? new GroupRollDialog(party); dialog.tabGroups.application = 'groupRoll'; await dialog.render({ force: true }); } @@ -439,3 +446,33 @@ Hooks.on('canvasTearDown', canvas => { Hooks.on('canvasReady', canas => { game.system.registeredTriggers.registerSceneTriggers(canvas.scene); }); + +/** Make the user to select a document type, instead of having a default doc type for them to accidentally keep */ +Hooks.on('renderDialogV2', (_dialog, html) => { + if (!html.classList.contains('dialog')) return; + const cls = html.classList.contains('item-create') + ? documents.DHItem.implementation + : html.classList.contains('actor-create') + ? documents.DhpActor.implementation + : null; + if (!cls) return; + + const form = html.querySelector('form'); + const submit = html.querySelector('button[type=submit]'); + const select = html.querySelector('select[name=type]'); + const nameInput = html.querySelector('input[name=name]'); + if (!form || !select || !submit || !nameInput) return; + + nameInput.placeholder = cls.defaultName({}); + const emptyOption = document.createElement('option'); + emptyOption.value = ''; + emptyOption.selected = true; + select.required = true; + select.prepend(emptyOption); + submit.addEventListener('click', event => { + if (!form.reportValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + }); +}); diff --git a/gulpfile.js b/gulpfile.js index 45479a5c..2e873ced 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,9 +1,15 @@ // Less configuration var gulp = require('gulp'); var less = require('gulp-less'); +var sourcemaps = require('gulp-sourcemaps'); gulp.task('less', function (cb) { - gulp.src('styles/daggerheart.less').pipe(less()).pipe(gulp.dest('styles')); + gulp.src('styles/daggerheart.less') + .pipe(sourcemaps.init()) + .pipe(less()) + .on('error', console.error.bind(console)) + .pipe(sourcemaps.write('.')) + .pipe(gulp.dest('styles')); cb(); }); diff --git a/lang/en.json b/lang/en.json index dbeb0c91..5bebae32 100755 --- a/lang/en.json +++ b/lang/en.json @@ -23,6 +23,7 @@ "companion": "Companion", "adversary": "Adversary", "environment": "Environment", + "npc": "NPC", "party": "Party" } }, @@ -119,8 +120,7 @@ "deleteTriggerContent": "Are you sure you want to delete the {trigger} trigger?", "advantageState": "Advantage State", "damageOnSave": "Damage on Save", - "useDefaultItemValues": "Use default Item values", - "itemDamageIsUsed": "Item Damage Is Used" + "useDefaultItemValues": "Use default Item values" }, "RollField": { "diceRolling": { @@ -135,7 +135,7 @@ "attackModifier": "Attack Modifier", "attackName": "Attack Name", "criticalThreshold": "Critical Threshold", - "includeBase": { "label": "Use Item Damage" }, + "includeBase": { "label": "Include Item Damage" }, "groupAttack": { "label": "Group Attack" }, "multiplier": "Multiplier", "saveHint": "Set a default Trait to enable Reaction Roll. It can be changed later in Reaction Roll Dialog.", @@ -340,6 +340,11 @@ }, "newAdversary": "New Adversary" }, + "NPC": { + "FIELDS": { + "motives": { "label": "Motives" } + } + }, "Party": { "Subtitle": { "character": "{community} {ancestry} | {subclass} {class}", @@ -413,7 +418,11 @@ "giveSpotlight": "Give The Spotlight", "requestingSpotlight": "Requesting The Spotlight", "requestSpotlight": "Request The Spotlight", - "openCountdowns": "Countdowns" + "openCountdowns": "Countdowns", + "adversaryCategories": { + "friendly": "Friendly", + "adversaries": "Adversaries" + } }, "CompendiumBrowserSettings": { "title": "Enable Compendiums", @@ -711,19 +720,13 @@ }, "PendingReactionsDialog": { "title": "Pending Reaction Rolls Found", - "unfinishedRolls": "Some Tokens still need to roll their Reaction Roll.", - "confirmation": "Are you sure you want to continue ?", - "warning": "Undone reaction rolls will be considered as failed" + "unfinishedRolls": "Some Tokens have not finished their Reaction Rolls.", + "warning": "Unfinished reaction rolls will be considered as failed.", + "confirmation": "Are you sure you want to continue?" }, "ReactionRoll": { "title": "Reaction Roll: {trait}" }, - "RerollDialog": { - "title": "Reroll", - "damageTitle": "Reroll Damage", - "deselectDiceNotification": "Deselect one of the selected dice first", - "acceptCurrentRolls": "Accept Current Rolls" - }, "ResourceDice": { "title": "{name} Resource", "rerollDice": "Reroll Dice" @@ -768,18 +771,25 @@ } }, "GroupRollSelect": { - "title": "Group Roll", - "aidingCharacters": "Aiding Characters", + "cancelConfirmText": "Are you sure you want to cancel the Group Roll? This will close it for all other players too.", + "cancelConfirmTitle": "Cancel Group Roll", + "initializationTitle": "Character Selection", + "finishGroupRoll": "Finish Group Roll", "leader": "Leader", "leaderRoll": "Leader Roll", + "members": "Members", "openDialogForAll": "Open Dialog For All", + "removeRoll": "Remove Roll", + "resultsHint": "Results will appear when characters roll", + "selectLeaderHint": "Select one Character to be the leader", + "selectParticipantsHint": "Select one Character to be the leader", "startGroupRoll": "Start Group Roll", - "finishGroupRoll": "Finish Group Roll", - "cancelConfirmTitle": "Cancel Group Roll", - "cancelConfirmText": "Are you sure you want to cancel the Group Roll? This will close it for all other players too." + "title": "Group Roll" }, "TokenConfig": { - "actorSizeUsed": "Actor size is set, determining the dimensions" + "actorSizeUsed": "Actor size is set, determining the dimensions", + "tokenSize": "Token Size", + "sizeCategory": "Size Category" } }, "CLASS": { @@ -2451,6 +2461,7 @@ "single": "Miss", "plural": "Miss" }, + "missingX": "Missing {x}", "maxWithThing": "Max {thing}", "missingDragDropThing": "Drop {thing} here", "multiclass": "Multiclass", @@ -2542,6 +2553,9 @@ "recovery": { "label": "Recovery" }, "type": { "label": "Type" }, "value": { "label": "Value" } + }, + "identifier": { + "label": "Identifier" } }, "Ancestry": { @@ -2567,10 +2581,11 @@ "tokenImg": { "label": "Token Image" }, "tokenRingImg": { "label": "Subject Texture" }, "tokenSize": { - "placeholder": "Using character dimensions", - "disabledPlaceholder": "Set by character size", + "placeholder": "Token Size", + "disabledPlaceholder": "Token Size", "height": { "label": "Height" }, "width": { "label": "Width" }, + "depth": { "label": "Depth" }, "scale": { "label": "Token Scale" } }, "evolved": { @@ -2832,6 +2847,15 @@ "hideObserverPermissionInChat": { "label": "Hide Chat Info From Players", "hint": "Information such as hit/miss on attack rolls against adversaries will be hidden" + }, + "hidePartyStats": { + "label": "Hide Party Stats", + "hint": "Resources and stats in the party sheet's member list will be hidden to the following users, even if the user is part of the same party", + "choices": { + "never": "Never, always show", + "players": "Hide From Players", + "always": "Hide from Everyone" + } } } }, @@ -3087,6 +3111,7 @@ } }, "ChatLog": { + "rerollActionRoll": "Reroll Action", "rerollDamage": "Reroll Damage", "assignTagRoll": "Assign as Tag Roll" }, @@ -3220,7 +3245,6 @@ "subclassesAlreadyPresent": "You already have a class and multiclass subclass", "noDiceSystem": "Your selected dice {system} does not have a {faces} dice", "gmMenuRefresh": "You refreshed all actions and resources {types}", - "subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class.", "gmRequired": "This action requires an online GM", "gmOnly": "This can only be accessed by the GM", "noActorOwnership": "You do not have permissions for this character", @@ -3230,7 +3254,8 @@ "domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used", "knowTheTide": "Know The Tide gained a token", "lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}", - "noTokenTargeted": "No token is targeted" + "noTokenTargeted": "No token is targeted", + "behaviorRegionRequiresGM": "Creating a Region with an attached Behavior requires an online GM" }, "Progress": { "migrationLabel": "Performing system migration. Please wait and do not close Foundry." diff --git a/module/applications/characterCreation/characterCreation.mjs b/module/applications/characterCreation/characterCreation.mjs index 936bb79d..82ca9ccb 100644 --- a/module/applications/characterCreation/characterCreation.mjs +++ b/module/applications/characterCreation/characterCreation.mjs @@ -439,10 +439,13 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl 'system.domain': { key: 'system.domain', value: this.setup.class?.system.domains ?? null } }; - if (type === 'subclasses') + if (type === 'subclasses') { + const classItem = this.setup.class; + const uuid = classItem?._stats.compendiumSource ?? classItem?.uuid; presets.filter = { - 'system.linkedClass.uuid': { key: 'system.linkedClass.uuid', value: this.setup.class?.uuid } + 'system.linkedClass': { key: 'system.linkedClass', value: uuid } }; + } if (equipment.includes(type)) presets.filter = { @@ -610,7 +613,8 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl [foundry.utils.randomID()]: {} }; } else if (item.type === 'subclass' && event.target.closest('.subclass-card')) { - if (this.setup.class.system.subclasses.every(subclass => subclass.uuid !== item.uuid)) { + const classSubclasses = await this.setup.class.system.fetchSubclasses(); + if (classSubclasses.every(subclass => subclass.uuid !== item.uuid)) { ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInClass')); return; } diff --git a/module/applications/dialogs/CompendiumBrowserSettings.mjs b/module/applications/dialogs/CompendiumBrowserSettings.mjs index bef54a6f..b0d83fb3 100644 --- a/module/applications/dialogs/CompendiumBrowserSettings.mjs +++ b/module/applications/dialogs/CompendiumBrowserSettings.mjs @@ -50,7 +50,7 @@ export default class CompendiumBrowserSettings extends HandlebarsApplicationMixi const excludedSourceData = this.browserSettings.excludedSources; const excludedPackData = this.browserSettings.excludedPacks; context.typePackCollections = game.packs.reduce((acc, pack) => { - const { type, label, packageType, packageName: basePackageName, id } = pack.metadata; + const { type, label, packageType, packageName: basePackageName, name, id } = pack.metadata; if (!CompendiumBrowserSettings.#browserPackTypes.includes(type)) return acc; const isWorldPack = packageType === 'world'; @@ -68,13 +68,15 @@ export default class CompendiumBrowserSettings extends HandlebarsApplicationMixi if (!acc[type].sources[packageName]) acc[type].sources[packageName] = { label: sourceLabel, checked: sourceChecked, packs: [] }; - const checked = !excludedPackData[id] || !excludedPackData[id].excludedDocumentTypes.includes(type); + const included = + !excludedPackData[packageName] || + !excludedPackData[packageName][name]?.excludedDocumentTypes.includes(type); acc[type].sources[packageName].packs.push({ - pack: id, + name, type, label: id === game.system.id ? game.system.title : game.i18n.localize(label), - checked: checked + checked: included }); return acc; @@ -106,16 +108,16 @@ export default class CompendiumBrowserSettings extends HandlebarsApplicationMixi toggleTypedPack(event) { event.stopPropagation(); - const { type, pack } = event.target.dataset; - const currentlyExcluded = this.browserSettings.excludedPacks[pack] - ? this.browserSettings.excludedPacks[pack].excludedDocumentTypes.includes(type) + const { type, source, packName } = event.target.dataset; + const currentlyExcluded = this.browserSettings.excludedPacks[source]?.[packName] + ? this.browserSettings.excludedPacks[source][packName].excludedDocumentTypes.includes(type) : false; - if (!this.browserSettings.excludedPacks[pack]) - this.browserSettings.excludedPacks[pack] = { excludedDocumentTypes: [] }; - this.browserSettings.excludedPacks[pack].excludedDocumentTypes = currentlyExcluded - ? this.browserSettings.excludedPacks[pack].excludedDocumentTypes.filter(x => x !== type) - : [...(this.browserSettings.excludedPacks[pack]?.excludedDocumentTypes ?? []), type]; + this.browserSettings.excludedPacks[source] ??= {}; + this.browserSettings.excludedPacks[source][packName] ??= { excludedDocumentTypes: [] }; + this.browserSettings.excludedPacks[source][packName].excludedDocumentTypes = currentlyExcluded + ? this.browserSettings.excludedPacks[source][packName].excludedDocumentTypes.filter(x => x !== type) + : [...(this.browserSettings.excludedPacks[source][packName]?.excludedDocumentTypes ?? []), type]; this.render(); } diff --git a/module/applications/dialogs/_module.mjs b/module/applications/dialogs/_module.mjs index bc516b9e..3b158ce6 100644 --- a/module/applications/dialogs/_module.mjs +++ b/module/applications/dialogs/_module.mjs @@ -10,7 +10,6 @@ export { default as ImageSelectDialog } from './imageSelectDialog.mjs'; export { default as ItemTransferDialog } from './itemTransfer.mjs'; export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.mjs'; export { default as OwnershipSelection } from './ownershipSelection.mjs'; -export { default as RerollDamageDialog } from './rerollDamageDialog.mjs'; export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs'; export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs'; export { default as TagTeamDialog } from './tagTeamDialog.mjs'; diff --git a/module/applications/dialogs/actionSelectionDialog.mjs b/module/applications/dialogs/actionSelectionDialog.mjs index 995c4894..6123c970 100644 --- a/module/applications/dialogs/actionSelectionDialog.mjs +++ b/module/applications/dialogs/actionSelectionDialog.mjs @@ -72,8 +72,8 @@ export default class ActionSelectionDialog extends HandlebarsApplicationMixin(Ap static async #onChooseAction(event, button) { const { actionId } = button.dataset; - this.action = this.item.system.actionsList.find(a => a._id === actionId); - Object.defineProperty(this.event, 'shiftKey', { + this.#action = this.item.system.actionsList.find(a => a._id === actionId); + Object.defineProperty(this.#event, 'shiftKey', { get() { return event.shiftKey; } diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index 926b3a80..76b2e751 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -175,14 +175,14 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio this.disadvantage = advantage === -1; this.config.roll.advantage = this.config.roll.advantage === advantage ? 0 : advantage; + if (this.config.roll.advantage === 0) return this.render(); - if (this.config.roll.advantage === 1 && this.config.data.rules.roll.advantageFaces) { - const faces = Number.parseInt(this.config.data.rules.roll.advantageFaces); - this.roll.advantageFaces = Number.isNaN(faces) ? this.roll.advantageFaces : faces; - } else if (this.config.roll.advantage === -1 && this.config.data.rules.roll.disadvantageFaces) { - const faces = Number.parseInt(this.config.data.rules.roll.disadvantageFaces); - this.roll.advantageFaces = Number.isNaN(faces) ? this.roll.advantageFaces : faces; - } + const defaultFaces = + this.config.roll.advantage === 1 + ? this.config.data.rules.roll.defaultAdvantageDice + : this.config.data.rules.roll.defaultDisadvantageDice; + const faces = Number.parseInt(defaultFaces); + this.roll.advantageFaces = Number.isNaN(faces) ? this.roll.advantageFaces : faces; this.render(); } diff --git a/module/applications/dialogs/damageReductionDialog.mjs b/module/applications/dialogs/damageReductionDialog.mjs index 930ca1a1..b916a5de 100644 --- a/module/applications/dialogs/damageReductionDialog.mjs +++ b/module/applications/dialogs/damageReductionDialog.mjs @@ -22,9 +22,10 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap ); const orderedArmorSources = getArmorSources(actor).filter(s => !s.disabled); - const armor = orderedArmorSources.reduce((acc, { document }) => { + const armor = orderedArmorSources.reduce((acc, { name, document }) => { const { current, max } = document.type === 'armor' ? document.system.armor : document.system.armorData; acc.push({ + name, effect: document, marks: [...Array(max).keys()].reduce((acc, _, index) => { const spent = index < current; @@ -152,14 +153,8 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap const armorSources = []; for (const source of this.marks.armor) { - const parent = source.effect.origin - ? await foundry.utils.fromUuid(source.effect.origin) - : source.effect.parent; - - const useEffectName = parent.type === 'armor' || parent instanceof Actor; - const label = useEffectName ? source.effect.name : parent.name; armorSources.push({ - label: label, + label: source.name, uuid: source.effect.uuid, marks: source.marks }); diff --git a/module/applications/dialogs/deathMove.mjs b/module/applications/dialogs/deathMove.mjs index 69ff758e..4a949b99 100644 --- a/module/applications/dialogs/deathMove.mjs +++ b/module/applications/dialogs/deathMove.mjs @@ -57,6 +57,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV let returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.avoidScar'); if (config.roll.fate.value <= this.actor.system.levelData.level.current) { + const maxHope = this.actor.system.resources.hope.max + this.actor.system.scars; const newScarAmount = this.actor.system.scars + 1; await this.actor.update({ system: { @@ -64,7 +65,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV } }); - if (newScarAmount >= this.actor.system.resources.hope.max) { + if (newScarAmount >= maxHope) { await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id); return game.i18n.format('DAGGERHEART.UI.Chat.deathMove.journeysEnd', { scars: newScarAmount }); } diff --git a/module/applications/dialogs/downtime.mjs b/module/applications/dialogs/downtime.mjs index 989e4625..367540bf 100644 --- a/module/applications/dialogs/downtime.mjs +++ b/module/applications/dialogs/downtime.mjs @@ -259,7 +259,9 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV const resetValue = increasing ? 0 : feature.system.resource.max - ? new Roll(Roll.replaceFormulaData(feature.system.resource.max, this.actor)).evaluateSync().total + ? new Roll( + Roll.replaceFormulaData(feature.system.resource.max, this.actor.getRollData()) + ).evaluateSync().total : 0; await feature.update({ 'system.resource.value': resetValue }); diff --git a/module/applications/dialogs/groupRollDialog.mjs b/module/applications/dialogs/groupRollDialog.mjs index a47dd0a8..dd504b4b 100644 --- a/module/applications/dialogs/groupRollDialog.mjs +++ b/module/applications/dialogs/groupRollDialog.mjs @@ -1,12 +1,12 @@ import { ResourceUpdateMap } from '../../data/action/baseAction.mjs'; -import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; +import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import Party from '../sheets/actors/party.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; export default class GroupRollDialog extends HandlebarsApplicationMixin(ApplicationV2) { constructor(party) { - super(); + super({ id: `GroupRollDialog-${party.id}` }); this.party = party; this.partyMembers = party.system.partyMembers @@ -35,19 +35,18 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat static DEFAULT_OPTIONS = { tag: 'form', - id: 'GroupRollDialog', classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'group-roll-dialog'], - position: { width: 550, height: 'auto' }, + position: { width: 390, height: 'auto' }, + window: { + icon: 'fa-solid fa-users' + }, actions: { toggleSelectMember: this.#toggleSelectMember, startGroupRoll: this.#startGroupRoll, makeRoll: this.#makeRoll, removeRoll: this.#removeRoll, rerollDice: this.#rerollDice, - makeLeaderRoll: this.#makeLeaderRoll, - removeLeaderRoll: this.#removeLeaderRoll, - rerollLeaderDice: this.#rerollLeaderDice, - markSuccessfull: this.#markSuccessfull, + markSuccessful: this.#markSuccessful, cancelRoll: this.#onCancelRoll, finishRoll: this.#finishRoll }, @@ -59,17 +58,21 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat id: 'initialization', template: 'systems/daggerheart/templates/dialogs/groupRollDialog/initialization.hbs' }, + main: { + id: 'main', + template: 'systems/daggerheart/templates/dialogs/groupRollDialog/main.hbs' + }, leader: { id: 'leader', - template: 'systems/daggerheart/templates/dialogs/groupRollDialog/leader.hbs' + template: 'systems/daggerheart/templates/dialogs/groupRollDialog/parts/member.hbs' }, - groupRoll: { - id: 'groupRoll', - template: 'systems/daggerheart/templates/dialogs/groupRollDialog/groupRoll.hbs' + result: { + id: 'result', + template: 'systems/daggerheart/templates/dialogs/groupRollDialog/parts/result.hbs' }, footer: { id: 'footer', - template: 'systems/daggerheart/templates/dialogs/groupRollDialog/footer.hbs' + template: 'systems/daggerheart/templates/dialogs/groupRollDialog/parts/footer.hbs' } }; @@ -89,51 +92,31 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat } _configureRenderParts(options) { - const { initialization, leader, groupRoll, footer } = super._configureRenderParts(options); - const augmentedParts = { initialization }; + const parts = super._configureRenderParts(options); for (const memberKey of Object.keys(this.party.system.groupRoll.aidingCharacters)) { - augmentedParts[memberKey] = { + parts[memberKey] = { id: memberKey, - template: 'systems/daggerheart/templates/dialogs/groupRollDialog/groupRollMember.hbs' + template: 'systems/daggerheart/templates/dialogs/groupRollDialog/parts/member.hbs' }; } - - augmentedParts.leader = leader; - augmentedParts.groupRoll = groupRoll; - augmentedParts.footer = footer; - - return augmentedParts; - } - - /**@inheritdoc */ - async _onRender(context, options) { - await super._onRender(context, options); - - if (this.element.querySelector('.team-container')) return; - - if (this.tabGroups.application !== this.constructor.PARTS.initialization.id) { - const initializationPart = this.element.querySelector('.initialization-container'); - initializationPart.insertAdjacentHTML('afterend', '
'); - initializationPart.insertAdjacentHTML( - 'afterend', - `
${game.i18n.localize('DAGGERHEART.APPLICATIONS.GroupRollSelect.aidingCharacters')}
` - ); - - const teamContainer = this.element.querySelector('.team-container'); - for (const memberContainer of this.element.querySelectorAll('.team-member-container')) - teamContainer.appendChild(memberContainer); - } + return parts; } async _prepareContext(_options) { const context = await super._prepareContext(_options); context.isGM = game.user.isGM; - context.isEditable = this.getIsEditable(); + context.isEditable = + game.user.isGM || + this.party.system.partyMembers.some(actor => { + const selected = Boolean(this.party.system.groupRoll.participants[actor.id]); + return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER); + }); context.fields = this.party.system.schema.fields.groupRoll.fields; context.data = this.party.system.groupRoll; context.traitOptions = CONFIG.DH.ACTOR.abilities; context.members = {}; + context.aidKeys = Object.keys(this.party.system.groupRoll.aidingCharacters); context.allHaveRolled = Object.keys(context.data.participants).every(key => { const data = context.data.participants[key]; return Boolean(data.rollData); @@ -145,6 +128,7 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat async _preparePartContext(partId, context, options) { const partContext = await super._preparePartContext(partId, context, options); partContext.partId = partId; + partContext.leader = this.getRollCharacterData(this.party.system.groupRoll.leader); switch (partId) { case 'initialization': @@ -162,19 +146,14 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat partContext.canStartGroupRoll = selectedMembers.length > 1 && this.leader?.memberId; partContext.openForAllPlayers = this.openForAllPlayers; break; - case 'leader': - partContext.leader = this.getRollCharacterData(this.party.system.groupRoll.leader); - break; - case 'groupRoll': + case 'result': const leader = this.party.system.groupRoll.leader; partContext.hasRolled = leader?.rollData || - Object.values(this.party.system.groupRoll?.aidingCharacters ?? {}).some( - x => x.successfull !== null - ); + Object.values(this.party.system.groupRoll?.aidingCharacters ?? {}).some(x => x.successful !== null); const { modifierTotal, modifiers } = Object.values(this.party.system.groupRoll.aidingCharacters).reduce( (acc, curr) => { - const modifier = curr.successfull === true ? 1 : curr.successfull === false ? -1 : null; + const modifier = curr.successful === true ? 1 : curr.successful === false ? -1 : null; if (modifier) { acc.modifierTotal += modifier; acc.modifiers.push(modifier); @@ -200,7 +179,7 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat case 'footer': partContext.canFinishRoll = Boolean(this.party.system.groupRoll.leader?.rollData) && - Object.values(this.party.system.groupRoll.aidingCharacters).every(x => x.successfull !== null); + Object.values(this.party.system.groupRoll.aidingCharacters).every(x => x.successful !== null); break; } @@ -216,20 +195,42 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat if (!data) return {}; const actor = game.actors.get(data.id); + const isLeader = data === this.party.system.groupRoll.leader; + + const roll = data.roll; + const withTypeSuffix = !roll ? null : roll.isCritical ? 'criticalShort' : roll.withHope ? 'hope' : 'fear'; + const thing = withTypeSuffix ? _loc(`DAGGERHEART.GENERAL.${withTypeSuffix}`) : null; return { ...data, + type: isLeader ? 'leader' : 'aid', + basePath: isLeader ? 'system.groupRoll.leader' : `system.groupRoll.aidingCharacters.${data.id}`, + rollChoiceLabel: _loc(CONFIG.DH.ACTOR.abilities[data.rollChoice]?.label), roll: data.roll, - isEditable: actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER), + isEditable: actor?.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER), key: partId, readyToRoll: Boolean(data.rollChoice), - hasRolled: Boolean(data.rollData) + hasRolled: Boolean(data.rollData), + modifier: data.successful ? 1 : data.successful === false ? -1 : 0, + withLabelShort: thing ? _loc('DAGGERHEART.GENERAL.withThing', { thing }) : null }; } + #getCharacterDataById(id) { + if (!id) return null; + + const groupRoll = this.party.system.groupRoll; + if (id === 'leader' || id === groupRoll.leader?.id) { + return { data: groupRoll.leader, basePath: 'system.groupRoll.leader' }; + } else if (id in groupRoll.aidingCharacters) { + return { data: groupRoll.aidingCharacters[id], basePath: `system.groupRoll.aidingCharacters.${id}` }; + } + + return null; + } + static async updateData(event, _, formData) { const partyData = foundry.utils.expandObject(formData.object); - this.updatePartyData(partyData, this.getUpdatingParts(event.target)); } @@ -246,7 +247,7 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat }); }; - await emitAsGM( + await emitGMUpdate( GMUpdateEvent.UpdateDocument, gmUpdate, update, @@ -256,26 +257,19 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat } getUpdatingParts(target) { - const { initialization, leader, groupRoll, footer } = this.constructor.PARTS; + const { initialization, leader, result, footer } = this.constructor.PARTS; const isInitialization = this.tabGroups.application === initialization.id; - const updatingMember = target.closest('.team-member-container')?.dataset?.memberKey; - const updatingLeader = target.closest('.main-character-outer-container'); + const updatingMember = target.closest('.member-roll-container.aid')?.dataset?.memberKey; + const updatingLeader = target.closest('.member-roll-container.leader'); return [ ...(isInitialization ? [initialization.id] : []), ...(updatingMember ? [updatingMember] : []), ...(updatingLeader ? [leader.id] : []), - ...(!isInitialization ? [groupRoll.id, footer.id] : []) + ...(!isInitialization ? [result.id, footer.id] : []) ]; } - getIsEditable() { - return this.party.system.partyMembers.some(actor => { - const selected = Boolean(this.party.system.groupRoll.participants[actor.id]); - return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER); - }); - } - groupRollRefresh = ({ refreshType, action, parts }) => { if (refreshType !== RefreshType.GroupRoll) return; @@ -304,6 +298,9 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat static #toggleSelectMember(_, button) { const member = this.partyMembers.find(x => x.id === button.dataset.id); member.selected = !member.selected; + if (this.leader?.memberId === member.id) { + this.leader = null; + } this.render(); } @@ -343,11 +340,14 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat } //#endregion - async makeRoll(button, characterData, path) { - const actor = game.actors.find(x => x.id === characterData.id); + /** @this GroupRollDialog */ + static async #makeRoll(_event, button) { + const member = button.closest('[data-member-key]').dataset.memberKey; + const { data, basePath } = this.#getCharacterDataById(member); + const actor = game.actors.find(x => x.id === data.id); if (!actor) return; - const result = await actor.rollTrait(characterData.rollChoice, { + const result = await actor.rollTrait(data.rollChoice, { skips: { createMessage: true, resources: true, @@ -356,53 +356,38 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat }); if (!result) return; - if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); const rollData = result.messageRoll.toJSON(); delete rollData.options.messageRoll; this.updatePartyData( { - [path]: rollData + [basePath]: { rollData, successful: null } }, this.getUpdatingParts(button) ); } - static async #makeRoll(_event, button) { - const { member } = button.dataset; - const character = this.party.system.groupRoll.aidingCharacters[member]; - this.makeRoll(button, character, `system.groupRoll.aidingCharacters.${member}.rollData`); - } - - static async #makeLeaderRoll(_event, button) { - const character = this.party.system.groupRoll.leader; - this.makeRoll(button, character, 'system.groupRoll.leader.rollData'); - } - - async removeRoll(button, path) { + /** @this GroupRollDialog */ + static async #removeRoll(_event, button) { + const member = button.closest('[data-member-key]').dataset.memberKey; + const { basePath } = this.#getCharacterDataById(member); this.updatePartyData( { - [path]: { + [basePath]: { rollData: null, rollChoice: null, selected: false, - successfull: null + successful: null } }, this.getUpdatingParts(button) ); } - static async #removeRoll(_event, button) { - this.removeRoll(button, `system.groupRoll.aidingCharacters.${button.dataset.member}`); - } - - static async #removeLeaderRoll(_event, button) { - this.removeRoll(button, 'system.groupRoll.leader'); - } - - async rerollDice(button, data, path) { + /** @this GroupRollDialog */ + static async #rerollDice(_, button) { const { diceType } = button.dataset; + const { data, basePath } = this.#getCharacterDataById(button.dataset.member); const dieIndex = diceType === 'hope' ? 0 : diceType === 'fear' ? 1 : 2; const newRoll = game.system.api.dice.DualityRoll.fromData(data.rollData); @@ -416,31 +401,19 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat const rollData = newRoll.toJSON(); this.updatePartyData( { - [path]: rollData + [`${basePath}.rollData`]: rollData }, this.getUpdatingParts(button) ); } - static async #rerollDice(_, button) { - const { member } = button.dataset; - this.rerollDice( - button, - this.party.system.groupRoll.aidingCharacters[member], - `system.groupRoll.aidingCharacters.${member}.rollData` - ); - } - - static async #rerollLeaderDice(_, button) { - this.rerollDice(button, this.party.system.groupRoll.leader, `system.groupRoll.leader.rollData`); - } - - static #markSuccessfull(_event, button) { - const previousValue = this.party.system.groupRoll.aidingCharacters[button.dataset.member].successfull; - const newValue = Boolean(button.dataset.successfull === 'true'); + static #markSuccessful(_event, button) { + const memberKey = button.closest('[data-member-key]').dataset.memberKey; + const previousValue = this.party.system.groupRoll.aidingCharacters[memberKey].successful; + const newValue = Boolean(button.dataset.success === 'true'); this.updatePartyData( { - [`system.groupRoll.aidingCharacters.${button.dataset.member}.successfull`]: + [`system.groupRoll.aidingCharacters.${memberKey}.successful`]: previousValue === newValue ? null : newValue }, this.getUpdatingParts(button) @@ -484,7 +457,7 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat static async #finishRoll() { const totalRoll = this.party.system.groupRoll.leader.roll; for (const character of Object.values(this.party.system.groupRoll.aidingCharacters)) { - totalRoll.terms.push(new foundry.dice.terms.OperatorTerm({ operator: character.successfull ? '+' : '-' })); + totalRoll.terms.push(new foundry.dice.terms.OperatorTerm({ operator: character.successful ? '+' : '-' })); totalRoll.terms.push(new foundry.dice.terms.NumericTerm({ number: 1 })); } diff --git a/module/applications/dialogs/rerollDamageDialog.mjs b/module/applications/dialogs/rerollDamageDialog.mjs deleted file mode 100644 index b821bd24..00000000 --- a/module/applications/dialogs/rerollDamageDialog.mjs +++ /dev/null @@ -1,280 +0,0 @@ -const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; - -export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(message, options = {}) { - super(options); - - this.message = message; - this.damage = Object.keys(message.system.damage).reduce((acc, typeKey) => { - const type = message.system.damage[typeKey]; - acc[typeKey] = Object.keys(type.parts).reduce((acc, partKey) => { - const part = type.parts[partKey]; - acc[partKey] = Object.keys(part.dice).reduce((acc, diceKey) => { - const dice = part.dice[diceKey]; - const activeResults = dice.results.filter(x => x.active); - acc[diceKey] = { - dice: dice.dice, - selectedResults: activeResults.length, - maxSelected: activeResults.length, - results: activeResults.map(x => ({ ...x, selected: true })) - }; - - return acc; - }, {}); - - return acc; - }, {}); - - return acc; - }, {}); - } - - static DEFAULT_OPTIONS = { - id: 'reroll-dialog', - classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'reroll-dialog'], - window: { - icon: 'fa-solid fa-dice' - }, - actions: { - toggleResult: RerollDamageDialog.#toggleResult, - selectRoll: RerollDamageDialog.#selectRoll, - doReroll: RerollDamageDialog.#doReroll, - save: RerollDamageDialog.#save - } - }; - - /** @override */ - static PARTS = { - main: { - id: 'main', - template: 'systems/daggerheart/templates/dialogs/rerollDialog/damage/main.hbs' - }, - footer: { - id: 'footer', - template: 'systems/daggerheart/templates/dialogs/rerollDialog/footer.hbs' - } - }; - - get title() { - return game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.damageTitle'); - } - - _attachPartListeners(partId, htmlElement, options) { - super._attachPartListeners(partId, htmlElement, options); - - htmlElement.querySelectorAll('.to-reroll-input').forEach(element => { - element.addEventListener('change', this.toggleDice.bind(this)); - }); - } - - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - context.damage = this.damage; - context.disabledReroll = !this.getRerollDice().length; - context.saveDisabled = !this.isSelectionDone(); - - return context; - } - - static async #save() { - const update = { - 'system.damage': Object.keys(this.damage).reduce((acc, typeKey) => { - const type = this.damage[typeKey]; - let typeTotal = 0; - const messageType = this.message.system.damage[typeKey]; - const parts = Object.keys(type).map(partKey => { - const part = type[partKey]; - const messagePart = messageType.parts[partKey]; - let partTotal = messagePart.modifierTotal; - const dice = Object.keys(part).map(diceKey => { - const dice = part[diceKey]; - const total = dice.results.reduce((acc, result) => { - if (result.active) acc += result.result; - return acc; - }, 0); - partTotal += total; - const messageDice = messagePart.dice[diceKey]; - return { - ...messageDice, - total: total, - results: dice.results.map(x => ({ - ...x, - hasRerolls: dice.results.length > 1 - })) - }; - }); - - typeTotal += partTotal; - return { - ...messagePart, - total: partTotal, - dice: dice - }; - }); - - acc[typeKey] = { - ...messageType, - total: typeTotal, - parts: parts - }; - - return acc; - }, {}) - }; - - await this.message.update(update); - await this.close(); - } - - getRerollDice() { - const rerollDice = []; - Object.keys(this.damage).forEach(typeKey => { - const type = this.damage[typeKey]; - Object.keys(type).forEach(partKey => { - const part = type[partKey]; - Object.keys(part).forEach(diceKey => { - const dice = part[diceKey]; - Object.keys(dice.results).forEach(resultKey => { - const result = dice.results[resultKey]; - if (result.toReroll) { - rerollDice.push({ - ...result, - dice: dice.dice, - type: typeKey, - part: partKey, - dice: diceKey, - result: resultKey - }); - } - }); - }); - }); - }); - - return rerollDice; - } - - isSelectionDone() { - const diceFinishedData = []; - Object.keys(this.damage).forEach(typeKey => { - const type = this.damage[typeKey]; - Object.keys(type).forEach(partKey => { - const part = type[partKey]; - Object.keys(part).forEach(diceKey => { - const dice = part[diceKey]; - const selected = dice.results.reduce((acc, result) => acc + (result.active ? 1 : 0), 0); - diceFinishedData.push(selected === dice.maxSelected); - }); - }); - }); - - return diceFinishedData.every(x => x); - } - - toggleDice(event) { - const target = event.target; - const { type, part, dice } = target.dataset; - const toggleDice = this.damage[type][part][dice]; - - const existingDiceRerolls = this.getRerollDice().filter( - x => x.type === type && x.part === part && x.dice === dice - ); - - const allRerolled = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length; - - toggleDice.toReroll = !allRerolled; - toggleDice.results.forEach(result => { - if (result.active) { - result.toReroll = !allRerolled; - } - }); - - this.render(); - } - - static #toggleResult(event) { - event.stopPropagation(); - - const target = event.target.closest('.to-reroll-result'); - const { type, part, dice, result } = target.dataset; - const toggleDice = this.damage[type][part][dice]; - const toggleResult = toggleDice.results[result]; - toggleResult.toReroll = !toggleResult.toReroll; - - const existingDiceRerolls = this.getRerollDice().filter( - x => x.type === type && x.part === part && x.dice === dice - ); - - const allToReroll = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length; - toggleDice.toReroll = allToReroll; - - this.render(); - } - - static async #selectRoll(_, button) { - const { type, part, dice, result } = button.dataset; - - const diceVal = this.damage[type][part][dice]; - const diceResult = diceVal.results[result]; - if (!diceResult.active && diceVal.results.filter(x => x.active).length === diceVal.maxSelected) { - return ui.notifications.warn( - game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.deselectDiceNotification') - ); - } - - if (diceResult.active) { - diceVal.toReroll = false; - diceResult.toReroll = false; - } - - diceVal.selectedResults += diceResult.active ? -1 : 1; - diceResult.active = !diceResult.active; - - this.render(); - } - - static async #doReroll() { - const toReroll = this.getRerollDice().map(x => { - const { type, part, dice, result } = x; - const diceData = this.damage[type][part][dice].results[result]; - return { - ...diceData, - dice: this.damage[type][part][dice].dice, - typeKey: type, - partKey: part, - diceKey: dice, - resultsIndex: result - }; - }); - - const roll = await new Roll(toReroll.map(x => `1${x.dice}`).join(' + ')).evaluate(); - - if (game.modules.get('dice-so-nice')?.active) { - const diceSoNiceRoll = { - _evaluated: true, - dice: roll.dice, - options: { appearance: {} } - }; - - await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true); - } - - toReroll.forEach((data, index) => { - const { typeKey, partKey, diceKey, resultsIndex } = data; - const rerolledDice = roll.dice[index]; - - const dice = this.damage[typeKey][partKey][diceKey]; - dice.toReroll = false; - dice.results[resultsIndex].active = false; - dice.results[resultsIndex].discarded = true; - dice.results[resultsIndex].toReroll = false; - dice.results.splice(dice.results.length, 0, { - ...rerolledDice.results[0], - toReroll: false, - selected: true - }); - }); - - this.render(); - } -} diff --git a/module/applications/dialogs/rerollDialog.mjs b/module/applications/dialogs/rerollDialog.mjs deleted file mode 100644 index cae4e53a..00000000 --- a/module/applications/dialogs/rerollDialog.mjs +++ /dev/null @@ -1,279 +0,0 @@ -const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; - -export default class RerollDialog extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(message, options = {}) { - super(options); - - this.message = message; - this.damage = Object.keys(message.system.damage).reduce((acc, typeKey) => { - const type = message.system.damage[typeKey]; - acc[typeKey] = Object.keys(type.parts).reduce((acc, partKey) => { - const part = type.parts[partKey]; - acc[partKey] = Object.keys(part.dice).reduce((acc, diceKey) => { - const dice = part.dice[diceKey]; - const activeResults = dice.results.filter(x => x.active); - acc[diceKey] = { - dice: dice.dice, - selectedResults: activeResults.length, - maxSelected: activeResults.length, - results: activeResults.map(x => ({ ...x, selected: true })) - }; - - return acc; - }, {}); - - return acc; - }, {}); - - return acc; - }, {}); - } - - static DEFAULT_OPTIONS = { - id: 'reroll-dialog', - classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'reroll-dialog'], - window: { - icon: 'fa-solid fa-dice' - }, - actions: { - toggleResult: RerollDialog.#toggleResult, - selectRoll: RerollDialog.#selectRoll, - doReroll: RerollDialog.#doReroll, - save: RerollDialog.#save - } - }; - - /** @override */ - static PARTS = { - main: { - id: 'main', - template: 'systems/daggerheart/templates/dialogs/rerollDialog/main.hbs' - }, - footer: { - id: 'footer', - template: 'systems/daggerheart/templates/dialogs/rerollDialog/footer.hbs' - } - }; - - get title() { - return game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.title'); - } - - _attachPartListeners(partId, htmlElement, options) { - super._attachPartListeners(partId, htmlElement, options); - - htmlElement.querySelectorAll('.to-reroll-input').forEach(element => { - element.addEventListener('change', this.toggleDice.bind(this)); - }); - } - - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - context.damage = this.damage; - context.disabledReroll = !this.getRerollDice().length; - context.saveDisabled = !this.isSelectionDone(); - - return context; - } - - static async #save() { - const update = { - 'system.damage': Object.keys(this.damage).reduce((acc, typeKey) => { - const type = this.damage[typeKey]; - let typeTotal = 0; - const messageType = this.message.system.damage[typeKey]; - const parts = Object.keys(type).map(partKey => { - const part = type[partKey]; - const messagePart = messageType.parts[partKey]; - let partTotal = messagePart.modifierTotal; - const dice = Object.keys(part).map(diceKey => { - const dice = part[diceKey]; - const total = dice.results.reduce((acc, result) => { - if (result.active) acc += result.result; - return acc; - }, 0); - partTotal += total; - const messageDice = messagePart.dice[diceKey]; - return { - ...messageDice, - total: total, - results: dice.results.map(x => ({ - ...x, - hasRerolls: dice.results.length > 1 - })) - }; - }); - - typeTotal += partTotal; - return { - ...messagePart, - total: partTotal, - dice: dice - }; - }); - - acc[typeKey] = { - ...messageType, - total: typeTotal, - parts: parts - }; - - return acc; - }, {}) - }; - await this.message.update(update); - await this.close(); - } - - getRerollDice() { - const rerollDice = []; - Object.keys(this.damage).forEach(typeKey => { - const type = this.damage[typeKey]; - Object.keys(type).forEach(partKey => { - const part = type[partKey]; - Object.keys(part).forEach(diceKey => { - const dice = part[diceKey]; - Object.keys(dice.results).forEach(resultKey => { - const result = dice.results[resultKey]; - if (result.toReroll) { - rerollDice.push({ - ...result, - dice: dice.dice, - type: typeKey, - part: partKey, - dice: diceKey, - result: resultKey - }); - } - }); - }); - }); - }); - - return rerollDice; - } - - isSelectionDone() { - const diceFinishedData = []; - Object.keys(this.damage).forEach(typeKey => { - const type = this.damage[typeKey]; - Object.keys(type).forEach(partKey => { - const part = type[partKey]; - Object.keys(part).forEach(diceKey => { - const dice = part[diceKey]; - const selected = dice.results.reduce((acc, result) => acc + (result.active ? 1 : 0), 0); - diceFinishedData.push(selected === dice.maxSelected); - }); - }); - }); - - return diceFinishedData.every(x => x); - } - - toggleDice(event) { - const target = event.target; - const { type, part, dice } = target.dataset; - const toggleDice = this.damage[type][part][dice]; - - const existingDiceRerolls = this.getRerollDice().filter( - x => x.type === type && x.part === part && x.dice === dice - ); - - const allRerolled = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length; - - toggleDice.toReroll = !allRerolled; - toggleDice.results.forEach(result => { - if (result.active) { - result.toReroll = !allRerolled; - } - }); - - this.render(); - } - - static #toggleResult(event) { - event.stopPropagation(); - - const target = event.target.closest('.to-reroll-result'); - const { type, part, dice, result } = target.dataset; - const toggleDice = this.damage[type][part][dice]; - const toggleResult = toggleDice.results[result]; - toggleResult.toReroll = !toggleResult.toReroll; - - const existingDiceRerolls = this.getRerollDice().filter( - x => x.type === type && x.part === part && x.dice === dice - ); - - const allToReroll = existingDiceRerolls.length === toggleDice.results.length; - toggleDice.toReroll = allToReroll; - - this.render(); - } - - static async #selectRoll(_, button) { - const { type, part, dice, result } = button.dataset; - - const diceVal = this.damage[type][part][dice]; - const diceResult = diceVal.results[result]; - if (!diceResult.active && diceVal.results.filter(x => x.active).length === diceVal.maxSelected) { - return ui.notifications.warn( - game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.deselectDiceNotification') - ); - } - - if (diceResult.active) { - diceVal.toReroll = false; - diceResult.toReroll = false; - } - - diceVal.selectedResults += diceResult.active ? -1 : 1; - diceResult.active = !diceResult.active; - - this.render(); - } - - static async #doReroll() { - const toReroll = this.getRerollDice().map(x => { - const { type, part, dice, result } = x; - const diceData = this.damage[type][part][dice].results[result]; - return { - ...diceData, - dice: this.damage[type][part][dice].dice, - typeKey: type, - partKey: part, - diceKey: dice, - resultsIndex: result - }; - }); - - const roll = await new Roll(toReroll.map(x => `1${x.dice}`).join(' + ')).evaluate(); - - if (game.modules.get('dice-so-nice')?.active) { - const diceSoNiceRoll = { - _evaluated: true, - dice: roll.dice, - options: { appearance: {} } - }; - - await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true); - } - - toReroll.forEach((data, index) => { - const { typeKey, partKey, diceKey, resultsIndex } = data; - const rerolledDice = roll.dice[index]; - - const dice = this.damage[typeKey][partKey][diceKey]; - dice.toReroll = false; - dice.results[resultsIndex].active = false; - dice.results[resultsIndex].discarded = true; - dice.results[resultsIndex].toReroll = false; - dice.results.splice(dice.results.length, 0, { - ...rerolledDice.results[0], - toReroll: false, - selected: true - }); - }); - - this.render(); - } -} diff --git a/module/applications/dialogs/resourceDiceDialog.mjs b/module/applications/dialogs/resourceDiceDialog.mjs index 32e1e5d8..8394538c 100644 --- a/module/applications/dialogs/resourceDiceDialog.mjs +++ b/module/applications/dialogs/resourceDiceDialog.mjs @@ -1,4 +1,4 @@ -import { itemAbleRollParse } from '../../helpers/utils.mjs'; +import { itemAbleRollParse, triggerChatRollFx } from '../../helpers/utils.mjs'; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; @@ -69,7 +69,7 @@ export default class ResourceDiceDialog extends HandlebarsApplicationMixin(Appli const max = itemAbleRollParse(this.item.system.resource.max, this.actor, this.item); const diceFormula = `${max}${this.item.system.resource.dieFaces}`; const roll = await new Roll(diceFormula).evaluate(); - if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true); + await triggerChatRollFx([roll]); this.rollValues = roll.terms[0].results.map(x => ({ value: x.result, used: false })); this.resetUsed = true; diff --git a/module/applications/dialogs/tagTeamDialog.mjs b/module/applications/dialogs/tagTeamDialog.mjs index 026c4bc0..3dc6b0fc 100644 --- a/module/applications/dialogs/tagTeamDialog.mjs +++ b/module/applications/dialogs/tagTeamDialog.mjs @@ -1,13 +1,13 @@ import { MemberData } from '../../data/tagTeamData.mjs'; import { getCritDamageBonus } from '../../helpers/utils.mjs'; -import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; +import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import Party from '../sheets/actors/party.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; export default class TagTeamDialog extends HandlebarsApplicationMixin(ApplicationV2) { constructor(party) { - super(); + super({ id: `TagTeamDialog-${party.id}` }); this.party = party; this.partyMembers = party.system.partyMembers @@ -36,9 +36,11 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio static DEFAULT_OPTIONS = { tag: 'form', - id: 'TagTeamDialog', classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'tag-team-dialog'], position: { width: 550, height: 'auto' }, + window: { + icon: 'fa-solid fa-user-group' + }, actions: { toggleSelectMember: TagTeamDialog.#toggleSelectMember, startTagTeamRoll: TagTeamDialog.#startTagTeamRoll, @@ -60,13 +62,17 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio id: 'initialization', template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/initialization.hbs' }, + tagTeamRoll: { + id: 'tagTeamRoll', + template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs' + }, rollSelection: { id: 'rollSelection', template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/rollSelection.hbs' }, - tagTeamRoll: { - id: 'tagTeamRoll', - template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs' + result: { + id: 'result', + template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/result.hbs' } }; @@ -97,41 +103,25 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio } _configureRenderParts(options) { - const { initialization, rollSelection, tagTeamRoll } = super._configureRenderParts(options); - const augmentedParts = { initialization }; + const parts = super._configureRenderParts(options); for (const memberKey of Object.keys(this.party.system.tagTeam.members)) { - augmentedParts[memberKey] = { + parts[memberKey] = { id: memberKey, template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/tagTeamMember.hbs' }; } - augmentedParts.rollSelection = rollSelection; - augmentedParts.tagTeamRoll = tagTeamRoll; - return augmentedParts; - } - - /**@inheritdoc */ - async _onRender(context, options) { - await super._onRender(context, options); - - // if (this.element.querySelector('.roll-selection')) { - // for (const element of this.element.querySelectorAll('.team-member-container')) { - // element.classList.add('select-padding'); - // } - // } - - if (this.element.querySelector('.team-container')) return; - const initializationPart = this.element.querySelector('.initialization-container'); - initializationPart.insertAdjacentHTML('afterend', '
'); - const teamContainer = this.element.querySelector('.team-container'); - for (const memberContainer of this.element.querySelectorAll('.team-member-container')) - teamContainer.appendChild(memberContainer); + return parts; } async _prepareContext(_options) { const context = await super._prepareContext(_options); - context.isEditable = this.getIsEditable(); + context.isEditable = + game.user.isGM || + this.party.system.partyMembers.some(actor => { + const selected = Boolean(this.party.system.tagTeam.members[actor.id]); + return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER); + }); context.fields = this.party.system.schema.fields.tagTeam.fields; context.data = this.party.system.tagTeam; context.rollTypes = CONFIG.DH.GENERAL.tagTeamRollTypes; @@ -167,6 +157,9 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio partContext.initiatorDisabled = !selectedMembers.length; partContext.openForAllPlayers = this.openForAllPlayers; + break; + case 'tagTeamRoll': + partContext.memberKeys = Object.keys(this.party.system.tagTeam.members); break; case 'rollSelection': partContext.members = Object.keys(this.party.system.tagTeam.members).reduce((acc, key) => { @@ -175,7 +168,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio return acc; }, {}); break; - case 'tagTeamRoll': + case 'result': const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected); const critSelected = !selectedRoll ? undefined @@ -191,59 +184,58 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio } if (Object.keys(this.party.system.tagTeam.members).includes(partId)) { - const data = this.party.system.tagTeam.members[partId]; - const actor = game.actors.get(partId); - - const rollOptions = []; - const damageRollOptions = []; - for (const item of actor.items) { - if (item.system.metadata.hasActions) { - const actions = [...item.system.actions, ...(item.system.attack ? [item.system.attack] : [])]; - for (const action of actions) { - if (action.hasRoll) { - const actionItem = { - value: action.uuid, - label: action.name, - group: item.name, - baseAction: action.baseAction - }; - - if (action.hasDamage) damageRollOptions.push(actionItem); - else rollOptions.push(actionItem); - } - } - } - } - - const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected); - const critSelected = !selectedRoll - ? undefined - : (selectedRoll?.rollData?.options?.roll?.isCritical ?? false); - - const damage = data.rollData?.options?.damage; - partContext.hasDamage |= Boolean(damage); - const critHitPointsDamage = await this.getCriticalDamage(damage); - - partContext.members[partId] = { - ...data, - roll: data.roll, - isEditable: actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER), - key: partId, - readyToRoll: Boolean(data.rollChoice), - hasRolled: Boolean(data.rollData), - rollOptions, - damageRollOptions, - damage: damage, - critDamage: critHitPointsDamage, - useCritDamage: critSelected || (critSelected === undefined && data.rollData?.options?.roll?.isCritical) - }; + const data = await this.#prepareMemberContext(partId); + partContext.hasDamage |= Boolean(data?.damage); + partContext.members[partId] = data; } return partContext; } + async #prepareMemberContext(partId) { + const data = this.party.system.tagTeam.members[partId] ?? {}; + const actor = game.actors.get(partId); + if (!actor) console.error(`Failed to get actor ${partId}`); + + const rollOptions = []; + const damageRollOptions = []; + for (const item of actor?.items ?? []) { + if (!item.system.metadata.hasActions) continue; + const actions = [...item.system.actions, ...(item.system.attack ? [item.system.attack] : [])]; + for (const action of actions) { + if (action.hasRoll) { + const collection = action.hasDamage ? damageRollOptions : rollOptions; + collection.push({ + value: action.uuid, + label: action.name, + group: item.name, + baseAction: action.baseAction + }); + } + } + } + + const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected); + const critSelected = !selectedRoll ? undefined : (selectedRoll?.rollData?.options?.roll?.isCritical ?? false); + const damage = data.rollData?.options?.damage; + + return { + ...data, + roll: data.roll, + isEditable: actor?.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER), + key: partId, + readyToRoll: Boolean(data.rollChoice), + hasRolled: Boolean(data.rollData), + rollOptions, + damageRollOptions, + damage: damage, + critDamage: await this.getCriticalDamage(damage), + useCritDamage: critSelected || (critSelected === undefined && data.rollData?.options?.roll?.isCritical) + }; + } + getUpdatingParts(target) { - const { initialization, rollSelection, tagTeamRoll } = this.constructor.PARTS; + const { initialization, rollSelection, result } = this.constructor.PARTS; const isInitialization = this.tabGroups.application === initialization.id; const updatingMember = target.closest('.team-member-container')?.dataset?.memberKey; @@ -251,7 +243,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio ...(isInitialization ? [initialization.id] : []), ...(updatingMember ? [updatingMember] : []), ...(!isInitialization ? [rollSelection.id] : []), - ...(!isInitialization ? [tagTeamRoll.id] : []) + ...(!isInitialization ? [result.id] : []) ]; } @@ -274,7 +266,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio }); }; - await emitAsGM( + await emitGMUpdate( GMUpdateEvent.UpdateDocument, gmUpdate, update, @@ -285,13 +277,6 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio ); } - getIsEditable() { - return this.party.system.partyMembers.some(actor => { - const selected = Boolean(this.party.system.tagTeam.members[actor.id]); - return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER); - }); - } - tagTeamRefresh = ({ refreshType, action, parts }) => { if (refreshType !== RefreshType.TagTeamRoll) return; @@ -446,8 +431,6 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio if (!result) return; - if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); - const rollData = result.messageRoll.toJSON(); delete rollData.options.messageRoll; this.updatePartyData( @@ -663,42 +646,50 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio } async getJoinedRoll({ overrideIsCritical, displayVersion } = {}) { - const memberValues = Object.values(this.party.system.tagTeam.members); - const selectedRoll = memberValues.find(x => x.selected); - let baseMainRoll = selectedRoll ?? memberValues[0]; - let baseSecondaryRoll = selectedRoll - ? memberValues.find(x => !x.selected) - : memberValues.length > 1 - ? memberValues[1] - : null; + try { + const memberValues = Object.values(this.party.system.tagTeam.members); + const selectedRoll = memberValues.find(x => x.selected); + const baseMainRoll = selectedRoll ?? memberValues[0]; + const baseSecondaryRoll = selectedRoll + ? memberValues.find(x => !x.selected) + : memberValues.length > 1 + ? memberValues[1] + : null; - if (!baseMainRoll?.rollData || !baseSecondaryRoll) return null; + if (!baseMainRoll?.rollData || !baseSecondaryRoll) return null; - const mainRoll = new MemberData(baseMainRoll.toObject()); - const secondaryRollData = new MemberData(baseSecondaryRoll.toObject()).rollData; - const systemData = mainRoll.rollData.options; - const isCritical = overrideIsCritical ?? systemData.roll.isCritical; - if (isCritical) systemData.damage = await this.getCriticalDamage(systemData.damage); + const mainRoll = new MemberData(baseMainRoll.toObject()); + const secondaryRollData = new MemberData(baseSecondaryRoll.toObject()).rollData; + const systemData = mainRoll.rollData.options; + const isCritical = overrideIsCritical ?? systemData.roll.isCritical; + if (isCritical) systemData.damage = await this.getCriticalDamage(systemData.damage); - if (secondaryRollData?.options.hasDamage) { - const secondaryDamage = (displayVersion ? overrideIsCritical : isCritical) - ? await this.getCriticalDamage(secondaryRollData.options.damage) - : secondaryRollData.options.damage; - if (systemData.damage) { - for (const key in secondaryDamage) { - const damage = secondaryDamage[key]; - systemData.damage[key].formula = [systemData.damage[key].formula, damage.formula] - .filter(x => x) - .join(' + '); - systemData.damage[key].total += damage.total; - systemData.damage[key].parts.push(...damage.parts); + if (secondaryRollData?.options.hasDamage) { + const secondaryDamage = (displayVersion ? overrideIsCritical : isCritical) + ? await this.getCriticalDamage(secondaryRollData.options.damage) + : secondaryRollData.options.damage; + if (systemData.damage) { + for (const [key, damage] of Object.entries(secondaryDamage ?? {})) { + if (key in systemData.damage) { + systemData.damage[key].formula = [systemData.damage[key]?.formula, damage.formula] + .filter(x => x) + .join(' + '); + systemData.damage[key].total += damage.total; + systemData.damage[key].parts.push(...damage.parts); + } else { + systemData.damage[key] = damage; + } + } + } else { + systemData.damage = secondaryDamage; } - } else { - systemData.damage = secondaryDamage; } - } - return mainRoll; + return mainRoll; + } catch (err) { + console.error(err); + return null; + } } static async #onCancelRoll(_event, _button, options = { confirm: true }) { diff --git a/module/applications/hud/tokenHUD.mjs b/module/applications/hud/tokenHUD.mjs index 943f3506..671b01a1 100644 --- a/module/applications/hud/tokenHUD.mjs +++ b/module/applications/hud/tokenHUD.mjs @@ -124,7 +124,9 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { const animationDuration = 500; const scene = game.scenes.get(game.user.viewedScene); /* getDependentTokens returns already removed tokens with id = null. Need to filter that until it's potentially fixed from Foundry */ - const activeTokens = actors.flatMap(member => member.getDependentTokens({ scenes: scene }).filter(x => x._id)); + const activeTokens = actors.flatMap(member => + member.getDependentTokens({ scenes: scene }).filter(x => x._id && !x._destroyed) + ); const { x: actorX, y: actorY } = this.document; if (activeTokens.length > 0) { for (let token of activeTokens) { diff --git a/module/applications/levelup/characterLevelup.mjs b/module/applications/levelup/characterLevelup.mjs index b2a09059..dfd2b3de 100644 --- a/module/applications/levelup/characterLevelup.mjs +++ b/module/applications/levelup/characterLevelup.mjs @@ -154,6 +154,7 @@ export default class DhCharacterLevelUp extends LevelUpBase { if (multiclasses?.[0]) { const data = multiclasses[0]; const multiclass = data.data.length > 0 ? await foundry.utils.fromUuid(data.data[0]) : {}; + const subclasses = (await multiclass?.system?.fetchSubclasses()) ?? []; context.multiclass = { ...data, @@ -173,13 +174,12 @@ export default class DhCharacterLevelUp extends LevelUpBase { alreadySelected }; }) ?? [], - subclasses: - multiclass?.system?.subclasses.map(subclass => ({ - ...subclass, - uuid: subclass.uuid, - selected: data.secondaryData.subclass === subclass.uuid, - disabled: data.secondaryData.subclass && data.secondaryData.subclass !== subclass.uuid - })) ?? [], + subclasses: subclasses.map(subclass => ({ + ...subclass, + uuid: subclass.uuid, + selected: data.secondaryData.subclass === subclass.uuid, + disabled: data.secondaryData.subclass && data.secondaryData.subclass !== subclass.uuid + })), compendium: 'classes', limit: 1 }; diff --git a/module/applications/levelup/levelup.mjs b/module/applications/levelup/levelup.mjs index 4a9afcac..939e8d31 100644 --- a/module/applications/levelup/levelup.mjs +++ b/module/applications/levelup/levelup.mjs @@ -358,14 +358,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, diff --git a/module/applications/scene/sceneConfigSettings.mjs b/module/applications/scene/sceneConfigSettings.mjs index ce312a45..0a999506 100644 --- a/module/applications/scene/sceneConfigSettings.mjs +++ b/module/applications/scene/sceneConfigSettings.mjs @@ -120,12 +120,6 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S foundry.utils.fromUuidSync(x) ); - for (const key of Object.keys(this.document._source.flags.daggerheart?.sceneEnvironments ?? {})) { - if (!submitData.flags.daggerheart.sceneEnvironments[key]) { - submitData.flags.daggerheart.sceneEnvironments[key] = _del; - } - } - super._processSubmitData(event, form, submitData, options); } } diff --git a/module/applications/settings/homebrewSettings.mjs b/module/applications/settings/homebrewSettings.mjs index 9f0b22c4..09bb00f2 100644 --- a/module/applications/settings/homebrewSettings.mjs +++ b/module/applications/settings/homebrewSettings.mjs @@ -1,6 +1,5 @@ import { DhHomebrew } from '../../data/settings/_module.mjs'; import { Resource } from '../../data/settings/Homebrew.mjs'; -import { slugify } from '../../helpers/utils.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -112,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; @@ -403,12 +402,12 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli const domainName = button.form.elements.domainName.value; if (!domainName) return; - const newSlug = slugify(domainName); + const newSlug = domainName.slugify(); const existingDomains = [ ...Object.values(this.settings.domains), ...Object.values(CONFIG.DH.DOMAIN.domains) ]; - if (existingDomains.find(x => slugify(game.i18n.localize(x.label)) === newSlug)) { + if (existingDomains.find(x => x.id === newSlug)) { ui.notifications.warn(game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.domains.duplicateDomain')); return; } @@ -529,7 +528,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli const identifier = button.form.elements.identifier.value; if (!identifier) return; - const sluggedIdentifier = slugify(identifier); + const sluggedIdentifier = identifier.slugify(); await this.settings.updateSource({ [`resources.${actorType}.resources.${sluggedIdentifier}`]: Resource.getDefaultResourceData(identifier) diff --git a/module/applications/sheets-configs/_module.mjs b/module/applications/sheets-configs/_module.mjs index 4b83a042..9528a424 100644 --- a/module/applications/sheets-configs/_module.mjs +++ b/module/applications/sheets-configs/_module.mjs @@ -2,6 +2,7 @@ export { default as ActionConfig } from './action-config.mjs'; export { default as ActionSettingsConfig } from './action-settings-config.mjs'; export { default as CharacterSettings } from './character-settings.mjs'; export { default as AdversarySettings } from './adversary-settings.mjs'; +export { default as NPCSettings } from './npc-settings.mjs'; export { default as CompanionSettings } from './companion-settings.mjs'; export { default as SettingFeatureConfig } from './setting-feature-config.mjs'; export { default as EnvironmentSettings } from './environment-settings.mjs'; diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs index a94abb26..b65e1cdf 100644 --- a/module/applications/sheets-configs/action-base-config.mjs +++ b/module/applications/sheets-configs/action-base-config.mjs @@ -156,7 +156,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) context.openSection = this.openSection; context.tabs = this._getTabs(this.constructor.TABS); context.config = CONFIG.DH; - if (this.action.hasDamage) { + if (this.action.damage) { context.allDamageTypesUsed = !getUnusedDamageTypes(this.action.damage.parts).length; if (this.action.damage.hasOwnProperty('includeBase') && this.action.type === 'attack') @@ -204,7 +204,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) }; } - if (this.action.parent.metadata?.isQuantifiable) { + if (this.action.parent.metadata?.isInventoryItem) { options.quantity = { label: 'DAGGERHEART.GENERAL.itemQuantity', group: 'Global' @@ -302,7 +302,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) static addDamage(_event) { if (!this.action.damage.parts) return; - const choices = getUnusedDamageTypes(this.action.damage.parts); + const choices = getUnusedDamageTypes(this.action._source.damage.parts); const content = new foundry.data.fields.StringField({ label: game.i18n.localize('Damage Type'), choices, diff --git a/module/applications/sheets-configs/activeEffectConfig.mjs b/module/applications/sheets-configs/activeEffectConfig.mjs index 834a57a8..2d9eafc3 100644 --- a/module/applications/sheets-configs/activeEffectConfig.mjs +++ b/module/applications/sheets-configs/activeEffectConfig.mjs @@ -41,7 +41,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac * @returns {ChangeChoice { value: string, label: string, hint: string, group: string }[]} */ static getChangeChoices() { - const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty']; + const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty', 'DhNPC']; const getAllLeaves = (root, group, parentPath = '') => { const leaves = []; @@ -175,6 +175,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac const partContext = await super._preparePartContext(partId, context); switch (partId) { case 'details': + partContext.isItemEffect = partContext.isItemEffect || this.options.isSetting; const useGeneric = game.settings.get( CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance diff --git a/module/applications/sheets-configs/adversary-settings.mjs b/module/applications/sheets-configs/adversary-settings.mjs index 6593f23d..57405675 100644 --- a/module/applications/sheets-configs/adversary-settings.mjs +++ b/module/applications/sheets-configs/adversary-settings.mjs @@ -110,6 +110,7 @@ export default class DHAdversarySettings extends DHBaseActorSettings { } async _onDrop(event) { + event.stopPropagation(); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const item = await fromUuid(data.uuid); diff --git a/module/applications/sheets-configs/environment-settings.mjs b/module/applications/sheets-configs/environment-settings.mjs index bc0efef2..6d74f9c6 100644 --- a/module/applications/sheets-configs/environment-settings.mjs +++ b/module/applications/sheets-configs/environment-settings.mjs @@ -121,6 +121,7 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings { } 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; @@ -138,8 +139,4 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings { this.render(); } } - - async _onDropItem(event, item) { - console.log(item); - } } diff --git a/module/applications/sheets-configs/npc-settings.mjs b/module/applications/sheets-configs/npc-settings.mjs new file mode 100644 index 00000000..c187877c --- /dev/null +++ b/module/applications/sheets-configs/npc-settings.mjs @@ -0,0 +1,85 @@ +import DHBaseActorSettings from '../sheets/api/actor-setting.mjs'; + +/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ + +export default class DHNPCSettings extends DHBaseActorSettings { + /**@inheritdoc */ + static DEFAULT_OPTIONS = { + classes: ['npc-settings'], + position: { width: 455, height: 'auto' }, + actions: {}, + dragDrop: [ + { dragSelector: null, dropSelector: '.tab.features' }, + { dragSelector: '.feature-item', dropSelector: null } + ] + }; + + /**@override */ + static PARTS = { + header: { + id: 'header', + template: 'systems/daggerheart/templates/sheets-settings/npc-settings/header.hbs' + }, + tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, + details: { + id: 'details', + template: 'systems/daggerheart/templates/sheets-settings/npc-settings/details.hbs' + }, + features: { + id: 'features', + template: 'systems/daggerheart/templates/sheets-settings/npc-settings/features.hbs' + } + }; + + /** @override */ + static TABS = { + primary: { + tabs: [{ id: 'details' }, { id: 'features' }], + initial: 'details', + labelPrefix: 'DAGGERHEART.GENERAL.Tabs' + } + }; + + 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 + ); + + 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 f90bb52f..a5bcc4f9 100644 --- a/module/applications/sheets-configs/setting-feature-config.mjs +++ b/module/applications/sheets-configs/setting-feature-config.mjs @@ -188,8 +188,9 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App if (type === 'effect') { const move = foundry.utils.getProperty(this.settings, this.movePath); for (const action of move.actions) { - const remainingEffects = action.effects.filter(x => x._id !== id); - if (action.effects.length !== remainingEffects.length) { + const actionEffects = action.effects ?? []; + const remainingEffects = actionEffects.filter(x => x._id !== id); + if (actionEffects.length !== remainingEffects.length) { await action.update({ effects: remainingEffects.map(x => { const { _id, ...rest } = x; diff --git a/module/applications/sheets/actors/_module.mjs b/module/applications/sheets/actors/_module.mjs index c4ea2d94..1a2bebfb 100644 --- a/module/applications/sheets/actors/_module.mjs +++ b/module/applications/sheets/actors/_module.mjs @@ -2,4 +2,5 @@ export { default as Adversary } from './adversary.mjs'; export { default as Character } from './character.mjs'; export { default as Companion } from './companion.mjs'; export { default as Environment } from './environment.mjs'; +export { default as NPC } from './npc.mjs'; export { default as Party } from './party.mjs'; diff --git a/module/applications/sheets/actors/adversary.mjs b/module/applications/sheets/actors/adversary.mjs index 04be3efb..06dd4a0f 100644 --- a/module/applications/sheets/actors/adversary.mjs +++ b/module/applications/sheets/actors/adversary.mjs @@ -31,6 +31,16 @@ export default class AdversarySheet extends DHBaseActorSheet { dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]', dropSelector: null } + ], + contextMenus: [ + { + handler: DHBaseActorSheet.getBaseAttackContextOptions, + selector: '[data-item-uuid][data-type="attack"]', + options: { + parentClassHooks: false, + fixed: true + } + } ] }; diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index c59dd64e..5d0e7144 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -3,7 +3,7 @@ import DhDeathMove from '../../dialogs/deathMove.mjs'; import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; import FilterMenu from '../../ux/filter-menu.mjs'; -import { getArmorSources, getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs'; +import { getArmorSources, getDocFromElement, getDocFromElementSync, sortBy } from '../../../helpers/utils.mjs'; /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ @@ -57,6 +57,22 @@ export default class CharacterSheet extends DHBaseActorSheet { } ], contextMenus: [ + { + handler: CharacterSheet.#getCreationMainContextOptions, + selector: '.character-details [data-action="editDoc"]', + options: { + parentClassHooks: false, + fixed: true + } + }, + { + handler: DHBaseActorSheet.getBaseAttackContextOptions, + selector: '[data-item-uuid][data-type="attack"]', + options: { + parentClassHooks: false, + fixed: true + } + }, { handler: CharacterSheet.#getDomainCardContextOptions, selector: '[data-item-uuid][data-type="domainCard"]', @@ -176,6 +192,9 @@ export default class CharacterSheet extends DHBaseActorSheet { for (const input of form.querySelectorAll('input:not([type=search]), .editor.prosemirror')) { input.disabled = disabled; } + for (const element of form.querySelectorAll('.input[contenteditable]')) { + element.classList.toggle('disabled', disabled); + } } /** @inheritDoc */ @@ -209,8 +228,9 @@ export default class CharacterSheet extends DHBaseActorSheet { context.attributes = Object.keys(this.document.system.traits).reduce((acc, key) => { acc[key] = { ...this.document.system.traits[key], - name: game.i18n.localize(CONFIG.DH.ACTOR.abilities[key].name), - verbs: CONFIG.DH.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x)) + label: _loc(CONFIG.DH.ACTOR.abilities[key].label), + verbs: CONFIG.DH.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x)), + isSpellcasting: this.document.system.spellcastModifierTrait?.key === key }; return acc; @@ -226,6 +246,11 @@ export default class CharacterSheet extends DHBaseActorSheet { context.resources.stress.emptyPips = context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0; + context.equippedItems = sortBy( + this.document.items.filter(i => i.system.equipped && (i.type === 'weapon' || i.usable)), + i => (i.type === 'weapon' ? (i.system.secondary ? 1 : 0) : 2) + ); + context.beastformActive = this.document.effects.find(x => x.type === 'beastform'); return context; @@ -313,6 +338,56 @@ export default class CharacterSheet extends DHBaseActorSheet { /* Context Menu */ /* -------------------------------------------- */ + static #getCreationMainContextOptions() { + /** Returns true if the item is managed by the level up wizard. Such items shouldn't allow things like manual removal */ + function isItemWizardManaged(item) { + const actor = item?.actor; + if (!actor) return false; + + // If levelup automation is off in general or for this character, all items are unmanaged + // This is disabled until we have proper granted feature removal, for now this feature is to correct errors + // const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto; + // if (!levelupAuto) return false; + + // Core items aren't part of levelup data. TODO: add some way to flag a specific character as no auto leveling + const classPair = actor.system.class; + const coreItems = [actor.system.ancestry, actor.system.community, classPair?.value, classPair?.subclass]; + if (coreItems.includes(item)) return true; + + const levelups = Object.values(actor.system.levelData?.levelups) ?? []; + const uuid = item.uuid; + const sourceUuid = item._stats.compendiumSource; // 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); + return sourceUuid + ? selectedSubclasses.includes(sourceUuid) + : selectedSubclasses.length && item.system.isMulticlass; + } + + const matchesCard = data.achievements.domainCards.some(i => i.itemUuid === uuid); + const matchesSelection = data.selections.some(s => s.itemUuid === uuid); + return matchesCard || matchesSelection; + }); + } + + return [ + { + label: 'CONTROLS.CommonDelete', + icon: 'fa-solid fa-trash', + visible: target => { + const doc = getDocFromElementSync(target); + return doc?.isOwner && !isItemWizardManaged(doc); + }, + onClick: async (event, target) => { + const doc = await getDocFromElement(target); + if (event.shiftKey) return doc.delete(); + else return doc.deleteDialog(); + } + } + ]; + } + /** * Get the set of ContextMenu options for DomainCards. * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance @@ -329,7 +404,7 @@ export default class CharacterSheet extends DHBaseActorSheet { const doc = getDocFromElementSync(target); return doc?.isOwner && doc.system.inVault; }, - callback: async target => { + onClick: async (_, target) => { const doc = await getDocFromElement(target); const actorLoadout = doc.actor.system.loadoutSlot; if (actorLoadout.available) return doc.update({ 'system.inVault': false }); @@ -343,7 +418,7 @@ export default class CharacterSheet extends DHBaseActorSheet { const doc = getDocFromElementSync(target); return doc?.isOwner && doc.system.inVault; }, - callback: async (target, event) => { + onClick: async (event, target) => { const doc = await getDocFromElement(target); const actorLoadout = doc.actor.system.loadoutSlot; if (!actorLoadout.available) { @@ -382,7 +457,7 @@ export default class CharacterSheet extends DHBaseActorSheet { const doc = getDocFromElementSync(target); return doc?.isOwner && !doc.system.inVault; }, - callback: async target => (await getDocFromElement(target)).update({ 'system.inVault': true }) + onClick: async (_, target) => (await getDocFromElement(target)).update({ 'system.inVault': true }) } ].map(option => ({ ...option, @@ -408,7 +483,7 @@ export default class CharacterSheet extends DHBaseActorSheet { const doc = getDocFromElementSync(target); return doc.isOwner && doc && !doc.system.equipped; }, - callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target) + onClick: (event, target) => CharacterSheet.#toggleEquipItem.call(this, event, target) }, { label: 'unequip', @@ -417,7 +492,7 @@ export default class CharacterSheet extends DHBaseActorSheet { const doc = getDocFromElementSync(target); return doc.isOwner && doc && doc.system.equipped; }, - callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target) + onClick: (event, target) => CharacterSheet.#toggleEquipItem.call(this, event, target) } ].map(option => ({ ...option, @@ -712,7 +787,7 @@ export default class CharacterSheet extends DHBaseActorSheet { ? { 'system.linkedClass.uuid': { key: 'system.linkedClass.uuid', - value: this.document.system.class.value._stats.compendiumSource + value: this.document.system.class.value?._stats.compendiumSource } } : undefined, @@ -978,7 +1053,7 @@ export default class CharacterSheet extends DHBaseActorSheet { game.tooltip.activate(target, { html, locked: true, - cssClass: 'bordered-tooltip', + cssClass: 'bordered-tooltip dh-style', direction: 'DOWN' }); @@ -1074,7 +1149,7 @@ export default class CharacterSheet extends DHBaseActorSheet { game.tooltip.activate(target, { html, locked: true, - cssClass: 'bordered-tooltip', + cssClass: 'bordered-tooltip dh-style', direction: 'DOWN', noOffset: true }); diff --git a/module/applications/sheets/actors/companion.mjs b/module/applications/sheets/actors/companion.mjs index b30b9c07..a01b4a64 100644 --- a/module/applications/sheets/actors/companion.mjs +++ b/module/applications/sheets/actors/companion.mjs @@ -11,7 +11,17 @@ export default class DhCompanionSheet extends DHBaseActorSheet { toggleStress: DhCompanionSheet.#toggleStress, actionRoll: DhCompanionSheet.#actionRoll, levelManagement: DhCompanionSheet.#levelManagement - } + }, + contextMenus: [ + { + handler: DHBaseActorSheet.getBaseAttackContextOptions, + selector: '[data-item-uuid][data-type="attack"]', + options: { + parentClassHooks: false, + fixed: true + } + } + ] }; static PARTS = { diff --git a/module/applications/sheets/actors/npc.mjs b/module/applications/sheets/actors/npc.mjs new file mode 100644 index 00000000..8c9048c2 --- /dev/null +++ b/module/applications/sheets/actors/npc.mjs @@ -0,0 +1,136 @@ +import DHBaseActorSheet from '../api/base-actor.mjs'; + +export default class NPCSheet extends DHBaseActorSheet { + /** @inheritDoc */ + static DEFAULT_OPTIONS = { + classes: ['npc'], + position: { width: 660, height: 600 }, + window: { resizable: true }, + actions: {}, + window: { + resizable: true, + controls: [ + { + icon: 'fa-solid fa-signature', + label: 'DAGGERHEART.UI.Tooltip.configureAttribution', + action: 'editAttribution' + } + ] + }, + dragDrop: [ + { + dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]', + dropSelector: null + } + ] + }; + + static PARTS = { + header: { template: 'systems/daggerheart/templates/sheets/actors/npc/header.hbs' }, + tabs: { template: 'systems/daggerheart/templates/sheets/actors/npc/navigation.hbs' }, + features: { + template: 'systems/daggerheart/templates/sheets/actors/npc/features.hbs', + scrollable: ['.feature-section'] + }, + notes: { + template: 'systems/daggerheart/templates/sheets/actors/npc/notes.hbs' + } + }; + + /** @inheritdoc */ + static TABS = { + primary: { + tabs: [{ id: 'notes' }, { id: 'features' }], + initial: 'notes', + labelPrefix: 'DAGGERHEART.GENERAL.Tabs' + } + }; + + /** @inheritdoc */ + _prepareTabs(group) { + const result = super._prepareTabs(group); + if (group === 'primary') { + result.features.empty = this.document.system.features.length === 0; + } + 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); + break; + case 'notes': + await this._prepareNotesContext(context, options); + break; + } + + return context; + } + + /** + * Prepare render context for the Header part. + * @param {ApplicationRenderContext} context + * @param {ApplicationRenderOptions} options + * @returns {Promise} + * @protected + */ + async _prepareHeaderContext(context, _options) { + const { system } = this.document; + const { TextEditor } = foundry.applications.ux; + + context.description = await TextEditor.implementation.enrichHTML(system.description, { + secrets: this.document.isOwner, + relativeTo: this.document + }); + } + + /** + * Prepare render context for the Features part. + * @param {ApplicationRenderContext} context + * @param {ApplicationRenderOptions} options + * @returns {Promise} + * @protected + */ + async _prepareFeaturesContext(context, _options) { + const featureForms = ['passive', 'action', 'reaction']; + 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) + : a.sort - b.sort + ); + } + + /** + * Prepare render context for the Biography part. + * @param {ApplicationRenderContext} context + * @param {ApplicationRenderOptions} options + * @returns {Promise} + * @protected + */ + async _prepareNotesContext(context, _options) { + const { system } = this.document; + const { TextEditor } = foundry.applications.ux; + + const paths = { + notes: 'notes' + }; + + for (const [key, path] of Object.entries(paths)) { + const value = foundry.utils.getProperty(system, path); + context[key] = { + field: system.schema.getField(path), + value, + enriched: await TextEditor.implementation.enrichHTML(value, { + secrets: this.document.isOwner, + relativeTo: this.document + }) + }; + } + } +} diff --git a/module/applications/sheets/actors/party.mjs b/module/applications/sheets/actors/party.mjs index a7eeccdf..927a8810 100644 --- a/module/applications/sheets/actors/party.mjs +++ b/module/applications/sheets/actors/party.mjs @@ -26,7 +26,6 @@ export default class Party extends DHBaseActorSheet { actions: { openDocument: Party.#openDocument, deletePartyMember: Party.#deletePartyMember, - deleteItem: Party.#deleteItem, toggleHope: Party.#toggleHope, toggleHitPoints: Party.#toggleHitPoints, toggleStress: Party.#toggleStress, @@ -44,12 +43,10 @@ export default class Party extends DHBaseActorSheet { static PARTS = { header: { template: 'systems/daggerheart/templates/sheets/actors/party/header.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, - partyMembers: { template: 'systems/daggerheart/templates/sheets/actors/party/party-members.hbs' }, - /* NOT YET IMPLEMENTED */ - // projects: { - // template: 'systems/daggerheart/templates/sheets/actors/party/projects.hbs', - // scrollable: [''] - // }, + partyMembers: { + template: 'systems/daggerheart/templates/sheets/actors/party/party-members.hbs', + scrollable: [''] + }, inventory: { template: 'systems/daggerheart/templates/sheets/actors/party/inventory.hbs', scrollable: ['.tab.inventory .items-section'] @@ -60,19 +57,13 @@ export default class Party extends DHBaseActorSheet { /** @inheritdoc */ static TABS = { primary: { - tabs: [ - { id: 'partyMembers' }, - /* NOT YET IMPLEMENTED */ - // { id: 'projects' }, - { id: 'inventory' }, - { id: 'notes' } - ], + tabs: [{ id: 'partyMembers' }, { id: 'inventory' }, { id: 'notes' }], initial: 'partyMembers', labelPrefix: 'DAGGERHEART.GENERAL.Tabs' } }; - static ALLOWED_ACTOR_TYPES = ['character', 'companion', 'adversary']; + static ALLOWED_ACTOR_TYPES = ['character', 'companion', 'adversary', 'npc']; static DICE_ROLL_ACTOR_TYPES = ['character']; async _onRender(context, options) { @@ -85,6 +76,14 @@ export default class Party extends DHBaseActorSheet { /* Prepare Context */ /* -------------------------------------------- */ + async _prepareContext(options) { + const context = await super._prepareContext(options); + const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming); + context.showStats = + settings.hidePartyStats === 'never' || (settings.hidePartyStats === 'players' && game.user.isGM); + return context; + } + async _preparePartContext(partId, context, options) { context = await super._preparePartContext(partId, context, options); switch (partId) { @@ -498,23 +497,4 @@ export default class Party extends DHBaseActorSheet { const newMembersList = currentMembers.filter(uuid => uuid !== doc.uuid); await this.document.update({ 'system.partyMembers': newMembersList }); } - - static async #deleteItem(event, target) { - const doc = await getDocFromElement(target.closest('.inventory-item')); - if (!event.shiftKey) { - const confirmed = await foundry.applications.api.DialogV2.confirm({ - window: { - title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { - type: game.i18n.localize('TYPES.Actor.party'), - name: doc.name - }) - }, - content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: doc.name }) - }); - - if (!confirmed) return; - } - - this.document.deleteEmbeddedDocuments('Item', [doc.id]); - } } diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 09008aaa..1b388940 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -89,7 +89,7 @@ export default function DHApplicationMixin(Base) { classes: ['daggerheart', 'sheet', 'dh-style'], actions: { triggerContextMenu: DHSheetV2.#triggerContextMenu, - createDoc: DHSheetV2.#createDoc, + createDoc: DHSheetV2.#onCreateDoc, editDoc: DHSheetV2.#editDoc, deleteDoc: DHSheetV2.#deleteDoc, toChat: DHSheetV2.#toChat, @@ -97,8 +97,8 @@ export default function DHApplicationMixin(Base) { viewItem: DHSheetV2.#viewItem, toggleEffect: DHSheetV2.#toggleEffect, toggleExtended: DHSheetV2.#toggleExtended, - addNewItem: DHSheetV2.#addNewItem, - browseItem: DHSheetV2.#browseItem, + addNewItem: DHSheetV2.#onAddNewItem, + browseItem: DHSheetV2.#onBrowseItem, editAttribution: DHSheetV2.#editAttribution, configureLevelUpOptions: DHSheetV2.#configureLevelUpOptions }, @@ -439,7 +439,7 @@ export default function DHApplicationMixin(Base) { const target = element.closest('[data-item-uuid]'); return !target.dataset.disabled && target.dataset.itemType !== 'beastform'; }, - callback: async target => (await getDocFromElement(target)).update({ disabled: true }) + onClick: async (_, target) => (await getDocFromElement(target)).update({ disabled: true }) }, { label: 'enableEffect', @@ -448,7 +448,7 @@ export default function DHApplicationMixin(Base) { const target = element.closest('[data-item-uuid]'); return target.dataset.disabled && target.dataset.itemType !== 'beastform'; }, - callback: async target => (await getDocFromElement(target)).update({ disabled: false }) + onClick: async (_, target) => (await getDocFromElement(target)).update({ disabled: false }) } ].map(option => ({ ...option, @@ -493,7 +493,9 @@ export default function DHApplicationMixin(Base) { (doc?.isOwner && (!doc?.hasOwnProperty('systemPath') || doc?.inCollection)) ); }, - callback: async target => (await getDocFromElement(target)).sheet.render({ force: true }) + onClick: async (_, target) => { + return (await getDocFromElement(target)).sheet.render({ force: true }); + } } ]; @@ -508,7 +510,7 @@ export default function DHApplicationMixin(Base) { !foundry.utils.isEmpty(doc?.damage?.parts); return doc?.isOwner && hasDamage; }, - callback: async (target, event) => { + onClick: async (event, target) => { const doc = await getDocFromElement(target), action = doc?.system?.attack ?? doc; const config = action.prepareConfig(event); @@ -528,7 +530,7 @@ export default function DHApplicationMixin(Base) { const doc = getDocFromElementSync(target); return doc?.isOwner && !(doc.type === 'domainCard' && doc.system.inVault); }, - callback: async (target, event) => (await getDocFromElement(target)).use(event) + onClick: async (event, target) => (await getDocFromElement(target)).use(event) }); } @@ -536,7 +538,7 @@ export default function DHApplicationMixin(Base) { options.push({ label: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat', icon: 'fa-solid fa-message', - callback: async target => (await getDocFromElement(target)).toChat(this.document.uuid) + onClick: async (_, target) => (await getDocFromElement(target)).toChat(this.document.uuid) }); if (deletable) @@ -546,9 +548,9 @@ export default function DHApplicationMixin(Base) { visible: element => { const target = element.closest('[data-item-uuid]'); const doc = getDocFromElementSync(target); - return doc?.isOwner && target.dataset.itemType !== 'beastform'; + return doc?.isOwner !== false && target.dataset.itemType !== 'beastform'; }, - callback: async (target, event) => { + onClick: async (event, target) => { const doc = await getDocFromElement(target); if (event.shiftKey) return doc.delete(); else return doc.deleteDialog(); @@ -654,7 +656,7 @@ export default function DHApplicationMixin(Base) { /* Application Clicks Actions */ /* -------------------------------------------- */ - static async #addNewItem(event, target) { + static async #onAddNewItem(event, target) { const createChoice = await foundry.applications.api.DialogV2.wait({ classes: ['dh-style', 'two-big-buttons'], buttons: [ @@ -673,11 +675,11 @@ export default function DHApplicationMixin(Base) { if (!createChoice) return; - if (createChoice === 'browse') return DHSheetV2.#browseItem.call(this, event, target); - else return DHSheetV2.#createDoc.call(this, event, target); + if (createChoice === 'browse') return DHSheetV2.#onBrowseItem.call(this, event, target); + else return DHSheetV2.#onCreateDoc.call(this, event, target); } - static async #browseItem(event, target) { + static async #onBrowseItem(_event, target) { const type = target.dataset.compendium ?? target.dataset.type; const presets = { @@ -732,7 +734,7 @@ export default function DHApplicationMixin(Base) { * Create an embedded document. * @type {ApplicationClickAction} */ - static async #createDoc(event, target) { + static async #onCreateDoc(event, target) { const { documentClass, type, inVault, disabled } = target.dataset; const parentIsItem = this.document.documentName === 'Item'; const featureOnCharacter = this.document.parent?.type === 'character' && type === 'feature'; @@ -760,11 +762,15 @@ export default function DHApplicationMixin(Base) { type, system: systemData }; - - if (inVault) data['system.inVault'] = true; if (disabled) data.disabled = true; - if (type === 'domainCard' && parent?.system.domains?.length) { - data.system.domain = parent.system.domains[0]; + + if (type === 'domainCard') { + if (parent?.system.domains?.length) data.system.domain = parent.system.domains[0]; + if (inVault) data.system.inVault = true; + } else if (type === 'weapon') { + // Passing an empty system object to weapon causes validation failure due to attack action initialization + // todo: determine why, fix it at its source, then remove this fallback + delete data.system; } const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey }); diff --git a/module/applications/sheets/api/base-actor.mjs b/module/applications/sheets/api/base-actor.mjs index e23a4426..7b820822 100644 --- a/module/applications/sheets/api/base-actor.mjs +++ b/module/applications/sheets/api/base-actor.mjs @@ -166,6 +166,15 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { } } + /** Add support for input content editables */ + _toggleDisabled(disabled) { + super._toggleDisabled(disabled); + const form = this.form; + for (const element of form.querySelectorAll('.input[contenteditable]')) { + element.classList.toggle('disabled', disabled); + } + } + /* -------------------------------------------- */ /* Context Menu */ /* -------------------------------------------- */ @@ -180,6 +189,43 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true }); } + /** + * Get the set of ContextMenu options for the base attack. + * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance + * @this {CharacterSheet} + * @protected + */ + static getBaseAttackContextOptions() { + /**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */ + return [ + { + label: 'DAGGERHEART.CONFIG.RollTypes.attack.name', + icon: 'fa-solid fa-burst', + onClick: async (event, target) => (await getDocFromElement(target)).use(event) + }, + { + label: 'DAGGERHEART.GENERAL.damage', + icon: 'fa-solid fa-explosion', + onClick: async (event, target) => { + 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( + this.document, + doc + ); + config.hasRoll = false; + return action && action.workflow.get('damage').execute(config, null, true); + } + }, + { + label: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat', + icon: 'fa-solid fa-message', + onClick: async (_, target) => (await getDocFromElement(target)).toChat(this.document.uuid) + } + ]; + } + /* -------------------------------------------- */ /* Application Listener Actions */ /* -------------------------------------------- */ diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index e3568b23..1e08fc05 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -126,7 +126,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { options.push({ name: 'CONTROLS.CommonDelete', icon: '', - callback: async target => { + onClick: async (_, target) => { const feature = await getDocFromElement(target); if (!feature) return; const confirmed = await foundry.applications.api.DialogV2.confirm({ diff --git a/module/applications/sheets/items/class.mjs b/module/applications/sheets/items/class.mjs index 05bb0229..25c631fe 100644 --- a/module/applications/sheets/items/class.mjs +++ b/module/applications/sheets/items/class.mjs @@ -104,9 +104,10 @@ export default class ClassSheet extends DHBaseItemSheet { } /**@inheritdoc */ - async _prepareContext(_options) { - const context = await super._prepareContext(_options); + async _prepareContext(options) { + const context = await super._prepareContext(options); context.domains = this.document.system.domains; + context.subclasses = await this.document.system.fetchSubclasses(); return context; } @@ -128,20 +129,8 @@ export default class ClassSheet extends DHBaseItemSheet { const item = await fromUuid(data.uuid); const itemType = data.type === 'ActiveEffect' ? data.type : item.type; const target = event.target.closest('fieldset.drop-section'); - if (itemType === 'subclass') { - if (item.system.linkedClass) { - return ui.notifications.warn( - game.i18n.format('DAGGERHEART.UI.Notifications.subclassAlreadyLinked', { - name: item.name, - class: this.document.name - }) - ); - } - await item.update({ 'system.linkedClass': this.document.uuid }); - await this.document.update({ - 'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid] - }); - } else if (['feature', 'ActiveEffect'].includes(itemType)) { + + if (['feature', 'ActiveEffect'].includes(itemType)) { super._onDrop(event); } else if (this.document.parent?.type !== 'character') { if (itemType === 'weapon') { @@ -200,12 +189,6 @@ export default class ClassSheet extends DHBaseItemSheet { static async #removeItemFromCollection(_event, element) { const { uuid, target } = element.dataset; const prop = foundry.utils.getProperty(this.document.system, target); - - if (target === 'subclasses') { - const subclass = await foundry.utils.fromUuid(uuid); - await subclass?.update({ 'system.linkedClass': null }); - } - await this.document.update({ [`system.${target}`]: prop.filter(i => i && i.uuid !== uuid).map(x => x.uuid) }); } diff --git a/module/applications/sheets/items/subclass.mjs b/module/applications/sheets/items/subclass.mjs index 5c731777..e9d8370e 100644 --- a/module/applications/sheets/items/subclass.mjs +++ b/module/applications/sheets/items/subclass.mjs @@ -40,4 +40,36 @@ export default class SubclassSheet extends DHBaseItemSheet { get relatedDocs() { return this.document.system.features.map(x => x.item); } + + async _prepareContext(options) { + const context = await super._prepareContext(options); + if (this.document.system.linkedClass) { + const classData = await fromUuid(this.document.system.linkedClass); + context.class = classData ?? { + name: _loc('DAGGERHEART.GENERAL.missingX', { x: _loc('TYPES.Item.class') }), + missing: true + }; + } + return context; + } + + async _onDrop(event) { + event.stopPropagation(); + const data = TextEditor.getDragEventData(event); + 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; + if (this.document.system.linkedClass !== uuid) { + await this.document.update({ 'system.linkedClass': uuid }); + // Re-render all class sheets for instant feedback + for (const app of foundry.applications.instances.values()) { + if (app.document?.type === 'class') app.render(); + } + } + return; + } + + return super._onDrop(event); + } } diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index 34b25591..7036a5df 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -103,6 +103,19 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo _getEntryContextOptions() { return [ ...super._getEntryContextOptions(), + { + label: 'DAGGERHEART.UI.ChatLog.rerollActionRoll', + icon: '', + visible: li => { + const message = game.messages.get(li.dataset.messageId); + return message.system.hasRoll && (game.user.isGM || message.isAuthor); + }, + callback: async li => { + const message = game.messages.get(li.dataset.messageId); + const reroll = await message.rolls[0].reroll({ liveRoll: true }); + message.update({ rolls: [reroll] }); + } + }, { label: 'DAGGERHEART.UI.ChatLog.rerollDamage', icon: '', @@ -113,9 +126,10 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo : false; return (game.user.isGM || message.isAuthor) && hasRolledDamage; }, - callback: li => { + callback: async li => { const message = game.messages.get(li.dataset.messageId); - new game.system.api.applications.dialogs.RerollDamageDialog(message).render({ force: true }); + const update = await message.system.getRerolledDamage(); + message.update(update); } } ]; diff --git a/module/applications/ui/combatTracker.mjs b/module/applications/ui/combatTracker.mjs index fb19a17e..25f3e06b 100644 --- a/module/applications/ui/combatTracker.mjs +++ b/module/applications/ui/combatTracker.mjs @@ -56,7 +56,9 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C async _prepareTrackerContext(context, options) { await super._prepareTrackerContext(context, options); - const adversaries = context.turns?.filter(x => x.isNPC) ?? []; + const npcs = context.turns?.filter(x => x.isNPC) ?? []; + const adversaries = npcs.filter(x => x.disposition !== CONST.TOKEN_DISPOSITIONS.FRIENDLY); + const friendlies = npcs.filter(x => x.disposition === CONST.TOKEN_DISPOSITIONS.FRIENDLY); const characters = context.turns?.filter(x => !x.isNPC) ?? []; const spotlightQueueEnabled = game.settings.get( CONFIG.DH.id, @@ -75,25 +77,56 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C Object.assign(context, { actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens, adversaries, + friendlies, allCharacters: characters, characters: characters.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0), spotlightRequests }); } + /** + * Open the dialog used to edit the name of the currently viewed Combat encounter. + * @this {CombatTracker} + * @returns {Promise} + */ + static async #onEditName() { + const combat = this.viewed; + if (!combat || !game.user.isGM) return null; + const field = combat.schema.fields.name; + const inputHTML = field.toFormGroup({}, { name: 'name', value: combat.name, autofocus: true }).outerHTML; + const formData = await foundry.applications.api.DialogV2.input({ + window: { icon: 'fa-solid fa-tag', title: 'COMBAT.ACTIONS.EditNameTitle' }, + position: { width: 480 }, + content: inputHTML + }); + await combat.update({ name: formData.name || '' }); + } + _getCombatContextOptions() { return [ { - label: 'COMBAT.ClearMovementHistories', - icon: '', - visible: () => game.user.isGM && this.viewed?.combatants.size > 0, - callback: () => this.viewed.clearMovementHistories() + label: 'COMBAT.ACTIONS.EditName', + icon: 'fa-solid fa-tag', + visible: () => game.user.isGM && !!this.viewed, + onClick: () => DhCombatTracker.#onEditName.call(this) }, { - label: 'COMBAT.Delete', - icon: '', + label: 'COMBAT.ACTIONS.LinkToScene', + icon: '', + visible: () => game.user.isGM && !this.scene, + onClick: () => this.viewed.toggleSceneLink() + }, + { + label: 'COMBAT.ACTIONS.UnlinkFromScene', + icon: '', + visible: () => game.user.isGM && !!this.scene, + onClick: () => this.viewed.toggleSceneLink() + }, + { + label: 'COMBAT.End', + icon: 'fa-solid fa-xmark', visible: () => game.user.isGM && !!this.viewed, - callback: () => this.viewed.endCombat() + onClick: () => this.viewed.endCombat() } ]; } @@ -129,7 +162,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C active: index === combat.turn, canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'), type: combatant.actor?.system?.type, - img: await this._getCombatantThumbnail(combatant) + img: await this._getCombatantThumbnail(combatant), + disposition: combatant.token?.disposition }; turn.css = [turn.active ? 'active' : null, hidden ? 'hide' : null, isDefeated ? 'defeated' : null].filterJoin( diff --git a/module/applications/ui/countdownEdit.mjs b/module/applications/ui/countdownEdit.mjs index 8bb9fc1d..b418107c 100644 --- a/module/applications/ui/countdownEdit.mjs +++ b/module/applications/ui/countdownEdit.mjs @@ -1,6 +1,6 @@ import { DhCountdown } from '../../data/countdowns.mjs'; import { waitForDiceSoNice } from '../../helpers/utils.mjs'; -import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; +import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -114,7 +114,7 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio } await this.data.updateSource(update); - await emitAsGM(GMUpdateEvent.UpdateCountdowns, this.gmSetSetting.bind(this.data), this.data, null, { + await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, this.gmSetSetting.bind(this.data), this.data, null, { refreshType: RefreshType.Countdown }); diff --git a/module/applications/ui/countdowns.mjs b/module/applications/ui/countdowns.mjs index 79a59a07..6fa05e29 100644 --- a/module/applications/ui/countdowns.mjs +++ b/module/applications/ui/countdowns.mjs @@ -1,5 +1,5 @@ import { waitForDiceSoNice } from '../../helpers/utils.mjs'; -import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; +import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -21,19 +21,19 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application static DEFAULT_OPTIONS = { id: 'countdowns', tag: 'div', - classes: ['daggerheart', 'dh-style', 'countdowns', 'faded-ui'], + classes: ['daggerheart', 'dh-style', 'countdowns'], window: { icon: 'fa-solid fa-clock-rotate-left', - frame: true, + frame: false, title: 'DAGGERHEART.UI.Countdowns.title', positioned: false, resizable: false, minimizable: false }, actions: { - toggleViewMode: DhCountdowns.#toggleViewMode, - editCountdowns: DhCountdowns.#editCountdowns, - loopCountdown: DhCountdowns.#loopCountdown, + toggleViewMode: DhCountdowns.#onToggleViewMode, + editCountdowns: DhCountdowns.#onEditCountdowns, + loopCountdown: DhCountdowns.#onLoopCountdown, decreaseCountdown: (_, target) => this.editCountdown(false, target), increaseCountdown: (_, target) => this.editCountdown(true, target) }, @@ -62,20 +62,6 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application if (iconOnly) frame.classList.add('icon-only'); else frame.classList.remove('icon-only'); - const header = frame.querySelector('.window-header'); - header.querySelector('button[data-action="close"]').remove(); - header.querySelector('button[data-action="toggleControls"]').remove(); - - if (game.user.isGM) { - const editTooltip = game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.editTitle'); - const editButton = ``; - header.insertAdjacentHTML('beforeEnd', editButton); - } - - const minimizeTooltip = game.i18n.localize('DAGGERHEART.UI.Countdowns.toggleIconMode'); - const minimizeButton = ``; - header.insertAdjacentHTML('beforeEnd', minimizeButton); - return frame; } @@ -161,7 +147,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application return true; } - static async #toggleViewMode() { + 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; @@ -172,15 +158,16 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application this.render(); } - static async #editCountdowns() { + static async #onEditCountdowns() { new game.system.api.applications.ui.CountdownEdit().render(true); } - static async #loopCountdown(_, target) { + 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; @@ -199,12 +186,12 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application await waitForDiceSoNice(message); await settings.updateSource({ - [`countdowns.${target.id}.progress`]: { + [`countdowns.${countdownId}.progress`]: { current: newMax, start: newMax } }); - await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { + await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { refreshType: RefreshType.Countdown }); } @@ -213,12 +200,13 @@ 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 emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { + await settings.updateSource({ [`countdowns.${countdownId}.progress.current`]: newCurrent }); + await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { refreshType: RefreshType.Countdown }); } @@ -277,7 +265,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application return acc; }, {}) }; - await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { + await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { refreshType: RefreshType.Countdown }); } diff --git a/module/applications/ui/fearTracker.mjs b/module/applications/ui/fearTracker.mjs index 4e5e1132..8c247f79 100644 --- a/module/applications/ui/fearTracker.mjs +++ b/module/applications/ui/fearTracker.mjs @@ -1,4 +1,4 @@ -import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; +import { emitGMUpdate, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -104,7 +104,7 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV } async updateFear(value) { - return emitAsGM( + return emitGMUpdate( GMUpdateEvent.UpdateFear, game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), value diff --git a/module/applications/ui/itemBrowser.mjs b/module/applications/ui/itemBrowser.mjs index 67a16f6a..8f38918a 100644 --- a/module/applications/ui/itemBrowser.mjs +++ b/module/applications/ui/itemBrowser.mjs @@ -1,3 +1,4 @@ +import { getDocFromElement } from '../../helpers/utils.mjs'; import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -47,7 +48,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { expandContent: this.expandContent, resetFilters: this.resetFilters, sortList: this.sortList, - openSettings: this.openSettings + openSettings: this.openSettings, + viewSheet: this.#onViewSheet }, position: { left: 100, @@ -109,8 +111,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position ); - options.position = userPresetPosition ?? ItemBrowser.DEFAULT_OPTIONS.position; + delete options.position.zIndex; if (!userPresetPosition) { const width = noFolder === true || lite === true ? 600 : 850; @@ -277,7 +279,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { (await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description)); } - this.fieldFilter = this._createFieldFilter(); + this.fieldFilter = await this._createFieldFilter(); if (this.presets?.filter) { Object.entries(this.presets.filter).forEach(([k, v]) => { @@ -306,7 +308,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { { items: this.items, menu: this.selectedMenu, - formatLabel: this.formatLabel + formatLabel: this.formatLabel, + viewSheet: this.items[0] instanceof Actor } ); @@ -355,12 +358,12 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { ); } - _createFieldFilter() { + async _createFieldFilter() { const filters = ItemBrowser.getFolderConfig(this.selectedMenu.data, 'filters'); - filters.forEach(f => { + for (const f of filters) { if (typeof f.field === 'string') f.field = foundry.utils.getProperty(game, f.field); else if (typeof f.choices === 'function') { - f.choices = f.choices(this.items); + f.choices = await f.choices(this.items); } // Clear field label so template uses our custom label parameter @@ -370,7 +373,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { f.name ??= f.key; f.value = this.presets?.filter?.[f.name]?.value ?? null; - }); + } + return filters; } @@ -567,6 +571,11 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { } } + static async #onViewSheet(_, target) { + const document = await getDocFromElement(target); + document?.sheet?.render(true); + } + _createDragProcess() { new foundry.applications.ux.DragDrop.implementation({ dragSelector: '.item-container', @@ -605,7 +614,16 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { items: { folder: 'equipments', render: { - noFolder: true + folders: [ + 'equipments', + 'ancestries', + 'classes', + 'subclasses', + 'domains', + 'communities', + 'beastforms' + // excluded: features + ] } }, compendium: {} diff --git a/module/applications/ui/sceneNavigation.mjs b/module/applications/ui/sceneNavigation.mjs index a0005fc7..982063e7 100644 --- a/module/applications/ui/sceneNavigation.mjs +++ b/module/applications/ui/sceneNavigation.mjs @@ -1,4 +1,4 @@ -import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; +import { emitGMUpdate, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; export default class DhSceneNavigation extends foundry.applications.ui.SceneNavigation { /** @inheritdoc */ @@ -68,7 +68,7 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi 1 )[0]; newEnvironments.unshift(newFirst); - emitAsGM( + emitGMUpdate( GMUpdateEvent.UpdateDocument, scene.update.bind(scene), { 'flags.daggerheart.sceneEnvironments': newEnvironments }, diff --git a/module/canvas/placeables/regionLayer.mjs b/module/canvas/placeables/regionLayer.mjs index c53cf782..684fdd5e 100644 --- a/module/canvas/placeables/regionLayer.mjs +++ b/module/canvas/placeables/regionLayer.mjs @@ -57,14 +57,14 @@ export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer { } async placeRegion(data, options = {}) { - const preConfirm = ({ _event, document, _create, _options }) => { - const shape = document.shapes[0]; + const preConfirm = data => { + const shape = data.document.shapes[0]; const isEmanation = shape.type === 'emanation'; if (isEmanation) { const token = this.#findTokenInBounds(shape.base.origin); - if (!token) return options.preConfirm?.() ?? true; + if (!token) return options.preConfirm?.(data) ?? true; const shapeData = shape.toObject(); - document.updateSource({ + data.document.updateSource({ shapes: [ { ...shapeData, @@ -80,10 +80,10 @@ export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer { }); } - return options?.preConfirm?.() ?? true; + return options?.preConfirm?.(data) ?? true; }; - super.placeRegion(data, { ...options, preConfirm }); + return await super.placeRegion(data, { ...options, preConfirm }); } /** Searches for token at origin point, returning null if there are no tokens or multiple overlapping tokens */ diff --git a/module/canvas/placeables/token.mjs b/module/canvas/placeables/token.mjs index 77c178d6..68e325c2 100644 --- a/module/canvas/placeables/token.mjs +++ b/module/canvas/placeables/token.mjs @@ -249,9 +249,6 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { /** @inheritDoc */ _drawBar(number, bar, data) { - const val = Number(data.value); - const pct = Math.clamp(val, 0, data.max) / data.max; - // Determine sizing const { width, height } = this.document.getSize(); const s = canvas.dimensions.uiScale; @@ -259,17 +256,19 @@ 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 fillColor = - number === 0 ? foundry.utils.Color.fromRGB([1, 0, 0]) : foundry.utils.Color.fromString('#0032b1'); + const Color = foundry.utils.Color; + const fillColor = number === 0 ? Color.fromRGB([1, 0, 0]) : Color.fromString('#0032b1'); + const emptyColor = Color.fromRGB([0, 0, 0]); - // Draw the bar - const widthUnit = bw / data.max; + // Draw the bar (accounting floating point numbers from bar animations) + const widthUnit = bw / Math.ceil(data.max); bar.clear().lineStyle(s, 0x000000, 1.0); - const sections = [...Array(data.max).keys()]; - for (let mark of sections) { + const sections = [...Array(Math.ceil(data.max)).keys()]; + for (const mark of sections) { const x = mark * widthUnit; - const marked = mark + 1 <= data.value; - const color = marked ? fillColor : foundry.utils.Color.fromRGB([0, 0, 0]); + 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; 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 { diff --git a/module/config/itemBrowserConfig.mjs b/module/config/itemBrowserConfig.mjs index 5c0a219b..ae5fa71b 100644 --- a/module/config/itemBrowserConfig.mjs +++ b/module/config/itemBrowserConfig.mjs @@ -70,6 +70,49 @@ export const typeConfig = { } ] }, + environments: { + columns: [ + { + key: 'system.tier', + label: 'DAGGERHEART.GENERAL.Tiers.singular' + }, + { + key: 'system.type', + label: 'DAGGERHEART.GENERAL.type', + format: type => { + if (!type) return '-'; + + return CONFIG.DH.ACTOR.environmentTypes[type].label; + } + } + ], + filters: [ + { + key: 'system.tier', + label: 'DAGGERHEART.GENERAL.Tiers.singular', + field: 'system.api.models.actors.DhEnvironment.schema.fields.tier' + }, + { + key: 'system.type', + label: 'DAGGERHEART.GENERAL.type', + field: 'system.api.models.actors.DhEnvironment.schema.fields.type' + }, + { + key: 'system.difficulty', + name: 'difficulty.min', + label: 'DAGGERHEART.UI.ItemBrowser.difficultyMin', + field: 'system.api.models.actors.DhEnvironment.schema.fields.difficulty', + operator: 'gte' + }, + { + key: 'system.difficulty', + name: 'difficulty.max', + label: 'DAGGERHEART.UI.ItemBrowser.difficultyMax', + field: 'system.api.models.actors.DhEnvironment.schema.fields.difficulty', + operator: 'lte' + } + ] + }, items: { columns: [ { @@ -177,8 +220,8 @@ export const typeConfig = { key: 'system.secondary', label: 'DAGGERHEART.UI.ItemBrowser.subtype', choices: [ - { value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon' }, - { value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon' } + { value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon.full' }, + { value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon.full' } ] }, { @@ -383,7 +426,8 @@ export const typeConfig = { { key: 'system.linkedClass', label: 'TYPES.Item.class', - format: linkedClass => linkedClass?.name ?? 'DAGGERHEART.UI.ItemBrowser.missing' + format: linkedClass => + foundry.utils.fromUuidSync(linkedClass)?.name ?? 'DAGGERHEART.UI.ItemBrowser.missing' }, { key: 'system.spellcastingTrait', @@ -393,15 +437,20 @@ export const typeConfig = { ], filters: [ { - key: 'system.linkedClass.uuid', + key: 'system.linkedClass', label: 'TYPES.Item.class', - choices: items => { - const list = items - .filter(item => item.system.linkedClass) - .map(item => ({ - value: item.system.linkedClass.uuid, - label: item.system.linkedClass.name - })); + choices: async items => { + const list = []; + for (const item of items.filter(item => item.system.linkedClass)) { + const linkedClass = await foundry.utils.fromUuid(item.system.linkedClass); + if (linkedClass) { + list.push({ + value: linkedClass.uuid, + label: linkedClass.name + }); + } + } + return list.reduce((a, c) => { if (!a.find(i => i.value === c.value)) a.push(c); return a; @@ -555,7 +604,8 @@ export const compendiumConfig = { id: 'environments', keys: ['environments'], label: 'DAGGERHEART.UI.ItemBrowser.folders.environments', - type: ['environment'] + type: ['environment'], + listType: 'environments' }, beastforms: { id: 'beastforms', diff --git a/module/config/itemConfig.mjs b/module/config/itemConfig.mjs index a3e785c3..43054ec5 100644 --- a/module/config/itemConfig.mjs +++ b/module/config/itemConfig.mjs @@ -453,7 +453,7 @@ export const allArmorFeatures = () => { const feature = homebrewFeatures[key]; const actions = feature.actions.map(action => ({ ...action, - effects: action.effects.map(effect => feature.effects.find(x => x.id === effect._id)), + effects: action.effects?.map(effect => feature.effects.find(x => x.id === effect._id)) ?? [], type: action.type })); const actionEffects = actions.flatMap(a => a.effects); @@ -1407,7 +1407,7 @@ export const allWeaponFeatures = () => { const actions = feature.actions.map(action => ({ ...action, - effects: action.effects.map(effect => feature.effects.find(x => x.id === effect._id)), + effects: action.effects?.map(effect => feature.effects.find(x => x.id === effect._id)) ?? [], type: action.type })); const actionEffects = actions.flatMap(a => a.effects); diff --git a/module/config/resourceConfig.mjs b/module/config/resourceConfig.mjs index 56ef6cd5..d33d7ab3 100644 --- a/module/config/resourceConfig.mjs +++ b/module/config/resourceConfig.mjs @@ -57,15 +57,9 @@ const companionBaseResources = Object.freeze({ stress: { id: 'stress', initial: 0, - max: 0, + max: 3, reverse: true, label: 'DAGGERHEART.GENERAL.stress' - }, - hope: { - id: 'hope', - initial: 0, - reverse: false, - label: 'DAGGERHEART.GENERAL.hope' } }); diff --git a/module/data/action/attackAction.mjs b/module/data/action/attackAction.mjs index 15cc1696..1f7e1c92 100644 --- a/module/data/action/attackAction.mjs +++ b/module/data/action/attackAction.mjs @@ -1,4 +1,3 @@ -import { DHDamageData } from '../fields/action/damageField.mjs'; import DHDamageAction from './damageAction.mjs'; export default class DHAttackAction extends DHDamageAction { @@ -12,8 +11,19 @@ export default class DHAttackAction extends DHDamageAction { super.prepareData(); if (!!this.item?.system?.attack) { if (this.damage.includeBase) { - const baseDamage = this.getParentDamage(); - this.damage.parts.hitPoints = new DHDamageData(baseDamage); + const baseDamage = this.getParentHitPointDamage(); + if (baseDamage) { + if (!this.damage.parts.hitPoints) { + this.damage.parts.hitPoints = baseDamage; + } else { + for (const type of baseDamage.type) this.damage.parts.hitPoints.type.add(type); + + this.damage.parts.hitPoints.value.custom = { + enabled: true, + formula: `${baseDamage.value.getFormula()} + ${this.damage.parts.hitPoints.value.getFormula()}` + }; + } + } } if (this.roll.useDefault) { this.roll.trait = this.item.system.attack.roll.trait; @@ -22,16 +32,8 @@ export default class DHAttackAction extends DHDamageAction { } } - getParentDamage() { - return { - value: { - multiplier: 'prof', - dice: this.item?.system?.attack.damage.parts.hitPoints.value.dice, - bonus: this.item?.system?.attack.damage.parts.hitPoints.value.bonus ?? 0 - }, - type: this.item?.system?.attack.damage.parts.hitPoints.type, - base: true - }; + getParentHitPointDamage() { + return this.item?.system?.attack.damage.parts.hitPoints; } get damageFormula() { @@ -73,7 +75,12 @@ export default class DHAttackAction extends DHDamageAction { const useAltDamage = this.actor?.effects?.find(x => x.type === 'horde')?.active; for (const { value, valueAlt, type } of damage.parts) { const usedValue = useAltDamage ? valueAlt : value; - const str = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {}); + const damageString = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {}); + const str = damageString + ? damageString + : game.i18n.format('DAGGERHEART.GENERAL.missingX', { + 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 b3775dc9..acd104a7 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -148,10 +148,14 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel : null; } - /** Returns true if the action is usable */ + /** + * Returns true if the action is usable. + * An action is usable on any actor type. For example, an adversary might have a base attack action. + */ get usable() { const actor = this.actor; - return this.isOwner && actor?.type === 'character'; + const pack = actor?.pack ? game.packs.get(actor.pack) : null; + return !pack?.locked && this.isOwner; } static getRollType(parent) { diff --git a/module/data/action/countdownAction.mjs b/module/data/action/countdownAction.mjs index abcc6b40..cb141637 100644 --- a/module/data/action/countdownAction.mjs +++ b/module/data/action/countdownAction.mjs @@ -36,7 +36,7 @@ export default class DhCountdownAction extends DHBaseAction { /** @inheritDoc */ static migrateData(source) { - for (const countdown of source.countdown) { + for (const countdown of Object.values(source.countdown)) { if (countdown.progress.max) { countdown.progress.startFormula = countdown.progress.max; countdown.progress.start = 1; diff --git a/module/data/activeEffect/beastformEffect.mjs b/module/data/activeEffect/beastformEffect.mjs index 128c0c52..0fbea122 100644 --- a/module/data/activeEffect/beastformEffect.mjs +++ b/module/data/activeEffect/beastformEffect.mjs @@ -35,6 +35,7 @@ export default class BeastformEffect extends BaseEffect { static migrateData(source) { if (!source.characterTokenData.tokenSize.height) source.characterTokenData.tokenSize.height = 1; if (!source.characterTokenData.tokenSize.width) source.characterTokenData.tokenSize.width = 1; + if (!source.characterTokenData.tokenSize.depth) source.characterTokenData.tokenSize.depth = 1; return super.migrateData(source); } @@ -52,7 +53,8 @@ export default class BeastformEffect extends BaseEffect { if (this.parent.parent.type === 'character') { const baseUpdate = { height: this.characterTokenData.tokenSize.height, - width: this.characterTokenData.tokenSize.width + width: this.characterTokenData.tokenSize.width, + depth: this.characterTokenData.tokenSize.depth }; const update = { ...baseUpdate, diff --git a/module/data/actor/_module.mjs b/module/data/actor/_module.mjs index 99577620..1fe1ef3f 100644 --- a/module/data/actor/_module.mjs +++ b/module/data/actor/_module.mjs @@ -1,15 +1,17 @@ import DhCharacter from './character.mjs'; import DhCompanion from './companion.mjs'; import DhAdversary from './adversary.mjs'; +import DhNPC from './npc.mjs'; import DhEnvironment from './environment.mjs'; import DhParty from './party.mjs'; -export { DhCharacter, DhCompanion, DhAdversary, DhEnvironment, DhParty }; +export { DhCharacter, DhCompanion, DhAdversary, DhNPC, DhEnvironment, DhParty }; export const config = { character: DhCharacter, companion: DhCompanion, adversary: DhAdversary, + npc: DhNPC, environment: DhEnvironment, party: DhParty }; diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs index d30e090a..d6d0dcdf 100644 --- a/module/data/actor/adversary.mjs +++ b/module/data/actor/adversary.mjs @@ -3,8 +3,7 @@ import { ActionField } from '../fields/actionField.mjs'; import { commonActorRules } from './base.mjs'; import DhCreature from './creature.mjs'; import { bonusField } from '../fields/actorField.mjs'; -import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs'; -import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs'; +import { getTierAdjustedAdversary } from './tierAdjustment.mjs'; export default class DhpAdversary extends DhCreature { static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary']; @@ -189,6 +188,9 @@ export default class DhpAdversary extends DhCreature { prepareDerivedData() { super.prepareDerivedData(); this.attack.roll.isStandardAttack = true; + + // Clamp resources (must be done last to ensure all updates occur) + this.resources.clamp(); } _getTags() { @@ -203,205 +205,6 @@ export default class DhpAdversary extends DhCreature { /** Returns source data for this actor adjusted to a new tier, which can be used to create a new actor. */ adjustForTier(tier) { const source = this.parent.toObject(true); - - /** @type {(2 | 3 | 4)[]} */ - const tiers = new Array(Math.abs(tier - this.tier)) - .fill(0) - .map((_, idx) => idx + Math.min(tier, this.tier) + 1); - if (tier < this.tier) tiers.reverse(); - const typeData = adversaryScalingData[source.system.type] ?? adversaryScalingData[source.system.standard]; - const tierEntries = tiers.map(t => ({ tier: t, ...typeData[t] })); - - // Apply simple tier changes - const scale = tier > this.tier ? 1 : -1; - for (const entry of tierEntries) { - source.system.difficulty += scale * entry.difficulty; - source.system.damageThresholds.major += scale * entry.majorThreshold; - source.system.damageThresholds.severe += scale * entry.severeThreshold; - source.system.resources.hitPoints.max += scale * entry.hp; - source.system.resources.stress.max += scale * entry.stress; - source.system.attack.roll.bonus += scale * entry.attack; - } - - // Get the mean and standard deviation of expected damage in the previous and new tier - // The data we have is for attack scaling, but we reuse this for action scaling later - const expectedDamageData = adversaryExpectedDamage[source.system.type] ?? adversaryExpectedDamage.basic; - const damageMeta = { - currentDamageRange: { tier: source.system.tier, ...expectedDamageData[source.system.tier] }, - newDamageRange: { tier, ...expectedDamageData[tier] }, - type: 'attack' - }; - - // Update damage of base attack - try { - this.#adjustActionDamage(source.system.attack, damageMeta); - } catch (err) { - ui.notifications.warn('Failed to convert attack damage of adversary'); - console.error(err); - } - - // Update damage of each item action, making sure to also update the description if possible - const damageRegex = /@Damage\[([^\[\]]*)\]({[^}]*})?/g; - for (const item of source.items) { - // Replace damage inlines with new formulas - 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; - - try { - const adjusted = this.#calculateAdjustedDamage(formula, { ...damageMeta, type: 'action' }); - const newFormula = [ - adjusted.diceQuantity ? `${adjusted.diceQuantity}d${adjusted.faces}` : null, - adjusted.bonus - ] - .filter(p => !!p) - .join('+'); - return match.replace(formula, newFormula); - } catch { - return match; - } - }); - } - - // Update damage in item actions - // Parse damage, and convert all formula matches in the descriptions to the new damage - for (const action of Object.values(item.system.actions)) { - try { - const result = this.#adjustActionDamage(action, { ...damageMeta, type: 'action' }); - if (!result) continue; - - for (const { previousFormula, formula } of Object.values(result)) { - const oldFormulaRegexp = new RegExp( - previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?') - ); - item.system.description = item.system.description.replace(oldFormulaRegexp, formula); - action.description = action.description.replace(oldFormulaRegexp, formula); - } - } catch (err) { - ui.notifications.warn(`Failed to convert action damage for item ${item.name}`); - console.error(err); - } - } - } - - // Finally set the tier of the source data, now that everything is complete - source.system.tier = tier; - return source; - } - - /** - * Converts a damage object to a new damage range - * @returns {{ diceQuantity: number; faces: number; bonus: number }} the adjusted result as a combined term - * @throws error if the formula is the wrong type - */ - #calculateAdjustedDamage(formula, { currentDamageRange, newDamageRange, type }) { - const terms = parseTermsFromSimpleFormula(formula); - const flatTerms = terms.filter(t => t.diceQuantity === 0); - const diceTerms = terms.filter(t => t.diceQuantity > 0); - if (flatTerms.length > 1 || diceTerms.length > 1) { - throw new Error('invalid formula for conversion'); - } - const value = { - ...(diceTerms[0] ?? { diceQuantity: 0, faces: 1 }), - bonus: flatTerms[0]?.bonus ?? 0 - }; - const previousExpected = calculateExpectedValue(value); - if (previousExpected === 0) return value; // nothing to do - - const dieSizes = [4, 6, 8, 10, 12, 20]; - const steps = newDamageRange.tier - currentDamageRange.tier; - const increasing = steps > 0; - const deviation = (previousExpected - currentDamageRange.mean) / currentDamageRange.deviation; - const expected = Math.max(1, newDamageRange.mean + newDamageRange.deviation * deviation); - - // If this was just a flat number, convert to the expected damage and exit - if (value.diceQuantity === 0) { - value.bonus = Math.round(expected); - return value; - } - - const getExpectedDie = () => calculateExpectedValue({ diceQuantity: 1, faces: value.faces }) || 1; - const getBaseAverage = () => calculateExpectedValue({ ...value, bonus: 0 }); - - // Check the number of base overages over the expected die. In the end, if the bonus inflates too much, we add a die - const baseOverages = Math.floor(value.bonus / getExpectedDie()); - - // Prestep. Change number of dice for attacks, bump up/down for actions - // We never bump up to d20, though we might bump down from it - if (type === 'attack') { - const minimum = increasing ? value.diceQuantity : 0; - value.diceQuantity = Math.max(minimum, newDamageRange.tier); - } else { - const currentIdx = dieSizes.indexOf(value.faces); - value.faces = dieSizes[Math.clamp(currentIdx + steps, 0, 4)]; - } - - value.bonus = Math.round(expected - getBaseAverage()); - - // Attempt to handle negative values. - // If we can do it with only step downs, do so. Otherwise remove tier dice, and try again - if (value.bonus < 0) { - let stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity); - const currentIdx = dieSizes.indexOf(value.faces); - - // If step downs alone don't suffice, change the flat modifier, then calculate steps required again - // If this isn't sufficient, the result will be slightly off. This is unlikely to happen - if (type !== 'attack' && stepsRequired > currentIdx && value.diceQuantity > 0) { - value.diceQuantity -= increasing ? 1 : Math.abs(steps); - value.bonus = Math.round(expected - getBaseAverage()); - if (value.bonus >= 0) return value; // complete - } - - stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity); - value.faces = dieSizes[Math.max(0, currentIdx - stepsRequired)]; - value.bonus = Math.max(0, Math.round(expected - getBaseAverage())); - } - - // If value is really high, we add a number of dice based on the number of overages - // This attempts to preserve a similar amount of variance when increasing an action - const overagesToRemove = Math.floor(value.bonus / getExpectedDie()) - baseOverages; - if (type !== 'attack' && increasing && overagesToRemove > 0) { - value.diceQuantity += overagesToRemove; - value.bonus = Math.round(expected - getBaseAverage()); - } - - return value; - } - - /** - * Updates damage to reflect a specific value. - * @throws if damage structure is invalid for conversion - * @returns the converted formula and value as a simplified term, or null if it doesn't deal HP damage - */ - #adjustActionDamage(action, damageMeta) { - if (!action.damage?.parts.hitPoints) return null; - - const result = {}; - for (const property of ['value', 'valueAlt']) { - const data = action.damage.parts.hitPoints[property]; - const previousFormula = data.custom.enabled - ? data.custom.formula - : [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0] - .filter(p => !!p) - .join('+'); - const value = this.#calculateAdjustedDamage(previousFormula, damageMeta); - const formula = [value.diceQuantity ? `${value.diceQuantity}d${value.faces}` : null, value.bonus] - .filter(p => !!p) - .join('+'); - if (value.diceQuantity) { - data.custom.enabled = false; - data.bonus = value.bonus; - data.dice = `d${value.faces}`; - data.flatMultiplier = value.diceQuantity; - } else if (!value.diceQuantity) { - data.custom.enabled = true; - data.custom.formula = formula; - } - - result[property] = { previousFormula, formula, value }; - } - - return result; + return getTierAdjustedAdversary(source, tier); } } diff --git a/module/data/actor/base.mjs b/module/data/actor/base.mjs index 13ff0a38..9efbe7d7 100644 --- a/module/data/actor/base.mjs +++ b/module/data/actor/base.mjs @@ -1,6 +1,6 @@ import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs'; import DHItem from '../../documents/item.mjs'; -import { getScrollTextData } from '../../helpers/utils.mjs'; +import { createShallowProxy, getScrollTextData } from '../../helpers/utils.mjs'; const fields = foundry.data.fields; @@ -180,8 +180,7 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel { * @returns {object} */ getRollData() { - const data = { ...this }; - return data; + return createShallowProxy(this); } /** diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 5904483f..f50e3db2 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -399,8 +399,9 @@ export default class DhCharacter extends DhCreature { return this.domains.map(key => { const domain = allDomainData[key]; return { + id: key, ...domain, - label: game.i18n.localize(domain.label) + label: game.i18n.localize(domain?.label) ?? key }; }); } @@ -418,14 +419,11 @@ export default class DhCharacter extends DhCreature { } get loadoutSlot() { - const loadoutCount = this.domainCards.loadout?.length ?? 0, - worldSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxLoadout, - max = !worldSetting ? null : worldSetting + this.bonuses.maxLoadout; - + const loadoutCount = this.domainCards.loadout?.length ?? 0; + const worldSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxLoadout; return { current: loadoutCount, - available: !max ? true : Math.max(max - loadoutCount, 0), - max + available: loadoutCount < worldSetting }; } @@ -600,6 +598,8 @@ export default class DhCharacter extends DhCreature { communityFeatures = [], classFeatures = [], subclassFeatures = [], + multiclassFeatures = [], + multiclassSubclassFeatures = [], companionFeatures = [], features = []; @@ -609,9 +609,9 @@ export default class DhCharacter extends DhCreature { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.community.id) { communityFeatures.push(item); } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.class.id) { - classFeatures.push(item); + (item.system.multiclassOrigin ? multiclassFeatures : classFeatures).push(item); } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) { - subclassFeatures.push(item); + (item.system.multiclassOrigin ? multiclassSubclassFeatures : subclassFeatures).push(item); } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.companion.id) { companionFeatures.push(item); } else if (item.type === 'feature' && !item.system.type) { @@ -640,6 +640,24 @@ export default class DhCharacter extends DhCreature { type: 'subclass', values: subclassFeatures }, + ...(multiclassFeatures.length + ? { + 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 + } + } + : {}), companionFeatures: { title: game.i18n.localize('DAGGERHEART.ACTORS.Character.companionFeatures'), type: 'companion', @@ -804,6 +822,8 @@ export default class DhCharacter extends DhCreature { prepareDerivedData() { super.prepareDerivedData(); + + this.resources.hope.max -= this.scars; if (this.companion) { for (let levelKey in this.companion.system.levelData.levelups) { const level = this.companion.system.levelData.levelups[levelKey]; @@ -817,7 +837,6 @@ export default class DhCharacter extends DhCreature { } } - this.resources.hope.max -= this.scars; this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait; this.resources.armor = { @@ -835,6 +854,9 @@ export default class DhCharacter extends DhCreature { } this.attack.damage.parts.hitPoints.value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`; + + // Clamp resources (must be done last to ensure all updates occur) + this.resources.clamp(); } getRollData() { @@ -866,16 +888,17 @@ export default class DhCharacter extends DhCreature { /* Scars can alter the amount of current hope */ if (changes.system?.scars) { - const diff = this.system.scars - changes.system.scars; - const newHopeMax = this.system.resources.hope.max + diff; - const newHopeValue = Math.min(newHopeMax, this.system.resources.hope.value); - if (newHopeValue != this.system.resources.hope.value) { - if (!changes.system.resources.hope) changes.system.resources.hope = { value: 0 }; - - changes.system.resources.hope = { - ...changes.system.resources.hope, - value: changes.system.resources.hope.value + newHopeValue - }; + const diff = this.scars - changes.system.scars; + const newHopeMax = this.resources.hope.max + diff; + const newHopeValue = Math.min(newHopeMax, this.resources.hope.value); + if (newHopeValue != this.resources.hope.value) { + changes.system = foundry.utils.mergeObject(changes.system ?? {}, { + resources: { + hope: { + value: (changes.system?.resources?.hope?.value ?? 0) + newHopeMax + } + } + }); } } diff --git a/module/data/actor/companion.mjs b/module/data/actor/companion.mjs index 956696c2..f6cbfe8d 100644 --- a/module/data/actor/companion.mjs +++ b/module/data/actor/companion.mjs @@ -146,9 +146,6 @@ export default class DhCompanion extends DhCreature { const level = this.levelData.levelups[levelKey]; for (let selection of level.selections) { switch (selection.type) { - case 'hope': - this.resources.hope += selection.value; - break; case 'vicious': if (selection.data[0] === 'damage') { this.attack.damage.parts.hitPoints.value.dice = adjustDice( @@ -185,6 +182,9 @@ export default class DhCompanion extends DhCreature { return acc; }, this.partner.system.companionData.levelupChoices); } + + // Clamp resources (must be done last to ensure all updates occur) + this.resources.clamp(); } async _preUpdate(changes, options, userId) { diff --git a/module/data/actor/creature.mjs b/module/data/actor/creature.mjs index 88646301..601068ad 100644 --- a/module/data/actor/creature.mjs +++ b/module/data/actor/creature.mjs @@ -60,14 +60,4 @@ export default class DhCreature extends BaseDataActor { } } } - - prepareDerivedData() { - const minLimitResource = resource => { - if (resource) resource.value = Math.min(resource.value, resource.max); - }; - - minLimitResource(this.resources.stress); - minLimitResource(this.resources.hitPoints); - minLimitResource(this.resources.hope); - } } diff --git a/module/data/actor/npc.mjs b/module/data/actor/npc.mjs new file mode 100644 index 00000000..2ccaf926 --- /dev/null +++ b/module/data/actor/npc.mjs @@ -0,0 +1,43 @@ +import DHNPCSettings from '../../applications/sheets-configs/npc-settings.mjs'; +import BaseDataActor from './base.mjs'; + +export default class DhpNPC extends BaseDataActor { + static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.NPC']; + + static get metadata() { + return foundry.utils.mergeObject(super.metadata, { + label: 'TYPES.Actor.npc', + type: 'npc', + settingSheet: DHNPCSettings, + hasResistances: false, + hasAttribution: true + }); + } + + static defineSchema() { + const fields = foundry.data.fields; + return { + ...super.defineSchema(), + difficulty: new fields.NumberField({ + nullable: true, + initial: null, + integer: true, + label: 'DAGGERHEART.GENERAL.difficulty' + }), + description: new fields.HTMLField({ label: 'DAGGERHEART.GENERAL.description' }), + motives: new fields.StringField(), + notes: new fields.HTMLField() + }; + } + + /**@inheritdoc */ + static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/actors/drama-masks.svg'; + + get features() { + return this.parent.items.filter(x => x.type === 'feature'); + } + + isItemValid(source) { + return super.isItemValid(source) || source.type === 'feature'; + } +} diff --git a/module/data/actor/party.mjs b/module/data/actor/party.mjs index 93596cda..5b9cccab 100644 --- a/module/data/actor/party.mjs +++ b/module/data/actor/party.mjs @@ -18,7 +18,7 @@ export default class DhParty extends BaseDataActor { const fields = foundry.data.fields; return { ...super.defineSchema(), - partyMembers: new ForeignDocumentUUIDArrayField({ type: 'Actor' }, { prune: true }), + partyMembers: new ForeignDocumentUUIDArrayField({ type: 'Actor' }), notes: new fields.HTMLField(), gold: new GoldField(), tagTeam: new fields.EmbeddedDataField(TagTeamData), diff --git a/module/data/actor/tierAdjustment.mjs b/module/data/actor/tierAdjustment.mjs new file mode 100644 index 00000000..bc6ad176 --- /dev/null +++ b/module/data/actor/tierAdjustment.mjs @@ -0,0 +1,219 @@ +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; + + /** @type {(2 | 3 | 4)[]} */ + const tiers = new Array(Math.abs(tier - currentTier)) + .fill(0) + .map((_, idx) => idx + Math.min(tier, currentTier) + 1); + if (tier < currentTier) tiers.reverse(); + const typeData = adversaryScalingData[source.system.type] ?? adversaryScalingData[source.system.standard]; + const tierEntries = tiers.map(t => ({ tier: t, ...typeData[t] })); + + // Apply simple tier changes + const scale = tier > currentTier ? 1 : -1; + for (const entry of tierEntries) { + source.system.difficulty += scale * entry.difficulty; + source.system.damageThresholds.major += scale * entry.majorThreshold; + source.system.damageThresholds.severe += scale * entry.severeThreshold; + source.system.resources.hitPoints.max += scale * entry.hp; + source.system.resources.stress.max += scale * entry.stress; + source.system.attack.roll.bonus += scale * entry.attack; + } + + // Get the mean and standard deviation of expected damage in the previous and new tier + // The data we have is for attack scaling, but we reuse this for action scaling later + const expectedDamageData = adversaryExpectedDamage[source.system.type] ?? adversaryExpectedDamage.basic; + const damageMeta = { + currentDamageRange: { tier: source.system.tier, ...expectedDamageData[source.system.tier] }, + newDamageRange: { tier, ...expectedDamageData[tier] } + }; + + // Store initial attack damage for abilities that have you deal a "standard attack" + const initialAttack = { + type: source.system.attack.damage?.parts.hitPoints?.type?.toSorted(), + value: getDamagePartsFormula(source.system.attack.damage?.parts.hitPoints?.value) + }; + + // Update damage of base attack. + try { + const damage = source.system.attack.damage; + if (!damage?.parts.hitPoints) throw new Error('Unexpected missing attack in adversary'); + + for (const property of ['value', 'valueAlt']) { + const data = damage.parts.hitPoints[property]; + const previousFormula = getDamagePartsFormula(data); + const { value, formula } = calculateAdjustedDamage(previousFormula, 'attack', damageMeta); + applyAdjustedDamage(data, value, formula); + } + } catch (err) { + ui.notifications.warn('Failed to convert attack damage of adversary'); + console.error(err); + } + + // Update damage of each item action, making sure to also update the description if possible + const damageRegex = /@Damage\[([^\[\]]*)\]({[^}]*})?/g; + for (const item of source.items) { + // Replace damage inlines with new formulas. Keep a record for a specific check later + 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, { first: 'value' }); + if (!formula) return match; + + try { + const newFormula = calculateAdjustedDamage(formula, 'action', damageMeta)?.formula; + descriptionFormulas.push(formula); + return match.replace(formula, newFormula); + } catch { + return match; + } + }); + } + + // Update damage in item actions and convert all formula matches in the descriptions to the new damage + for (const action of Object.values(item.system.actions)) { + if (!action.damage?.parts.hitPoints) continue; + try { + // Apply conversions and save a record. If it matches attack damage *and* Its not in the description, use attack conversion instead + const result = []; + for (const property of ['value', 'valueAlt']) { + const { [property]: data, type: damageType } = action.damage.parts.hitPoints; + const previousFormula = getDamagePartsFormula(data); + const isActuallyAttack = + previousFormula === initialAttack.value && + foundry.utils.equals(damageType.toSorted(), initialAttack.type) && + !descriptionFormulas.includes(previousFormula); + const type = isActuallyAttack ? 'attack' : 'action'; + const { value, formula } = calculateAdjustedDamage(previousFormula, type, damageMeta); + applyAdjustedDamage(data, value, formula); + result.push({ previousFormula, formula }); + } + + // Override text in the description with those values + for (const { previousFormula, formula } of Object.values(result)) { + const oldFormulaRegexp = new RegExp( + previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?') + ); + item.system.description = item.system.description.replace(oldFormulaRegexp, formula); + action.description = action.description.replace(oldFormulaRegexp, formula); + } + } catch (err) { + ui.notifications.warn(`Failed to convert action damage for item ${item.name}`); + console.error(err); + } + } + } + + // Finally set the tier of the source data, now that everything is complete + source.system.tier = tier; + return source; +} + +/** + * Converts a damage object to a new damage range + * @returns {{ diceQuantity: number; faces: number; bonus: number }} the adjusted result as a combined term + * @throws error if the formula is the wrong type + */ +function calculateAdjustedDamage(formula, type, { currentDamageRange, newDamageRange }) { + const terms = parseTermsFromSimpleFormula(formula); + const flatTerms = terms.filter(t => t.diceQuantity === 0); + const diceTerms = terms.filter(t => t.diceQuantity > 0); + if (flatTerms.length > 1 || diceTerms.length > 1) { + throw new Error('invalid formula for conversion'); + } + const value = { + ...(diceTerms[0] ?? { diceQuantity: 0, faces: 1 }), + bonus: flatTerms[0]?.bonus ?? 0 + }; + const previousExpected = calculateExpectedValue(value); + if (previousExpected === 0) return value; // nothing to do + + const dieSizes = [4, 6, 8, 10, 12, 20]; + const steps = newDamageRange.tier - currentDamageRange.tier; + const increasing = steps > 0; + const deviation = (previousExpected - currentDamageRange.mean) / currentDamageRange.deviation; + const expected = Math.max(1, newDamageRange.mean + newDamageRange.deviation * deviation); + + // If this was just a flat number, convert to the expected damage and exit + if (value.diceQuantity === 0) { + value.bonus = Math.round(expected); + return value; + } + + const getExpectedDie = () => calculateExpectedValue({ diceQuantity: 1, faces: value.faces }) || 1; + const getBaseAverage = () => calculateExpectedValue({ ...value, bonus: 0 }); + + // Check the number of base overages over the expected die. In the end, if the bonus inflates too much, we add a die + const baseOverages = Math.floor(value.bonus / getExpectedDie()); + + // Prestep. Change number of dice for attacks, bump up/down for actions + // We never bump up to d20, though we might bump down from it + if (type === 'attack') { + const minimum = increasing ? value.diceQuantity : 0; + value.diceQuantity = Math.max(minimum, newDamageRange.tier); + } else { + const currentIdx = dieSizes.indexOf(value.faces); + value.faces = dieSizes[Math.clamp(currentIdx + steps, 0, 4)]; + } + + value.bonus = Math.round(expected - getBaseAverage()); + + // Attempt to handle negative values. + // If we can do it with only step downs, do so. Otherwise remove tier dice, and try again + if (value.bonus < 0) { + let stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity); + const currentIdx = dieSizes.indexOf(value.faces); + + // If step downs alone don't suffice, change the flat modifier, then calculate steps required again + // If this isn't sufficient, the result will be slightly off. This is unlikely to happen + if (type !== 'attack' && stepsRequired > currentIdx && value.diceQuantity > 0) { + value.diceQuantity -= increasing ? 1 : Math.abs(steps); + value.bonus = Math.round(expected - getBaseAverage()); + if (value.bonus >= 0) return value; // complete + } + + stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity); + value.faces = dieSizes[Math.max(0, currentIdx - stepsRequired)]; + value.bonus = Math.max(0, Math.round(expected - getBaseAverage())); + } + + // If value is really high, we add a number of dice based on the number of overages + // This attempts to preserve a similar amount of variance when increasing an action + const overagesToRemove = Math.floor(value.bonus / getExpectedDie()) - baseOverages; + if (type !== 'attack' && increasing && overagesToRemove > 0) { + value.diceQuantity += overagesToRemove; + value.bonus = Math.round(expected - getBaseAverage()); + } + + const newFormula = [value.diceQuantity ? `${value.diceQuantity}d${value.faces}` : null, value.bonus] + .filter(p => !!p) + .join('+'); + return { value, formula: newFormula }; +} + +function getDamagePartsFormula(data) { + return data.custom.enabled + ? data.custom.formula + : [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0].filter(p => !!p).join('+'); +} + +/** + * Updates damage to reflect a specific value. + * @throws if damage structure is invalid for conversion + * @returns the converted formula and value as a simplified term, or null if it doesn't deal HP damage + */ +function applyAdjustedDamage(diceData, value, formula) { + if (value.diceQuantity) { + diceData.custom.enabled = false; + diceData.bonus = value.bonus; + diceData.dice = `d${value.faces}`; + diceData.flatMultiplier = value.diceQuantity; + } else if (!value.diceQuantity) { + diceData.custom.enabled = true; + diceData.custom.formula = formula; + } +} diff --git a/module/data/chat-message/actorRoll.mjs b/module/data/chat-message/actorRoll.mjs index eaa1cdc2..ccfe25ea 100644 --- a/module/data/chat-message/actorRoll.mjs +++ b/module/data/chat-message/actorRoll.mjs @@ -1,3 +1,5 @@ +import { triggerChatRollFx } from '../../helpers/utils.mjs'; + const fields = foundry.data.fields; const targetsField = () => @@ -130,6 +132,35 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { }); } + /* TODO: Change how damage data is stored somehow to enable better rerolling */ + async getRerolledDamage() { + if (!this.damage) return; + + const rerolls = []; + const update = { system: { damage: {} } }; + for (const partKey in this.damage) { + const part = this.damage[partKey]; + const testRoll = Roll.fromData(part.parts[0].roll); + const rerolled = await testRoll.reroll(); + rerolls.push(rerolled); + + if (!update.system.damage[partKey]) update.system.damage[partKey] = { parts: [part.parts[0]] }; + const partData = update.system.damage[partKey].parts[0]; + update.system.damage[partKey].total = rerolled.total; + partData.modifierTotal = rerolled.terms.reduce((acc, x) => { + if (x.isDeterministic && !x.operator) acc += x.total; + return acc; + }, 0); + partData.dice = rerolled.dice.map(d => ({ ...d.toJSON(), dice: d.denomination })); + partData.total = rerolled.total; + partData.roll = rerolled.toJSON(); + } + + await triggerChatRollFx(rerolls); + + return update; + } + registerTargetHook() { if (!this.parent.isAuthor || !this.hasTarget) return; if (this.targetMode && this.parent.targetHook !== null) { diff --git a/module/data/compendiumBrowserSettings.mjs b/module/data/compendiumBrowserSettings.mjs index ea71c439..ed70c88f 100644 --- a/module/data/compendiumBrowserSettings.mjs +++ b/module/data/compendiumBrowserSettings.mjs @@ -11,11 +11,13 @@ export default class CompendiumBrowserSettings extends foundry.abstract.DataMode }) ), excludedPacks: new fields.TypedObjectField( - new fields.SchemaField({ - excludedDocumentTypes: new fields.ArrayField( - new fields.StringField({ required: true, choices: CONST.SYSTEM_SPECIFIC_COMPENDIUM_TYPES }) - ) - }) + new fields.TypedObjectField( + new fields.SchemaField({ + excludedDocumentTypes: new fields.ArrayField( + new fields.StringField({ required: true, choices: CONST.SYSTEM_SPECIFIC_COMPENDIUM_TYPES }) + ) + }) + ) ) }; } @@ -28,7 +30,7 @@ export default class CompendiumBrowserSettings extends foundry.abstract.DataMode const excludedSourceData = this.excludedSources[packageName]; if (excludedSourceData && excludedSourceData.excludedDocumentTypes.includes(pack.metadata.type)) return true; - const excludedPackData = this.excludedPacks[item.pack]; + const excludedPackData = this.excludedPacks[packageName]?.[pack.metadata.name]; if (excludedPackData && excludedPackData.excludedDocumentTypes.includes(pack.metadata.type)) return true; return false; diff --git a/module/data/fields/action/costField.mjs b/module/data/fields/action/costField.mjs index 9271f6b0..1928af41 100644 --- a/module/data/fields/action/costField.mjs +++ b/module/data/fields/action/costField.mjs @@ -103,7 +103,7 @@ export default class CostField extends fields.ArrayField { static calcCosts(costs) { const resources = CostField.getResources.call(this, costs); let filteredCosts = costs; - if (this.parent?.metadata.isQuantifiable && this.parent.consumeOnUse === false) { + if (this.parent?.isInventoryItem && this.parent.consumeOnUse === false) { filteredCosts = filteredCosts.filter(c => c.key !== 'quantity'); } diff --git a/module/data/fields/action/countdownField.mjs b/module/data/fields/action/countdownField.mjs index 719ca749..990f8ef1 100644 --- a/module/data/fields/action/countdownField.mjs +++ b/module/data/fields/action/countdownField.mjs @@ -1,4 +1,4 @@ -import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../../systemRegistration/socket.mjs'; +import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../../systemRegistration/socket.mjs'; const fields = foundry.data.fields; @@ -78,7 +78,7 @@ export default class CountdownField extends fields.ArrayField { ); } - await emitAsGM( + await emitGMUpdate( GMUpdateEvent.UpdateCountdowns, async () => { const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index 30a5ad7c..9b21d3ba 100644 --- a/module/data/fields/action/damageField.mjs +++ b/module/data/fields/action/damageField.mjs @@ -72,9 +72,6 @@ export default class DamageField extends fields.SchemaField { damageConfig.source.message = messageId; damageConfig.directDamage = !!damageConfig.source?.message; - // if(damageConfig.source?.message && game.modules.get('dice-so-nice')?.active) - // await game.dice3d.waitFor3DAnimationByMessageID(damageConfig.source.message); - const damageResult = await CONFIG.Dice.daggerheart.DamageRoll.build(damageConfig); if (!damageResult) return false; if (damageResult.actionChatMessageHandled) config.actionChatMessageHandled = true; diff --git a/module/data/fields/action/effectsField.mjs b/module/data/fields/action/effectsField.mjs index 9a4ffc31..d2ee1682 100644 --- a/module/data/fields/action/effectsField.mjs +++ b/module/data/fields/action/effectsField.mjs @@ -1,5 +1,3 @@ -import { emitAsGM, GMUpdateEvent } from '../../../systemRegistration/socket.mjs'; - const fields = foundry.data.fields; export default class EffectsField extends fields.ArrayField { @@ -34,8 +32,7 @@ export default class EffectsField extends fields.ArrayField { } if (EffectsField.getAutomation() || force) { targets ??= (message.system?.targets ?? config.targets).filter(t => !config.hasRoll || t.hit); - await emitAsGM(GMUpdateEvent.UpdateEffect, EffectsField.applyEffects.bind(this), targets, this.uuid); - // EffectsField.applyEffects.call(this, config.targets.filter(t => !config.hasRoll || t.hit)); + EffectsField.applyEffects.call(this, targets); } } @@ -59,7 +56,7 @@ export default class EffectsField extends fields.ArrayField { if (!token) return; const messageToken = token.document ?? token; - const conditionImmunities = messageToken.actor.system.rules.conditionImmunities ?? {}; + const conditionImmunities = messageToken.actor.system.rules?.conditionImmunities ?? {}; messageTargets.push({ token: messageToken, conditionImmunities: Object.values(conditionImmunities).some(x => x) diff --git a/module/data/fields/action/summonField.mjs b/module/data/fields/action/summonField.mjs index ec7881f7..a2275fa5 100644 --- a/module/data/fields/action/summonField.mjs +++ b/module/data/fields/action/summonField.mjs @@ -1,4 +1,4 @@ -import { itemAbleRollParse } from '../../../helpers/utils.mjs'; +import { itemAbleRollParse, triggerChatRollFx } from '../../../helpers/utils.mjs'; import FormulaField from '../formulaField.mjs'; const fields = foundry.data.fields; @@ -40,7 +40,7 @@ export default class DHSummonField extends fields.ArrayField { const roll = new Roll(itemAbleRollParse(summon.count, this.actor, this.item)); await roll.evaluate(); const count = roll.total; - if (!roll.isDeterministic && game.modules.get('dice-so-nice')?.active) rolls.push(roll); + if (!roll.isDeterministic) rolls.push(roll); const actor = await DHSummonField.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. */ @@ -56,7 +56,7 @@ export default class DHSummonField extends fields.ArrayField { } } - if (rolls.length) await Promise.all(rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true))); + if (rolls.length) await triggerChatRollFx(rolls); this.actor.sheet?.minimize(); DHSummonField.handleSummon(summonData, this.actor); diff --git a/module/data/fields/actorField.mjs b/module/data/fields/actorField.mjs index d6b58675..a5f6f379 100644 --- a/module/data/fields/actorField.mjs +++ b/module/data/fields/actorField.mjs @@ -80,6 +80,18 @@ class ResourcesField extends fields.TypedObjectField { value.isReversed = resources[key].reverse; value.max = typeof resource.max === 'number' ? (value.max ?? resource.max) : null; } + Object.defineProperty(data, 'clamp', { + value: function () { + for (const key of Object.keys(this)) { + const resource = this[key]; + if (typeof resource?.max === 'number') { + resource.value = Math.clamp(resource.value, 0, resource.max); + } + } + }, + enumerable: false + }); + return data; } diff --git a/module/data/fields/foreignDocumentUUIDArrayField.mjs b/module/data/fields/foreignDocumentUUIDArrayField.mjs index 456c0593..f8969d33 100644 --- a/module/data/fields/foreignDocumentUUIDArrayField.mjs +++ b/module/data/fields/foreignDocumentUUIDArrayField.mjs @@ -10,6 +10,7 @@ export default class ForeignDocumentUUIDArrayField extends foundry.data.fields.A */ constructor(fieldOption = {}, options = {}, context = {}) { super(new ForeignDocumentUUIDField(fieldOption), options, context); + this.options.prune ??= true; } /** @inheritdoc */ diff --git a/module/data/groupRollData.mjs b/module/data/groupRollData.mjs index 78a06b13..10123152 100644 --- a/module/data/groupRollData.mjs +++ b/module/data/groupRollData.mjs @@ -30,7 +30,7 @@ export class CharacterData extends foundry.abstract.DataModel { }), rollData: new fields.JSONField({ nullable: true, initial: null }), selected: new fields.BooleanField({ initial: false }), - successfull: new fields.BooleanField({ nullable: true, initial: null }) + successful: new fields.BooleanField({ nullable: true, initial: null }) }; } 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 b0e4847f..21c56f9a 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -141,16 +141,6 @@ export default class DHArmor extends AttachableItem { } } - _onUpdate(a, b, c) { - super._onUpdate(a, b, c); - - if (this.actor?.type === 'character') { - for (const party of this.actor.parties) { - party.render(); - } - } - } - /** @inheritDoc */ static migrateDocumentData(source) { if (!source.system.armor) { diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index f6c794f1..ba114fda 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -7,7 +7,12 @@ * @property {boolean} isInventoryItem- Indicates whether items of this type is a Inventory Item */ -import { addLinkedItemsDiff, getScrollTextData, updateLinkedItemApps } from '../../helpers/utils.mjs'; +import { + addLinkedItemsDiff, + getScrollTextData, + createShallowProxy, + updateLinkedItemApps +} from '../../helpers/utils.mjs'; import { ActionsField } from '../fields/actionField.mjs'; import FormulaField from '../fields/formulaField.mjs'; @@ -159,8 +164,8 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { * @returns {object} */ getRollData(options = {}) { - const actorRollData = this.actor?.getRollData() ?? {}; - const data = { ...actorRollData, item: { ...this } }; + const data = this.actor?.getRollData() ?? {}; + data.item = createShallowProxy(this); return data; } @@ -195,7 +200,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { const features = []; for (let f of this.features) { const fBase = f.item ?? f; - const feature = fBase.system ? fBase : await foundry.utils.fromUuid(fBase.uuid); + const feature = fBase.pack ? await foundry.utils.fromUuid(fBase.uuid) : fBase; features.push( foundry.utils.mergeObject( feature.toObject(), diff --git a/module/data/item/beastform.mjs b/module/data/item/beastform.mjs index d30d6046..ee9d9839 100644 --- a/module/data/item/beastform.mjs +++ b/module/data/item/beastform.mjs @@ -51,7 +51,8 @@ export default class DHBeastform extends BaseDataItem { }), scale: new fields.NumberField({ nullable: false, min: 0.2, max: 3, step: 0.05, initial: 1 }), height: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }), - width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }) + width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }), + depth: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }) }), mainTrait: new fields.StringField({ required: true, @@ -192,7 +193,8 @@ export default class DHBeastform extends BaseDataItem { tokenSize: { scale: this.parent.parent.prototypeToken.texture.scaleX, height: this.parent.parent.prototypeToken.height, - width: this.parent.parent.prototypeToken.width + width: this.parent.parent.prototypeToken.width, + depth: this.parent.parent.prototypeToken.depth } }, advantageOn: this.advantageOn, @@ -211,10 +213,12 @@ export default class DHBeastform extends BaseDataItem { : null; const width = autoTokenSize ?? this.tokenSize.width; const height = autoTokenSize ?? this.tokenSize.height; + const depth = autoTokenSize ?? this.tokenSize.depth; const prototypeTokenUpdate = { height, width, + depth, texture: { src: this.tokenImg, scaleX: this.tokenSize.scale, diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs index f1f7a96d..691f9253 100644 --- a/module/data/item/class.mjs +++ b/module/data/item/class.mjs @@ -2,7 +2,7 @@ import BaseDataItem from './base.mjs'; import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; import ItemLinkFields from '../fields/itemLinkFields.mjs'; -import { addLinkedItemsDiff, getFeaturesHTMLData, updateLinkedItemApps } from '../../helpers/utils.mjs'; +import { addLinkedItemsDiff, fromUuids, getFeaturesHTMLData, updateLinkedItemApps } from '../../helpers/utils.mjs'; import { DhLevelOption } from '../levelTier.mjs'; export default class DHClass extends BaseDataItem { @@ -32,7 +32,6 @@ export default class DHClass extends BaseDataItem { }), evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }), features: new ItemLinkFields(), - subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), inventory: new fields.SchemaField({ take: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), choiceA: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), @@ -78,6 +77,25 @@ export default class DHClass extends BaseDataItem { return this.features.filter(x => x.type === CONFIG.DH.ITEM.featureSubTypes.class).map(x => x.item); } + 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)); + 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 (subclasses.find(x => x.uuid === index.uuid)) continue; + packIds.push(index._id); + } + + if (packIds.length > 0) subclasses.push(...(await pack.getDocuments({ _id__in: packIds }))); + } + + return subclasses; + } + async _preCreate(data, options, user) { if (this.actor?.type === 'character') { const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto; @@ -207,6 +225,10 @@ export default class DHClass extends BaseDataItem { classItems.push(contentLink.outerHTML); } + // Preload all class 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 hopeFeatures = await getFeaturesHTMLData(this.hopeFeatures); const classFeatures = await getFeaturesHTMLData(this.classFeatures); 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 d421cc6d..934b55d3 100644 --- a/module/data/item/subclass.mjs +++ b/module/data/item/subclass.mjs @@ -1,5 +1,4 @@ -import { getFeaturesHTMLData } from '../../helpers/utils.mjs'; -import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; +import { fromUuids, getFeaturesHTMLData } from '../../helpers/utils.mjs'; import ItemLinkFields from '../fields/itemLinkFields.mjs'; import BaseDataItem from './base.mjs'; @@ -28,7 +27,7 @@ export default class DHSubclass extends BaseDataItem { features: new ItemLinkFields(), featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }), isMulticlass: new fields.BooleanField({ initial: false }), - linkedClass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true, initial: null }) + linkedClass: new fields.DocumentUUIDField({ type: 'Item', nullable: true, initial: null }) }; } @@ -56,37 +55,30 @@ export default class DHSubclass extends BaseDataItem { if (allowed === false) return; if (this.actor?.type === 'character') { - const dataUuid = data.uuid ?? data._stats.compendiumSource ?? `Item.${data._id}`; - if (this.actor.system.class.subclass) { - if (this.actor.system.multiclass.subclass) { - ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassesAlreadyPresent')); - return false; - } else { - const multiclass = this.actor.items.find(x => x.type === 'class' && x.system.isMulticlass); - if (!multiclass) { - ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.missingMulticlass')); - return false; - } + const { value: actorClass, subclass: existingSubclass } = this.actor.system.class; + const { value: multiclass, subclass: existingMultisubclass } = this.actor.system.multiclass; + if (!actorClass && !multiclass) { + ui.notifications.warn('DAGGERHEART.UI.Notifications.missingClass', { localize: true }); + return false; + } + if (existingSubclass && existingMultisubclass) { + ui.notifications.warn('DAGGERHEART.UI.Notifications.subclassesAlreadyPresent', { localize: true }); + return false; + } + if (existingSubclass && !multiclass) { + ui.notifications.warn('DAGGERHEART.UI.Notifications.missingMulticlass', { localize: true }); + return false; + } - if (multiclass.system.subclasses.every(x => x.uuid !== dataUuid)) { - ui.notifications.error( - game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInMulticlass') - ); - return false; - } - - await this.updateSource({ isMulticlass: true }); - } - } else { - const actorClass = this.actor.items.find(x => x.type === 'class' && !x.system.isMulticlass); - if (!actorClass) { - ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.missingClass')); - return false; - } - if (actorClass.system.subclasses.every(x => x.uuid !== dataUuid)) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInClass')); - return false; - } + const match = [multiclass, actorClass].find( + c => c && (c._stats.compendiumSource ?? c.uuid) === this.linkedClass + ); + if (!match) { + const key = multiclass ? 'subclassNotInMulticlass' : 'subclassNotInClass'; + ui.notifications.warn(`DAGGERHEART.UI.Notifications.${key}`, { localize: true }); + return false; + } else if (match.system.isMulticlass) { + await this.updateSource({ isMulticlass: true }); } } } @@ -98,6 +90,11 @@ export default class DHSubclass extends BaseDataItem { const spellcastTrait = this.spellcastingTrait ? game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.spellcastingTrait].label) : null; + + // 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)); + const foundationFeatures = await getFeaturesHTMLData(this.foundationFeatures); const specializationFeatures = await getFeaturesHTMLData(this.specializationFeatures); const masteryFeatures = await getFeaturesHTMLData(this.masteryFeatures); diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 75e6dc8e..84e4de7f 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -28,7 +28,10 @@ export default class DHWeapon extends AttachableItem { equipped: new fields.BooleanField({ initial: false }), //SETTINGS - secondary: new fields.BooleanField({ initial: false, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon' }), + secondary: new fields.BooleanField({ + initial: false, + label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon.full' + }), burden: new fields.StringField({ required: true, choices: CONFIG.DH.GENERAL.burden, diff --git a/module/data/scene/scene.mjs b/module/data/scene/scene.mjs index f2a24308..50416573 100644 --- a/module/data/scene/scene.mjs +++ b/module/data/scene/scene.mjs @@ -19,7 +19,7 @@ export default class DHScene extends foundry.abstract.DataModel { close: new fields.NumberField({ integer: true, label: 'DAGGERHEART.CONFIG.Range.close.name' }), far: new fields.NumberField({ integer: true, label: 'DAGGERHEART.CONFIG.Range.far.name' }) }), - sceneEnvironments: new ForeignDocumentUUIDArrayField({ type: 'Actor', prune: true }) + sceneEnvironments: new ForeignDocumentUUIDArrayField({ type: 'Actor' }) }; } } diff --git a/module/data/settings/Homebrew.mjs b/module/data/settings/Homebrew.mjs index d4b7b03f..31247458 100644 --- a/module/data/settings/Homebrew.mjs +++ b/module/data/settings/Homebrew.mjs @@ -1,4 +1,5 @@ import { defaultRestOptions } from '../../config/generalConfig.mjs'; +import { resetAndRerenderActors } from '../../helpers/utils.mjs'; import { ActionsField } from '../fields/actionField.mjs'; const currencyField = (initial, label, icon) => @@ -54,7 +55,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel { maxDomains: new fields.NumberField({ required: true, integer: true, - min: 1, + min: 0, initial: 2, label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxDomains.label' }), @@ -196,6 +197,12 @@ export default class DhHomebrew extends foundry.abstract.DataModel { return source; } + _initialize(options) { + super._initialize(options); + this.maxDomains ||= Infinity; + this.maxLoadout ||= Infinity; + } + /** Invoked by the setting when data changes */ handleChange() { if (this.maxFear) { @@ -203,7 +210,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel { } this.refreshConfig(); - this.#resetActors(); + resetAndRerenderActors(); } /** Update config values based on homebrew data. Make sure the references don't change */ @@ -224,29 +231,6 @@ export default class DhHomebrew extends foundry.abstract.DataModel { }); } } - - /** - * Triggers a reset and non-forced re-render on all given actors (if given) - * or all world actors and actors in all scenes to show immediate results for a changed setting. - */ - #resetActors() { - const actors = new Set( - [ - game.actors.contents, - game.scenes.contents.flatMap(s => s.tokens.contents).flatMap(t => t.actor ?? []) - ].flat() - ); - for (const actor of actors) { - for (const app of Object.values(actor.apps)) { - for (const element of app.element?.querySelectorAll('prose-mirror.active')) { - element.open = false; // This triggers a save - } - } - - actor.reset(); - actor.render(); - } - } } export class Resource extends foundry.abstract.DataModel { diff --git a/module/data/settings/Metagaming.mjs b/module/data/settings/Metagaming.mjs index 2bb5afdf..3694e09e 100644 --- a/module/data/settings/Metagaming.mjs +++ b/module/data/settings/Metagaming.mjs @@ -1,3 +1,5 @@ +import { resetAndRerenderActors } from '../../helpers/utils.mjs'; + export default class DhMetagaming extends foundry.abstract.DataModel { static defineSchema() { const fields = foundry.data.fields; @@ -6,7 +8,24 @@ export default class DhMetagaming extends foundry.abstract.DataModel { initial: false, label: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hideObserverPermissionInChat.label', hint: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hideObserverPermissionInChat.hint' + }), + hidePartyStats: new fields.StringField({ + initial: 'never', + label: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hidePartyStats.label', + hint: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hidePartyStats.hint', + required: true, + nullable: false, + choices: { + never: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hidePartyStats.choices.never', + players: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hidePartyStats.choices.players', + always: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hidePartyStats.choices.always' + } }) }; } + + /** Invoked by the setting when data changes */ + handleChange() { + resetAndRerenderActors(); + } } diff --git a/module/dice/d20Roll.mjs b/module/dice/d20Roll.mjs index 509f5d69..b1d3bd0b 100644 --- a/module/dice/d20Roll.mjs +++ b/module/dice/d20Roll.mjs @@ -1,4 +1,5 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; +import { triggerChatRollFx } from '../helpers/utils.mjs'; import DHRoll from './dhRoll.mjs'; export default class D20Roll extends DHRoll { @@ -224,4 +225,15 @@ export default class D20Roll extends DHRoll { resetFormula() { return (this._formula = this.constructor.getFormula(this.terms)); } + + async reroll(options) { + const result = await super.reroll(options); + if (this instanceof game.system.api.dice.DualityRoll) return result; + + if (options?.liveRoll) { + await triggerChatRollFx([result]); + } + + return result; + } } diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index 98fd8401..ef810ed7 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -1,5 +1,5 @@ import DamageDialog from '../applications/dialogs/damageDialog.mjs'; -import { parseRallyDice } from '../helpers/utils.mjs'; +import { parseRallyDice, triggerChatRollFx } from '../helpers/utils.mjs'; import DHRoll from './dhRoll.mjs'; export default class DamageRoll extends DHRoll { @@ -18,7 +18,12 @@ export default class DamageRoll extends DHRoll { if (config.evaluate !== false) for (const roll of config.roll) await roll.roll.evaluate(); roll._evaluated = true; - const parts = config.roll.map(r => this.postEvaluate(r)); + + const parts = []; + for (const roll of config.roll) { + parts.push(this.postEvaluate(roll)); + roll.roll = JSON.stringify(roll.roll.toJSON()); + } config.damage = this.unifyDamageRoll(parts); } @@ -38,25 +43,24 @@ export default class DamageRoll extends DHRoll { const chatMessage = config.source?.message ? ui.chat.collection.get(config.source.message) : getDocumentClass('ChatMessage').applyMode({}, config.rollMode ?? 'public'); + + const diceRolls = []; if (game.modules.get('dice-so-nice')?.active) { - const pool = foundry.dice.terms.PoolTerm.fromRolls( - Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll)) - ), - diceRoll = Roll.fromTerms([pool]); - await game.dice3d.showForRoll( - diceRoll, - game.user, - true, - chatMessage.whisper?.length > 0 ? chatMessage.whisper : null, - chatMessage.blind - ); config.mute = true; + const pool = foundry.dice.terms.PoolTerm.fromRolls( + Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll)) + ); + diceRolls.push(Roll.fromTerms([pool])); } + + await triggerChatRollFx(diceRolls, { + whisper: chatMessage.whisper?.length > 0 ? chatMessage.whisper : null, + blind: chatMessage.blind + }); await super.buildPost(roll, config, message); + if (config.source?.message) { chatMessage.update({ 'system.damage': config.damage }); - - if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); } } @@ -319,9 +323,10 @@ export default class DamageRoll extends DHRoll { const newIndex = parsedDiceTerms[dice].results.length; await term.reroll(`/r1=${termResult.result}`); + const diceRolls = []; if (game.modules.get('dice-so-nice')?.active) { const newResult = parsedDiceTerms[dice].results[newIndex]; - const diceSoNiceRoll = { + diceRolls.push({ _evaluated: true, dice: [ new foundry.dice.terms.Die({ @@ -332,11 +337,10 @@ export default class DamageRoll extends DHRoll { }) ], options: { appearance: {} } - }; - - await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true); + }); } + await triggerChatRollFx(diceRolls); await parsedRoll.evaluate(); const results = parsedRoll.dice[dice].results.map(result => ({ diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index 209631f6..fb20870f 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -1,4 +1,5 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; +import { triggerChatRollFx } from '../helpers/utils.mjs'; export default class DHRoll extends Roll { baseTerms = []; @@ -36,6 +37,7 @@ export default class DHRoll extends Roll { 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; @@ -75,9 +77,7 @@ export default class DHRoll extends Roll { } if (config.skips?.createMessage) { - if (game.modules.get('dice-so-nice')?.active) { - await game.dice3d.showForRoll(roll, game.user, true); - } + await triggerChatRollFx([roll]); } else if (!config.source?.message) { config.message = await this.toMessage(roll, config); } @@ -85,6 +85,7 @@ export default class DHRoll extends Roll { static postEvaluate(roll, config = {}) { return { + ...roll.options.roll, total: roll.total, formula: roll.formula, dice: roll.dice.map(d => ({ @@ -141,8 +142,8 @@ export default class DHRoll extends Roll { const metagamingSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming); const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options }); return foundry.applications.handlebars.renderTemplate(template, { - ...chatData, roll: this, + ...chatData, parent: chatData.parent, targetMode: chatData.targetMode, areas: chatData.action?.areas, @@ -257,7 +258,7 @@ export default class DHRoll extends Roll { if (!roll.terms[i].isDeterministic) continue; const termTotal = roll.terms[i].total; if (typeof termTotal === 'number') { - const multiplier = roll.terms[i - 1]?.operator === ' - ' ? -1 : 1; + const multiplier = roll.terms[i - 1]?.operator === '-' ? -1 : 1; modifierTotal += multiplier * termTotal; } } diff --git a/module/dice/die/dualityDie.mjs b/module/dice/die/dualityDie.mjs index 83229425..cc7ee75e 100644 --- a/module/dice/die/dualityDie.mjs +++ b/module/dice/die/dualityDie.mjs @@ -1,4 +1,4 @@ -import { ResourceUpdateMap } from '../../data/action/baseAction.mjs'; +import { updateResourcesForDualityReroll } from '../helpers.mjs'; export default class DualityDie extends foundry.dice.terms.Die { constructor(options) { @@ -12,24 +12,6 @@ export default class DualityDie extends foundry.dice.terms.Die { return roll.withHope ? 1 : roll.withFear ? -1 : 0; } - #updateResources(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; - - const updates = []; - 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 resourceUpdates = new ResourceUpdateMap(actor); - resourceUpdates.addResources(updates); - resourceUpdates.updateResources(); - } - async reroll(modifier, options) { const oldDuality = this.#getDualityState(options.liveRoll.roll); await super.reroll(modifier, options); @@ -57,7 +39,7 @@ export default class DualityDie extends foundry.dice.terms.Die { if (options.liveRoll.isReaction) return; const newDuality = this.#getDualityState(options.liveRoll.roll); - this.#updateResources(oldDuality, newDuality, options.liveRoll.actor); + updateResourcesForDualityReroll(oldDuality, newDuality, options.liveRoll.actor); } } diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index ae50d307..2bd9c6d4 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -1,6 +1,8 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; import D20Roll from './d20Roll.mjs'; import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; +import { getDiceSoNicePresets } from '../config/generalConfig.mjs'; +import { updateResourcesForDualityReroll } from './helpers.mjs'; export default class DualityRoll extends D20Roll { _advantageNumber = 1; @@ -128,32 +130,34 @@ export default class DualityRoll extends D20Roll { } createBaseDice() { - if ( - this.dice[0] instanceof game.system.api.dice.diceTypes.HopeDie && - this.dice[1] instanceof game.system.api.dice.diceTypes.FearDie - ) { - this.terms = [this.terms[0], this.terms[1], this.terms[2]]; - return; - } + this.terms = [this.terms[0], this.terms[1], this.terms[2]]; this.terms[0] = new game.system.api.dice.diceTypes.HopeDie({ - faces: this.data.rules.roll?.hopeFaces ?? 12 + faces: this.terms[0]?.faces ?? this.data.rules.dualityRoll?.defaultHopeDice ?? 12 }); this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' }); this.terms[2] = new game.system.api.dice.diceTypes.FearDie({ - faces: this.data.rules.roll?.fearFaces ?? 12 + faces: this.terms[2]?.faces ?? this.data.rules.dualityRoll?.defaultFearDice ?? 12 }); } applyAdvantage() { - if (this.hasAdvantage || this.hasDisadvantage) { - const dieFaces = this.advantageFaces, - advDie = new foundry.dice.terms.Die({ faces: dieFaces, number: this.advantageNumber }); - if (this.advantageNumber > 1) advDie.modifiers = ['kh']; - this.terms.push( - new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }), - advDie - ); + const advDieClass = this.hasAdvantage + ? game.system.api.dice.diceTypes.AdvantageDie + : this.hasDisadvantage + ? game.system.api.dice.diceTypes.DisadvantageDie + : null; + if (advDieClass) { + const advDie = new advDieClass({ faces: this.advantageFaces, number: this.advantageNumber }); + if (this.terms.length < 4) { + if (this.advantageNumber > 1) advDie.modifiers = ['kh']; + this.terms.push( + new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }), + advDie + ); + } else { + this.terms[4] = advDie; + } } if (this.rallyFaces) this.terms.push( @@ -378,4 +382,40 @@ export default class DualityRoll extends D20Roll { if (currentCombatant?.actorId == config.data.id) ui.combat.setCombatantSpotlight(currentCombatant.id); } } + + async reroll(options) { + const oldDuality = this.withHope ? 1 : this.withFear ? -1 : 0; + const rerolled = DualityRoll.fromData((await super.reroll(options)).toJSON()); + + if (options?.liveRoll) { + if (game.modules.get('dice-so-nice')?.active) { + const diceAppearance = await getDiceSoNicePresets( + rerolled, + rerolled.dHope.denomination, + rerolled.dFear.denomination + ); + rerolled.dHope.options.appearance = diceAppearance.hope.appearance; + rerolled.dFear.options.appearance = diceAppearance.fear.appearance; + if (rerolled.dAdvantage) rerolled.dAdvantage.options.appearance = diceAppearance.advantage.appearance; + if (rerolled.dDisadvantage) + rerolled.dDisadvantage.options.appearance = diceAppearance.disadvantage.appearance; + + await game.dice3d.showForRoll(rerolled, game.user, true); + } else { + foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); + } + + if (this.options.actionType === 'reaction') return; + + const newDuality = rerolled.withHope ? 1 : rerolled.withFear ? -1 : 0; + const actor = await foundry.utils.fromUuid(this.options.source.actor); + updateResourcesForDualityReroll(oldDuality, newDuality, actor); + } + + return rerolled; + } + + fromJSON(json) { + return super.fromJSON(json); + } } diff --git a/module/dice/helpers.mjs b/module/dice/helpers.mjs new file mode 100644 index 00000000..35adb8b7 --- /dev/null +++ b/module/dice/helpers.mjs @@ -0,0 +1,19 @@ +import { ResourceUpdateMap } from '../data/action/baseAction.mjs'; + +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; + + const updates = []; + 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 resourceUpdates = new ResourceUpdateMap(actor); + resourceUpdates.addResources(updates); + resourceUpdates.updateResources(); +} diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index 227e2613..f9239a90 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -169,27 +169,37 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { super._applyChangeUnguided(actor, change, changes, options); } + /** Recursively finds the first parent document of the given object */ + static #resolveParentDocument(model, documentClass) { + if (!model) return null; + return model instanceof documentClass + ? model + : model.parent + ? this.#resolveParentDocument(model.parent, documentClass) + : null; + } + static getChangeValue(model, change, effect) { - let key = change.value.toString(); - const isOriginTarget = key.toLowerCase().includes('origin.@'); - let parseModel = model; - if (isOriginTarget && effect.origin) { - key = change.key.replaceAll(/origin\.@/gi, '@'); - try { - const originEffect = foundry.utils.fromUuidSync(effect.origin); - const doc = - originEffect.parent?.parent instanceof game.system.api.documents.DhpActor - ? originEffect.parent - : originEffect.parent.parent; - if (doc) parseModel = doc; - } catch (_) {} + let value = change.value.toString(); + const useOrigin = value.toLowerCase().includes('origin.@') && effect.origin; + let origin = null; + if (effect.origin) { + if (useOrigin) value = value.replaceAll(/origin\.@/gi, '@'); + const originEffect = foundry.utils.fromUuidSync(effect.origin); + origin = this.#resolveParentDocument(originEffect, Item); } + // Get the actor and item documents. Note that actor roll data is inclusive of system roll data + const actor = this.#resolveParentDocument(model, Actor); + const item = + (useOrigin ? origin : null) ?? + this.#resolveParentDocument(effect.parent, Item) ?? + (origin?.actor === actor ? origin : null); const stackingParsedValue = effect.system.stacking - ? Roll.replaceFormulaData(key, { stacks: effect.system.stacking.value }) - : key; - const evalValue = itemAbleRollParse(stackingParsedValue, parseModel, effect.parent); - return evalValue ?? key; + ? Roll.replaceFormulaData(value, { stacks: effect.system.stacking.value }) + : value; + const evalValue = this.effectSafeEval(itemAbleRollParse(stackingParsedValue, actor, item)); + return evalValue ?? value; } /** diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index db249033..fb10435f 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -1,7 +1,7 @@ -import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs'; +import { emitGMUpdate, GMUpdateEvent } from '../systemRegistration/socket.mjs'; import { LevelOptionType } from '../data/levelTier.mjs'; import DHFeature from '../data/item/feature.mjs'; -import { createScrollText, damageKeyToNumber, getDamageKey } from '../helpers/utils.mjs'; +import { createScrollText, damageKeyToNumber, getDamageKey, createShallowProxy } from '../helpers/utils.mjs'; import DhCompanionLevelUp from '../applications/levelup/companionLevelup.mjs'; import { ResourceUpdateMap } from '../data/action/baseAction.mjs'; import { abilities } from '../config/actorConfig.mjs'; @@ -65,6 +65,11 @@ export default class DhpActor extends Actor { }; } + static createDialog(data, createOptions, options, renderOptions) { + options.classes = [options.classes ?? [], 'actor-create'].flat(); // handled in hook + return super.createDialog(data, createOptions, options, renderOptions); + } + /* -------------------------------------------- */ /** @inheritDoc */ @@ -99,7 +104,7 @@ export default class DhpActor extends Actor { } // Configure prototype token settings - if (['character', 'companion', 'party'].includes(this.type)) + if (['character', 'companion', 'party'].includes(this.type)) { Object.assign(update, { prototypeToken: { sight: { enabled: true }, @@ -107,13 +112,35 @@ export default class DhpActor extends Actor { disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY } }); + } + + if (this.type === 'npc') { + Object.assign(update, { + prototypeToken: { + disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY + } + }); + } + this.updateSource(update); } + /** Perform a render, debounced in order to prevent overloading repeat render requests */ + renderDebounced = foundry.utils.debounce(options => { + return this.render(options); + }, 10); + + _onUpdateDescendantDocuments(parent, collection, documents, changes, options, userId) { + super._onUpdateDescendantDocuments(parent, collection, documents, changes, options, userId); + for (const party of this.parties) { + party.renderDebounced({ parts: ['partyMembers'] }); + } + } + _onUpdate(changes, options, userId) { super._onUpdate(changes, options, userId); for (const party of this.parties) { - party.render({ parts: ['partyMembers'] }); + party.renderDebounced({ parts: ['partyMembers'] }); } } @@ -132,17 +159,20 @@ export default class DhpActor extends Actor { _onDelete(options, userId) { super._onDelete(options, userId); for (const party of this.parties) { - party.render({ parts: ['partyMembers'] }); + party.renderDebounced({ parts: ['partyMembers'] }); } } async updateLevel(newLevel) { if (!['character', 'companion'].includes(this.type) || newLevel === this.system.levelData.level.changed) return; + const tiers = Object.values(game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers); + const maxLevel = tiers.reduce((acc, tier) => Math.max(acc, tier.levels.end), 0); + const multiclassMinLevel = Math.min( + maxLevel, + ...tiers.filter(t => t.options.multiclass).map(t => t.levels.start) + ); if (newLevel > this.system.levelData.level.current) { - const maxLevel = Object.values( - game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers - ).reduce((acc, tier) => Math.max(acc, tier.levels.end), 0); if (newLevel > maxLevel) { ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.tooHighLevel')); } @@ -217,18 +247,19 @@ export default class DhpActor extends Actor { this.system.multiclass.subclass.update({ 'system.featureState': subclassFeatureState.multiclass }); } - if (multiclass) { - const multiclassItem = this.items.find(x => x.uuid === multiclass.itemUuid); - const multiclassFeatures = this.items.filter( - x => x.system.originItemType === 'class' && x.system.multiclassOrigin - ); - const subclassFeatures = this.items.filter( - x => x.system.originItemType === 'subclass' && x.system.multiclassOrigin + // Remove multiclass if we're removing a multiclass feature or if we're below the multiclass minimum level + // Multclasses cannot be manually removed on the sheet, so this allows recovering in the case of errors + if (multiclass || newLevel < multiclassMinLevel) { + const multiclassItems = this.items.filter( + x => + x.uuid === multiclass?.itemUuid || + x.system.isMulticlass || + (['class', 'subclass'].includes(x.system.originItemType) && x.system.multiclassOrigin) ); this.deleteEmbeddedDocuments( 'Item', - [multiclassItem, ...multiclassFeatures, ...subclassFeatures].map(x => x.id) + multiclassItems.map(x => x.id) ); this.update({ @@ -267,6 +298,7 @@ export default class DhpActor extends Actor { async levelUp(levelupData) { const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto; + const getStatsWithSource = document => ({ ...(document._stats ?? {}), compendiumSource: document.uuid }); const levelups = {}; for (var levelKey of Object.keys(levelupData)) { @@ -379,8 +411,8 @@ export default class DhpActor extends Actor { const embeddedItem = await this.createEmbeddedDocuments('Item', [ { ...multiclassData, - uuid: multiclassItem.uuid, - _stats: multiclassItem._stats, + uuid: multiclassItem.uuid, // todo: replace with setting an id and using keepId + _stats: getStatsWithSource(multiclassItem), system: { ...multiclassData.system, features: multiclassData.system.features.filter(x => x.type !== 'hope'), @@ -393,8 +425,8 @@ export default class DhpActor extends Actor { await this.createEmbeddedDocuments('Item', [ { ...subclassData, - uuid: subclassItem.uuid, - _stats: subclassItem._stats, + uuid: subclassItem.uuid, // todo: replace with setting an id and using keepId + _stats: getStatsWithSource(subclassItem), system: { ...subclassData.system, isMulticlass: true @@ -414,8 +446,8 @@ export default class DhpActor extends Actor { const embeddedItem = await this.createEmbeddedDocuments('Item', [ { ...cardData, - uuid: cardItem.uuid, - _stats: cardItem._stats, + uuid: cardItem.uuid, // todo: replace with setting an id and using keepId + _stats: getStatsWithSource(cardItem), system: { ...cardData.system, inVault: true @@ -436,8 +468,7 @@ export default class DhpActor extends Actor { const embeddedItem = await this.createEmbeddedDocuments('Item', [ { ...cardData, - uuid: cardItem.uuid, - _stats: cardItem._stats, + _stats: getStatsWithSource(cardItem), system: { ...cardData.system, inVault: true @@ -593,10 +624,7 @@ export default class DhpActor extends Actor { /**@inheritdoc */ getRollData() { - const rollData = foundry.utils.deepClone(super.getRollData()); - /* system gets repeated infinately which causes issues when trying to use the data for document creation */ - delete rollData.system; - + const rollData = createShallowProxy(super.getRollData()); rollData.id = this.id; rollData.name = this.name; rollData.system = this.system.getRollData(); @@ -828,7 +856,7 @@ export default class DhpActor extends Actor { const u = updates[key]; if (key === 'items') { Object.values(u).forEach(async item => { - await emitAsGM( + await emitGMUpdate( GMUpdateEvent.UpdateDocument, item.target.update.bind(item.target), item.resources, @@ -837,7 +865,7 @@ export default class DhpActor extends Actor { }); } else { if (Object.keys(u.resources).length > 0) { - await emitAsGM( + await emitGMUpdate( GMUpdateEvent.UpdateDocument, u.target.update.bind(u.target), u.resources, diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 2e20fb87..893e6e5c 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -1,4 +1,4 @@ -import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs'; +import { emitGMUpdate, emitGMCreate, GMUpdateEvent } from '../systemRegistration/socket.mjs'; export default class DhpChatMessage extends foundry.documents.ChatMessage { targetHook = null; @@ -183,7 +183,11 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { if (pendingingSaves.length) { const confirm = await foundry.applications.api.DialogV2.confirm({ window: { title: game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.title') }, - content: `

${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.unfinishedRolls')}

${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.confirmation')}

${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.warning')}

` + content: ` +

${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.unfinishedRolls')}

+

${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.warning')}

+

${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.confirmation')}

+ ` }); if (!confirm) return; } @@ -214,7 +218,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { const action = this.system.action; if (!action || !action?.hasSave) return; game.system.api.fields.ActionFields.SaveField.rollSave.call(action, token.actor, event).then(result => - emitAsGM( + emitGMUpdate( GMUpdateEvent.UpdateSaveMessage, game.system.api.fields.ActionFields.SaveField.updateSaveMessage.bind( action, @@ -247,8 +251,24 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { const targets = this.filterPermTargets(this.system.hitTargets), config = foundry.utils.deepClone(this.system); config.event = event; + if (targets.length === 0) - ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm')); + return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm')); + else if (config.hasSave) { + const pendingingSaves = targets.filter(t => t.saved.success === null); + if (pendingingSaves.length) { + const confirm = await foundry.applications.api.DialogV2.confirm({ + window: { title: game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.title') }, + content: ` +

${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.unfinishedRolls')}

+

${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.warning')}

+

${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.confirmation')}

+ ` + }); + if (!confirm) return; + } + } + this.consumeOnSuccess(); this.system.action?.workflow.get('effects')?.execute(config, targets, true); } @@ -259,27 +279,47 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { const { shape: type, size: range } = selectedArea; const shapeData = CONFIG.Canvas.layers.regions.layerClass.getTemplateShape({ type, range }); - await canvas.regions.placeRegion( - { - name: selectedArea.name, - shapes: [shapeData], - restriction: { enabled: false, type: 'move', priority: 0 }, - behaviors: [ - { - name: game.i18n.localize('TYPES.RegionBehavior.applyActiveEffect'), - type: 'applyActiveEffect', - system: { - effects: effects - } - } - ], - displayMeasurements: true, - locked: false, - ownership: { default: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE }, - visibility: CONST.REGION_VISIBILITY.ALWAYS - }, - { create: true } - ); + const scene = game.scenes.get(game.user.viewedScene); + const level = scene.levels.find(x => x.isView); + + const regionData = { + name: selectedArea.name, + levels: level ? [level.id] : [], + shapes: [shapeData], + restriction: { enabled: false, type: 'move', priority: 0 }, + behaviors: + effects.length > 0 + ? [ + { + name: game.i18n.localize('TYPES.RegionBehavior.applyActiveEffect'), + type: 'applyActiveEffect', + system: { + effects: effects + } + } + ] + : [], + displayMeasurements: true, + locked: false, + ownership: { default: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE }, + visibility: CONST.REGION_VISIBILITY.ALWAYS + }; + const placeRegion = data => { + canvas.regions.placeRegion(data, { create: true }); + }; + + // Regions with effects must be placed by the GM + if (effects.length > 0 && !game.user.isGM) { + if (!game.users.activeGM) + return ui.notifications.error( + game.i18n.localize('DAGGERHEART.UI.Notifications.behaviorRegionRequiresGM') + ); + + const region = await canvas.regions.placeRegion(regionData, { create: false }); + emitGMCreate('Region', placeRegion, region, scene.id); + } else { + placeRegion(regionData); + } }; if (this.system.action.areas.length === 1) createArea(this.system.action.areas[0]); diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 8ece56fa..f46e24e6 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -54,13 +54,7 @@ export default class DHItem extends foundry.documents.Item { * @returns */ getRollData(options = {}) { - let data; - if (this.system.getRollData) data = this.system.getRollData(options); - else { - const actorRollData = this.actor?.getRollData(options) ?? {}; - data = { ...actorRollData, item: { ...this.system } }; - } - + let data = this.system.getRollData(options); if (data?.item) { data.item.flags = { ...this.flags }; data.item.name = this.name; @@ -79,13 +73,16 @@ export default class DHItem extends foundry.documents.Item { /** Returns true if the item can be used */ get usable() { const actor = this.actor; - const actionsList = this.system.actionsList; - return this.isOwner && actor?.type === 'character' && (actionsList?.size || actionsList?.length); + const pack = actor?.pack ? game.packs.get(actor.pack) : null; + const hasActions = this.system.actionsList?.size || this.system.actionsList?.length; + const isValidType = actor?.type === 'character' || this.type === 'feature'; + return !pack?.locked && this.isOwner && isValidType && hasActions; } /** @inheritdoc */ static async createDialog(data = {}, createOptions = {}, options = {}) { const { folders, types, template, context = {}, ...dialogOptions } = options; + dialogOptions.classes = [options.classes ?? [], 'item-create'].flat(); // handled in hook if (types?.length === 0) { throw new Error('The array of sub-types to restrict to must not be empty.'); diff --git a/module/documents/scene.mjs b/module/documents/scene.mjs index 59b8091f..bf700610 100644 --- a/module/documents/scene.mjs +++ b/module/documents/scene.mjs @@ -20,7 +20,7 @@ export default class DhScene extends Scene { const prototype = tokenDoc.actor?.prototypeToken ?? tokenDoc; this.#sizeSyncBatch.set(tokenDoc.id, { size: tokenSize, - prototypeSize: { width: prototype.width, height: prototype.height }, + prototypeSize: { width: prototype.width, height: prototype.height, depth: prototype.depth }, position: { x: tokenDoc.x, y: tokenDoc.y, elevation: tokenDoc.elevation } }); this.#processSyncBatch(); @@ -36,11 +36,13 @@ export default class DhScene extends Scene { const tokenSize = tokenSizes[size]; const width = size !== CONFIG.DH.ACTOR.tokenSize.custom.id ? tokenSize : prototypeSize.width; const height = size !== CONFIG.DH.ACTOR.tokenSize.custom.id ? tokenSize : prototypeSize.height; + const depth = size !== CONFIG.DH.ACTOR.tokenSize.custom.id ? tokenSize : prototypeSize.depth; const updatedPosition = DHToken.getSnappedPositionInSquareGrid(this.grid, position, width, height); return { _id, width, height, + depth, ...updatedPosition }; }); diff --git a/module/documents/token.mjs b/module/documents/token.mjs index 4ee7ce05..30862724 100644 --- a/module/documents/token.mjs +++ b/module/documents/token.mjs @@ -66,7 +66,8 @@ export default class DHToken extends CONFIG.Token.documentClass { if (tokenSize && actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) { document.updateSource({ width: tokenSize, - height: tokenSize + height: tokenSize, + depth: tokenSize }); } } @@ -90,7 +91,7 @@ export default class DHToken extends CONFIG.Token.documentClass { ) { const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes; const tokenSize = tokenSizes[update.system.size]; - if (tokenSize !== this.width || tokenSize !== this.height) { + if (tokenSize !== this.width || tokenSize !== this.height || tokenSize !== this.depth) { this.parent?.syncTokenDimensions(this, update.system.size); } } diff --git a/module/documents/tokenManager.mjs b/module/documents/tokenManager.mjs index 3ccff4e2..7678d2c7 100644 --- a/module/documents/tokenManager.mjs +++ b/module/documents/tokenManager.mjs @@ -15,6 +15,7 @@ export default class DhTokenManager { if (tokenSize && actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) { tokenData.width = tokenSize; tokenData.height = tokenSize; + tokenData.depth = tokenSize; } } diff --git a/module/documents/tooltipManager.mjs b/module/documents/tooltipManager.mjs index e10dc5fa..3e3f4a16 100644 --- a/module/documents/tooltipManager.mjs +++ b/module/documents/tooltipManager.mjs @@ -3,7 +3,6 @@ import { AdversaryBPPerEncounter, BaseBPPerEncounter } from '../config/encounter export default class DhTooltipManager extends foundry.helpers.interaction.TooltipManager { #wide = false; #bordered = false; - #active = false; async activate(element, options = {}) { const { TextEditor } = foundry.applications.ux; @@ -350,7 +349,9 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti async getBattlepointHTML(combatId) { const combat = game.combats.get(combatId); const adversaries = - combat.turns?.filter(x => x.actor?.isNPC)?.map(x => ({ ...x.actor, type: x.actor.system.type })) ?? []; + combat.turns + ?.filter(x => x.actor?.isNPC && x.token.disposition === CONST.TOKEN_DISPOSITIONS.HOSTILE) + ?.map(x => ({ ...x.actor, type: x.actor.system.type })) ?? []; const characters = combat.turns?.filter(x => !x.isNPC && x.actor) ?? []; const nrCharacters = characters.length; 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/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 4527da1a..ddc353b1 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -189,17 +189,14 @@ export const getDeleteKeys = (property, innerProperty, innerPropertyDefaultValue // Fix on Foundry native formula replacement for DH const nativeReplaceFormulaData = Roll.replaceFormulaData; -Roll.replaceFormulaData = function (formula, baseData = {}, { missing, warn = false } = {}) { +Roll.replaceFormulaData = function (formula, data = {}, { missing, warn = false } = {}) { /* Inserting global data */ - const data = { - ...baseData, - partySize: game.actors?.party?.system.partyMembers.length ?? 0 - }; + const defaultingTypes = [ + ...Object.keys(CONFIG.DH.GENERAL.multiplierTypes).map(x => ({ term: x, default: 1 })), + { term: 'partySize', default: game.actors?.party?.system.partyMembers.length ?? 0 } + ]; - const terms = Object.keys(CONFIG.DH.GENERAL.multiplierTypes).map(type => { - return { term: type, default: 1 }; - }); - formula = terms.reduce((a, c) => a.replaceAll(`@${c.term}`, data[c.term] ?? c.default), formula); + formula = defaultingTypes.reduce((a, c) => a.replaceAll(`@${c.term}`, data[c.term] ?? c.default), formula); return nativeReplaceFormulaData(formula, data, { missing, warn }); }; @@ -375,10 +372,11 @@ export const itemAbleRollParse = (value, actor, item) => { const isItemTarget = value.toLowerCase().includes('item.@'); const slicedValue = isItemTarget ? value.replaceAll(/item\.@/gi, '@') : value; - const model = isItemTarget ? item : actor; + const model = isItemTarget || item instanceof Item ? item : actor; + const rollData = isItemTarget || !model?.getRollData ? model : model.getRollData(); try { - return Roll.replaceFormulaData(slicedValue, isItemTarget || !model?.getRollData ? model : model.getRollData()); + return Roll.replaceFormulaData(slicedValue, rollData); } catch (_) { return ''; } @@ -451,14 +449,9 @@ export async function createEmbeddedItemsWithEffects(actor, baseData) { effects: data.effects?.map(effect => effect.toObject()) }); } - await actor.createEmbeddedDocuments('Item', effectData); } -export const slugify = name => { - return name.toLowerCase().replaceAll(' ', '-').replaceAll('.', ''); -}; - export function shuffleArray(array) { let currentIndex = array.length; while (currentIndex != 0) { @@ -759,9 +752,12 @@ export function getArmorSources(actor) { // Get the origin item. Since the actor is already loaded, it should already be cached // Consider the relative function versions if this causes an issue const origin = doc.origin ? foundry.utils.fromUuidSync(doc.origin) : doc; + const useParentName = doc.parent && !(doc.parent instanceof Actor); + const name = doc.origin || !useParentName ? doc.name : doc.parent.name; + return { origin, - name: origin.name, + name, document: doc, data: doc.system.armor ?? doc.system.armorData, disabled: !!doc.disabled || !!doc.isSuppressed @@ -792,6 +788,26 @@ export function getArmorSources(actor) { }); } +/** + * Triggers a reset and non-forced re-render on all given actors (if given) + * or all world actors and actors in all scenes to show immediate results for a changed setting. + */ +export function resetAndRerenderActors() { + const actors = new Set( + [game.actors.contents, game.scenes.contents.flatMap(s => s.tokens.contents).flatMap(t => t.actor ?? [])].flat() + ); + for (const actor of actors) { + for (const app of Object.values(actor.apps)) { + for (const element of app.element?.querySelectorAll('prose-mirror.active')) { + element.open = false; // This triggers a save + } + } + + actor.reset(); + actor.render(); + } +} + /** * Returns an array sorted by a function that returns a thing to compare, or an array to compare in order * Similar to lodash's sortBy function. @@ -812,3 +828,94 @@ export function sortBy(arr, fn) { }; return arr.sort(cmp); } + +/** + * Creates a proxy that allows retrieval of top data but diverts updates to a different object. + * Generally used for roll data + */ +export function createShallowProxy(obj) { + if (obj._isShallowProxy) return obj; + + const overrides = {}; + return new Proxy(obj, { + get(target, prop, receiver) { + if (prop === '_isShallowProxy') return true; + if (prop in overrides) return overrides[prop]; + return Reflect.get(target, prop, receiver); + }, + set(_target, prop, newValue) { + overrides[prop] = newValue; + return true; + }, + deleteProperty(_target, prop) { + delete overrides[prop]; + return true; + }, + has(target, key) { + return key in overrides || key in target; + } + }); +} + +export function camelize(str) { + return str + .replace(/(?:^\w|[A-Z]|\b\w)/g, (part, index) => { + return index === 0 ? part.toLowerCase() : part.toUpperCase(); + }) + .replace(/\s+/g, ''); +} + +/** Bulk load a list of documents using uuids. Returns the documents in the same order */ +export async function fromUuids(uuids) { + // Set up base entries. Each step works on a sublist of these objects + const entries = uuids.map(uuid => ({ + uuid, + parsed: foundry.utils.parseUuid(uuid), + value: foundry.utils.fromUuidSync(uuid) + })); + + // Handle missing uuids for embedded documents first + // A value may be index data, so we check if its a document + const packEmbeddedEntries = entries.filter( + e => + !(e.value instanceof Document) && + e.parsed && + e.parsed.collection instanceof foundry.documents.collections.CompendiumCollection && + e.parsed.embedded.length > 0 + ); + await Promise.all( + packEmbeddedEntries.map(async e => { + e.value = await fromUuid(e.uuid); + return true; + }) + ); + + // Handle missing top level pack stuff, by batching per pack + const missingTopLevel = entries.filter(e => !(e.value instanceof Document) && e.value?.pack); + for (const packGroup of Object.values(Object.groupBy(missingTopLevel, e => e.value.pack))) { + const pack = game.packs.get(packGroup[0].value.pack); + if (!pack) continue; + + 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; + } + } + + return entries.map(e => e.value); +} +/** + * Triggers DiceSoNice rolls or dice roll audio for rolls. Not used for duality rolls. + * @param { Roll[] } rolls + * @return { void } + */ +export async function triggerChatRollFx(rolls, options = { whisper: false, blind: false }) { + const { whisper, blind } = options; + if (game.modules.get('dice-so-nice')?.active) { + const rerollPromises = rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true, whisper, blind)); + await Promise.allSettled(rerollPromises); + } else { + foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); + } +} diff --git a/module/systemRegistration/settings.mjs b/module/systemRegistration/settings.mjs index 41cab011..a66323c7 100644 --- a/module/systemRegistration/settings.mjs +++ b/module/systemRegistration/settings.mjs @@ -91,7 +91,10 @@ const registerMenuSettings = () => { game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming, { scope: 'world', config: false, - type: DhMetagaming + type: DhMetagaming, + onChange: value => { + value.handleChange(); + } }); game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, { diff --git a/module/systemRegistration/socket.mjs b/module/systemRegistration/socket.mjs index 8fed346d..de9bf00c 100644 --- a/module/systemRegistration/socket.mjs +++ b/module/systemRegistration/socket.mjs @@ -6,6 +6,9 @@ export function handleSocketEvent({ action = null, data = {} } = {}) { case socketEvent.GMUpdate: Hooks.callAll(socketEvent.GMUpdate, data); break; + case socketEvent.GMCreate: + Hooks.callAll(socketEvent.GMCreate, data); + break; case socketEvent.DhpFearUpdate: Hooks.callAll(socketEvent.DhpFearUpdate); break; @@ -25,6 +28,7 @@ export function handleSocketEvent({ action = null, data = {} } = {}) { export const socketEvent = { GMUpdate: 'DhGMUpdate', + GMCreate: 'DhGMCreate', Refresh: 'DhRefresh', DhpFearUpdate: 'DhFearUpdate', DowntimeTrigger: 'DowntimeTrigger', @@ -56,14 +60,14 @@ export const registerSocketHooks = () => { const document = data.uuid ? await fromUuid(data.uuid) : null; switch (data.action) { case GMUpdateEvent.UpdateDocument: - if (document && data.update) await document.update(data.update); + if (document && data.data) await document.update(data.data); break; case GMUpdateEvent.UpdateEffect: - if (document && data.update) - await game.system.api.fields.ActionFields.EffectsField.applyEffects.call(document, data.update); + if (document && data.data) + await game.system.api.fields.ActionFields.EffectsField.applyEffects.call(document, data.data); break; case GMUpdateEvent.UpdateSetting: - await game.settings.set(CONFIG.DH.id, data.uuid, data.update); + await game.settings.set(CONFIG.DH.id, data.uuid, data.data); break; case GMUpdateEvent.UpdateFear: await game.settings.set( @@ -73,22 +77,22 @@ export const registerSocketHooks = () => { 0, Math.min( game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear, - data.update + data.data ) ) ); break; case GMUpdateEvent.UpdateCountdowns: - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data.update); + await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data.data); Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown }); break; case GMUpdateEvent.UpdateSaveMessage: - const message = game.messages.get(data.update.message); + const message = game.messages.get(data.data.message); if (!message) return; game.system.api.fields.ActionFields.SaveField.updateSaveMessage( - data.update.result, + data.data.result, message, - data.update.token + data.data.token ); break; } @@ -102,6 +106,17 @@ export const registerSocketHooks = () => { } } }); + + Hooks.on(socketEvent.GMCreate, async ({ data, documentType, scene }) => { + if (!game.user.isGM) return; + + switch (documentType) { + default: + const cls = getDocumentClass(documentType); + cls.create(data, { parent: game.scenes.get(scene) }); + break; + } + }); }; export const registerUserQueries = () => { @@ -109,18 +124,21 @@ export const registerUserQueries = () => { CONFIG.queries.reactionRoll = game.system.api.fields.ActionFields.SaveField.rollSaveQuery; }; -export const emitAsGM = async (eventName, callback, update, uuid = null, refresh = null) => { +export const emitGMUpdate = async (eventName, callback, update, uuid = null, refresh = null) => { + return await emitAsGM(socketEvent.GMUpdate, { action: eventName, callback, data: update, uuid, refresh }); +}; + +export const emitGMCreate = async (documentType, callback, data, scene) => { + return await emitAsGM(socketEvent.GMCreate, { documentType, callback, data, scene }); +}; + +export const emitAsGM = async (event, data = { callback: () => {}, data: {} }) => { if (!game.user.isGM) { return await game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.GMUpdate, - data: { - action: eventName, - uuid, - update, - refresh - } + action: event, + data: data }); - } else return callback(update); + } else return data.callback(data.data); }; export const emitAsOwner = (eventName, userId, args) => { diff --git a/package-lock.json b/package-lock.json index b8fef1cd..28223032 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "autocompleter": "^9.3.2", "gulp": "^5.0.0", "gulp-less": "^5.0.0", + "gulp-sourcemaps": "^3.0.0", "rollup": "^4.40.0" }, "devDependencies": { @@ -19,8 +20,8 @@ "eslint": "^10.2.1", "eslint-plugin-prettier": "^5.5.5", "globals": "^17.5.0", - "husky": "^9.1.5", - "lint-staged": "^15.2.10", + "husky": "^9.1.7", + "lint-staged": "^16.4.0", "postcss": "^8.4.32", "prettier": "^3.5.3", "rollup-plugin-postcss": "^4.0.2" @@ -202,6 +203,132 @@ "node": ">17.0.0" } }, + "node_modules/@gulp-sourcemaps/identity-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", + "integrity": "sha512-Tb+nSISZku+eQ4X1lAkevcQa+jknn/OVUgZ3XCxEKIsLsqYuPoJwJOPQeaOk75X3WPftb29GWY1eqE7GLsXb1Q==", + "license": "MIT", + "dependencies": { + "acorn": "^6.4.1", + "normalize-path": "^3.0.0", + "postcss": "^7.0.16", + "source-map": "^0.6.0", + "through2": "^3.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@gulp-sourcemaps/identity-map/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/@gulp-sourcemaps/identity-map/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "license": "ISC" + }, + "node_modules/@gulp-sourcemaps/identity-map/node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "license": "MIT", + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/@gulp-sourcemaps/identity-map/node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "node_modules/@gulp-sourcemaps/map-sources": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", + "integrity": "sha512-o/EatdaGt8+x2qpb0vFLC/2Gug/xYPRXb6a+ET1wGYKozKN3krDWC/zZFZAtrzxJHuDL12mwdfEFKcKMNvc55A==", + "license": "MIT", + "dependencies": { + "normalize-path": "^2.0.1", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@gulp-sourcemaps/map-sources/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@gulp-sourcemaps/map-sources/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/@gulp-sourcemaps/map-sources/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/@gulp-sourcemaps/map-sources/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/@gulp-sourcemaps/map-sources/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/@gulpjs/messages": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@gulpjs/messages/-/messages-1.1.0.tgz", @@ -752,10 +879,11 @@ } }, "node_modules/ansi-escapes": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", "dev": true, + "license": "MIT", "dependencies": { "environment": "^1.0.0" }, @@ -767,10 +895,11 @@ } }, "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -779,10 +908,11 @@ } }, "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -891,6 +1021,18 @@ "node": ">= 10.13.0" } }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, "node_modules/autocompleter": { "version": "9.3.2", "resolved": "https://registry.npmjs.org/autocompleter/-/autocompleter-9.3.2.tgz", @@ -1207,6 +1349,7 @@ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, + "license": "MIT", "dependencies": { "restore-cursor": "^5.0.0" }, @@ -1218,39 +1361,34 @@ } }, "node_modules/cli-truncate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", "dev": true, + "license": "MIT", "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-truncate/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true - }, "node_modules/cli-truncate/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1357,15 +1495,17 @@ "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/commondir": { @@ -1481,6 +1621,12 @@ "node": ">= 10.13.0" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1495,6 +1641,17 @@ "node": ">= 8" } }, + "node_modules/css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + } + }, "node_modules/css-declaration-sorter": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", @@ -1666,6 +1823,19 @@ "node": ">=8.0.0" } }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -1699,6 +1869,35 @@ } } }, + "node_modules/debug-fabulous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-1.1.0.tgz", + "integrity": "sha512-GZqvGIgKNlUnHUPQhepnUZFIMoi3dgZKQBzKDeL2g7oJF9SNAji/AAu36dusFUas0O+pae74lNeoIPHqXWDkLg==", + "license": "MIT", + "dependencies": { + "debug": "3.X", + "memoizee": "0.4.X", + "object-assign": "4.X" + } + }, + "node_modules/debug-fabulous/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1740,6 +1939,15 @@ "node": ">=0.10.0" } }, + "node_modules/detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -1854,6 +2062,7 @@ "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -1903,6 +2112,58 @@ "node": ">= 0.4" } }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2104,6 +2365,21 @@ "node": ">=6" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/espree": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", @@ -2174,35 +2450,23 @@ "node": ">=0.10.0" } }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true - }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" + "d": "1", + "es5-ext": "~0.10.14" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, "node_modules/expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", @@ -2214,6 +2478,15 @@ "node": ">=0.10.0" } }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -2471,10 +2744,11 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -2519,18 +2793,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", @@ -2851,6 +3113,86 @@ "node": ">=6" } }, + "node_modules/gulp-sourcemaps": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-3.0.0.tgz", + "integrity": "sha512-RqvUckJkuYqy4VaIH60RMal4ZtG0IbQ6PXMNkNsshEGJ9cldUPRb/YCgboYae+CLAs1HQNb4ADTKCx65HInquQ==", + "license": "ISC", + "dependencies": { + "@gulp-sourcemaps/identity-map": "^2.0.1", + "@gulp-sourcemaps/map-sources": "^1.0.0", + "acorn": "^6.4.1", + "convert-source-map": "^1.0.0", + "css": "^3.0.0", + "debug-fabulous": "^1.0.0", + "detect-newline": "^2.0.0", + "graceful-fs": "^4.0.0", + "source-map": "^0.6.0", + "strip-bom-string": "^1.0.0", + "through2": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gulp-sourcemaps/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/gulp-sourcemaps/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/gulp-sourcemaps/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/gulp-sourcemaps/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/gulp-sourcemaps/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/gulp-sourcemaps/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/gulplog": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-2.2.0.tgz", @@ -2931,20 +3273,12 @@ "node": ">=0.10.0" } }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "engines": { - "node": ">=16.17.0" - } - }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true, + "license": "MIT", "bin": { "husky": "bin.js" }, @@ -3213,12 +3547,16 @@ } }, "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -3283,6 +3621,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -3321,18 +3665,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-typed-array": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", @@ -3380,6 +3712,12 @@ "node": ">=0.10.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3545,52 +3883,38 @@ "node": ">=10.13.0" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/lint-staged": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.2.tgz", - "integrity": "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", + "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==", "dev": true, + "license": "MIT", "dependencies": { - "chalk": "^5.4.1", - "commander": "^13.1.0", - "debug": "^4.4.0", - "execa": "^8.0.1", - "lilconfig": "^3.1.3", - "listr2": "^8.2.5", - "micromatch": "^4.0.8", - "pidtree": "^0.6.0", + "commander": "^14.0.3", + "listr2": "^9.0.5", + "picomatch": "^4.0.3", "string-argv": "^0.3.2", - "yaml": "^2.7.0" + "tinyexec": "^1.0.4", + "yaml": "^2.8.2" }, "bin": { "lint-staged": "bin/lint-staged.js" }, "engines": { - "node": ">=18.12.0" + "node": ">=20.17" }, "funding": { "url": "https://opencollective.com/lint-staged" } }, "node_modules/listr2": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", - "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "dev": true, + "license": "MIT", "dependencies": { - "cli-truncate": "^4.0.0", + "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", @@ -3598,7 +3922,7 @@ "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/loader-utils": { @@ -3664,6 +3988,7 @@ "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, + "license": "MIT", "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", @@ -3678,26 +4003,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", - "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", - "dev": true, - "dependencies": { - "get-east-asian-width": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", - "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" @@ -3721,6 +4032,15 @@ "loose-envify": "cli.js" } }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "license": "MIT", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -3766,11 +4086,24 @@ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", "dev": true }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } }, "node_modules/micromatch": { "version": "4.0.8", @@ -3807,23 +4140,12 @@ "node": ">=4" } }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -3870,8 +4192,7 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/mute-stdout": { "version": "2.0.0", @@ -3937,6 +4258,12 @@ "node": ">= 4.4.x" } }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -3985,33 +4312,6 @@ "node": ">= 10.13.0" } }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -4066,15 +4366,16 @@ } }, "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, + "license": "MIT", "dependencies": { - "mimic-fn": "^4.0.0" + "mimic-function": "^5.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4259,10 +4560,11 @@ "dev": true }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -4270,18 +4572,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -4955,6 +5245,12 @@ "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", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/promise.series": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/promise.series/-/promise.series-0.2.0.tgz", @@ -5169,6 +5465,7 @@ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, + "license": "MIT", "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" @@ -5180,21 +5477,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -5208,7 +5490,8 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/rollup": { "version": "4.44.0", @@ -5488,6 +5771,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -5496,16 +5780,17 @@ } }, "node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" }, "engines": { - "node": ">=12" + "node": ">=20" }, "funding": { "url": "https://github.com/chalk/slice-ansi?sponsor=1" @@ -5515,7 +5800,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -5529,6 +5813,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "license": "MIT", + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, "node_modules/sparkles": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-2.1.0.tgz", @@ -5639,12 +5934,13 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -5653,16 +5949,13 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, "node_modules/style-inject": { @@ -5800,6 +6093,29 @@ "readable-stream": "3" } }, + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/tinyexec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5836,6 +6152,12 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6082,10 +6404,11 @@ } }, "node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -6099,16 +6422,18 @@ } }, "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, "node_modules/wrap-ansi/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -6126,6 +6451,15 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -6135,15 +6469,19 @@ } }, "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, + "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yargs": { diff --git a/package.json b/package.json index 28c83549..73a7fe99 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "autocompleter": "^9.3.2", "gulp": "^5.0.0", "gulp-less": "^5.0.0", + "gulp-sourcemaps": "^3.0.0", "rollup": "^4.40.0" }, "scripts": { @@ -19,7 +20,8 @@ "createSymlink": "node ./tools/create-symlink.mjs", "setup:dev": "node ./tools/dev-setup.mjs", "lint": "eslint", - "lint:fix": "eslint --fix" + "lint:fix": "eslint --fix", + "prepare": "husky" }, "devDependencies": { "@foundryvtt/foundryvtt-cli": "^1.0.2", @@ -29,13 +31,13 @@ "eslint": "^10.2.1", "eslint-plugin-prettier": "^5.5.5", "globals": "^17.5.0", - "husky": "^9.1.5", - "lint-staged": "^15.2.10", + "husky": "^9.1.7", + "lint-staged": "^16.4.0", "postcss": "^8.4.32", "prettier": "^3.5.3", "rollup-plugin-postcss": "^4.0.2" }, "lint-staged": { - "**/*": "prettier --write --ignore-unknown" + "**/*": "eslint --fix" } } diff --git a/src/packs/adversaries/adversary_Dire_Bat_tBWHW00epmMnkawe.json b/src/packs/adversaries/adversary_Dire_Bat_tBWHW00epmMnkawe.json index c2064395..a1107f7c 100644 --- a/src/packs/adversaries/adversary_Dire_Bat_tBWHW00epmMnkawe.json +++ b/src/packs/adversaries/adversary_Dire_Bat_tBWHW00epmMnkawe.json @@ -40,7 +40,8 @@ "experiences": { "ti3Z1mq2M92KK4GJ": { "name": "Bloodthirsty", - "description": "" + "description": "", + "value": 3 } }, "bonuses": { @@ -242,27 +243,24 @@ "type": "withinRange", "target": "hostile", "range": "melee" - } + }, + "changes": [ + { + "key": "system.difficulty", + "value": 3, + "priority": null, + "type": "add" + } + ] }, "_id": "qZfNiqw1iAIxeuYg", "img": "icons/commodities/biological/wing-lizard-brown.webp", - "changes": [ - { - "key": "system.difficulty", - "mode": 2, - "value": "3", - "priority": null - } - ], "disabled": false, "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null + "value": null, + "units": "seconds", + "expiry": null, + "expired": false }, "description": "

While flying, the Bat gains a +3 bonus to their Difficulty.

", "origin": null, @@ -274,6 +272,9 @@ "_stats": { "compendiumSource": null }, + "start": null, + "showIcon": 1, + "folder": null, "_key": "!actors.items.effects!tBWHW00epmMnkawe.gx22MpD8fWoi8klZ.qZfNiqw1iAIxeuYg" } ], diff --git a/src/packs/adversaries/adversary_Greater_Earth_Elemental_dsfB3YhoL5SudvS2.json b/src/packs/adversaries/adversary_Greater_Earth_Elemental_dsfB3YhoL5SudvS2.json index e59d2683..72ad8ae2 100644 --- a/src/packs/adversaries/adversary_Greater_Earth_Elemental_dsfB3YhoL5SudvS2.json +++ b/src/packs/adversaries/adversary_Greater_Earth_Elemental_dsfB3YhoL5SudvS2.json @@ -249,7 +249,7 @@ "name": "Crushing Blows", "type": "feature", "system": { - "description": "

When the @Lookup[@name] makes a successful attack, the target must mark an Armor Slot without receiving its benefi ts (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", + "description": "

When the @Lookup[@name] makes a successful attack, the target must mark an Armor Slot without receiving its benefits (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", "resource": null, "actions": { "0sXciTiPc30v8czv": { diff --git a/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json b/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json index 1615dec8..183719f2 100644 --- a/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json +++ b/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json @@ -138,12 +138,9 @@ "src": "systems/daggerheart/assets/icons/documents/actors/dragon-head.svg", "anchorX": 0.5, "anchorY": 0.5, - "offsetX": 0, - "offsetY": 0, "fit": "contain", "scaleX": 1, "scaleY": 1, - "rotation": 0, "tint": "#ffffff", "alphaThreshold": 0.75 }, @@ -194,7 +191,7 @@ "saturation": 0, "contrast": 0 }, - "detectionModes": [], + "detectionModes": {}, "occludable": { "radius": 0 }, @@ -220,7 +217,8 @@ "flags": {}, "randomImg": false, "appendNumber": false, - "prependAdjective": false + "prependAdjective": false, + "depth": 1 }, "items": [ { @@ -257,7 +255,7 @@ "name": "Acidic Form", "type": "feature", "system": { - "description": "

When the @Lookup[@name] makes a successful attack, the target must mark an Armor Slot without receiving its benefi ts (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", + "description": "

When the @Lookup[@name] makes a successful attack, the target must mark an Armor Slot without receiving its benefits (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", "resource": null, "actions": { "gtT2oHSyZg9OHHJD": { diff --git a/src/packs/domains/domainCard_Book_of_Norai_WtwSWXTRZa7QVvmo.json b/src/packs/domains/domainCard_Book_of_Norai_WtwSWXTRZa7QVvmo.json index f32f380a..78028bab 100644 --- a/src/packs/domains/domainCard_Book_of_Norai_WtwSWXTRZa7QVvmo.json +++ b/src/packs/domains/domainCard_Book_of_Norai_WtwSWXTRZa7QVvmo.json @@ -91,7 +91,7 @@ "type": "attack", "_id": "GI2VkIcGDOjFRxpT", "systemPath": "actions", - "description": "

Make a Spellcast Roll against a target within Very Far range. On a success, hurl a sphere of fire toward them that explodes on impact. The target and all creatures within Very Close range of them must make a Reaction Roll (13). Targets who fail take d20+5 magic damage using your Proficiency. Targets who succeed take half damage.

@Template[type:emanation|range:vc]

", + "description": "

Make a Spellcast Roll against a target within Very Far range. On a success, hurl a sphere of fire toward them that explodes on impact.

", "chatDisplay": true, "actionType": "action", "cost": [], @@ -101,34 +101,7 @@ "recovery": null }, "damage": { - "parts": { - "hitPoints": { - "resultBased": false, - "value": { - "custom": { - "enabled": false - }, - "multiplier": "prof", - "dice": "d20", - "bonus": 5, - "flatMultiplier": 1 - }, - "applyTo": "hitPoints", - "type": [ - "magical" - ], - "base": false, - "valueAlt": { - "multiplier": "prof", - "flatMultiplier": 1, - "dice": "d6", - "bonus": null, - "custom": { - "enabled": false - } - } - } - }, + "parts": {}, "includeBase": false }, "target": { @@ -151,14 +124,105 @@ }, "useDefault": false }, + "save": { + "trait": null, + "difficulty": 13, + "damageMod": "half" + }, + "name": "Fireball - Cast", + "img": "icons/magic/fire/explosion-fireball-large-red-orange.webp", + "range": "veryFar" + }, + "HJ749c2a8WTjkSHY": { + "type": "attack", + "_id": "HJ749c2a8WTjkSHY", + "systemPath": "actions", + "baseAction": false, + "description": "

The target and all creatures within Very Close range of them must make a Reaction Roll (13). Targets who fail take d20+5 magic damage using your Proficiency. Targets who succeed take half damage.

", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "triggers": [], + "areas": [ + { + "name": "Fireball", + "type": "placed", + "shape": "emanation", + "size": "veryClose", + "effects": [] + } + ], + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "damage": { + "parts": { + "hitPoints": { + "applyTo": "hitPoints", + "resultBased": false, + "value": { + "multiplier": "prof", + "flatMultiplier": 1, + "dice": "d20", + "bonus": 5, + "custom": { + "enabled": false, + "formula": "" + } + }, + "valueAlt": { + "multiplier": "flat", + "flatMultiplier": 1, + "dice": "d6", + "bonus": null, + "custom": { + "enabled": false, + "formula": "" + } + }, + "base": false, + "type": [ + "magical" + ] + } + }, + "includeBase": false, + "direct": false + }, + "target": { + "type": "any", + "amount": null + }, + "effects": [], + "roll": { + "type": null, + "trait": null, + "difficulty": null, + "bonus": null, + "advState": "neutral", + "diceRolling": { + "multiplier": "prof", + "flatMultiplier": 1, + "dice": "d6", + "compare": null, + "treshold": null + }, + "useDefault": false + }, "save": { "trait": "agility", "difficulty": 13, "damageMod": "half" }, - "name": "Fireball", - "img": "icons/magic/fire/explosion-fireball-large-red-orange.webp", - "range": "veryFar" + "name": "Fireball - Explosion", + "range": "", + "img": "icons/magic/fire/explosion-fireball-large-red-orange.webp" } }, "attribution": { diff --git a/src/packs/items/weapons/weapon_Glowing_Rings_wG9f5NpCwSbaLy8t.json b/src/packs/items/weapons/weapon_Glowing_Rings_wG9f5NpCwSbaLy8t.json index 8996dbc8..3879c599 100644 --- a/src/packs/items/weapons/weapon_Glowing_Rings_wG9f5NpCwSbaLy8t.json +++ b/src/packs/items/weapons/weapon_Glowing_Rings_wG9f5NpCwSbaLy8t.json @@ -45,7 +45,7 @@ "hitPoints": { "value": { "dice": "d10", - "bonus": 1, + "bonus": 2, "multiplier": "prof", "flatMultiplier": 1, "custom": { diff --git a/styles/less/dialog/beastform/sheet.less b/styles/less/dialog/beastform/sheet.less index 0e1fe746..6d1a8a2a 100644 --- a/styles/less/dialog/beastform/sheet.less +++ b/styles/less/dialog/beastform/sheet.less @@ -43,7 +43,7 @@ text-align: center; font-size: var(--font-size-16); margin: 0 4px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; color: light-dark(@dark, @beige); background-image: url('../assets/parchments/dh-parchment-dark.png'); @@ -58,7 +58,7 @@ position: relative; display: flex; justify-content: center; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; cursor: pointer; width: 120px; @@ -164,7 +164,7 @@ .hybrid-data { padding: 0 2px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; color: light-dark(@dark, @beige); background-image: url('../assets/parchments/dh-parchment-dark.png'); @@ -191,7 +191,7 @@ flex-direction: column; gap: 4px; padding: 0 4px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; color: light-dark(@dark, @beige); background-image: url('../assets/parchments/dh-parchment-dark.png'); @@ -226,7 +226,7 @@ gap: 4px; .trait-card { - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; padding: 2px; opacity: 0.4; diff --git a/styles/less/dialog/character-creation/selections-container.less b/styles/less/dialog/character-creation/selections-container.less index 2bbac484..ebf12eda 100644 --- a/styles/less/dialog/character-creation/selections-container.less +++ b/styles/less/dialog/character-creation/selections-container.less @@ -79,7 +79,7 @@ font-weight: bold; padding: 0 2px; background-image: url(../assets/parchments/dh-parchment-light.png); - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; color: light-dark(@beige, @dark); opacity: 0.4; @@ -114,9 +114,6 @@ .card-preview-container { flex: 1; - } - - .card-preview-container { border-color: light-dark(@dark-blue, @golden); } @@ -206,7 +203,7 @@ height: 16px; width: 110px; min-height: unset; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; color: light-dark(@beige, @beige); background-color: light-dark(var(--color-warm-3), var(--color-warm-3)); @@ -233,7 +230,7 @@ .suggested-trait-container { width: 56px; white-space: nowrap; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; color: light-dark(@beige, @dark); background-image: url('../assets/parchments/dh-parchment-light.png'); @@ -348,7 +345,7 @@ display: flex; justify-content: center; position: relative; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; .nav-section-text { @@ -386,7 +383,7 @@ width: 56px; text-align: center; line-height: 1; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; color: light-dark(@beige, @dark); background-image: url(../assets/parchments/dh-parchment-light.png); @@ -450,7 +447,7 @@ height: 100%; .simple-equipment { - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 8px; position: relative; display: flex; @@ -469,7 +466,7 @@ top: -8px; font-size: var(--font-size-12); white-space: nowrap; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; color: @dark; background-image: url('../assets/parchments/dh-parchment-light.png'); diff --git a/styles/less/dialog/character-creation/tab-navigation.less b/styles/less/dialog/character-creation/tab-navigation.less index 36b89d5a..85541db7 100644 --- a/styles/less/dialog/character-creation/tab-navigation.less +++ b/styles/less/dialog/character-creation/tab-navigation.less @@ -7,7 +7,7 @@ border-top: 0; a { - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; &[disabled] { opacity: 0.4; diff --git a/styles/less/dialog/compendiumBrowserPackDialog/sheet.less b/styles/less/dialog/compendiumBrowserPackDialog/sheet.less index dfe375b5..b16f1086 100644 --- a/styles/less/dialog/compendiumBrowserPackDialog/sheet.less +++ b/styles/less/dialog/compendiumBrowserPackDialog/sheet.less @@ -67,7 +67,6 @@ i { font-size: 18px; - // color: light-dark(@dark-blue, @golden); } } } diff --git a/styles/less/dialog/damage-reduction/damage-reduction-container.less b/styles/less/dialog/damage-reduction/damage-reduction-container.less index e8242bdd..6f7ffb51 100644 --- a/styles/less/dialog/damage-reduction/damage-reduction-container.less +++ b/styles/less/dialog/damage-reduction/damage-reduction-container.less @@ -81,7 +81,7 @@ .mark-container { cursor: pointer; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; height: 26px; padding: 0 1px; @@ -126,7 +126,7 @@ width: 100%; .chip-inner-container { - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; height: 26px; padding: 0 4px; diff --git a/styles/less/dialog/dice-roll/roll-selection.less b/styles/less/dialog/dice-roll/roll-selection.less index a1a01e6b..e3551902 100644 --- a/styles/less/dialog/dice-roll/roll-selection.less +++ b/styles/less/dialog/dice-roll/roll-selection.less @@ -56,7 +56,7 @@ cursor: pointer; padding: 5px; background: light-dark(@dark-blue-10, @golden-10); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; .label { font-style: normal; @@ -129,7 +129,7 @@ cursor: pointer; padding: 5px; background: light-dark(@dark-blue-10, @golden-10); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; .label { font-style: normal; diff --git a/styles/less/dialog/downtime/downtime-container.less b/styles/less/dialog/downtime/downtime-container.less index eb615ef0..33d153fd 100644 --- a/styles/less/dialog/downtime/downtime-container.less +++ b/styles/less/dialog/downtime/downtime-container.less @@ -37,7 +37,7 @@ .activity-marker { font-size: 0.5rem; flex: none; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; margin-right: 4px; } @@ -55,7 +55,7 @@ .activity-selected-marker { font-size: var(--font-size-14); - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; color: light-dark(@dark, @beige); background-image: url(../assets/parchments/dh-parchment-dark.png); @@ -78,7 +78,7 @@ } .refreshable-container { - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; color: light-dark(@dark, @beige); background-image: url('../assets/parchments/dh-parchment-dark.png'); diff --git a/styles/less/dialog/group-roll-dialog/_common.less b/styles/less/dialog/group-roll-dialog/_common.less new file mode 100644 index 00000000..f74ab8a0 --- /dev/null +++ b/styles/less/dialog/group-roll-dialog/_common.less @@ -0,0 +1,44 @@ +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 new file mode 100644 index 00000000..27925fa2 --- /dev/null +++ b/styles/less/dialog/group-roll-dialog/index.less @@ -0,0 +1,8 @@ +.daggerheart.dialog.dh-style.views.group-roll-dialog { + .window-content { + @import "./_common.less"; + } +} + +@import "./initialization.less"; +@import "./main.less"; diff --git a/styles/less/dialog/group-roll-dialog/initialization.less b/styles/less/dialog/group-roll-dialog/initialization.less index b2e7e021..b32f4756 100644 --- a/styles/less/dialog/group-roll-dialog/initialization.less +++ b/styles/less/dialog/group-roll-dialog/initialization.less @@ -1,62 +1,59 @@ -.theme-light .daggerheart.dialog.dh-style.views.group-roll-dialog { - .initialization-container .members-container .member-container { - .member-name { - background-image: url('../assets/parchments/dh-parchment-light.png'); - } - } -} - .daggerheart.dialog.dh-style.views.group-roll-dialog { - .initialization-container { - h2 { - text-align: center; - } - - .members-container { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr; - gap: 8px; - - .member-container { - position: relative; - display: flex; - justify-content: center; - - &.inactive { - opacity: 0.4; - } - - .member-name { - position: absolute; - padding: 0 2px; - border: 1px solid; - border-radius: 6px; - margin-top: 4px; - color: light-dark(@dark, @beige); - background-image: url('../assets/parchments/dh-parchment-dark.png'); - text-align: center; - } - - img { - border-radius: 6px; - border: 1px solid light-dark(@dark-blue, @golden); - } - } - } + .initialization-container.active { + display: flex; + flex-direction: column; + overflow: hidden; .main-roll { - margin-top: 8px; - display: grid; - grid-template-columns: 1fr 1fr; - gap: 8px; + display: flex; + flex-direction: column; + text-align: center; + padding-bottom: 4px; &.inactive { opacity: 0.4; } } + .hint { + color: var(--color-form-hint); + line-height: 1; + padding: var(--spacer-8) 0; + font-family: var(--dh-font-body); + font-size: var(--font-size-12); + font-weight: 300; + } + + .members-container { + display: flex; + flex-direction: column; + gap: 8px; + overflow-y: auto; + + .member-container { + display: flex; + position: relative; + justify-content: center; + height: unset; + padding: 4px 8px; + gap: var(--spacer-8); + flex: 0 0 auto; + + &:not(.inactive) { + background: @golden-bg; + } + + .name { + flex: 1; + color: light-dark(@dark, @beige); + font-weight: 500; + text-align: left; + } + } + } + footer { - margin-top: 8px; + margin-top: 12px; display: flex; gap: 8px; diff --git a/styles/less/dialog/group-roll-dialog/leader.less b/styles/less/dialog/group-roll-dialog/leader.less deleted file mode 100644 index b3fa3a3b..00000000 --- a/styles/less/dialog/group-roll-dialog/leader.less +++ /dev/null @@ -1,35 +0,0 @@ -.daggerheart.dialog.dh-style.views.group-roll-dialog { - .main-character-outer-container { - &.inactive { - opacity: 0.3; - pointer-events: none; - } - - .main-character-container { - .character-info { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - height: 64px; - - img { - height: 64px; - border-radius: 6px; - border: 1px solid light-dark(@dark-blue, @golden); - } - - .character-data { - padding-left: 0.75rem; - flex: 1; - height: 100%; - display: flex; - flex-direction: column; - justify-content: space-between; - text-align: left; - font-size: var(--font-size-18); - } - } - } - } -} diff --git a/styles/less/dialog/group-roll-dialog/main.less b/styles/less/dialog/group-roll-dialog/main.less new file mode 100644 index 00000000..e30f4e29 --- /dev/null +++ b/styles/less/dialog/group-roll-dialog/main.less @@ -0,0 +1,273 @@ +.daggerheart.dialog.dh-style.views.group-roll-dialog { + .group-roll.active { + display: flex; + flex-direction: column; + overflow: hidden; + + a.roll-button { + &:hover { + text-shadow: none; + filter: drop-shadow(0 0 3px @golden-90); + } + } + + .aiding-members { + display: flex; + flex-direction: column; + gap: 6px; + overflow-y: auto; + } + + .item-tags { + gap: 6px; + .tag.failure, + .tag.success { + font-weight: 600; + justify-content: center; + min-width: 3ch; + } + + .tag.success { + border-color: @green; + background: @green-10; + color: @green; + } + + .tag.failure { + border-color: @red; + background: @red-10; + color: @red; + } + + .tag { + padding-top: 0; + padding-bottom: 0; + line-height: 1.1875rem; + } + } + + .with-result { + border-radius: 5px; + background: var(--duality-bg); + color: var(--color-light-2); + + &.hope { + --duality-text-color: @golden; + --duality-bg: url(../assets/parchments/dh-parchment-hope.png); + } + + &.fear { + --duality-text-color: @chat-blue; + --duality-bg: url(../assets/parchments/dh-parchment-fear.png); + } + + &.critical { + --duality-text-color: @chat-purple; + --duality-bg: url(../assets/parchments/dh-parchment-critical.png); + } + + .duality-label { + font-family: var(--dh-font-subtitle); + color: var(--duality-text-color); + font-weight: 700; + } + } + + .member-roll-container { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + min-height: 3.375rem; + + &.inactive { + pointer-events: none; + } + + .name-area { + display: flex; + flex-direction: column; + flex: 1; + justify-content: center; + .name { + font-weight: 500; + } + .trait { + display: flex; + align-items: center; + gap: 6px; + select { + --input-height: 2em; + width: auto; + } + } + .item-tags { + align-items: stretch; + } + } + + .buttons { + display: flex; + flex-direction: row; + button { + --button-text-color: @color-text-primary; + --button-size: 1.5em; + padding: 0 var(--spacer-4); + img { + width: 100%; + height: 100%; + object-fit: contain; + } + } + } + + a.roll-button.initial-roll { + width: 1.875rem; + height: 1.875rem; + margin-right: 2px; /** makes hover look a bit nicer */ + } + + .with-result { + display: flex; + justify-content: end; + align-items: center; + gap: 6px; + + .roll-data { + display: flex; + flex-direction: column; + align-items: end; + justify-content: center; + padding: 0 4px; + min-height: 3rem; + + .duality-label { + font-size: var(--font-size-15); + + .unused-damage { + text-decoration: line-through; + } + + .with { + font-size: var(--font-size-10); + } + } + + .roll-dice-container { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 2px; + + .roll-dice { + position: relative; + display: flex; + align-items: center; + justify-content: center; + + .dice-label { + position: absolute; + color: white; + font-size: 1rem; + paint-order: stroke fill; + -webkit-text-stroke: 2px black; + } + + img { + height: 1.3125rem; + } + } + + .roll-operator { + font-size: var(--font-size-18); + padding: 0 1px; + } + + .roll-value { + font-size: var(--font-size-16); + padding: 0 1px; + } + } + } + + .buttons { + flex-direction: column; + button { + color: var(--medium-red); + &[data-success=true] { + color: var(--green); + } + &.active { + text-shadow: 0 0 1px light-dark(@dark-80, @beige-80); + } + &.inactive { + opacity: 0.35; + } + } + } + } + } + } + + .group-roll-results { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 4px; + font-size: var(--font-size-12); + padding: 6px 12px; + margin-top: 8px; + + &.empty { + color: light-dark(@dark-blue-90, @beige-80); + border-radius: 3px; + justify-content: center; + border: 1px dashed light-dark(@dark-blue-90, @beige-80); + text-align: center; + height: 3.25rem; + font-family: @font-body; + } + + .row { + display: flex; + align-items: center; + justify-content: space-between; + } + + .divider { + height: 1px; + background-image: linear-gradient(90deg, transparent 0%, var(--duality-text-color) 50%, transparent 100%); + margin-block: var(--spacer-4); + } + + .modifiers .item-tags { + min-height: calc(2px + 1.1875rem); + } + + .total { + .label { + font-size: var(--font-size-14); + } + .duality-label { + display: flex; + align-items: center; + gap: var(--spacer-4); + .value { + font-size: 20px; + } + } + } + } + + .finish-container { + margin-top: 16px; + gap: 16px; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + + .finish-button { + grid-column: span 2; + } + } +} diff --git a/styles/less/dialog/group-roll-dialog/sheet.less b/styles/less/dialog/group-roll-dialog/sheet.less deleted file mode 100644 index 70afc1fe..00000000 --- a/styles/less/dialog/group-roll-dialog/sheet.less +++ /dev/null @@ -1,265 +0,0 @@ -.daggerheart.dialog.dh-style.views.group-roll-dialog { - .team-container { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 16px; - margin-bottom: 16px; - - .team-member-container { - display: flex; - flex-direction: column; - justify-content: space-between; - gap: 8px; - flex: 1; - - &.inactive { - opacity: 0.3; - pointer-events: none; - } - - .data-container { - display: flex; - flex-direction: column; - gap: 8px; - width: 100%; - } - - .member-info { - display: flex; - align-items: start; - width: 100%; - - img { - height: 64px; - border-radius: 6px; - border: 1px solid light-dark(@dark-blue, @golden); - } - - .member-data { - padding-left: 0.75rem; - flex: 1; - height: 100%; - display: flex; - flex-direction: column; - justify-content: space-between; - text-align: left; - font-size: var(--font-size-18); - } - } - } - } - - .roll-container { - display: flex; - flex-direction: column; - } - - .roll-title { - font-size: var(--font-size-20); - font-weight: bold; - color: light-dark(@dark-blue, @golden); - text-align: center; - display: flex; - align-items: center; - gap: 8px; - - &.hope, - &.fear, - &.critical { - color: var(--text-color); - } - - &.hope { - --text-color: @golden; - } - - &.fear { - --text-color: @chat-blue; - } - - &.critical { - --text-color: @chat-purple; - } - - &::before, - &::after { - color: var(--text-color); - content: ''; - flex: 1; - height: 2px; - } - - &::before { - background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, light-dark(@dark-blue, @golden) 100%); - } - - &::after { - background: linear-gradient(90deg, light-dark(@dark-blue, @golden) 0%, rgba(0, 0, 0, 0) 100%); - } - } - - .roll-tools { - display: flex; - gap: 4px; - align-items: center; - - img { - height: 16px; - } - - a { - display: flex; - font-size: 16px; - - &:hover { - text-shadow: none; - filter: drop-shadow(0 0 8px var(--golden)); - } - } - } - - .roll-data { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; - - &.hope { - --text-color: @golden; - --bg-color: @golden-40; - } - - &.fear { - --text-color: @chat-blue; - --bg-color: @chat-blue-40; - } - - &.critical { - --text-color: @chat-purple; - --bg-color: @chat-purple-40; - } - - .duality-label { - color: var(--text-color); - font-size: var(--font-size-20); - font-weight: bold; - text-align: center; - - .unused-damage { - text-decoration: line-through; - } - } - - .roll-dice-container { - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - gap: 8px; - - .roll-dice { - position: relative; - display: flex; - align-items: center; - justify-content: center; - - .dice-label { - position: absolute; - color: white; - font-size: 1rem; - paint-order: stroke fill; - -webkit-text-stroke: 2px black; - } - - img { - height: 32px; - } - } - - .roll-operator { - font-size: var(--font-size-24); - } - - .roll-value { - font-size: 18px; - } - } - - .roll-total { - background: var(--bg-color); - color: var(--text-color); - border-radius: 4px; - padding: 3px; - } - } - - .roll-success-container { - display: flex; - align-items: center; - justify-content: space-around; - - .roll-success-tools { - display: flex; - align-items: center; - gap: 4px; - color: light-dark(@dark-blue, @golden); - - i { - font-size: 24px; - } - } - - .roll-success-modifier { - display: flex; - align-items: center; - justify-content: right; - gap: 2px; - font-size: var(--font-size-20); - padding: 0px 4px; - - &.success { - background: @green-10; - color: @green; - } - - &.failure { - background: @red-10; - color: @red; - } - } - } - - .section-title { - font-size: var(--font-size-18); - font-weight: bold; - } - - .group-roll-results { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; - font-size: var(--font-size-20); - - .group-roll-container { - display: flex; - align-items: center; - gap: 2px; - } - } - - .finish-container { - margin-top: 16px; - gap: 16px; - display: grid; - grid-template-columns: 1fr 1fr 1fr; - - .finish-button { - grid-column: span 2; - } - } - - .hint { - text-align: center; - } -} diff --git a/styles/less/dialog/group-roll/group-roll.less b/styles/less/dialog/group-roll/group-roll.less deleted file mode 100644 index f2895d31..00000000 --- a/styles/less/dialog/group-roll/group-roll.less +++ /dev/null @@ -1,50 +0,0 @@ -@import '../../utils/colors.less'; - -.application.daggerheart.group-roll { - fieldset.one-column { - min-width: 500px; - margin-bottom: 10px; - } - .actor-item { - display: flex; - align-items: center; - gap: 15px; - width: 100%; - - img { - height: 40px; - width: 40px; - border-radius: 50%; - object-fit: cover; - } - - .actor-info { - display: flex; - flex-direction: column; - gap: 10px; - - .actor-check-info { - display: flex; - gap: 10px; - - .form-fields { - display: flex; - gap: 5px; - align-items: center; - - input { - max-width: 40px; - text-align: center; - } - } - } - } - - .controls { - margin-left: auto; - } - } - .tooltip-container { - width: 100%; - } -} diff --git a/styles/less/dialog/image-select/sheet.less b/styles/less/dialog/image-select/sheet.less index 3ed4f583..7a3a8468 100644 --- a/styles/less/dialog/image-select/sheet.less +++ b/styles/less/dialog/image-select/sheet.less @@ -12,7 +12,7 @@ img { width: 136px; height: 136px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; opacity: 0.4; diff --git a/styles/less/dialog/index.less b/styles/less/dialog/index.less index 80e358af..5f827ec8 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -1,10 +1,5 @@ @import './attribution/sheet.less'; -@import './level-up/navigation-container.less'; -@import './level-up/selections-container.less'; -@import './level-up/sheet.less'; -@import './level-up/summary-container.less'; -@import './level-up/tiers-container.less'; -@import './level-up/footer.less'; +@import './level-up/index.less'; @import './resource-dice/sheet.less'; @@ -29,16 +24,10 @@ @import './multiclass-choice/sheet.less'; -@import './reroll-dialog/sheet.less'; - -@import './group-roll/group-roll.less'; - @import './tag-team-dialog/initialization.less'; @import './tag-team-dialog/sheet.less'; -@import './group-roll-dialog/initialization.less'; -@import './group-roll-dialog/leader.less'; -@import './group-roll-dialog/sheet.less'; +@import './group-roll-dialog/index.less'; @import './image-select/sheet.less'; diff --git a/styles/less/dialog/level-up/index.less b/styles/less/dialog/level-up/index.less new file mode 100644 index 00000000..849a4d36 --- /dev/null +++ b/styles/less/dialog/level-up/index.less @@ -0,0 +1,6 @@ +@import './navigation-container.less'; +@import './selections-container.less'; +@import './summary-container.less'; +@import './tiers-container.less'; +@import './footer.less'; +@import './sheet.less'; diff --git a/styles/less/dialog/level-up/navigation-container.less b/styles/less/dialog/level-up/navigation-container.less index 282d683f..6bf80d7c 100644 --- a/styles/less/dialog/level-up/navigation-container.less +++ b/styles/less/dialog/level-up/navigation-container.less @@ -19,7 +19,7 @@ a, span { - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; } } } diff --git a/styles/less/dialog/level-up/selections-container.less b/styles/less/dialog/level-up/selections-container.less index 6a551865..8c0dbaec 100644 --- a/styles/less/dialog/level-up/selections-container.less +++ b/styles/less/dialog/level-up/selections-container.less @@ -3,12 +3,7 @@ .daggerheart.levelup { .levelup-selections-container { - overflow: auto; padding: 10px 0; - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; - max-height: 500px; - mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); .achievement-experience-cards { display: flex; @@ -45,20 +40,22 @@ .levelup-card-selection { display: flex; - flex-wrap: wrap; justify-content: center; gap: 40px; height: 190px; + align-items: stretch; .card-preview-container { - height: 100%; + height: 190px; max-width: 200px; } .levelup-domains-selection-container { - display: flex; - flex-direction: column; - gap: 8px; + display: grid; + grid-auto-flow: column; + grid-template-rows: repeat(2, minmax(0, 1fr)); + height: 100%; + gap: 4px; .levelup-domain-selection-container { display: flex; @@ -66,6 +63,8 @@ align-items: center; position: relative; cursor: pointer; + overflow: hidden; + width: 93px; &.disabled { pointer-events: none; @@ -74,16 +73,20 @@ .levelup-domain-label { position: absolute; + left: 0; + right: 0; + bottom: 0; text-align: center; - top: 4px; background: grey; - padding: 0 12px; - border-radius: 6px; + padding: 2px 12px; z-index: 2; + line-height: 1; } img { - height: 124px; + object-fit: cover; + width: auto; + height: auto; &.svg { filter: @beige-filter; @@ -92,17 +95,18 @@ .levelup-domain-selected { position: absolute; - height: 54px; - width: 54px; + height: 40px; + width: 40px; border-radius: 50%; - border: 2px solid; - font-size: var(--font-size-48); + border: 2px solid @golden; + font-size: var(--font-size-24); display: flex; align-items: center; justify-content: center; - background-image: url(../assets/parchments/dh-parchment-light.png); - color: var(--color-dark-5); - top: calc(50% - 29px); + background: @dark-golden; + color: @golden; + top: 10px; + z-index: 2; i { position: relative; diff --git a/styles/less/dialog/level-up/sheet.less b/styles/less/dialog/level-up/sheet.less index ade7c8a9..9ebd9331 100644 --- a/styles/less/dialog/level-up/sheet.less +++ b/styles/less/dialog/level-up/sheet.less @@ -11,9 +11,10 @@ }); .daggerheart.levelup { - .window-content { - max-height: 960px; + .tab.active { + flex: 1; overflow: auto; + .with-scroll-shadows(); } div[data-application-part='form'] { @@ -22,15 +23,13 @@ gap: 8px; } - section { - .section-container { - display: flex; - flex-direction: row; - justify-content: center; - gap: 20px 8px; - margin-top: 8px; - flex-wrap: wrap; - } + .section-container { + display: flex; + flex-direction: row; + justify-content: center; + gap: 20px 8px; + margin-top: 8px; + flex-wrap: wrap; } .levelup-footer { diff --git a/styles/less/dialog/level-up/summary-container.less b/styles/less/dialog/level-up/summary-container.less index d67abff6..de7c9f4a 100644 --- a/styles/less/dialog/level-up/summary-container.less +++ b/styles/less/dialog/level-up/summary-container.less @@ -17,10 +17,8 @@ .levelup-summary-container { overflow: auto; padding: 10px 0; - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; max-height: 700px; - mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); + .with-scroll-shadows(); .level-achievements-container, .level-advancements-container { diff --git a/styles/less/dialog/multiclass-choice/sheet.less b/styles/less/dialog/multiclass-choice/sheet.less index 1f38449a..0c487cbc 100644 --- a/styles/less/dialog/multiclass-choice/sheet.less +++ b/styles/less/dialog/multiclass-choice/sheet.less @@ -35,7 +35,7 @@ width: 120px; height: 120px; background: light-dark(@dark-blue-10, @golden-10); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; &.selected { background: light-dark(@dark-blue-40, @golden-40); @@ -57,7 +57,7 @@ display: flex; flex-wrap: wrap; font-style: italic; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; padding: 4px 4px; } diff --git a/styles/less/dialog/reroll-dialog/sheet.less b/styles/less/dialog/reroll-dialog/sheet.less deleted file mode 100644 index 71c94d80..00000000 --- a/styles/less/dialog/reroll-dialog/sheet.less +++ /dev/null @@ -1,125 +0,0 @@ -.daggerheart.dialog.dh-style.views.reroll-dialog { - .window-content { - max-width: 648px; - } - - .reroll-outer-container { - h2 { - margin: 0; - } - - .dices-container { - display: flex; - flex-wrap: wrap; - gap: 8px; - } - - .dice-outer-container { - width: 300px; - - legend { - display: flex; - align-items: center; - gap: 4px; - - i { - margin-right: 4px; - } - } - - .dice-container { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr; - - .result-container { - position: relative; - aspect-ratio: 1; - display: flex; - align-items: center; - justify-content: center; - font-size: 1.375rem; - opacity: 0.8; - - &.selected { - opacity: 1; - border: 1px solid; - border-radius: 6px; - border-color: light-dark(@dark-blue, @golden); - filter: drop-shadow(0 0 3px @golden); - } - - &:before { - content: ' '; - position: absolute; - width: 100%; - height: 100%; - z-index: -1; - mask: var(--svg-die) no-repeat center; - mask-size: contain; - background: linear-gradient(139.01deg, #efe6d8 3.51%, #372e1f 96.49%); - } - - &.d4:before { - --svg-die: url(../assets/icons/dice/default/d4.svg); - } - &.d6:before { - --svg-die: url(../assets/icons/dice/default/d6.svg); - } - &.d8:before { - --svg-die: url(../assets/icons/dice/default/d8.svg); - } - &.d10:before { - --svg-die: url(../assets/icons/dice/default/d10.svg); - } - &.d12:before { - --svg-die: url('../assets/icons/dice/default/d12.svg'); - } - &.d20:before { - --svg-die: url(../assets/icons/dice/default/d20.svg); - } - - .to-reroll-result { - position: absolute; - bottom: -7px; - gap: 2px; - border: 1px solid; - border-radius: 6px; - background-image: url(../assets/parchments/dh-parchment-dark.png); - display: flex; - align-items: center; - padding: 2px 6px; - - input { - margin: 0; - height: 12px; - line-height: 0px; - position: relative; - top: 1px; - - &:before, - &:after { - line-height: 12px; - font-size: var(--font-size-12); - } - } - - i { - font-size: var(--font-size-10); - } - } - } - } - } - } - - footer { - margin-top: 8px; - display: flex; - justify-content: space-between; - - .controls { - display: flex; - gap: 8px; - } - } -} diff --git a/styles/less/dialog/tag-team-dialog/initialization.less b/styles/less/dialog/tag-team-dialog/initialization.less index 8557d231..d6f7ad29 100644 --- a/styles/less/dialog/tag-team-dialog/initialization.less +++ b/styles/less/dialog/tag-team-dialog/initialization.less @@ -1,3 +1,5 @@ +@import '../../utils/mixin.less'; + .theme-light .daggerheart.dialog.dh-style.views.tag-team-dialog { .initialization-container .members-container .member-container { .member-name { @@ -7,39 +9,75 @@ } .daggerheart.dialog.dh-style.views.tag-team-dialog { - .initialization-container { + .initialization-container.active { + display: flex; + flex-direction: column; + gap: var(--spacer-4); + h2 { text-align: center; } .members-container { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr; + display: flex; + flex-wrap: wrap; + justify-content: center; gap: 8px; + // Force 3 columns for 5 -> 6 players + &:has(> :nth-child(5)):not(:has(> :nth-child(7))) { + padding-left: 10%; + padding-right: 10%; + } + .member-container { position: relative; display: flex; + align-items: stretch; justify-content: center; + border-radius: 6px; + border: 1px solid @color-border; + overflow: hidden; + height: 11.5rem; + width: 122px; &.inactive { - opacity: 0.4; + border-color: light-dark(@dark-blue-40, @golden-40); + img { + opacity: 0.4; + } } .member-name { + --shadow-color: light-dark(white, black); position: absolute; - padding: 0 2px; - border: 1px solid; - border-radius: 6px; - margin-top: 4px; - color: light-dark(@dark, @beige); - background-image: url('../assets/parchments/dh-parchment-dark.png'); + bottom: 0; + left: 0; + right: 0; + + display: flex; + flex-direction: column; + justify-content: flex-end; + min-height: 4rem; + padding: 5rem 4px var(--spacer-8) 4px; text-align: center; + + color: @color-text-primary; + text-shadow: 1px 1px 2px var(--shadow-color), 0 0 10px var(--shadow-color); + .smooth-gradient-ease-in-out(background-image, to bottom, var(--shadow-color), 100%); } img { - border-radius: 6px; - border: 1px solid light-dark(@dark-blue, @golden); + object-fit: cover; + object-position: top center; + flex: 1; + } + + .leader-mark { + position: absolute; + top: 4px; + right: 4px; + text-shadow: var(--shadow-text-stroke), 0 0 20px black; } } } diff --git a/styles/less/dialog/tag-team-dialog/sheet.less b/styles/less/dialog/tag-team-dialog/sheet.less index dc8f16dc..82bc0270 100644 --- a/styles/less/dialog/tag-team-dialog/sheet.less +++ b/styles/less/dialog/tag-team-dialog/sheet.less @@ -1,4 +1,10 @@ -.daggerheart.dialog.dh-style.views.tag-team-dialog { +.daggerheart.dialog.dh-style.views.tag-team-dialog .window-content { + h1 { + color: @color-text-emphatic; + font: 700 var(--font-size-24) var(--dh-font-subtitle); + text-align: center; + } + .team-container { display: flex; gap: 16px; @@ -36,7 +42,7 @@ img { height: 64px; border-radius: 6px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; } .member-name { @@ -58,7 +64,7 @@ .roll-title { font-size: var(--font-size-20); font-weight: bold; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; text-align: center; display: flex; align-items: center; @@ -66,7 +72,7 @@ &::before, &::after { - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; content: ''; flex: 1; height: 2px; @@ -188,6 +194,7 @@ .roll-selection-container { display: flex; + gap: 16px; .select-roll-button { margin-top: 8px; @@ -196,7 +203,7 @@ justify-content: center; i { - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; font-size: 48px; &.inactive { diff --git a/styles/less/global/dialog.less b/styles/less/global/dialog.less index a3400700..1313d68b 100644 --- a/styles/less/global/dialog.less +++ b/styles/less/global/dialog.less @@ -36,8 +36,8 @@ padding: 0; &:hover { - border: 1px solid light-dark(@dark-blue, @golden); - color: light-dark(@dark-blue, @golden); + border: 1px solid @color-border; + color: @color-text-emphatic; } } } @@ -81,7 +81,7 @@ cursor: pointer; padding: 5px; background: light-dark(@dark-blue-10, @golden-10); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; .label { font-style: normal; diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index c5bca1da..e57ba50d 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -2,24 +2,22 @@ @import '../utils/fonts.less'; .dh-style { - border: 1px solid light-dark(@dark-blue, @golden); - input[type='text'], input[type='number'], textarea, + file-picker, .input[contenteditable] { background: light-dark(transparent, transparent); border-radius: 6px; box-shadow: 0 4px 30px @soft-shadow; backdrop-filter: blur(9.5px); - -webkit-backdrop-filter: blur(9.5px); outline: 2px solid transparent; - color: light-dark(@dark-blue, @golden); - border: 1px solid light-dark(@dark, @beige); + color: @input-color-text; + border: 1px solid @input-color-border; transition: all 0.3s ease; &::placeholder { - color: light-dark(@dark-40, @beige-50); + color: @color-text-subtle; } &:hover, @@ -30,7 +28,7 @@ &:focus[type='number'] { background: light-dark(@soft-shadow, @semi-transparent-dark-blue); box-shadow: none; - outline: 2px solid light-dark(@dark, @beige); + outline: 2px solid @input-color-border; } &:disabled[type='text'], @@ -47,7 +45,7 @@ .input[contenteditable] { cursor: var(--cursor-text); &:empty:before { - color: light-dark(@dark-40, @beige-50); + color: @color-text-subtle; content: attr(placeholder); } } @@ -96,11 +94,9 @@ textarea { color: light-dark(@dark, @beige); - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; } - button { + button:where(:not(.plain, color-picker *, file-picker *)) { background: light-dark(transparent, @golden); border: 1px solid light-dark(@dark-blue, @dark-blue); color: light-dark(@dark-blue, @dark-blue); @@ -109,7 +105,7 @@ &:hover { background: light-dark(@light-black, @dark-blue); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; } &.glow { @@ -130,7 +126,7 @@ &.reverted { background: light-dark(@dark-blue-10, @golden-10); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; border: 1px solid light-dark(@dark, transparent); &:hover { background: light-dark(transparent, @golden); @@ -177,7 +173,7 @@ height: inherit; .tag { padding: 0.3rem 0.5rem; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; background-color: light-dark(@dark-blue-10, @golden-40); border-radius: 3px; transition: 0.13s ease-out; @@ -251,15 +247,23 @@ a:hover, a.active { - font-weight: bold; - text-shadow: 0 0 8px light-dark(@dark-blue, @golden); + text-shadow: 0 0 1px currentColor, 0 0 1px currentColor, 0 0 8px light-dark(@dark-blue, @golden); + } + + file-picker, color-picker { + > input[type=text] { + background: transparent; + border: none; + outline: none; + backdrop-filter: unset; + } } fieldset { align-items: center; margin-top: 5px; border-radius: 6px; - border-color: light-dark(@dark-blue, @golden); + border-color: @color-fieldset-border; &.glassy { background-color: light-dark(@dark-blue-10, @golden-10); @@ -268,8 +272,9 @@ legend { padding: 2px 12px; border-radius: 3px; - background-color: light-dark(@dark-blue, @golden); + background-color: @color-fieldset-border; color: light-dark(@beige, @dark-blue); + margin-bottom: var(--spacer-4); } } @@ -278,7 +283,7 @@ } &.fit-height { - height: 95%; + flex: 1; } &.flex { @@ -346,7 +351,7 @@ legend { font-weight: bold; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; &.with-icon { display: flex; @@ -358,18 +363,6 @@ } } - input[type='text'], - input[type='number'] { - color: light-dark(@dark, @beige); - transition: all 0.3s ease; - outline: 2px solid transparent; - - &:focus, - &:hover { - outline: 2px solid light-dark(@dark, @beige); - } - } - &[disabled], &.child-disabled .form-group, select[disabled], @@ -507,7 +500,7 @@ display: block; height: 1px; width: 100%; - border-bottom: 1px solid light-dark(@dark-blue, @golden); + border-bottom: 1px solid @color-border; mask-image: linear-gradient(270deg, transparent 0%, black 50%, transparent 100%); } @@ -515,7 +508,7 @@ display: block; height: 1px; width: 100%; - border-bottom: 1px solid light-dark(@dark-blue, @golden); + border-bottom: 1px solid @color-border; mask-image: linear-gradient(270deg, transparent 0%, black 100%); &.invert { @@ -564,7 +557,7 @@ border: 0; &:hover { - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; } &:not(:first-child) { @@ -599,59 +592,6 @@ } } -.application.setting.dh-style { - h2, - h3, - h4 { - margin: 8px 0 4px; - text-align: center; - } - - footer { - margin-top: 8px; - display: flex; - gap: 8px; - - button { - flex: 1; - } - } - - .form-group { - display: flex; - justify-content: space-between; - align-items: center; - gap: 0.25rem 0.5rem; - flex-wrap: wrap; - - label { - font-size: var(--font-size-14); - font-weight: normal; - } - - .form-fields { - display: flex; - gap: 4px; - align-items: center; - } - - &.setting-two-values { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 0.25rem 0.5rem; - - .form-group { - justify-content: end; - flex-wrap: nowrap; - } - - .hint { - grid-column: 1 / -1; - } - } - } -} - .system-daggerheart { .tagify { background: light-dark(transparent, transparent); @@ -804,6 +744,7 @@ .preview-image-container { width: 100%; + min-height: 0; flex-grow: 1; object-fit: cover; border-radius: 4px 4px 0 0; diff --git a/styles/less/global/feature-section.less b/styles/less/global/feature-section.less index 13feb92c..2fd4e20f 100644 --- a/styles/less/global/feature-section.less +++ b/styles/less/global/feature-section.less @@ -5,8 +5,6 @@ .tab.features { padding: 0 10px; overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; .feature-list { display: flex; flex-direction: column; @@ -19,28 +17,36 @@ &:last-child { margin-bottom: 0px; } - .feature-line { - display: grid; + } + .feature-line { + display: grid; + align-items: center; + grid-template-columns: 1fr 4fr 1fr; + h4 { + font-weight: lighter; + color: light-dark(@dark, @beige); + } + .image { + height: 40px; + width: 40px; + object-fit: cover; + border-radius: 6px; + border: none; + } + .image-icon { + font-size: 26px; + width: 40px; + height: 40px; + display: flex; + justify-content: center; align-items: center; - grid-template-columns: 1fr 4fr 1fr; - h4 { - font-weight: lighter; - color: light-dark(@dark, @beige); - } - .image { - height: 40px; - width: 40px; - object-fit: cover; - border-radius: 6px; - border: none; - } - .controls { - display: flex; - justify-content: center; - gap: 10px; - a { - text-shadow: none; - } + } + .controls { + display: flex; + justify-content: center; + gap: 10px; + a { + text-shadow: none; } } } diff --git a/styles/less/global/filter-menu.less b/styles/less/global/filter-menu.less index 65a184f8..a0545950 100644 --- a/styles/less/global/filter-menu.less +++ b/styles/less/global/filter-menu.less @@ -13,7 +13,7 @@ legend { font-weight: bold; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; font-size: var(--font-size-12); } @@ -25,7 +25,7 @@ button { background: light-dark(@light-black, @dark-blue); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; outline: none; box-shadow: none; border: 1px solid light-dark(@dark-blue, @dark-blue); diff --git a/styles/less/global/global.less b/styles/less/global/global.less index b9af67c0..c0e7f3fc 100644 --- a/styles/less/global/global.less +++ b/styles/less/global/global.less @@ -12,6 +12,11 @@ } .daggerheart.dh-style { + * { + scrollbar-width: thin; + scrollbar-color: light-dark(@dark-blue, @golden) transparent; + } + .hint { flex: 0 0 100%; margin: 0; @@ -33,7 +38,7 @@ } &:before { - font-family: 'Font Awesome 6 Pro'; + font-family: var(--font-awesome); content: '\f110'; position: absolute; height: 100%; diff --git a/styles/less/global/inventory-item.less b/styles/less/global/inventory-item.less index a2b9ebd8..3a5a9321 100644 --- a/styles/less/global/inventory-item.less +++ b/styles/less/global/inventory-item.less @@ -287,7 +287,7 @@ position: relative; height: 120px; width: 98px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; cursor: pointer; diff --git a/styles/less/global/prose-mirror.less b/styles/less/global/prose-mirror.less index 8a663e28..e4b1249f 100644 --- a/styles/less/global/prose-mirror.less +++ b/styles/less/global/prose-mirror.less @@ -10,8 +10,6 @@ background-color: transparent; } .editor-content { - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; h1 { font-size: var(--font-size-32); } @@ -42,6 +40,11 @@ ul { list-style: disc; } + } + // Fixes centering and makes it not render over scrollbar + &:hover button.toggle:enabled { + display: flex; + right: 12px; } } } diff --git a/styles/less/global/resource-bar.less b/styles/less/global/resource-bar.less index be9bc68b..d06b43a8 100644 --- a/styles/less/global/resource-bar.less +++ b/styles/less/global/resource-bar.less @@ -50,16 +50,16 @@ flex-wrap: wrap; gap: 5px; padding: 5px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; z-index: 1; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; width: fit-content; .slot { width: 15px; height: 10px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; background: light-dark(@dark-blue-10, @golden-10); border-radius: 3px; transition: all 0.3s ease; @@ -148,7 +148,7 @@ appearance: none; width: 100px; height: 40px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; z-index: 1; background: @dark-blue; diff --git a/styles/less/global/sheet.less b/styles/less/global/sheet.less index 0c400564..e3072da1 100755 --- a/styles/less/global/sheet.less +++ b/styles/less/global/sheet.less @@ -4,8 +4,8 @@ // Theme handling .appTheme({ - background: @dark-blue-90; - backdrop-filter: blur(8px); + background: @dark-blue-c0; + backdrop-filter: blur(7px); }, { background: url('../assets/parchments/dh-parchment-light.png') no-repeat center; }); @@ -43,7 +43,7 @@ body.game:is(.performance-low, .noblur) { &:hover { border-color: light-dark(@dark-blue, @golden); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; } } } diff --git a/styles/less/global/tab-navigation.less b/styles/less/global/tab-navigation.less index 3f8844f2..3d143b4c 100755 --- a/styles/less/global/tab-navigation.less +++ b/styles/less/global/tab-navigation.less @@ -3,8 +3,7 @@ .daggerheart.dh-style { .tab-navigation { - margin: 5px 0; - height: 40px; + margin: 5px 0 10px 0; width: 100%; .navigation-container { @@ -20,7 +19,11 @@ white-space: nowrap; a { - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; + + &.empty:not(.active) { + opacity: 0.4; + } } } } diff --git a/styles/less/hud/token-hud/token-hud.less b/styles/less/hud/token-hud/token-hud.less index e31ede4a..3b998f4e 100644 --- a/styles/less/hud/token-hud/token-hud.less +++ b/styles/less/hud/token-hud/token-hud.less @@ -24,7 +24,7 @@ .palette-category-title { grid-column: span var(--effect-columns); font-weight: bold; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; } } } @@ -38,6 +38,9 @@ } .status-effects { + // TODO: Remove when the issue https://github.com/foundryvtt/foundryvtt/issues/14410 is resolved and Foundry handles it cleanly themselves. + grid-template-rows: min-content; + .effect-control-container { position: relative; diff --git a/styles/less/sheets-settings/adversary-settings/features.less b/styles/less/sheets-settings/adversary-settings/features.less index 4e0f6a8f..15b1fa18 100644 --- a/styles/less/sheets-settings/adversary-settings/features.less +++ b/styles/less/sheets-settings/adversary-settings/features.less @@ -5,8 +5,6 @@ .tab.features { max-height: 450px; overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; .add-feature-btn { width: 100%; diff --git a/styles/less/sheets-settings/adversary-settings/sheet.less b/styles/less/sheets-settings/adversary-settings/sheet.less index b4b0683b..e6eb8d0b 100644 --- a/styles/less/sheets-settings/adversary-settings/sheet.less +++ b/styles/less/sheets-settings/adversary-settings/sheet.less @@ -7,7 +7,7 @@ &.attack.active { display: flex; flex-direction: column; - gap: 16px; + gap: 12px; } .fieldsets-section { diff --git a/styles/less/sheets-settings/character-settings/sheet.less b/styles/less/sheets-settings/character-settings/sheet.less index eab29436..37906712 100644 --- a/styles/less/sheets-settings/character-settings/sheet.less +++ b/styles/less/sheets-settings/character-settings/sheet.less @@ -1,17 +1,19 @@ @import '../../utils/colors.less'; -.theme-light .application.daggerheart.dh-style.dialog { - .tab.details { - .traits-inner-container .trait-container { - background: url('../assets/svg/trait-shield-light.svg') no-repeat; +.appTheme({}, { + &.dialog.character-settings { + .tab.details { + .traits-inner-container .trait-container { + background: url('../assets/svg/trait-shield-light.svg') no-repeat; - div { - filter: drop-shadow(0 0 3px @beige); - text-shadow: 0 0 3px @beige; + div { + filter: drop-shadow(0 0 3px @beige); + text-shadow: 0 0 3px @beige; + } } } } -} +}); .application.daggerheart.dh-style.dialog { .tab.details { diff --git a/styles/less/sheets-settings/environment-settings/adversaries.less b/styles/less/sheets-settings/environment-settings/adversaries.less index 1a27eaca..2ce4819a 100644 --- a/styles/less/sheets-settings/environment-settings/adversaries.less +++ b/styles/less/sheets-settings/environment-settings/adversaries.less @@ -5,8 +5,6 @@ .tab.adversaries { max-height: 450px; overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; .add-action-btn { width: 100%; diff --git a/styles/less/sheets-settings/environment-settings/features.less b/styles/less/sheets-settings/environment-settings/features.less index d907837a..db6b544d 100644 --- a/styles/less/sheets-settings/environment-settings/features.less +++ b/styles/less/sheets-settings/environment-settings/features.less @@ -5,8 +5,6 @@ .tab.features { max-height: 450px; overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; .add-feature-btn { width: 100%; diff --git a/styles/less/sheets-settings/header.less b/styles/less/sheets-settings/header.less index 82f3c488..04e2fa90 100644 --- a/styles/less/sheets-settings/header.less +++ b/styles/less/sheets-settings/header.less @@ -13,7 +13,7 @@ font-size: var(--font-size-24); margin: 0; text-align: center; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; } } } diff --git a/styles/less/sheets/actors/actor-sheet-shared.less b/styles/less/sheets/actors/actor-sheet-shared.less index bd82ef83..b3eb0469 100644 --- a/styles/less/sheets/actors/actor-sheet-shared.less +++ b/styles/less/sheets/actors/actor-sheet-shared.less @@ -34,15 +34,27 @@ .attribution-header-label { font-style: italic; font-family: @font-body; - color: light-dark(@chat-blue-bg, @beige-50); + color: @color-text-subtle; } .tab.inventory { - .search-section { - display: flex; + .gold-section { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; gap: 10px; - align-items: center; + padding: 10px 10px 0; + + .input { + color: light-dark(@dark, @beige); + } } + } + + .search-section { + display: flex; + gap: 10px; + align-items: center; + justify-content: space-between; .search-bar { position: relative; color: light-dark(@dark-blue-50, @beige-50); @@ -72,22 +84,11 @@ height: 32px; position: absolute; right: 20px; - font-size: 16px; + font-size: var(--font-size-16); z-index: 1; color: light-dark(@dark-blue-50, @beige-50); } } - - .gold-section { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr; - gap: 10px; - padding: 10px 10px 0; - - .input { - color: light-dark(@dark, @beige); - } - } } &.limited { @@ -127,7 +128,7 @@ .title-name { text-align: start; font-size: var(--font-size-28); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; text-align: center; } } @@ -180,9 +181,9 @@ display: flex; gap: 10px; background-color: light-dark(transparent, @dark-blue); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; padding: 5px 10px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; align-items: center; width: fit-content; @@ -194,7 +195,7 @@ font-size: var(--font-size-14); font-weight: bold; text-transform: uppercase; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; } .domain { @@ -206,7 +207,7 @@ font-size: var(--font-size-14); font-weight: bold; text-transform: uppercase; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; } img { @@ -230,7 +231,7 @@ padding: 10px; border-radius: 5px; min-width: 90px; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; background-color: light-dark(@dark-blue-10, @golden-40); } } diff --git a/styles/less/sheets/actors/adversary/effects.less b/styles/less/sheets/actors/adversary/effects.less index 4afe2454..4aa44e51 100644 --- a/styles/less/sheets/actors/adversary/effects.less +++ b/styles/less/sheets/actors/adversary/effects.less @@ -1,4 +1,5 @@ @import '../../../utils/colors.less'; +@import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.adversary { .tab.effects { @@ -7,11 +8,8 @@ flex-direction: column; gap: 10px; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%); padding-bottom: 20px; - - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/actors/adversary/actions.less b/styles/less/sheets/actors/adversary/features.less similarity index 65% rename from styles/less/sheets/actors/adversary/actions.less rename to styles/less/sheets/actors/adversary/features.less index 00395ebd..447d050e 100644 --- a/styles/less/sheets/actors/adversary/actions.less +++ b/styles/less/sheets/actors/adversary/features.less @@ -1,5 +1,6 @@ @import '../../../utils/colors.less'; @import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.adversary { .tab.features { @@ -8,11 +9,8 @@ flex-direction: column; gap: 10px; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%); padding-bottom: 20px; - - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/actors/adversary/header.less b/styles/less/sheets/actors/adversary/header.less index 8bd3fcee..1e5e4fa5 100644 --- a/styles/less/sheets/actors/adversary/header.less +++ b/styles/less/sheets/actors/adversary/header.less @@ -35,7 +35,7 @@ .tags { display: flex; gap: 10px; - padding-bottom: 16px; + padding-bottom: 8px; .tag { display: flex; @@ -67,11 +67,5 @@ gap: 12px; padding: 16px 0; } - - .adversary-navigation { - display: flex; - gap: 8px; - align-items: center; - } } } diff --git a/styles/less/sheets/actors/adversary/index.less b/styles/less/sheets/actors/adversary/index.less new file mode 100644 index 00000000..28ff9d22 --- /dev/null +++ b/styles/less/sheets/actors/adversary/index.less @@ -0,0 +1,7 @@ +@import './features.less'; +@import './header.less'; +@import './sheet.less'; +@import './sidebar.less'; +@import './effects.less'; +@import './notes.less'; + diff --git a/styles/less/sheets/actors/adversary/notes.less b/styles/less/sheets/actors/adversary/notes.less new file mode 100644 index 00000000..a95d070e --- /dev/null +++ b/styles/less/sheets/actors/adversary/notes.less @@ -0,0 +1,3 @@ +.application.sheet.daggerheart.actor.dh-style.adversary .tab.notes.active { + padding-bottom: 20px; +} diff --git a/styles/less/sheets/actors/adversary/sidebar.less b/styles/less/sheets/actors/adversary/sidebar.less index 4e7535c1..5db9f5e9 100644 --- a/styles/less/sheets/actors/adversary/sidebar.less +++ b/styles/less/sheets/actors/adversary/sidebar.less @@ -65,9 +65,9 @@ display: flex; gap: 10px; background-color: light-dark(transparent, @dark-blue); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; padding: 5px 10px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; align-items: center; width: fit-content; @@ -77,7 +77,7 @@ font-size: var(--font-size-14); font-weight: bold; text-transform: uppercase; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; &.threshold-value { color: light-dark(@dark, @beige); @@ -191,7 +191,7 @@ appearance: none; width: 100px; height: 40px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; z-index: 1; background: @dark-blue; @@ -237,7 +237,7 @@ display: flex; width: 50px; height: 30px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-bottom: none; border-radius: 6px 6px 0 0; padding: 0 6px; @@ -286,13 +286,11 @@ overflow-y: hidden; padding-top: 10px; padding-bottom: 20px; - mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); - scrollbar-width: thin; scrollbar-gutter: stable; + .with-scroll-shadows(); &:hover { overflow-y: auto; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; } } diff --git a/styles/less/sheets/actors/character/biography.less b/styles/less/sheets/actors/character/biography.less index 12a662ff..8548a2fb 100644 --- a/styles/less/sheets/actors/character/biography.less +++ b/styles/less/sheets/actors/character/biography.less @@ -1,5 +1,6 @@ @import '../../../utils/colors.less'; @import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.character { .tab.biography { @@ -9,12 +10,15 @@ gap: 10px; height: 100%; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 10%, black 98%, transparent 100%); - padding-bottom: 10px; + padding-top: 8px; + padding-bottom: 20px; height: 100%; + .with-scroll-shadows(); + } - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; + .characteristics-section { + gap: 20px; + padding: 0 10px; } .biography-section { diff --git a/styles/less/sheets/actors/character/effects.less b/styles/less/sheets/actors/character/effects.less index ceadd05e..0ab1007d 100644 --- a/styles/less/sheets/actors/character/effects.less +++ b/styles/less/sheets/actors/character/effects.less @@ -1,5 +1,6 @@ @import '../../../utils/colors.less'; @import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.character { .tab.effects { @@ -8,11 +9,8 @@ flex-direction: column; gap: 10px; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%); padding-bottom: 20px; - - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/actors/character/features.less b/styles/less/sheets/actors/character/features.less index 6a6438ff..52b41826 100644 --- a/styles/less/sheets/actors/character/features.less +++ b/styles/less/sheets/actors/character/features.less @@ -1,5 +1,6 @@ @import '../../../utils/colors.less'; @import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.character { .tab.features { @@ -8,11 +9,8 @@ flex-direction: column; gap: 10px; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%); padding-bottom: 20px; - - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/actors/character/header.less b/styles/less/sheets/actors/character/header.less index 31fd4256..91b3545a 100644 --- a/styles/less/sheets/actors/character/header.less +++ b/styles/less/sheets/actors/character/header.less @@ -5,20 +5,12 @@ // Theme header backgrounds .appTheme({ .character-header-sheet { - .trait { - background: url(../assets/svg/trait-shield.svg) no-repeat; - } - .character-row .domains-section img { filter: @golden-filter; } } }, { .character-header-sheet { - .trait { - background: url('../assets/svg/trait-shield-light.svg') no-repeat; - } - .character-row .domains-section img { filter: brightness(0) saturate(100%); } @@ -33,13 +25,19 @@ .name-row { display: flex; - gap: 5px; + gap: 6px; align-items: start; justify-content: space-between; padding: 0; padding-top: 5px; flex: 1; + [contenteditable], + input { + border: 1px solid @soft-shadow; + background-color: light-dark(@dark-15, @soft-white-shadow); + } + h1 { display: flex; flex: 1; @@ -65,14 +63,16 @@ .label { display: flex; - align-items: center; + align-items: baseline; gap: 4px; } input { + border: none; width: 40px; padding: 0; text-align: center; + font-weight: 600; } .level-button { @@ -101,9 +101,9 @@ display: flex; justify-content: space-between; padding: 5px 0; - margin-bottom: 10px; + margin-bottom: 8px; font-size: var(--font-size-12); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; .missing-header-feature { opacity: 0.5; @@ -131,7 +131,7 @@ display: flex; align-items: center; padding: 0; - margin-bottom: 15px; + margin-bottom: 12px; .resource-section { display: flex; @@ -168,11 +168,11 @@ .domains-section { position: relative; display: flex; - gap: 10px; + gap: 4px; background-color: light-dark(transparent, @dark-blue); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; padding: 5px 10px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; align-items: center; width: fit-content; @@ -182,7 +182,8 @@ font-size: var(--font-size-14); font-weight: bold; text-transform: uppercase; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; + margin-right: 4px; } .domain { @@ -194,7 +195,7 @@ font-size: var(--font-size-14); font-weight: bold; text-transform: uppercase; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; } img { @@ -217,37 +218,110 @@ .character-traits { display: flex; - justify-content: space-between; padding: 0; margin-bottom: 15px; + justify-content: space-between; + max-width: 38.5rem; + gap: 0.5rem; + padding-left: 0.5rem; .trait { - height: 60px; - width: 60px; cursor: pointer; + position: relative; + + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + min-width: 4.375rem; .trait-name { + position: relative; + background-color: @trait-color-bg; + border: 1px solid @trait-color-border; + border-radius: 3px; + color: @color-text-emphatic; + font-size: var(--font-size-12); + font-weight: 600; + height: 1rem; + line-height: 1rem; + white-space: nowrap; + width: 100%; + padding: 0 0.1876px 0 0.375rem; + margin-right: 0.125rem; /* makes it center SLIGHTLY */ + text-shadow: 1px 1px 3px @color-text-shadow-contrast; + display: flex; align-items: center; - padding-top: 5px; - color: light-dark(@dark-blue, @golden); - font-size: var(--font-size-14); - font-weight: 600; - align-items: center; justify-content: center; - gap: 3px; - i { - line-height: 17px; - font-size: var(--font-size-10); + .tier-mark { + position: absolute; + background-color: @dark-blue; + border: 1px solid @color-border; + border-radius: 50%; + width: 1rem; + height: 1rem; + right: calc(100% - 0.4375rem); + display: flex; + justify-content: center; + align-items: center; + &.marked::before { + content: ' '; + position: absolute; + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background-color: @golden; + } } } - .trait-value { - font-style: normal; - font-weight: 400; - font-size: var(--font-size-20); - text-align: center; + .trait-value-area { + --color-border: @trait-color-border; + --background: light-dark(#e8e6e3, @dark-blue); + display: flex; + position: relative; + .trait-value { + position: absolute; + inset: 0; + display: flex; + align-items: center; + + justify-content: center; + font-style: normal; + font-weight: 600; + font-size: var(--font-size-20); + text-align: center; + margin-bottom: 0.375rem; + } + + .spellcasting-mark { + position: absolute; + border: 1px solid @color-border; + color: @golden; + left: 0; + right: 0; + bottom: -0.375rem; + margin-inline: auto; + border-radius: 50%; + width: 1.125rem; + height: 1.125rem; + background: radial-gradient(190.63% 190.63% at 50% -80.63%, #18152E 70%, #4D4494 80%, #A0837E 90%, var(--color-border) 100%); + font-size: var(--font-size-9); + text-shadow: 0 0 2px @light-black; + + display: flex; + align-items: center; + justify-content: center; + } + } + + &:hover { + .trait-name { + color: light-dark(@dark, @beige); + text-shadow: 0 0 8px light-dark(@dark-80, @beige-80); + } } } } diff --git a/styles/less/sheets/actors/character/index.less b/styles/less/sheets/actors/character/index.less new file mode 100644 index 00000000..edefe0a1 --- /dev/null +++ b/styles/less/sheets/actors/character/index.less @@ -0,0 +1,8 @@ +@import './biography.less'; +@import './effects.less'; +@import './features.less'; +@import './header.less'; +@import './inventory.less'; +@import './loadout.less'; +@import './sheet.less'; +@import './sidebar.less'; diff --git a/styles/less/sheets/actors/character/inventory.less b/styles/less/sheets/actors/character/inventory.less index 12f63753..fcfbbee9 100644 --- a/styles/less/sheets/actors/character/inventory.less +++ b/styles/less/sheets/actors/character/inventory.less @@ -1,5 +1,6 @@ @import '../../../utils/colors.less'; @import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.character { .tab.inventory { @@ -8,11 +9,9 @@ flex-direction: column; gap: 10px; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); - padding: 20px 0; - - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; + margin-top: 20px; + padding-bottom: 20px; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/actors/character/loadout.less b/styles/less/sheets/actors/character/loadout.less index eba55890..fa3e0176 100644 --- a/styles/less/sheets/actors/character/loadout.less +++ b/styles/less/sheets/actors/character/loadout.less @@ -1,51 +1,13 @@ @import '../../../utils/colors.less'; @import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.character { .tab.loadout { .search-section { - display: flex; - align-items: center; - justify-content: space-between; - - .search-bar { - position: relative; - color: light-dark(@dark-blue-50, @beige-50); - width: 80%; - padding-top: 5px; - - input { - border-radius: 50px; - background: light-dark(@dark-blue-10, @golden-10); - border: none; - outline: 2px solid transparent; - transition: all 0.3s ease; - padding: 0 20px; - - &:hover { - outline: 2px solid light-dark(@dark, @golden); - } - - &::-webkit-search-cancel-button { - -webkit-appearance: none; - display: none; - } - } - - .icon { - align-content: center; - height: 32px; - position: absolute; - right: 20px; - font-size: var(--font-size-16); - z-index: 1; - color: light-dark(@dark-blue-50, @beige-50); - } - } - .btn-toggle-view { background: light-dark(@dark-blue-10, @dark-blue); - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 15px; padding: 0; gap: 0; @@ -54,7 +16,7 @@ span { margin: 1px; width: 26px; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; &.list-icon { i { @@ -90,11 +52,9 @@ gap: 10px; height: 100%; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 10%, black 98%, transparent 100%); - padding: 20px 0; - - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; + margin-top: 20px; + padding-bottom: 20px; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/actors/character/sidebar.less b/styles/less/sheets/actors/character/sidebar.less index b159a8e8..c76ee9ff 100644 --- a/styles/less/sheets/actors/character/sidebar.less +++ b/styles/less/sheets/actors/character/sidebar.less @@ -40,7 +40,7 @@ .application.sheet.dh-style .character-sidebar-sheet { width: 275px; min-width: 275px; - border-right: 1px solid light-dark(@dark-blue, @golden); + border-right: 1px solid @color-border; .portrait { position: relative; @@ -74,62 +74,6 @@ .death-roll-btn { display: none; } - - .icons-list { - position: absolute; - display: flex; - flex-direction: column; - gap: 5px; - align-items: end; - justify-content: center; - top: 45px; - right: 10px; - - .spellcast-icon { - display: flex; - align-items: center; - justify-content: end; - text-align: center; - padding-right: 8px; - max-width: 50px; - height: 50px; - font-size: 1.2rem; - background: light-dark(@dark-blue-60, @dark-golden-80); - backdrop-filter: blur(8px); - border: 4px double light-dark(@beige, @golden); - color: light-dark(@beige, @golden); - border-radius: 999px; - transition: all 0.3s ease; - - .spellcast-label { - font-size: var(--font-size-14); - opacity: 0; - margin-right: 0.3rem; - transition: all 0.3s ease; - } - - i { - height: 24px; - width: 24px; - align-content: center; - margin-right: 3px; - } - - &:not(.no-label):hover { - max-width: 300px; - padding: 0 10px; - border-radius: 60px; - - .spellcast-label { - opacity: 1; - } - - i { - margin-right: 0px; - } - } - } - } } .info-section { @@ -224,7 +168,7 @@ appearance: none; width: 100px; height: 40px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; z-index: 1; background: @dark-blue; @@ -338,11 +282,11 @@ &:hover { background: light-dark(@light-black, @dark-blue); - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; h4, i { - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; } } } @@ -365,12 +309,12 @@ flex-wrap: wrap; gap: 4px; padding: 5px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; z-index: 1; background: @dark-blue; justify-content: center; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; .armor-slot { cursor: pointer; @@ -399,12 +343,12 @@ &:hover { background: light-dark(@light-black, @dark-blue); - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; .label, .value, i { - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; } } @@ -431,7 +375,7 @@ text-align: center; z-index: 2; color: light-dark(@dark-blue, @beige); - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-bottom: none; border-radius: 6px 6px 0 0; @@ -467,7 +411,7 @@ appearance: none; width: 80px; height: 30px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; z-index: 1; background: light-dark(transparent, @dark-blue); @@ -506,7 +450,7 @@ display: flex; width: 50px; height: 30px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-bottom: none; border-radius: 6px 6px 0 0; padding: 0 6px; @@ -569,9 +513,9 @@ align-self: center; gap: 10px; background-color: light-dark(transparent, @dark-blue); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; padding: 5px 10px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; align-items: center; width: fit-content; @@ -581,7 +525,7 @@ font-size: var(--font-size-14); font-weight: bold; text-transform: uppercase; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; &.threshold-value { color: light-dark(@dark, @beige); @@ -605,13 +549,11 @@ overflow-y: hidden; padding-top: 10px; padding-bottom: 20px; - mask-image: linear-gradient(0deg, transparent 0%, black 5%); scrollbar-gutter: stable; - scrollbar-width: thin; - + .with-scroll-shadows(); + &:hover { overflow-y: auto; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; } } diff --git a/styles/less/sheets/actors/companion/effects.less b/styles/less/sheets/actors/companion/effects.less index 12e1d847..c0cac669 100644 --- a/styles/less/sheets/actors/companion/effects.less +++ b/styles/less/sheets/actors/companion/effects.less @@ -7,11 +7,8 @@ flex-direction: column; gap: 10px; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%); padding-bottom: 20px; - - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/actors/companion/header.less b/styles/less/sheets/actors/companion/header.less index 2a162a25..aca789a6 100644 --- a/styles/less/sheets/actors/companion/header.less +++ b/styles/less/sheets/actors/companion/header.less @@ -11,7 +11,7 @@ .profile { height: 235px; cursor: pointer; - mask-image: linear-gradient(0deg, transparent 0%, black 10%); + .smooth-gradient-ease-in-out(mask-image, to top, black, 2.25rem); } .actor-name { @@ -24,15 +24,20 @@ margin-bottom: -30px; input[type='text'] { + backdrop-filter: none; + border: none; + font-family: @font-title; font-size: var(--font-size-24); - height: 32px; - text-align: center; - border: 1px solid transparent; outline: 2px solid transparent; + box-shadow: unset; + text-shadow: 0 0 4px @color-text-shadow-contrast, 0 0 8px @color-text-shadow-contrast, 0 0 14px @color-text-shadow-contrast; + + height: 2rem; + text-align: center; transition: all 0.3s ease; &:hover { - outline: 2px solid light-dark(@dark, @golden); + outline: 2px solid @color-border; } } } @@ -63,7 +68,7 @@ display: flex; width: 50px; height: 40px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-bottom: none; border-radius: 6px 6px 0 0; padding: 0 6px; @@ -95,104 +100,6 @@ } } - // .status-bar { - // display: flex; - // justify-content: center; - // position: relative; - // width: 100px; - // height: 40px; - - // .status-label { - // position: relative; - // top: 40px; - // height: 22px; - // width: 79px; - // clip-path: path('M0 0H79L74 16.5L39 22L4 16.5L0 0Z'); - // background: light-dark(@dark-blue, @golden); - - // h4 { - // font-weight: bold; - // text-align: center; - // line-height: 18px; - // color: light-dark(@beige, @dark-blue); - // } - // } - // .status-value { - // position: absolute; - // display: flex; - // padding: 0 6px; - // font-size: 1.5rem; - // align-items: center; - // width: 100px; - // height: 40px; - // justify-content: center; - // text-align: center; - // z-index: 2; - // color: @beige; - - // input[type='number'] { - // background: transparent; - // font-size: 1.5rem; - // width: 40px; - // height: 30px; - // text-align: center; - // border: none; - // outline: 2px solid transparent; - // color: @beige; - - // &.bar-input { - // padding: 0; - // color: @beige; - // backdrop-filter: none; - // background: transparent; - // transition: all 0.3s ease; - - // &:hover, - // &:focus { - // background: @semi-transparent-dark-blue; - // backdrop-filter: blur(9.5px); - // } - // } - // } - - // .bar-label { - // width: 40px; - // } - // } - // .progress-bar { - // position: absolute; - // appearance: none; - // width: 100px; - // height: 40px; - // border: 1px solid light-dark(@dark-blue, @golden); - // border-radius: 6px; - // z-index: 1; - // background: @dark-blue; - - // &::-webkit-progress-bar { - // border: none; - // background: @dark-blue; - // border-radius: 6px; - // } - // &::-webkit-progress-value { - // background: @gradient-hp; - // border-radius: 6px; - // } - // &.stress-color::-webkit-progress-value { - // background: @gradient-stress; - // border-radius: 6px; - // } - // &::-moz-progress-bar { - // background: @gradient-hp; - // border-radius: 6px; - // } - // &.stress-color::-moz-progress-bar { - // background: @gradient-stress; - // border-radius: 6px; - // } - // } - // } - .level-div { white-space: nowrap; display: flex; @@ -241,10 +148,8 @@ } .companion-navigation { - display: flex; - gap: 8px; - align-items: center; width: 100%; + padding: 0 10px; } } } diff --git a/styles/less/sheets/actors/companion/index.less b/styles/less/sheets/actors/companion/index.less new file mode 100644 index 00000000..c4931814 --- /dev/null +++ b/styles/less/sheets/actors/companion/index.less @@ -0,0 +1,4 @@ +@import './details.less'; +@import './header.less'; +@import './sheet.less'; +@import './effects.less'; diff --git a/styles/less/sheets/actors/companion/sheet.less b/styles/less/sheets/actors/companion/sheet.less index f31679ba..8bf8a0b9 100644 --- a/styles/less/sheets/actors/companion/sheet.less +++ b/styles/less/sheets/actors/companion/sheet.less @@ -10,3 +10,16 @@ background: url('../assets/parchments/dh-parchment-light.png'); } }); + +.application.sheet.daggerheart.actor.dh-style.companion { + .window-content { + display: flex; + } + + .tab.active { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + } +} diff --git a/styles/less/sheets/actors/environment/actions.less b/styles/less/sheets/actors/environment/features.less similarity index 59% rename from styles/less/sheets/actors/environment/actions.less rename to styles/less/sheets/actors/environment/features.less index 51385322..84cf26f8 100644 --- a/styles/less/sheets/actors/environment/actions.less +++ b/styles/less/sheets/actors/environment/features.less @@ -1,5 +1,6 @@ @import '../../../utils/colors.less'; @import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.environment { .tab.features { @@ -8,11 +9,8 @@ flex-direction: column; gap: 10px; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%); - padding-bottom: 20px; - - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; + padding-bottom: 4px; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/actors/environment/header.less b/styles/less/sheets/actors/environment/header.less index 670f6c92..da6954e0 100644 --- a/styles/less/sheets/actors/environment/header.less +++ b/styles/less/sheets/actors/environment/header.less @@ -1,5 +1,6 @@ @import '../../../utils/colors.less'; @import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.environment { .environment-header-sheet { @@ -10,60 +11,82 @@ .profile { height: 235px; - mask-image: linear-gradient(0deg, transparent 0%, black 10%); cursor: pointer; + .smooth-gradient-ease-in-out(mask-image, to top, black, 3.5rem); } .item-container { - display: flex; + display: grid; + grid-auto-flow: row; + grid-template-columns: 1fr min-content; + align-items: center; position: relative; - top: -45px; - gap: 20px; + top: -36px; + gap: 0 var(--spacer-12); padding: 0 20px; - margin-bottom: -30px; + margin-bottom: -26px; - .item-info { + .item-name input[type='text'] { + backdrop-filter: none; + border: none; + font-family: @font-title; + font-size: var(--font-size-32); + text-align: start; + transition: all 0.3s ease; + outline: 2px solid transparent; + box-shadow: none; + text-shadow: 0 0 4px @color-text-shadow-contrast, 0 0 8px @color-text-shadow-contrast, 0 0 14px @color-text-shadow-contrast; + padding-left: 0; + height: 2.625rem; + + &:hover[type='text'], + &:focus[type='text'] { + box-shadow: none; + outline: 2px solid @color-border; + } + } + + .flexrow { + align-items: baseline; + grid-column: span 2; + } + + .tags { display: flex; - flex-direction: column; - gap: 8px; + gap: 10px; + padding-bottom: 0; + flex: 0; - .tags { + .tag { display: flex; - gap: 10px; - padding-bottom: 0; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 3px 5px; + font-size: var(--font-size-12); + font: @font-body; + white-space: nowrap; - .tag { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - padding: 3px 5px; - font-size: var(--font-size-12); - font: @font-body; - - background: light-dark(@dark-15, @beige-15); - border: 1px solid light-dark(@dark, @beige); - border-radius: 3px; - } - - .label { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - font-size: var(--font-size-12); - } + background: light-dark(@dark-15, @beige-15); + border: 1px solid light-dark(@dark, @beige); + border-radius: 3px; } - .attribution-header-label { - text-align: left; - position: relative; - top: 4px; - margin-bottom: -6px; + .label { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + font-size: var(--font-size-12); } } + .attribution-header-label { + text-align: right; + position: relative; + } + .status-number { display: flex; align-items: center; @@ -74,14 +97,14 @@ display: flex; width: 50px; height: 30px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-bottom: none; border-radius: 6px 6px 0 0; padding: 0 6px; font-size: 1.2rem; align-items: center; justify-content: center; - background: light-dark(transparent, @dark-blue); + background: light-dark(#e8e6e3, @dark-blue); z-index: 2; &.armor-slots { @@ -93,7 +116,7 @@ .status-label { padding: 2px 10px; width: 100%; - border-radius: 3px; + border-radius: 0 0 3px 3px; background: light-dark(@dark-blue, @golden); h4 { @@ -105,37 +128,21 @@ } } } - - .item-name { - input[type='text'] { - font-size: var(--font-size-32); - height: 42px; - text-align: start; - transition: all 0.3s ease; - outline: 2px solid transparent; - border: 1px solid transparent; - - &:hover[type='text'], - &:focus[type='text'] { - box-shadow: none; - outline: 2px solid light-dark(@dark-blue, @golden); - } - } - } } .environment-info { display: flex; flex-direction: column; - gap: 12px; + gap: var(--spacer-8); padding: 10px 20px; } .environment-navigation { - display: flex; - gap: 20px; - align-items: center; padding: 0 20px; + + .tab-navigation { + margin-top: 0; + } } } } diff --git a/styles/less/sheets/actors/environment/index.less b/styles/less/sheets/actors/environment/index.less new file mode 100644 index 00000000..211c8e60 --- /dev/null +++ b/styles/less/sheets/actors/environment/index.less @@ -0,0 +1,4 @@ +@import './features.less'; +@import './header.less'; +@import './potentialAdversaries.less'; +@import './sheet.less'; diff --git a/styles/less/sheets/actors/environment/potentialAdversaries.less b/styles/less/sheets/actors/environment/potentialAdversaries.less index 274362a5..f112c0d2 100644 --- a/styles/less/sheets/actors/environment/potentialAdversaries.less +++ b/styles/less/sheets/actors/environment/potentialAdversaries.less @@ -7,11 +7,8 @@ flex-direction: column; gap: 10px; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%); - padding-bottom: 20px; - - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; + padding-bottom: 4px; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/actors/environment/sheet.less b/styles/less/sheets/actors/environment/sheet.less index 3ea14bc7..2d9cc188 100644 --- a/styles/less/sheets/actors/environment/sheet.less +++ b/styles/less/sheets/actors/environment/sheet.less @@ -5,10 +5,6 @@ .appTheme({ &.environment { background-image: url('../assets/parchments/dh-parchment-dark.png'); - - .attribution-header-label { - background-image: url('../assets/parchments/dh-parchment-dark.png'); - } } }, { &.environment { @@ -20,8 +16,6 @@ .tab { flex: 1; overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; &.active { overflow: hidden; diff --git a/styles/less/sheets/actors/npc/features.less b/styles/less/sheets/actors/npc/features.less new file mode 100644 index 00000000..a579d9f8 --- /dev/null +++ b/styles/less/sheets/actors/npc/features.less @@ -0,0 +1,18 @@ +.application.sheet.daggerheart.actor.dh-style.npc { + .tab.features { + &.active { + overflow: hidden; + display: flex; + flex-direction: column; + } + + .feature-section { + display: flex; + flex-direction: column; + gap: 10px; + overflow-y: auto; + padding-bottom: 4px; + .with-scroll-shadows(); + } + } +} diff --git a/styles/less/sheets/actors/npc/header.less b/styles/less/sheets/actors/npc/header.less new file mode 100644 index 00000000..086a254c --- /dev/null +++ b/styles/less/sheets/actors/npc/header.less @@ -0,0 +1,83 @@ +.application.sheet.daggerheart.actor.dh-style.npc { + .npc-header-sheet { + width: 100%; + display: flex; + + .portrait { + cursor: pointer; + max-width: 275px; + + img { + height: 275px; + } + } + + .tags { + display: flex; + gap: 10px; + padding-bottom: 8px; + + .tag { + display: flex; + flex-direction: row; + gap: 4px; + justify-content: center; + align-items: center; + padding: 3px 5px; + font-size: var(--font-size-12); + font: @font-body; + + background: light-dark(@dark-15, @beige-15); + border: 1px solid light-dark(@dark, @beige); + border-radius: 3px; + } + + .label { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + font-size: var(--font-size-12); + } + } + + .info-section { + flex: 1; + padding: 0 15px; + padding-top: var(--header-height); + display: flex; + flex-direction: column; + + .name-row { + display: flex; + gap: 5px; + align-items: center; + justify-content: space-between; + padding: 8px 0; + + h1 { + display: flex; + flex: 1; + padding: 6px 0 0 0; + font-size: var(--font-size-32); + text-align: start; + border: 1px solid transparent; + outline: 2px solid transparent; + transition: all 0.3s ease; + word-break: break-word; + + &:hover { + outline: 2px solid light-dark(@dark, @golden); + } + } + } + + .npc-info { + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px 0; + } + } + } +} \ No newline at end of file diff --git a/styles/less/sheets/actors/npc/index.less b/styles/less/sheets/actors/npc/index.less new file mode 100644 index 00000000..2d7d54e3 --- /dev/null +++ b/styles/less/sheets/actors/npc/index.less @@ -0,0 +1,3 @@ +@import './sheet.less'; +@import './header.less'; +@import './features.less'; \ No newline at end of file diff --git a/styles/less/sheets/actors/npc/sheet.less b/styles/less/sheets/actors/npc/sheet.less new file mode 100644 index 00000000..8ba3b7a9 --- /dev/null +++ b/styles/less/sheets/actors/npc/sheet.less @@ -0,0 +1,10 @@ +.application.sheet.daggerheart.actor.dh-style.npc { + .window-content { + display: grid; + grid-template-rows: auto auto 1fr; + } + + .npc-navigation { + padding: 0 15px; + } +} \ No newline at end of file diff --git a/styles/less/sheets/actors/party/header.less b/styles/less/sheets/actors/party/header.less index 9a2c7350..58e3bc31 100644 --- a/styles/less/sheets/actors/party/header.less +++ b/styles/less/sheets/actors/party/header.less @@ -1,5 +1,6 @@ @import '../../../utils/colors.less'; @import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; .party-header-sheet { display: flex; @@ -9,26 +10,31 @@ .profile { height: 235px; - mask-image: linear-gradient(0deg, transparent 0%, black 10%); cursor: pointer; + .smooth-gradient-ease-in-out(mask-image, to top, black, 3.5rem); } .item-container { - .item-name { - padding: 0 20px; - input[type='text'] { - font-size: 32px; - height: 42px; - text-align: center; - transition: all 0.3s ease; - outline: 2px solid transparent; - border: 1px solid transparent; + margin-top: -2rem; + z-index: 1; + input.item-name[type='text'] { + backdrop-filter: none; + border: none; + color: @color-text-emphatic; + font-family: @font-title; + font-size: var(--font-size-32); + outline: 2px solid transparent; + box-shadow: unset; + text-shadow: 0 0 4px @color-text-shadow-contrast, 0 0 8px @color-text-shadow-contrast, 0 0 14px @color-text-shadow-contrast; - &:hover[type='text'], - &:focus[type='text'] { - box-shadow: none; - outline: 2px solid light-dark(@dark-blue, @golden); - } + text-align: center; + transition: all 0.3s ease; + width: calc(100% - 40px); + height: 2.625rem; + + &:hover[type='text'], + &:focus[type='text'] { + outline: 2px solid @color-border; } } diff --git a/styles/less/sheets/actors/party/index.less b/styles/less/sheets/actors/party/index.less new file mode 100644 index 00000000..56f7a457 --- /dev/null +++ b/styles/less/sheets/actors/party/index.less @@ -0,0 +1,4 @@ +@import './header.less'; +@import './party-members.less'; +@import './sheet.less'; +@import './inventory.less'; diff --git a/styles/less/sheets/actors/party/inventory.less b/styles/less/sheets/actors/party/inventory.less index ac59e1de..444c6a57 100644 --- a/styles/less/sheets/actors/party/inventory.less +++ b/styles/less/sheets/actors/party/inventory.less @@ -1,5 +1,6 @@ @import '../../../utils/colors.less'; @import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.party { .tab.inventory { @@ -8,11 +9,9 @@ flex-direction: column; gap: 10px; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); - padding: 20px 0; - - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; + margin-top: 20px; + padding-bottom: 4px; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/actors/party/party-members.less b/styles/less/sheets/actors/party/party-members.less index 2e2f4cf8..dc464291 100644 --- a/styles/less/sheets/actors/party/party-members.less +++ b/styles/less/sheets/actors/party/party-members.less @@ -2,16 +2,6 @@ @import '../../../utils/fonts.less'; @import '../../../utils/mixin.less'; -body.game:is(.performance-low, .noblur) { - .application.sheet.daggerheart.actor.dh-style.party .tab.resources .actors-list .actor-resources { - background: light-dark(@dark-blue, @dark-golden); - - .actor-name { - background: light-dark(@dark-blue, @dark-golden); - } - } -} - .application.sheet.daggerheart.actor.dh-style.party .tab.partyMembers { overflow: auto; @@ -58,7 +48,7 @@ body.game:is(.performance-low, .noblur) { border-radius: 50%; width: 24px; height: 24px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; object-fit: cover; } } @@ -89,9 +79,9 @@ body.game:is(.performance-low, .noblur) { display: flex; gap: 4px; background-color: light-dark(var(--color-light-1), @dark-blue); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; padding: 4px 6px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 3px; align-items: baseline; width: fit-content; @@ -103,7 +93,7 @@ body.game:is(.performance-low, .noblur) { &.threshold-label { font-size: var(--font-size-10); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; } &.threshold-value { @@ -126,7 +116,7 @@ body.game:is(.performance-low, .noblur) { width: 100%; z-index: 1; font-size: var(--font-size-20); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; font-weight: bold; } @@ -142,9 +132,9 @@ body.game:is(.performance-low, .noblur) { .hope-section { display: flex; background-color: light-dark(transparent, @dark-blue); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; padding: 3px 6px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 3px; align-items: center; width: fit-content; @@ -154,7 +144,7 @@ body.game:is(.performance-low, .noblur) { font-size: var(--font-size-12); font-weight: bold; text-transform: uppercase; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; margin-right: 3px; } @@ -222,9 +212,9 @@ body.game:is(.performance-low, .noblur) { gap: 4px; background-color: light-dark(@dark-blue-10, @dark-blue); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; padding: 2px 5px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 0 6px 6px 0; width: fit-content; min-height: 22px; @@ -242,7 +232,7 @@ body.game:is(.performance-low, .noblur) { .slot { width: 16px; height: 10px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; background: light-dark(@dark-blue-10, @golden-10); border-radius: 3px; transition: all 0.3s ease; @@ -258,7 +248,7 @@ body.game:is(.performance-low, .noblur) { .traits { background-color: light-dark(@dark-blue-10, @dark-blue); - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; display: grid; grid-template-columns: 1fr 1fr; @@ -270,7 +260,7 @@ body.game:is(.performance-low, .noblur) { justify-content: space-between; gap: 3px; .label { - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; } .value { font-weight: 600; @@ -280,6 +270,17 @@ body.game:is(.performance-low, .noblur) { } } + .actors-list.limited { + .actor-resources { + display: flex; + align-items: center; + } + .actor-img-frame { + width: 3rem; + height: 3rem; + } + } + .actors-dragger { display: flex; align-items: center; diff --git a/styles/less/sheets/actors/party/sheet.less b/styles/less/sheets/actors/party/sheet.less index 6b51de53..852b6cfc 100644 --- a/styles/less/sheets/actors/party/sheet.less +++ b/styles/less/sheets/actors/party/sheet.less @@ -20,8 +20,6 @@ .tab { flex: 1; overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; scrollbar-gutter: stable; &.active { diff --git a/styles/less/sheets/index.less b/styles/less/sheets/index.less index 7d595614..4312f755 100644 --- a/styles/less/sheets/index.less +++ b/styles/less/sheets/index.less @@ -2,35 +2,12 @@ @import './actors/actor-sheet-shared.less'; -@import './actors/adversary/actions.less'; -@import './actors/adversary/header.less'; -@import './actors/adversary/sheet.less'; -@import './actors/adversary/sidebar.less'; -@import './actors/adversary/effects.less'; - -@import './actors/character/biography.less'; -@import './actors/character/effects.less'; -@import './actors/character/features.less'; -@import './actors/character/header.less'; -@import './actors/character/inventory.less'; -@import './actors/character/loadout.less'; -@import './actors/character/sheet.less'; -@import './actors/character/sidebar.less'; - -@import './actors/companion/details.less'; -@import './actors/companion/header.less'; -@import './actors/companion/sheet.less'; -@import './actors/companion/effects.less'; - -@import './actors/environment/actions.less'; -@import './actors/environment/header.less'; -@import './actors/environment/potentialAdversaries.less'; -@import './actors/environment/sheet.less'; - -@import './actors/party/header.less'; -@import './actors/party/party-members.less'; -@import './actors/party/sheet.less'; -@import './actors/party/inventory.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'; diff --git a/styles/less/sheets/items/domain-card.less b/styles/less/sheets/items/domain-card.less index a784b3a2..54378fd0 100644 --- a/styles/less/sheets/items/domain-card.less +++ b/styles/less/sheets/items/domain-card.less @@ -5,7 +5,5 @@ section.tab { height: 400px; overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; } } diff --git a/styles/less/sheets/items/feature.less b/styles/less/sheets/items/feature.less index b7493f15..f3c7cd49 100644 --- a/styles/less/sheets/items/feature.less +++ b/styles/less/sheets/items/feature.less @@ -14,7 +14,5 @@ section.tab { height: 400px; overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; } } diff --git a/styles/less/sheets/items/item-sheet-shared.less b/styles/less/sheets/items/item-sheet-shared.less index d0a8cc48..5155ad70 100644 --- a/styles/less/sheets/items/item-sheet-shared.less +++ b/styles/less/sheets/items/item-sheet-shared.less @@ -10,4 +10,8 @@ font-family: @font-body; color: light-dark(@chat-blue-bg, @beige-50); } + + button.plain.inline-control { + flex: 0 0 auto; + } } diff --git a/styles/less/ui/chat/group-roll.less b/styles/less/ui/chat/group-roll.less index 9ed87220..98f0cfac 100644 --- a/styles/less/ui/chat/group-roll.less +++ b/styles/less/ui/chat/group-roll.less @@ -31,7 +31,7 @@ } i { - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; } } } @@ -71,7 +71,7 @@ align-items: center; justify-content: center; gap: 10px; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; .main-value { font-size: var(--font-size-24); @@ -153,7 +153,7 @@ cursor: pointer; padding: 5px; background: light-dark(@dark-blue-10, @golden-10); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; &.finished { background-color: initial; diff --git a/styles/less/ui/chat/sheet.less b/styles/less/ui/chat/sheet.less index 66b539b4..bdf22364 100644 --- a/styles/less/ui/chat/sheet.less +++ b/styles/less/ui/chat/sheet.less @@ -93,7 +93,7 @@ } a[href] { - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; } a[href]:hover, @@ -195,7 +195,7 @@ fieldset.daggerheart.chat { &:before, &:after { content: '\f0d8'; - font-family: 'Font Awesome 6 Pro'; + font-family: var(--font-awesome); } } &.expanded { diff --git a/styles/less/ui/countdown/countdown-edit.less b/styles/less/ui/countdown/countdown-edit.less index d6c4da93..78ad3a06 100644 --- a/styles/less/ui/countdown/countdown-edit.less +++ b/styles/less/ui/countdown/countdown-edit.less @@ -60,8 +60,6 @@ overflow-y: auto; overflow-x: hidden; max-height: 500px; - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; .countdown-edit-outer-container { display: flex; diff --git a/styles/less/ui/countdown/countdown.less b/styles/less/ui/countdown/countdown.less index 47f06eb7..63e539ba 100644 --- a/styles/less/ui/countdown/countdown.less +++ b/styles/less/ui/countdown/countdown.less @@ -1,28 +1,47 @@ @import '../../utils/colors.less'; @import '../../utils/fonts.less'; -.theme-dark { +#interface.theme-dark { .daggerheart.dh-style.countdowns { - background-image: url(../assets/parchments/dh-parchment-dark.png); - - .window-header { - background-image: url(../assets/parchments/dh-parchment-dark.png); - } + --background: url(../assets/parchments/dh-parchment-dark.png); } } -.daggerheart.dh-style.countdowns { +#interface.theme-light { + .daggerheart.dh-style.countdowns { + --background: url('../assets/parchments/dh-parchment-light.png') no-repeat center; + } +} + +.daggerheart.dh-style.countdowns { position: relative; border: 0; - border-radius: 4px; box-shadow: none; - width: 300px; + color: @color-text-primary; + width: 18.75rem; pointer-events: all; align-self: flex-end; transition: 0.3s right ease-in-out; - .window-title { - font-family: @font-body; + display: flex; + flex-direction: column; + + &::before { + content: ' '; + position: absolute; + inset: 0; + background: var(--background); + border-radius: 4px; + opacity: var(--ui-fade-opacity); + transition: opacity var(--ui-fade-duration); + } + + &:not(.performance-low, .noblur) { + backdrop-filter: blur(5px); + } + + &:hover::before { + opacity: 1; } #ui-right:has(#effects-display .effect-container) & { @@ -30,127 +49,139 @@ } &.icon-only { - width: 180px; - min-width: 180px; + width: 12rem; } - .window-header { - cursor: default; - border-bottom: 0; + .countdowns-header, + .countdowns-container { + position: relative; // allow rendering over the background } - .window-content { - padding-top: 4px; - padding-bottom: 16px; + .countdowns-header { + display: flex; + align-items: center; + gap: 0.25rem; + flex: 0 0 36px; + 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; + } + } + + .countdowns-container { + display: flex; + flex-direction: column; + gap: 8px; + padding: 4px var(--spacer-16) var(--spacer-16) var(--spacer-16); overflow: auto; max-height: 312px; - .countdowns-container { + .countdown-container { display: flex; - flex-direction: column; - gap: 8px; + width: 100%; - .countdown-container { - display: flex; - width: 100%; + &.icon-only { + gap: 8px; - &.icon-only { - gap: 8px; + .countdown-main-container { + .countdown-content { + justify-content: center; - .countdown-main-container { - .countdown-content { - justify-content: center; - - .countdown-tools { - gap: 8px; - } + .countdown-tools { + gap: 8px; } } } + } - .countdown-main-container { - width: 100%; + .countdown-main-container { + width: 100%; + display: flex; + align-items: center; + gap: 16px; + + img { + width: 2.75rem; + height: 2.75rem; + border-radius: 6px; + } + + .countdown-content { + flex: 1; display: flex; - align-items: center; - gap: 16px; + flex-direction: column; + justify-content: space-between; - img { - width: 44px; - height: 44px; - border-radius: 6px; - } - - .countdown-content { - flex: 1; + .countdown-tools { display: flex; - flex-direction: column; + align-items: center; justify-content: space-between; - .countdown-tools { + .countdown-tool-controls { display: flex; align-items: center; - justify-content: space-between; + gap: var(--spacer-12); + } - .countdown-tool-controls { - display: flex; - align-items: center; - gap: 16px; - } + .progress-tag { + border: 1px solid; + border-radius: 4px; + padding: 2px 4px; + background-color: light-dark(@beige, @dark-blue); + } - .progress-tag { - border: 1px solid; - border-radius: 4px; + .countdown-tool-icons { + display: flex; + align-items: center; + gap: 8px; + + .looping-container { + position: relative; + border: 1px solid light-dark(@dark-blue, white); + border-radius: 6px; padding: 2px 4px; - background-color: light-dark(@beige, @dark-blue); - } - .countdown-tool-icons { - display: flex; - align-items: center; - gap: 8px; + &.should-loop { + background: light-dark(@golden, @golden); - .looping-container { - position: relative; - border: 1px solid light-dark(@dark-blue, white); - border-radius: 6px; - padding: 2px 4px; - - &.should-loop { - background: light-dark(@golden, @golden); - - .loop-marker { - color: light-dark(@dark-blue, @dark-blue); - } + .loop-marker { + color: light-dark(@dark-blue, @dark-blue); } + } - .direction-marker { - position: absolute; - font-size: 10px; - filter: drop-shadow(0 0 3px black); - top: -3px; - } + .direction-marker { + position: absolute; + font-size: 10px; + filter: drop-shadow(0 0 3px black); + top: -3px; } } } } } - /* Keep incase we want to reintroduce the player pips */ - // .countdown-access-container { - // display: grid; - // grid-template-columns: 1fr 1fr 1fr; - // grid-auto-rows: min-content; - // width: 38px; - // gap: 4px; - - // .countdown-access { - // height: 10px; - // width: 10px; - // border-radius: 50%; - // border: 1px solid light-dark(@dark-blue, @beige-80); - // content: ''; - // } - // } } + /* Keep incase we want to reintroduce the player pips */ + // .countdown-access-container { + // display: grid; + // grid-template-columns: 1fr 1fr 1fr; + // grid-auto-rows: min-content; + // width: 38px; + // gap: 4px; + + // .countdown-access { + // height: 10px; + // width: 10px; + // border-radius: 50%; + // border: 1px solid light-dark(@dark-blue, @beige-80); + // content: ''; + // } + // } } } } diff --git a/styles/less/ui/effects-display/sheet.less b/styles/less/ui/effects-display/sheet.less index 17d9889f..80ad0d65 100644 --- a/styles/less/ui/effects-display/sheet.less +++ b/styles/less/ui/effects-display/sheet.less @@ -20,7 +20,7 @@ .effect-container { position: relative; pointer-events: all; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 3px; img { diff --git a/styles/less/ui/item-browser/item-browser.less b/styles/less/ui/item-browser/item-browser.less index f558a0ba..aac63d7a 100644 --- a/styles/less/ui/item-browser/item-browser.less +++ b/styles/less/ui/item-browser/item-browser.less @@ -1,5 +1,6 @@ @import '../../utils/colors.less'; @import '../../utils/fonts.less'; +@import '../../utils/mixin.less'; .application.daggerheart.dh-style.compendium-browser { border: initial; @@ -125,6 +126,7 @@ .form-group { white-space: nowrap; + gap: 0; } .filter-header { @@ -168,6 +170,10 @@ } } } + + .filter-content { + margin-top: 8px; + } } .folder-list { @@ -195,7 +201,7 @@ font-weight: bold; border-radius: 3px; background-color: light-dark(@dark-blue-40, @golden-40); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; } .subfolder-list { @@ -213,7 +219,7 @@ font-weight: bold; border-radius: 3px; background-color: light-dark(@dark-blue-10, @golden-10); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; } .wrapper { @@ -237,12 +243,11 @@ .compendium-sidebar > .folder-list { overflow-y: auto; scrollbar-gutter: stable; - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; + .with-scroll-shadows(); } .item-list-header, - .item-list [data-action='expandContent'] { + .item-list .item-info[data-action] { display: flex; > * { @@ -262,8 +267,8 @@ .item-list-header { align-items: center; background-color: light-dark(@dark-15, @dark-golden-80); - color: light-dark(@dark-blue, @golden); - border: 1px solid light-dark(@dark-blue, @golden); + color: @color-text-emphatic; + border: 1px solid @color-border; border-radius: 3px; min-height: 30px; font-weight: bold; @@ -274,7 +279,7 @@ div[data-sort-key] { &:after { - font-family: 'Font Awesome 6 Pro'; + font-family: var(--font-awesome); margin-left: 5px; } diff --git a/styles/less/ui/settings/homebrew-settings/domains.less b/styles/less/ui/settings/homebrew-settings/domains.less index da258fcd..6314cc66 100644 --- a/styles/less/ui/settings/homebrew-settings/domains.less +++ b/styles/less/ui/settings/homebrew-settings/domains.less @@ -55,14 +55,12 @@ gap: 8px; max-height: 184px; overflow: auto; - scrollbar-width: thin; - scrollbar-color: light-dark(#18162e, #f3c267) transparent; .domain-container { position: relative; display: flex; justify-content: center; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; &.selectable { @@ -78,7 +76,7 @@ .domain-label { position: absolute; top: 4px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; padding: 0 2px; color: light-dark(@dark, @beige); diff --git a/styles/less/ui/settings/homebrew-settings/resources.less b/styles/less/ui/settings/homebrew-settings/resources.less index 5333e54d..9d562756 100644 --- a/styles/less/ui/settings/homebrew-settings/resources.less +++ b/styles/less/ui/settings/homebrew-settings/resources.less @@ -24,7 +24,7 @@ .resource-icons-container { display: flex; justify-content: space-between; - gap: 8px; + gap: 10px; width: 100%; .resource-icon-container { @@ -61,7 +61,7 @@ display: flex; align-items: center; gap: 4px; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; i { font-size: 14px; diff --git a/styles/less/ui/settings/homebrew-settings/types.less b/styles/less/ui/settings/homebrew-settings/types.less index d09431f7..1d568853 100644 --- a/styles/less/ui/settings/homebrew-settings/types.less +++ b/styles/less/ui/settings/homebrew-settings/types.less @@ -21,7 +21,7 @@ border: 1px solid; border-radius: 6px; padding: 0 8px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; color: light-dark(@dark, @beige); background-image: url('../assets/parchments/dh-parchment-dark.png'); cursor: pointer; diff --git a/styles/less/ui/settings/settings.less b/styles/less/ui/settings/settings.less index 34f17d53..35c48480 100644 --- a/styles/less/ui/settings/settings.less +++ b/styles/less/ui/settings/settings.less @@ -1,6 +1,67 @@ @import '../../utils/colors.less'; .daggerheart.dh-style.setting { + --color-form-label: @color-text-primary; + + h2, + h3, + h4 { + margin: 8px 0 4px; + text-align: center; + } + + footer { + margin-top: 8px; + display: flex; + gap: 8px; + + button { + flex: 1; + } + } + + .standard-form { + gap: var(--spacer-8); + .form-group .form-fields { + width: unset; + } + } + + .form-group { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.25rem 0.5rem; + flex-wrap: wrap; + + label { + font-size: var(--font-size-14); + font-weight: normal; + line-height: var(--input-height); + } + + .form-fields { + display: flex; + gap: 4px; + align-items: center; + } + + &.setting-two-values { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.25rem 0.5rem; + + .form-group { + justify-content: end; + flex-wrap: nowrap; + } + + .hint { + grid-column: 1 / -1; + } + } + } + fieldset { display: flex; flex-direction: column; @@ -19,7 +80,10 @@ &.three-columns { display: grid; grid-template-columns: 1fr 1fr 1fr; - gap: 2px; + gap: 4px; + .form-group label { + line-height: unset; + } } &.six-columns { diff --git a/styles/less/ui/sidebar/daggerheartMenu.less b/styles/less/ui/sidebar/daggerheartMenu.less index 88b139c5..280d5ad3 100644 --- a/styles/less/ui/sidebar/daggerheartMenu.less +++ b/styles/less/ui/sidebar/daggerheartMenu.less @@ -34,7 +34,7 @@ cursor: pointer; padding: 5px; background: light-dark(@dark-blue-10, @golden-10); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; .label { font-style: normal; diff --git a/styles/less/utils/colors.less b/styles/less/utils/colors.less index 8f6418bf..bb219ebb 100755 --- a/styles/less/utils/colors.less +++ b/styles/less/utils/colors.less @@ -5,6 +5,7 @@ --golden: #f3c267; --golden-10: #f3c26710; --golden-40: #f3c26740; + --golden-60: #f3c26760; --golden-90: #f3c26790; --golden-bg: #f3c2671a; --golden-secondary: #eaaf42; @@ -50,6 +51,7 @@ --dark-blue-50: #18162e50; --dark-blue-60: #18162e60; --dark-blue-90: #18162e90; + --dark-blue-c0: #18162ec0; --semi-transparent-dark-blue: rgba(24, 22, 46, 0.33); --dark: #222222; @@ -69,6 +71,7 @@ --beige-filter: brightness(0) saturate(100%) invert(87%) sepia(25%) saturate(164%) hue-rotate(339deg) brightness(106%) contrast(87%); --bright-beige-filter: brightness(0) saturate(100%) invert(87%) sepia(15%) saturate(343%) hue-rotate(333deg) brightness(110%) contrast(87%); + --light-white: rgba(255, 255, 255, 0.3); --soft-white-shadow: rgba(255, 255, 255, 0.05); --light-black: rgba(0, 0, 0, 0.3); @@ -83,12 +86,54 @@ --primary-color-fear: rgba(9, 71, 179, 0.75); } +/** + * Override core foundry color variables in theme scopes, and add some new theme specific variants. + * These are intended to be more representative of its use case, so avoid naming these after the color. + * Some foundry variables are heavily scoped and we need to redefine them (https://github.com/foundryvtt/foundryvtt/issues/12893) + * Convention for the redefined ones is --dh-{element}-color-{thing} + */ +@scope (.theme-light) to (.themed) { + .dh-style :scope, + :scope.dh-style, + .dh-style, + .duality { + --color-border: @dark-blue; + --color-fieldset-border: @dark-blue; + --color-text-emphatic: @dark-blue; + --color-text-subtle: #555; + + --dh-color-text-shadow-contrast: #ffffffa0; + --dh-input-color-border: @dark; + --dh-input-color-text: @dark; + --dh-trait-color-bg: #b1afb6; + --dh-trait-color-border: #8e8d96; + } +} +@scope (.theme-dark) to (.themed) { + .dh-style :scope, + :scope.dh-style, + .dh-style, + .duality { + --color-border: @golden; + --color-fieldset-border: @golden; + --color-text-emphatic: @golden; + --color-text-subtle: #a29086; + + --dh-color-text-shadow-contrast: @dark-80; + --dh-input-color-border: @beige; + --dh-input-color-text: @beige; + --dh-trait-color-bg: #50433F; + --dh-trait-color-border: #927952; + } +} + @primary-blue: var(--primary-blue, #1488cc); @secondary-blue: var(--secondary-blue, #2b32b2); @golden: var(--golden, #f3c267); @golden-10: var(--golden-10, #f3c26710); @golden-40: var(--golden-40, #f3c26740); +@golden-60: var(--golden-60, #f3c26760); @golden-90: var(--golden-90, #f3c26790); @golden-bg: var(--golden-bg, #f3c2671a); @golden-secondary: var(--golden-secondary, #eaaf42); @@ -134,6 +179,7 @@ @dark-blue-50: var(--dark-blue-50, #18162e50); @dark-blue-60: var(--dark-blue-60, #18162e60); @dark-blue-90: var(--dark-blue-90, #18162e90); +@dark-blue-c0: var(--dark-blue-c0, #18162ec0); @semi-transparent-dark-blue: var(--semi-transparent-dark-blue, rgba(24, 22, 46, 0.33)); @dark: var(--dark, #222222); @@ -155,6 +201,7 @@ @soft-white-shadow: var(--soft-white-shadow, rgba(255, 255, 255, 0.05)); +@light-white: var(--light-white, rgba(255, 255, 255, 0.3)); @light-black: var(--light-black, rgba(0, 0, 0, 0.3)); @soft-shadow: var(--soft-shadow, rgba(0, 0, 0, 0.05)); @@ -186,3 +233,15 @@ box-shadow: 0 0 2px 2px @dark-blue; } } + +// LESS variable versions of core foundry color variables +@color-border: var(--color-border); +@color-fieldset-border: var(--color-fieldset-border); +@color-text-emphatic: var(--color-text-emphatic); +@color-text-primary: var(--color-text-primary); +@color-text-subtle: var(--color-text-subtle); +@color-text-shadow-contrast: var(--dh-color-text-shadow-contrast); +@input-color-border: var(--dh-input-color-border); +@input-color-text: var(--dh-input-color-text); +@trait-color-bg: var(--dh-trait-color-bg); +@trait-color-border: var(--dh-trait-color-border); \ No newline at end of file diff --git a/styles/less/utils/fonts.less b/styles/less/utils/fonts.less index 5c1e597a..201ea356 100755 --- a/styles/less/utils/fonts.less +++ b/styles/less/utils/fonts.less @@ -5,6 +5,11 @@ --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"; diff --git a/styles/less/utils/mixin.less b/styles/less/utils/mixin.less index 49e97a1f..18b1f9a6 100644 --- a/styles/less/utils/mixin.less +++ b/styles/less/utils/mixin.less @@ -5,16 +5,16 @@ */ .appTheme(@darkRules, @lightRules) { // Dark theme selectors - .themed.theme-dark .application.daggerheart.sheet.dh-style, - .themed.theme-dark.application.daggerheart.sheet.dh-style, + .themed.theme-dark .application.daggerheart.dh-style, + .themed.theme-dark.application.daggerheart.dh-style, body.theme-dark .application.daggerheart, body.theme-dark.application.daggerheart { @darkRules(); } // Light theme selectors - .themed.theme-light .application.daggerheart.sheet.dh-style, - .themed.theme-light.application.daggerheart.sheet.dh-style, + .themed.theme-light .application.daggerheart.dh-style, + .themed.theme-light.application.daggerheart.dh-style, body.theme-light .application.daggerheart, body.theme-light.application.daggerheart { @lightRules(); @@ -50,14 +50,15 @@ */ .dh-typography() { h1 { + --dh-input-color-text: @color-text-emphatic; font-family: @font-title; margin: 0; border: none; font-weight: normal; - } - - h1 input[type='text'] { - font-family: @font-title; + input[type='text'], + .input[contenteditable] { + font-family: @font-title; + } } h2, @@ -114,3 +115,92 @@ font-family: @font-body; } } + + +// A simple ease-out mask +.smooth-gradient-ease-out(@param, @to, @destination, @length) { + @{param}+: linear-gradient( + @to, + transparent 0%, + rgb(from @destination r g b / ~"calc(alpha * 0.013)") calc(0.008 * @length), + rgb(from @destination r g b / ~"calc(alpha * 0.049)") calc(0.029 * @length), + rgb(from @destination r g b / ~"calc(alpha * 0.104)") calc(0.064 * @length), + rgb(from @destination r g b / ~"calc(alpha * 0.259)") calc(0.166 * @length), + rgb(from @destination r g b / ~"calc(alpha * 0.45)") calc(0.304 * @length), + rgb(from @destination r g b / ~"calc(alpha * 0.741)") calc(0.554 * @length), + rgb(from @destination r g b / ~"calc(alpha * 0.825)") calc(0.644 * @length), + rgb(from @destination r g b / ~"calc(alpha * 0.896)") calc(0.735 * @length), + rgb(from @destination r g b / ~"calc(alpha * 0.951)") calc(0.825 * @length), + rgb(from @destination r g b / ~"calc(alpha * 0.987)") calc(0.914 * @length), + @destination @length + ); +} + +/** + * A simple ease in and out mask. + * @param - the parameter to add to + * @param - direction, such as "to top" + * @destination - the color at the destination. The origin is always transparent + * @length - the value at the destination + */ +.smooth-gradient-ease-in-out(@param, @to, @destination, @length: 100%) { + @{param}+: linear-gradient( + @to, + transparent 0%, + rgb(from @destination r g b / ~"calc(alpha * 0.013)") calc(0.081 * @length), + rgb(from @destination r g b / ~"calc(alpha * 0.049)") calc(0.155 * @length), + rgb(from @destination r g b / ~"calc(alpha * 0.104)") calc(0.225 * @length), + rgb(from @destination r g b / ~"calc(alpha * 0.259)") calc(0.353 * @length), + rgb(from @destination r g b / ~"calc(alpha * 0.45)") calc(0.471 * @length), + rgb(from @destination r g b / ~"calc(alpha * 0.741)") calc(0.647 * @length), + rgb(from @destination r g b / ~"calc(alpha * 0.825)") calc(0.71 * @length), + rgb(from @destination r g b / ~"calc(alpha * 0.896)") calc(0.775 * @length), + rgb(from @destination r g b / ~"calc(alpha * 0.951)") calc(0.845 * @length), + rgb(from @destination r g b / ~"calc(alpha * 0.987)") calc(0.914 * @length), + @destination @length + ); +} + +// Scroll shadows, but only if the browser supports. At the time of writing, this doesn't work on firefox +@supports ((animation-timeline: scroll()) and (animation-range: 0% 100%)) { + @property --fade-start { + syntax: ""; + inherits: false; + initial-value: 0; + } + + @property --fade-end { + syntax: ""; + inherits: false; + initial-value: 0; + } + + @keyframes scrollfade { + 0% { + --fade-start: 0; + } + 10%, 100% { + --fade-start: 12px; + } + 0%, 90% { + --fade-end: 12px; + } + 100% { + --fade-end: 0; + } + } +} + +.with-scroll-shadows() { + animation: scrollfade; + animation-timeline: --scrollfade; + animation-range: entry 0% exit 100%; + scroll-timeline: --scrollfade y; + mask-image: linear-gradient( + 0deg, + transparent 0%, + black var(--fade-end), + black calc(100% - var(--fade-start)), + transparent 100% + ); +} diff --git a/styles/less/ux/autocomplete/autocomplete.less b/styles/less/ux/autocomplete/autocomplete.less index 7f799449..e778f0da 100644 --- a/styles/less/ux/autocomplete/autocomplete.less +++ b/styles/less/ux/autocomplete/autocomplete.less @@ -21,8 +21,6 @@ flex-direction: column; gap: 2px; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; - .group { font-weight: bold; font-size: var(--font-size-14); diff --git a/styles/less/ux/tooltip/armorManagement.less b/styles/less/ux/tooltip/armorManagement.less index e1ac6bb9..497df4f5 100644 --- a/styles/less/ux/tooltip/armorManagement.less +++ b/styles/less/ux/tooltip/armorManagement.less @@ -83,7 +83,7 @@ appearance: none; width: 100%; height: 30px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; z-index: 1; background: @dark-blue; @@ -118,13 +118,13 @@ flex-wrap: wrap; gap: 4px; padding: 5px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; z-index: 1; background: @dark-blue; align-items: center; justify-content: center; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; min-height: 30px; width: 100%; diff --git a/styles/less/ux/tooltip/resource-management.less b/styles/less/ux/tooltip/resource-management.less index ff1f4dd2..6e7e7851 100644 --- a/styles/less/ux/tooltip/resource-management.less +++ b/styles/less/ux/tooltip/resource-management.less @@ -9,9 +9,9 @@ display: flex; gap: 10px; background-color: light-dark(transparent, @dark-blue); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; padding: 5px 10px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; align-items: center; width: fit-content; @@ -22,7 +22,7 @@ font-size: var(--font-size-14); font-weight: bold; text-transform: uppercase; - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; margin: 0; } diff --git a/styles/less/ux/tooltip/sheet.less b/styles/less/ux/tooltip/sheet.less index 59e4e638..cc4166da 100644 --- a/styles/less/ux/tooltip/sheet.less +++ b/styles/less/ux/tooltip/sheet.less @@ -3,7 +3,7 @@ aside[role='tooltip']:has(div.daggerheart.dh-style.tooltip), #tooltip.bordered-tooltip { .tooltip-title { font-size: var(--font-size-20); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; font-weight: 700; } @@ -48,9 +48,6 @@ aside[role='tooltip']:has(div.daggerheart.dh-style.tooltip), overflow-y: auto; position: relative; - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; - .tooltip-tag { display: flex; gap: 10px; diff --git a/styles/less/ux/tooltip/tooltip.less b/styles/less/ux/tooltip/tooltip.less index 1566059f..f02499e2 100644 --- a/styles/less/ux/tooltip/tooltip.less +++ b/styles/less/ux/tooltip/tooltip.less @@ -132,7 +132,7 @@ aside[role='tooltip']:has(div.daggerheart.dh-style.tooltip.card-style) { border-radius: 3px; padding: 3px; background: light-dark(@dark-blue-60, @rustic-brown-80); - color: light-dark(@dark-blue, @golden); + color: @color-text-emphatic; font-size: 12px; margin-bottom: 10px; } @@ -238,7 +238,7 @@ aside[role='tooltip'].locked-tooltip:has(div.daggerheart.dh-style.tooltip.card-s .tooltip-chip { font-size: var(--font-size-18); padding: 2px 4px; - border: 1px solid light-dark(@dark-blue, @golden); + border: 1px solid @color-border; border-radius: 6px; color: light-dark(@dark, @beige); background-image: url(../assets/parchments/dh-parchment-dark.png); diff --git a/system.json b/system.json index babdde26..ed14a17b 100644 --- a/system.json +++ b/system.json @@ -2,19 +2,24 @@ "id": "daggerheart", "title": "Daggerheart", "description": "An unofficial implementation of the Daggerheart system", - "version": "2.1.2", + "version": "2.3.2", "compatibility": { - "minimum": "14.359", - "verified": "14.360", + "minimum": "14.361", + "verified": "14.363", "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.1.2/system.zip", + "download": "https://github.com/Foundryborne/daggerheart/releases/download/2.3.2/system.zip", "authors": [ { "name": "WBHarry" }, + { + "name": "Supe", + "url": "https://github.com/CarlosFdez", + "discord": "supe" + }, { "name": "cptn-cosmo", "url": "https://github.com/cptn-cosmo", @@ -239,11 +244,14 @@ "adversary": { "htmlFields": ["notes", "description"] }, + "npc": { + "htmlFields": ["notes"] + }, "environment": { "htmlFields": ["notes", "description"] }, "party": { - "htmlFields": ["notes"] + "htmlFields": ["notes", "description"] } }, "Item": { @@ -298,5 +306,11 @@ }, "background": "systems/daggerheart/assets/logos/FoundrybornBackgroundLogo.png", "primaryTokenAttribute": "resources.hitPoints", - "secondaryTokenAttribute": "resources.stress" + "secondaryTokenAttribute": "resources.stress", + "flags": { + "hotReload": { + "extensions": ["css", "hbs", "json"], + "paths": ["styles/daggerheart.css", "templates", "lang"] + } + } } diff --git a/templates/actionTypes/damage.hbs b/templates/actionTypes/damage.hbs index 454d0413..03300840 100644 --- a/templates/actionTypes/damage.hbs +++ b/templates/actionTypes/damage.hbs @@ -31,44 +31,40 @@ {{/unless}} - {{#unless (and @root.source.damage.includeBase (eq key 'hitPoints'))}} - {{#if (and (not @root.isNPC) @root.hasRoll (not dmg.base))}} - {{formField ../fields.resultBased value=dmg.resultBased name=(concat "damage.parts." dmg.applyTo ".resultBased") localize=true classes="checkbox"}} - {{/if}} - {{#if (and (not @root.isNPC) @root.hasRoll (not dmg.base) dmg.resultBased)}} -
-
- {{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.hope")}} - {{> formula fields=../fields.value.fields type=../fields.type dmg=dmg source=dmg.value target="value" key=dmg.applyTo path=../path}} -
-
- {{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.fear")}} - {{> formula fields=../fields.valueAlt.fields type=../fields.type dmg=dmg source=dmg.valueAlt target="valueAlt" key=dmg.applyTo path=../path}} -
-
- {{else}} - {{> formula fields=../fields.value.fields type=../fields.type dmg=dmg source=dmg.value target="value" key=dmg.applyTo path=../path}} - {{/if}} - - {{#if (and (eq dmg.applyTo 'hitPoints') (ne @root.source.type 'healing'))}} - {{formField ../fields.type value=dmg.type name=(concat ../path "damage.parts." dmg.applyTo ".type") localize=true}} - {{/if}} - - {{#if ../horde}} + {{#if (and (not @root.isNPC) @root.hasRoll (not dmg.base))}} + {{formField ../fields.resultBased value=dmg.resultBased name=(concat "damage.parts." dmg.applyTo ".resultBased") localize=true classes="checkbox"}} + {{/if}} + {{#if (and (not @root.isNPC) @root.hasRoll (not dmg.base) dmg.resultBased)}} +
- {{localize "DAGGERHEART.ACTORS.Adversary.hordeDamage"}} -
- - {{formField ../fields.valueAlt.fields.flatMultiplier value=dmg.valueAlt.flatMultiplier name=(concat ../path "damage.parts." dmg.applyTo ".valueAlt.flatMultiplier") label="DAGGERHEART.ACTIONS.Settings.multiplier" classes="inline-child" localize=true }} - {{formField ../fields.valueAlt.fields.dice value=dmg.valueAlt.dice name=(concat ../path "damage.parts." dmg.applyTo ".valueAlt.dice") classes="inline-child" localize=true}} - {{formField ../fields.valueAlt.fields.bonus value=dmg.valueAlt.bonus name=(concat ../path "damage.parts." dmg.applyTo ".valueAlt.bonus") localize=true classes="inline-child"}} -
+ {{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.hope")}} + {{> formula fields=../fields.value.fields type=../fields.type dmg=dmg source=dmg.value target="value" key=dmg.applyTo path=../path}}
- {{/if}} - +
+ {{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.fear")}} + {{> formula fields=../fields.valueAlt.fields type=../fields.type dmg=dmg source=dmg.valueAlt target="valueAlt" key=dmg.applyTo path=../path}} +
+
{{else}} - {{localize "DAGGERHEART.ACTIONS.Config.itemDamageIsUsed"}} - {{/unless}} + {{> formula fields=../fields.value.fields type=../fields.type dmg=dmg source=dmg.value target="value" key=dmg.applyTo path=../path}} + {{/if}} + + {{#if (and (eq dmg.applyTo 'hitPoints') (ne @root.source.type 'healing'))}} + {{formField ../fields.type value=dmg.type name=(concat ../path "damage.parts." dmg.applyTo ".type") localize=true}} + {{/if}} + + {{#if ../horde}} +
+ {{localize "DAGGERHEART.ACTORS.Adversary.hordeDamage"}} +
+ + {{formField ../fields.valueAlt.fields.flatMultiplier value=dmg.valueAlt.flatMultiplier name=(concat ../path "damage.parts." dmg.applyTo ".valueAlt.flatMultiplier") label="DAGGERHEART.ACTIONS.Settings.multiplier" classes="inline-child" localize=true }} + {{formField ../fields.valueAlt.fields.dice value=dmg.valueAlt.dice name=(concat ../path "damage.parts." dmg.applyTo ".valueAlt.dice") classes="inline-child" localize=true}} + {{formField ../fields.valueAlt.fields.bonus value=dmg.valueAlt.bonus name=(concat ../path "damage.parts." dmg.applyTo ".valueAlt.bonus") localize=true classes="inline-child"}} +
+
+ {{/if}} + {{/each}} diff --git a/templates/dialogs/compendiumBrowserSettingsDialog/packs.hbs b/templates/dialogs/compendiumBrowserSettingsDialog/packs.hbs index dcda8108..ca2739b2 100644 --- a/templates/dialogs/compendiumBrowserSettingsDialog/packs.hbs +++ b/templates/dialogs/compendiumBrowserSettingsDialog/packs.hbs @@ -22,7 +22,7 @@
{{#each source.packs as |pack|}}
- +
{{/each}} diff --git a/templates/dialogs/group-roll/group-roll.hbs b/templates/dialogs/group-roll/group-roll.hbs deleted file mode 100644 index 9b23c0a5..00000000 --- a/templates/dialogs/group-roll/group-roll.hbs +++ /dev/null @@ -1,84 +0,0 @@ -
-
-

{{localize "DAGGERHEART.UI.Chat.groupRoll.title"}}

-
- -
- {{localize "DAGGERHEART.UI.Chat.groupRoll.leader"}} - {{#unless leader.actor}} - -
- {{localize "DAGGERHEART.UI.Chat.groupRoll.selectLeader"}} -
- {{else}} -
- {{leader.actor.name}} -
- {{leader.actor.name}} -
-
- - -
- {{!-- Not used yet --}} - {{!--
- - -
--}} -
-
-
- - - -
-
- {{/unless}} -
- -
- {{localize "DAGGERHEART.UI.Chat.groupRoll.partyTeam"}} - - - - {{#if (gt this.members.length 0)}} - {{#each members as |member index|}} -
- {{member.actor.name}} -
- {{member.actor.name}} -
-
- - -
- {{!-- Not used yet --}} - {{!--
- - -
--}} -
-
-
- - - -
-
- {{/each}} - {{/if}} - {{#unless allSelected}} -
- {{localize "DAGGERHEART.UI.Chat.groupRoll.selectMember"}} -
- {{/unless}} -
- -
\ No newline at end of file diff --git a/templates/dialogs/groupRollDialog/groupRoll.hbs b/templates/dialogs/groupRollDialog/groupRoll.hbs deleted file mode 100644 index edf1c980..00000000 --- a/templates/dialogs/groupRollDialog/groupRoll.hbs +++ /dev/null @@ -1,20 +0,0 @@ -
-
- {{localize "DAGGERHEART.GENERAL.result.single"}} - -
- {{#if hasRolled}}{{groupRoll.total}} {{groupRoll.totalLabel}}{{/if}} -
- {{#if groupRoll.leaderTotal includeZero=true}}{{groupRoll.leaderTotal}}{{else}}{{localize "DAGGERHEART.APPLICATIONS.GroupRollSelect.leaderRoll"}}{{/if}} - {{#each groupRoll.modifiers as |modifier|}} - {{#if (gte modifier 0)}}+{{else}}-{{/if}} - {{positive modifier}} - {{/each}} - {{#unless groupRoll.modifiers.length}} - + - {{localize "DAGGERHEART.GENERAL.Modifier.plural"}} - {{/unless}} -
-
-
-
\ No newline at end of file diff --git a/templates/dialogs/groupRollDialog/groupRollMember.hbs b/templates/dialogs/groupRollDialog/groupRollMember.hbs deleted file mode 100644 index acf8e8f1..00000000 --- a/templates/dialogs/groupRollDialog/groupRollMember.hbs +++ /dev/null @@ -1,85 +0,0 @@ -{{#with (lookup members partId)}} -
-
-
- -
- {{name}} -
-
-
- {{!-- --}} - -
-
-
-
-
- {{#if readyToRoll}} -
- - {{localize "DAGGERHEART.GENERAL.roll"}} -
- - - - - {{#if hasRolled}} - - - - {{/if}} -
-
- - {{#if roll}} -
-
{{roll.total}} {{localize "DAGGERHEART.GENERAL.withThing" thing=roll.totalLabel}}
-
- - {{roll.dHope.total}} - - - + - - {{roll.dFear.total}} - - - {{#if roll.advantage.type}} - {{#if (eq roll.advantage.type 1)}}+{{else}}-{{/if}} - - {{roll.advantage.value}} - - - {{/if}} - {{#if (gte roll.modifierTotal 0)}}+{{else}}-{{/if}} - {{positive roll.modifierTotal}} -
-
- {{else}} - {{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.makeYourRoll"}} - {{/if}} -
- {{/if}} - {{#if hasRolled}} -
- {{#if ../isGM}} - - {{/if}} -
- {{localize "DAGGERHEART.GENERAL.Modifier.single"}}{{#if successfull}} + 1{{else if (isNullish successfull)}} + ?{{else}} - 1{{/if}} -
-
- {{/if}} -
-
-{{/with}} \ No newline at end of file diff --git a/templates/dialogs/groupRollDialog/initialization.hbs b/templates/dialogs/groupRollDialog/initialization.hbs index a520b8bd..a01a00bb 100644 --- a/templates/dialogs/groupRollDialog/initialization.hbs +++ b/templates/dialogs/groupRollDialog/initialization.hbs @@ -1,32 +1,43 @@
+

{{localize "DAGGERHEART.APPLICATIONS.GroupRollSelect.initializationTitle"}}

+
{{localize "DAGGERHEART.APPLICATIONS.GroupRollSelect.leader"}}
+
+
{{localize "DAGGERHEART.APPLICATIONS.GroupRollSelect.selectLeaderHint"}}
+ +
+ +
{{localize "DAGGERHEART.APPLICATIONS.GroupRollSelect.members"}}
+
{{"DAGGERHEART.APPLICATIONS.GroupRollSelect.selectParticipantsHint"}}
{{#each memberSelection as |member|}} - - {{member.name}} - - + + + {{member.name}} + {{#if (eq @root.selectedLeader.memberId member.id)}} + + {{/if}} + + + {{/each}}
-
-
- -
- -
-
-
-
-
{{localize "DAGGERHEART.APPLICATIONS.GroupRollSelect.openDialogForAll"}}
+
\ No newline at end of file diff --git a/templates/dialogs/groupRollDialog/leader.hbs b/templates/dialogs/groupRollDialog/leader.hbs deleted file mode 100644 index 3d5db3f7..00000000 --- a/templates/dialogs/groupRollDialog/leader.hbs +++ /dev/null @@ -1,73 +0,0 @@ -
- {{#with leader}} -
-
{{localize "DAGGERHEART.APPLICATIONS.GroupRollSelect.leader"}}
-
-
-
- -
- {{name}} -
-
-
- -
-
-
-
-
-
- - {{#if readyToRoll}} -
- - {{localize "DAGGERHEART.GENERAL.roll"}} -
- - - - - {{#if hasRolled}} - - - - {{/if}} -
-
- - {{#if roll}} -
-
{{roll.total}} {{localize "DAGGERHEART.GENERAL.withThing" thing=roll.totalLabel}}
-
- - {{roll.dHope.total}} - - - + - - {{roll.dFear.total}} - - - {{#if roll.advantage.type}} - {{#if (eq roll.advantage.type 1)}}+{{else}}-{{/if}} - - {{roll.advantage.value}} - - - {{/if}} - {{#if (gte roll.modifierTotal 0)}}+{{else}}-{{/if}} - {{positive roll.modifierTotal}} -
-
- {{else}} - {{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.makeYourRoll"}} - {{/if}} -
- {{/if}} -
-
- {{/with}} -
\ No newline at end of file diff --git a/templates/dialogs/groupRollDialog/main.hbs b/templates/dialogs/groupRollDialog/main.hbs new file mode 100644 index 00000000..6807a7e4 --- /dev/null +++ b/templates/dialogs/groupRollDialog/main.hbs @@ -0,0 +1,15 @@ +
+

{{localize "DAGGERHEART.APPLICATIONS.GroupRollSelect.title"}}

+
{{localize "DAGGERHEART.APPLICATIONS.GroupRollSelect.members"}}
+
+ {{#each aidKeys as |key|}} +
+ {{/each}} +
+ +
{{localize "DAGGERHEART.APPLICATIONS.GroupRollSelect.leader"}}
+
+
{{localize "DAGGERHEART.GENERAL.result.single"}}
+
+
+
\ No newline at end of file diff --git a/templates/dialogs/groupRollDialog/footer.hbs b/templates/dialogs/groupRollDialog/parts/footer.hbs similarity index 62% rename from templates/dialogs/groupRollDialog/footer.hbs rename to templates/dialogs/groupRollDialog/parts/footer.hbs index e401966b..34b4efa1 100644 --- a/templates/dialogs/groupRollDialog/footer.hbs +++ b/templates/dialogs/groupRollDialog/parts/footer.hbs @@ -1,6 +1,9 @@
- +
\ No newline at end of file diff --git a/templates/dialogs/groupRollDialog/parts/member.hbs b/templates/dialogs/groupRollDialog/parts/member.hbs new file mode 100644 index 00000000..ee857794 --- /dev/null +++ b/templates/dialogs/groupRollDialog/parts/member.hbs @@ -0,0 +1,95 @@ +{{#with (ifThen (eq partId "leader") leader (lookup members partId))}} +
+ +
+ {{name}} + {{#if hasRolled}} +
+
{{rollChoiceLabel}}
+ {{#if modifier}} + + {{numberFormat modifier sign=true}} + + {{/if}} + {{#if isEditable}} +
+ + +
+ {{/if}} +
+ {{else if readyToRoll}} +
+ {{localize "DAGGERHEART.CONFIG.RollTypes.trait.name" }} + +
+ {{/if}} +
+ {{#if roll}} +
+
+
+ {{roll.total}} + {{withLabelShort}} +
+
+ + {{roll.dHope.total}} + + + + {{roll.dFear.total}} + + + {{#if roll.hasAdvantage}} + + + + {{roll.dAdvantage.total}} + + + {{/if}} + {{#if roll.hasDisadvantage}} + - + + {{roll.dDisadvantage.total}} + + + {{/if}} + {{#if (gte roll.modifierTotal 0)}}+{{else}}-{{/if}} + {{positive roll.modifierTotal}} +
+
+ {{#if (and @root.isGM (ne ../partId "leader"))}} +
+ + +
+ {{/if}} +
+ {{else if (and readyToRoll isEditable)}} + + 2d12 + + {{/if}} +
+{{/with}} \ No newline at end of file diff --git a/templates/dialogs/groupRollDialog/parts/result.hbs b/templates/dialogs/groupRollDialog/parts/result.hbs new file mode 100644 index 00000000..d90dfb5c --- /dev/null +++ b/templates/dialogs/groupRollDialog/parts/result.hbs @@ -0,0 +1,30 @@ +{{#if (and hasRolled leader.roll)}} +
+
+ {{localize "DAGGERHEART.APPLICATIONS.GroupRollSelect.leaderRoll"}} + + {{leader.roll.total}} + {{localize "DAGGERHEART.GENERAL.withThing" thing=leader.roll.totalLabel}} + +
+
+ {{localize "DAGGERHEART.GENERAL.Modifier.plural"}} +
+ {{#each groupRoll.modifiers as |modifier|}} + + {{numberFormat modifier sign=true}} + + {{/each}} +
+
+
+
+ {{localize "DAGGERHEART.GENERAL.total"}} + {{groupRoll.total}} {{groupRoll.totalLabel}} +
+
+{{else}} +
+ {{localize "DAGGERHEART.APPLICATIONS.GroupRollSelect.resultsHint"}} +
+{{/if}} \ No newline at end of file diff --git a/templates/dialogs/rerollDialog/damage/main.hbs b/templates/dialogs/rerollDialog/damage/main.hbs deleted file mode 100644 index 5b994bf6..00000000 --- a/templates/dialogs/rerollDialog/damage/main.hbs +++ /dev/null @@ -1,35 +0,0 @@ -
- {{#each damage}} -

{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' @key '.name')}}

- {{#each this}} -
- {{#each this}} -
- - - - {{this.selectedResults}}/{{this.maxSelected}} Selected - - -
- {{#each this.results}} -
- {{this.result}} - {{#if this.active}} - - - - - {{/if}} -
- {{/each}} -
-
- {{/each}} -
- {{/each}} - {{/each}} -
\ No newline at end of file diff --git a/templates/dialogs/rerollDialog/footer.hbs b/templates/dialogs/rerollDialog/footer.hbs deleted file mode 100644 index 5d4ae2b2..00000000 --- a/templates/dialogs/rerollDialog/footer.hbs +++ /dev/null @@ -1,4 +0,0 @@ -
- - -
\ No newline at end of file diff --git a/templates/dialogs/rerollDialog/main.hbs b/templates/dialogs/rerollDialog/main.hbs deleted file mode 100644 index 6f10ce33..00000000 --- a/templates/dialogs/rerollDialog/main.hbs +++ /dev/null @@ -1,35 +0,0 @@ -
- {{#each damage}} -

{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' @key '.name')}}

- {{#each this}} -
- {{#each this}} -
- - - - {{this.selectedResults}}/{{this.results.length}} Selected - - -
- {{#each this.results}} -
- {{this.result}} - {{#if this.active}} - - - - - {{/if}} -
- {{/each}} -
-
- {{/each}} -
- {{/each}} - {{/each}} -
\ No newline at end of file diff --git a/templates/dialogs/tagTeamDialog/initialization.hbs b/templates/dialogs/tagTeamDialog/initialization.hbs index 7ccdf566..0b92e68e 100644 --- a/templates/dialogs/tagTeamDialog/initialization.hbs +++ b/templates/dialogs/tagTeamDialog/initialization.hbs @@ -1,13 +1,18 @@
-

{{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.selectParticipants"}}

+

{{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.selectParticipants"}}

@@ -30,10 +35,13 @@
-
{{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.openDialogForAll"}}
+
\ No newline at end of file diff --git a/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs b/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs index 49fc8f4f..2a366269 100644 --- a/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs +++ b/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs @@ -16,7 +16,7 @@ {{/unless}} {{/each}} {{#if part.modifierTotal}} - {{#if (gte part.modifierTotal 0)}}+{{else}}-{{/if}} + {{#if part.dice.length}}{{#if (gte part.modifierTotal 0)}}+{{else}}-{{/if}}{{/if}} {{positive part.modifierTotal}} {{/if}} diff --git a/templates/dialogs/tagTeamDialog/result.hbs b/templates/dialogs/tagTeamDialog/result.hbs new file mode 100644 index 00000000..151c8c87 --- /dev/null +++ b/templates/dialogs/tagTeamDialog/result.hbs @@ -0,0 +1,41 @@ +
+
+
+ {{localize "DAGGERHEART.GENERAL.result.plural"}} +
+ {{#if hintText}} +
{{localize hintText}}
+ {{else}} + {{#if joinedRoll.roll}} +
+ +
+
{{joinedRoll.roll.total}}
+
{{localize "DAGGERHEART.GENERAL.withThing" thing=joinedRoll.roll.totalLabel}}
+
+
+ {{/if}} + {{#if joinedRoll.rollData.options.hasDamage}} +
+ + {{#each joinedRoll.rollData.options.damage as |damage key|}} +
+
{{localize (concat "DAGGERHEART.CONFIG.HealingType." key ".name")}}
+
{{damage.total}}
+
+ {{/each}} +
+ {{/if}} + {{/if}} +
+
+ +
+ + +
+
+
\ No newline at end of file diff --git a/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs b/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs index 79b5138e..1a4b974b 100644 --- a/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs +++ b/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs @@ -1,38 +1,9 @@ -
-
-
- {{localize "DAGGERHEART.GENERAL.result.plural"}} -
- {{#if hintText}} -
{{localize hintText}}
- {{else}} - {{#if joinedRoll.roll}} -
- -
-
{{joinedRoll.roll.total}}
-
{{localize "DAGGERHEART.GENERAL.withThing" thing=joinedRoll.roll.totalLabel}}
-
-
- {{/if}} - {{#if joinedRoll.rollData.options.hasDamage}} -
- - {{#each joinedRoll.rollData.options.damage as |damage key|}} -
-
{{localize (concat "DAGGERHEART.CONFIG.HealingType." key ".name")}}
-
{{damage.total}}
-
- {{/each}} -
- {{/if}} - {{/if}} -
-
- -
- - -
+
+
+ {{#each memberKeys as |key|}} +
+ {{/each}}
-
\ No newline at end of file +
+
+ diff --git a/templates/hud/tokenHUD.hbs b/templates/hud/tokenHUD.hbs index ab0872bf..cf43e15d 100644 --- a/templates/hud/tokenHUD.hbs +++ b/templates/hud/tokenHUD.hbs @@ -4,10 +4,6 @@ - - {{#if canChangeLevel}} + {{#if hasCompanion}} {{/if}} + {{#if isGM}} + + {{/if}} + {{#if canConfigure}} - {{/if}} @@ -49,8 +56,9 @@
{{#if isGM}} - {{/if}} @@ -119,13 +127,15 @@ {{/each}}
- {{#if canToggleCombat}} - {{/if}} diff --git a/templates/levelup/tabs/selections.hbs b/templates/levelup/tabs/selections.hbs index 7ebe32bb..deb7f6c0 100644 --- a/templates/levelup/tabs/selections.hbs +++ b/templates/levelup/tabs/selections.hbs @@ -43,45 +43,6 @@ {{/if}} - {{#if (gt this.domainCards.length 0)}} -
-
- -

{{localize "DAGGERHEART.APPLICATIONS.Levelup.summary.domainCards"}}

- -
-
- -
- -
- {{#each this.domainCards}} - {{#> "systems/daggerheart/templates/components/card-preview.hbs" this }} - {{#each this.emptySubtexts}} -
{{this}}
- {{/each}} - {{/"systems/daggerheart/templates/components/card-preview.hbs"}} - {{/each}} -
-
- {{/if}} - - {{#if (gt this.subclassCards.length 0)}} -
-
- -

{{localize "DAGGERHEART.APPLICATIONS.Levelup.summary.subclass"}}

- -
- -
- {{#each this.subclassCards}} - {{> "systems/daggerheart/templates/levelup/parts/selectable-card-preview.hbs" img=this.img header=this.featureLabel name=this.name path=this.path selected=this.selected uuid=this.uuid isMulticlass=this.isMulticlass featureState=this.featureState disabled=this.disabled }} - {{/each}} -
-
- {{/if}} - {{#if this.multiclass}}
@@ -128,6 +89,45 @@
{{/if}} + {{#if (gt this.domainCards.length 0)}} +
+
+ +

{{localize "DAGGERHEART.APPLICATIONS.Levelup.summary.domainCards"}}

+ +
+
+ +
+ +
+ {{#each this.domainCards}} + {{#> "systems/daggerheart/templates/components/card-preview.hbs" this }} + {{#each this.emptySubtexts}} +
{{this}}
+ {{/each}} + {{/"systems/daggerheart/templates/components/card-preview.hbs"}} + {{/each}} +
+
+ {{/if}} + + {{#if (gt this.subclassCards.length 0)}} +
+
+ +

{{localize "DAGGERHEART.APPLICATIONS.Levelup.summary.subclass"}}

+ +
+ +
+ {{#each this.subclassCards}} + {{> "systems/daggerheart/templates/levelup/parts/selectable-card-preview.hbs" img=this.img header=this.featureLabel name=this.name path=this.path selected=this.selected uuid=this.uuid isMulticlass=this.isMulticlass featureState=this.featureState disabled=this.disabled }} + {{/each}} +
+
+ {{/if}} + {{#if this.vicious}}

{{localize "DAGGERHEART.APPLICATIONS.Levelup.summary.vicious"}}

diff --git a/templates/settings/metagaming-settings/general.hbs b/templates/settings/metagaming-settings/general.hbs index 7f36715e..9f5bd514 100644 --- a/templates/settings/metagaming-settings/general.hbs +++ b/templates/settings/metagaming-settings/general.hbs @@ -1,3 +1,4 @@ -
+
{{formGroup settingFields.schema.fields.hideObserverPermissionInChat value=settingFields._source.hideObserverPermissionInChat localize=true}} -
\ No newline at end of file + {{formGroup settingFields.schema.fields.hidePartyStats value=settingFields._source.hidePartyStats localize=true}} +
diff --git a/templates/settings/variant-rules.hbs b/templates/settings/variant-rules.hbs index cdaef024..9c9650dd 100644 --- a/templates/settings/variant-rules.hbs +++ b/templates/settings/variant-rules.hbs @@ -32,7 +32,7 @@