mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-04-21 23:13:39 +02:00
Merged with v14-Dev
This commit is contained in:
commit
6bf0fffcb7
735 changed files with 9587 additions and 6016 deletions
|
|
@ -3,6 +3,7 @@ import * as applications from './module/applications/_module.mjs';
|
||||||
import * as data from './module/data/_module.mjs';
|
import * as data from './module/data/_module.mjs';
|
||||||
import * as models from './module/data/_module.mjs';
|
import * as models from './module/data/_module.mjs';
|
||||||
import * as documents from './module/documents/_module.mjs';
|
import * as documents from './module/documents/_module.mjs';
|
||||||
|
import { macros } from './module/_module.mjs';
|
||||||
import * as collections from './module/documents/collections/_module.mjs';
|
import * as collections from './module/documents/collections/_module.mjs';
|
||||||
import * as dice from './module/dice/_module.mjs';
|
import * as dice from './module/dice/_module.mjs';
|
||||||
import * as fields from './module/data/fields/_module.mjs';
|
import * as fields from './module/data/fields/_module.mjs';
|
||||||
|
|
@ -20,7 +21,6 @@ import {
|
||||||
} from './module/systemRegistration/_module.mjs';
|
} from './module/systemRegistration/_module.mjs';
|
||||||
import { placeables, DhTokenLayer } from './module/canvas/_module.mjs';
|
import { placeables, DhTokenLayer } from './module/canvas/_module.mjs';
|
||||||
import './node_modules/@yaireo/tagify/dist/tagify.css';
|
import './node_modules/@yaireo/tagify/dist/tagify.css';
|
||||||
import TemplateManager from './module/documents/templateManager.mjs';
|
|
||||||
import TokenManager from './module/documents/tokenManager.mjs';
|
import TokenManager from './module/documents/tokenManager.mjs';
|
||||||
|
|
||||||
CONFIG.DH = SYSTEM;
|
CONFIG.DH = SYSTEM;
|
||||||
|
|
@ -44,6 +44,7 @@ CONFIG.Item.dataModels = models.items.config;
|
||||||
|
|
||||||
CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect;
|
CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect;
|
||||||
CONFIG.ActiveEffect.dataModels = models.activeEffects.config;
|
CONFIG.ActiveEffect.dataModels = models.activeEffects.config;
|
||||||
|
CONFIG.ActiveEffect.changeTypes = { ...CONFIG.ActiveEffect.changeTypes, ...models.activeEffects.changeEffects };
|
||||||
|
|
||||||
CONFIG.Combat.documentClass = documents.DhpCombat;
|
CONFIG.Combat.documentClass = documents.DhpCombat;
|
||||||
CONFIG.Combat.dataModels = { base: models.DhCombat };
|
CONFIG.Combat.dataModels = { base: models.DhCombat };
|
||||||
|
|
@ -55,11 +56,13 @@ CONFIG.ChatMessage.documentClass = documents.DhChatMessage;
|
||||||
CONFIG.ChatMessage.template = 'systems/daggerheart/templates/ui/chat/chat-message.hbs';
|
CONFIG.ChatMessage.template = 'systems/daggerheart/templates/ui/chat/chat-message.hbs';
|
||||||
|
|
||||||
CONFIG.Canvas.rulerClass = placeables.DhRuler;
|
CONFIG.Canvas.rulerClass = placeables.DhRuler;
|
||||||
CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer;
|
CONFIG.Canvas.layers.regions.layerClass = placeables.DhRegionLayer;
|
||||||
CONFIG.Canvas.layers.tokens.layerClass = DhTokenLayer;
|
CONFIG.Canvas.layers.tokens.layerClass = DhTokenLayer;
|
||||||
|
|
||||||
CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate;
|
CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate;
|
||||||
|
|
||||||
|
CONFIG.Region.objectClass = placeables.DhRegion;
|
||||||
|
|
||||||
CONFIG.RollTable.documentClass = documents.DhRollTable;
|
CONFIG.RollTable.documentClass = documents.DhRollTable;
|
||||||
CONFIG.RollTable.resultTemplate = 'systems/daggerheart/templates/ui/chat/table-result.hbs';
|
CONFIG.RollTable.resultTemplate = 'systems/daggerheart/templates/ui/chat/table-result.hbs';
|
||||||
|
|
||||||
|
|
@ -83,7 +86,6 @@ CONFIG.ui.resources = applications.ui.DhFearTracker;
|
||||||
CONFIG.ui.countdowns = applications.ui.DhCountdowns;
|
CONFIG.ui.countdowns = applications.ui.DhCountdowns;
|
||||||
CONFIG.ux.ContextMenu = applications.ux.DHContextMenu;
|
CONFIG.ux.ContextMenu = applications.ux.DHContextMenu;
|
||||||
CONFIG.ux.TooltipManager = documents.DhTooltipManager;
|
CONFIG.ux.TooltipManager = documents.DhTooltipManager;
|
||||||
CONFIG.ux.TemplateManager = new TemplateManager();
|
|
||||||
CONFIG.ux.TokenManager = new TokenManager();
|
CONFIG.ux.TokenManager = new TokenManager();
|
||||||
CONFIG.debug.triggers = false;
|
CONFIG.debug.triggers = false;
|
||||||
|
|
||||||
|
|
@ -93,6 +95,7 @@ Hooks.once('init', () => {
|
||||||
data,
|
data,
|
||||||
models,
|
models,
|
||||||
documents,
|
documents,
|
||||||
|
macros,
|
||||||
dice,
|
dice,
|
||||||
fields
|
fields
|
||||||
};
|
};
|
||||||
|
|
@ -211,6 +214,7 @@ Hooks.once('init', () => {
|
||||||
SYSTEM.id,
|
SYSTEM.id,
|
||||||
applications.sheetConfigs.ActiveEffectConfig,
|
applications.sheetConfigs.ActiveEffectConfig,
|
||||||
{
|
{
|
||||||
|
types: ['base', 'beastform', 'horde'],
|
||||||
makeDefault: true,
|
makeDefault: true,
|
||||||
label: sheetLabel('DOCUMENT.ActiveEffect')
|
label: sheetLabel('DOCUMENT.ActiveEffect')
|
||||||
}
|
}
|
||||||
|
|
@ -268,7 +272,6 @@ Hooks.on('setup', () => {
|
||||||
...damageThresholds,
|
...damageThresholds,
|
||||||
'proficiency',
|
'proficiency',
|
||||||
'evasion',
|
'evasion',
|
||||||
'armorScore',
|
|
||||||
'scars',
|
'scars',
|
||||||
'levelData.level.current'
|
'levelData.level.current'
|
||||||
]
|
]
|
||||||
|
|
@ -402,6 +405,17 @@ Hooks.on('chatMessage', (_, message) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Hooks.on(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, async data => {
|
||||||
|
if (data.openForAllPlayers && data.partyId) {
|
||||||
|
const party = game.actors.get(data.partyId);
|
||||||
|
if (!party) return;
|
||||||
|
|
||||||
|
const dialog = new game.system.api.applications.dialogs.TagTeamDialog(party);
|
||||||
|
dialog.tabGroups.application = 'tagTeamRoll';
|
||||||
|
await dialog.render({ force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const updateActorsRangeDependentEffects = async token => {
|
const updateActorsRangeDependentEffects = async token => {
|
||||||
const rangeMeasurement = game.settings.get(
|
const rangeMeasurement = game.settings.get(
|
||||||
CONFIG.DH.id,
|
CONFIG.DH.id,
|
||||||
|
|
|
||||||
98
lang/en.json
98
lang/en.json
|
|
@ -14,7 +14,9 @@
|
||||||
"beastform": "Beastform"
|
"beastform": "Beastform"
|
||||||
},
|
},
|
||||||
"ActiveEffect": {
|
"ActiveEffect": {
|
||||||
"beastform": "Beastform"
|
"base": "Standard",
|
||||||
|
"beastform": "Beastform",
|
||||||
|
"horde": "Horde"
|
||||||
},
|
},
|
||||||
"Actor": {
|
"Actor": {
|
||||||
"character": "Character",
|
"character": "Character",
|
||||||
|
|
@ -151,7 +153,8 @@
|
||||||
"Config": {
|
"Config": {
|
||||||
"rangeDependence": {
|
"rangeDependence": {
|
||||||
"title": "Range Dependence"
|
"title": "Range Dependence"
|
||||||
}
|
},
|
||||||
|
"stacking": { "title": "Stacking" }
|
||||||
},
|
},
|
||||||
"RangeDependance": {
|
"RangeDependance": {
|
||||||
"hint": "Settings for an optional distance at which this effect should activate",
|
"hint": "Settings for an optional distance at which this effect should activate",
|
||||||
|
|
@ -449,7 +452,7 @@
|
||||||
"text": "Are you sure you want to delete {name}?"
|
"text": "Are you sure you want to delete {name}?"
|
||||||
},
|
},
|
||||||
"DamageReduction": {
|
"DamageReduction": {
|
||||||
"armorMarks": "Armor Marks",
|
"maxUseableArmor": "Useable Armor Slots",
|
||||||
"armorWithStress": "Spend 1 stress to use an extra mark",
|
"armorWithStress": "Spend 1 stress to use an extra mark",
|
||||||
"thresholdImmunities": "Threshold Immunities",
|
"thresholdImmunities": "Threshold Immunities",
|
||||||
"stress": "Stress",
|
"stress": "Stress",
|
||||||
|
|
@ -675,16 +678,35 @@
|
||||||
},
|
},
|
||||||
"TagTeamSelect": {
|
"TagTeamSelect": {
|
||||||
"title": "Tag Team Roll",
|
"title": "Tag Team Roll",
|
||||||
|
"FIELDS": {
|
||||||
|
"initiator": {
|
||||||
|
"memberId": { "label": "Initiating Character" },
|
||||||
|
"cost": { "label": "Initiation Cost" }
|
||||||
|
}
|
||||||
|
},
|
||||||
"leaderTitle": "Initiating Character",
|
"leaderTitle": "Initiating Character",
|
||||||
"membersTitle": "Participants",
|
"membersTitle": "Participants",
|
||||||
"partyTeam": "Party Team",
|
"partyTeam": "Party Team",
|
||||||
"hopeCost": "Hope Cost",
|
"hopeCost": "Hope Cost",
|
||||||
"initiatingCharacter": "Initiating Character",
|
"initiatingCharacter": "Initiating Character",
|
||||||
|
"selectParticipants": "Select the two participants",
|
||||||
|
"startTagTeamRoll": "Start Tag Team Roll",
|
||||||
|
"openDialogForAll": "Open Dialog For All",
|
||||||
|
"rollType": "Roll Type",
|
||||||
|
"makeYourRoll": "Make your roll",
|
||||||
|
"cancelTagTeamRoll": "Cancel Tag Team Roll",
|
||||||
|
"finishTagTeamRoll": "Finish Tag Team Roll",
|
||||||
"linkMessageHint": "Make a roll from your character sheet to link it to the Tag Team Roll",
|
"linkMessageHint": "Make a roll from your character sheet to link it to the Tag Team Roll",
|
||||||
"damageNotRolled": "Damage not rolled in chat message yet",
|
"damageNotRolled": "Damage not rolled in chat message yet",
|
||||||
"insufficientHope": "The initiating character doesn't have enough hope",
|
"insufficientHope": "The initiating character doesn't have enough hope",
|
||||||
"createTagTeam": "Create TagTeam Roll",
|
"createTagTeam": "Create Tag Team Roll",
|
||||||
"chatMessageRollTitle": "Roll"
|
"chatMessageRollTitle": "Roll",
|
||||||
|
"cancelConfirmTitle": "Cancel Tag Team Roll",
|
||||||
|
"cancelConfirmText": "Are you sure you want to cancel the Tag Team Roll? This will close it for all other players too.",
|
||||||
|
"hints": {
|
||||||
|
"completeRolls": "Set up and complete the rolls for the characters",
|
||||||
|
"selectRoll": "Select which roll value to be used for the Tag Team"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"TokenConfig": {
|
"TokenConfig": {
|
||||||
"actorSizeUsed": "Actor size is set, determining the dimensions"
|
"actorSizeUsed": "Actor size is set, determining the dimensions"
|
||||||
|
|
@ -697,6 +719,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CONFIG": {
|
"CONFIG": {
|
||||||
|
"ActiveEffectDuration": {
|
||||||
|
"temporary": "Temporary",
|
||||||
|
"act": "Next Spotlight",
|
||||||
|
"scene": "Next Scene",
|
||||||
|
"shortRest": "Next Rest",
|
||||||
|
"longRest": "Next Long Rest",
|
||||||
|
"session": "Next Session",
|
||||||
|
"custom": "Custom"
|
||||||
|
},
|
||||||
"AdversaryTrait": {
|
"AdversaryTrait": {
|
||||||
"relentless": {
|
"relentless": {
|
||||||
"name": "Relentless",
|
"name": "Relentless",
|
||||||
|
|
@ -764,6 +795,11 @@
|
||||||
"bruiser": "for each Bruiser adversary.",
|
"bruiser": "for each Bruiser adversary.",
|
||||||
"solo": "for each Solo adversary."
|
"solo": "for each Solo adversary."
|
||||||
},
|
},
|
||||||
|
"ArmorInteraction": {
|
||||||
|
"none": { "label": "Ignores Armor" },
|
||||||
|
"active": { "label": "Active w/ Armor" },
|
||||||
|
"inactive": { "label": "Inactive w/ Armor" }
|
||||||
|
},
|
||||||
"ArmorFeature": {
|
"ArmorFeature": {
|
||||||
"burning": {
|
"burning": {
|
||||||
"name": "Burning",
|
"name": "Burning",
|
||||||
|
|
@ -1219,6 +1255,11 @@
|
||||||
"selectType": "Select Action Type",
|
"selectType": "Select Action Type",
|
||||||
"selectAction": "Action Selection"
|
"selectAction": "Action Selection"
|
||||||
},
|
},
|
||||||
|
"TagTeamRollTypes": {
|
||||||
|
"trait": "Trait",
|
||||||
|
"ability": "Ability",
|
||||||
|
"damageAbility": "Damage Ability"
|
||||||
|
},
|
||||||
"TargetTypes": {
|
"TargetTypes": {
|
||||||
"any": "Any",
|
"any": "Any",
|
||||||
"friendly": "Friendly",
|
"friendly": "Friendly",
|
||||||
|
|
@ -1231,8 +1272,8 @@
|
||||||
"cone": "Cone",
|
"cone": "Cone",
|
||||||
"emanation": "Emanation",
|
"emanation": "Emanation",
|
||||||
"inFront": "In Front",
|
"inFront": "In Front",
|
||||||
"rect": "Rectangle",
|
"rectangle": "Rectangle",
|
||||||
"ray": "Ray"
|
"line": "Line"
|
||||||
},
|
},
|
||||||
"TokenSize": {
|
"TokenSize": {
|
||||||
"tiny": "Tiny",
|
"tiny": "Tiny",
|
||||||
|
|
@ -1847,6 +1888,17 @@
|
||||||
"name": "Healing Roll"
|
"name": "Healing Roll"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ChangeTypes": {
|
||||||
|
"armor": {
|
||||||
|
"newArmorEffect": "Armor Effect",
|
||||||
|
"FIELDS": {
|
||||||
|
"interaction": {
|
||||||
|
"label": "Armor Interaction",
|
||||||
|
"hint": "Does the character wearing armor suppress this effect?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Duration": {
|
"Duration": {
|
||||||
"passive": "Passive",
|
"passive": "Passive",
|
||||||
"temporary": "Temporary"
|
"temporary": "Temporary"
|
||||||
|
|
@ -1871,6 +1923,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"GENERAL": {
|
"GENERAL": {
|
||||||
|
"Ability": {
|
||||||
|
"single": "Ability",
|
||||||
|
"plural": "Abilities"
|
||||||
|
},
|
||||||
"Action": {
|
"Action": {
|
||||||
"single": "Action",
|
"single": "Action",
|
||||||
"plural": "Actions"
|
"plural": "Actions"
|
||||||
|
|
@ -2252,6 +2308,7 @@
|
||||||
"duality": "Duality",
|
"duality": "Duality",
|
||||||
"dualityDice": "Duality Dice",
|
"dualityDice": "Duality Dice",
|
||||||
"dualityRoll": "Duality Roll",
|
"dualityRoll": "Duality Roll",
|
||||||
|
"effect": "Effect",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"evasion": "Evasion",
|
"evasion": "Evasion",
|
||||||
"equipment": "Equipment",
|
"equipment": "Equipment",
|
||||||
|
|
@ -2324,6 +2381,10 @@
|
||||||
"rerolled": "Rerolled",
|
"rerolled": "Rerolled",
|
||||||
"rerollThing": "Reroll {thing}",
|
"rerollThing": "Reroll {thing}",
|
||||||
"resource": "Resource",
|
"resource": "Resource",
|
||||||
|
"result": {
|
||||||
|
"single": "Result",
|
||||||
|
"plural": "Results"
|
||||||
|
},
|
||||||
"roll": "Roll",
|
"roll": "Roll",
|
||||||
"rollAll": "Roll All",
|
"rollAll": "Roll All",
|
||||||
"rollDamage": "Roll Damage",
|
"rollDamage": "Roll Damage",
|
||||||
|
|
@ -2486,6 +2547,14 @@
|
||||||
"secondaryWeapon": "Secondary Weapon"
|
"secondaryWeapon": "Secondary Weapon"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"MACROS": {
|
||||||
|
"Spotlight": {
|
||||||
|
"errors": {
|
||||||
|
"noActiveCombat": "There is no active encounter",
|
||||||
|
"noCombatantSelected": "A combatant token must be either selected or hovered to spotlight it"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"ROLLTABLES": {
|
"ROLLTABLES": {
|
||||||
"FIELDS": {
|
"FIELDS": {
|
||||||
"formulaName": { "label": "Formula Name" }
|
"formulaName": { "label": "Formula Name" }
|
||||||
|
|
@ -2557,6 +2626,10 @@
|
||||||
"hint": "Automatically increase the GM's fear pool on a fear duality roll result."
|
"hint": "Automatically increase the GM's fear pool on a fear duality roll result."
|
||||||
},
|
},
|
||||||
"FIELDS": {
|
"FIELDS": {
|
||||||
|
"autoExpireActiveEffects": {
|
||||||
|
"label": "Auto Expire Active Effects",
|
||||||
|
"hint": "Active Effects with set durations will automatically be removed when their durations are up"
|
||||||
|
},
|
||||||
"damageReductionRulesDefault": {
|
"damageReductionRulesDefault": {
|
||||||
"label": "Damage Reduction Rules Default",
|
"label": "Damage Reduction Rules Default",
|
||||||
"hint": "Wether using armor and reductions has rules on by default"
|
"hint": "Wether using armor and reductions has rules on by default"
|
||||||
|
|
@ -2725,6 +2798,12 @@
|
||||||
"setResourceIdentifier": "Set Resource Identifier"
|
"setResourceIdentifier": "Set Resource Identifier"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Keybindings": {
|
||||||
|
"spotlight": {
|
||||||
|
"name": "Spotlight Combatant",
|
||||||
|
"hint": "Move the spotlight to a hovered or selected token that's present in an active encounter"
|
||||||
|
}
|
||||||
|
},
|
||||||
"Menu": {
|
"Menu": {
|
||||||
"title": "Daggerheart Game Settings",
|
"title": "Daggerheart Game Settings",
|
||||||
"automation": {
|
"automation": {
|
||||||
|
|
@ -2923,6 +3002,8 @@
|
||||||
},
|
},
|
||||||
"EffectsDisplay": {
|
"EffectsDisplay": {
|
||||||
"removeThing": "[Right Click] Remove {thing}",
|
"removeThing": "[Right Click] Remove {thing}",
|
||||||
|
"increaseStacks": "[Left Click] Increment Stacks",
|
||||||
|
"decreaseStacks": "[Right Click] Decrement Stacks",
|
||||||
"appliedBy": "Applied By: {by}"
|
"appliedBy": "Applied By: {by}"
|
||||||
},
|
},
|
||||||
"ItemBrowser": {
|
"ItemBrowser": {
|
||||||
|
|
@ -3049,6 +3130,9 @@
|
||||||
"knowTheTide": "Know The Tide gained a token",
|
"knowTheTide": "Know The Tide gained a token",
|
||||||
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}"
|
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}"
|
||||||
},
|
},
|
||||||
|
"Progress": {
|
||||||
|
"migrationLabel": "Performing system migration. Please wait and do not close Foundry."
|
||||||
|
},
|
||||||
"Sidebar": {
|
"Sidebar": {
|
||||||
"actorDirectory": {
|
"actorDirectory": {
|
||||||
"tier": "Tier {tier} {type}",
|
"tier": "Tier {tier} {type}",
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,4 @@ export * as documents from './documents/_module.mjs';
|
||||||
export * as enrichers from './enrichers/_module.mjs';
|
export * as enrichers from './enrichers/_module.mjs';
|
||||||
export * as helpers from './helpers/_module.mjs';
|
export * as helpers from './helpers/_module.mjs';
|
||||||
export * as systemRegistration from './systemRegistration/_module.mjs';
|
export * as systemRegistration from './systemRegistration/_module.mjs';
|
||||||
|
export * as macros from './macros/_modules.mjs';
|
||||||
|
|
|
||||||
|
|
@ -554,7 +554,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
|
||||||
experiences: {
|
experiences: {
|
||||||
...this.setup.experiences,
|
...this.setup.experiences,
|
||||||
...Object.keys(this.character.system.experiences).reduce((acc, key) => {
|
...Object.keys(this.character.system.experiences).reduce((acc, key) => {
|
||||||
acc[`-=${key}`] = null;
|
acc[`${key}`] = _del;
|
||||||
return acc;
|
return acc;
|
||||||
}, {})
|
}, {})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,8 +77,8 @@ export default class CharacterResetDialog extends HandlebarsApplicationMixin(App
|
||||||
|
|
||||||
if (!this.data.optional.portrait.keep) {
|
if (!this.data.optional.portrait.keep) {
|
||||||
foundry.utils.setProperty(update, 'img', this.actor.schema.fields.img.initial(this.actor));
|
foundry.utils.setProperty(update, 'img', this.actor.schema.fields.img.initial(this.actor));
|
||||||
foundry.utils.setProperty(update, 'prototypeToken.==texture', {});
|
foundry.utils.setProperty(update, 'prototypeToken.texture', _replace({}));
|
||||||
foundry.utils.setProperty(update, 'prototypeToken.==ring', {});
|
foundry.utils.setProperty(update, 'prototypeToken.ring', _replace({}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.data.optional.biography.keep)
|
if (this.data.optional.biography.keep)
|
||||||
|
|
@ -89,7 +89,7 @@ export default class CharacterResetDialog extends HandlebarsApplicationMixin(App
|
||||||
const { system, ...rest } = update;
|
const { system, ...rest } = update;
|
||||||
await this.actor.update({
|
await this.actor.update({
|
||||||
...rest,
|
...rest,
|
||||||
'==system': system ?? {}
|
system: _replace(system ?? {})
|
||||||
});
|
});
|
||||||
|
|
||||||
const inventoryItemTypes = ['weapon', 'armor', 'consumable', 'loot'];
|
const inventoryItemTypes = ['weapon', 'armor', 'consumable', 'loot'];
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
updateIsAdvantage: this.updateIsAdvantage,
|
updateIsAdvantage: this.updateIsAdvantage,
|
||||||
selectExperience: this.selectExperience,
|
selectExperience: this.selectExperience,
|
||||||
toggleReaction: this.toggleReaction,
|
toggleReaction: this.toggleReaction,
|
||||||
toggleTagTeamRoll: this.toggleTagTeamRoll,
|
|
||||||
toggleSelectedEffect: this.toggleSelectedEffect,
|
toggleSelectedEffect: this.toggleSelectedEffect,
|
||||||
submitRoll: this.submitRoll
|
submitRoll: this.submitRoll
|
||||||
},
|
},
|
||||||
|
|
@ -71,8 +70,8 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
context.rollConfig = this.config;
|
context.rollConfig = this.config;
|
||||||
context.hasRoll = !!this.config.roll;
|
context.hasRoll = !!this.config.roll;
|
||||||
context.canRoll = true;
|
context.canRoll = true;
|
||||||
context.selectedRollMode = this.config.selectedRollMode ?? game.settings.get('core', 'rollMode');
|
context.selectedMessageMode = this.config.selectedMessageMode ?? game.settings.get('core', 'messageMode');
|
||||||
context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({
|
context.rollModes = Object.entries(CONFIG.ChatMessage.modes).map(([action, { label, icon }]) => ({
|
||||||
action,
|
action,
|
||||||
label,
|
label,
|
||||||
icon
|
icon
|
||||||
|
|
@ -133,12 +132,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
context.reactionOverride = this.reactionOverride;
|
context.reactionOverride = this.reactionOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagTeamSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
|
|
||||||
if (this.actor && tagTeamSetting.members[this.actor.id] && !this.config.skips?.createMessage) {
|
|
||||||
context.activeTagTeamRoll = true;
|
|
||||||
context.tagTeamSelected = this.config.tagTeamSelected;
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,10 +142,10 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
static updateRollConfiguration(event, _, formData) {
|
static updateRollConfiguration(_event, _, formData) {
|
||||||
const { ...rest } = foundry.utils.expandObject(formData.object);
|
const { ...rest } = foundry.utils.expandObject(formData.object);
|
||||||
|
|
||||||
this.config.selectedRollMode = rest.selectedRollMode;
|
this.config.selectedMessageMode = rest.selectedMessageMode;
|
||||||
|
|
||||||
if (this.config.costs) {
|
if (this.config.costs) {
|
||||||
this.config.costs = foundry.utils.mergeObject(this.config.costs, rest.costs);
|
this.config.costs = foundry.utils.mergeObject(this.config.costs, rest.costs);
|
||||||
|
|
@ -215,11 +208,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static toggleTagTeamRoll() {
|
|
||||||
this.config.tagTeamSelected = !this.config.tagTeamSelected;
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
static toggleSelectedEffect(_event, button) {
|
static toggleSelectedEffect(_event, button) {
|
||||||
this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
|
this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
|
||||||
this.render();
|
this.render();
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,8 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
|
||||||
context.formula = this.roll.constructFormula(this.config);
|
context.formula = this.roll.constructFormula(this.config);
|
||||||
context.hasHealing = this.config.hasHealing;
|
context.hasHealing = this.config.hasHealing;
|
||||||
context.directDamage = this.config.directDamage;
|
context.directDamage = this.config.directDamage;
|
||||||
context.selectedRollMode = this.config.selectedRollMode;
|
context.selectedMessageMode = this.config.selectedMessageMode;
|
||||||
context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({
|
context.rollModes = Object.entries(CONFIG.ChatMessage.modes).map(([action, { label, icon }]) => ({
|
||||||
action,
|
action,
|
||||||
label,
|
label,
|
||||||
icon
|
icon
|
||||||
|
|
@ -69,7 +69,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
|
||||||
const { ...rest } = foundry.utils.expandObject(formData.object);
|
const { ...rest } = foundry.utils.expandObject(formData.object);
|
||||||
foundry.utils.mergeObject(this.config.roll, rest.roll);
|
foundry.utils.mergeObject(this.config.roll, rest.roll);
|
||||||
foundry.utils.mergeObject(this.config.modifiers, rest.modifiers);
|
foundry.utils.mergeObject(this.config.modifiers, rest.modifiers);
|
||||||
this.config.selectedRollMode = rest.selectedRollMode;
|
this.config.selectedMessageMode = rest.selectedMessageMode;
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { damageKeyToNumber, getDamageLabel } from '../../helpers/utils.mjs';
|
import { damageKeyToNumber, getArmorSources, getDamageLabel } from '../../helpers/utils.mjs';
|
||||||
|
|
||||||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
|
|
||||||
|
|
@ -10,6 +10,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
||||||
this.reject = reject;
|
this.reject = reject;
|
||||||
this.actor = actor;
|
this.actor = actor;
|
||||||
this.damage = damage;
|
this.damage = damage;
|
||||||
|
|
||||||
this.damageType = damageType;
|
this.damageType = damageType;
|
||||||
this.rulesDefault = game.settings.get(
|
this.rulesDefault = game.settings.get(
|
||||||
CONFIG.DH.id,
|
CONFIG.DH.id,
|
||||||
|
|
@ -20,14 +21,20 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
||||||
this.rulesDefault
|
this.rulesDefault
|
||||||
);
|
);
|
||||||
|
|
||||||
const canApplyArmor = damageType.every(t => actor.system.armorApplicableDamageTypes[t] === true);
|
const orderedArmorSources = getArmorSources(actor).filter(s => !s.disabled);
|
||||||
const availableArmor = actor.system.armorScore - actor.system.armor.system.marks.value;
|
const armor = orderedArmorSources.reduce((acc, { document }) => {
|
||||||
const maxArmorMarks = canApplyArmor ? availableArmor : 0;
|
const { current, max } = document.type === 'armor' ? document.system.armor : document.system.armorData;
|
||||||
|
acc.push({
|
||||||
|
effect: document,
|
||||||
|
marks: [...Array(max).keys()].reduce((acc, _, index) => {
|
||||||
|
const spent = index < current;
|
||||||
|
acc[foundry.utils.randomID()] = { selected: false, disabled: spent, spent };
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
|
});
|
||||||
|
|
||||||
const armor = [...Array(maxArmorMarks).keys()].reduce((acc, _) => {
|
|
||||||
acc[foundry.utils.randomID()] = { selected: false };
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, []);
|
||||||
const stress = [...Array(actor.system.rules.damageReduction.maxArmorMarked.stressExtra ?? 0).keys()].reduce(
|
const stress = [...Array(actor.system.rules.damageReduction.maxArmorMarked.stressExtra ?? 0).keys()].reduce(
|
||||||
(acc, _) => {
|
(acc, _) => {
|
||||||
acc[foundry.utils.randomID()] = { selected: false };
|
acc[foundry.utils.randomID()] = { selected: false };
|
||||||
|
|
@ -121,13 +128,11 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
||||||
context.thresholdImmunities =
|
context.thresholdImmunities =
|
||||||
Object.keys(this.thresholdImmunities).length > 0 ? this.thresholdImmunities : null;
|
Object.keys(this.thresholdImmunities).length > 0 ? this.thresholdImmunities : null;
|
||||||
|
|
||||||
const { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage } =
|
const { selectedStressMarks, stressReductions, currentMarks, currentDamage, maxArmorUsed, availableArmor } =
|
||||||
this.getDamageInfo();
|
this.getDamageInfo();
|
||||||
|
|
||||||
context.armorScore = this.actor.system.armorScore;
|
context.armorScore = this.actor.system.armorScore.max;
|
||||||
context.armorMarks = currentMarks;
|
context.armorMarks = currentMarks;
|
||||||
context.basicMarksUsed =
|
|
||||||
selectedArmorMarks.length === this.actor.system.rules.damageReduction.maxArmorMarked.value;
|
|
||||||
|
|
||||||
const stressReductionStress = this.availableStressReductions
|
const stressReductionStress = this.availableStressReductions
|
||||||
? stressReductions.reduce((acc, red) => acc + red.cost, 0)
|
? stressReductions.reduce((acc, red) => acc + red.cost, 0)
|
||||||
|
|
@ -141,16 +146,30 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const maxArmor = this.actor.system.rules.damageReduction.maxArmorMarked.value;
|
context.maxArmorUsed = maxArmorUsed;
|
||||||
context.marks = {
|
context.availableArmor = availableArmor;
|
||||||
armor: Object.keys(this.marks.armor).reduce((acc, key, index) => {
|
context.basicMarksUsed = availableArmor === 0 || selectedStressMarks.length;
|
||||||
const mark = this.marks.armor[key];
|
|
||||||
if (!this.rulesOn || index + 1 <= maxArmor) acc[key] = mark;
|
|
||||||
|
|
||||||
return acc;
|
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,
|
||||||
|
uuid: source.effect.uuid,
|
||||||
|
marks: source.marks
|
||||||
|
});
|
||||||
|
}
|
||||||
|
context.marks = {
|
||||||
|
armor: armorSources,
|
||||||
stress: this.marks.stress
|
stress: this.marks.stress
|
||||||
};
|
};
|
||||||
|
|
||||||
|
context.usesStressArmor = Object.keys(context.marks.stress).length;
|
||||||
context.availableStressReductions = this.availableStressReductions;
|
context.availableStressReductions = this.availableStressReductions;
|
||||||
|
|
||||||
context.damage = getDamageLabel(this.damage);
|
context.damage = getDamageLabel(this.damage);
|
||||||
|
|
@ -167,27 +186,31 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
||||||
}
|
}
|
||||||
|
|
||||||
getDamageInfo = () => {
|
getDamageInfo = () => {
|
||||||
const selectedArmorMarks = Object.values(this.marks.armor).filter(x => x.selected);
|
const selectedArmorMarks = this.marks.armor.flatMap(x => Object.values(x.marks).filter(x => x.selected));
|
||||||
const selectedStressMarks = Object.values(this.marks.stress).filter(x => x.selected);
|
const selectedStressMarks = Object.values(this.marks.stress).filter(x => x.selected);
|
||||||
const stressReductions = this.availableStressReductions
|
const stressReductions = this.availableStressReductions
|
||||||
? Object.values(this.availableStressReductions).filter(red => red.selected)
|
? Object.values(this.availableStressReductions).filter(red => red.selected)
|
||||||
: [];
|
: [];
|
||||||
const currentMarks =
|
const currentMarks = this.actor.system.armorScore.value + selectedArmorMarks.length;
|
||||||
this.actor.system.armor.system.marks.value + selectedArmorMarks.length + selectedStressMarks.length;
|
|
||||||
|
const maxArmorUsed = this.actor.system.rules.damageReduction.maxArmorMarked.value + selectedStressMarks.length;
|
||||||
|
const availableArmor =
|
||||||
|
maxArmorUsed -
|
||||||
|
this.marks.armor.reduce((acc, source) => {
|
||||||
|
acc += Object.values(source.marks).filter(x => x.selected).length;
|
||||||
|
return acc;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
const armorMarkReduction =
|
const armorMarkReduction =
|
||||||
selectedArmorMarks.length * this.actor.system.rules.damageReduction.increasePerArmorMark;
|
selectedArmorMarks.length * this.actor.system.rules.damageReduction.increasePerArmorMark;
|
||||||
let currentDamage = Math.max(
|
let currentDamage = Math.max(this.damage - armorMarkReduction - stressReductions.length, 0);
|
||||||
this.damage - armorMarkReduction - selectedStressMarks.length - stressReductions.length,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
if (this.reduceSeverity) {
|
if (this.reduceSeverity) {
|
||||||
currentDamage = Math.max(currentDamage - this.reduceSeverity, 0);
|
currentDamage = Math.max(currentDamage - this.reduceSeverity, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.thresholdImmunities[currentDamage]) currentDamage = 0;
|
if (this.thresholdImmunities[currentDamage]) currentDamage = 0;
|
||||||
|
|
||||||
return { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage };
|
return { selectedStressMarks, stressReductions, currentMarks, currentDamage, maxArmorUsed, availableArmor };
|
||||||
};
|
};
|
||||||
|
|
||||||
static toggleRules() {
|
static toggleRules() {
|
||||||
|
|
@ -195,13 +218,10 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
||||||
|
|
||||||
const maxArmor = this.actor.system.rules.damageReduction.maxArmorMarked.value;
|
const maxArmor = this.actor.system.rules.damageReduction.maxArmorMarked.value;
|
||||||
this.marks = {
|
this.marks = {
|
||||||
armor: Object.keys(this.marks.armor).reduce((acc, key, index) => {
|
armor: this.marks.armor.map((mark, index) => {
|
||||||
const mark = this.marks.armor[key];
|
|
||||||
const keepSelectValue = !this.rulesOn || index + 1 <= maxArmor;
|
const keepSelectValue = !this.rulesOn || index + 1 <= maxArmor;
|
||||||
acc[key] = { ...mark, selected: keepSelectValue ? mark.selected : false };
|
return { ...mark, selected: keepSelectValue ? mark.selected : false };
|
||||||
|
}),
|
||||||
return acc;
|
|
||||||
}, {}),
|
|
||||||
stress: this.marks.stress
|
stress: this.marks.stress
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -209,8 +229,8 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
||||||
}
|
}
|
||||||
|
|
||||||
static setMarks(_, target) {
|
static setMarks(_, target) {
|
||||||
const currentMark = this.marks[target.dataset.type][target.dataset.key];
|
const currentMark = foundry.utils.getProperty(this.marks, target.dataset.path);
|
||||||
const { selectedStressMarks, stressReductions, currentMarks, currentDamage } = this.getDamageInfo();
|
const { selectedStressMarks, stressReductions, currentDamage, availableArmor } = this.getDamageInfo();
|
||||||
|
|
||||||
if (!currentMark.selected && currentDamage === 0) {
|
if (!currentMark.selected && currentDamage === 0) {
|
||||||
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.damageAlreadyNone'));
|
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.damageAlreadyNone'));
|
||||||
|
|
@ -218,12 +238,18 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.rulesOn) {
|
if (this.rulesOn) {
|
||||||
if (!currentMark.selected && currentMarks === this.actor.system.armorScore) {
|
if (target.dataset.type === 'armor' && !currentMark.selected && !availableArmor) {
|
||||||
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noAvailableArmorMarks'));
|
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noAvailableArmorMarks'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stressUsed = selectedStressMarks.length;
|
||||||
|
if (target.dataset.type === 'armor' && stressUsed) {
|
||||||
|
const updateResult = this.updateStressArmor(target.dataset.id, !currentMark.selected);
|
||||||
|
if (updateResult === false) return;
|
||||||
|
}
|
||||||
|
|
||||||
if (currentMark.selected) {
|
if (currentMark.selected) {
|
||||||
const currentDamageLabel = getDamageLabel(currentDamage);
|
const currentDamageLabel = getDamageLabel(currentDamage);
|
||||||
for (let reduction of stressReductions) {
|
for (let reduction of stressReductions) {
|
||||||
|
|
@ -232,8 +258,16 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (target.dataset.type === 'armor' && selectedStressMarks.length > 0) {
|
if (target.dataset.type === 'stress' && currentMark.armorMarkId) {
|
||||||
selectedStressMarks.forEach(mark => (mark.selected = false));
|
for (const source of this.marks.armor) {
|
||||||
|
const match = Object.keys(source.marks).find(key => key === currentMark.armorMarkId);
|
||||||
|
if (match) {
|
||||||
|
source.marks[match].selected = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentMark.armorMarkId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,6 +275,25 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateStressArmor(armorMarkId, select) {
|
||||||
|
let stressMarkKey = null;
|
||||||
|
if (select) {
|
||||||
|
stressMarkKey = Object.keys(this.marks.stress).find(
|
||||||
|
key => this.marks.stress[key].selected && !this.marks.stress[key].armorMarkId
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
stressMarkKey = Object.keys(this.marks.stress).find(
|
||||||
|
key => this.marks.stress[key].armorMarkId === armorMarkId
|
||||||
|
);
|
||||||
|
if (!stressMarkKey)
|
||||||
|
stressMarkKey = Object.keys(this.marks.stress).find(key => this.marks.stress[key].selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stressMarkKey) return false;
|
||||||
|
|
||||||
|
this.marks.stress[stressMarkKey].armorMarkId = select ? armorMarkId : null;
|
||||||
|
}
|
||||||
|
|
||||||
static useStressReduction(_, target) {
|
static useStressReduction(_, target) {
|
||||||
const damageValue = Number(target.dataset.reduction);
|
const damageValue = Number(target.dataset.reduction);
|
||||||
const stressReduction = this.availableStressReductions[damageValue];
|
const stressReduction = this.availableStressReductions[damageValue];
|
||||||
|
|
@ -279,11 +332,18 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
||||||
}
|
}
|
||||||
|
|
||||||
static async takeDamage() {
|
static async takeDamage() {
|
||||||
const { selectedArmorMarks, selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo();
|
const { selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo();
|
||||||
const armorSpent = selectedArmorMarks.length + selectedStressMarks.length;
|
const armorChanges = this.marks.armor.reduce((acc, source) => {
|
||||||
const stressSpent = selectedStressMarks.length + stressReductions.reduce((acc, red) => acc + red.cost, 0);
|
const amount = Object.values(source.marks).filter(x => x.selected).length;
|
||||||
|
if (amount) acc.push({ uuid: source.effect.uuid, amount });
|
||||||
|
|
||||||
this.resolve({ modifiedDamage: currentDamage, armorSpent, stressSpent });
|
return acc;
|
||||||
|
}, []);
|
||||||
|
const stressSpent =
|
||||||
|
selectedStressMarks.filter(x => x.armorMarkId).length +
|
||||||
|
stressReductions.reduce((acc, red) => acc + red.cost, 0);
|
||||||
|
|
||||||
|
this.resolve({ modifiedDamage: currentDamage, armorChanges, stressSpent });
|
||||||
await this.close(true);
|
await this.close(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { refreshIsAllowed } from '../../helpers/utils.mjs';
|
import { expireActiveEffects, refreshIsAllowed } from '../../helpers/utils.mjs';
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
|
|
@ -203,7 +203,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
|
||||||
const msg = {
|
const msg = {
|
||||||
user: game.user.id,
|
user: game.user.id,
|
||||||
system: {
|
system: {
|
||||||
moves: moves,
|
moves: moves.map(move => ({ ...move, actions: Array.from(move.actions) })),
|
||||||
actor: this.actor.uuid
|
actor: this.actor.uuid
|
||||||
},
|
},
|
||||||
speaker: cls.getSpeaker(),
|
speaker: cls.getSpeaker(),
|
||||||
|
|
@ -264,6 +264,8 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
|
||||||
await feature.update({ 'system.resource.value': resetValue });
|
await feature.update({ 'system.resource.value': resetValue });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expireActiveEffects(this.actor, [this.shortRest ? 'shortRest' : 'longRest']);
|
||||||
|
|
||||||
this.close();
|
this.close();
|
||||||
} else {
|
} else {
|
||||||
this.render();
|
this.render();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
|
|
||||||
|
|
||||||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
|
|
||||||
export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
|
@ -123,16 +121,8 @@ export default class RerollDamageDialog extends HandlebarsApplicationMixin(Appli
|
||||||
return acc;
|
return acc;
|
||||||
}, {})
|
}, {})
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.message.update(update);
|
await this.message.update(update);
|
||||||
|
|
||||||
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
|
|
||||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
|
||||||
action: socketEvent.Refresh,
|
|
||||||
data: {
|
|
||||||
refreshType: RefreshType.TagTeamRoll
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.close();
|
await this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -67,7 +67,7 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
|
||||||
break;
|
break;
|
||||||
case 'summary':
|
case 'summary':
|
||||||
const levelKeys = Object.keys(this.levelup.levels);
|
const levelKeys = Object.keys(this.levelup.levels);
|
||||||
const actorDamageDice = this.actor.system.attack.damage.parts[0].value.dice;
|
const actorDamageDice = this.actor.system.attack.damage.parts.hitPoints.value.dice;
|
||||||
const actorRange = this.actor.system.attack.range;
|
const actorRange = this.actor.system.attack.range;
|
||||||
|
|
||||||
let achievementExperiences = [];
|
let achievementExperiences = [];
|
||||||
|
|
|
||||||
|
|
@ -477,7 +477,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
|
||||||
const secondaryData = Object.keys(
|
const secondaryData = Object.keys(
|
||||||
foundry.utils.getProperty(this.levelup, `${target.dataset.path}.secondaryData`)
|
foundry.utils.getProperty(this.levelup, `${target.dataset.path}.secondaryData`)
|
||||||
).reduce((acc, key) => {
|
).reduce((acc, key) => {
|
||||||
acc[`-=${key}`] = null;
|
acc[key] = _del;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
await this.levelup.updateSource({
|
await this.levelup.updateSource({
|
||||||
|
|
@ -511,9 +511,9 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
|
||||||
const current = foundry.utils.getProperty(this.levelup, `${basePath}.${button.dataset.option}`);
|
const current = foundry.utils.getProperty(this.levelup, `${basePath}.${button.dataset.option}`);
|
||||||
if (Number(button.dataset.cost) > 1 || Object.keys(current).length === 1) {
|
if (Number(button.dataset.cost) > 1 || Object.keys(current).length === 1) {
|
||||||
// Simple handling that doesn't cover potential Custom LevelTiers.
|
// Simple handling that doesn't cover potential Custom LevelTiers.
|
||||||
update[`${basePath}.-=${button.dataset.option}`] = null;
|
update[`${basePath}.${button.dataset.option}`] = _del;
|
||||||
} else {
|
} else {
|
||||||
update[`${basePath}.${button.dataset.option}.-=${button.dataset.checkboxNr}`] = null;
|
update[`${basePath}.${button.dataset.option}.${button.dataset.checkboxNr}`] = _del;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.levelup.levels[this.levelup.currentLevel].nrSelections.available < Number(button.dataset.cost)) {
|
if (this.levelup.levels[this.levelup.currentLevel].nrSelections.available < Number(button.dataset.cost)) {
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,15 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onDrop(event) {
|
async _onDrop(event) {
|
||||||
|
event.stopPropagation();
|
||||||
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
|
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
|
||||||
|
if (data.type === 'Level') {
|
||||||
|
const level = await foundry.documents.Level.fromDropData(data);
|
||||||
|
if (level?.parent === this.document) return this._onSortLevel(event, level);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const item = await foundry.utils.fromUuid(data.uuid);
|
const item = await foundry.utils.fromUuid(data.uuid);
|
||||||
if (item instanceof game.system.api.documents.DhpActor && item.type === 'environment') {
|
if (item instanceof game.system.api.documents.DhpActor && item.type === 'environment') {
|
||||||
let sceneUuid = data.uuid;
|
let sceneUuid = data.uuid;
|
||||||
|
|
@ -114,7 +122,7 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
|
||||||
|
|
||||||
for (const key of Object.keys(this.document._source.flags.daggerheart?.sceneEnvironments ?? {})) {
|
for (const key of Object.keys(this.document._source.flags.daggerheart?.sceneEnvironments ?? {})) {
|
||||||
if (!submitData.flags.daggerheart.sceneEnvironments[key]) {
|
if (!submitData.flags.daggerheart.sceneEnvironments[key]) {
|
||||||
submitData.flags.daggerheart.sceneEnvironments[`-=${key}`] = null;
|
submitData.flags.daggerheart.sceneEnvironments[key] = _del;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -298,7 +298,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
const isDowntime = ['shortRest', 'longRest'].includes(type);
|
const isDowntime = ['shortRest', 'longRest'].includes(type);
|
||||||
const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`;
|
const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`;
|
||||||
await this.settings.updateSource({
|
await this.settings.updateSource({
|
||||||
[`${path}.-=${id}`]: null
|
[`${path}.${id}`]: _del
|
||||||
});
|
});
|
||||||
|
|
||||||
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
|
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
|
||||||
|
|
@ -322,7 +322,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
const fields = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).schema.fields;
|
const fields = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).schema.fields;
|
||||||
|
|
||||||
const removeUpdate = Object.keys(this.settings.restMoves[target.dataset.type].moves).reduce((acc, key) => {
|
const removeUpdate = Object.keys(this.settings.restMoves[target.dataset.type].moves).reduce((acc, key) => {
|
||||||
acc[`-=${key}`] = null;
|
acc[key] = _del;
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
@ -382,7 +382,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
[`itemFeatures.${target.dataset.type}`]: Object.keys(
|
[`itemFeatures.${target.dataset.type}`]: Object.keys(
|
||||||
this.settings.itemFeatures[target.dataset.type]
|
this.settings.itemFeatures[target.dataset.type]
|
||||||
).reduce((acc, key) => {
|
).reduce((acc, key) => {
|
||||||
acc[`-=${key}`] = null;
|
acc[key] = _del;
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {})
|
}, {})
|
||||||
|
|
@ -455,12 +455,12 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
await this.settings.updateSource({
|
await this.settings.updateSource({
|
||||||
[`domains.-=${this.selected.domain}`]: null
|
[`domains.${this.selected.domain}`]: _del
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew);
|
const currentSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew);
|
||||||
if (currentSettings.domains[this.selected.domain]) {
|
if (currentSettings.domains[this.selected.domain]) {
|
||||||
await currentSettings.updateSource({ [`domains.-=${this.selected.domain}`]: null });
|
await currentSettings.updateSource({ [`domains.${this.selected.domain}`]: _del });
|
||||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, currentSettings);
|
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, currentSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -507,7 +507,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
|
|
||||||
static async deleteAdversaryType(_, target) {
|
static async deleteAdversaryType(_, target) {
|
||||||
const { key } = target.dataset;
|
const { key } = target.dataset;
|
||||||
await this.settings.updateSource({ [`adversaryTypes.-=${key}`]: null });
|
await this.settings.updateSource({ [`adversaryTypes.${key}`]: _del });
|
||||||
|
|
||||||
this.selected.adversaryType = this.selected.adversaryType === key ? null : this.selected.adversaryType;
|
this.selected.adversaryType = this.selected.adversaryType === key ? null : this.selected.adversaryType;
|
||||||
this.render();
|
this.render();
|
||||||
|
|
@ -563,7 +563,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
|
|
||||||
const { actorType, resourceKey } = target.dataset;
|
const { actorType, resourceKey } = target.dataset;
|
||||||
await this.settings.updateSource({
|
await this.settings.updateSource({
|
||||||
[`resources.${actorType}.resources.-=${resourceKey}`]: null
|
[`resources.${actorType}.resources.${resourceKey}`]: _del
|
||||||
});
|
});
|
||||||
|
|
||||||
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
|
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ export { default as ActionSettingsConfig } from './action-settings-config.mjs';
|
||||||
export { default as CharacterSettings } from './character-settings.mjs';
|
export { default as CharacterSettings } from './character-settings.mjs';
|
||||||
export { default as AdversarySettings } from './adversary-settings.mjs';
|
export { default as AdversarySettings } from './adversary-settings.mjs';
|
||||||
export { default as CompanionSettings } from './companion-settings.mjs';
|
export { default as CompanionSettings } from './companion-settings.mjs';
|
||||||
export { default as SettingActiveEffectConfig } from './setting-active-effect-config.mjs';
|
|
||||||
export { default as SettingFeatureConfig } from './setting-feature-config.mjs';
|
export { default as SettingFeatureConfig } from './setting-feature-config.mjs';
|
||||||
export { default as EnvironmentSettings } from './environment-settings.mjs';
|
export { default as EnvironmentSettings } from './environment-settings.mjs';
|
||||||
export { default as ActiveEffectConfig } from './activeEffectConfig.mjs';
|
export { default as ActiveEffectConfig } from './activeEffectConfig.mjs';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { getUnusedDamageTypes } from '../../helpers/utils.mjs';
|
||||||
import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs';
|
import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs';
|
||||||
|
|
||||||
const { ApplicationV2 } = foundry.applications.api;
|
const { ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
@ -104,7 +105,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects', 'summon'];
|
static CLEAN_ARRAYS = ['cost', 'effects', 'summon'];
|
||||||
|
|
||||||
_getTabs(tabs) {
|
_getTabs(tabs) {
|
||||||
for (const v of Object.values(tabs)) {
|
for (const v of Object.values(tabs)) {
|
||||||
|
|
@ -153,8 +154,13 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
||||||
context.openSection = this.openSection;
|
context.openSection = this.openSection;
|
||||||
context.tabs = this._getTabs(this.constructor.TABS);
|
context.tabs = this._getTabs(this.constructor.TABS);
|
||||||
context.config = CONFIG.DH;
|
context.config = CONFIG.DH;
|
||||||
if (this.action.damage?.hasOwnProperty('includeBase') && this.action.type === 'attack')
|
if (this.action.hasDamage) {
|
||||||
context.hasBaseDamage = !!this.action.parent.attack;
|
context.allDamageTypesUsed = !getUnusedDamageTypes(this.action.damage.parts).length;
|
||||||
|
|
||||||
|
if (this.action.damage.hasOwnProperty('includeBase') && this.action.type === 'attack')
|
||||||
|
context.hasBaseDamage = !!this.action.parent.attack;
|
||||||
|
}
|
||||||
|
|
||||||
context.costOptions = this.getCostOptions();
|
context.costOptions = this.getCostOptions();
|
||||||
context.getRollTypeOptions = this.getRollTypeOptions();
|
context.getRollTypeOptions = this.getRollTypeOptions();
|
||||||
context.disableOption = this.disableOption.bind(this);
|
context.disableOption = this.disableOption.bind(this);
|
||||||
|
|
@ -291,18 +297,64 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
||||||
|
|
||||||
static addDamage(_event) {
|
static addDamage(_event) {
|
||||||
if (!this.action.damage.parts) return;
|
if (!this.action.damage.parts) return;
|
||||||
const data = this.action.toObject(),
|
|
||||||
part = {};
|
const choices = getUnusedDamageTypes(this.action.damage.parts);
|
||||||
if (this.action.actor?.isNPC) part.value = { multiplier: 'flat' };
|
const content = new foundry.data.fields.StringField({
|
||||||
data.damage.parts.push(part);
|
label: game.i18n.localize('Damage Type'),
|
||||||
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
|
choices,
|
||||||
|
required: true
|
||||||
|
}).toFormGroup(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
localize: true,
|
||||||
|
nameAttr: 'value',
|
||||||
|
labelAttr: 'label'
|
||||||
|
}
|
||||||
|
).outerHTML;
|
||||||
|
|
||||||
|
const callback = (_, button) => {
|
||||||
|
const data = this.action.toObject();
|
||||||
|
const type = choices[button.form.elements.type.value].value;
|
||||||
|
const part = this.action.schema.fields.damage.fields.parts.element.getInitialValue();
|
||||||
|
part.applyTo = type;
|
||||||
|
if (type === CONFIG.DH.GENERAL.healingTypes.hitPoints.id)
|
||||||
|
part.type = this.action.schema.fields.damage.fields.parts.element.fields.type.element.initial;
|
||||||
|
|
||||||
|
data.damage.parts[type] = part;
|
||||||
|
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeDialog = new foundry.applications.api.DialogV2({
|
||||||
|
buttons: [
|
||||||
|
foundry.utils.mergeObject(
|
||||||
|
{
|
||||||
|
action: 'ok',
|
||||||
|
label: 'Confirm',
|
||||||
|
icon: 'fas fa-check',
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
{ callback: callback }
|
||||||
|
)
|
||||||
|
],
|
||||||
|
content: content,
|
||||||
|
rejectClose: false,
|
||||||
|
modal: false,
|
||||||
|
window: {
|
||||||
|
title: game.i18n.localize('Add Damage')
|
||||||
|
},
|
||||||
|
position: { width: 300 }
|
||||||
|
});
|
||||||
|
|
||||||
|
typeDialog.render(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
static removeDamage(_event, button) {
|
static removeDamage(_event, button) {
|
||||||
if (!this.action.damage.parts) return;
|
if (!this.action.damage.parts) return;
|
||||||
const data = this.action.toObject(),
|
const data = this.action.toObject();
|
||||||
index = button.dataset.index;
|
const key = button.dataset.key;
|
||||||
data.damage.parts.splice(index, 1);
|
delete data.damage.parts[key];
|
||||||
|
data.damage.parts[`${key}`] = _del;
|
||||||
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
|
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,13 @@ export default class DHActionConfig extends DHActionBaseConfig {
|
||||||
const effectData = this._addEffectData.bind(this)();
|
const effectData = this._addEffectData.bind(this)();
|
||||||
const data = this.action.toObject();
|
const data = this.action.toObject();
|
||||||
|
|
||||||
const [created] = await this.action.item.createEmbeddedDocuments('ActiveEffect', [effectData], {
|
const created = await this.action.item.createEmbeddedDocuments('ActiveEffect', [
|
||||||
render: false
|
game.system.api.data.activeEffects.BaseEffect.getDefaultObject()
|
||||||
});
|
]);
|
||||||
data.effects.push({ _id: created._id });
|
|
||||||
|
data.effects.push({ _id: created[0]._id });
|
||||||
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
|
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
|
||||||
this.action.item.effects.get(created._id).sheet.render(true);
|
this.action.item.effects.get(created[0]._id).sheet.render(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export default class DHActionSettingsConfig extends DHActionBaseConfig {
|
||||||
|
|
||||||
static async editEffect(event) {
|
static async editEffect(event) {
|
||||||
const id = event.target.closest('[data-effect-id]')?.dataset?.effectId;
|
const id = event.target.closest('[data-effect-id]')?.dataset?.effectId;
|
||||||
const updatedEffect = await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(
|
const updatedEffect = await game.system.api.applications.sheetConfigs.ActiveEffectConfig.configureSetting(
|
||||||
this.getEffectDetails(id)
|
this.getEffectDetails(id)
|
||||||
);
|
);
|
||||||
if (!updatedEffect) return;
|
if (!updatedEffect) return;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
|
||||||
settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' },
|
settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' },
|
||||||
changes: {
|
changes: {
|
||||||
template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs',
|
template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs',
|
||||||
|
templates: ['systems/daggerheart/templates/sheets/activeEffect/change.hbs'],
|
||||||
scrollable: ['ol[data-changes]']
|
scrollable: ['ol[data-changes]']
|
||||||
},
|
},
|
||||||
footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' }
|
footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' }
|
||||||
|
|
@ -149,6 +150,18 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
|
||||||
minLength: 0
|
minLength: 0
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
htmlElement
|
||||||
|
.querySelector('.stacking-change-checkbox')
|
||||||
|
?.addEventListener('change', this.stackingChangeToggle.bind(this));
|
||||||
|
|
||||||
|
htmlElement
|
||||||
|
.querySelector('.armor-change-checkbox')
|
||||||
|
?.addEventListener('change', this.armorChangeToggle.bind(this));
|
||||||
|
|
||||||
|
htmlElement
|
||||||
|
.querySelector('.armor-damage-thresholds-checkbox')
|
||||||
|
?.addEventListener('change', this.armorDamageThresholdToggle.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
async _prepareContext(options) {
|
async _prepareContext(options) {
|
||||||
|
|
@ -173,8 +186,166 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'settings':
|
||||||
|
const groups = {
|
||||||
|
time: _loc('EFFECT.DURATION.UNITS.GROUPS.time'),
|
||||||
|
combat: _loc('EFFECT.DURATION.UNITS.GROUPS.combat')
|
||||||
|
};
|
||||||
|
partContext.durationUnits = CONST.ACTIVE_EFFECT_DURATION_UNITS.map(value => ({
|
||||||
|
value,
|
||||||
|
label: _loc(`EFFECT.DURATION.UNITS.${value}`),
|
||||||
|
group: CONST.ACTIVE_EFFECT_TIME_DURATION_UNITS.includes(value) ? groups.time : groups.combat
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
case 'changes':
|
||||||
|
const singleTypes = ['armor'];
|
||||||
|
const typedChanges = context.source.changes.reduce((acc, change, index) => {
|
||||||
|
if (singleTypes.includes(change.type)) {
|
||||||
|
acc[change.type] = { ...change, index };
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
partContext.changes = partContext.changes.filter(c => !!c);
|
||||||
|
partContext.typedChanges = typedChanges;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return partContext;
|
return partContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stackingChangeToggle(event) {
|
||||||
|
const stackingFields = this.document.system.schema.fields.stacking.fields;
|
||||||
|
const systemData = {
|
||||||
|
stacking: event.target.checked
|
||||||
|
? { value: stackingFields.value.initial, max: stackingFields.max.initial }
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
return this.submit({ updateData: { system: systemData } });
|
||||||
|
}
|
||||||
|
|
||||||
|
armorChangeToggle(event) {
|
||||||
|
if (event.target.checked) {
|
||||||
|
this.addArmorChange();
|
||||||
|
} else {
|
||||||
|
this.removeTypedChange(event.target.dataset.index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Could be generalised if needed later */
|
||||||
|
addArmorChange() {
|
||||||
|
const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form));
|
||||||
|
const changes = Object.values(submitData.system?.changes ?? {});
|
||||||
|
changes.push(game.system.api.data.activeEffects.changeTypes.armor.getInitialValue());
|
||||||
|
return this.submit({ updateData: { system: { changes } } });
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTypedChange(indexString) {
|
||||||
|
const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form));
|
||||||
|
const changes = Object.values(submitData.system.changes);
|
||||||
|
const index = Number(indexString);
|
||||||
|
changes.splice(index, 1);
|
||||||
|
return this.submit({ updateData: { system: { changes } } });
|
||||||
|
}
|
||||||
|
|
||||||
|
armorDamageThresholdToggle(event) {
|
||||||
|
const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form));
|
||||||
|
const changes = Object.values(submitData.system?.changes ?? {});
|
||||||
|
const index = Number(event.target.dataset.index);
|
||||||
|
if (event.target.checked) {
|
||||||
|
changes[index].value.damageThresholds = { major: 0, severe: 0 };
|
||||||
|
} else {
|
||||||
|
changes[index].value.damageThresholds = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.submit({ updateData: { system: { changes } } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
_renderChange(context) {
|
||||||
|
const { change, index, defaultPriority } = context;
|
||||||
|
if (!(change.type in CONFIG.DH.GENERAL.baseActiveEffectModes)) return null;
|
||||||
|
|
||||||
|
const changeTypesSchema = this.document.system.schema.fields.changes.element.types;
|
||||||
|
const fields = context.fields ?? (changeTypesSchema[change.type] ?? changeTypesSchema.add).fields;
|
||||||
|
if (typeof change.value !== 'string') change.value = JSON.stringify(change.value);
|
||||||
|
Object.assign(
|
||||||
|
change,
|
||||||
|
['key', 'type', 'value', 'priority'].reduce((paths, fieldName) => {
|
||||||
|
paths[`${fieldName}Path`] = `system.changes.${index}.${fieldName}`;
|
||||||
|
return paths;
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
game.system.api.documents.DhActiveEffect.CHANGE_TYPES[change.type].render?.(
|
||||||
|
change,
|
||||||
|
index,
|
||||||
|
defaultPriority
|
||||||
|
) ??
|
||||||
|
foundry.applications.handlebars.renderTemplate(
|
||||||
|
'systems/daggerheart/templates/sheets/activeEffect/change.hbs',
|
||||||
|
{
|
||||||
|
change,
|
||||||
|
index,
|
||||||
|
defaultPriority,
|
||||||
|
fields,
|
||||||
|
types: Object.keys(CONFIG.DH.GENERAL.baseActiveEffectModes).reduce((r, key) => {
|
||||||
|
r[key] = CONFIG.DH.GENERAL.baseActiveEffectModes[key].label;
|
||||||
|
return r;
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
|
_onChangeForm(_formConfig, event) {
|
||||||
|
if (foundry.utils.isElementInstanceOf(event.target, 'select') && event.target.name === 'system.duration.type') {
|
||||||
|
const durationSection = this.element.querySelector('.custom-duration-section');
|
||||||
|
if (event.target.value === 'custom') durationSection.classList.add('visible');
|
||||||
|
else durationSection.classList.remove('visible');
|
||||||
|
|
||||||
|
const durationDescription = this.element.querySelector('.duration-description');
|
||||||
|
if (event.target.value === 'temporary') durationDescription.classList.add('visible');
|
||||||
|
else durationDescription.classList.remove('visible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
|
_processFormData(event, form, formData) {
|
||||||
|
const submitData = super._processFormData(event, form, formData);
|
||||||
|
if (submitData.start && !submitData.start.time) submitData.start.time = '0';
|
||||||
|
else if (!submitData) submitData.start = null;
|
||||||
|
|
||||||
|
return submitData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
|
_processSubmitData(event, form, submitData, options) {
|
||||||
|
if (this.options.isSetting) {
|
||||||
|
// Settings should update source instead
|
||||||
|
this.document.updateSource(submitData);
|
||||||
|
this.render();
|
||||||
|
} else {
|
||||||
|
return super._processSubmitData(event, form, submitData, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates an active effect config for a setting */
|
||||||
|
static async configureSetting(effect, options = {}) {
|
||||||
|
const document = new CONFIG.ActiveEffect.documentClass({ ...foundry.utils.duplicate(effect), _id: effect.id });
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const app = new this({ document, ...options, isSetting: true });
|
||||||
|
app.addEventListener(
|
||||||
|
'close',
|
||||||
|
() => {
|
||||||
|
const newEffect = app.document.toObject(true);
|
||||||
|
newEffect.id = newEffect._id;
|
||||||
|
delete newEffect._id;
|
||||||
|
resolve(newEffect);
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
app.render({ force: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ export default class DHAdversarySettings extends DHBaseActorSettings {
|
||||||
});
|
});
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
await this.actor.update({ [`system.experiences.-=${target.dataset.experience}`]: null });
|
await this.actor.update({ [`system.experiences.${target.dataset.experience}`]: _del });
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onDragStart(event) {
|
async _onDragStart(event) {
|
||||||
|
|
|
||||||
|
|
@ -101,8 +101,8 @@ export default class DHCharacterSettings extends DHBaseActorSettings {
|
||||||
|
|
||||||
if (relinkAchievementData.length > 0) {
|
if (relinkAchievementData.length > 0) {
|
||||||
relinkAchievementData.forEach(data => {
|
relinkAchievementData.forEach(data => {
|
||||||
updates[`system.levelData.levelups.${data.levelKey}.achievements.experiences.-=${data.experience}`] =
|
updates[`system.levelData.levelups.${data.levelKey}.achievements.experiences.${data.experience}`] =
|
||||||
null;
|
_del;
|
||||||
});
|
});
|
||||||
} else if (relinkSelectionData.length > 0) {
|
} else if (relinkSelectionData.length > 0) {
|
||||||
relinkSelectionData.forEach(data => {
|
relinkSelectionData.forEach(data => {
|
||||||
|
|
@ -137,7 +137,7 @@ export default class DHCharacterSettings extends DHBaseActorSettings {
|
||||||
|
|
||||||
await this.actor.update({
|
await this.actor.update({
|
||||||
...updates,
|
...updates,
|
||||||
[`system.experiences.-=${target.dataset.experience}`]: null
|
[`system.experiences.${target.dataset.experience}`]: _del
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,6 @@ export default class DHCompanionSettings extends DHBaseActorSettings {
|
||||||
});
|
});
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
await this.actor.update({ [`system.experiences.-=${target.dataset.experience}`]: null });
|
await this.actor.update({ [`system.experiences.${target.dataset.experience}`]: _del });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,9 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
|
||||||
*/
|
*/
|
||||||
static async #addCategory() {
|
static async #addCategory() {
|
||||||
await this.actor.update({
|
await this.actor.update({
|
||||||
[`system.potentialAdversaries.${foundry.utils.randomID()}.label`]: game.i18n.localize(
|
[`system.potentialAdversaries.${foundry.utils.randomID()}`]: {
|
||||||
'DAGGERHEART.ACTORS.Environment.newAdversary'
|
label: game.i18n.localize('DAGGERHEART.ACTORS.Environment.newAdversary')
|
||||||
)
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,7 +79,7 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
|
||||||
* @type {ApplicationClickAction}
|
* @type {ApplicationClickAction}
|
||||||
*/
|
*/
|
||||||
static async #removeCategory(_, target) {
|
static async #removeCategory(_, target) {
|
||||||
await this.actor.update({ [`system.potentialAdversaries.-=${target.dataset.categoryId}`]: null });
|
await this.actor.update({ [`system.potentialAdversaries.${target.dataset.categoryId}`]: _del });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -138,4 +138,8 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _onDropItem(event, item) {
|
||||||
|
console.log(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,223 +0,0 @@
|
||||||
import autocomplete from 'autocompleter';
|
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
|
||||||
|
|
||||||
export default class SettingActiveEffectConfig extends HandlebarsApplicationMixin(ApplicationV2) {
|
|
||||||
constructor(effect) {
|
|
||||||
super({});
|
|
||||||
|
|
||||||
this.effect = foundry.utils.deepClone(effect);
|
|
||||||
this.changeChoices = game.system.api.applications.sheetConfigs.ActiveEffectConfig.getChangeChoices();
|
|
||||||
}
|
|
||||||
|
|
||||||
static DEFAULT_OPTIONS = {
|
|
||||||
classes: ['daggerheart', 'sheet', 'dh-style', 'active-effect-config', 'standard-form'],
|
|
||||||
tag: 'form',
|
|
||||||
position: {
|
|
||||||
width: 560
|
|
||||||
},
|
|
||||||
form: {
|
|
||||||
submitOnChange: false,
|
|
||||||
closeOnSubmit: false,
|
|
||||||
handler: SettingActiveEffectConfig.#onSubmit
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
editImage: SettingActiveEffectConfig.#editImage,
|
|
||||||
addChange: SettingActiveEffectConfig.#addChange,
|
|
||||||
deleteChange: SettingActiveEffectConfig.#deleteChange
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
static PARTS = {
|
|
||||||
header: { template: 'systems/daggerheart/templates/sheets/activeEffect/header.hbs' },
|
|
||||||
tabs: { template: 'templates/generic/tab-navigation.hbs' },
|
|
||||||
details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] },
|
|
||||||
settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' },
|
|
||||||
changes: {
|
|
||||||
template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs',
|
|
||||||
scrollable: ['ol[data-changes]']
|
|
||||||
},
|
|
||||||
footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' }
|
|
||||||
};
|
|
||||||
|
|
||||||
static TABS = {
|
|
||||||
sheet: {
|
|
||||||
tabs: [
|
|
||||||
{ id: 'details', icon: 'fa-solid fa-book' },
|
|
||||||
{ id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' },
|
|
||||||
{ id: 'changes', icon: 'fa-solid fa-gears' }
|
|
||||||
],
|
|
||||||
initial: 'details',
|
|
||||||
labelPrefix: 'EFFECT.TABS'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**@inheritdoc */
|
|
||||||
async _onFirstRender(context, options) {
|
|
||||||
await super._onFirstRender(context, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _prepareContext(_options) {
|
|
||||||
const context = await super._prepareContext(_options);
|
|
||||||
context.source = this.effect;
|
|
||||||
context.fields = game.system.api.documents.DhActiveEffect.schema.fields;
|
|
||||||
context.systemFields = game.system.api.data.activeEffects.BaseEffect._schema.fields;
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
_attachPartListeners(partId, htmlElement, options) {
|
|
||||||
super._attachPartListeners(partId, htmlElement, options);
|
|
||||||
const changeChoices = this.changeChoices;
|
|
||||||
|
|
||||||
htmlElement.querySelectorAll('.effect-change-input').forEach(element => {
|
|
||||||
autocomplete({
|
|
||||||
input: element,
|
|
||||||
fetch: function (text, update) {
|
|
||||||
if (!text) {
|
|
||||||
update(changeChoices);
|
|
||||||
} else {
|
|
||||||
text = text.toLowerCase();
|
|
||||||
var suggestions = changeChoices.filter(n => n.label.toLowerCase().includes(text));
|
|
||||||
update(suggestions);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function (item, search) {
|
|
||||||
const label = game.i18n.localize(item.label);
|
|
||||||
const matchIndex = label.toLowerCase().indexOf(search);
|
|
||||||
|
|
||||||
const beforeText = label.slice(0, matchIndex);
|
|
||||||
const matchText = label.slice(matchIndex, matchIndex + search.length);
|
|
||||||
const after = label.slice(matchIndex + search.length, label.length);
|
|
||||||
|
|
||||||
const element = document.createElement('li');
|
|
||||||
element.innerHTML =
|
|
||||||
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
|
|
||||||
' ',
|
|
||||||
' '
|
|
||||||
);
|
|
||||||
if (item.hint) {
|
|
||||||
element.dataset.tooltip = game.i18n.localize(item.hint);
|
|
||||||
}
|
|
||||||
|
|
||||||
return element;
|
|
||||||
},
|
|
||||||
renderGroup: function (label) {
|
|
||||||
const itemElement = document.createElement('div');
|
|
||||||
itemElement.textContent = game.i18n.localize(label);
|
|
||||||
return itemElement;
|
|
||||||
},
|
|
||||||
onSelect: function (item) {
|
|
||||||
element.value = `system.${item.value}`;
|
|
||||||
},
|
|
||||||
click: e => e.fetch(),
|
|
||||||
customize: function (_input, _inputRect, container) {
|
|
||||||
container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ;
|
|
||||||
},
|
|
||||||
minLength: 0
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async _preparePartContext(partId, context) {
|
|
||||||
if (partId in context.tabs) context.tab = context.tabs[partId];
|
|
||||||
switch (partId) {
|
|
||||||
case 'details':
|
|
||||||
context.statuses = CONFIG.statusEffects.map(s => ({ value: s.id, label: game.i18n.localize(s.name) }));
|
|
||||||
context.isActorEffect = false;
|
|
||||||
context.isItemEffect = true;
|
|
||||||
const useGeneric = game.settings.get(
|
|
||||||
CONFIG.DH.id,
|
|
||||||
CONFIG.DH.SETTINGS.gameSettings.appearance
|
|
||||||
).showGenericStatusEffects;
|
|
||||||
if (!useGeneric) {
|
|
||||||
context.statuses = [
|
|
||||||
...context.statuses,
|
|
||||||
Object.values(CONFIG.DH.GENERAL.conditions).map(status => ({
|
|
||||||
value: status.id,
|
|
||||||
label: game.i18n.localize(status.name)
|
|
||||||
}))
|
|
||||||
];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'changes':
|
|
||||||
context.modes = Object.entries(CONST.ACTIVE_EFFECT_MODES).reduce((modes, [key, value]) => {
|
|
||||||
modes[value] = game.i18n.localize(`EFFECT.MODE_${key}`);
|
|
||||||
return modes;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
context.priorities = ActiveEffectConfig.DEFAULT_PRIORITIES;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async #onSubmit(_event, _form, formData) {
|
|
||||||
this.data = foundry.utils.expandObject(formData.object);
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Edit a Document image.
|
|
||||||
* @this {DocumentSheetV2}
|
|
||||||
* @type {ApplicationClickAction}
|
|
||||||
*/
|
|
||||||
static async #editImage(_event, target) {
|
|
||||||
if (target.nodeName !== 'IMG') {
|
|
||||||
throw new Error('The editImage action is available only for IMG elements.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const attr = target.dataset.edit;
|
|
||||||
const current = foundry.utils.getProperty(this.effect, attr);
|
|
||||||
const fp = new FilePicker.implementation({
|
|
||||||
current,
|
|
||||||
type: 'image',
|
|
||||||
callback: path => (target.src = path),
|
|
||||||
position: {
|
|
||||||
top: this.position.top + 40,
|
|
||||||
left: this.position.left + 10
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await fp.browse();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a new change to the effect's changes array.
|
|
||||||
* @this {ActiveEffectConfig}
|
|
||||||
* @type {ApplicationClickAction}
|
|
||||||
*/
|
|
||||||
static async #addChange() {
|
|
||||||
const { changes, ...rest } = foundry.utils.expandObject(new FormDataExtended(this.form).object);
|
|
||||||
const updatedChanges = Object.values(changes ?? {});
|
|
||||||
updatedChanges.push({});
|
|
||||||
|
|
||||||
this.effect = { ...rest, changes: updatedChanges };
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a change from the effect's changes array.
|
|
||||||
* @this {ActiveEffectConfig}
|
|
||||||
* @type {ApplicationClickAction}
|
|
||||||
*/
|
|
||||||
static async #deleteChange(event) {
|
|
||||||
const submitData = foundry.utils.expandObject(new FormDataExtended(this.form).object);
|
|
||||||
const updatedChanges = Object.values(submitData.changes);
|
|
||||||
const row = event.target.closest('li');
|
|
||||||
const index = Number(row.dataset.index) || 0;
|
|
||||||
updatedChanges.splice(index, 1);
|
|
||||||
|
|
||||||
this.effect = { ...submitData, changes: updatedChanges };
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
static async configure(effect, options = {}) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const app = new this(effect, options);
|
|
||||||
app.addEventListener('close', () => resolve(app.data), { once: true });
|
|
||||||
app.render({ force: true });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -147,7 +147,7 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
|
||||||
const effectIndex = this.move.effects.findIndex(x => x.id === id);
|
const effectIndex = this.move.effects.findIndex(x => x.id === id);
|
||||||
const effect = this.move.effects[effectIndex];
|
const effect = this.move.effects[effectIndex];
|
||||||
const updatedEffect =
|
const updatedEffect =
|
||||||
await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect);
|
await game.system.api.applications.sheetConfigs.ActiveEffectConfig.configureSetting(effect);
|
||||||
if (!updatedEffect) return;
|
if (!updatedEffect) return;
|
||||||
|
|
||||||
await this.updateMove({
|
await this.updateMove({
|
||||||
|
|
@ -205,7 +205,7 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await this.updateMove({ [`${this.actionsPath}.-=${target.dataset.id}`]: null });
|
await this.updateMove({ [`${this.actionsPath}.${target.dataset.id}`]: _del });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
|
|
|
||||||
|
|
@ -67,9 +67,9 @@ export default function DHTokenConfigMixin(Base) {
|
||||||
changes.height = tokenSize;
|
changes.height = tokenSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deletions = { '-=actorId': null, '-=actorLink': null };
|
// const deletions = { actorId: _del };
|
||||||
const mergeOptions = { inplace: false, performDeletions: true };
|
// const mergeOptions = { inplace: false, performDeletions: true, actorLink: false };
|
||||||
this._preview.updateSource(mergeObject(changes, deletions, mergeOptions));
|
this._preview.updateSource(changes);
|
||||||
|
|
||||||
if (this._preview?.object?.destroyed === false) {
|
if (this._preview?.object?.destroyed === false) {
|
||||||
this._preview.object.initializeSources();
|
this._preview.object.initializeSources();
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import DHBaseActorSheet from '../api/base-actor.mjs';
|
import DHBaseActorSheet from '../api/base-actor.mjs';
|
||||||
import DhDeathMove from '../../dialogs/deathMove.mjs';
|
import DhDeathMove from '../../dialogs/deathMove.mjs';
|
||||||
import { abilities } from '../../../config/actorConfig.mjs';
|
|
||||||
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
|
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
|
||||||
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
|
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
|
||||||
import FilterMenu from '../../ux/filter-menu.mjs';
|
import FilterMenu from '../../ux/filter-menu.mjs';
|
||||||
import { getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs';
|
import { getArmorSources, getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs';
|
||||||
|
|
||||||
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
|
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
|
||||||
|
|
||||||
|
|
@ -35,7 +34,8 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
cancelBeastform: CharacterSheet.#cancelBeastform,
|
cancelBeastform: CharacterSheet.#cancelBeastform,
|
||||||
toggleResourceManagement: CharacterSheet.#toggleResourceManagement,
|
toggleResourceManagement: CharacterSheet.#toggleResourceManagement,
|
||||||
useDowntime: this.useDowntime,
|
useDowntime: this.useDowntime,
|
||||||
viewParty: CharacterSheet.#viewParty
|
viewParty: CharacterSheet.#viewParty,
|
||||||
|
toggleArmorMangement: CharacterSheet.#toggleArmorManagement
|
||||||
},
|
},
|
||||||
window: {
|
window: {
|
||||||
resizable: true,
|
resizable: true,
|
||||||
|
|
@ -639,12 +639,12 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateArmorMarks(event) {
|
async updateArmorMarks(event) {
|
||||||
const armor = this.document.system.armor;
|
const inputValue = Number(event.currentTarget.value);
|
||||||
if (!armor) return;
|
const { value, max } = this.document.system.armorScore;
|
||||||
|
const changeValue = Math.min(inputValue - value, max - value);
|
||||||
|
|
||||||
const maxMarks = this.document.system.armorScore;
|
event.currentTarget.value = inputValue < 0 ? 0 : value + changeValue;
|
||||||
const value = Math.min(Math.max(Number(event.currentTarget.value), 0), maxMarks);
|
this.document.system.updateArmorValue({ value: changeValue });
|
||||||
await armor.update({ 'system.marks.value': value });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
@ -720,35 +720,16 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
* Rolls an attribute check based on the clicked button's dataset attribute.
|
* Rolls an attribute check based on the clicked button's dataset attribute.
|
||||||
* @type {ApplicationClickAction}
|
* @type {ApplicationClickAction}
|
||||||
*/
|
*/
|
||||||
static async #rollAttribute(event, button) {
|
static async #rollAttribute(_event, button) {
|
||||||
const abilityLabel = game.i18n.localize(abilities[button.dataset.attribute].label);
|
const result = await this.document.rollTrait(button.dataset.attribute);
|
||||||
const config = {
|
|
||||||
event: event,
|
|
||||||
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`,
|
|
||||||
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
|
||||||
ability: abilityLabel
|
|
||||||
}),
|
|
||||||
effects: await game.system.api.data.actions.actionsTypes.base.getEffects(this.document),
|
|
||||||
roll: {
|
|
||||||
trait: button.dataset.attribute,
|
|
||||||
type: 'trait'
|
|
||||||
},
|
|
||||||
hasRoll: true,
|
|
||||||
actionType: 'action',
|
|
||||||
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`,
|
|
||||||
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
|
||||||
ability: abilityLabel
|
|
||||||
})
|
|
||||||
};
|
|
||||||
const result = await this.document.diceRoll(config);
|
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
/* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */
|
/* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */
|
||||||
const costResources =
|
const costResources =
|
||||||
result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) ||
|
result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) ||
|
||||||
{};
|
{};
|
||||||
config.resourceUpdates.addResources(costResources);
|
result.resourceUpdates.addResources(costResources);
|
||||||
await config.resourceUpdates.updateResources();
|
await result.resourceUpdates.updateResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: redo toggleEquipItem method
|
//TODO: redo toggleEquipItem method
|
||||||
|
|
@ -823,10 +804,13 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
* Toggles ArmorScore resource value.
|
* Toggles ArmorScore resource value.
|
||||||
* @type {ApplicationClickAction}
|
* @type {ApplicationClickAction}
|
||||||
*/
|
*/
|
||||||
static async #toggleArmor(_, button, element) {
|
static async #toggleArmor(_, button, _element) {
|
||||||
const ArmorValue = Number.parseInt(button.dataset.value);
|
const { value, max } = this.document.system.armorScore;
|
||||||
const newValue = this.document.system.armor.system.marks.value >= ArmorValue ? ArmorValue - 1 : ArmorValue;
|
const inputValue = Number.parseInt(button.dataset.value);
|
||||||
await this.document.system.armor.update({ 'system.marks.value': newValue });
|
const newValue = value >= inputValue ? inputValue - 1 : inputValue;
|
||||||
|
const changeValue = Math.min(newValue - value, max - value);
|
||||||
|
|
||||||
|
this.document.system.updateArmorValue({ value: changeValue });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -952,6 +936,99 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async #toggleArmorManagement(_event, target) {
|
||||||
|
const existingTooltip = document.body.querySelector('.locked-tooltip .armor-management-container');
|
||||||
|
if (existingTooltip) {
|
||||||
|
game.tooltip.dismissLockedTooltips();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const armorSources = getArmorSources(this.document)
|
||||||
|
.filter(s => !s.disabled)
|
||||||
|
.toReversed()
|
||||||
|
.map(({ name, document, data }) => ({
|
||||||
|
...data,
|
||||||
|
uuid: document.uuid,
|
||||||
|
name
|
||||||
|
}));
|
||||||
|
if (!armorSources.length) return;
|
||||||
|
|
||||||
|
const useResourcePips = game.settings.get(
|
||||||
|
CONFIG.DH.id,
|
||||||
|
CONFIG.DH.SETTINGS.gameSettings.appearance
|
||||||
|
).useResourcePips;
|
||||||
|
const html = document.createElement('div');
|
||||||
|
html.innerHTML = await foundry.applications.handlebars.renderTemplate(
|
||||||
|
`systems/daggerheart/templates/ui/tooltip/armorManagement.hbs`,
|
||||||
|
{
|
||||||
|
sources: armorSources,
|
||||||
|
useResourcePips
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
game.tooltip.dismissLockedTooltips();
|
||||||
|
game.tooltip.activate(target, {
|
||||||
|
html,
|
||||||
|
locked: true,
|
||||||
|
cssClass: 'bordered-tooltip',
|
||||||
|
direction: 'DOWN'
|
||||||
|
});
|
||||||
|
|
||||||
|
html.querySelectorAll('.armor-slot').forEach(element => {
|
||||||
|
element.addEventListener('click', CharacterSheet.armorSourcePipUpdate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async armorSourceInput(event) {
|
||||||
|
const effect = await foundry.utils.fromUuid(event.target.dataset.uuid);
|
||||||
|
const value = Math.max(Math.min(Number.parseInt(event.target.value), effect.system.armorData.max), 0);
|
||||||
|
event.target.value = value;
|
||||||
|
const progressBar = event.target.closest('.status-bar.armor-slots').querySelector('progress');
|
||||||
|
progressBar.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update specific armor source */
|
||||||
|
static async armorSourcePipUpdate(event) {
|
||||||
|
const target = event.target.closest('.armor-slot');
|
||||||
|
const { uuid, value } = target.dataset;
|
||||||
|
const document = await foundry.utils.fromUuid(uuid);
|
||||||
|
|
||||||
|
let inputValue = Number.parseInt(value);
|
||||||
|
let decreasing = false;
|
||||||
|
let newCurrent = 0;
|
||||||
|
|
||||||
|
if (document.type === 'armor') {
|
||||||
|
decreasing = document.system.armor.current >= inputValue;
|
||||||
|
newCurrent = decreasing ? inputValue - 1 : inputValue;
|
||||||
|
await document.update({ 'system.armor.current': newCurrent });
|
||||||
|
} else if (document.system.armorData) {
|
||||||
|
const { current } = document.system.armorData;
|
||||||
|
decreasing = current >= inputValue;
|
||||||
|
newCurrent = decreasing ? inputValue - 1 : inputValue;
|
||||||
|
|
||||||
|
const newChanges = document.system.changes.map(change => ({
|
||||||
|
...change,
|
||||||
|
value: change.type === 'armor' ? { ...change.value, current: newCurrent } : change.value
|
||||||
|
}));
|
||||||
|
|
||||||
|
await document.update({ 'system.changes': newChanges });
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = target.closest('.slot-bar');
|
||||||
|
for (const armorSlot of container.querySelectorAll('.armor-slot i')) {
|
||||||
|
const index = Number.parseInt(armorSlot.dataset.index);
|
||||||
|
if (decreasing && index >= newCurrent) {
|
||||||
|
armorSlot.classList.remove('fa-shield');
|
||||||
|
armorSlot.classList.add('fa-shield-halved');
|
||||||
|
} else if (!decreasing && index < newCurrent) {
|
||||||
|
armorSlot.classList.add('fa-shield');
|
||||||
|
armorSlot.classList.remove('fa-shield-halved');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static async #toggleResourceManagement(event, button) {
|
static async #toggleResourceManagement(event, button) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const existingTooltip = document.body.querySelector('.locked-tooltip .resource-management-container');
|
const existingTooltip = document.body.querySelector('.locked-tooltip .resource-management-container');
|
||||||
|
|
@ -985,7 +1062,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
);
|
);
|
||||||
|
|
||||||
const target = button.closest('.resource-section');
|
const target = button.closest('.resource-section');
|
||||||
|
|
||||||
game.tooltip.dismissLockedTooltips();
|
game.tooltip.dismissLockedTooltips();
|
||||||
game.tooltip.activate(target, {
|
game.tooltip.activate(target, {
|
||||||
html,
|
html,
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,7 @@ export default class Party extends DHBaseActorSheet {
|
||||||
refeshActions: Party.#refeshActions,
|
refeshActions: Party.#refeshActions,
|
||||||
triggerRest: Party.#triggerRest,
|
triggerRest: Party.#triggerRest,
|
||||||
tagTeamRoll: Party.#tagTeamRoll,
|
tagTeamRoll: Party.#tagTeamRoll,
|
||||||
groupRoll: Party.#groupRoll,
|
groupRoll: Party.#groupRoll
|
||||||
selectRefreshable: DaggerheartMenu.selectRefreshable,
|
|
||||||
refreshActors: DaggerheartMenu.refreshActors
|
|
||||||
},
|
},
|
||||||
dragDrop: [{ dragSelector: '[data-item-id]', dropSelector: null }]
|
dragDrop: [{ dragSelector: '[data-item-id]', dropSelector: null }]
|
||||||
};
|
};
|
||||||
|
|
@ -120,6 +118,7 @@ export default class Party extends DHBaseActorSheet {
|
||||||
secrets: this.document.isOwner,
|
secrets: this.document.isOwner,
|
||||||
relativeTo: this.document
|
relativeTo: this.document
|
||||||
});
|
});
|
||||||
|
context.tagTeamActive = Boolean(this.document.system.tagTeam.initiator);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -190,11 +189,14 @@ export default class Party extends DHBaseActorSheet {
|
||||||
* Toggles a armor slot resource value.
|
* Toggles a armor slot resource value.
|
||||||
* @type {ApplicationClickAction}
|
* @type {ApplicationClickAction}
|
||||||
*/
|
*/
|
||||||
static async #toggleArmorSlot(_, target, element) {
|
static async #toggleArmorSlot(_, target) {
|
||||||
const armorItem = await foundry.utils.fromUuid(target.dataset.itemUuid);
|
const actor = game.actors.get(target.dataset.actorId);
|
||||||
const armorValue = Number.parseInt(target.dataset.value);
|
const { value, max } = actor.system.armorScore;
|
||||||
const newValue = armorItem.system.marks.value >= armorValue ? armorValue - 1 : armorValue;
|
const inputValue = Number.parseInt(target.dataset.value);
|
||||||
await armorItem.update({ 'system.marks.value': newValue });
|
const newValue = value >= inputValue ? inputValue - 1 : inputValue;
|
||||||
|
const changeValue = Math.min(newValue - value, max - value);
|
||||||
|
|
||||||
|
await actor.system.updateArmorValue({ value: changeValue });
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,11 +257,7 @@ export default class Party extends DHBaseActorSheet {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #tagTeamRoll() {
|
static async #tagTeamRoll() {
|
||||||
new game.system.api.applications.dialogs.TagTeamDialog(
|
new game.system.api.applications.dialogs.TagTeamDialog(this.document).render({ force: true });
|
||||||
this.document.system.partyMembers.filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type))
|
|
||||||
).render({
|
|
||||||
force: true
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #groupRoll(_params) {
|
static async #groupRoll(_params) {
|
||||||
|
|
|
||||||
|
|
@ -72,19 +72,16 @@ const typeSettingsMap = {
|
||||||
*/
|
*/
|
||||||
export default function DHApplicationMixin(Base) {
|
export default function DHApplicationMixin(Base) {
|
||||||
class DHSheetV2 extends HandlebarsApplicationMixin(Base) {
|
class DHSheetV2 extends HandlebarsApplicationMixin(Base) {
|
||||||
|
#nonHeaderAttribution = ['environment', 'ancestry', 'community', 'domainCard'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {DHSheetV2Configuration} [options={}]
|
* @param {DHSheetV2Configuration} [options={}]
|
||||||
*/
|
*/
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
super(options);
|
super(options);
|
||||||
/**
|
|
||||||
* @type {foundry.applications.ux.DragDrop[]}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
this._dragDrop = this._createDragDropHandlers();
|
|
||||||
}
|
|
||||||
|
|
||||||
#nonHeaderAttribution = ['environment', 'ancestry', 'community', 'domainCard'];
|
this._setupDragDrop();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default options for the sheet.
|
* The default options for the sheet.
|
||||||
|
|
@ -177,7 +174,9 @@ export default function DHApplicationMixin(Base) {
|
||||||
/**@inheritdoc */
|
/**@inheritdoc */
|
||||||
_attachPartListeners(partId, htmlElement, options) {
|
_attachPartListeners(partId, htmlElement, options) {
|
||||||
super._attachPartListeners(partId, htmlElement, options);
|
super._attachPartListeners(partId, htmlElement, options);
|
||||||
this._dragDrop.forEach(d => d.bind(htmlElement));
|
|
||||||
|
/* Core dragDrop from ActorDocument is always only 1. Possible we could refactor our own */
|
||||||
|
if (Array.isArray(this._dragDrop)) this._dragDrop.forEach(d => d.bind(htmlElement));
|
||||||
|
|
||||||
// Handle delta inputs
|
// Handle delta inputs
|
||||||
for (const deltaInput of htmlElement.querySelectorAll('input[data-allow-delta]')) {
|
for (const deltaInput of htmlElement.querySelectorAll('input[data-allow-delta]')) {
|
||||||
|
|
@ -355,14 +354,19 @@ export default function DHApplicationMixin(Base) {
|
||||||
* @returns {foundry.applications.ux.DragDrop[]}
|
* @returns {foundry.applications.ux.DragDrop[]}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_createDragDropHandlers() {
|
_setupDragDrop() {
|
||||||
return this.options.dragDrop.map(d => {
|
if (this._dragDrop) {
|
||||||
d.callbacks = {
|
this._dragDrop.callbacks.dragStart = this._onDragStart;
|
||||||
dragstart: this._onDragStart.bind(this),
|
this._dragDrop.callback.drop = this._onDrop;
|
||||||
drop: this._onDrop.bind(this)
|
} else {
|
||||||
};
|
this._dragDrop = this.options.dragDrop.map(d => {
|
||||||
return new foundry.applications.ux.DragDrop.implementation(d);
|
d.callbacks = {
|
||||||
});
|
dragstart: this._onDragStart.bind(this),
|
||||||
|
drop: this._onDrop.bind(this)
|
||||||
|
};
|
||||||
|
return new foundry.applications.ux.DragDrop.implementation(d);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -499,7 +503,10 @@ export default function DHApplicationMixin(Base) {
|
||||||
icon: 'fa-solid fa-explosion',
|
icon: 'fa-solid fa-explosion',
|
||||||
condition: target => {
|
condition: target => {
|
||||||
const doc = getDocFromElementSync(target);
|
const doc = getDocFromElementSync(target);
|
||||||
return doc?.system?.attack?.damage.parts.length || doc?.damage?.parts.length;
|
return (
|
||||||
|
!foundry.utils.isEmpty(doc?.system?.attack?.damage.parts) ||
|
||||||
|
!foundry.utils.isEmpty(doc?.damage?.parts)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
callback: async (target, event) => {
|
callback: async (target, event) => {
|
||||||
const doc = await getDocFromElement(target),
|
const doc = await getDocFromElement(target),
|
||||||
|
|
@ -742,11 +749,13 @@ export default function DHApplicationMixin(Base) {
|
||||||
|
|
||||||
const cls =
|
const cls =
|
||||||
type === 'action' ? game.system.api.models.actions.actionsTypes.base : getDocumentClass(documentClass);
|
type === 'action' ? game.system.api.models.actions.actionsTypes.base : getDocumentClass(documentClass);
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
name: cls.defaultName({ type, parent }),
|
name: cls.defaultName({ type, parent }),
|
||||||
type,
|
type,
|
||||||
system: systemData
|
system: systemData
|
||||||
};
|
};
|
||||||
|
|
||||||
if (inVault) data['system.inVault'] = true;
|
if (inVault) data['system.inVault'] = true;
|
||||||
if (disabled) data.disabled = true;
|
if (disabled) data.disabled = true;
|
||||||
if (type === 'domainCard' && parent?.system.domains?.length) {
|
if (type === 'domainCard' && parent?.system.domains?.length) {
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
|
||||||
inactives: []
|
inactives: []
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const effect of this.actor.allApplicableEffects()) {
|
for (const effect of this.actor.allApplicableEffects({ noTransferArmor: true })) {
|
||||||
const list = effect.active ? context.effects.actives : context.effects.inactives;
|
const list = effect.active ? context.effects.actives : context.effects.inactives;
|
||||||
list.push(effect);
|
list.push(effect);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,15 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateArmorEffect(event) {
|
||||||
|
const value = Number.parseInt(event.target.value);
|
||||||
|
const armorEffect = this.document.system.armorEffect;
|
||||||
|
if (Number.isNaN(value) || !armorEffect) return;
|
||||||
|
|
||||||
|
await armorEffect.system.armorChange.updateArmorMax(value);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback function used by `tagifyElement`.
|
* Callback function used by `tagifyElement`.
|
||||||
* @param {Array<Object>} selectedOptions - The currently selected tag objects.
|
* @param {Array<Object>} selectedOptions - The currently selected tag objects.
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ export default class BeastformSheet extends DHBaseItemSheet {
|
||||||
|
|
||||||
async advantageOnRemove(event) {
|
async advantageOnRemove(event) {
|
||||||
await this.document.update({
|
await this.document.update({
|
||||||
[`system.advantageOn.-=${event.detail.data.value}`]: null
|
[`system.advantageOn.${event.detail.data.value}`]: _del
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,14 +108,15 @@ export default class DhRollTableSheet extends foundry.applications.sheets.RollTa
|
||||||
getSystemFlagUpdate() {
|
getSystemFlagUpdate() {
|
||||||
const deleteUpdate = Object.keys(this.document._source.flags.daggerheart?.altFormula ?? {}).reduce(
|
const deleteUpdate = Object.keys(this.document._source.flags.daggerheart?.altFormula ?? {}).reduce(
|
||||||
(acc, formulaKey) => {
|
(acc, formulaKey) => {
|
||||||
if (!this.daggerheartFlag.altFormula[formulaKey]) acc.altFormula[`-=${formulaKey}`] = null;
|
if (!this.daggerheartFlag.altFormula[formulaKey]) acc.altFormula[formulaKey] = _del;
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{ altFormula: {} }
|
{ altFormula: {} }
|
||||||
);
|
);
|
||||||
|
|
||||||
return { ['flags.daggerheart']: foundry.utils.mergeObject(this.daggerheartFlag.toObject(), deleteUpdate) };
|
const flagData = this.daggerheartFlag.toObject();
|
||||||
|
return { ...flagData, altFormula: { ...flagData.altFormula, ...deleteUpdate.altFormula } };
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #addFormula() {
|
static async #addFormula() {
|
||||||
|
|
@ -127,7 +128,7 @@ export default class DhRollTableSheet extends foundry.applications.sheets.RollTa
|
||||||
|
|
||||||
static async #removeFormula(_event, target) {
|
static async #removeFormula(_event, target) {
|
||||||
await this.daggerheartFlag.updateSource({
|
await this.daggerheartFlag.updateSource({
|
||||||
[`altFormula.-=${target.dataset.key}`]: null
|
[`altFormula.${target.dataset.key}`]: _del
|
||||||
});
|
});
|
||||||
this.render({ internalRefresh: true });
|
this.render({ internalRefresh: true });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,19 @@
|
||||||
export default class DhSidebar extends foundry.applications.sidebar.Sidebar {
|
export default class DhSidebar extends foundry.applications.sidebar.Sidebar {
|
||||||
|
static buildTabs() {
|
||||||
|
const { settings, ...tabs } = super.TABS;
|
||||||
|
return {
|
||||||
|
...tabs,
|
||||||
|
daggerheartMenu: {
|
||||||
|
tooltip: 'DAGGERHEART.UI.Sidebar.daggerheartMenu.title',
|
||||||
|
img: 'systems/daggerheart/assets/logos/FoundryBorneLogoWhite.svg',
|
||||||
|
gmOnly: true
|
||||||
|
},
|
||||||
|
settings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** @override */
|
/** @override */
|
||||||
static TABS = {
|
static TABS = DhSidebar.buildTabs();
|
||||||
chat: {
|
|
||||||
documentName: 'ChatMessage'
|
|
||||||
},
|
|
||||||
combat: {
|
|
||||||
documentName: 'Combat'
|
|
||||||
},
|
|
||||||
scenes: {
|
|
||||||
documentName: 'Scene',
|
|
||||||
gmOnly: true
|
|
||||||
},
|
|
||||||
actors: {
|
|
||||||
documentName: 'Actor'
|
|
||||||
},
|
|
||||||
items: {
|
|
||||||
documentName: 'Item'
|
|
||||||
},
|
|
||||||
journal: {
|
|
||||||
documentName: 'JournalEntry',
|
|
||||||
tooltip: 'SIDEBAR.TabJournal'
|
|
||||||
},
|
|
||||||
tables: {
|
|
||||||
documentName: 'RollTable'
|
|
||||||
},
|
|
||||||
cards: {
|
|
||||||
documentName: 'Cards'
|
|
||||||
},
|
|
||||||
macros: {
|
|
||||||
documentName: 'Macro'
|
|
||||||
},
|
|
||||||
playlists: {
|
|
||||||
documentName: 'Playlist'
|
|
||||||
},
|
|
||||||
compendium: {
|
|
||||||
tooltip: 'SIDEBAR.TabCompendium',
|
|
||||||
icon: 'fa-solid fa-book-atlas'
|
|
||||||
},
|
|
||||||
daggerheartMenu: {
|
|
||||||
tooltip: 'DAGGERHEART.UI.Sidebar.daggerheartMenu.title',
|
|
||||||
img: 'systems/daggerheart/assets/logos/FoundryBorneLogoWhite.svg',
|
|
||||||
gmOnly: true
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
tooltip: 'SIDEBAR.TabSettings',
|
|
||||||
icon: 'fa-solid fa-gears'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @override */
|
/** @override */
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,4 @@ export { default as DhFearTracker } from './fearTracker.mjs';
|
||||||
export { default as DhHotbar } from './hotbar.mjs';
|
export { default as DhHotbar } from './hotbar.mjs';
|
||||||
export { default as DhSceneNavigation } from './sceneNavigation.mjs';
|
export { default as DhSceneNavigation } from './sceneNavigation.mjs';
|
||||||
export { ItemBrowser } from './itemBrowser.mjs';
|
export { ItemBrowser } from './itemBrowser.mjs';
|
||||||
|
export { default as DhProgress } from './progress.mjs';
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,24 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
||||||
const target = event.target.closest('[data-die-index]');
|
const target = event.target.closest('[data-die-index]');
|
||||||
|
|
||||||
if (target.dataset.type === 'damage') {
|
if (target.dataset.type === 'damage') {
|
||||||
game.system.api.dice.DamageRoll.reroll(target, message);
|
const { damageType, part, dice, result } = target.dataset;
|
||||||
|
const damagePart = message.system.damage[damageType].parts[part];
|
||||||
|
const { parsedRoll, rerolledDice } = await game.system.api.dice.DamageRoll.reroll(damagePart, dice, result);
|
||||||
|
const damageParts = message.system.damage[damageType].parts.map((damagePart, index) => {
|
||||||
|
if (index !== Number(part)) return damagePart;
|
||||||
|
return {
|
||||||
|
...damagePart,
|
||||||
|
total: parsedRoll.total,
|
||||||
|
dice: rerolledDice
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const updateMessage = game.messages.get(message._id);
|
||||||
|
await updateMessage.update({
|
||||||
|
[`system.damage.${damageType}`]: {
|
||||||
|
total: parsedRoll.total,
|
||||||
|
parts: damageParts
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
let originalRoll_parsed = message.rolls.map(roll => JSON.parse(roll))[0];
|
let originalRoll_parsed = message.rolls.map(roll => JSON.parse(roll))[0];
|
||||||
const rollClass =
|
const rollClass =
|
||||||
|
|
@ -204,20 +221,16 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
||||||
|
|
||||||
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
|
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
|
||||||
|
|
||||||
const { newRoll, parsedRoll } = await rollClass.reroll(originalRoll_parsed, target, message);
|
const { newRoll, parsedRoll } = await rollClass.reroll(
|
||||||
|
originalRoll_parsed,
|
||||||
|
target.dataset.dieIndex,
|
||||||
|
target.dataset.type
|
||||||
|
);
|
||||||
|
|
||||||
await game.messages.get(message._id).update({
|
await game.messages.get(message._id).update({
|
||||||
'system.roll': newRoll,
|
'system.roll': newRoll,
|
||||||
'rolls': [parsedRoll]
|
'rolls': [parsedRoll]
|
||||||
});
|
});
|
||||||
|
|
||||||
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
|
|
||||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
|
||||||
action: socketEvent.Refresh,
|
|
||||||
data: {
|
|
||||||
refreshType: RefreshType.TagTeamRoll
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { AdversaryBPPerEncounter } from '../../config/encounterConfig.mjs';
|
import { AdversaryBPPerEncounter } from '../../config/encounterConfig.mjs';
|
||||||
|
import { expireActiveEffects } from '../../helpers/utils.mjs';
|
||||||
|
|
||||||
export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
|
export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
|
|
@ -177,6 +178,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
|
||||||
if (autoPoints) {
|
if (autoPoints) {
|
||||||
update.system.actionTokens = Math.max(combatant.system.actionTokens - 1, 0);
|
update.system.actionTokens = Math.max(combatant.system.actionTokens - 1, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (combatant.actor) expireActiveEffects(combatant.actor, [CONFIG.DH.GENERAL.activeEffectDurations.act.id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.viewed.update({
|
await this.viewed.update({
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,6 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.editingCountdowns.has(countdownId)) this.editingCountdowns.delete(countdownId);
|
if (this.editingCountdowns.has(countdownId)) this.editingCountdowns.delete(countdownId);
|
||||||
this.updateSetting({ [`countdowns.-=${countdownId}`]: null });
|
this.updateSetting({ [`countdowns.${countdownId}`]: _del });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,6 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
get element() {
|
|
||||||
return document.body.querySelector('.daggerheart.dh-style.countdowns');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**@inheritdoc */
|
/**@inheritdoc */
|
||||||
async _renderFrame(options) {
|
async _renderFrame(options) {
|
||||||
const frame = await super._renderFrame(options);
|
const frame = await super._renderFrame(options);
|
||||||
|
|
@ -68,6 +64,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
|
||||||
|
|
||||||
const header = frame.querySelector('.window-header');
|
const header = frame.querySelector('.window-header');
|
||||||
header.querySelector('button[data-action="close"]').remove();
|
header.querySelector('button[data-action="close"]').remove();
|
||||||
|
header.querySelector('button[data-action="toggleControls"]').remove();
|
||||||
|
|
||||||
if (game.user.isGM) {
|
if (game.user.isGM) {
|
||||||
const editTooltip = game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.editTitle');
|
const editTooltip = game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.editTitle');
|
||||||
|
|
@ -278,10 +275,8 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
|
||||||
return acc;
|
return acc;
|
||||||
}, {})
|
}, {})
|
||||||
};
|
};
|
||||||
await emitAsGM(GMUpdateEvent.UpdateCountdowns,
|
await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
|
||||||
DhCountdowns.gmSetSetting.bind(settings),
|
refreshType: RefreshType.Countdown
|
||||||
settings, null, {
|
|
||||||
refreshType: RefreshType.Countdown
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { getIconVisibleActiveEffects } from '../../helpers/utils.mjs';
|
||||||
import { RefreshType } from '../../systemRegistration/socket.mjs';
|
import { RefreshType } from '../../systemRegistration/socket.mjs';
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
@ -48,11 +49,9 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
|
||||||
|
|
||||||
_attachPartListeners(partId, htmlElement, options) {
|
_attachPartListeners(partId, htmlElement, options) {
|
||||||
super._attachPartListeners(partId, htmlElement, options);
|
super._attachPartListeners(partId, htmlElement, options);
|
||||||
|
for (const element of this.element?.querySelectorAll('.effect-container a') ?? []) {
|
||||||
if (this.element) {
|
element.addEventListener('click', e => this.#onClickEffect(e));
|
||||||
this.element.querySelectorAll('.effect-container a').forEach(element => {
|
element.addEventListener('contextmenu', e => this.#onClickEffect(e, -1));
|
||||||
element.addEventListener('contextmenu', this.removeEffect.bind(this));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,7 +71,7 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
|
||||||
? game.user.character
|
? game.user.character
|
||||||
: null
|
: null
|
||||||
: canvas.tokens.controlled[0].actor;
|
: canvas.tokens.controlled[0].actor;
|
||||||
return actor?.getActiveEffects() ?? [];
|
return getIconVisibleActiveEffects(actor?.getActiveEffects() ?? []);
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleHidden(token, focused) {
|
toggleHidden(token, focused) {
|
||||||
|
|
@ -86,11 +85,21 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeEffect(event) {
|
async #onClickEffect(event, delta = 1) {
|
||||||
const element = event.target.closest('.effect-container');
|
const element = event.target.closest('.effect-container');
|
||||||
const effects = DhEffectsDisplay.getTokenEffects();
|
const effects = DhEffectsDisplay.getTokenEffects();
|
||||||
const effect = effects.find(x => x.id === element.dataset.effectId);
|
const effect = effects.find(x => x.id === element.dataset.effectId);
|
||||||
await effect.delete();
|
if (!effect || (delta >= 0 && !effect.system.stacking)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxValue = effect.system.stacking?.max ?? Infinity;
|
||||||
|
const newValue = Math.clamp((effect.system.stacking?.value ?? 1) + delta, 0, maxValue);
|
||||||
|
if (newValue > 0) {
|
||||||
|
await effect.update({ 'system.stacking.value': newValue });
|
||||||
|
} else {
|
||||||
|
await effect.delete();
|
||||||
|
}
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
27
module/applications/ui/progress.mjs
Normal file
27
module/applications/ui/progress.mjs
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
export default class DhProgress {
|
||||||
|
#notification;
|
||||||
|
|
||||||
|
constructor({ max, label = '' }) {
|
||||||
|
this.max = max;
|
||||||
|
this.label = label;
|
||||||
|
this.#notification = ui.notifications.info(this.label, { progress: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMax(newMax) {
|
||||||
|
this.max = newMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
advance({ by = 1, label = this.label } = {}) {
|
||||||
|
if (this.value === this.max) return;
|
||||||
|
this.value = (this.value ?? 0) + Math.abs(by);
|
||||||
|
this.#notification.update({ message: label, pct: this.value / this.max });
|
||||||
|
}
|
||||||
|
|
||||||
|
close({ label = '' } = {}) {
|
||||||
|
this.#notification.update({ message: label, pct: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
static createMigrationProgress(max = 0) {
|
||||||
|
return new DhProgress({ max, label: game.i18n.localize('DAGGERHEART.UI.Progress.migrationLabel') });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,7 +31,7 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi
|
||||||
const environments = daggerheartInfo.sceneEnvironments.filter(
|
const environments = daggerheartInfo.sceneEnvironments.filter(
|
||||||
x => x && x.testUserPermission(game.user, 'LIMITED')
|
x => x && x.testUserPermission(game.user, 'LIMITED')
|
||||||
);
|
);
|
||||||
const hasEnvironments = environments.length > 0 && x.isView;
|
const hasEnvironments = environments.length > 0 && x.active;
|
||||||
return {
|
return {
|
||||||
...x,
|
...x,
|
||||||
hasEnvironments,
|
hasEnvironments,
|
||||||
|
|
@ -39,9 +39,10 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi
|
||||||
environments: environments
|
environments: environments
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
context.scenes.active = extendScenes(context.scenes.active);
|
context.scenes.active = extendScenes(context.scenes.active);
|
||||||
context.scenes.inactive = extendScenes(context.scenes.inactive);
|
context.scenes.inactive = extendScenes(context.scenes.inactive);
|
||||||
|
context.scenes.viewed = context.scenes.viewed ? extendScenes([context.scenes.viewed])[0] : null;
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,97 +1,4 @@
|
||||||
/**
|
|
||||||
* @typedef ContextMenuEntry
|
|
||||||
* @property {string} name The context menu label. Can be localized.
|
|
||||||
* @property {string} [icon] A string containing an HTML icon element for the menu item.
|
|
||||||
* @property {string} [classes] Additional CSS classes to apply to this menu item.
|
|
||||||
* @property {string} [group] An identifier for a group this entry belongs to.
|
|
||||||
* @property {ContextMenuJQueryCallback} callback The function to call when the menu item is clicked.
|
|
||||||
* @property {ContextMenuCondition|boolean} [condition] A function to call or boolean value to determine if this entry
|
|
||||||
* appears in the menu.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @callback ContextMenuCondition
|
|
||||||
* @param {jQuery|HTMLElement} html The element of the context menu entry.
|
|
||||||
* @returns {boolean} Whether the entry should be rendered in the context menu.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @callback ContextMenuCallback
|
|
||||||
* @param {HTMLElement} target The element that the context menu has been triggered for.
|
|
||||||
* @returns {unknown}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @callback ContextMenuJQueryCallback
|
|
||||||
* @param {HTMLElement|jQuery} target The element that the context menu has been triggered for. Will
|
|
||||||
* either be a jQuery object or an HTMLElement instance, depending
|
|
||||||
* on how the ContextMenu was configured.
|
|
||||||
* @returns {unknown}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef ContextMenuOptions
|
|
||||||
* @property {string} [eventName="contextmenu"] Optionally override the triggering event which can spawn the menu. If
|
|
||||||
* the menu is using fixed positioning, this event must be a MouseEvent.
|
|
||||||
* @property {ContextMenuCallback} [onOpen] A function to call when the context menu is opened.
|
|
||||||
* @property {ContextMenuCallback} [onClose] A function to call when the context menu is closed.
|
|
||||||
* @property {boolean} [fixed=false] If true, the context menu is given a fixed position rather than being
|
|
||||||
* injected into the target.
|
|
||||||
* @property {boolean} [jQuery=true] If true, callbacks will be passed jQuery objects instead of HTMLElement
|
|
||||||
* instances.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef ContextMenuRenderOptions
|
|
||||||
* @property {Event} [event] The event that triggered the context menu opening.
|
|
||||||
* @property {boolean} [animate=true] Animate the context menu opening.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A subclass of ContextMenu.
|
|
||||||
* @extends {foundry.applications.ux.ContextMenu}
|
|
||||||
*/
|
|
||||||
export default class DHContextMenu extends foundry.applications.ux.ContextMenu {
|
export default class DHContextMenu extends foundry.applications.ux.ContextMenu {
|
||||||
/**
|
|
||||||
* @param {HTMLElement|jQuery} container - The HTML element that contains the context menu targets.
|
|
||||||
* @param {string} selector - A CSS selector which activates the context menu.
|
|
||||||
* @param {ContextMenuEntry[]} menuItems - An Array of entries to display in the menu
|
|
||||||
* @param {ContextMenuOptions} [options] - Additional options to configure the context menu.
|
|
||||||
*/
|
|
||||||
constructor(container, selector, menuItems, options) {
|
|
||||||
super(container, selector, menuItems, options);
|
|
||||||
|
|
||||||
/** @deprecated since v13 until v15 */
|
|
||||||
this.#jQuery = options.jQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to pass jQuery objects or HTMLElement instances to callback.
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
#jQuery;
|
|
||||||
|
|
||||||
/**@inheritdoc */
|
|
||||||
activateListeners(menu) {
|
|
||||||
menu.addEventListener('click', this.#onClickItem.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle click events on context menu items.
|
|
||||||
* @param {PointerEvent} event The click event
|
|
||||||
*/
|
|
||||||
#onClickItem(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
const element = event.target.closest('.context-item');
|
|
||||||
if (!element) return;
|
|
||||||
const item = this.menuItems.find(i => i.element === element);
|
|
||||||
item?.callback(this.#jQuery ? $(this.target) : this.target, event);
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger a context menu event in response to a normal click on a additional options button.
|
* Trigger a context menu event in response to a normal click on a additional options button.
|
||||||
* @param {PointerEvent} event
|
* @param {PointerEvent} event
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export { default as DhMeasuredTemplate } from './measuredTemplate.mjs';
|
export { default as DhMeasuredTemplate } from './measuredTemplate.mjs';
|
||||||
export { default as DhRuler } from './ruler.mjs';
|
export { default as DhRuler } from './ruler.mjs';
|
||||||
export { default as DhTemplateLayer } from './templateLayer.mjs';
|
export { default as DhRegion } from './region.mjs';
|
||||||
|
export { default as DhRegionLayer } from './regionLayer.mjs';
|
||||||
export { default as DhTokenPlaceable } from './token.mjs';
|
export { default as DhTokenPlaceable } from './token.mjs';
|
||||||
export { default as DhTokenRuler } from './tokenRuler.mjs';
|
export { default as DhTokenRuler } from './tokenRuler.mjs';
|
||||||
|
|
|
||||||
12
module/canvas/placeables/region.mjs
Normal file
12
module/canvas/placeables/region.mjs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import DhMeasuredTemplate from './measuredTemplate.mjs';
|
||||||
|
|
||||||
|
export default class DhRegion extends foundry.canvas.placeables.Region {
|
||||||
|
/**@inheritdoc */
|
||||||
|
_formatMeasuredDistance(distance) {
|
||||||
|
const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement;
|
||||||
|
if (!range.enabled) return super._formatMeasuredDistance(distance);
|
||||||
|
|
||||||
|
const { distance: resultDistance, units } = DhMeasuredTemplate.getRangeLabels(distance, range);
|
||||||
|
return `${resultDistance} ${units}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
98
module/canvas/placeables/regionLayer.mjs
Normal file
98
module/canvas/placeables/regionLayer.mjs
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer {
|
||||||
|
static prepareSceneControls() {
|
||||||
|
const sc = foundry.applications.ui.SceneControls;
|
||||||
|
const { tools, ...rest } = super.prepareSceneControls();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
tools: {
|
||||||
|
select: tools.select,
|
||||||
|
templateMode: tools.templateMode,
|
||||||
|
rectangle: tools.rectangle,
|
||||||
|
circle: tools.circle,
|
||||||
|
ellipse: tools.ellipse,
|
||||||
|
cone: tools.cone,
|
||||||
|
inFront: {
|
||||||
|
name: 'inFront',
|
||||||
|
order: 7,
|
||||||
|
title: 'CONTROLS.inFront',
|
||||||
|
icon: 'fa-solid fa-eye',
|
||||||
|
toolclip: {
|
||||||
|
src: 'toolclips/tools/measure-cone.webm',
|
||||||
|
heading: 'CONTROLS.inFront',
|
||||||
|
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ring: { ...tools.ring, order: 8 },
|
||||||
|
line: { ...tools.line, order: 9 },
|
||||||
|
emanation: { ...tools.emanation, order: 10 },
|
||||||
|
polygon: { ...tools.polygon, order: 11 },
|
||||||
|
hole: { ...tools.hole, order: 12 },
|
||||||
|
snap: { ...tools.snap, order: 13 },
|
||||||
|
clear: { ...tools.clear, order: 14 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
|
_isCreationToolActive() {
|
||||||
|
return this.active && (game.activeTool === 'inFront' || game.activeTool in foundry.data.BaseShapeData.TYPES);
|
||||||
|
}
|
||||||
|
|
||||||
|
_createDragShapeData(event) {
|
||||||
|
const hole = ui.controls.controls[this.options.name].tools.hole?.active ?? false;
|
||||||
|
if (game.activeTool === 'inFront') return { type: 'cone', x: 0, y: 0, radius: 0, angle: 180, hole };
|
||||||
|
|
||||||
|
const shape = super._createDragShapeData(event);
|
||||||
|
const token =
|
||||||
|
shape?.type === 'emanation' && shape.base?.type === 'token'
|
||||||
|
? this.#findTokenInBounds(event.interactionData.origin)
|
||||||
|
: null;
|
||||||
|
if (token) {
|
||||||
|
shape.base.width = token.width;
|
||||||
|
shape.base.height = token.height;
|
||||||
|
event.interactionData.origin = token.getCenterPoint();
|
||||||
|
}
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
async placeRegion(data, options = {}) {
|
||||||
|
const preConfirm = ({ _event, document, _create, _options }) => {
|
||||||
|
const shape = document.shapes[0];
|
||||||
|
const isEmanation = shape.type === 'emanation';
|
||||||
|
if (isEmanation) {
|
||||||
|
const token = this.#findTokenInBounds(shape.base.origin);
|
||||||
|
if (!token) return options.preConfirm?.() ?? true;
|
||||||
|
const shapeData = shape.toObject();
|
||||||
|
document.updateSource({
|
||||||
|
shapes: [
|
||||||
|
{
|
||||||
|
...shapeData,
|
||||||
|
base: {
|
||||||
|
...shapeData.base,
|
||||||
|
height: token.height,
|
||||||
|
width: token.width,
|
||||||
|
x: token.x,
|
||||||
|
y: token.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return options?.preConfirm?.() ?? true;
|
||||||
|
};
|
||||||
|
|
||||||
|
super.placeRegion(data, { ...options, preConfirm });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Searches for token at origin point, returning null if there are no tokens or multiple overlapping tokens */
|
||||||
|
#findTokenInBounds(origin) {
|
||||||
|
const { x, y } = origin;
|
||||||
|
const gridSize = canvas.grid.size;
|
||||||
|
const inBounds = canvas.scene.tokens.filter(t => {
|
||||||
|
return x.between(t.x, t.x + t.width * gridSize) && y.between(t.y, t.y + t.height * gridSize);
|
||||||
|
});
|
||||||
|
return inBounds.length === 1 ? inBounds[0] : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
export default class DhTemplateLayer extends foundry.canvas.layers.TemplateLayer {
|
|
||||||
static prepareSceneControls() {
|
|
||||||
const sc = foundry.applications.ui.SceneControls;
|
|
||||||
return {
|
|
||||||
name: 'templates',
|
|
||||||
order: 2,
|
|
||||||
title: 'CONTROLS.GroupMeasure',
|
|
||||||
icon: 'fa-solid fa-ruler-combined',
|
|
||||||
visible: game.user.can('TEMPLATE_CREATE'),
|
|
||||||
onChange: (event, active) => {
|
|
||||||
if (active) canvas.templates.activate();
|
|
||||||
},
|
|
||||||
onToolChange: () => canvas.templates.setAllRenderFlags({ refreshState: true }),
|
|
||||||
tools: {
|
|
||||||
circle: {
|
|
||||||
name: 'circle',
|
|
||||||
order: 1,
|
|
||||||
title: 'CONTROLS.MeasureCircle',
|
|
||||||
icon: 'fa-regular fa-circle',
|
|
||||||
toolclip: {
|
|
||||||
src: 'toolclips/tools/measure-circle.webm',
|
|
||||||
heading: 'CONTROLS.MeasureCircle',
|
|
||||||
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete'])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cone: {
|
|
||||||
name: 'cone',
|
|
||||||
order: 2,
|
|
||||||
title: 'CONTROLS.MeasureCone',
|
|
||||||
icon: 'fa-solid fa-angle-left',
|
|
||||||
toolclip: {
|
|
||||||
src: 'toolclips/tools/measure-cone.webm',
|
|
||||||
heading: 'CONTROLS.MeasureCone',
|
|
||||||
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
inFront: {
|
|
||||||
name: 'inFront',
|
|
||||||
order: 3,
|
|
||||||
title: 'CONTROLS.inFront',
|
|
||||||
icon: 'fa-solid fa-eye',
|
|
||||||
toolclip: {
|
|
||||||
src: 'toolclips/tools/measure-cone.webm',
|
|
||||||
heading: 'CONTROLS.inFront',
|
|
||||||
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rect: {
|
|
||||||
name: 'rect',
|
|
||||||
order: 4,
|
|
||||||
title: 'CONTROLS.MeasureRect',
|
|
||||||
icon: 'fa-regular fa-square',
|
|
||||||
toolclip: {
|
|
||||||
src: 'toolclips/tools/measure-rect.webm',
|
|
||||||
heading: 'CONTROLS.MeasureRect',
|
|
||||||
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ray: {
|
|
||||||
name: 'ray',
|
|
||||||
order: 5,
|
|
||||||
title: 'CONTROLS.MeasureRay',
|
|
||||||
icon: 'fa-solid fa-up-down',
|
|
||||||
toolclip: {
|
|
||||||
src: 'toolclips/tools/measure-ray.webm',
|
|
||||||
heading: 'CONTROLS.MeasureRay',
|
|
||||||
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
clear: {
|
|
||||||
name: 'clear',
|
|
||||||
order: 6,
|
|
||||||
title: 'CONTROLS.MeasureClear',
|
|
||||||
icon: 'fa-solid fa-trash',
|
|
||||||
visible: game.user.isGM,
|
|
||||||
onChange: () => canvas.templates.deleteAll(),
|
|
||||||
button: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
activeTool: 'circle'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_onDragLeftStart(event) {
|
|
||||||
const interaction = event.interactionData;
|
|
||||||
|
|
||||||
// Snap the origin to the grid
|
|
||||||
if (!event.shiftKey) interaction.origin = this.getSnappedPoint(interaction.origin);
|
|
||||||
|
|
||||||
// Create a pending MeasuredTemplateDocument
|
|
||||||
const tool = game.activeTool === 'inFront' ? 'cone' : game.activeTool;
|
|
||||||
const previewData = {
|
|
||||||
user: game.user.id,
|
|
||||||
t: tool,
|
|
||||||
x: interaction.origin.x,
|
|
||||||
y: interaction.origin.y,
|
|
||||||
sort: Math.max(this.getMaxSort() + 1, 0),
|
|
||||||
distance: 1,
|
|
||||||
direction: 0,
|
|
||||||
fillColor: game.user.color || '#FF0000',
|
|
||||||
hidden: event.altKey
|
|
||||||
};
|
|
||||||
const defaults = CONFIG.MeasuredTemplate.defaults;
|
|
||||||
if (game.activeTool === 'cone') previewData.angle = defaults.angle;
|
|
||||||
else if (game.activeTool === 'inFront') previewData.angle = 180;
|
|
||||||
else if (game.activeTool === 'ray') previewData.width = defaults.width * canvas.dimensions.distance;
|
|
||||||
const cls = foundry.utils.getDocumentClass('MeasuredTemplate');
|
|
||||||
const doc = new cls(previewData, { parent: canvas.scene });
|
|
||||||
|
|
||||||
// Create a preview MeasuredTemplate object
|
|
||||||
const template = new this.constructor.placeableClass(doc);
|
|
||||||
doc._object = template;
|
|
||||||
interaction.preview = this.preview.addChild(template);
|
|
||||||
template.draw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { getIconVisibleActiveEffects } from '../../helpers/utils.mjs';
|
||||||
import DhMeasuredTemplate from './measuredTemplate.mjs';
|
import DhMeasuredTemplate from './measuredTemplate.mjs';
|
||||||
|
|
||||||
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
||||||
|
|
@ -20,7 +21,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
||||||
this.effects.overlay = null;
|
this.effects.overlay = null;
|
||||||
|
|
||||||
// Categorize effects
|
// Categorize effects
|
||||||
const activeEffects = this.actor?.getActiveEffects() ?? [];
|
const activeEffects = getIconVisibleActiveEffects(Array.from(this.actor?.allApplicableEffects() ?? []));
|
||||||
const overlayEffect = activeEffects.findLast(e => e.img && e.getFlag?.('core', 'overlay'));
|
const overlayEffect = activeEffects.findLast(e => e.img && e.getFlag?.('core', 'overlay'));
|
||||||
|
|
||||||
// Draw effects
|
// Draw effects
|
||||||
|
|
@ -29,8 +30,8 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
||||||
if (!effect.img) continue;
|
if (!effect.img) continue;
|
||||||
const promise =
|
const promise =
|
||||||
effect === overlayEffect
|
effect === overlayEffect
|
||||||
? this._drawOverlay(effect.img, effect.tint)
|
? this._drawOverlay(effect.img, effect.tint, effect)
|
||||||
: this._drawEffect(effect.img, effect.tint);
|
: this._drawEffect(effect.img, effect.tint, effect);
|
||||||
promises.push(
|
promises.push(
|
||||||
promise.then(e => {
|
promise.then(e => {
|
||||||
if (e) e.zIndex = i;
|
if (e) e.zIndex = i;
|
||||||
|
|
@ -44,6 +45,39 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
||||||
this.renderFlags.set({ refreshEffects: true });
|
this.renderFlags.set({ refreshEffects: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**@inheritdoc */
|
||||||
|
async _drawEffect(src, tint, effect) {
|
||||||
|
if (!src) return;
|
||||||
|
const tex = await foundry.canvas.loadTexture(src, { fallback: 'icons/svg/hazard.svg' });
|
||||||
|
const icon = new PIXI.Sprite(tex);
|
||||||
|
icon.tint = tint ?? 0xffffff;
|
||||||
|
|
||||||
|
if (effect.system.stacking?.value > 1) {
|
||||||
|
const stackOverlay = new PIXI.Text(effect.system.stacking.value, {
|
||||||
|
fill: '#f3c267',
|
||||||
|
stroke: '#000000',
|
||||||
|
fontSize: 96,
|
||||||
|
strokeThickness: 4
|
||||||
|
});
|
||||||
|
const nrDigits = Math.floor(Math.log10(effect.system.stacking.value)) + 1;
|
||||||
|
stackOverlay.y = -8;
|
||||||
|
/* This does not account for 1:s being much less wide than other digits. I don't think it's desired however as it makes it look jumpy */
|
||||||
|
stackOverlay.x = icon.width - 8 - nrDigits * 56;
|
||||||
|
stackOverlay.anchor.set(0, 0);
|
||||||
|
|
||||||
|
icon.addChild(stackOverlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.effects.addChild(icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _drawOverlay(src, tint, effect) {
|
||||||
|
const icon = await this._drawEffect(src, tint, effect);
|
||||||
|
if (icon) icon.alpha = 0.8;
|
||||||
|
this.effects.overlay = icon ?? null;
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the distance from this token to another token object.
|
* Returns the distance from this token to another token object.
|
||||||
* This value is corrected to handle alternate token sizes and other grid types
|
* This value is corrected to handle alternate token sizes and other grid types
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,12 @@ export const range = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* circle|cone|rect|ray used to be CONST.MEASURED_TEMPLATE_TYPES. Hardcoded for now */
|
||||||
export const templateTypes = {
|
export const templateTypes = {
|
||||||
...CONST.MEASURED_TEMPLATE_TYPES,
|
CIRCLE: 'circle',
|
||||||
|
CONE: 'cone',
|
||||||
|
RECTANGLE: 'rectangle',
|
||||||
|
LINE: 'line',
|
||||||
EMANATION: 'emanation',
|
EMANATION: 'emanation',
|
||||||
INFRONT: 'inFront'
|
INFRONT: 'inFront'
|
||||||
};
|
};
|
||||||
|
|
@ -241,8 +245,8 @@ export const defaultRestOptions = {
|
||||||
type: 'friendly'
|
type: 'friendly'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
parts: [
|
parts: {
|
||||||
{
|
hitPoints: {
|
||||||
applyTo: healingTypes.hitPoints.id,
|
applyTo: healingTypes.hitPoints.id,
|
||||||
value: {
|
value: {
|
||||||
custom: {
|
custom: {
|
||||||
|
|
@ -251,7 +255,7 @@ export const defaultRestOptions = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -275,8 +279,8 @@ export const defaultRestOptions = {
|
||||||
type: 'self'
|
type: 'self'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
parts: [
|
parts: {
|
||||||
{
|
stress: {
|
||||||
applyTo: healingTypes.stress.id,
|
applyTo: healingTypes.stress.id,
|
||||||
value: {
|
value: {
|
||||||
custom: {
|
custom: {
|
||||||
|
|
@ -285,7 +289,7 @@ export const defaultRestOptions = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -310,8 +314,8 @@ export const defaultRestOptions = {
|
||||||
type: 'friendly'
|
type: 'friendly'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
parts: [
|
parts: {
|
||||||
{
|
armor: {
|
||||||
applyTo: healingTypes.armor.id,
|
applyTo: healingTypes.armor.id,
|
||||||
value: {
|
value: {
|
||||||
custom: {
|
custom: {
|
||||||
|
|
@ -320,7 +324,7 @@ export const defaultRestOptions = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -344,8 +348,8 @@ export const defaultRestOptions = {
|
||||||
type: 'self'
|
type: 'self'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
parts: [
|
parts: {
|
||||||
{
|
hope: {
|
||||||
applyTo: healingTypes.hope.id,
|
applyTo: healingTypes.hope.id,
|
||||||
value: {
|
value: {
|
||||||
custom: {
|
custom: {
|
||||||
|
|
@ -354,7 +358,7 @@ export const defaultRestOptions = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
prepareWithFriends: {
|
prepareWithFriends: {
|
||||||
|
|
@ -368,8 +372,8 @@ export const defaultRestOptions = {
|
||||||
type: 'self'
|
type: 'self'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
parts: [
|
parts: {
|
||||||
{
|
hope: {
|
||||||
applyTo: healingTypes.hope.id,
|
applyTo: healingTypes.hope.id,
|
||||||
value: {
|
value: {
|
||||||
custom: {
|
custom: {
|
||||||
|
|
@ -378,7 +382,7 @@ export const defaultRestOptions = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -405,8 +409,8 @@ export const defaultRestOptions = {
|
||||||
type: 'friendly'
|
type: 'friendly'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
parts: [
|
parts: {
|
||||||
{
|
hitPoints: {
|
||||||
applyTo: healingTypes.hitPoints.id,
|
applyTo: healingTypes.hitPoints.id,
|
||||||
value: {
|
value: {
|
||||||
custom: {
|
custom: {
|
||||||
|
|
@ -415,7 +419,7 @@ export const defaultRestOptions = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -439,8 +443,8 @@ export const defaultRestOptions = {
|
||||||
type: 'self'
|
type: 'self'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
parts: [
|
parts: {
|
||||||
{
|
stress: {
|
||||||
applyTo: healingTypes.stress.id,
|
applyTo: healingTypes.stress.id,
|
||||||
value: {
|
value: {
|
||||||
custom: {
|
custom: {
|
||||||
|
|
@ -449,7 +453,7 @@ export const defaultRestOptions = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -474,8 +478,8 @@ export const defaultRestOptions = {
|
||||||
type: 'friendly'
|
type: 'friendly'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
parts: [
|
parts: {
|
||||||
{
|
armor: {
|
||||||
applyTo: healingTypes.armor.id,
|
applyTo: healingTypes.armor.id,
|
||||||
value: {
|
value: {
|
||||||
custom: {
|
custom: {
|
||||||
|
|
@ -484,7 +488,7 @@ export const defaultRestOptions = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -508,8 +512,8 @@ export const defaultRestOptions = {
|
||||||
type: 'self'
|
type: 'self'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
parts: [
|
parts: {
|
||||||
{
|
hope: {
|
||||||
applyTo: healingTypes.hope.id,
|
applyTo: healingTypes.hope.id,
|
||||||
value: {
|
value: {
|
||||||
custom: {
|
custom: {
|
||||||
|
|
@ -518,7 +522,7 @@ export const defaultRestOptions = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
prepareWithFriends: {
|
prepareWithFriends: {
|
||||||
|
|
@ -532,8 +536,8 @@ export const defaultRestOptions = {
|
||||||
type: 'self'
|
type: 'self'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
parts: [
|
parts: {
|
||||||
{
|
hope: {
|
||||||
applyTo: healingTypes.hope.id,
|
applyTo: healingTypes.hope.id,
|
||||||
value: {
|
value: {
|
||||||
custom: {
|
custom: {
|
||||||
|
|
@ -542,7 +546,7 @@ export const defaultRestOptions = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -954,3 +958,102 @@ export const sceneRangeMeasurementSetting = {
|
||||||
label: 'Custom'
|
label: 'Custom'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const tagTeamRollTypes = {
|
||||||
|
trait: {
|
||||||
|
id: 'trait',
|
||||||
|
label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.trait'
|
||||||
|
},
|
||||||
|
ability: {
|
||||||
|
id: 'ability',
|
||||||
|
label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.ability'
|
||||||
|
},
|
||||||
|
damageAbility: {
|
||||||
|
id: 'damageAbility',
|
||||||
|
label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.damageAbility'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const baseActiveEffectModes = {
|
||||||
|
custom: {
|
||||||
|
id: 'custom',
|
||||||
|
priority: 0,
|
||||||
|
label: 'EFFECT.CHANGES.TYPES.custom'
|
||||||
|
},
|
||||||
|
multiply: {
|
||||||
|
id: 'multiply',
|
||||||
|
priority: 10,
|
||||||
|
label: 'EFFECT.CHANGES.TYPES.multiply'
|
||||||
|
},
|
||||||
|
add: {
|
||||||
|
id: 'add',
|
||||||
|
priority: 20,
|
||||||
|
label: 'EFFECT.CHANGES.TYPES.add'
|
||||||
|
},
|
||||||
|
subtract: {
|
||||||
|
id: 'subtract',
|
||||||
|
priority: 20,
|
||||||
|
label: 'EFFECT.CHANGES.TYPES.subtract'
|
||||||
|
},
|
||||||
|
downgrade: {
|
||||||
|
id: 'downgrade',
|
||||||
|
priority: 30,
|
||||||
|
label: 'EFFECT.CHANGES.TYPES.downgrade'
|
||||||
|
},
|
||||||
|
upgrade: {
|
||||||
|
id: 'upgrade',
|
||||||
|
priority: 40,
|
||||||
|
label: 'EFFECT.CHANGES.TYPES.upgrade'
|
||||||
|
},
|
||||||
|
override: {
|
||||||
|
id: 'override',
|
||||||
|
priority: 50,
|
||||||
|
label: 'EFFECT.CHANGES.TYPES.override'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const activeEffectModes = {
|
||||||
|
armor: {
|
||||||
|
id: 'armor',
|
||||||
|
priority: 20,
|
||||||
|
label: 'TYPES.ActiveEffect.armor'
|
||||||
|
},
|
||||||
|
...baseActiveEffectModes
|
||||||
|
};
|
||||||
|
|
||||||
|
export const activeEffectArmorInteraction = {
|
||||||
|
none: { id: 'none', label: 'DAGGERHEART.CONFIG.ArmorInteraction.none.label' },
|
||||||
|
active: { id: 'active', label: 'DAGGERHEART.CONFIG.ArmorInteraction.active.label' },
|
||||||
|
inactive: { id: 'inactive', label: 'DAGGERHEART.CONFIG.ArmorInteraction.inactive.label' }
|
||||||
|
};
|
||||||
|
|
||||||
|
export const activeEffectDurations = {
|
||||||
|
temporary: {
|
||||||
|
id: 'temporary',
|
||||||
|
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.temporary'
|
||||||
|
},
|
||||||
|
act: {
|
||||||
|
id: 'act',
|
||||||
|
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.act'
|
||||||
|
},
|
||||||
|
scene: {
|
||||||
|
id: 'scene',
|
||||||
|
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.scene'
|
||||||
|
},
|
||||||
|
shortRest: {
|
||||||
|
id: 'shortRest',
|
||||||
|
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.shortRest'
|
||||||
|
},
|
||||||
|
longRest: {
|
||||||
|
id: 'longRest',
|
||||||
|
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.longRest'
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
id: 'session',
|
||||||
|
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.session'
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
id: 'custom',
|
||||||
|
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.custom'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export const hooksConfig = {
|
export const hooksConfig = {
|
||||||
effectDisplayToggle: 'DHEffectDisplayToggle',
|
effectDisplayToggle: 'DHEffectDisplayToggle',
|
||||||
lockedTooltipDismissed: 'DHLockedTooltipDismissed'
|
lockedTooltipDismissed: 'DHLockedTooltipDismissed',
|
||||||
|
tagTeamStart: 'DHTagTeamRollStart'
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ export const armorFeatures = {
|
||||||
type: 'hostile'
|
type: 'hostile'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
parts: [
|
parts: {
|
||||||
{
|
stress: {
|
||||||
applyTo: 'stress',
|
applyTo: 'stress',
|
||||||
value: {
|
value: {
|
||||||
custom: {
|
custom: {
|
||||||
|
|
@ -24,7 +24,7 @@ export const armorFeatures = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -489,15 +489,18 @@ export const weaponFeatures = {
|
||||||
description: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.effects.barrier.description',
|
description: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.effects.barrier.description',
|
||||||
img: 'icons/skills/melee/shield-block-bash-blue.webp',
|
img: 'icons/skills/melee/shield-block-bash-blue.webp',
|
||||||
changes: [
|
changes: [
|
||||||
{
|
|
||||||
key: 'system.armorScore',
|
|
||||||
mode: 2,
|
|
||||||
value: 'ITEM.@system.tier + 1'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'system.evasion',
|
key: 'system.evasion',
|
||||||
mode: 2,
|
mode: 2,
|
||||||
value: '-1'
|
value: '-1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Armor',
|
||||||
|
type: 'armor',
|
||||||
|
typeData: {
|
||||||
|
type: 'armor',
|
||||||
|
max: 'ITEM.@system.tier + 1'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -732,8 +735,8 @@ export const weaponFeatures = {
|
||||||
type: 'hostile'
|
type: 'hostile'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
parts: [
|
parts: {
|
||||||
{
|
stress: {
|
||||||
applyTo: 'stress',
|
applyTo: 'stress',
|
||||||
value: {
|
value: {
|
||||||
custom: {
|
custom: {
|
||||||
|
|
@ -742,7 +745,7 @@ export const weaponFeatures = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -789,11 +792,6 @@ export const weaponFeatures = {
|
||||||
description: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.description',
|
description: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.description',
|
||||||
img: 'icons/skills/melee/sword-shield-stylized-white.webp',
|
img: 'icons/skills/melee/sword-shield-stylized-white.webp',
|
||||||
changes: [
|
changes: [
|
||||||
{
|
|
||||||
key: 'system.armorScore',
|
|
||||||
mode: 2,
|
|
||||||
value: '1'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'system.bonuses.damage.primaryWeapon.bonus',
|
key: 'system.bonuses.damage.primaryWeapon.bonus',
|
||||||
mode: 2,
|
mode: 2,
|
||||||
|
|
@ -808,6 +806,22 @@ export const weaponFeatures = {
|
||||||
type: 'withinRange'
|
type: 'withinRange'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.name',
|
||||||
|
description: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.description',
|
||||||
|
img: 'icons/skills/melee/sword-shield-stylized-white.webp',
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
key: 'Armor',
|
||||||
|
type: 'armor',
|
||||||
|
value: 0,
|
||||||
|
typeData: {
|
||||||
|
type: 'armor',
|
||||||
|
max: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -914,8 +928,8 @@ export const weaponFeatures = {
|
||||||
type: 'self'
|
type: 'self'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
parts: [
|
parts: {
|
||||||
{
|
hitPoints: {
|
||||||
applyTo: 'hitPoints',
|
applyTo: 'hitPoints',
|
||||||
value: {
|
value: {
|
||||||
custom: {
|
custom: {
|
||||||
|
|
@ -924,7 +938,7 @@ export const weaponFeatures = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -1191,9 +1205,13 @@ export const weaponFeatures = {
|
||||||
img: 'icons/skills/melee/shield-block-gray-orange.webp',
|
img: 'icons/skills/melee/shield-block-gray-orange.webp',
|
||||||
changes: [
|
changes: [
|
||||||
{
|
{
|
||||||
key: 'system.armorScore',
|
key: 'Armor',
|
||||||
mode: 2,
|
type: 'armor',
|
||||||
value: 'ITEM.@system.tier'
|
value: 0,
|
||||||
|
typeData: {
|
||||||
|
type: 'armor',
|
||||||
|
max: 'ITEM.@system.tier'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
export const keybindings = {
|
||||||
|
spotlight: 'DHSpotlight'
|
||||||
|
};
|
||||||
|
|
||||||
export const menu = {
|
export const menu = {
|
||||||
Automation: {
|
Automation: {
|
||||||
Name: 'GameSettingsAutomation',
|
Name: 'GameSettingsAutomation',
|
||||||
|
|
@ -34,7 +38,6 @@ export const gameSettings = {
|
||||||
LevelTiers: 'LevelTiers',
|
LevelTiers: 'LevelTiers',
|
||||||
Countdowns: 'Countdowns',
|
Countdowns: 'Countdowns',
|
||||||
LastMigrationVersion: 'LastMigrationVersion',
|
LastMigrationVersion: 'LastMigrationVersion',
|
||||||
TagTeamRoll: 'TagTeamRoll',
|
|
||||||
SpotlightRequestQueue: 'SpotlightRequestQueue',
|
SpotlightRequestQueue: 'SpotlightRequestQueue',
|
||||||
CompendiumBrowserSettings: 'CompendiumBrowserSettings'
|
CompendiumBrowserSettings: 'CompendiumBrowserSettings'
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
export { default as DhCombat } from './combat.mjs';
|
export { default as DhCombat } from './combat.mjs';
|
||||||
export { default as DhCombatant } from './combatant.mjs';
|
export { default as DhCombatant } from './combatant.mjs';
|
||||||
export { default as DhTagTeamRoll } from './tagTeamRoll.mjs';
|
|
||||||
export { default as DhRollTable } from './rollTable.mjs';
|
export { default as DhRollTable } from './rollTable.mjs';
|
||||||
export { default as RegisteredTriggers } from './registeredTriggers.mjs';
|
export { default as RegisteredTriggers } from './registeredTriggers.mjs';
|
||||||
export { default as CompendiumBrowserSettings } from './compendiumBrowserSettings.mjs';
|
export { default as CompendiumBrowserSettings } from './compendiumBrowserSettings.mjs';
|
||||||
|
export { default as TagTeamData } from './tagTeamData.mjs';
|
||||||
|
|
||||||
export * as countdowns from './countdowns.mjs';
|
export * as countdowns from './countdowns.mjs';
|
||||||
export * as actions from './action/_module.mjs';
|
export * as actions from './action/_module.mjs';
|
||||||
|
|
|
||||||
|
|
@ -26,23 +26,23 @@ export default class DHAttackAction extends DHDamageAction {
|
||||||
return {
|
return {
|
||||||
value: {
|
value: {
|
||||||
multiplier: 'prof',
|
multiplier: 'prof',
|
||||||
dice: this.item?.system?.attack.damage.parts[0].value.dice,
|
dice: this.item?.system?.attack.damage.parts.hitPoints.value.dice,
|
||||||
bonus: this.item?.system?.attack.damage.parts[0].value.bonus ?? 0
|
bonus: this.item?.system?.attack.damage.parts.hitPoints.value.bonus ?? 0
|
||||||
},
|
},
|
||||||
type: this.item?.system?.attack.damage.parts[0].type,
|
type: this.item?.system?.attack.damage.parts.hitPoints.type,
|
||||||
base: true
|
base: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
get damageFormula() {
|
get damageFormula() {
|
||||||
const hitPointsPart = this.damage.parts.find(x => x.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
|
const hitPointsPart = this.damage.parts.hitPoints;
|
||||||
if (!hitPointsPart) return '0';
|
if (!hitPointsPart) return '0';
|
||||||
|
|
||||||
return hitPointsPart.value.getFormula();
|
return hitPointsPart.value.getFormula();
|
||||||
}
|
}
|
||||||
|
|
||||||
get altDamageFormula() {
|
get altDamageFormula() {
|
||||||
const hitPointsPart = this.damage.parts.find(x => x.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
|
const hitPointsPart = this.damage.parts.hitPoints;
|
||||||
if (!hitPointsPart) return '0';
|
if (!hitPointsPart) return '0';
|
||||||
|
|
||||||
return hitPointsPart.valueAlt.getFormula();
|
return hitPointsPart.valueAlt.getFormula();
|
||||||
|
|
@ -50,9 +50,8 @@ export default class DHAttackAction extends DHDamageAction {
|
||||||
|
|
||||||
async use(event, options) {
|
async use(event, options) {
|
||||||
const result = await super.use(event, options);
|
const result = await super.use(event, options);
|
||||||
if (!result.message) return;
|
|
||||||
|
|
||||||
if (result.message.system.action.roll?.type === 'attack') {
|
if (result.message?.system.action.roll?.type === 'attack') {
|
||||||
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
|
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
|
||||||
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.characterAttack.id);
|
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.characterAttack.id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -207,10 +207,10 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
* @param {Event} event Event from the button used to trigger the Action
|
* @param {Event} event Event from the button used to trigger the Action
|
||||||
* @returns {object}
|
* @returns {object}
|
||||||
*/
|
*/
|
||||||
async use(event) {
|
async use(event, configOptions = {}) {
|
||||||
if (!this.actor) throw new Error("An Action can't be used outside of an Actor context.");
|
if (!this.actor) throw new Error("An Action can't be used outside of an Actor context.");
|
||||||
|
|
||||||
let config = this.prepareConfig(event);
|
let config = this.prepareConfig(event, configOptions);
|
||||||
if (!config) return;
|
if (!config) return;
|
||||||
|
|
||||||
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(this.actor, this.item);
|
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(this.actor, this.item);
|
||||||
|
|
@ -231,7 +231,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
|
|
||||||
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;
|
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;
|
||||||
|
|
||||||
if (this.chatDisplay && !config.actionChatMessageHandled) await this.toChat();
|
if (this.chatDisplay && !config.skips.createMessage && !config.actionChatMessageHandled) await this.toChat();
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
@ -241,7 +241,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
* @param {Event} event Event from the button used to trigger the Action
|
* @param {Event} event Event from the button used to trigger the Action
|
||||||
* @returns {object}
|
* @returns {object}
|
||||||
*/
|
*/
|
||||||
prepareBaseConfig(event) {
|
prepareBaseConfig(event, configOptions = {}) {
|
||||||
const isActor = this.item instanceof CONFIG.Actor.documentClass;
|
const isActor = this.item instanceof CONFIG.Actor.documentClass;
|
||||||
const actionTitle = game.i18n.localize(this.name);
|
const actionTitle = game.i18n.localize(this.name);
|
||||||
const itemTitle = isActor || this.item.name === actionTitle ? '' : `${this.item.name} - `;
|
const itemTitle = isActor || this.item.name === actionTitle ? '' : `${this.item.name} - `;
|
||||||
|
|
@ -264,11 +264,20 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
hasSave: this.hasSave,
|
hasSave: this.hasSave,
|
||||||
onSave: this.save?.damageMod,
|
onSave: this.save?.damageMod,
|
||||||
isDirect: !!this.damage?.direct,
|
isDirect: !!this.damage?.direct,
|
||||||
selectedRollMode: game.settings.get('core', 'rollMode'),
|
selectedMessageMode: game.settings.get('core', 'messageMode'),
|
||||||
data: this.getRollData(),
|
data: this.getRollData(),
|
||||||
evaluate: this.hasRoll,
|
evaluate: this.hasRoll,
|
||||||
resourceUpdates: new ResourceUpdateMap(this.actor),
|
resourceUpdates: new ResourceUpdateMap(this.actor),
|
||||||
targetUuid: this.targetUuid
|
targetUuid: this.targetUuid,
|
||||||
|
...configOptions,
|
||||||
|
skips: {
|
||||||
|
resources: false,
|
||||||
|
triggers: false,
|
||||||
|
createMessage: false,
|
||||||
|
updateCountdowns: false,
|
||||||
|
reaction: false,
|
||||||
|
...(configOptions.skips ?? {})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
DHBaseAction.applyKeybindings(config);
|
DHBaseAction.applyKeybindings(config);
|
||||||
|
|
@ -280,8 +289,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
* @param {Event} event Event from the button used to trigger the Action
|
* @param {Event} event Event from the button used to trigger the Action
|
||||||
* @returns {object}
|
* @returns {object}
|
||||||
*/
|
*/
|
||||||
prepareConfig(event) {
|
prepareConfig(event, configOptions = {}) {
|
||||||
const config = this.prepareBaseConfig(event);
|
const config = this.prepareBaseConfig(event, configOptions);
|
||||||
for (const clsField of Object.values(this.schema.fields)) {
|
for (const clsField of Object.values(this.schema.fields)) {
|
||||||
if (clsField?.prepareConfig) if (clsField.prepareConfig.call(this, config) === false) return false;
|
if (clsField?.prepareConfig) if (clsField.prepareConfig.call(this, config) === false) return false;
|
||||||
}
|
}
|
||||||
|
|
@ -297,17 +306,19 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
static async getEffects(actor, effectParent) {
|
static async getEffects(actor, effectParent) {
|
||||||
if (!actor) return [];
|
if (!actor) return [];
|
||||||
|
|
||||||
return Array.from(await actor.allApplicableEffects()).filter(effect => {
|
return Array.from(await actor.allApplicableEffects({ noTransferArmor: true, noSelfArmor: true })).filter(
|
||||||
/* Effects on weapons only ever apply for the weapon itself */
|
effect => {
|
||||||
if (effect.parent.type === 'weapon') {
|
/* Effects on weapons only ever apply for the weapon itself */
|
||||||
/* Unless they're secondary - then they apply only to other primary weapons */
|
if (effect.parent.type === 'weapon') {
|
||||||
if (effect.parent.system.secondary) {
|
/* Unless they're secondary - then they apply only to other primary weapons */
|
||||||
if (effectParent?.type !== 'weapon' || effectParent?.system.secondary) return false;
|
if (effect.parent.system.secondary) {
|
||||||
} else if (effectParent?.id !== effect.parent.id) return false;
|
if (effectParent?.type !== 'weapon' || effectParent?.system.secondary) return false;
|
||||||
}
|
} else if (effectParent?.id !== effect.parent.id) return false;
|
||||||
|
}
|
||||||
|
|
||||||
return !effect.isSuppressed;
|
return !effect.isSuppressed;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -326,6 +337,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
* @param {boolean} successCost
|
* @param {boolean} successCost
|
||||||
*/
|
*/
|
||||||
async consume(config, successCost = false) {
|
async consume(config, successCost = false) {
|
||||||
|
config.resourceUpdates = new ResourceUpdateMap(config.actionActor);
|
||||||
await this.workflow.get('cost')?.execute(config, successCost);
|
await this.workflow.get('cost')?.execute(config, successCost);
|
||||||
await this.workflow.get('uses')?.execute(config, successCost);
|
await this.workflow.get('uses')?.execute(config, successCost);
|
||||||
|
|
||||||
|
|
@ -354,11 +366,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasDamage() {
|
get hasDamage() {
|
||||||
return this.damage?.parts?.length && this.type !== 'healing';
|
return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type !== 'healing';
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasHealing() {
|
get hasHealing() {
|
||||||
return this.damage?.parts?.length && this.type === 'healing';
|
return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type === 'healing';
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasSave() {
|
get hasSave() {
|
||||||
|
|
@ -378,6 +390,15 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
|
|
||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static migrateData(source) {
|
||||||
|
if (source.damage?.parts && Array.isArray(source.damage.parts)) {
|
||||||
|
source.damage.parts = source.damage.parts.reduce((acc, part) => {
|
||||||
|
acc[part.applyTo] = part;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ResourceUpdateMap extends Map {
|
export class ResourceUpdateMap extends Map {
|
||||||
|
|
|
||||||
|
|
@ -2,84 +2,4 @@ import DHBaseAction from './baseAction.mjs';
|
||||||
|
|
||||||
export default class DhBeastformAction extends DHBaseAction {
|
export default class DhBeastformAction extends DHBaseAction {
|
||||||
static extraSchemas = [...super.extraSchemas, 'beastform'];
|
static extraSchemas = [...super.extraSchemas, 'beastform'];
|
||||||
|
|
||||||
/* async use(event, options) {
|
|
||||||
const beastformConfig = this.prepareBeastformConfig();
|
|
||||||
|
|
||||||
const abort = await this.handleActiveTransformations();
|
|
||||||
if (abort) return;
|
|
||||||
|
|
||||||
const calcCosts = game.system.api.fields.ActionFields.CostField.calcCosts.call(this, this.cost);
|
|
||||||
const hasCost = game.system.api.fields.ActionFields.CostField.hasCost.call(this, calcCosts);
|
|
||||||
if (!hasCost) {
|
|
||||||
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.insufficientResources'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { selected, evolved, hybrid } = await BeastformDialog.configure(beastformConfig, this.item);
|
|
||||||
if (!selected) return;
|
|
||||||
|
|
||||||
const result = await super.use(event, options);
|
|
||||||
if (!result) return;
|
|
||||||
|
|
||||||
await this.transform(selected, evolved, hybrid);
|
|
||||||
}
|
|
||||||
|
|
||||||
prepareBeastformConfig(config) {
|
|
||||||
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
|
|
||||||
const actorLevel = this.actor.system.levelData.level.current;
|
|
||||||
const actorTier =
|
|
||||||
Object.values(settingsTiers).find(
|
|
||||||
tier => actorLevel >= tier.levels.start && actorLevel <= tier.levels.end
|
|
||||||
) ?? 1;
|
|
||||||
|
|
||||||
return {
|
|
||||||
tierLimit: this.beastform.tierAccess.exact ?? actorTier
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async transform(selectedForm, evolvedData, hybridData) {
|
|
||||||
const formData = evolvedData?.form ? evolvedData.form.toObject() : selectedForm.toObject();
|
|
||||||
const beastformEffect = formData.effects.find(x => x.type === 'beastform');
|
|
||||||
if (!beastformEffect) {
|
|
||||||
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (evolvedData?.form) {
|
|
||||||
const evolvedForm = selectedForm.effects.find(x => x.type === 'beastform');
|
|
||||||
if (!evolvedForm) {
|
|
||||||
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
beastformEffect.changes = [...beastformEffect.changes, ...evolvedForm.changes];
|
|
||||||
formData.system.features = [...formData.system.features, ...selectedForm.system.features.map(x => x.uuid)];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedForm.system.beastformType === CONFIG.DH.ITEM.beastformTypes.hybrid.id) {
|
|
||||||
formData.system.advantageOn = Object.values(hybridData.advantages).reduce((advantages, formCategory) => {
|
|
||||||
Object.keys(formCategory).forEach(advantageKey => {
|
|
||||||
advantages[advantageKey] = formCategory[advantageKey];
|
|
||||||
});
|
|
||||||
return advantages;
|
|
||||||
}, {});
|
|
||||||
formData.system.features = [
|
|
||||||
...formData.system.features,
|
|
||||||
...Object.values(hybridData.features).flatMap(x => Object.keys(x))
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.actor.createEmbeddedDocuments('Item', [formData]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleActiveTransformations() {
|
|
||||||
const beastformEffects = this.actor.effects.filter(x => x.type === 'beastform');
|
|
||||||
const existingEffects = beastformEffects.length > 0;
|
|
||||||
await this.actor.deleteEmbeddedDocuments(
|
|
||||||
'ActiveEffect',
|
|
||||||
beastformEffects.map(x => x.id)
|
|
||||||
);
|
|
||||||
return existingEffects;
|
|
||||||
} */
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import BaseEffect from './baseEffect.mjs';
|
import BaseEffect from './baseEffect.mjs';
|
||||||
import BeastformEffect from './beastformEffect.mjs';
|
import BeastformEffect from './beastformEffect.mjs';
|
||||||
import HordeEffect from './hordeEffect.mjs';
|
import HordeEffect from './hordeEffect.mjs';
|
||||||
|
export { changeTypes, changeEffects } from './changeTypes/_module.mjs';
|
||||||
|
|
||||||
export { BaseEffect, BeastformEffect, HordeEffect };
|
export { BaseEffect, BeastformEffect, HordeEffect };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,50 @@
|
||||||
* "Anything that uses another data model value as its value": +1 - Effects that increase traits have to be calculated first at Base priority. (EX: Raise evasion by half your agility)
|
* "Anything that uses another data model value as its value": +1 - Effects that increase traits have to be calculated first at Base priority. (EX: Raise evasion by half your agility)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default class BaseEffect extends foundry.abstract.TypeDataModel {
|
import { getScrollTextData } from '../../helpers/utils.mjs';
|
||||||
|
import { changeTypes } from './_module.mjs';
|
||||||
|
|
||||||
|
export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
const baseChanges = Object.keys(CONFIG.DH.GENERAL.baseActiveEffectModes).reduce((r, type) => {
|
||||||
|
r[type] = new fields.SchemaField({
|
||||||
|
key: new fields.StringField({ required: true }),
|
||||||
|
type: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
choices: [type],
|
||||||
|
initial: type,
|
||||||
|
validate: BaseEffect.#validateType
|
||||||
|
}),
|
||||||
|
value: new fields.AnyField({
|
||||||
|
required: true,
|
||||||
|
nullable: true,
|
||||||
|
serializable: true,
|
||||||
|
initial: ''
|
||||||
|
}),
|
||||||
|
phase: new fields.StringField({ required: true, blank: false, initial: 'initial' }),
|
||||||
|
priority: new fields.NumberField()
|
||||||
|
});
|
||||||
|
return r;
|
||||||
|
}, {});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
...super.defineSchema(),
|
||||||
|
changes: new fields.ArrayField(
|
||||||
|
new fields.TypedSchemaField(
|
||||||
|
{ ...changeTypes, ...baseChanges },
|
||||||
|
{ initial: baseChanges.add.getInitialValue() }
|
||||||
|
)
|
||||||
|
),
|
||||||
|
duration: new fields.SchemaField({
|
||||||
|
type: new fields.StringField({
|
||||||
|
choices: CONFIG.DH.GENERAL.activeEffectDurations,
|
||||||
|
blank: true,
|
||||||
|
label: 'DAGGERHEART.GENERAL.type'
|
||||||
|
}),
|
||||||
|
description: new fields.HTMLField({ label: 'DAGGERHEART.GENERAL.description' })
|
||||||
|
}),
|
||||||
rangeDependence: new fields.SchemaField({
|
rangeDependence: new fields.SchemaField({
|
||||||
enabled: new fields.BooleanField({
|
enabled: new fields.BooleanField({
|
||||||
required: true,
|
required: true,
|
||||||
|
|
@ -41,10 +80,57 @@ export default class BaseEffect extends foundry.abstract.TypeDataModel {
|
||||||
initial: CONFIG.DH.GENERAL.range.melee.id,
|
initial: CONFIG.DH.GENERAL.range.melee.id,
|
||||||
label: 'DAGGERHEART.GENERAL.range'
|
label: 'DAGGERHEART.GENERAL.range'
|
||||||
})
|
})
|
||||||
})
|
}),
|
||||||
|
stacking: new fields.SchemaField(
|
||||||
|
{
|
||||||
|
value: new fields.NumberField({
|
||||||
|
initial: 1,
|
||||||
|
min: 1,
|
||||||
|
integer: true,
|
||||||
|
nullable: false,
|
||||||
|
label: 'DAGGERHEART.GENERAL.value'
|
||||||
|
}),
|
||||||
|
max: new fields.NumberField({ integer: true, label: 'DAGGERHEART.GENERAL.max' })
|
||||||
|
},
|
||||||
|
{ nullable: true, initial: null }
|
||||||
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that an {@link EffectChangeData#type} string is well-formed.
|
||||||
|
* @param {string} type The string to be validated
|
||||||
|
* @returns {true}
|
||||||
|
* @throws {Error} An error if the type string is malformed
|
||||||
|
*/
|
||||||
|
static #validateType(type) {
|
||||||
|
if (type.length < 3) throw new Error('must be at least three characters long');
|
||||||
|
if (!/^custom\.-?\d+$/.test(type) && !type.split('.').every(s => /^[a-z0-9]+$/i.test(s))) {
|
||||||
|
throw new Error(
|
||||||
|
'A change type must either be a sequence of dot-delimited, alpha-numeric substrings or of the form' +
|
||||||
|
' "custom.{number}"'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isSuppressed() {
|
||||||
|
for (const change of this.changes) {
|
||||||
|
if (change.isSuppressed) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get armorChange() {
|
||||||
|
return this.changes.find(x => x.type === CONFIG.DH.GENERAL.activeEffectModes.armor.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
get armorData() {
|
||||||
|
const armorChange = this.armorChange;
|
||||||
|
if (!armorChange) return null;
|
||||||
|
|
||||||
|
return armorChange.getArmorData();
|
||||||
|
}
|
||||||
|
|
||||||
static getDefaultObject() {
|
static getDefaultObject() {
|
||||||
return {
|
return {
|
||||||
name: 'New Effect',
|
name: 'New Effect',
|
||||||
|
|
@ -64,4 +150,32 @@ export default class BaseEffect extends foundry.abstract.TypeDataModel {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _preUpdate(changed, options, userId) {
|
||||||
|
const allowed = await super._preUpdate(changed, options, userId);
|
||||||
|
if (allowed === false) return false;
|
||||||
|
|
||||||
|
const autoSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
|
||||||
|
if (
|
||||||
|
autoSettings.resourceScrollTexts &&
|
||||||
|
this.parent.actor?.type === 'character' &&
|
||||||
|
this.parent.actor.system.resources.armor
|
||||||
|
) {
|
||||||
|
const armorEffect = changed.system?.changes?.find(x => x.type === 'armor');
|
||||||
|
const newArmorTotal =
|
||||||
|
armorEffect?.value?.current + (this.parent.actor.system.armor?.system?.armor?.current ?? 0);
|
||||||
|
|
||||||
|
if (armorEffect && newArmorTotal !== this.parent.actor.system.armorScore.value) {
|
||||||
|
const armorData = getScrollTextData(this.parent.actor, { value: newArmorTotal }, 'armor');
|
||||||
|
options.scrollingTextData = [armorData];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onUpdate(changed, options, userId) {
|
||||||
|
super._onUpdate(changed, options, userId);
|
||||||
|
|
||||||
|
if (this.parent.actor && options.scrollingTextData)
|
||||||
|
this.parent.actor.queueScrollText(options.scrollingTextData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ export default class BeastformEffect extends BaseEffect {
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
return {
|
return {
|
||||||
|
...super.defineSchema(),
|
||||||
characterTokenData: new fields.SchemaField({
|
characterTokenData: new fields.SchemaField({
|
||||||
usesDynamicToken: new fields.BooleanField({ initial: false }),
|
usesDynamicToken: new fields.BooleanField({ initial: false }),
|
||||||
tokenImg: new fields.FilePathField({
|
tokenImg: new fields.FilePathField({
|
||||||
|
|
@ -99,7 +100,7 @@ export default class BeastformEffect extends BaseEffect {
|
||||||
token.flags.daggerheart?.beastformSubjectTexture ?? this.characterTokenData.tokenRingImg
|
token.flags.daggerheart?.beastformSubjectTexture ?? this.characterTokenData.tokenRingImg
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'flags.daggerheart': { '-=beastformTokenImg': null, '-=beastformSubjectTexture': null }
|
'flags.daggerheart': { beastformTokenImg: _del, beastformSubjectTexture: _del }
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
9
module/data/activeEffect/changeTypes/_module.mjs
Normal file
9
module/data/activeEffect/changeTypes/_module.mjs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import Armor from './armor.mjs';
|
||||||
|
|
||||||
|
export const changeEffects = {
|
||||||
|
armor: Armor.changeEffect
|
||||||
|
};
|
||||||
|
|
||||||
|
export const changeTypes = {
|
||||||
|
armor: Armor
|
||||||
|
};
|
||||||
206
module/data/activeEffect/changeTypes/armor.mjs
Normal file
206
module/data/activeEffect/changeTypes/armor.mjs
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
import { itemAbleRollParse } from '../../../helpers/utils.mjs';
|
||||||
|
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
export default class ArmorChange extends foundry.abstract.DataModel {
|
||||||
|
static defineSchema() {
|
||||||
|
return {
|
||||||
|
type: new fields.StringField({ required: true, choices: ['armor'], initial: 'armor' }),
|
||||||
|
priority: new fields.NumberField(),
|
||||||
|
phase: new fields.StringField({ required: true, blank: false, initial: 'initial' }),
|
||||||
|
value: new fields.SchemaField({
|
||||||
|
current: new fields.NumberField({ integer: true, min: 0, initial: 0 }),
|
||||||
|
max: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
nullable: false,
|
||||||
|
initial: '1',
|
||||||
|
label: 'DAGGERHEART.GENERAL.max'
|
||||||
|
}),
|
||||||
|
damageThresholds: new fields.SchemaField(
|
||||||
|
{
|
||||||
|
major: new fields.StringField({
|
||||||
|
initial: '0',
|
||||||
|
label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold'
|
||||||
|
}),
|
||||||
|
severe: new fields.StringField({
|
||||||
|
initial: '0',
|
||||||
|
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ nullable: true, initial: null }
|
||||||
|
),
|
||||||
|
interaction: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
choices: CONFIG.DH.GENERAL.activeEffectArmorInteraction,
|
||||||
|
initial: CONFIG.DH.GENERAL.activeEffectArmorInteraction.none.id,
|
||||||
|
label: 'DAGGERHEART.EFFECTS.ChangeTypes.armor.FIELDS.interaction.label',
|
||||||
|
hint: 'DAGGERHEART.EFFECTS.ChangeTypes.armor.FIELDS.interaction.hint'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static changeEffect = {
|
||||||
|
label: 'Armor',
|
||||||
|
defaultPriority: 20,
|
||||||
|
handler: (actor, change, _options, _field, replacementData) => {
|
||||||
|
const parsedMax = itemAbleRollParse(change.value.max, actor, change.effect.parent);
|
||||||
|
game.system.api.documents.DhActiveEffect.applyChange(
|
||||||
|
actor,
|
||||||
|
{
|
||||||
|
...change,
|
||||||
|
key: 'system.armorScore.value',
|
||||||
|
type: CONFIG.DH.GENERAL.activeEffectModes.add.id,
|
||||||
|
value: change.value.current
|
||||||
|
},
|
||||||
|
replacementData
|
||||||
|
);
|
||||||
|
game.system.api.documents.DhActiveEffect.applyChange(
|
||||||
|
actor,
|
||||||
|
{
|
||||||
|
...change,
|
||||||
|
key: 'system.armorScore.max',
|
||||||
|
type: CONFIG.DH.GENERAL.activeEffectModes.add.id,
|
||||||
|
value: parsedMax
|
||||||
|
},
|
||||||
|
replacementData
|
||||||
|
);
|
||||||
|
|
||||||
|
if (change.value.damageThresholds) {
|
||||||
|
const getThresholdValue = value => {
|
||||||
|
const parsed = itemAbleRollParse(value, actor, change.effect.parent);
|
||||||
|
const roll = new Roll(parsed).evaluateSync();
|
||||||
|
return roll ? (roll.isDeterministic ? roll.total : null) : null;
|
||||||
|
};
|
||||||
|
const major = getThresholdValue(change.value.damageThresholds.major);
|
||||||
|
const severe = getThresholdValue(change.value.damageThresholds.severe);
|
||||||
|
|
||||||
|
if (major) {
|
||||||
|
game.system.api.documents.DhActiveEffect.applyChange(
|
||||||
|
actor,
|
||||||
|
{
|
||||||
|
...change,
|
||||||
|
key: 'system.damageThresholds.major',
|
||||||
|
type: CONFIG.DH.GENERAL.activeEffectModes.override.id,
|
||||||
|
priority: 50,
|
||||||
|
value: major
|
||||||
|
},
|
||||||
|
replacementData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (severe) {
|
||||||
|
game.system.api.documents.DhActiveEffect.applyChange(
|
||||||
|
actor,
|
||||||
|
{
|
||||||
|
...change,
|
||||||
|
key: 'system.damageThresholds.severe',
|
||||||
|
type: CONFIG.DH.GENERAL.activeEffectModes.override.id,
|
||||||
|
priority: 50,
|
||||||
|
value: severe
|
||||||
|
},
|
||||||
|
replacementData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
render: null
|
||||||
|
};
|
||||||
|
|
||||||
|
get isSuppressed() {
|
||||||
|
switch (this.value.interaction) {
|
||||||
|
case CONFIG.DH.GENERAL.activeEffectArmorInteraction.active.id:
|
||||||
|
return !this.parent.parent?.actor.system.armor;
|
||||||
|
case CONFIG.DH.GENERAL.activeEffectArmorInteraction.inactive.id:
|
||||||
|
return Boolean(this.parent.parent?.actor.system.armor);
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInitialValue() {
|
||||||
|
return {
|
||||||
|
type: CONFIG.DH.GENERAL.activeEffectModes.armor.id,
|
||||||
|
value: {
|
||||||
|
current: 0,
|
||||||
|
max: 0
|
||||||
|
},
|
||||||
|
phase: 'initial',
|
||||||
|
priority: 20
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDefaultArmorEffect() {
|
||||||
|
return {
|
||||||
|
name: game.i18n.localize('DAGGERHEART.EFFECTS.ChangeTypes.armor.newArmorEffect'),
|
||||||
|
img: 'icons/equipment/chest/breastplate-helmet-metal.webp',
|
||||||
|
system: {
|
||||||
|
changes: [ArmorChange.getInitialValue()]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helpers */
|
||||||
|
|
||||||
|
getArmorData() {
|
||||||
|
const actor = this.parent.parent?.actor?.type === 'character' ? this.parent.parent.actor : null;
|
||||||
|
const maxParse = actor ? itemAbleRollParse(this.value.max, actor, this.parent.parent.parent) : null;
|
||||||
|
const maxRoll = maxParse ? new Roll(maxParse).evaluateSync() : null;
|
||||||
|
const maxEvaluated = maxRoll ? (maxRoll.isDeterministic ? maxRoll.total : null) : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
current: this.value.current,
|
||||||
|
max: maxEvaluated ?? this.value.max
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateArmorMax(newMax) {
|
||||||
|
const newChanges = [
|
||||||
|
...this.parent.changes.map(change => ({
|
||||||
|
...change,
|
||||||
|
value:
|
||||||
|
change.type === 'armor'
|
||||||
|
? {
|
||||||
|
...change.value,
|
||||||
|
current: Math.min(change.value.current, newMax),
|
||||||
|
max: newMax
|
||||||
|
}
|
||||||
|
: change.value
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
await this.parent.parent.update({ 'system.changes': newChanges });
|
||||||
|
}
|
||||||
|
|
||||||
|
static orderEffectsForAutoChange(armorEffects, increasing) {
|
||||||
|
const getEffectWeight = effect => {
|
||||||
|
switch (effect.parent.type) {
|
||||||
|
case 'class':
|
||||||
|
case 'subclass':
|
||||||
|
case 'ancestry':
|
||||||
|
case 'community':
|
||||||
|
case 'feature':
|
||||||
|
case 'domainCard':
|
||||||
|
return 2;
|
||||||
|
case 'armor':
|
||||||
|
return 3;
|
||||||
|
case 'loot':
|
||||||
|
case 'consumable':
|
||||||
|
return 4;
|
||||||
|
case 'weapon':
|
||||||
|
return 5;
|
||||||
|
case 'character':
|
||||||
|
return 6;
|
||||||
|
default:
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return armorEffects
|
||||||
|
.filter(x => !x.disabled && !x.isSuppressed)
|
||||||
|
.sort((a, b) =>
|
||||||
|
increasing ? getEffectWeight(b) - getEffectWeight(a) : getEffectWeight(a) - getEffectWeight(b)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -85,14 +85,14 @@ export default class DhpAdversary extends DhCreature {
|
||||||
type: 'attack'
|
type: 'attack'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
parts: [
|
parts: {
|
||||||
{
|
hitPoints: {
|
||||||
type: ['physical'],
|
type: ['physical'],
|
||||||
value: {
|
value: {
|
||||||
multiplier: 'flat'
|
multiplier: 'flat'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
@ -265,12 +265,12 @@ export default class DhpAdversary extends DhCreature {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update damage in item actions
|
// 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)) {
|
for (const action of Object.values(item.system.actions)) {
|
||||||
if (!action.damage) continue;
|
|
||||||
|
|
||||||
// Parse damage, and convert all formula matches in the descriptions to the new damage
|
|
||||||
try {
|
try {
|
||||||
const result = this.#adjustActionDamage(action, { ...damageMeta, type: 'action' });
|
const result = this.#adjustActionDamage(action, { ...damageMeta, type: 'action' });
|
||||||
|
if (!result) continue;
|
||||||
|
|
||||||
for (const { previousFormula, formula } of Object.values(result)) {
|
for (const { previousFormula, formula } of Object.values(result)) {
|
||||||
const oldFormulaRegexp = new RegExp(
|
const oldFormulaRegexp = new RegExp(
|
||||||
previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?')
|
previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?')
|
||||||
|
|
@ -372,16 +372,14 @@ export default class DhpAdversary extends DhCreature {
|
||||||
/**
|
/**
|
||||||
* Updates damage to reflect a specific value.
|
* Updates damage to reflect a specific value.
|
||||||
* @throws if damage structure is invalid for conversion
|
* @throws if damage structure is invalid for conversion
|
||||||
* @returns the converted formula and value as a simplified term
|
* @returns the converted formula and value as a simplified term, or null if it doesn't deal HP damage
|
||||||
*/
|
*/
|
||||||
#adjustActionDamage(action, damageMeta) {
|
#adjustActionDamage(action, damageMeta) {
|
||||||
// The current algorithm only returns a value if there is a single damage part
|
if (!action.damage?.parts.hitPoints) return null;
|
||||||
const hpDamageParts = action.damage.parts.filter(d => d.applyTo === 'hitPoints');
|
|
||||||
if (hpDamageParts.length !== 1) throw new Error('incorrect number of hp parts');
|
|
||||||
|
|
||||||
const result = {};
|
const result = {};
|
||||||
for (const property of ['value', 'valueAlt']) {
|
for (const property of ['value', 'valueAlt']) {
|
||||||
const data = hpDamageParts[0][property];
|
const data = action.damage.parts.hitPoints[property];
|
||||||
const previousFormula = data.custom.enabled
|
const previousFormula = data.custom.enabled
|
||||||
? data.custom.formula
|
? data.custom.formula
|
||||||
: [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0]
|
: [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0]
|
||||||
|
|
|
||||||
|
|
@ -189,21 +189,6 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _preDelete() {
|
|
||||||
/* Clear all partyMembers from tagTeam setting.*/
|
|
||||||
/* Revisit this when tagTeam is improved for many parties */
|
|
||||||
if (this.parent.parties.size > 0) {
|
|
||||||
const tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
|
|
||||||
await tagTeam.updateSource({
|
|
||||||
initiator: this.parent.id === tagTeam.initiator ? null : tagTeam.initiator,
|
|
||||||
members: Object.keys(tagTeam.members).find(x => x === this.parent.id)
|
|
||||||
? { [`-=${this.parent.id}`]: null }
|
|
||||||
: {}
|
|
||||||
});
|
|
||||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, tagTeam);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async _preUpdate(changes, options, userId) {
|
async _preUpdate(changes, options, userId) {
|
||||||
const allowed = await super._preUpdate(changes, options, userId);
|
const allowed = await super._preUpdate(changes, options, userId);
|
||||||
if (allowed === false) return;
|
if (allowed === false) return;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import DhCreature from './creature.mjs';
|
||||||
import { attributeField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs';
|
import { attributeField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs';
|
||||||
import { ActionField } from '../fields/actionField.mjs';
|
import { ActionField } from '../fields/actionField.mjs';
|
||||||
import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs';
|
import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs';
|
||||||
|
import { getArmorSources } from '../../helpers/utils.mjs';
|
||||||
|
|
||||||
export default class DhCharacter extends DhCreature {
|
export default class DhCharacter extends DhCreature {
|
||||||
/**@override */
|
/**@override */
|
||||||
|
|
@ -41,17 +42,16 @@ export default class DhCharacter extends DhCreature {
|
||||||
label: 'DAGGERHEART.GENERAL.proficiency'
|
label: 'DAGGERHEART.GENERAL.proficiency'
|
||||||
}),
|
}),
|
||||||
evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }),
|
evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }),
|
||||||
armorScore: new fields.NumberField({ integer: true, initial: 0, label: 'DAGGERHEART.GENERAL.armorScore' }),
|
|
||||||
damageThresholds: new fields.SchemaField({
|
damageThresholds: new fields.SchemaField({
|
||||||
severe: new fields.NumberField({
|
|
||||||
integer: true,
|
|
||||||
initial: 0,
|
|
||||||
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
|
|
||||||
}),
|
|
||||||
major: new fields.NumberField({
|
major: new fields.NumberField({
|
||||||
integer: true,
|
integer: true,
|
||||||
initial: 0,
|
initial: 0,
|
||||||
label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold'
|
label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold'
|
||||||
|
}),
|
||||||
|
severe: new fields.NumberField({
|
||||||
|
integer: true,
|
||||||
|
initial: 0,
|
||||||
|
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
experiences: new fields.TypedObjectField(
|
experiences: new fields.TypedObjectField(
|
||||||
|
|
@ -96,8 +96,8 @@ export default class DhCharacter extends DhCreature {
|
||||||
trait: 'strength'
|
trait: 'strength'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
parts: [
|
parts: {
|
||||||
{
|
hitPoints: {
|
||||||
type: ['physical'],
|
type: ['physical'],
|
||||||
value: {
|
value: {
|
||||||
custom: {
|
custom: {
|
||||||
|
|
@ -106,7 +106,7 @@ export default class DhCharacter extends DhCreature {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
@ -465,6 +465,101 @@ export default class DhCharacter extends DhCreature {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateArmorValue({ value: armorChange = 0, clear = false }) {
|
||||||
|
if (armorChange === 0 && !clear) return;
|
||||||
|
|
||||||
|
const increasing = armorChange >= 0;
|
||||||
|
let remainingChange = Math.abs(armorChange);
|
||||||
|
const orderedSources = getArmorSources(this.parent).filter(s => !s.disabled);
|
||||||
|
|
||||||
|
const handleArmorData = (embeddedUpdates, doc, armorData) => {
|
||||||
|
let usedArmorChange = 0;
|
||||||
|
if (clear) {
|
||||||
|
usedArmorChange -= armorData.current;
|
||||||
|
} else {
|
||||||
|
if (increasing) {
|
||||||
|
const remainingArmor = armorData.max - armorData.current;
|
||||||
|
usedArmorChange = Math.min(remainingChange, remainingArmor);
|
||||||
|
remainingChange -= usedArmorChange;
|
||||||
|
} else {
|
||||||
|
const changeChange = Math.min(armorData.current, remainingChange);
|
||||||
|
usedArmorChange -= changeChange;
|
||||||
|
remainingChange -= changeChange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!usedArmorChange) return usedArmorChange;
|
||||||
|
else {
|
||||||
|
if (!embeddedUpdates[doc.id]) embeddedUpdates[doc.id] = { doc: doc, updates: [] };
|
||||||
|
|
||||||
|
return usedArmorChange;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const armorUpdates = [];
|
||||||
|
const effectUpdates = [];
|
||||||
|
for (const { document: armorSource } of orderedSources) {
|
||||||
|
const usedArmorChange = handleArmorData(
|
||||||
|
armorSource.type === 'armor' ? armorUpdates : effectUpdates,
|
||||||
|
armorSource.parent,
|
||||||
|
armorSource.type === 'armor' ? armorSource.system.armor : armorSource.system.armorData
|
||||||
|
);
|
||||||
|
if (!usedArmorChange) continue;
|
||||||
|
|
||||||
|
if (armorSource.type === 'armor') {
|
||||||
|
armorUpdates[armorSource.parent.id].updates.push({
|
||||||
|
'_id': armorSource.id,
|
||||||
|
'system.armor.current': armorSource.system.armor.current + usedArmorChange
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
effectUpdates[armorSource.parent.id].updates.push({
|
||||||
|
'_id': armorSource.id,
|
||||||
|
'system.changes': armorSource.system.changes.map(change => ({
|
||||||
|
...change,
|
||||||
|
value:
|
||||||
|
change.type === 'armor'
|
||||||
|
? {
|
||||||
|
...change.value,
|
||||||
|
current: armorSource.system.armorChange.value.current + usedArmorChange
|
||||||
|
}
|
||||||
|
: change.value
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingChange === 0 && !clear) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const armorUpdateValues = Object.values(armorUpdates);
|
||||||
|
for (const [index, { doc, updates }] of armorUpdateValues.entries())
|
||||||
|
await doc.updateEmbeddedDocuments('Item', updates, { render: index === armorUpdateValues.length - 1 });
|
||||||
|
|
||||||
|
const effectUpdateValues = Object.values(effectUpdates);
|
||||||
|
for (const [index, { doc, updates }] of effectUpdateValues.entries())
|
||||||
|
await doc.updateEmbeddedDocuments('ActiveEffect', updates, {
|
||||||
|
render: index === effectUpdateValues.length - 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateArmorEffectValue({ uuid, value }) {
|
||||||
|
const source = await foundry.utils.fromUuid(uuid);
|
||||||
|
if (source.type === 'armor') {
|
||||||
|
await source.update({
|
||||||
|
'system.armor.current': source.system.armor.current + value
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const effectValue = source.system.armorChange.value;
|
||||||
|
await source.update({
|
||||||
|
'system.changes': [
|
||||||
|
{
|
||||||
|
...source.system.armorChange,
|
||||||
|
value: { ...effectValue, current: effectValue.current + value }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get sheetLists() {
|
get sheetLists() {
|
||||||
const ancestryFeatures = [],
|
const ancestryFeatures = [],
|
||||||
communityFeatures = [],
|
communityFeatures = [],
|
||||||
|
|
@ -588,6 +683,10 @@ export default class DhCharacter extends DhCreature {
|
||||||
|
|
||||||
prepareBaseData() {
|
prepareBaseData() {
|
||||||
super.prepareBaseData();
|
super.prepareBaseData();
|
||||||
|
this.armorScore = {
|
||||||
|
max: this.armor?.system.armor.max ?? 0,
|
||||||
|
value: this.armor?.system.armor.current ?? 0
|
||||||
|
};
|
||||||
this.evasion += this.class.value?.system?.evasion ?? 0;
|
this.evasion += this.class.value?.system?.evasion ?? 0;
|
||||||
|
|
||||||
const currentLevel = this.levelData.level.current;
|
const currentLevel = this.levelData.level.current;
|
||||||
|
|
@ -637,14 +736,12 @@ export default class DhCharacter extends DhCreature {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const armor = this.armor;
|
|
||||||
this.armorScore = armor ? armor.system.baseScore : 0;
|
|
||||||
this.damageThresholds = {
|
this.damageThresholds = {
|
||||||
major: armor
|
major: this.armor
|
||||||
? armor.system.baseThresholds.major + this.levelData.level.current
|
? this.armor.system.baseThresholds.major + this.levelData.level.current
|
||||||
: this.levelData.level.current,
|
: this.levelData.level.current,
|
||||||
severe: armor
|
severe: this.armor
|
||||||
? armor.system.baseThresholds.severe + this.levelData.level.current
|
? this.armor.system.baseThresholds.severe + this.levelData.level.current
|
||||||
: this.levelData.level.current * 2
|
: this.levelData.level.current * 2
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -677,13 +774,12 @@ export default class DhCharacter extends DhCreature {
|
||||||
this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait;
|
this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait;
|
||||||
|
|
||||||
this.resources.armor = {
|
this.resources.armor = {
|
||||||
|
...this.armorScore,
|
||||||
label: 'DAGGERHEART.GENERAL.armor',
|
label: 'DAGGERHEART.GENERAL.armor',
|
||||||
value: this.armor?.system?.marks?.value ?? 0,
|
|
||||||
max: this.armorScore,
|
|
||||||
isReversed: true
|
isReversed: true
|
||||||
};
|
};
|
||||||
|
|
||||||
this.attack.damage.parts[0].value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`;
|
this.attack.damage.parts.hitPoints.value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRollData() {
|
getRollData() {
|
||||||
|
|
|
||||||
|
|
@ -81,15 +81,15 @@ export default class DhCompanion extends DhCreature {
|
||||||
bonus: 0
|
bonus: 0
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
parts: [
|
parts: {
|
||||||
{
|
hitPoints: {
|
||||||
type: ['physical'],
|
type: ['physical'],
|
||||||
value: {
|
value: {
|
||||||
dice: 'd6',
|
dice: 'd6',
|
||||||
multiplier: 'prof'
|
multiplier: 'prof'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
@ -135,7 +135,9 @@ export default class DhCompanion extends DhCreature {
|
||||||
break;
|
break;
|
||||||
case 'vicious':
|
case 'vicious':
|
||||||
if (selection.data[0] === 'damage') {
|
if (selection.data[0] === 'damage') {
|
||||||
this.attack.damage.parts[0].value.dice = adjustDice(this.attack.damage.parts[0].value.dice);
|
this.attack.damage.parts.hitPoints.value.dice = adjustDice(
|
||||||
|
this.attack.damage.parts.hitPoints.value.dice
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.attack.range = adjustRange(this.attack.range).id;
|
this.attack.range = adjustRange(this.attack.range).id;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,10 +75,6 @@ export default class DhEnvironment extends BaseDataActor {
|
||||||
);
|
);
|
||||||
scene.update({ 'flags.daggerheart.sceneEnvironments': newSceneEnvironments }).then(() => {
|
scene.update({ 'flags.daggerheart.sceneEnvironments': newSceneEnvironments }).then(() => {
|
||||||
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Scene });
|
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Scene });
|
||||||
game.socket.emit(`system.${CONFIG.DH.id}`, {
|
|
||||||
action: socketEvent.Refresh,
|
|
||||||
data: { refreshType: RefreshType.TagTeamRoll }
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import BaseDataActor from './base.mjs';
|
import BaseDataActor from './base.mjs';
|
||||||
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
|
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
|
||||||
|
import TagTeamData from '../tagTeamData.mjs';
|
||||||
|
|
||||||
export default class DhParty extends BaseDataActor {
|
export default class DhParty extends BaseDataActor {
|
||||||
/**@inheritdoc */
|
/**@inheritdoc */
|
||||||
|
|
@ -14,7 +15,8 @@ export default class DhParty extends BaseDataActor {
|
||||||
handfuls: new fields.NumberField({ initial: 1, integer: true }),
|
handfuls: new fields.NumberField({ initial: 1, integer: true }),
|
||||||
bags: new fields.NumberField({ initial: 0, integer: true }),
|
bags: new fields.NumberField({ initial: 0, integer: true }),
|
||||||
chests: new fields.NumberField({ initial: 0, integer: true })
|
chests: new fields.NumberField({ initial: 0, integer: true })
|
||||||
})
|
}),
|
||||||
|
tagTeam: new fields.EmbeddedDataField(TagTeamData)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,23 +42,6 @@ export default class DhParty extends BaseDataActor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _preDelete() {
|
|
||||||
/* Clear all partyMembers from tagTeam setting.*/
|
|
||||||
/* Revisit this when tagTeam is improved for many parties */
|
|
||||||
const tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
|
|
||||||
await tagTeam.updateSource({
|
|
||||||
initiator: this.partyMembers.some(x => x.id === tagTeam.initiator) ? null : tagTeam.initiator,
|
|
||||||
members: Object.keys(tagTeam.members).reduce((acc, key) => {
|
|
||||||
if (this.partyMembers.find(x => x.id === key)) {
|
|
||||||
acc[`-=${key}`] = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {})
|
|
||||||
});
|
|
||||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, tagTeam);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onDelete(options, userId) {
|
_onDelete(options, userId) {
|
||||||
super._onDelete(options, userId);
|
super._onDelete(options, userId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
export { ActionCollection } from './actionField.mjs';
|
export { ActionCollection } from './actionField.mjs';
|
||||||
|
export { default as IterableTypedObjectField } from './iterableTypedObjectField.mjs';
|
||||||
export { default as FormulaField } from './formulaField.mjs';
|
export { default as FormulaField } from './formulaField.mjs';
|
||||||
export { default as ForeignDocumentUUIDField } from './foreignDocumentUUIDField.mjs';
|
export { default as ForeignDocumentUUIDField } from './foreignDocumentUUIDField.mjs';
|
||||||
export { default as ForeignDocumentUUIDArrayField } from './foreignDocumentUUIDArrayField.mjs';
|
export { default as ForeignDocumentUUIDArrayField } from './foreignDocumentUUIDArrayField.mjs';
|
||||||
export { default as TriggerField } from './triggerField.mjs';
|
export { default as TriggerField } from './triggerField.mjs';
|
||||||
export { default as MappingField } from './mappingField.mjs';
|
|
||||||
export * as ActionFields from './action/_module.mjs';
|
export * as ActionFields from './action/_module.mjs';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import FormulaField from '../formulaField.mjs';
|
import FormulaField from '../formulaField.mjs';
|
||||||
import { setsEqual } from '../../../helpers/utils.mjs';
|
import { setsEqual } from '../../../helpers/utils.mjs';
|
||||||
|
import IterableTypedObjectField from '../iterableTypedObjectField.mjs';
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
|
@ -12,7 +13,7 @@ export default class DamageField extends fields.SchemaField {
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
constructor(options, context = {}) {
|
constructor(options, context = {}) {
|
||||||
const damageFields = {
|
const damageFields = {
|
||||||
parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)),
|
parts: new IterableTypedObjectField(DHDamageData),
|
||||||
includeBase: new fields.BooleanField({
|
includeBase: new fields.BooleanField({
|
||||||
initial: false,
|
initial: false,
|
||||||
label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label'
|
label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label'
|
||||||
|
|
@ -49,9 +50,9 @@ export default class DamageField extends fields.SchemaField {
|
||||||
formulas = DamageField.formatFormulas.call(this, formulas, config);
|
formulas = DamageField.formatFormulas.call(this, formulas, config);
|
||||||
|
|
||||||
const damageConfig = {
|
const damageConfig = {
|
||||||
|
dialog: {},
|
||||||
...config,
|
...config,
|
||||||
roll: formulas,
|
roll: formulas,
|
||||||
dialog: {},
|
|
||||||
data: this.getRollData()
|
data: this.getRollData()
|
||||||
};
|
};
|
||||||
delete damageConfig.evaluate;
|
delete damageConfig.evaluate;
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export default class EffectsField extends fields.ArrayField {
|
||||||
static async execute(config, targets = null, force = false) {
|
static async execute(config, targets = null, force = false) {
|
||||||
if (!config.hasEffect) return;
|
if (!config.hasEffect) return;
|
||||||
let message = config.message ?? ui.chat.collection.get(config.parent?._id);
|
let message = config.message ?? ui.chat.collection.get(config.parent?._id);
|
||||||
if (!message) {
|
if (!message && !config.skips.createMessage) {
|
||||||
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
|
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
|
||||||
roll._evaluated = true;
|
roll._evaluated = true;
|
||||||
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
|
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
|
||||||
|
|
@ -106,22 +106,11 @@ export default class EffectsField extends fields.ArrayField {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply an Effect to a target or enable it if already on it
|
* Apply an Effect to a target
|
||||||
* @param {object} effect Effect object containing ActiveEffect UUID
|
* @param {object} effect Effect object containing ActiveEffect UUID
|
||||||
* @param {object} actor Actor Document
|
* @param {object} actor Actor Document
|
||||||
*/
|
*/
|
||||||
static async applyEffect(effect, actor) {
|
static async applyEffect(effect, actor) {
|
||||||
const existingEffect = actor.effects.find(e => e.origin === effect.uuid);
|
|
||||||
if (existingEffect) {
|
|
||||||
return effect.update(
|
|
||||||
foundry.utils.mergeObject({
|
|
||||||
...effect.constructor.getInitialDuration(),
|
|
||||||
disabled: false
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, create a new effect on the target
|
|
||||||
const effectData = foundry.utils.mergeObject({
|
const effectData = foundry.utils.mergeObject({
|
||||||
...(effect.toObject?.() ?? effect),
|
...(effect.toObject?.() ?? effect),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export default class SaveField extends fields.SchemaField {
|
||||||
if (!config.hasSave) return;
|
if (!config.hasSave) return;
|
||||||
let message = config.message ?? ui.chat.collection.get(config.parent?._id);
|
let message = config.message ?? ui.chat.collection.get(config.parent?._id);
|
||||||
|
|
||||||
if (!message) {
|
if (!message && !config.skips.createMessage) {
|
||||||
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
|
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
|
||||||
roll._evaluated = true;
|
roll._evaluated = true;
|
||||||
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
|
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import DHActionConfig from '../../applications/sheets-configs/action-config.mjs';
|
import DHActionConfig from '../../applications/sheets-configs/action-config.mjs';
|
||||||
import { itemAbleRollParse } from '../../helpers/utils.mjs';
|
import { itemAbleRollParse } from '../../helpers/utils.mjs';
|
||||||
import MappingField from './mappingField.mjs';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specialized collection type for stored actions.
|
* Specialized collection type for stored actions.
|
||||||
|
|
@ -11,9 +10,9 @@ export class ActionCollection extends Collection {
|
||||||
constructor(model, entries) {
|
constructor(model, entries) {
|
||||||
super();
|
super();
|
||||||
this.#model = model;
|
this.#model = model;
|
||||||
for (const entry of entries) {
|
for (const [key, value] of entries) {
|
||||||
if (!(entry instanceof game.system.api.models.actions.actionsTypes.base)) continue;
|
if (!(value instanceof game.system.api.models.actions.actionsTypes.base)) continue;
|
||||||
this.set(entry._id, entry);
|
this.set(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,7 +60,7 @@ export class ActionCollection extends Collection {
|
||||||
/**
|
/**
|
||||||
* Field that stores actions.
|
* Field that stores actions.
|
||||||
*/
|
*/
|
||||||
export class ActionsField extends MappingField {
|
export class ActionsField extends foundry.data.fields.TypedObjectField {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(new ActionField(), options);
|
super(new ActionField(), options);
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +69,7 @@ export class ActionsField extends MappingField {
|
||||||
|
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
initialize(value, model, options) {
|
initialize(value, model, options) {
|
||||||
const actions = Object.values(super.initialize(value, model, options));
|
const actions = Object.entries(super.initialize(value, model, options));
|
||||||
return new ActionCollection(model, actions);
|
return new ActionCollection(model, actions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -88,10 +87,11 @@ export class ActionField extends foundry.data.fields.ObjectField {
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/** @override */
|
/** @override */
|
||||||
_cleanType(value, options) {
|
_cleanType(value, options, _state) {
|
||||||
if (!(typeof value === 'object')) value = {};
|
if (!(typeof value === 'object')) value = {};
|
||||||
|
value = super._cleanType(value, options, _state);
|
||||||
const cls = this.getModel(value);
|
const cls = this.getModel(value);
|
||||||
if (cls) return cls.cleanData(value, options);
|
if (cls) return cls.cleanData(value, options, _state);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,9 +111,17 @@ export class ActionField extends foundry.data.fields.ObjectField {
|
||||||
* @param {object} sourceData Candidate source data of the root model.
|
* @param {object} sourceData Candidate source data of the root model.
|
||||||
* @param {any} fieldData The value of this field within the source data.
|
* @param {any} fieldData The value of this field within the source data.
|
||||||
*/
|
*/
|
||||||
migrateSource(sourceData, fieldData) {
|
_migrate(sourceData, _fieldData) {
|
||||||
const cls = this.getModel(fieldData);
|
const source = sourceData ?? this.options.initial;
|
||||||
if (cls) cls.migrateDataSafe(fieldData);
|
if (!source) return sourceData;
|
||||||
|
|
||||||
|
const cls = this.getModel(source);
|
||||||
|
if (cls) {
|
||||||
|
cls.migrateDataSafe(source);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sourceData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -237,11 +245,11 @@ export function ActionMixin(Base) {
|
||||||
: foundry.utils.getProperty(result, basePath);
|
: foundry.utils.getProperty(result, basePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete() {
|
async delete() {
|
||||||
if (!this.inCollection) return this.item;
|
if (!this.inCollection) return this.item;
|
||||||
const action = foundry.utils.getProperty(this.item, `system.${this.systemPath}`)?.get(this.id);
|
const action = foundry.utils.getProperty(this.item, `system.${this.systemPath}`)?.get(this.id);
|
||||||
if (!action) return this.item;
|
if (!action) return this.item;
|
||||||
this.item.update({ [`system.${this.systemPath}.-=${this.id}`]: null });
|
await this.item.update({ [`system.${this.systemPath}.${this.id}`]: _del }); // Does not work. Unsure why. It worked in v13 <_<'
|
||||||
this.constructor._sheets.get(this.uuid)?.close();
|
this.constructor._sheets.get(this.uuid)?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -302,7 +310,7 @@ export function ActionMixin(Base) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ChatMessage.applyRollMode(msg, game.settings.get('core', 'rollMode'));
|
ChatMessage.applyMode(msg, game.settings.get('core', 'messageMode'));
|
||||||
cls.create(msg);
|
cls.create(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,8 @@ class ResourcesField extends fields.TypedObjectField {
|
||||||
return key in CONFIG.DH.RESOURCE[this.actorType].all;
|
return key in CONFIG.DH.RESOURCE[this.actorType].all;
|
||||||
}
|
}
|
||||||
|
|
||||||
_cleanType(value, options) {
|
_cleanType(value, options, _state) {
|
||||||
value = super._cleanType(value, options);
|
value = super._cleanType(value, options, _state);
|
||||||
|
|
||||||
// If not partial, ensure all data exists
|
// If not partial, ensure all data exists
|
||||||
if (!options.partial) {
|
if (!options.partial) {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export default class ForeignDocumentUUIDArrayField extends foundry.data.fields.A
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
initialize(value, model, options = {}) {
|
initialize(value, model, options = {}) {
|
||||||
const v = super.initialize(value, model, options);
|
const v = super.initialize(value ?? [], model, options);
|
||||||
return () => {
|
return () => {
|
||||||
const data = v.map(entry => (typeof entry === 'function' ? entry() : entry));
|
const data = v.map(entry => (typeof entry === 'function' ? entry() : entry));
|
||||||
return this.options.prune ? data.filter(d => !!d) : data;
|
return this.options.prune ? data.filter(d => !!d) : data;
|
||||||
|
|
|
||||||
32
module/data/fields/iterableTypedObjectField.mjs
Normal file
32
module/data/fields/iterableTypedObjectField.mjs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
export default class IterableTypedObjectField extends foundry.data.fields.TypedObjectField {
|
||||||
|
constructor(model, options = { collectionClass: foundry.utils.Collection }, context = {}) {
|
||||||
|
super(new foundry.data.fields.EmbeddedDataField(model), options, context);
|
||||||
|
this.#elementClass = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
#elementClass;
|
||||||
|
|
||||||
|
/** Initializes an object with an iterator. This modifies the prototype instead of */
|
||||||
|
initialize(values) {
|
||||||
|
const object = Object.create(IterableObjectPrototype);
|
||||||
|
for (const [key, value] of Object.entries(values)) {
|
||||||
|
object[key] = new this.#elementClass(value);
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The prototype of an iterable object.
|
||||||
|
* This allows the functionality of a class but also allows foundry.utils.getType() to return "Object" instead of "Unknown".
|
||||||
|
*/
|
||||||
|
const IterableObjectPrototype = {
|
||||||
|
[Symbol.iterator]: function* () {
|
||||||
|
for (const value of Object.values(this)) {
|
||||||
|
yield value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
map: function (func) {
|
||||||
|
return Array.from(this, func);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
/**
|
|
||||||
* A subclass of ObjectField that represents a mapping of keys to the provided DataField type.
|
|
||||||
*
|
|
||||||
* @param {DataField} model The class of DataField which should be embedded in this field.
|
|
||||||
* @param {MappingFieldOptions} [options={}] Options which configure the behavior of the field.
|
|
||||||
* @property {string[]} [initialKeys] Keys that will be created if no data is provided.
|
|
||||||
* @property {MappingFieldInitialValueBuilder} [initialValue] Function to calculate the initial value for a key.
|
|
||||||
* @property {boolean} [initialKeysOnly=false] Should the keys in the initialized data be limited to the keys provided
|
|
||||||
* by `options.initialKeys`?
|
|
||||||
*/
|
|
||||||
export default class MappingField extends foundry.data.fields.ObjectField {
|
|
||||||
constructor(model, options) {
|
|
||||||
if (!(model instanceof foundry.data.fields.DataField)) {
|
|
||||||
throw new Error('MappingField must have a DataField as its contained element');
|
|
||||||
}
|
|
||||||
super(options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The embedded DataField definition which is contained in this field.
|
|
||||||
* @type {DataField}
|
|
||||||
*/
|
|
||||||
this.model = model;
|
|
||||||
model.parent = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @inheritDoc */
|
|
||||||
static get _defaults() {
|
|
||||||
return foundry.utils.mergeObject(super._defaults, {
|
|
||||||
initialKeys: null,
|
|
||||||
initialValue: null,
|
|
||||||
initialKeysOnly: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @inheritDoc */
|
|
||||||
_cleanType(value, options) {
|
|
||||||
Object.entries(value).forEach(([k, v]) => {
|
|
||||||
if (k.startsWith('-=')) return;
|
|
||||||
value[k] = this.model.clean(v, options);
|
|
||||||
});
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @inheritDoc */
|
|
||||||
getInitialValue(data) {
|
|
||||||
let keys = this.initialKeys;
|
|
||||||
const initial = super.getInitialValue(data);
|
|
||||||
if (!keys || !foundry.utils.isEmpty(initial)) return initial;
|
|
||||||
if (!(keys instanceof Array)) keys = Object.keys(keys);
|
|
||||||
for (const key of keys) initial[key] = this._getInitialValueForKey(key);
|
|
||||||
return initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the initial value for the provided key.
|
|
||||||
* @param {string} key Key within the object being built.
|
|
||||||
* @param {object} [object] Any existing mapping data.
|
|
||||||
* @returns {*} Initial value based on provided field type.
|
|
||||||
*/
|
|
||||||
_getInitialValueForKey(key, object) {
|
|
||||||
const initial = this.model.getInitialValue();
|
|
||||||
return this.initialValue?.(key, initial, object) ?? initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @override */
|
|
||||||
_validateType(value, options = {}) {
|
|
||||||
if (foundry.utils.getType(value) !== 'Object') throw new Error('must be an Object');
|
|
||||||
const errors = this._validateValues(value, options);
|
|
||||||
if (!foundry.utils.isEmpty(errors)) {
|
|
||||||
const failure = new foundry.data.validation.DataModelValidationFailure();
|
|
||||||
failure.elements = Object.entries(errors).map(([id, failure]) => ({ id, failure }));
|
|
||||||
throw failure.asError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate each value of the object.
|
|
||||||
* @param {object} value The object to validate.
|
|
||||||
* @param {object} options Validation options.
|
|
||||||
* @returns {Record<string, Error>} An object of value-specific errors by key.
|
|
||||||
*/
|
|
||||||
_validateValues(value, options) {
|
|
||||||
const errors = {};
|
|
||||||
for (const [k, v] of Object.entries(value)) {
|
|
||||||
if (k.startsWith('-=')) continue;
|
|
||||||
const error = this.model.validate(v, options);
|
|
||||||
if (error) errors[k] = error;
|
|
||||||
}
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @override */
|
|
||||||
initialize(value, model, options = {}) {
|
|
||||||
if (!value) return value;
|
|
||||||
const obj = {};
|
|
||||||
const initialKeys = this.initialKeys instanceof Array ? this.initialKeys : Object.keys(this.initialKeys ?? {});
|
|
||||||
const keys = this.initialKeysOnly ? initialKeys : Object.keys(value);
|
|
||||||
for (const key of keys) {
|
|
||||||
const data = value[key] ?? this._getInitialValueForKey(key, value);
|
|
||||||
obj[key] = this.model.initialize(data, model, options);
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @inheritDoc */
|
|
||||||
_getField(path) {
|
|
||||||
if (path.length === 0) return this;
|
|
||||||
else if (path.length === 1) return this.model;
|
|
||||||
path.shift();
|
|
||||||
return this.model._getField(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -19,7 +19,14 @@ export default class DHArmor extends AttachableItem {
|
||||||
...super.defineSchema(),
|
...super.defineSchema(),
|
||||||
tier: new fields.NumberField({ required: true, integer: true, initial: 1, min: 1 }),
|
tier: new fields.NumberField({ required: true, integer: true, initial: 1, min: 1 }),
|
||||||
equipped: new fields.BooleanField({ initial: false }),
|
equipped: new fields.BooleanField({ initial: false }),
|
||||||
baseScore: new fields.NumberField({ integer: true, initial: 0 }),
|
armor: new fields.SchemaField({
|
||||||
|
current: new fields.NumberField({ integer: true, min: 0, initial: 0 }),
|
||||||
|
max: new fields.NumberField({ required: true, integer: true, initial: 0 })
|
||||||
|
}),
|
||||||
|
baseThresholds: new fields.SchemaField({
|
||||||
|
major: new fields.NumberField({ integer: true, initial: 0 }),
|
||||||
|
severe: new fields.NumberField({ integer: true, initial: 0 })
|
||||||
|
}),
|
||||||
armorFeatures: new fields.ArrayField(
|
armorFeatures: new fields.ArrayField(
|
||||||
new fields.SchemaField({
|
new fields.SchemaField({
|
||||||
value: new fields.StringField({
|
value: new fields.StringField({
|
||||||
|
|
@ -28,14 +35,7 @@ export default class DHArmor extends AttachableItem {
|
||||||
effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
|
effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
|
||||||
actionIds: new fields.ArrayField(new fields.StringField({ required: true }))
|
actionIds: new fields.ArrayField(new fields.StringField({ required: true }))
|
||||||
})
|
})
|
||||||
),
|
)
|
||||||
marks: new fields.SchemaField({
|
|
||||||
value: new fields.NumberField({ initial: 0, integer: true })
|
|
||||||
}),
|
|
||||||
baseThresholds: new fields.SchemaField({
|
|
||||||
major: new fields.NumberField({ integer: true, initial: 0 }),
|
|
||||||
severe: new fields.NumberField({ integer: true, initial: 0 })
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,7 +84,7 @@ export default class DHArmor extends AttachableItem {
|
||||||
}
|
}
|
||||||
await this.parent.deleteEmbeddedDocuments('ActiveEffect', effectIds);
|
await this.parent.deleteEmbeddedDocuments('ActiveEffect', effectIds);
|
||||||
changes.system.actions = actionIds.reduce((acc, id) => {
|
changes.system.actions = actionIds.reduce((acc, id) => {
|
||||||
acc[`-=${id}`] = null;
|
acc[id] = _del;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
|
@ -151,13 +151,20 @@ export default class DHArmor extends AttachableItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
|
static migrateDocumentData(source) {
|
||||||
|
if (!source.system.armor) {
|
||||||
|
source.system.armor = { current: source.system.marks?.value ?? 0, max: source.system.baseScore ?? 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a list of localized tags based on this item's type-specific properties.
|
* Generates a list of localized tags based on this item's type-specific properties.
|
||||||
* @returns {string[]} An array of localized tag strings.
|
* @returns {string[]} An array of localized tag strings.
|
||||||
*/
|
*/
|
||||||
_getTags() {
|
_getTags() {
|
||||||
const tags = [
|
const tags = [
|
||||||
`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`,
|
`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.armor.max}`,
|
||||||
`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseThresholds.base')}: ${this.baseThresholds.major} / ${this.baseThresholds.severe}`
|
`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseThresholds.base')}: ${this.baseThresholds.major} / ${this.baseThresholds.severe}`
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -169,9 +176,7 @@ export default class DHArmor extends AttachableItem {
|
||||||
* @returns {(string | { value: string, icons: string[] })[]} An array of localized strings and damage label objects.
|
* @returns {(string | { value: string, icons: string[] })[]} An array of localized strings and damage label objects.
|
||||||
*/
|
*/
|
||||||
_getLabels() {
|
_getLabels() {
|
||||||
const labels = [];
|
const labels = [`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.armor.max}`];
|
||||||
if (this.baseScore)
|
|
||||||
labels.push(`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`);
|
|
||||||
return labels;
|
return labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -222,17 +222,22 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
|
||||||
|
|
||||||
const autoSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
|
const autoSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
|
||||||
const armorChanged =
|
const armorChanged =
|
||||||
changed.system?.marks?.value !== undefined && changed.system.marks.value !== this.marks.value;
|
changed.system?.armor?.current !== undefined && changed.system.armor.current !== this.armor.current;
|
||||||
if (armorChanged && autoSettings.resourceScrollTexts && this.parent.parent?.type === 'character') {
|
if (armorChanged && autoSettings.resourceScrollTexts && this.parent.parent?.type === 'character') {
|
||||||
const armorData = getScrollTextData(this.parent.parent, changed.system.marks, 'armor');
|
const armorChangeValue = changed.system.armor.current - this.armor.current;
|
||||||
|
const armorData = getScrollTextData(
|
||||||
|
this.parent.parent,
|
||||||
|
{ value: armorChangeValue + this.parent.parent.system.armorScore.value },
|
||||||
|
'armor'
|
||||||
|
);
|
||||||
options.scrollingTextData = [armorData];
|
options.scrollingTextData = [armorData];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changed.system?.actions) {
|
if (changed.system?.actions) {
|
||||||
const triggersToRemove = Object.keys(changed.system.actions).reduce((acc, key) => {
|
const triggersToRemove = Object.keys(changed.system.actions).reduce((acc, key) => {
|
||||||
if (!changed.system.actions[key]) {
|
const action = changed.system.actions[key];
|
||||||
const strippedKey = key.replace('-=', '');
|
if (action && Object.keys(action).length === 0) {
|
||||||
acc.push(...this.actions.get(strippedKey).triggers.map(x => x.trigger));
|
acc.push(...this.actions.get(key).triggers.map(x => x.trigger));
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
|
|
|
||||||
|
|
@ -100,13 +100,13 @@ export default class DHBeastform extends BaseDataItem {
|
||||||
static getBeastformAttackData(effect) {
|
static getBeastformAttackData(effect) {
|
||||||
if (!effect) return null;
|
if (!effect) return null;
|
||||||
|
|
||||||
const mainTrait = effect.changes.find(x => x.key === 'system.rules.attack.roll.trait')?.value;
|
const mainTrait = effect.system.changes.find(x => x.key === 'system.rules.attack.roll.trait')?.value;
|
||||||
const traitBonus = effect.changes.find(x => x.key === `system.traits.${mainTrait}.value`)?.value ?? 0;
|
const traitBonus = effect.system.changes.find(x => x.key === `system.traits.${mainTrait}.value`)?.value ?? 0;
|
||||||
const evasionBonus = effect.changes.find(x => x.key === 'system.evasion')?.value ?? 0;
|
const evasionBonus = effect.system.changes.find(x => x.key === 'system.evasion')?.value ?? 0;
|
||||||
|
|
||||||
const damageDiceIndex = effect.changes.find(x => x.key === 'system.rules.attack.damage.diceIndex');
|
const damageDiceIndex = effect.system.changes.find(x => x.key === 'system.rules.attack.damage.diceIndex');
|
||||||
const damageDice = damageDiceIndex ? Object.keys(CONFIG.DH.GENERAL.diceTypes)[damageDiceIndex.value] : null;
|
const damageDice = damageDiceIndex ? Object.keys(CONFIG.DH.GENERAL.diceTypes)[damageDiceIndex.value] : null;
|
||||||
const damageBonus = effect.changes.find(x => x.key === 'system.rules.attack.damage.bonus')?.value ?? 0;
|
const damageBonus = effect.system.changes.find(x => x.key === 'system.rules.attack.damage.bonus')?.value ?? 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
trait: game.i18n.localize(CONFIG.DH.ACTOR.abilities[mainTrait]?.label),
|
trait: game.i18n.localize(CONFIG.DH.ACTOR.abilities[mainTrait]?.label),
|
||||||
|
|
@ -169,17 +169,17 @@ export default class DHBeastform extends BaseDataItem {
|
||||||
|
|
||||||
const beastformEffect = this.parent.effects.find(x => x.type === 'beastform');
|
const beastformEffect = this.parent.effects.find(x => x.type === 'beastform');
|
||||||
await beastformEffect.updateSource({
|
await beastformEffect.updateSource({
|
||||||
changes: [
|
|
||||||
...beastformEffect.changes,
|
|
||||||
{
|
|
||||||
key: 'system.advantageSources',
|
|
||||||
mode: 2,
|
|
||||||
value: Object.values(this.advantageOn)
|
|
||||||
.map(x => x.value)
|
|
||||||
.join(', ')
|
|
||||||
}
|
|
||||||
],
|
|
||||||
system: {
|
system: {
|
||||||
|
changes: [
|
||||||
|
...beastformEffect.system.changes,
|
||||||
|
{
|
||||||
|
key: 'system.advantageSources',
|
||||||
|
mode: 2,
|
||||||
|
value: Object.values(this.advantageOn)
|
||||||
|
.map(x => x.value)
|
||||||
|
.join(', ')
|
||||||
|
}
|
||||||
|
],
|
||||||
characterTokenData: {
|
characterTokenData: {
|
||||||
usesDynamicToken: this.parent.parent.prototypeToken.ring.enabled,
|
usesDynamicToken: this.parent.parent.prototypeToken.ring.enabled,
|
||||||
tokenImg: this.parent.parent.prototypeToken.texture.src,
|
tokenImg: this.parent.parent.prototypeToken.texture.src,
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,9 @@ export default class DHSubclass extends BaseDataItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _preCreate(data, options, user) {
|
async _preCreate(data, options, user) {
|
||||||
|
const allowed = await super._preCreate(data, options, user);
|
||||||
|
if (allowed === false) return;
|
||||||
|
|
||||||
if (this.actor?.type === 'character') {
|
if (this.actor?.type === 'character') {
|
||||||
const dataUuid = data.uuid ?? data._stats.compendiumSource ?? `Item.${data._id}`;
|
const dataUuid = data.uuid ?? data._stats.compendiumSource ?? `Item.${data._id}`;
|
||||||
if (this.actor.system.class.subclass) {
|
if (this.actor.system.class.subclass) {
|
||||||
|
|
@ -86,9 +89,6 @@ export default class DHSubclass extends BaseDataItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowed = await super._preCreate(data, options, user);
|
|
||||||
if (allowed === false) return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**@inheritdoc */
|
/**@inheritdoc */
|
||||||
|
|
|
||||||
|
|
@ -63,15 +63,15 @@ export default class DHWeapon extends AttachableItem {
|
||||||
type: 'attack'
|
type: 'attack'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
parts: [
|
parts: {
|
||||||
{
|
hitPoints: {
|
||||||
type: ['physical'],
|
type: ['physical'],
|
||||||
value: {
|
value: {
|
||||||
multiplier: 'prof',
|
multiplier: 'prof',
|
||||||
dice: 'd8'
|
dice: 'd8'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
@ -148,7 +148,7 @@ export default class DHWeapon extends AttachableItem {
|
||||||
|
|
||||||
await this.parent.deleteEmbeddedDocuments('ActiveEffect', removedEffectsUpdate);
|
await this.parent.deleteEmbeddedDocuments('ActiveEffect', removedEffectsUpdate);
|
||||||
changes.system.actions = removedActionsUpdate.reduce((acc, id) => {
|
changes.system.actions = removedActionsUpdate.reduce((acc, id) => {
|
||||||
acc[`-=${id}`] = null;
|
acc[id] = _del;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,11 @@ export default class DhAutomation extends foundry.abstract.DataModel {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
autoExpireActiveEffects: new fields.BooleanField({
|
||||||
|
required: true,
|
||||||
|
initial: true,
|
||||||
|
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.autoExpireActiveEffects.label'
|
||||||
|
}),
|
||||||
triggers: new fields.SchemaField({
|
triggers: new fields.SchemaField({
|
||||||
enabled: new fields.BooleanField({
|
enabled: new fields.BooleanField({
|
||||||
nullable: false,
|
nullable: false,
|
||||||
|
|
|
||||||
47
module/data/tagTeamData.mjs
Normal file
47
module/data/tagTeamData.mjs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
export default class TagTeamData extends foundry.abstract.DataModel {
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
return {
|
||||||
|
initiator: new fields.SchemaField(
|
||||||
|
{
|
||||||
|
memberId: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
label: 'DAGGERHEART.APPLICATIONS.TagTeamSelect.FIELDS.initiator.memberId.label'
|
||||||
|
}),
|
||||||
|
cost: new fields.NumberField({
|
||||||
|
integer: true,
|
||||||
|
initial: 3,
|
||||||
|
label: 'DAGGERHEART.APPLICATIONS.TagTeamSelect.FIELDS.initiator.cost.label'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ nullable: true, initial: null }
|
||||||
|
),
|
||||||
|
members: new fields.TypedObjectField(new fields.EmbeddedDataField(MemberData))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MemberData extends foundry.abstract.DataModel {
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: new fields.StringField({ required: true }),
|
||||||
|
img: new fields.StringField({ required: true }),
|
||||||
|
rollType: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
choices: CONFIG.DH.GENERAL.tagTeamRollTypes,
|
||||||
|
initial: CONFIG.DH.GENERAL.tagTeamRollTypes.trait.id,
|
||||||
|
label: 'Roll Type'
|
||||||
|
}),
|
||||||
|
rollChoice: new fields.StringField({ nullable: true, initial: null }),
|
||||||
|
rollData: new fields.JSONField({ nullable: true, initial: null }),
|
||||||
|
selected: new fields.BooleanField({ initial: false })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get roll() {
|
||||||
|
return this.rollData ? CONFIG.Dice.daggerheart.DualityRoll.fromData(this.rollData) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { DhCharacter } from './actor/_module.mjs';
|
|
||||||
|
|
||||||
export default class DhTagTeamRoll extends foundry.abstract.DataModel {
|
|
||||||
static defineSchema() {
|
|
||||||
const fields = foundry.data.fields;
|
|
||||||
|
|
||||||
return {
|
|
||||||
initiator: new fields.SchemaField({
|
|
||||||
id: new fields.StringField({ nullable: true, initial: null }),
|
|
||||||
cost: new fields.NumberField({ integer: true, min: 0, initial: 3 })
|
|
||||||
}),
|
|
||||||
members: new fields.TypedObjectField(
|
|
||||||
new fields.SchemaField({
|
|
||||||
messageId: new fields.StringField({ required: true, nullable: true, initial: null }),
|
|
||||||
selected: new fields.BooleanField({ required: true, initial: false })
|
|
||||||
})
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import DamageDialog from '../applications/dialogs/damageDialog.mjs';
|
import DamageDialog from '../applications/dialogs/damageDialog.mjs';
|
||||||
import { parseRallyDice } from '../helpers/utils.mjs';
|
import { parseRallyDice } from '../helpers/utils.mjs';
|
||||||
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
|
|
||||||
import DHRoll from './dhRoll.mjs';
|
import DHRoll from './dhRoll.mjs';
|
||||||
|
|
||||||
export default class DamageRoll extends DHRoll {
|
export default class DamageRoll extends DHRoll {
|
||||||
|
|
@ -34,7 +33,7 @@ export default class DamageRoll extends DHRoll {
|
||||||
static async buildPost(roll, config, message) {
|
static async buildPost(roll, config, message) {
|
||||||
const chatMessage = config.source?.message
|
const chatMessage = config.source?.message
|
||||||
? ui.chat.collection.get(config.source.message)
|
? ui.chat.collection.get(config.source.message)
|
||||||
: getDocumentClass('ChatMessage').applyRollMode({}, config.rollMode ?? CONST.DICE_ROLL_MODES.PUBLIC);
|
: getDocumentClass('ChatMessage').applyMode({}, config.rollMode ?? 'public');
|
||||||
if (game.modules.get('dice-so-nice')?.active) {
|
if (game.modules.get('dice-so-nice')?.active) {
|
||||||
const pool = foundry.dice.terms.PoolTerm.fromRolls(
|
const pool = foundry.dice.terms.PoolTerm.fromRolls(
|
||||||
Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll))
|
Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll))
|
||||||
|
|
@ -197,7 +196,7 @@ export default class DamageRoll extends DHRoll {
|
||||||
if (config.data.parent.appliedEffects) {
|
if (config.data.parent.appliedEffects) {
|
||||||
// Bardic Rally
|
// Bardic Rally
|
||||||
const rallyChoices = config.data?.parent?.appliedEffects.reduce((a, c) => {
|
const rallyChoices = config.data?.parent?.appliedEffects.reduce((a, c) => {
|
||||||
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
|
const change = c.system.changes.find(ch => ch.key === 'system.bonuses.rally');
|
||||||
if (change) a.push({ value: c.id, label: parseRallyDice(change.value, c) });
|
if (change) a.push({ value: c.id, label: parseRallyDice(change.value, c) });
|
||||||
return a;
|
return a;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -281,10 +280,7 @@ export default class DamageRoll extends DHRoll {
|
||||||
return mods;
|
return mods;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async reroll(target, message) {
|
static async reroll(rollPart, dice, result) {
|
||||||
const { damageType, part, dice, result } = target.dataset;
|
|
||||||
const rollPart = message.system.damage[damageType].parts[part];
|
|
||||||
|
|
||||||
let diceIndex = 0;
|
let diceIndex = 0;
|
||||||
let parsedRoll = game.system.api.dice.DamageRoll.fromData({
|
let parsedRoll = game.system.api.dice.DamageRoll.fromData({
|
||||||
...rollPart.roll,
|
...rollPart.roll,
|
||||||
|
|
@ -353,29 +349,6 @@ export default class DamageRoll extends DHRoll {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateMessage = game.messages.get(message._id);
|
return { parsedRoll, rerolledDice };
|
||||||
const damageParts = updateMessage.system.damage[damageType].parts.map((damagePart, index) => {
|
|
||||||
if (index !== Number(part)) return damagePart;
|
|
||||||
return {
|
|
||||||
...rollPart,
|
|
||||||
total: parsedRoll.total,
|
|
||||||
dice: rerolledDice
|
|
||||||
};
|
|
||||||
});
|
|
||||||
await updateMessage.update({
|
|
||||||
[`system.damage.${damageType}`]: {
|
|
||||||
...updateMessage,
|
|
||||||
total: parsedRoll.total,
|
|
||||||
parts: damageParts
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
|
|
||||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
|
||||||
action: socketEvent.Refresh,
|
|
||||||
data: {
|
|
||||||
refreshType: RefreshType.TagTeamRoll
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ export default class DHRoll extends Roll {
|
||||||
static async build(config = {}, message = {}) {
|
static async build(config = {}, message = {}) {
|
||||||
const roll = await this.buildConfigure(config, message);
|
const roll = await this.buildConfigure(config, message);
|
||||||
if (!roll) return;
|
if (!roll) return;
|
||||||
|
|
||||||
|
if (config.skips?.createMessage) config.messageRoll = roll;
|
||||||
|
|
||||||
await this.buildEvaluate(roll, config, (message = {}));
|
await this.buildEvaluate(roll, config, (message = {}));
|
||||||
await this.buildPost(roll, config, (message = {}));
|
await this.buildPost(roll, config, (message = {}));
|
||||||
return config;
|
return config;
|
||||||
|
|
@ -30,12 +33,6 @@ export default class DHRoll extends Roll {
|
||||||
config.hooks = [...this.getHooks(), ''];
|
config.hooks = [...this.getHooks(), ''];
|
||||||
config.dialog ??= {};
|
config.dialog ??= {};
|
||||||
|
|
||||||
const actorIdSplit = config.source?.actor?.split('.');
|
|
||||||
if (actorIdSplit) {
|
|
||||||
const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
|
|
||||||
config.tagTeamSelected = Boolean(tagTeamSettings.members[actorIdSplit[actorIdSplit.length - 1]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const hook of config.hooks) {
|
for (const hook of config.hooks) {
|
||||||
if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null;
|
if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null;
|
||||||
}
|
}
|
||||||
|
|
@ -120,10 +117,10 @@ export default class DHRoll extends Roll {
|
||||||
rolls: [roll]
|
rolls: [roll]
|
||||||
};
|
};
|
||||||
|
|
||||||
config.selectedRollMode ??= game.settings.get('core', 'rollMode');
|
config.selectedMessageMode ??= game.settings.get('core', 'messageMode');
|
||||||
|
|
||||||
if (roll._evaluated) {
|
if (roll._evaluated) {
|
||||||
const message = await cls.create(msgData, { rollMode: config.selectedRollMode });
|
const message = await cls.create(msgData, { messageMode: config.selectedMessageMode });
|
||||||
|
|
||||||
if (config.tagTeamSelected) {
|
if (config.tagTeamSelected) {
|
||||||
game.system.api.applications.dialogs.TagTeamDialog.assignRoll(message.speakerActor, message);
|
game.system.api.applications.dialogs.TagTeamDialog.assignRoll(message.speakerActor, message);
|
||||||
|
|
@ -269,12 +266,12 @@ export default class DHRoll extends Roll {
|
||||||
const changeKeys = this.getActionChangeKeys();
|
const changeKeys = this.getActionChangeKeys();
|
||||||
return (
|
return (
|
||||||
this.options.effects?.reduce((acc, effect) => {
|
this.options.effects?.reduce((acc, effect) => {
|
||||||
if (effect.changes.some(x => changeKeys.some(key => x.key.includes(key)))) {
|
if (effect.system.changes.some(x => changeKeys.some(key => x.key.includes(key)))) {
|
||||||
acc[effect.id] = {
|
acc[effect.id] = {
|
||||||
id: effect.id,
|
id: effect.id,
|
||||||
name: effect.name,
|
name: effect.name,
|
||||||
description: effect.description,
|
description: effect.description,
|
||||||
changes: effect.changes,
|
changes: effect.system.changes,
|
||||||
origEffect: effect,
|
origEffect: effect,
|
||||||
selected: !effect.disabled
|
selected: !effect.disabled
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ export default class DualityRoll extends D20Roll {
|
||||||
|
|
||||||
setRallyChoices() {
|
setRallyChoices() {
|
||||||
return this.data?.parent?.appliedEffects.reduce((a, c) => {
|
return this.data?.parent?.appliedEffects.reduce((a, c) => {
|
||||||
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
|
const change = c.system.changes.find(ch => ch.key === 'system.bonuses.rally');
|
||||||
if (change) a.push({ value: c.id, label: parseRallyDice(change.value, c) });
|
if (change) a.push({ value: c.id, label: parseRallyDice(change.value, c) });
|
||||||
return a;
|
return a;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -179,7 +179,7 @@ export default class DualityRoll extends D20Roll {
|
||||||
static async buildConfigure(config = {}, message = {}) {
|
static async buildConfigure(config = {}, message = {}) {
|
||||||
config.dialog ??= {};
|
config.dialog ??= {};
|
||||||
config.guaranteedCritical = config.data?.parent?.appliedEffects.reduce((a, c) => {
|
config.guaranteedCritical = config.data?.parent?.appliedEffects.reduce((a, c) => {
|
||||||
const change = c.changes.find(ch => ch.key === 'system.rules.roll.guaranteedCritical');
|
const change = c.system.changes.find(ch => ch.key === 'system.rules.roll.guaranteedCritical');
|
||||||
if (change) a = true;
|
if (change) a = true;
|
||||||
return a;
|
return a;
|
||||||
}, false);
|
}, false);
|
||||||
|
|
@ -374,9 +374,9 @@ export default class DualityRoll extends D20Roll {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async reroll(rollString, target, message) {
|
static async reroll(rollBase, dieIndex, diceType) {
|
||||||
let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollString, evaluated: false });
|
let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollBase, evaluated: false });
|
||||||
const term = parsedRoll.terms[target.dataset.dieIndex];
|
const term = parsedRoll.terms[dieIndex];
|
||||||
await term.reroll(`/r1=${term.total}`);
|
await term.reroll(`/r1=${term.total}`);
|
||||||
const result = await parsedRoll.evaluate();
|
const result = await parsedRoll.evaluate();
|
||||||
|
|
||||||
|
|
@ -393,35 +393,35 @@ export default class DualityRoll extends D20Roll {
|
||||||
options: { appearance: {} }
|
options: { appearance: {} }
|
||||||
};
|
};
|
||||||
|
|
||||||
const diceSoNicePresets = await getDiceSoNicePresets(result, `d${term._faces}`, `d${term._faces}`);
|
const diceSoNicePresets = await getDiceSoNicePresets(`d${term._faces}`, `d${term._faces}`);
|
||||||
const type = target.dataset.type;
|
if (diceSoNicePresets[diceType]) {
|
||||||
if (diceSoNicePresets[type]) {
|
diceSoNiceRoll.dice[0].options = diceSoNicePresets[diceType];
|
||||||
diceSoNiceRoll.dice[0].options = diceSoNicePresets[type];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
|
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
|
||||||
|
} else {
|
||||||
|
foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRoll = game.system.api.dice.DualityRoll.postEvaluate(parsedRoll, {
|
const newRoll = game.system.api.dice.DualityRoll.postEvaluate(parsedRoll, {
|
||||||
targets: message.system.targets,
|
targets: parsedRoll.options.targets ?? [],
|
||||||
roll: {
|
roll: {
|
||||||
advantage: message.system.roll.advantage?.type,
|
advantage: parsedRoll.options.roll.advantage?.type,
|
||||||
difficulty: message.system.roll.difficulty ? Number(message.system.roll.difficulty) : null
|
difficulty: parsedRoll.options.roll.difficulty ? Number(parsedRoll.options.roll.difficulty) : null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const extraIndex = newRoll.advantage ? 3 : 2;
|
const extraIndex = newRoll.advantage ? 3 : 2;
|
||||||
newRoll.extra = newRoll.extra.slice(extraIndex);
|
newRoll.extra = newRoll.extra.slice(extraIndex);
|
||||||
|
|
||||||
const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
|
const actor = parsedRoll.options.source.actor
|
||||||
|
? await foundry.utils.fromUuid(parsedRoll.options.source.actor)
|
||||||
const actor = message.system.source.actor ? await foundry.utils.fromUuid(message.system.source.actor) : null;
|
: null;
|
||||||
const config = {
|
const config = {
|
||||||
source: { actor: message.system.source.actor ?? '' },
|
source: { actor: parsedRoll.options.source.actor ?? '' },
|
||||||
targets: message.system.targets,
|
targets: parsedRoll.targets,
|
||||||
tagTeamSelected: Object.values(tagTeamSettings.members).some(x => x.messageId === message._id),
|
|
||||||
roll: newRoll,
|
roll: newRoll,
|
||||||
rerolledRoll: message.system.roll,
|
rerolledRoll: parsedRoll.roll,
|
||||||
resourceUpdates: new ResourceUpdateMap(actor)
|
resourceUpdates: new ResourceUpdateMap(actor)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,4 @@ export { default as DhRollTable } from './rollTable.mjs';
|
||||||
export { default as DhScene } from './scene.mjs';
|
export { default as DhScene } from './scene.mjs';
|
||||||
export { default as DhToken } from './token.mjs';
|
export { default as DhToken } from './token.mjs';
|
||||||
export { default as DhTooltipManager } from './tooltipManager.mjs';
|
export { default as DhTooltipManager } from './tooltipManager.mjs';
|
||||||
export { default as DhTemplateManager } from './templateManager.mjs';
|
|
||||||
export { default as DhTokenManager } from './tokenManager.mjs';
|
export { default as DhTokenManager } from './tokenManager.mjs';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { itemAbleRollParse } from '../helpers/utils.mjs';
|
import { itemAbleRollParse } from '../helpers/utils.mjs';
|
||||||
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
|
import { RefreshType } from '../systemRegistration/socket.mjs';
|
||||||
|
|
||||||
export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
@ -8,6 +8,8 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
||||||
|
|
||||||
/**@override */
|
/**@override */
|
||||||
get isSuppressed() {
|
get isSuppressed() {
|
||||||
|
if (this.system.isSuppressed === true) return true;
|
||||||
|
|
||||||
// If this is a copied effect from an attachment, never suppress it
|
// If this is a copied effect from an attachment, never suppress it
|
||||||
// (These effects have attachmentSource metadata)
|
// (These effects have attachmentSource metadata)
|
||||||
if (this.flags?.daggerheart?.attachmentSource) {
|
if (this.flags?.daggerheart?.attachmentSource) {
|
||||||
|
|
@ -15,7 +17,7 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then apply the standard suppression rules
|
// Then apply the standard suppression rules
|
||||||
if (['weapon', 'armor'].includes(this.parent?.type)) {
|
if (['weapon', 'armor'].includes(this.parent?.type) && this.transfer) {
|
||||||
return !this.parent.system.equipped;
|
return !this.parent.system.equipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,10 +52,55 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this Active Effect is eligible to be registered with the {@link ActiveEffectRegistry}
|
||||||
|
*/
|
||||||
|
get isExpiryTrackable() {
|
||||||
|
return (
|
||||||
|
this.persisted &&
|
||||||
|
!this.inCompendium &&
|
||||||
|
this.modifiesActor &&
|
||||||
|
this.start &&
|
||||||
|
this.isTemporary &&
|
||||||
|
!this.isExpired
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
/* Event Handlers */
|
/* Event Handlers */
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
static async createDialog(data = {}, createOptions = {}, options = {}) {
|
||||||
|
const { folders, types, template, context = {}, ...dialogOptions } = options;
|
||||||
|
|
||||||
|
if (types?.length === 0) {
|
||||||
|
throw new Error('The array of sub-types to restrict to must not be empty.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const creatableEffects = types || ['base'];
|
||||||
|
const documentTypes = this.TYPES.filter(type => creatableEffects.includes(type)).map(type => {
|
||||||
|
const labelKey = `TYPES.ActiveEffect.${type}`;
|
||||||
|
const label = game.i18n.has(labelKey) ? game.i18n.localize(labelKey) : type;
|
||||||
|
|
||||||
|
return { value: type, label };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!documentTypes.length) {
|
||||||
|
throw new Error('No document types were permitted to be created.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedTypes = documentTypes.sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang));
|
||||||
|
|
||||||
|
return await super.createDialog(data, createOptions, {
|
||||||
|
folders,
|
||||||
|
types,
|
||||||
|
template,
|
||||||
|
context: { types: sortedTypes, ...context },
|
||||||
|
...dialogOptions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**@inheritdoc*/
|
/**@inheritdoc*/
|
||||||
async _preCreate(data, options, user) {
|
async _preCreate(data, options, user) {
|
||||||
const update = {};
|
const update = {};
|
||||||
|
|
@ -61,6 +108,18 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
||||||
update.img = 'icons/magic/life/heart-cross-blue.webp';
|
update.img = 'icons/magic/life/heart-cross-blue.webp';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existingEffect = this.actor.effects.find(x => x.origin === data.origin);
|
||||||
|
const stacks = Boolean(data.system?.stacking);
|
||||||
|
if (existingEffect && !stacks) return false;
|
||||||
|
|
||||||
|
if (existingEffect && stacks) {
|
||||||
|
const incrementedValue = existingEffect.system.stacking.value + 1;
|
||||||
|
await existingEffect.update({
|
||||||
|
'system.stacking.value': Math.min(incrementedValue, existingEffect.system.stacking.max ?? Infinity)
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const statuses = Object.keys(data.statuses ?? {});
|
const statuses = Object.keys(data.statuses ?? {});
|
||||||
const immuneStatuses =
|
const immuneStatuses =
|
||||||
statuses.filter(
|
statuses.filter(
|
||||||
|
|
@ -109,23 +168,24 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/**@inheritdoc*/
|
/**@inheritdoc*/
|
||||||
static applyField(model, change, field) {
|
static applyChangeField(model, change, field) {
|
||||||
change.value = DhActiveEffect.getChangeValue(model, change, change.effect);
|
change.value = Number.isNumeric(change.value)
|
||||||
super.applyField(model, change, field);
|
? change.value
|
||||||
|
: DhActiveEffect.getChangeValue(model, change, change.effect);
|
||||||
|
super.applyChangeField(model, change, field);
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyLegacy(actor, change, changes) {
|
static _applyChangeUnguided(actor, change, changes, options) {
|
||||||
change.value = DhActiveEffect.getChangeValue(actor, change, change.effect);
|
change.value = DhActiveEffect.getChangeValue(actor, change, change.effect);
|
||||||
super._applyLegacy(actor, change, changes);
|
super._applyChangeUnguided(actor, change, changes, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** */
|
|
||||||
static getChangeValue(model, change, effect) {
|
static getChangeValue(model, change, effect) {
|
||||||
let value = change.value;
|
let key = change.value.toString();
|
||||||
const isOriginTarget = value.toLowerCase().includes('origin.@');
|
const isOriginTarget = key.toLowerCase().includes('origin.@');
|
||||||
let parseModel = model;
|
let parseModel = model;
|
||||||
if (isOriginTarget && effect.origin) {
|
if (isOriginTarget && effect.origin) {
|
||||||
value = change.value.replaceAll(/origin\.@/gi, '@');
|
key = change.key.replaceAll(/origin\.@/gi, '@');
|
||||||
try {
|
try {
|
||||||
const originEffect = foundry.utils.fromUuidSync(effect.origin);
|
const originEffect = foundry.utils.fromUuidSync(effect.origin);
|
||||||
const doc =
|
const doc =
|
||||||
|
|
@ -136,8 +196,11 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const evalValue = this.effectSafeEval(itemAbleRollParse(value, parseModel, effect.parent));
|
const stackingParsedValue = effect.system.stacking
|
||||||
return evalValue ?? value;
|
? Roll.replaceFormulaData(key, { stacks: effect.system.stacking.value })
|
||||||
|
: key;
|
||||||
|
const evalValue = itemAbleRollParse(stackingParsedValue, parseModel, effect.parent);
|
||||||
|
return evalValue ?? key;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import DHFeature from '../data/item/feature.mjs';
|
||||||
import { createScrollText, damageKeyToNumber, getDamageKey } from '../helpers/utils.mjs';
|
import { createScrollText, damageKeyToNumber, getDamageKey } from '../helpers/utils.mjs';
|
||||||
import DhCompanionLevelUp from '../applications/levelup/companionLevelup.mjs';
|
import DhCompanionLevelUp from '../applications/levelup/companionLevelup.mjs';
|
||||||
import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
|
import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
|
||||||
|
import { abilities } from '../config/actorConfig.mjs';
|
||||||
|
|
||||||
export default class DhpActor extends Actor {
|
export default class DhpActor extends Actor {
|
||||||
parties = new Set();
|
parties = new Set();
|
||||||
|
|
@ -29,6 +30,18 @@ export default class DhpActor extends Actor {
|
||||||
return this.system.metadata.isNPC;
|
return this.system.metadata.isNPC;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prepareData() {
|
||||||
|
super.prepareData();
|
||||||
|
|
||||||
|
// Update effects if it is the user's character or is controlled
|
||||||
|
if (canvas.ready) {
|
||||||
|
const controlled = canvas.tokens.controlled.some(t => t.actor === this);
|
||||||
|
if (game.user.character === this || controlled) {
|
||||||
|
ui.effectsDisplay.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
|
|
@ -142,7 +155,7 @@ export default class DhpActor extends Actor {
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedLevelups = Object.keys(this.system.levelData.levelups).reduce((acc, level) => {
|
const updatedLevelups = Object.keys(this.system.levelData.levelups).reduce((acc, level) => {
|
||||||
if (Number(level) > usedLevel) acc[`-=${level}`] = null;
|
if (Number(level) > usedLevel) acc[level] = _del;
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
@ -187,7 +200,7 @@ export default class DhpActor extends Actor {
|
||||||
if (experiences.length > 0) {
|
if (experiences.length > 0) {
|
||||||
const getUpdate = () => ({
|
const getUpdate = () => ({
|
||||||
'system.experiences': experiences.reduce((acc, key) => {
|
'system.experiences': experiences.reduce((acc, key) => {
|
||||||
acc[`-=${key}`] = null;
|
acc[key] = _del;
|
||||||
return acc;
|
return acc;
|
||||||
}, {})
|
}, {})
|
||||||
});
|
});
|
||||||
|
|
@ -509,6 +522,30 @@ export default class DhpActor extends Actor {
|
||||||
return await rollClass.build(config);
|
return await rollClass.build(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async rollTrait(trait, options = {}) {
|
||||||
|
const abilityLabel = game.i18n.localize(abilities[trait].label);
|
||||||
|
const config = {
|
||||||
|
event: event,
|
||||||
|
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.name}`,
|
||||||
|
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
||||||
|
ability: abilityLabel
|
||||||
|
}),
|
||||||
|
effects: await game.system.api.data.actions.actionsTypes.base.getEffects(this),
|
||||||
|
roll: {
|
||||||
|
trait: trait,
|
||||||
|
type: 'trait'
|
||||||
|
},
|
||||||
|
hasRoll: true,
|
||||||
|
actionType: 'action',
|
||||||
|
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.name}`,
|
||||||
|
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
||||||
|
ability: abilityLabel
|
||||||
|
}),
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
return await this.diceRoll(config);
|
||||||
|
}
|
||||||
|
|
||||||
get rollClass() {
|
get rollClass() {
|
||||||
return CONFIG.Dice.daggerheart[['character', 'companion'].includes(this.type) ? 'DualityRoll' : 'D20Roll'];
|
return CONFIG.Dice.daggerheart[['character', 'companion'].includes(this.type) ? 'DualityRoll' : 'D20Roll'];
|
||||||
}
|
}
|
||||||
|
|
@ -573,8 +610,7 @@ export default class DhpActor extends Actor {
|
||||||
const availableStress = this.system.resources.stress.max - this.system.resources.stress.value;
|
const availableStress = this.system.resources.stress.max - this.system.resources.stress.value;
|
||||||
|
|
||||||
const canUseArmor =
|
const canUseArmor =
|
||||||
this.system.armor &&
|
this.system.armorScore.value < this.system.armorScore.max &&
|
||||||
this.system.armor.system.marks.value < this.system.armorScore &&
|
|
||||||
type.every(t => this.system.armorApplicableDamageTypes[t] === true);
|
type.every(t => this.system.armorApplicableDamageTypes[t] === true);
|
||||||
const canUseStress = Object.keys(stressDamageReduction).reduce((acc, x) => {
|
const canUseStress = Object.keys(stressDamageReduction).reduce((acc, x) => {
|
||||||
const rule = stressDamageReduction[x];
|
const rule = stressDamageReduction[x];
|
||||||
|
|
@ -614,12 +650,7 @@ export default class DhpActor extends Actor {
|
||||||
const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
|
const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
|
||||||
if (hpDamage?.value) {
|
if (hpDamage?.value) {
|
||||||
hpDamage.value = this.convertDamageToThreshold(hpDamage.value);
|
hpDamage.value = this.convertDamageToThreshold(hpDamage.value);
|
||||||
if (
|
if (this.type === 'character' && !isDirect && this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes)) {
|
||||||
this.type === 'character' &&
|
|
||||||
!isDirect &&
|
|
||||||
this.system.armor &&
|
|
||||||
this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes)
|
|
||||||
) {
|
|
||||||
const armorSlotResult = await this.owner.query(
|
const armorSlotResult = await this.owner.query(
|
||||||
'armorSlot',
|
'armorSlot',
|
||||||
{
|
{
|
||||||
|
|
@ -632,12 +663,10 @@ export default class DhpActor extends Actor {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (armorSlotResult) {
|
if (armorSlotResult) {
|
||||||
const { modifiedDamage, armorSpent, stressSpent } = armorSlotResult;
|
const { modifiedDamage, armorChanges, stressSpent } = armorSlotResult;
|
||||||
updates.find(u => u.key === 'hitPoints').value = modifiedDamage;
|
updates.find(u => u.key === 'hitPoints').value = modifiedDamage;
|
||||||
if (armorSpent) {
|
for (const armorChange of armorChanges) {
|
||||||
const armorUpdate = updates.find(u => u.key === 'armor');
|
updates.push({ value: armorChange.amount, key: 'armor', uuid: armorChange.uuid });
|
||||||
if (armorUpdate) armorUpdate.value += armorSpent;
|
|
||||||
else updates.push({ value: armorSpent, key: 'armor' });
|
|
||||||
}
|
}
|
||||||
if (stressSpent) {
|
if (stressSpent) {
|
||||||
const stressUpdate = updates.find(u => u.key === 'stress');
|
const stressUpdate = updates.find(u => u.key === 'stress');
|
||||||
|
|
@ -774,12 +803,8 @@ export default class DhpActor extends Actor {
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'armor':
|
case 'armor':
|
||||||
if (this.system.armor?.system?.marks) {
|
if (!r.uuid) this.system.updateArmorValue(r);
|
||||||
updates.armor.resources['system.marks.value'] = Math.max(
|
else this.system.updateArmorEffectValue(r);
|
||||||
Math.min(valueFunc(this.system.armor.system.marks, r), this.system.armorScore),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (this.system.resources?.[r.key]) {
|
if (this.system.resources?.[r.key]) {
|
||||||
|
|
@ -995,4 +1020,20 @@ export default class DhpActor extends Actor {
|
||||||
|
|
||||||
return allTokens;
|
return allTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**@inheritdoc */
|
||||||
|
*allApplicableEffects({ noSelfArmor, noTransferArmor } = {}) {
|
||||||
|
for (const effect of this.effects) {
|
||||||
|
if (!noSelfArmor || effect.type !== 'armor') yield effect;
|
||||||
|
}
|
||||||
|
for (const item of this.items) {
|
||||||
|
for (const effect of item.effects) {
|
||||||
|
if (effect.transfer && (!noTransferArmor || effect.type !== 'armor')) yield effect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyActiveEffects(phase) {
|
||||||
|
super.applyActiveEffects(phase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -177,14 +177,6 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
||||||
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(actor, item);
|
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(actor, item);
|
||||||
await this.system.action.workflow.get('damage')?.execute(config, this._id, true);
|
await this.system.action.workflow.get('damage')?.execute(config, this._id, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
|
|
||||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
|
||||||
action: socketEvent.Refresh,
|
|
||||||
data: {
|
|
||||||
refreshType: RefreshType.TagTeamRoll
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onApplyDamage(event) {
|
async onApplyDamage(event) {
|
||||||
|
|
|
||||||
|
|
@ -230,4 +230,14 @@ export default class DHItem extends foundry.documents.Item {
|
||||||
async _preDelete() {
|
async _preDelete() {
|
||||||
this.deleteTriggers();
|
this.deleteTriggers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
|
static migrateData(source) {
|
||||||
|
const documentClass = game.system.api.data.items[`DH${source.type?.capitalize()}`];
|
||||||
|
if (documentClass?.migrateDocumentData) {
|
||||||
|
documentClass.migrateDocumentData(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.migrateData(source);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ export default class DhRollTable extends foundry.documents.RollTable {
|
||||||
}
|
}
|
||||||
|
|
||||||
async toMessage(results, { roll, messageData = {}, messageOptions = {} } = {}) {
|
async toMessage(results, { roll, messageData = {}, messageOptions = {} } = {}) {
|
||||||
messageOptions.rollMode ??= game.settings.get('core', 'rollMode');
|
messageOptions.rollMode ??= game.settings.get('core', 'messageMode');
|
||||||
|
|
||||||
// Construct chat data
|
// Construct chat data
|
||||||
messageData = foundry.utils.mergeObject(
|
messageData = foundry.utils.mergeObject(
|
||||||
|
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
/**
|
|
||||||
* A singleton class that handles preview templates.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default class DhTemplateManager {
|
|
||||||
#activePreview;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a template preview, deactivating any existing ones.
|
|
||||||
* @param {object} data
|
|
||||||
*/
|
|
||||||
async createPreview(data) {
|
|
||||||
const template = await canvas.templates._createPreview(data, { renderSheet: false });
|
|
||||||
|
|
||||||
this.#activePreview = {
|
|
||||||
document: template.document,
|
|
||||||
object: template,
|
|
||||||
origin: { x: template.document.x, y: template.document.y }
|
|
||||||
};
|
|
||||||
|
|
||||||
this.#activePreview.events = {
|
|
||||||
contextmenu: this.#cancelTemplate.bind(this),
|
|
||||||
mousedown: this.#confirmTemplate.bind(this),
|
|
||||||
mousemove: this.#onDragMouseMove.bind(this),
|
|
||||||
wheel: this.#onMouseWheel.bind(this)
|
|
||||||
};
|
|
||||||
canvas.stage.on('mousemove', this.#activePreview.events.mousemove);
|
|
||||||
canvas.stage.on('mousedown', this.#activePreview.events.mousedown);
|
|
||||||
|
|
||||||
canvas.app.view.addEventListener('wheel', this.#activePreview.events.wheel, true);
|
|
||||||
canvas.app.view.addEventListener('contextmenu', this.#activePreview.events.contextmenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the movement of the temlate preview on mousedrag.
|
|
||||||
* @param {mousemove Event} event
|
|
||||||
*/
|
|
||||||
#onDragMouseMove(event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
const { moveTime, object } = this.#activePreview;
|
|
||||||
const update = {};
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - (moveTime || 0) <= 16) return;
|
|
||||||
this.#activePreview.moveTime = now;
|
|
||||||
|
|
||||||
let cursor = event.getLocalPosition(canvas.templates);
|
|
||||||
|
|
||||||
Object.assign(update, canvas.grid.getCenterPoint(cursor));
|
|
||||||
|
|
||||||
object.document.updateSource(update);
|
|
||||||
object.renderFlags.set({ refresh: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the rotation of the preview template on scrolling.
|
|
||||||
* @param {wheel Event} event
|
|
||||||
*/
|
|
||||||
#onMouseWheel(event) {
|
|
||||||
if (!this.#activePreview) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!event.shiftKey && !event.ctrlKey) return;
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
const { moveTime, object } = this.#activePreview;
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - (moveTime || 0) <= 16) return;
|
|
||||||
this.#activePreview.moveTime = now;
|
|
||||||
|
|
||||||
const multiplier = event.shiftKey ? 0.2 : 0.1;
|
|
||||||
|
|
||||||
object.document.updateSource({
|
|
||||||
direction: object.document.direction + event.deltaY * multiplier
|
|
||||||
});
|
|
||||||
object.renderFlags.set({ refresh: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancels the preview template on right-click.
|
|
||||||
* @param {contextmenu Event} event
|
|
||||||
*/
|
|
||||||
#cancelTemplate(event) {
|
|
||||||
const { mousemove, mousedown, contextmenu, wheel } = this.#activePreview.events;
|
|
||||||
canvas.templates._onDragLeftCancel(event);
|
|
||||||
|
|
||||||
canvas.stage.off('mousemove', mousemove);
|
|
||||||
canvas.stage.off('mousedown', mousedown);
|
|
||||||
canvas.app.view.removeEventListener('contextmenu', contextmenu);
|
|
||||||
canvas.app.view.removeEventListener('wheel', wheel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a real MeasuredTemplate at the preview location and cancels the preview.
|
|
||||||
* @param {click Event} event
|
|
||||||
*/
|
|
||||||
#confirmTemplate(event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
this.#cancelTemplate(event);
|
|
||||||
|
|
||||||
canvas.scene.createEmbeddedDocuments('MeasuredTemplate', [this.#activePreview.document.toObject()]);
|
|
||||||
this.#activePreview = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -494,4 +494,62 @@ export default class DHToken extends CONFIG.Token.documentClass {
|
||||||
game.system.registeredTriggers.unregisterItemTriggers(this.actor.items);
|
game.system.registeredTriggers.unregisterItemTriggers(this.actor.items);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* V14 TEMP until foundry fixes: https://discord.com/channels/170995199584108546/1421197211194228907/1467296028700049566 */
|
||||||
|
_onRelatedUpdate(update = {}, operation = {}) {
|
||||||
|
this.#refreshOverrides(operation);
|
||||||
|
this._prepareBars();
|
||||||
|
|
||||||
|
// Update tracked Combat resource
|
||||||
|
const combatant = this.combatant;
|
||||||
|
if (combatant) {
|
||||||
|
const isActorUpdate = [this, null, undefined].includes(operation.parent);
|
||||||
|
const resource = game.combat.settings.resource;
|
||||||
|
const updates = Array.isArray(update) ? update : [update];
|
||||||
|
if (isActorUpdate && resource && updates.some(u => foundry.utils.hasProperty(u.system ?? {}, resource))) {
|
||||||
|
combatant.updateResource();
|
||||||
|
}
|
||||||
|
ui.combat.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger redraws on the token
|
||||||
|
if (this.parent.isView) {
|
||||||
|
if (this.object?.hasActiveHUD) canvas.tokens.hud.render();
|
||||||
|
this.object?.renderFlags.set({ redrawEffects: true });
|
||||||
|
for (const key of ['bar1', 'bar2']) {
|
||||||
|
const name = `${this.object?.objectId}.animate${key.capitalize()}`;
|
||||||
|
const easing = foundry.canvas.animation.CanvasAnimation.easeInOutCosine;
|
||||||
|
this.object?.animate({ [key]: this[key] }, { name, easing });
|
||||||
|
}
|
||||||
|
for (const app of foundry.applications.sheets.TokenConfig.instances()) {
|
||||||
|
app._preview?.updateSource({ delta: this.toObject().delta }, { diff: false, recursive: false });
|
||||||
|
app._preview?.object?.renderFlags.set({ refreshBars: true, redrawEffects: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* V14 TEMP until foundry fixes: https://discord.com/channels/170995199584108546/1421197211194228907/1467296028700049566 */
|
||||||
|
#refreshOverrides(operation) {
|
||||||
|
if (!this.actor) return;
|
||||||
|
|
||||||
|
const { deepClone, mergeObject, equals, isEmpty } = foundry.utils;
|
||||||
|
const oldOverrides = deepClone(this._overrides) ?? {};
|
||||||
|
const newOverrides = deepClone(this.actor?.tokenOverrides ?? {}, { prune: true });
|
||||||
|
if (!equals(oldOverrides, newOverrides)) {
|
||||||
|
this._overrides = newOverrides;
|
||||||
|
this.reset();
|
||||||
|
|
||||||
|
// Send emulated update data to the PlaceableObject
|
||||||
|
if (!canvas.ready || canvas.scene !== this.scene) return;
|
||||||
|
const { width, height, depth, ...changes } = mergeObject(
|
||||||
|
mergeObject(oldOverrides, this, { insertKeys: false, insertValues: false }),
|
||||||
|
this._overrides
|
||||||
|
);
|
||||||
|
this.object?._onUpdate(changes, {}, game.user.id);
|
||||||
|
|
||||||
|
// Hand off size changes to a secondary handler requiring downstream implementation.
|
||||||
|
const sizeChanges = deepClone({ width, height, depth }, { prune: true });
|
||||||
|
if (!isEmpty(sizeChanges)) this._onOverrideSize(sizeChanges, operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,8 @@ export default function DhTemplateEnricher(match, _options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const renderMeasuredTemplate = async event => {
|
export const renderMeasuredTemplate = async event => {
|
||||||
|
const { LINE, RECTANGLE, INFRONT, CONE } = CONFIG.DH.GENERAL.templateTypes;
|
||||||
|
|
||||||
const button = event.currentTarget,
|
const button = event.currentTarget,
|
||||||
type = button.dataset.type,
|
type = button.dataset.type,
|
||||||
range = button.dataset.range,
|
range = button.dataset.range,
|
||||||
|
|
@ -57,13 +59,9 @@ export const renderMeasuredTemplate = async event => {
|
||||||
|
|
||||||
if (!type || !range || !game.canvas.scene) return;
|
if (!type || !range || !game.canvas.scene) return;
|
||||||
|
|
||||||
const usedType = type === 'inFront' ? 'cone' : type === 'emanation' ? 'circle' : type;
|
const usedType = type === 'inFront' ? 'cone' : type;
|
||||||
const usedAngle =
|
const usedAngle =
|
||||||
type === CONST.MEASURED_TEMPLATE_TYPES.CONE
|
type === CONE ? (angle ?? CONFIG.MeasuredTemplate.defaults.angle) : type === INFRONT ? '180' : undefined;
|
||||||
? (angle ?? CONFIG.MeasuredTemplate.defaults.angle)
|
|
||||||
: type === CONFIG.DH.GENERAL.templateTypes.INFRONT
|
|
||||||
? '180'
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
let baseDistance = range;
|
let baseDistance = range;
|
||||||
if (Number.isNaN(Number(range))) {
|
if (Number.isNaN(Number(range))) {
|
||||||
|
|
@ -71,18 +69,49 @@ export const renderMeasuredTemplate = async event => {
|
||||||
range
|
range
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
const distance = type === CONFIG.DH.GENERAL.templateTypes.EMANATION ? baseDistance + 2.5 : baseDistance;
|
|
||||||
|
const dimensionConstant = game.scenes.active.grid.size / game.scenes.active.grid.distance;
|
||||||
|
|
||||||
|
baseDistance *= dimensionConstant;
|
||||||
|
|
||||||
|
const length = baseDistance;
|
||||||
|
const radius = length;
|
||||||
|
|
||||||
|
const shapeWidth = type === LINE ? 5 * dimensionConstant : type === RECTANGLE ? length : undefined;
|
||||||
|
|
||||||
const { width, height } = game.canvas.scene.dimensions;
|
const { width, height } = game.canvas.scene.dimensions;
|
||||||
const data = {
|
const shapeData = {
|
||||||
x: width / 2,
|
x: width / 2,
|
||||||
y: height / 2,
|
y: height / 2,
|
||||||
|
base: {
|
||||||
|
type: 'token',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
shape: game.canvas.grid.isHexagonal ? CONST.TOKEN_SHAPES.ELLIPSE_1 : CONST.TOKEN_SHAPES.RECTANGLE_1
|
||||||
|
},
|
||||||
t: usedType,
|
t: usedType,
|
||||||
distance: distance,
|
length: length,
|
||||||
width: type === CONST.MEASURED_TEMPLATE_TYPES.RAY ? 5 : undefined,
|
width: shapeWidth,
|
||||||
|
height: length,
|
||||||
angle: usedAngle,
|
angle: usedAngle,
|
||||||
direction: direction
|
radius: radius,
|
||||||
|
direction: direction,
|
||||||
|
type: usedType
|
||||||
};
|
};
|
||||||
|
|
||||||
CONFIG.ux.TemplateManager.createPreview(data);
|
await canvas.regions.placeRegion(
|
||||||
|
{
|
||||||
|
name: usedType.capitalize(),
|
||||||
|
shapes: [shapeData],
|
||||||
|
restriction: { enabled: false, type: 'move', priority: 0 },
|
||||||
|
behaviors: [],
|
||||||
|
displayMeasurements: true,
|
||||||
|
locked: false,
|
||||||
|
ownership: { default: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE },
|
||||||
|
visibility: CONST.REGION_VISIBILITY.ALWAYS
|
||||||
|
},
|
||||||
|
{ create: true }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -49,9 +49,8 @@ export default class RegisterHandlebarsHelpers {
|
||||||
}
|
}
|
||||||
|
|
||||||
static damageSymbols(damageParts) {
|
static damageSymbols(damageParts) {
|
||||||
const symbols = [...new Set(damageParts.reduce((a, c) => a.concat([...c.type]), []))].map(
|
const allTypes = [...new Set([...damageParts].flatMap(x => Array.from(x.type)))];
|
||||||
p => CONFIG.DH.GENERAL.damageTypes[p].icon
|
const symbols = allTypes.map(p => CONFIG.DH.GENERAL.damageTypes[p].icon);
|
||||||
);
|
|
||||||
return new Handlebars.SafeString(Array.from(symbols).map(symbol => `<i class="fa-solid ${symbol}"></i>`));
|
return new Handlebars.SafeString(Array.from(symbols).map(symbol => `<i class="fa-solid ${symbol}"></i>`));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -177,10 +177,10 @@ export const getDeleteKeys = (property, innerProperty, innerPropertyDefaultValue
|
||||||
[innerProperty]: innerPropertyDefaultValue
|
[innerProperty]: innerPropertyDefaultValue
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
acc[`${key}.-=${innerProperty}`] = null;
|
acc[`${key}.${innerProperty}`] = _del;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
acc[`-=${key}`] = null;
|
acc[`${key}`] = _del;
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
|
|
@ -422,7 +422,12 @@ export async function createEmbeddedItemWithEffects(actor, baseData, update) {
|
||||||
...baseData,
|
...baseData,
|
||||||
id: data.id,
|
id: data.id,
|
||||||
uuid: data.uuid,
|
uuid: data.uuid,
|
||||||
effects: data.effects?.map(effect => effect.toObject())
|
_uuid: data.uuid,
|
||||||
|
effects: data.effects?.map(effect => effect.toObject()),
|
||||||
|
_stats: {
|
||||||
|
...data._stats,
|
||||||
|
compendiumSource: data.pack ? `Compendium.${data.pack}.Item.${data.id}` : null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -475,6 +480,8 @@ export async function waitForDiceSoNice(message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function refreshIsAllowed(allowedTypes, typeToCheck) {
|
export function refreshIsAllowed(allowedTypes, typeToCheck) {
|
||||||
|
if (!allowedTypes) return true;
|
||||||
|
|
||||||
switch (typeToCheck) {
|
switch (typeToCheck) {
|
||||||
case CONFIG.DH.GENERAL.refreshTypes.scene.id:
|
case CONFIG.DH.GENERAL.refreshTypes.scene.id:
|
||||||
case CONFIG.DH.GENERAL.refreshTypes.session.id:
|
case CONFIG.DH.GENERAL.refreshTypes.session.id:
|
||||||
|
|
@ -491,9 +498,38 @@ export function refreshIsAllowed(allowedTypes, typeToCheck) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function expireActiveEffectIsAllowed(allowedTypes, typeToCheck) {
|
||||||
|
if (typeToCheck === CONFIG.DH.GENERAL.activeEffectDurations.act.id) return true;
|
||||||
|
|
||||||
|
return refreshIsAllowed(allowedTypes, typeToCheck);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expireActiveEffects(actor, allowedTypes = null) {
|
||||||
|
const shouldExpireEffects = game.settings.get(
|
||||||
|
CONFIG.DH.id,
|
||||||
|
CONFIG.DH.SETTINGS.gameSettings.Automation
|
||||||
|
).autoExpireActiveEffects;
|
||||||
|
if (!shouldExpireEffects) return;
|
||||||
|
|
||||||
|
const effectsToExpire = actor
|
||||||
|
.getActiveEffects()
|
||||||
|
.filter(effect => {
|
||||||
|
if (!effect.system?.duration.type) return false;
|
||||||
|
|
||||||
|
const { temporary, custom } = CONFIG.DH.GENERAL.activeEffectDurations;
|
||||||
|
if ([temporary.id, custom.id].includes(effect.system.duration.type)) return false;
|
||||||
|
|
||||||
|
return expireActiveEffectIsAllowed(allowedTypes, effect.system.duration.type);
|
||||||
|
})
|
||||||
|
.map(x => x.id);
|
||||||
|
|
||||||
|
actor.deleteEmbeddedDocuments('ActiveEffect', effectsToExpire);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getCritDamageBonus(formula) {
|
export async function getCritDamageBonus(formula) {
|
||||||
const critRoll = new Roll(formula);
|
const critRoll = new Roll(formula);
|
||||||
return critRoll.dice.reduce((acc, dice) => acc + dice.faces * dice.number, 0);
|
await critRoll.evaluate();
|
||||||
|
return critRoll.dice.reduce((acc, dice) => acc + dice.faces * dice.results.filter(r => r.active).length, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function htmlToText(html) {
|
export function htmlToText(html) {
|
||||||
|
|
@ -503,6 +539,16 @@ export function htmlToText(html) {
|
||||||
return tempDivElement.textContent || tempDivElement.innerText || '';
|
return tempDivElement.textContent || tempDivElement.innerText || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getIconVisibleActiveEffects(effects) {
|
||||||
|
return effects.filter(effect => {
|
||||||
|
if (!(effect instanceof game.system.api.documents.DhActiveEffect)) return true;
|
||||||
|
|
||||||
|
const alwaysShown = effect.showIcon === CONST.ACTIVE_EFFECT_SHOW_ICON.ALWAYS;
|
||||||
|
const conditionalShown = effect.showIcon === CONST.ACTIVE_EFFECT_SHOW_ICON.CONDITIONAL && !effect.transfer; // TODO: system specific logic
|
||||||
|
|
||||||
|
return !effect.disabled && (alwaysShown || conditionalShown);
|
||||||
|
});
|
||||||
|
}
|
||||||
export async function getFeaturesHTMLData(features) {
|
export async function getFeaturesHTMLData(features) {
|
||||||
const result = [];
|
const result = [];
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
|
|
@ -588,6 +634,8 @@ export async function RefreshFeatures(
|
||||||
const refreshedActors = {};
|
const refreshedActors = {};
|
||||||
for (let actor of game.actors) {
|
for (let actor of game.actors) {
|
||||||
if (actorTypes.includes(actor.type) && actor.prototypeToken.actorLink) {
|
if (actorTypes.includes(actor.type) && actor.prototypeToken.actorLink) {
|
||||||
|
expireActiveEffects(actor, refreshTypes);
|
||||||
|
|
||||||
const updates = {};
|
const updates = {};
|
||||||
for (let item of actor.items) {
|
for (let item of actor.items) {
|
||||||
if (
|
if (
|
||||||
|
|
@ -682,3 +730,79 @@ export async function RefreshFeatures(
|
||||||
|
|
||||||
return refreshedActors;
|
return refreshedActors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getUnusedDamageTypes(parts) {
|
||||||
|
const usedKeys = Object.keys(parts);
|
||||||
|
return Object.keys(CONFIG.DH.GENERAL.healingTypes).reduce((acc, key) => {
|
||||||
|
if (!usedKeys.includes(key))
|
||||||
|
acc.push({
|
||||||
|
value: key,
|
||||||
|
label: game.i18n.localize(CONFIG.DH.GENERAL.healingTypes[key].label)
|
||||||
|
});
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns resolved armor sources ordered by application order */
|
||||||
|
export function getArmorSources(actor) {
|
||||||
|
const rawArmorSources = Array.from(actor.allApplicableEffects()).filter(x => x.system.armorData);
|
||||||
|
if (actor.system.armor) rawArmorSources.push(actor.system.armor);
|
||||||
|
|
||||||
|
const data = rawArmorSources.map(doc => {
|
||||||
|
// 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;
|
||||||
|
return {
|
||||||
|
origin,
|
||||||
|
name: origin.name,
|
||||||
|
document: doc,
|
||||||
|
data: doc.system.armor ?? doc.system.armorData,
|
||||||
|
disabled: !!doc.disabled || !!doc.isSuppressed
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortBy(data, ({ origin }) => {
|
||||||
|
switch (origin?.type) {
|
||||||
|
case 'class':
|
||||||
|
case 'subclass':
|
||||||
|
case 'ancestry':
|
||||||
|
case 'community':
|
||||||
|
case 'feature':
|
||||||
|
case 'domainCard':
|
||||||
|
return 2;
|
||||||
|
case 'loot':
|
||||||
|
case 'consumable':
|
||||||
|
return 3;
|
||||||
|
case 'character':
|
||||||
|
return 4;
|
||||||
|
case 'weapon':
|
||||||
|
return 5;
|
||||||
|
case 'armor':
|
||||||
|
return 6;
|
||||||
|
default:
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
export function sortBy(arr, fn) {
|
||||||
|
const directCompare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
||||||
|
const cmp = (a, b) => {
|
||||||
|
const resultA = fn(a);
|
||||||
|
const resultB = fn(b);
|
||||||
|
if (Array.isArray(resultA) && Array.isArray(resultB)) {
|
||||||
|
for (let idx = 0; idx < Math.min(resultA.length, resultB.length); idx++) {
|
||||||
|
const result = directCompare(resultA[idx], resultB[idx]);
|
||||||
|
if (result !== 0) return result;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return directCompare(resultA, resultB);
|
||||||
|
};
|
||||||
|
return arr.sort(cmp);
|
||||||
|
}
|
||||||
|
|
|
||||||
1
module/macros/_modules.mjs
Normal file
1
module/macros/_modules.mjs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as spotlightCombatant } from './spotlightCombatant.mjs';
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue