mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-04-21 23:13:39 +02:00
Merge branch 'main' into release
This commit is contained in:
commit
cd93246358
767 changed files with 10444 additions and 6815 deletions
|
|
@ -1,3 +1,3 @@
|
|||
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.12012 0.5H51.8799C55.2901 0.500041 57.8779 3.57175 57.2998 6.93262L50.4639 46.6777C50.1604 48.4411 49.0179 49.9467 47.4014 50.7139L31.3584 58.3271C29.8661 59.0354 28.1339 59.0354 26.6416 58.3271L10.5986 50.7139C8.98214 49.9467 7.83959 48.4411 7.53613 46.6777L0.700195 6.93262C0.122088 3.57175 2.7099 0.500042 6.12012 0.5Z" fill="transparent" stroke="#18162e"/>
|
||||
<path d="M 7.12 0.5 H 52.88 C 56.29 0.5 58.88 3.57 58.3 6.93 L 51.46 46.68 C 51.16 48.44 50.02 49.95 48.4 50.71 L 32.36 58.33 C 30.87 59.04 29.13 59.04 27.64 58.33 L 11.6 50.71 C 9.98 49.95 8.84 48.44 8.54 46.68 L 1.7 6.93 C 1.12 3.57 3.71 0.5 7.12 0.5 Z" fill="transparent" stroke="#18162e"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 397 B |
|
|
@ -1,3 +1,3 @@
|
|||
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.12012 0.5H51.8799C55.2901 0.500041 57.8779 3.57175 57.2998 6.93262L50.4639 46.6777C50.1604 48.4411 49.0179 49.9467 47.4014 50.7139L31.3584 58.3271C29.8661 59.0354 28.1339 59.0354 26.6416 58.3271L10.5986 50.7139C8.98214 49.9467 7.83959 48.4411 7.53613 46.6777L0.700195 6.93262C0.122088 3.57175 2.7099 0.500042 6.12012 0.5Z" fill="#18152E" stroke="#F3C267"/>
|
||||
<path d="M 7.12 0.5 H 52.88 C 56.29 0.5 58.88 3.57 58.3 6.93 L 51.46 46.68 C 51.16 48.44 50.02 49.95 48.4 50.71 L 32.36 58.33 C 30.87 59.04 29.13 59.04 27.64 58.33 L 11.6 50.71 C 9.98 49.95 8.84 48.44 8.54 46.68 L 1.7 6.93 C 1.12 3.57 3.71 0.5 7.12 0.5 Z" fill="#18152E" stroke="#F3C267"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 472 B After Width: | Height: | Size: 393 B |
|
|
@ -9,10 +9,7 @@ import * as dice from './module/dice/_module.mjs';
|
|||
import * as fields from './module/data/fields/_module.mjs';
|
||||
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
|
||||
import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs';
|
||||
import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs';
|
||||
import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll } from './module/dice/_module.mjs';
|
||||
import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs';
|
||||
import { enrichedFateRoll, getFateTypeData } from './module/enrichers/FateRollEnricher.mjs';
|
||||
import {
|
||||
handlebarsRegistration,
|
||||
runMigrations,
|
||||
|
|
@ -21,7 +18,6 @@ import {
|
|||
} from './module/systemRegistration/_module.mjs';
|
||||
import { placeables, DhTokenLayer } from './module/canvas/_module.mjs';
|
||||
import './node_modules/@yaireo/tagify/dist/tagify.css';
|
||||
import TemplateManager from './module/documents/templateManager.mjs';
|
||||
import TokenManager from './module/documents/tokenManager.mjs';
|
||||
|
||||
CONFIG.DH = SYSTEM;
|
||||
|
|
@ -36,6 +32,8 @@ CONFIG.Dice.daggerheart = {
|
|||
FateRoll: FateRoll
|
||||
};
|
||||
|
||||
Object.assign(CONFIG.Dice.termTypes, dice.diceTypes);
|
||||
|
||||
CONFIG.Actor.documentClass = documents.DhpActor;
|
||||
CONFIG.Actor.dataModels = models.actors.config;
|
||||
CONFIG.Actor.collection = collections.DhActorCollection;
|
||||
|
|
@ -45,6 +43,7 @@ CONFIG.Item.dataModels = models.items.config;
|
|||
|
||||
CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect;
|
||||
CONFIG.ActiveEffect.dataModels = models.activeEffects.config;
|
||||
CONFIG.ActiveEffect.changeTypes = { ...CONFIG.ActiveEffect.changeTypes, ...models.activeEffects.changeEffects };
|
||||
|
||||
CONFIG.Combat.documentClass = documents.DhpCombat;
|
||||
CONFIG.Combat.dataModels = { base: models.DhCombat };
|
||||
|
|
@ -56,11 +55,13 @@ CONFIG.ChatMessage.documentClass = documents.DhChatMessage;
|
|||
CONFIG.ChatMessage.template = 'systems/daggerheart/templates/ui/chat/chat-message.hbs';
|
||||
|
||||
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.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate;
|
||||
|
||||
CONFIG.Region.objectClass = placeables.DhRegion;
|
||||
|
||||
CONFIG.RollTable.documentClass = documents.DhRollTable;
|
||||
CONFIG.RollTable.resultTemplate = 'systems/daggerheart/templates/ui/chat/table-result.hbs';
|
||||
|
||||
|
|
@ -84,7 +85,6 @@ CONFIG.ui.resources = applications.ui.DhFearTracker;
|
|||
CONFIG.ui.countdowns = applications.ui.DhCountdowns;
|
||||
CONFIG.ux.ContextMenu = applications.ux.DHContextMenu;
|
||||
CONFIG.ux.TooltipManager = documents.DhTooltipManager;
|
||||
CONFIG.ux.TemplateManager = new TemplateManager();
|
||||
CONFIG.ux.TokenManager = new TokenManager();
|
||||
CONFIG.debug.triggers = false;
|
||||
|
||||
|
|
@ -213,6 +213,7 @@ Hooks.once('init', () => {
|
|||
SYSTEM.id,
|
||||
applications.sheetConfigs.ActiveEffectConfig,
|
||||
{
|
||||
types: ['base', 'beastform', 'horde'],
|
||||
makeDefault: true,
|
||||
label: sheetLabel('DOCUMENT.ActiveEffect')
|
||||
}
|
||||
|
|
@ -270,7 +271,6 @@ Hooks.on('setup', () => {
|
|||
...damageThresholds,
|
||||
'proficiency',
|
||||
'evasion',
|
||||
'armorScore',
|
||||
'scars',
|
||||
'levelData.level.current'
|
||||
]
|
||||
|
|
@ -332,75 +332,14 @@ Hooks.on('renderHandlebarsApplication', (_, element) => {
|
|||
enricherRenderSetup(element);
|
||||
});
|
||||
|
||||
Hooks.on('chatMessage', (_, message) => {
|
||||
if (message.startsWith('/dr')) {
|
||||
const result =
|
||||
message.trim().toLowerCase() === '/dr' ? { result: {} } : rollCommandToJSON(message.replace(/\/dr\s?/, ''));
|
||||
if (!result) {
|
||||
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.dualityParsing'));
|
||||
return false;
|
||||
}
|
||||
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 { result: rollCommand, flavor } = result;
|
||||
|
||||
const reaction = rollCommand.reaction;
|
||||
const traitValue = rollCommand.trait?.toLowerCase();
|
||||
const advantage = rollCommand.advantage
|
||||
? CONFIG.DH.ACTIONS.advantageState.advantage.value
|
||||
: rollCommand.disadvantage
|
||||
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
|
||||
: undefined;
|
||||
const difficulty = rollCommand.difficulty;
|
||||
const grantResources = rollCommand.grantResources;
|
||||
|
||||
const target = getCommandTarget({ allowNull: true });
|
||||
const title =
|
||||
(flavor ?? traitValue)
|
||||
? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
||||
ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label)
|
||||
})
|
||||
: game.i18n.localize('DAGGERHEART.GENERAL.duality');
|
||||
|
||||
enrichedDualityRoll({
|
||||
reaction,
|
||||
traitValue,
|
||||
target,
|
||||
difficulty,
|
||||
title,
|
||||
label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'),
|
||||
actionType: null,
|
||||
advantage,
|
||||
grantResources
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (message.startsWith('/fr')) {
|
||||
const result =
|
||||
message.trim().toLowerCase() === '/fr' ? { result: {} } : rollCommandToJSON(message.replace(/\/fr\s?/, ''));
|
||||
|
||||
if (!result) {
|
||||
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateParsing'));
|
||||
return false;
|
||||
}
|
||||
|
||||
const { result: rollCommand, flavor } = result;
|
||||
const fateTypeData = getFateTypeData(rollCommand?.type);
|
||||
|
||||
if (!fateTypeData)
|
||||
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
|
||||
|
||||
const { value: fateType, label: fateTypeLabel } = fateTypeData;
|
||||
const target = getCommandTarget({ allowNull: true });
|
||||
const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll');
|
||||
|
||||
enrichedFateRoll({
|
||||
target,
|
||||
title,
|
||||
label: fateTypeLabel,
|
||||
fateType
|
||||
});
|
||||
return false;
|
||||
const dialog = new game.system.api.applications.dialogs.TagTeamDialog(party);
|
||||
dialog.tabGroups.application = 'tagTeamRoll';
|
||||
await dialog.render({ force: true });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
108
lang/en.json
108
lang/en.json
|
|
@ -14,7 +14,9 @@
|
|||
"beastform": "Beastform"
|
||||
},
|
||||
"ActiveEffect": {
|
||||
"beastform": "Beastform"
|
||||
"base": "Standard",
|
||||
"beastform": "Beastform",
|
||||
"horde": "Horde"
|
||||
},
|
||||
"Actor": {
|
||||
"character": "Character",
|
||||
|
|
@ -53,6 +55,7 @@
|
|||
},
|
||||
"damage": {
|
||||
"name": "Damage",
|
||||
"critical": "Damage (Critical)",
|
||||
"tooltip": "Direct damage without a roll."
|
||||
},
|
||||
"effect": {
|
||||
|
|
@ -87,9 +90,14 @@
|
|||
},
|
||||
"Config": {
|
||||
"beastform": {
|
||||
"exact": "Beastform Max Tier",
|
||||
"exactHint": "The Character's Tier is used if empty",
|
||||
"label": "Beastform"
|
||||
"exact": { "label": "Beastform Max Tier", "hint": "The Character's Tier is used if empty" },
|
||||
"modifications": {
|
||||
"traitBonuses": {
|
||||
"label": { "single": "Trait Bonus", "plural": "Trait Bonuses" },
|
||||
"hint": "Pick bonuses you apply to freely chosen traits at the time of transforming",
|
||||
"bonus": "Bonus Amount"
|
||||
}
|
||||
}
|
||||
},
|
||||
"countdown": {
|
||||
"defaultOwnership": "Default Ownership",
|
||||
|
|
@ -151,7 +159,8 @@
|
|||
"Config": {
|
||||
"rangeDependence": {
|
||||
"title": "Range Dependence"
|
||||
}
|
||||
},
|
||||
"stacking": { "title": "Stacking" }
|
||||
},
|
||||
"RangeDependance": {
|
||||
"hint": "Settings for an optional distance at which this effect should activate",
|
||||
|
|
@ -442,14 +451,15 @@
|
|||
},
|
||||
"DaggerheartMenu": {
|
||||
"title": "GM Tools",
|
||||
"refreshFeatures": "Refresh Features"
|
||||
"refreshFeatures": "Refresh Features",
|
||||
"fallingAndCollision": "Falling And Collision Damage"
|
||||
},
|
||||
"DeleteConfirmation": {
|
||||
"title": "Delete {type} - {name}",
|
||||
"text": "Are you sure you want to delete {name}?"
|
||||
},
|
||||
"DamageReduction": {
|
||||
"armorMarks": "Armor Marks",
|
||||
"maxUseableArmor": "Useable Armor Slots",
|
||||
"armorWithStress": "Spend 1 stress to use an extra mark",
|
||||
"thresholdImmunities": "Threshold Immunities",
|
||||
"stress": "Stress",
|
||||
|
|
@ -675,16 +685,35 @@
|
|||
},
|
||||
"TagTeamSelect": {
|
||||
"title": "Tag Team Roll",
|
||||
"FIELDS": {
|
||||
"initiator": {
|
||||
"memberId": { "label": "Initiating Character" },
|
||||
"cost": { "label": "Initiation Cost" }
|
||||
}
|
||||
},
|
||||
"leaderTitle": "Initiating Character",
|
||||
"membersTitle": "Participants",
|
||||
"partyTeam": "Party Team",
|
||||
"hopeCost": "Hope Cost",
|
||||
"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",
|
||||
"damageNotRolled": "Damage not rolled in chat message yet",
|
||||
"insufficientHope": "The initiating character doesn't have enough hope",
|
||||
"createTagTeam": "Create TagTeam Roll",
|
||||
"chatMessageRollTitle": "Roll"
|
||||
"createTagTeam": "Create Tag Team 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": {
|
||||
"actorSizeUsed": "Actor size is set, determining the dimensions"
|
||||
|
|
@ -697,6 +726,15 @@
|
|||
}
|
||||
},
|
||||
"CONFIG": {
|
||||
"ActiveEffectDuration": {
|
||||
"temporary": "Temporary",
|
||||
"act": "Next Spotlight",
|
||||
"scene": "Next Scene",
|
||||
"shortRest": "Next Rest",
|
||||
"longRest": "Next Long Rest",
|
||||
"session": "Next Session",
|
||||
"custom": "Custom"
|
||||
},
|
||||
"AdversaryTrait": {
|
||||
"relentless": {
|
||||
"name": "Relentless",
|
||||
|
|
@ -764,6 +802,11 @@
|
|||
"bruiser": "for each Bruiser adversary.",
|
||||
"solo": "for each Solo adversary."
|
||||
},
|
||||
"ArmorInteraction": {
|
||||
"none": { "label": "Ignores Armor" },
|
||||
"active": { "label": "Active w/ Armor" },
|
||||
"inactive": { "label": "Inactive w/ Armor" }
|
||||
},
|
||||
"ArmorFeature": {
|
||||
"burning": {
|
||||
"name": "Burning",
|
||||
|
|
@ -1114,6 +1157,12 @@
|
|||
"description": ""
|
||||
}
|
||||
},
|
||||
"fallAndCollision": {
|
||||
"veryClose": { "label": "Very Close", "chatTitle": "Fall Damage: Very Close" },
|
||||
"close": { "label": "Close", "chatTitle": "Fall Damage: Close" },
|
||||
"far": { "label": "Far", "chatTitle": "Fall Damage: Far" },
|
||||
"collision": { "label": "Collision", "chatTitle": "Dangerous Collision" }
|
||||
},
|
||||
"FeatureForm": {
|
||||
"label": "Feature Form",
|
||||
"passive": "Passive",
|
||||
|
|
@ -1219,6 +1268,11 @@
|
|||
"selectType": "Select Action Type",
|
||||
"selectAction": "Action Selection"
|
||||
},
|
||||
"TagTeamRollTypes": {
|
||||
"trait": "Trait",
|
||||
"ability": "Ability",
|
||||
"damageAbility": "Damage Ability"
|
||||
},
|
||||
"TargetTypes": {
|
||||
"any": "Any",
|
||||
"friendly": "Friendly",
|
||||
|
|
@ -1231,8 +1285,8 @@
|
|||
"cone": "Cone",
|
||||
"emanation": "Emanation",
|
||||
"inFront": "In Front",
|
||||
"rect": "Rectangle",
|
||||
"ray": "Ray"
|
||||
"rectangle": "Rectangle",
|
||||
"line": "Line"
|
||||
},
|
||||
"TokenSize": {
|
||||
"tiny": "Tiny",
|
||||
|
|
@ -1847,6 +1901,17 @@
|
|||
"name": "Healing Roll"
|
||||
}
|
||||
},
|
||||
"ChangeTypes": {
|
||||
"armor": {
|
||||
"newArmorEffect": "Armor Effect",
|
||||
"FIELDS": {
|
||||
"interaction": {
|
||||
"label": "Armor Interaction",
|
||||
"hint": "Does the character wearing armor suppress this effect?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Duration": {
|
||||
"passive": "Passive",
|
||||
"temporary": "Temporary"
|
||||
|
|
@ -1871,6 +1936,10 @@
|
|||
}
|
||||
},
|
||||
"GENERAL": {
|
||||
"Ability": {
|
||||
"single": "Ability",
|
||||
"plural": "Abilities"
|
||||
},
|
||||
"Action": {
|
||||
"single": "Action",
|
||||
"plural": "Actions"
|
||||
|
|
@ -2252,6 +2321,7 @@
|
|||
"duality": "Duality",
|
||||
"dualityDice": "Duality Dice",
|
||||
"dualityRoll": "Duality Roll",
|
||||
"effect": "Effect",
|
||||
"enabled": "Enabled",
|
||||
"evasion": "Evasion",
|
||||
"equipment": "Equipment",
|
||||
|
|
@ -2324,6 +2394,10 @@
|
|||
"rerolled": "Rerolled",
|
||||
"rerollThing": "Reroll {thing}",
|
||||
"resource": "Resource",
|
||||
"result": {
|
||||
"single": "Result",
|
||||
"plural": "Results"
|
||||
},
|
||||
"roll": "Roll",
|
||||
"rollAll": "Roll All",
|
||||
"rollDamage": "Roll Damage",
|
||||
|
|
@ -2489,8 +2563,7 @@
|
|||
"MACROS": {
|
||||
"Spotlight": {
|
||||
"errors": {
|
||||
"noActiveCombat": "There is no active encounter",
|
||||
"noCombatantSelected": "A combatant token must be either selected or hovered to spotlight it"
|
||||
"noTokenSelected": "A token on the canvas must either be selected or hovered to spotlight it"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -2565,6 +2638,10 @@
|
|||
"hint": "Automatically increase the GM's fear pool on a fear duality roll result."
|
||||
},
|
||||
"FIELDS": {
|
||||
"autoExpireActiveEffects": {
|
||||
"label": "Auto Expire Active Effects",
|
||||
"hint": "Active Effects with set durations will automatically be removed when their durations are up"
|
||||
},
|
||||
"damageReductionRulesDefault": {
|
||||
"label": "Damage Reduction Rules Default",
|
||||
"hint": "Wether using armor and reductions has rules on by default"
|
||||
|
|
@ -2937,6 +3014,8 @@
|
|||
},
|
||||
"EffectsDisplay": {
|
||||
"removeThing": "[Right Click] Remove {thing}",
|
||||
"increaseStacks": "[Left Click] Increment Stacks",
|
||||
"decreaseStacks": "[Right Click] Decrement Stacks",
|
||||
"appliedBy": "Applied By: {by}"
|
||||
},
|
||||
"ItemBrowser": {
|
||||
|
|
@ -3063,6 +3142,9 @@
|
|||
"knowTheTide": "Know The Tide gained a token",
|
||||
"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": {
|
||||
"actorDirectory": {
|
||||
"tier": "Tier {tier} {type}",
|
||||
|
|
|
|||
|
|
@ -554,7 +554,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
|
|||
experiences: {
|
||||
...this.setup.experiences,
|
||||
...Object.keys(this.character.system.experiences).reduce((acc, key) => {
|
||||
acc[`-=${key}`] = null;
|
||||
acc[`${key}`] = _del;
|
||||
return acc;
|
||||
}, {})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
|
|||
this.selected = null;
|
||||
this.evolved = { form: null };
|
||||
this.hybrid = { forms: {}, advantages: {}, features: {} };
|
||||
this.modifications = {
|
||||
traitBonuses: configData.modifications.traitBonuses.map(x => ({
|
||||
trait: null,
|
||||
bonus: x.bonus
|
||||
}))
|
||||
};
|
||||
|
||||
this._dragDrop = this._createDragDropHandlers();
|
||||
}
|
||||
|
|
@ -28,6 +34,7 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
|
|||
selectBeastform: this.selectBeastform,
|
||||
toggleHybridFeature: this.toggleHybridFeature,
|
||||
toggleHybridAdvantage: this.toggleHybridAdvantage,
|
||||
toggleTraitBonus: this.toggleTraitBonus,
|
||||
submitBeastform: this.submitBeastform
|
||||
},
|
||||
form: {
|
||||
|
|
@ -48,6 +55,7 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
|
|||
tabs: { template: 'systems/daggerheart/templates/dialogs/beastform/tabs.hbs' },
|
||||
beastformTier: { template: 'systems/daggerheart/templates/dialogs/beastform/beastformTier.hbs' },
|
||||
advanced: { template: 'systems/daggerheart/templates/dialogs/beastform/advanced.hbs' },
|
||||
modifications: { template: 'systems/daggerheart/templates/dialogs/beastform/modifications.hbs' },
|
||||
footer: { template: 'systems/daggerheart/templates/dialogs/beastform/footer.hbs' }
|
||||
};
|
||||
|
||||
|
|
@ -146,6 +154,9 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
|
|||
{}
|
||||
);
|
||||
|
||||
context.modifications = this.modifications;
|
||||
context.traits = CONFIG.DH.ACTOR.abilities;
|
||||
|
||||
context.tier = beastformTiers[this.tabGroups.primary];
|
||||
context.tierKey = this.tabGroups.primary;
|
||||
|
||||
|
|
@ -155,6 +166,9 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
|
|||
}
|
||||
|
||||
canSubmit() {
|
||||
const modificationsFinished = this.modifications.traitBonuses.every(x => x.trait);
|
||||
if (!modificationsFinished) return false;
|
||||
|
||||
if (this.selected) {
|
||||
switch (this.selected.system.beastformType) {
|
||||
case 'normal':
|
||||
|
|
@ -261,6 +275,13 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
|
|||
this.render();
|
||||
}
|
||||
|
||||
static toggleTraitBonus(_, button) {
|
||||
const { index, trait } = button.dataset;
|
||||
this.modifications.traitBonuses[index].trait =
|
||||
this.modifications.traitBonuses[index].trait === trait ? null : trait;
|
||||
this.render();
|
||||
}
|
||||
|
||||
static async submitBeastform() {
|
||||
await this.close({ submitted: true });
|
||||
}
|
||||
|
|
@ -292,6 +313,23 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
|
|||
}
|
||||
}
|
||||
|
||||
const beastformEffect = selected.effects.find(x => x.type === 'beastform');
|
||||
for (const traitBonus of app.modifications.traitBonuses) {
|
||||
const existingChange = beastformEffect.changes.find(
|
||||
x => x.key === `system.traits.${traitBonus.trait}.value`
|
||||
);
|
||||
if (existingChange) {
|
||||
existingChange.value = Number.parseInt(existingChange.value) + traitBonus.bonus;
|
||||
} else {
|
||||
beastformEffect.changes.push({
|
||||
key: `system.traits.${traitBonus.trait}.value`,
|
||||
mode: 2,
|
||||
priority: null,
|
||||
value: traitBonus.bonus
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
resolve({
|
||||
selected: selected,
|
||||
evolved: { ...app.evolved, form: evolved },
|
||||
|
|
|
|||
|
|
@ -77,8 +77,8 @@ export default class CharacterResetDialog extends HandlebarsApplicationMixin(App
|
|||
|
||||
if (!this.data.optional.portrait.keep) {
|
||||
foundry.utils.setProperty(update, 'img', this.actor.schema.fields.img.initial(this.actor));
|
||||
foundry.utils.setProperty(update, 'prototypeToken.==texture', {});
|
||||
foundry.utils.setProperty(update, 'prototypeToken.==ring', {});
|
||||
foundry.utils.setProperty(update, 'prototypeToken.texture', _replace({}));
|
||||
foundry.utils.setProperty(update, 'prototypeToken.ring', _replace({}));
|
||||
}
|
||||
|
||||
if (this.data.optional.biography.keep)
|
||||
|
|
@ -89,7 +89,7 @@ export default class CharacterResetDialog extends HandlebarsApplicationMixin(App
|
|||
const { system, ...rest } = update;
|
||||
await this.actor.update({
|
||||
...rest,
|
||||
'==system': system ?? {}
|
||||
system: _replace(system ?? {})
|
||||
});
|
||||
|
||||
const inventoryItemTypes = ['weapon', 'armor', 'consumable', 'loot'];
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
|||
updateIsAdvantage: this.updateIsAdvantage,
|
||||
selectExperience: this.selectExperience,
|
||||
toggleReaction: this.toggleReaction,
|
||||
toggleTagTeamRoll: this.toggleTagTeamRoll,
|
||||
toggleSelectedEffect: this.toggleSelectedEffect,
|
||||
submitRoll: this.submitRoll
|
||||
},
|
||||
|
|
@ -71,8 +70,8 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
|||
context.rollConfig = this.config;
|
||||
context.hasRoll = !!this.config.roll;
|
||||
context.canRoll = true;
|
||||
context.selectedRollMode = this.config.selectedRollMode ?? game.settings.get('core', 'rollMode');
|
||||
context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({
|
||||
context.selectedMessageMode = this.config.selectedMessageMode ?? game.settings.get('core', 'messageMode');
|
||||
context.rollModes = Object.entries(CONFIG.ChatMessage.modes).map(([action, { label, icon }]) => ({
|
||||
action,
|
||||
label,
|
||||
icon
|
||||
|
|
@ -133,12 +132,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
|
||||
this.config.selectedRollMode = rest.selectedRollMode;
|
||||
this.config.selectedMessageMode = rest.selectedMessageMode;
|
||||
|
||||
if (this.config.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) {
|
||||
this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
|
||||
this.render();
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
|
|||
},
|
||||
actions: {
|
||||
toggleSelectedEffect: this.toggleSelectedEffect,
|
||||
toggleCritical: this.toggleCritical,
|
||||
submitRoll: this.submitRoll
|
||||
},
|
||||
form: {
|
||||
|
|
@ -52,8 +53,9 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
|
|||
context.formula = this.roll.constructFormula(this.config);
|
||||
context.hasHealing = this.config.hasHealing;
|
||||
context.directDamage = this.config.directDamage;
|
||||
context.selectedRollMode = this.config.selectedRollMode;
|
||||
context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({
|
||||
context.selectedMessageMode = this.config.selectedMessageMode;
|
||||
context.isCritical = this.config.isCritical;
|
||||
context.rollModes = Object.entries(CONFIG.ChatMessage.modes).map(([action, { label, icon }]) => ({
|
||||
action,
|
||||
label,
|
||||
icon
|
||||
|
|
@ -69,11 +71,16 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
|
|||
const { ...rest } = foundry.utils.expandObject(formData.object);
|
||||
foundry.utils.mergeObject(this.config.roll, rest.roll);
|
||||
foundry.utils.mergeObject(this.config.modifiers, rest.modifiers);
|
||||
this.config.selectedRollMode = rest.selectedRollMode;
|
||||
this.config.selectedMessageMode = rest.selectedMessageMode;
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
static toggleCritical() {
|
||||
this.config.isCritical = !this.config.isCritical;
|
||||
this.render();
|
||||
}
|
||||
|
||||
static toggleSelectedEffect(_event, button) {
|
||||
this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
|
||||
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;
|
||||
|
||||
|
|
@ -10,6 +10,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
this.reject = reject;
|
||||
this.actor = actor;
|
||||
this.damage = damage;
|
||||
|
||||
this.damageType = damageType;
|
||||
this.rulesDefault = game.settings.get(
|
||||
CONFIG.DH.id,
|
||||
|
|
@ -20,14 +21,20 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
this.rulesDefault
|
||||
);
|
||||
|
||||
const canApplyArmor = damageType.every(t => actor.system.armorApplicableDamageTypes[t] === true);
|
||||
const availableArmor = actor.system.armorScore - actor.system.armor.system.marks.value;
|
||||
const maxArmorMarks = canApplyArmor ? availableArmor : 0;
|
||||
const orderedArmorSources = getArmorSources(actor).filter(s => !s.disabled);
|
||||
const armor = orderedArmorSources.reduce((acc, { document }) => {
|
||||
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;
|
||||
}, {});
|
||||
}, []);
|
||||
const stress = [...Array(actor.system.rules.damageReduction.maxArmorMarked.stressExtra ?? 0).keys()].reduce(
|
||||
(acc, _) => {
|
||||
acc[foundry.utils.randomID()] = { selected: false };
|
||||
|
|
@ -121,13 +128,11 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
context.thresholdImmunities =
|
||||
Object.keys(this.thresholdImmunities).length > 0 ? this.thresholdImmunities : null;
|
||||
|
||||
const { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage } =
|
||||
const { selectedStressMarks, stressReductions, currentMarks, currentDamage, maxArmorUsed, availableArmor } =
|
||||
this.getDamageInfo();
|
||||
|
||||
context.armorScore = this.actor.system.armorScore;
|
||||
context.armorScore = this.actor.system.armorScore.max;
|
||||
context.armorMarks = currentMarks;
|
||||
context.basicMarksUsed =
|
||||
selectedArmorMarks.length === this.actor.system.rules.damageReduction.maxArmorMarked.value;
|
||||
|
||||
const stressReductionStress = this.availableStressReductions
|
||||
? stressReductions.reduce((acc, red) => acc + red.cost, 0)
|
||||
|
|
@ -141,16 +146,30 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
}
|
||||
: null;
|
||||
|
||||
const maxArmor = this.actor.system.rules.damageReduction.maxArmorMarked.value;
|
||||
context.marks = {
|
||||
armor: Object.keys(this.marks.armor).reduce((acc, key, index) => {
|
||||
const mark = this.marks.armor[key];
|
||||
if (!this.rulesOn || index + 1 <= maxArmor) acc[key] = mark;
|
||||
context.maxArmorUsed = maxArmorUsed;
|
||||
context.availableArmor = availableArmor;
|
||||
context.basicMarksUsed = availableArmor === 0 || selectedStressMarks.length;
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
context.usesStressArmor = Object.keys(context.marks.stress).length;
|
||||
context.availableStressReductions = this.availableStressReductions;
|
||||
|
||||
context.damage = getDamageLabel(this.damage);
|
||||
|
|
@ -167,27 +186,31 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
}
|
||||
|
||||
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 stressReductions = this.availableStressReductions
|
||||
? Object.values(this.availableStressReductions).filter(red => red.selected)
|
||||
: [];
|
||||
const currentMarks =
|
||||
this.actor.system.armor.system.marks.value + selectedArmorMarks.length + selectedStressMarks.length;
|
||||
const currentMarks = this.actor.system.armorScore.value + selectedArmorMarks.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 =
|
||||
selectedArmorMarks.length * this.actor.system.rules.damageReduction.increasePerArmorMark;
|
||||
let currentDamage = Math.max(
|
||||
this.damage - armorMarkReduction - selectedStressMarks.length - stressReductions.length,
|
||||
0
|
||||
);
|
||||
let currentDamage = Math.max(this.damage - armorMarkReduction - stressReductions.length, 0);
|
||||
if (this.reduceSeverity) {
|
||||
currentDamage = Math.max(currentDamage - this.reduceSeverity, 0);
|
||||
}
|
||||
|
||||
if (this.thresholdImmunities[currentDamage]) currentDamage = 0;
|
||||
|
||||
return { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage };
|
||||
return { selectedStressMarks, stressReductions, currentMarks, currentDamage, maxArmorUsed, availableArmor };
|
||||
};
|
||||
|
||||
static toggleRules() {
|
||||
|
|
@ -195,13 +218,10 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
|
||||
const maxArmor = this.actor.system.rules.damageReduction.maxArmorMarked.value;
|
||||
this.marks = {
|
||||
armor: Object.keys(this.marks.armor).reduce((acc, key, index) => {
|
||||
const mark = this.marks.armor[key];
|
||||
armor: this.marks.armor.map((mark, index) => {
|
||||
const keepSelectValue = !this.rulesOn || index + 1 <= maxArmor;
|
||||
acc[key] = { ...mark, selected: keepSelectValue ? mark.selected : false };
|
||||
|
||||
return acc;
|
||||
}, {}),
|
||||
return { ...mark, selected: keepSelectValue ? mark.selected : false };
|
||||
}),
|
||||
stress: this.marks.stress
|
||||
};
|
||||
|
||||
|
|
@ -209,8 +229,8 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
}
|
||||
|
||||
static setMarks(_, target) {
|
||||
const currentMark = this.marks[target.dataset.type][target.dataset.key];
|
||||
const { selectedStressMarks, stressReductions, currentMarks, currentDamage } = this.getDamageInfo();
|
||||
const currentMark = foundry.utils.getProperty(this.marks, target.dataset.path);
|
||||
const { selectedStressMarks, stressReductions, currentDamage, availableArmor } = this.getDamageInfo();
|
||||
|
||||
if (!currentMark.selected && currentDamage === 0) {
|
||||
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 (!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'));
|
||||
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) {
|
||||
const currentDamageLabel = getDamageLabel(currentDamage);
|
||||
for (let reduction of stressReductions) {
|
||||
|
|
@ -232,8 +258,16 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
}
|
||||
}
|
||||
|
||||
if (target.dataset.type === 'armor' && selectedStressMarks.length > 0) {
|
||||
selectedStressMarks.forEach(mark => (mark.selected = false));
|
||||
if (target.dataset.type === 'stress' && currentMark.armorMarkId) {
|
||||
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();
|
||||
}
|
||||
|
||||
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) {
|
||||
const damageValue = Number(target.dataset.reduction);
|
||||
const stressReduction = this.availableStressReductions[damageValue];
|
||||
|
|
@ -279,11 +332,18 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
}
|
||||
|
||||
static async takeDamage() {
|
||||
const { selectedArmorMarks, selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo();
|
||||
const armorSpent = selectedArmorMarks.length + selectedStressMarks.length;
|
||||
const stressSpent = selectedStressMarks.length + stressReductions.reduce((acc, red) => acc + red.cost, 0);
|
||||
const { selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo();
|
||||
const armorChanges = this.marks.armor.reduce((acc, source) => {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { refreshIsAllowed } from '../../helpers/utils.mjs';
|
||||
import { expireActiveEffects, refreshIsAllowed } from '../../helpers/utils.mjs';
|
||||
|
||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||
|
||||
|
|
@ -203,7 +203,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
|
|||
const msg = {
|
||||
user: game.user.id,
|
||||
system: {
|
||||
moves: moves,
|
||||
moves: moves.map(move => ({ ...move, actions: Array.from(move.actions) })),
|
||||
actor: this.actor.uuid
|
||||
},
|
||||
speaker: cls.getSpeaker(),
|
||||
|
|
@ -264,6 +264,8 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
|
|||
await feature.update({ 'system.resource.value': resetValue });
|
||||
}
|
||||
|
||||
expireActiveEffects(this.actor, [this.shortRest ? 'shortRest' : 'longRest']);
|
||||
|
||||
this.close();
|
||||
} else {
|
||||
this.render();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
|
||||
|
||||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
|
|
@ -123,16 +121,8 @@ export default class RerollDamageDialog extends HandlebarsApplicationMixin(Appli
|
|||
return acc;
|
||||
}, {})
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -67,7 +67,7 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
|
|||
break;
|
||||
case 'summary':
|
||||
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;
|
||||
|
||||
let achievementExperiences = [];
|
||||
|
|
|
|||
|
|
@ -477,7 +477,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
|
|||
const secondaryData = Object.keys(
|
||||
foundry.utils.getProperty(this.levelup, `${target.dataset.path}.secondaryData`)
|
||||
).reduce((acc, key) => {
|
||||
acc[`-=${key}`] = null;
|
||||
acc[key] = _del;
|
||||
return acc;
|
||||
}, {});
|
||||
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}`);
|
||||
if (Number(button.dataset.cost) > 1 || Object.keys(current).length === 1) {
|
||||
// Simple handling that doesn't cover potential Custom LevelTiers.
|
||||
update[`${basePath}.-=${button.dataset.option}`] = null;
|
||||
update[`${basePath}.${button.dataset.option}`] = _del;
|
||||
} else {
|
||||
update[`${basePath}.${button.dataset.option}.-=${button.dataset.checkboxNr}`] = null;
|
||||
update[`${basePath}.${button.dataset.option}.${button.dataset.checkboxNr}`] = _del;
|
||||
}
|
||||
} else {
|
||||
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) {
|
||||
event.stopPropagation();
|
||||
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);
|
||||
if (item instanceof game.system.api.documents.DhpActor && item.type === 'environment') {
|
||||
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 ?? {})) {
|
||||
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 path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`;
|
||||
await this.settings.updateSource({
|
||||
[`${path}.-=${id}`]: null
|
||||
[`${path}.${id}`]: _del
|
||||
});
|
||||
|
||||
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 removeUpdate = Object.keys(this.settings.restMoves[target.dataset.type].moves).reduce((acc, key) => {
|
||||
acc[`-=${key}`] = null;
|
||||
acc[key] = _del;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
|
@ -382,7 +382,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
|||
[`itemFeatures.${target.dataset.type}`]: Object.keys(
|
||||
this.settings.itemFeatures[target.dataset.type]
|
||||
).reduce((acc, key) => {
|
||||
acc[`-=${key}`] = null;
|
||||
acc[key] = _del;
|
||||
|
||||
return acc;
|
||||
}, {})
|
||||
|
|
@ -455,12 +455,12 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
|||
if (!confirmed) return;
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -507,7 +507,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
|||
|
||||
static async deleteAdversaryType(_, target) {
|
||||
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.render();
|
||||
|
|
@ -563,7 +563,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
|||
|
||||
const { actorType, resourceKey } = target.dataset;
|
||||
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());
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ export { default as ActionSettingsConfig } from './action-settings-config.mjs';
|
|||
export { default as CharacterSettings } from './character-settings.mjs';
|
||||
export { default as AdversarySettings } from './adversary-settings.mjs';
|
||||
export { default as 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 EnvironmentSettings } from './environment-settings.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';
|
||||
|
||||
const { ApplicationV2 } = foundry.applications.api;
|
||||
|
|
@ -35,7 +36,9 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
|||
editDoc: this.editDoc,
|
||||
addTrigger: this.addTrigger,
|
||||
removeTrigger: this.removeTrigger,
|
||||
expandTrigger: this.expandTrigger
|
||||
expandTrigger: this.expandTrigger,
|
||||
addBeastformTraitBonus: this.addBeastformTraitBonus,
|
||||
removeBeastformTraitBonus: this.removeBeastformTraitBonus
|
||||
},
|
||||
form: {
|
||||
handler: this.updateForm,
|
||||
|
|
@ -104,7 +107,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
|||
}
|
||||
};
|
||||
|
||||
static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects', 'summon'];
|
||||
static CLEAN_ARRAYS = ['cost', 'effects', 'summon'];
|
||||
|
||||
_getTabs(tabs) {
|
||||
for (const v of Object.values(tabs)) {
|
||||
|
|
@ -153,8 +156,13 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
|||
context.openSection = this.openSection;
|
||||
context.tabs = this._getTabs(this.constructor.TABS);
|
||||
context.config = CONFIG.DH;
|
||||
if (this.action.damage?.hasOwnProperty('includeBase') && this.action.type === 'attack')
|
||||
context.hasBaseDamage = !!this.action.parent.attack;
|
||||
if (this.action.hasDamage) {
|
||||
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.getRollTypeOptions = this.getRollTypeOptions();
|
||||
context.disableOption = this.disableOption.bind(this);
|
||||
|
|
@ -291,18 +299,64 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
|||
|
||||
static addDamage(_event) {
|
||||
if (!this.action.damage.parts) return;
|
||||
const data = this.action.toObject(),
|
||||
part = {};
|
||||
if (this.action.actor?.isNPC) part.value = { multiplier: 'flat' };
|
||||
data.damage.parts.push(part);
|
||||
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
|
||||
|
||||
const choices = getUnusedDamageTypes(this.action.damage.parts);
|
||||
const content = new foundry.data.fields.StringField({
|
||||
label: game.i18n.localize('Damage Type'),
|
||||
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) {
|
||||
if (!this.action.damage.parts) return;
|
||||
const data = this.action.toObject(),
|
||||
index = button.dataset.index;
|
||||
data.damage.parts.splice(index, 1);
|
||||
const data = this.action.toObject();
|
||||
const key = button.dataset.key;
|
||||
delete data.damage.parts[key];
|
||||
data.damage.parts[`${key}`] = _del;
|
||||
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
|
||||
}
|
||||
|
||||
|
|
@ -360,6 +414,21 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
|||
}
|
||||
}
|
||||
|
||||
static async addBeastformTraitBonus() {
|
||||
const data = this.action.toObject();
|
||||
data.beastform.modifications.traitBonuses = [
|
||||
...data.beastform.modifications.traitBonuses,
|
||||
this.action.schema.fields.beastform.fields.modifications.fields.traitBonuses.element.getInitialValue()
|
||||
];
|
||||
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
|
||||
}
|
||||
|
||||
static async removeBeastformTraitBonus(_event, button) {
|
||||
const data = this.action.toObject();
|
||||
data.beastform.modifications.traitBonuses.splice(button.dataset.index, 1);
|
||||
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
|
||||
}
|
||||
|
||||
updateSummonCount(event) {
|
||||
event.stopPropagation();
|
||||
const wrapper = event.target.closest('.summon-count-wrapper');
|
||||
|
|
|
|||
|
|
@ -24,12 +24,13 @@ export default class DHActionConfig extends DHActionBaseConfig {
|
|||
const effectData = this._addEffectData.bind(this)();
|
||||
const data = this.action.toObject();
|
||||
|
||||
const [created] = await this.action.item.createEmbeddedDocuments('ActiveEffect', [effectData], {
|
||||
render: false
|
||||
});
|
||||
data.effects.push({ _id: created._id });
|
||||
const created = await this.action.item.createEmbeddedDocuments('ActiveEffect', [
|
||||
game.system.api.data.activeEffects.BaseEffect.getDefaultObject()
|
||||
]);
|
||||
|
||||
data.effects.push({ _id: created[0]._id });
|
||||
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) {
|
||||
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)
|
||||
);
|
||||
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' },
|
||||
changes: {
|
||||
template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs',
|
||||
templates: ['systems/daggerheart/templates/sheets/activeEffect/change.hbs'],
|
||||
scrollable: ['ol[data-changes]']
|
||||
},
|
||||
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
|
||||
});
|
||||
});
|
||||
|
||||
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) {
|
||||
|
|
@ -173,8 +186,166 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
|
|||
}));
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
await this.actor.update({ [`system.experiences.-=${target.dataset.experience}`]: null });
|
||||
await this.actor.update({ [`system.experiences.${target.dataset.experience}`]: _del });
|
||||
}
|
||||
|
||||
async _onDragStart(event) {
|
||||
|
|
|
|||
|
|
@ -101,8 +101,8 @@ export default class DHCharacterSettings extends DHBaseActorSettings {
|
|||
|
||||
if (relinkAchievementData.length > 0) {
|
||||
relinkAchievementData.forEach(data => {
|
||||
updates[`system.levelData.levelups.${data.levelKey}.achievements.experiences.-=${data.experience}`] =
|
||||
null;
|
||||
updates[`system.levelData.levelups.${data.levelKey}.achievements.experiences.${data.experience}`] =
|
||||
_del;
|
||||
});
|
||||
} else if (relinkSelectionData.length > 0) {
|
||||
relinkSelectionData.forEach(data => {
|
||||
|
|
@ -137,7 +137,7 @@ export default class DHCharacterSettings extends DHBaseActorSettings {
|
|||
|
||||
await this.actor.update({
|
||||
...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;
|
||||
|
||||
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() {
|
||||
await this.actor.update({
|
||||
[`system.potentialAdversaries.${foundry.utils.randomID()}.label`]: game.i18n.localize(
|
||||
'DAGGERHEART.ACTORS.Environment.newAdversary'
|
||||
)
|
||||
[`system.potentialAdversaries.${foundry.utils.randomID()}`]: {
|
||||
label: game.i18n.localize('DAGGERHEART.ACTORS.Environment.newAdversary')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
|
|||
* @type {ApplicationClickAction}
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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 effect = this.move.effects[effectIndex];
|
||||
const updatedEffect =
|
||||
await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect);
|
||||
await game.system.api.applications.sheetConfigs.ActiveEffectConfig.configureSetting(effect);
|
||||
if (!updatedEffect) return;
|
||||
|
||||
await this.updateMove({
|
||||
|
|
@ -205,7 +205,7 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
|
|||
}
|
||||
});
|
||||
} else {
|
||||
await this.updateMove({ [`${this.actionsPath}.-=${target.dataset.id}`]: null });
|
||||
await this.updateMove({ [`${this.actionsPath}.${target.dataset.id}`]: _del });
|
||||
}
|
||||
|
||||
this.render();
|
||||
|
|
|
|||
|
|
@ -67,9 +67,9 @@ export default function DHTokenConfigMixin(Base) {
|
|||
changes.height = tokenSize;
|
||||
}
|
||||
|
||||
const deletions = { '-=actorId': null, '-=actorLink': null };
|
||||
const mergeOptions = { inplace: false, performDeletions: true };
|
||||
this._preview.updateSource(mergeObject(changes, deletions, mergeOptions));
|
||||
// const deletions = { actorId: _del };
|
||||
// const mergeOptions = { inplace: false, performDeletions: true, actorLink: false };
|
||||
this._preview.updateSource(changes);
|
||||
|
||||
if (this._preview?.object?.destroyed === false) {
|
||||
this._preview.object.initializeSources();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import DHBaseActorSheet from '../api/base-actor.mjs';
|
||||
import DhDeathMove from '../../dialogs/deathMove.mjs';
|
||||
import { abilities } from '../../../config/actorConfig.mjs';
|
||||
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
|
||||
import DhCharacterCreation from '../../characterCreation/characterCreation.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 */
|
||||
|
||||
|
|
@ -35,7 +34,8 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
cancelBeastform: CharacterSheet.#cancelBeastform,
|
||||
toggleResourceManagement: CharacterSheet.#toggleResourceManagement,
|
||||
useDowntime: this.useDowntime,
|
||||
viewParty: CharacterSheet.#viewParty
|
||||
viewParty: CharacterSheet.#viewParty,
|
||||
toggleArmorMangement: CharacterSheet.#toggleArmorManagement
|
||||
},
|
||||
window: {
|
||||
resizable: true,
|
||||
|
|
@ -639,12 +639,12 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
}
|
||||
|
||||
async updateArmorMarks(event) {
|
||||
const armor = this.document.system.armor;
|
||||
if (!armor) return;
|
||||
const inputValue = Number(event.currentTarget.value);
|
||||
const { value, max } = this.document.system.armorScore;
|
||||
const changeValue = Math.min(inputValue - value, max - value);
|
||||
|
||||
const maxMarks = this.document.system.armorScore;
|
||||
const value = Math.min(Math.max(Number(event.currentTarget.value), 0), maxMarks);
|
||||
await armor.update({ 'system.marks.value': value });
|
||||
event.currentTarget.value = inputValue < 0 ? 0 : value + changeValue;
|
||||
this.document.system.updateArmorValue({ value: changeValue });
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
|
@ -720,35 +720,16 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
* Rolls an attribute check based on the clicked button's dataset attribute.
|
||||
* @type {ApplicationClickAction}
|
||||
*/
|
||||
static async #rollAttribute(event, button) {
|
||||
const abilityLabel = game.i18n.localize(abilities[button.dataset.attribute].label);
|
||||
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);
|
||||
static async #rollAttribute(_event, button) {
|
||||
const result = await this.document.rollTrait(button.dataset.attribute);
|
||||
if (!result) return;
|
||||
|
||||
/* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */
|
||||
const costResources =
|
||||
result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) ||
|
||||
{};
|
||||
config.resourceUpdates.addResources(costResources);
|
||||
await config.resourceUpdates.updateResources();
|
||||
result.resourceUpdates.addResources(costResources);
|
||||
await result.resourceUpdates.updateResources();
|
||||
}
|
||||
|
||||
//TODO: redo toggleEquipItem method
|
||||
|
|
@ -823,10 +804,13 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
* Toggles ArmorScore resource value.
|
||||
* @type {ApplicationClickAction}
|
||||
*/
|
||||
static async #toggleArmor(_, button, element) {
|
||||
const ArmorValue = Number.parseInt(button.dataset.value);
|
||||
const newValue = this.document.system.armor.system.marks.value >= ArmorValue ? ArmorValue - 1 : ArmorValue;
|
||||
await this.document.system.armor.update({ 'system.marks.value': newValue });
|
||||
static async #toggleArmor(_, button, _element) {
|
||||
const { value, max } = this.document.system.armorScore;
|
||||
const inputValue = Number.parseInt(button.dataset.value);
|
||||
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) {
|
||||
event.stopPropagation();
|
||||
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');
|
||||
|
||||
game.tooltip.dismissLockedTooltips();
|
||||
game.tooltip.activate(target, {
|
||||
html,
|
||||
|
|
|
|||
|
|
@ -35,9 +35,7 @@ export default class Party extends DHBaseActorSheet {
|
|||
refeshActions: Party.#refeshActions,
|
||||
triggerRest: Party.#triggerRest,
|
||||
tagTeamRoll: Party.#tagTeamRoll,
|
||||
groupRoll: Party.#groupRoll,
|
||||
selectRefreshable: DaggerheartMenu.selectRefreshable,
|
||||
refreshActors: DaggerheartMenu.refreshActors
|
||||
groupRoll: Party.#groupRoll
|
||||
},
|
||||
dragDrop: [{ dragSelector: '[data-item-id]', dropSelector: null }]
|
||||
};
|
||||
|
|
@ -120,6 +118,7 @@ export default class Party extends DHBaseActorSheet {
|
|||
secrets: this.document.isOwner,
|
||||
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.
|
||||
* @type {ApplicationClickAction}
|
||||
*/
|
||||
static async #toggleArmorSlot(_, target, element) {
|
||||
const armorItem = await foundry.utils.fromUuid(target.dataset.itemUuid);
|
||||
const armorValue = Number.parseInt(target.dataset.value);
|
||||
const newValue = armorItem.system.marks.value >= armorValue ? armorValue - 1 : armorValue;
|
||||
await armorItem.update({ 'system.marks.value': newValue });
|
||||
static async #toggleArmorSlot(_, target) {
|
||||
const actor = game.actors.get(target.dataset.actorId);
|
||||
const { value, max } = actor.system.armorScore;
|
||||
const inputValue = Number.parseInt(target.dataset.value);
|
||||
const newValue = value >= inputValue ? inputValue - 1 : inputValue;
|
||||
const changeValue = Math.min(newValue - value, max - value);
|
||||
|
||||
await actor.system.updateArmorValue({ value: changeValue });
|
||||
this.render();
|
||||
}
|
||||
|
||||
|
|
@ -255,11 +257,7 @@ export default class Party extends DHBaseActorSheet {
|
|||
}
|
||||
|
||||
static async #tagTeamRoll() {
|
||||
new game.system.api.applications.dialogs.TagTeamDialog(
|
||||
this.document.system.partyMembers.filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type))
|
||||
).render({
|
||||
force: true
|
||||
});
|
||||
new game.system.api.applications.dialogs.TagTeamDialog(this.document).render({ force: true });
|
||||
}
|
||||
|
||||
static async #groupRoll(_params) {
|
||||
|
|
|
|||
|
|
@ -72,20 +72,15 @@ const typeSettingsMap = {
|
|||
*/
|
||||
export default function DHApplicationMixin(Base) {
|
||||
class DHSheetV2 extends HandlebarsApplicationMixin(Base) {
|
||||
#nonHeaderAttribution = ['environment', 'ancestry', 'community', 'domainCard'];
|
||||
|
||||
/**
|
||||
* @param {DHSheetV2Configuration} [options={}]
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
super(options);
|
||||
/**
|
||||
* @type {foundry.applications.ux.DragDrop[]}
|
||||
* @private
|
||||
*/
|
||||
this._dragDrop = this._createDragDropHandlers();
|
||||
}
|
||||
|
||||
#nonHeaderAttribution = ['environment', 'ancestry', 'community', 'domainCard'];
|
||||
|
||||
/**
|
||||
* The default options for the sheet.
|
||||
* @type {DHSheetV2Configuration}
|
||||
|
|
@ -177,7 +172,6 @@ export default function DHApplicationMixin(Base) {
|
|||
/**@inheritdoc */
|
||||
_attachPartListeners(partId, htmlElement, options) {
|
||||
super._attachPartListeners(partId, htmlElement, options);
|
||||
this._dragDrop.forEach(d => d.bind(htmlElement));
|
||||
|
||||
// Handle delta inputs
|
||||
for (const deltaInput of htmlElement.querySelectorAll('input[data-allow-delta]')) {
|
||||
|
|
@ -290,6 +284,16 @@ export default function DHApplicationMixin(Base) {
|
|||
async _onRender(context, options) {
|
||||
await super._onRender(context, options);
|
||||
this._createTagifyElements(this.options.tagifyConfigs);
|
||||
|
||||
for (const d of this.options.dragDrop) {
|
||||
new foundry.applications.ux.DragDrop.implementation({
|
||||
...d,
|
||||
callbacks: {
|
||||
dragstart: this._onDragStart.bind(this),
|
||||
drop: this._onDrop.bind(this)
|
||||
}
|
||||
}).bind(this.element);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
|
@ -350,21 +354,6 @@ export default function DHApplicationMixin(Base) {
|
|||
/* Drag and Drop */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Creates drag-drop handlers from the configured options.
|
||||
* @returns {foundry.applications.ux.DragDrop[]}
|
||||
* @private
|
||||
*/
|
||||
_createDragDropHandlers() {
|
||||
return this.options.dragDrop.map(d => {
|
||||
d.callbacks = {
|
||||
dragstart: this._onDragStart.bind(this),
|
||||
drop: this._onDrop.bind(this)
|
||||
};
|
||||
return new foundry.applications.ux.DragDrop.implementation(d);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dragStart event.
|
||||
* @param {DragEvent} event
|
||||
|
|
@ -499,7 +488,10 @@ export default function DHApplicationMixin(Base) {
|
|||
icon: 'fa-solid fa-explosion',
|
||||
condition: 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) => {
|
||||
const doc = await getDocFromElement(target),
|
||||
|
|
@ -742,11 +734,13 @@ export default function DHApplicationMixin(Base) {
|
|||
|
||||
const cls =
|
||||
type === 'action' ? game.system.api.models.actions.actionsTypes.base : getDocumentClass(documentClass);
|
||||
|
||||
const data = {
|
||||
name: cls.defaultName({ type, parent }),
|
||||
type,
|
||||
system: systemData
|
||||
};
|
||||
|
||||
if (inVault) data['system.inVault'] = true;
|
||||
if (disabled) data.disabled = true;
|
||||
if (type === 'domainCard' && parent?.system.domains?.length) {
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
|
|||
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;
|
||||
list.push(effect);
|
||||
}
|
||||
|
|
@ -228,7 +228,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
|
|||
'systems/daggerheart/templates/ui/chat/action.hbs',
|
||||
systemData
|
||||
),
|
||||
title: game.i18n.localize('DAGGERHEART.ACTIONS.Config.displayInChat'),
|
||||
speaker: cls.getSpeaker(),
|
||||
flags: {
|
||||
daggerheart: {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,15 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
|
|||
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`.
|
||||
* @param {Array<Object>} selectedOptions - The currently selected tag objects.
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export default class BeastformSheet extends DHBaseItemSheet {
|
|||
|
||||
async advantageOnRemove(event) {
|
||||
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() {
|
||||
const deleteUpdate = Object.keys(this.document._source.flags.daggerheart?.altFormula ?? {}).reduce(
|
||||
(acc, formulaKey) => {
|
||||
if (!this.daggerheartFlag.altFormula[formulaKey]) acc.altFormula[`-=${formulaKey}`] = null;
|
||||
if (!this.daggerheartFlag.altFormula[formulaKey]) acc.altFormula[formulaKey] = _del;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ 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() {
|
||||
|
|
@ -127,7 +128,7 @@ export default class DhRollTableSheet extends foundry.applications.sheets.RollTa
|
|||
|
||||
static async #removeFormula(_event, target) {
|
||||
await this.daggerheartFlag.updateSource({
|
||||
[`altFormula.-=${target.dataset.key}`]: null
|
||||
[`altFormula.${target.dataset.key}`]: _del
|
||||
});
|
||||
this.render({ internalRefresh: true });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +1,19 @@
|
|||
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 */
|
||||
static TABS = {
|
||||
chat: {
|
||||
documentName: 'ChatMessage'
|
||||
},
|
||||
combat: {
|
||||
documentName: 'Combat'
|
||||
},
|
||||
scenes: {
|
||||
documentName: 'Scene',
|
||||
gmOnly: true
|
||||
},
|
||||
actors: {
|
||||
documentName: 'Actor'
|
||||
},
|
||||
items: {
|
||||
documentName: 'Item'
|
||||
},
|
||||
journal: {
|
||||
documentName: 'JournalEntry',
|
||||
tooltip: 'SIDEBAR.TabJournal'
|
||||
},
|
||||
tables: {
|
||||
documentName: 'RollTable'
|
||||
},
|
||||
cards: {
|
||||
documentName: 'Cards'
|
||||
},
|
||||
macros: {
|
||||
documentName: 'Macro'
|
||||
},
|
||||
playlists: {
|
||||
documentName: 'Playlist'
|
||||
},
|
||||
compendium: {
|
||||
tooltip: 'SIDEBAR.TabCompendium',
|
||||
icon: 'fa-solid fa-book-atlas'
|
||||
},
|
||||
daggerheartMenu: {
|
||||
tooltip: 'DAGGERHEART.UI.Sidebar.daggerheartMenu.title',
|
||||
img: 'systems/daggerheart/assets/logos/FoundryBorneLogoWhite.svg',
|
||||
gmOnly: true
|
||||
},
|
||||
settings: {
|
||||
tooltip: 'SIDEBAR.TabSettings',
|
||||
icon: 'fa-solid fa-gears'
|
||||
}
|
||||
};
|
||||
static TABS = DhSidebar.buildTabs();
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
|
|||
},
|
||||
actions: {
|
||||
selectRefreshable: DaggerheartMenu.#selectRefreshable,
|
||||
refreshActors: DaggerheartMenu.#refreshActors
|
||||
refreshActors: DaggerheartMenu.#refreshActors,
|
||||
createFallCollisionDamage: DaggerheartMenu.#createFallCollisionDamage
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -50,6 +51,7 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
|
|||
const context = await super._prepareContext(options);
|
||||
context.refreshables = this.refreshSelections;
|
||||
context.disableRefresh = Object.values(this.refreshSelections).every(x => !x.selected);
|
||||
context.fallAndCollision = CONFIG.DH.GENERAL.fallAndCollisionDamage;
|
||||
|
||||
return context;
|
||||
}
|
||||
|
|
@ -71,4 +73,22 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
|
|||
this.refreshSelections = DaggerheartMenu.defaultRefreshSelections();
|
||||
this.render();
|
||||
}
|
||||
|
||||
static async #createFallCollisionDamage(_event, button) {
|
||||
const data = CONFIG.DH.GENERAL.fallAndCollisionDamage[button.dataset.key];
|
||||
const roll = new Roll(data.damageFormula);
|
||||
await roll.evaluate();
|
||||
|
||||
/* class BaseRoll needed to get rendered by foundryRoll.hbs */
|
||||
const rollJSON = roll.toJSON();
|
||||
rollJSON.class = 'BaseRoll';
|
||||
|
||||
foundry.documents.ChatMessage.implementation.create({
|
||||
title: game.i18n.localize(data.chatTitle),
|
||||
author: game.user.id,
|
||||
speaker: foundry.documents.ChatMessage.implementation.getSpeaker(),
|
||||
rolls: [rollJSON],
|
||||
sound: CONFIG.sounds.dice
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,3 +7,4 @@ export { default as DhFearTracker } from './fearTracker.mjs';
|
|||
export { default as DhHotbar } from './hotbar.mjs';
|
||||
export { default as DhSceneNavigation } from './sceneNavigation.mjs';
|
||||
export { ItemBrowser } from './itemBrowser.mjs';
|
||||
export { default as DhProgress } from './progress.mjs';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { abilities } from '../../config/actorConfig.mjs';
|
||||
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
|
||||
import { enrichedDualityRoll } from '../../enrichers/DualityRollEnricher.mjs';
|
||||
import { enrichedFateRoll, getFateTypeData } from '../../enrichers/FateRollEnricher.mjs';
|
||||
import { getCommandTarget, rollCommandToJSON } from '../../helpers/utils.mjs';
|
||||
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
|
||||
|
||||
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
|
||||
constructor(options) {
|
||||
|
|
@ -21,6 +24,84 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
|||
classes: ['daggerheart']
|
||||
};
|
||||
|
||||
static CHAT_COMMANDS = {
|
||||
...super.CHAT_COMMANDS,
|
||||
dr: {
|
||||
rgx: /^(?:\/dr)((?:\s)[^]*)?/,
|
||||
fn: (_, match) => {
|
||||
const argString = match[1]?.trim();
|
||||
const result = argString ? rollCommandToJSON(argString) : { result: {} };
|
||||
if (!result) {
|
||||
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.dualityParsing'));
|
||||
return false;
|
||||
}
|
||||
|
||||
const { result: rollCommand, flavor } = result;
|
||||
|
||||
const reaction = rollCommand.reaction;
|
||||
const traitValue = rollCommand.trait?.toLowerCase();
|
||||
const advantage = rollCommand.advantage
|
||||
? CONFIG.DH.ACTIONS.advantageState.advantage.value
|
||||
: rollCommand.disadvantage
|
||||
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
|
||||
: undefined;
|
||||
const difficulty = rollCommand.difficulty;
|
||||
const grantResources = rollCommand.grantResources;
|
||||
|
||||
const target = getCommandTarget({ allowNull: true });
|
||||
const title =
|
||||
(flavor ?? traitValue)
|
||||
? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
||||
ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label)
|
||||
})
|
||||
: game.i18n.localize('DAGGERHEART.GENERAL.duality');
|
||||
|
||||
enrichedDualityRoll({
|
||||
reaction,
|
||||
traitValue,
|
||||
target,
|
||||
difficulty,
|
||||
title,
|
||||
label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'),
|
||||
actionType: null,
|
||||
advantage,
|
||||
grantResources
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
fr: {
|
||||
rgx: /^(?:\/fr)((?:\s)[^]*)?/,
|
||||
fn: (_, match) => {
|
||||
const argString = match[1]?.trim();
|
||||
const result = argString ? rollCommandToJSON(argString) : { result: {} };
|
||||
|
||||
if (!result) {
|
||||
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateParsing'));
|
||||
return false;
|
||||
}
|
||||
|
||||
const { result: rollCommand, flavor } = result;
|
||||
const fateTypeData = getFateTypeData(rollCommand?.type);
|
||||
|
||||
if (!fateTypeData)
|
||||
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
|
||||
|
||||
const { value: fateType, label: fateTypeLabel } = fateTypeData;
|
||||
const target = getCommandTarget({ allowNull: true });
|
||||
const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll');
|
||||
|
||||
enrichedFateRoll({
|
||||
target,
|
||||
title,
|
||||
label: fateTypeLabel,
|
||||
fateType
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_getEntryContextOptions() {
|
||||
return [
|
||||
...super._getEntryContextOptions(),
|
||||
|
|
@ -175,7 +256,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
|||
action.use(event);
|
||||
}
|
||||
|
||||
async rerollEvent(event, message) {
|
||||
async rerollEvent(event, messageData) {
|
||||
event.stopPropagation();
|
||||
if (!event.shiftKey) {
|
||||
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
||||
|
|
@ -187,37 +268,40 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
|||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
const message = game.messages.get(messageData._id);
|
||||
const target = event.target.closest('[data-die-index]');
|
||||
|
||||
if (target.dataset.type === 'damage') {
|
||||
game.system.api.dice.DamageRoll.reroll(target, message);
|
||||
} else {
|
||||
let originalRoll_parsed = message.rolls.map(roll => JSON.parse(roll))[0];
|
||||
const rollClass =
|
||||
game.system.api.dice[
|
||||
message.type === 'dualityRoll'
|
||||
? 'DualityRoll'
|
||||
: target.dataset.type === 'damage'
|
||||
? 'DHRoll'
|
||||
: 'D20Roll'
|
||||
];
|
||||
|
||||
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);
|
||||
|
||||
await game.messages.get(message._id).update({
|
||||
'system.roll': newRoll,
|
||||
'rolls': [parsedRoll]
|
||||
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
|
||||
};
|
||||
});
|
||||
|
||||
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
|
||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
||||
action: socketEvent.Refresh,
|
||||
data: {
|
||||
refreshType: RefreshType.TagTeamRoll
|
||||
const updateMessage = game.messages.get(message._id);
|
||||
await updateMessage.update({
|
||||
[`system.damage.${damageType}`]: {
|
||||
total: parsedRoll.total,
|
||||
parts: damageParts
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const rerollDice = message.system.roll.dice[target.dataset.dieIndex];
|
||||
await rerollDice.reroll(`/r1=${rerollDice.total}`, {
|
||||
liveRoll: {
|
||||
roll: message.system.roll,
|
||||
actor: message.system.actionActor,
|
||||
isReaction: message.system.roll.options.actionType === 'reaction'
|
||||
}
|
||||
});
|
||||
await message.update({
|
||||
rolls: [message.system.roll.toJSON()]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { AdversaryBPPerEncounter } from '../../config/encounterConfig.mjs';
|
||||
import { expireActiveEffects } from '../../helpers/utils.mjs';
|
||||
import { clearPreviousSpotlight } from '../../macros/spotlightCombatant.mjs';
|
||||
|
||||
export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
|
||||
static DEFAULT_OPTIONS = {
|
||||
|
|
@ -149,13 +151,13 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
|
|||
}
|
||||
|
||||
async setCombatantSpotlight(combatantId) {
|
||||
const combatant = this.viewed.combatants.get(combatantId);
|
||||
const update = {
|
||||
system: {
|
||||
'spotlight.requesting': false,
|
||||
'spotlight.requestOrderIndex': 0
|
||||
}
|
||||
};
|
||||
const combatant = this.viewed.combatants.get(combatantId);
|
||||
|
||||
const toggleTurn = this.viewed.combatants.contents
|
||||
.sort(this.viewed._sortCombatants)
|
||||
|
|
@ -177,6 +179,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
|
|||
if (autoPoints) {
|
||||
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({
|
||||
|
|
@ -184,6 +188,14 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
|
|||
round: this.viewed.round + 1
|
||||
});
|
||||
await combatant.update(update);
|
||||
if (combatant.token) clearPreviousSpotlight();
|
||||
}
|
||||
|
||||
async clearTurn() {
|
||||
await this.viewed.update({
|
||||
turn: null,
|
||||
round: this.viewed.round + 1
|
||||
});
|
||||
}
|
||||
|
||||
static async requestSpotlight(_, target) {
|
||||
|
|
|
|||
|
|
@ -233,6 +233,6 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio
|
|||
}
|
||||
|
||||
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 */
|
||||
async _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');
|
||||
header.querySelector('button[data-action="close"]').remove();
|
||||
header.querySelector('button[data-action="toggleControls"]').remove();
|
||||
|
||||
if (game.user.isGM) {
|
||||
const editTooltip = game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.editTitle');
|
||||
|
|
@ -278,10 +275,8 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
|
|||
return acc;
|
||||
}, {})
|
||||
};
|
||||
await emitAsGM(GMUpdateEvent.UpdateCountdowns,
|
||||
DhCountdowns.gmSetSetting.bind(settings),
|
||||
settings, null, {
|
||||
refreshType: RefreshType.Countdown
|
||||
await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
|
||||
refreshType: RefreshType.Countdown
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { getIconVisibleActiveEffects } from '../../helpers/utils.mjs';
|
||||
import { RefreshType } from '../../systemRegistration/socket.mjs';
|
||||
|
||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||
|
|
@ -48,11 +49,9 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
|
|||
|
||||
_attachPartListeners(partId, htmlElement, options) {
|
||||
super._attachPartListeners(partId, htmlElement, options);
|
||||
|
||||
if (this.element) {
|
||||
this.element.querySelectorAll('.effect-container a').forEach(element => {
|
||||
element.addEventListener('contextmenu', this.removeEffect.bind(this));
|
||||
});
|
||||
for (const element of this.element?.querySelectorAll('.effect-container a') ?? []) {
|
||||
element.addEventListener('click', e => this.#onClickEffect(e));
|
||||
element.addEventListener('contextmenu', e => this.#onClickEffect(e, -1));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +71,7 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
|
|||
? game.user.character
|
||||
: null
|
||||
: canvas.tokens.controlled[0].actor;
|
||||
return actor?.getActiveEffects() ?? [];
|
||||
return getIconVisibleActiveEffects(actor?.getActiveEffects() ?? []);
|
||||
};
|
||||
|
||||
toggleHidden(token, focused) {
|
||||
|
|
@ -86,11 +85,22 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
|
|||
this.render();
|
||||
}
|
||||
|
||||
async removeEffect(event) {
|
||||
async #onClickEffect(event, delta = 1) {
|
||||
const element = event.target.closest('.effect-container');
|
||||
const effects = DhEffectsDisplay.getTokenEffects();
|
||||
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();
|
||||
}
|
||||
|
||||
setupHooks() {
|
||||
|
|
|
|||
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(
|
||||
x => x && x.testUserPermission(game.user, 'LIMITED')
|
||||
);
|
||||
const hasEnvironments = environments.length > 0 && x.isView;
|
||||
const hasEnvironments = environments.length > 0 && x.active;
|
||||
return {
|
||||
...x,
|
||||
hasEnvironments,
|
||||
|
|
@ -39,9 +39,10 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi
|
|||
environments: environments
|
||||
};
|
||||
});
|
||||
|
||||
context.scenes.active = extendScenes(context.scenes.active);
|
||||
context.scenes.inactive = extendScenes(context.scenes.inactive);
|
||||
|
||||
context.scenes.viewed = context.scenes.viewed ? extendScenes([context.scenes.viewed])[0] : null;
|
||||
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 {
|
||||
/**
|
||||
* @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.
|
||||
* @param {PointerEvent} event
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export { default as DhMeasuredTemplate } from './measuredTemplate.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 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';
|
||||
|
||||
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
||||
|
|
@ -9,6 +10,36 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
|||
this.previewHelp ||= this.addChild(this.#drawPreviewHelp());
|
||||
}
|
||||
|
||||
/**@inheritdoc */
|
||||
_refreshTurnMarker() {
|
||||
// Should a Turn Marker be active?
|
||||
const { turnMarker } = this.document;
|
||||
const markersEnabled =
|
||||
CONFIG.Combat.settings.turnMarker.enabled && turnMarker.mode !== CONST.TOKEN_TURN_MARKER_MODES.DISABLED;
|
||||
const spotlighted = game.settings
|
||||
.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.SpotlightTracker)
|
||||
.spotlightedTokens.has(this.document.uuid);
|
||||
|
||||
const turnIsSet = typeof game.combat?.turn === 'number';
|
||||
const isTurn = game.combat?.combatant?.tokenId === this.id;
|
||||
const markerActive = markersEnabled && turnIsSet ? isTurn : spotlighted;
|
||||
|
||||
// Activate a Turn Marker
|
||||
if (markerActive) {
|
||||
if (!this.turnMarker)
|
||||
this.turnMarker = this.addChildAt(new foundry.canvas.placeables.tokens.TokenTurnMarker(this), 0);
|
||||
canvas.tokens.turnMarkers.add(this);
|
||||
this.turnMarker.draw();
|
||||
}
|
||||
|
||||
// Remove a Turn Marker
|
||||
else if (this.turnMarker) {
|
||||
canvas.tokens.turnMarkers.delete(this);
|
||||
this.turnMarker.destroy();
|
||||
this.turnMarker = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
async _drawEffects() {
|
||||
this.effects.renderable = false;
|
||||
|
|
@ -20,7 +51,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
|||
this.effects.overlay = null;
|
||||
|
||||
// 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'));
|
||||
|
||||
// Draw effects
|
||||
|
|
@ -29,8 +60,8 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
|||
if (!effect.img) continue;
|
||||
const promise =
|
||||
effect === overlayEffect
|
||||
? this._drawOverlay(effect.img, effect.tint)
|
||||
: this._drawEffect(effect.img, effect.tint);
|
||||
? this._drawOverlay(effect.img, effect.tint, effect)
|
||||
: this._drawEffect(effect.img, effect.tint, effect);
|
||||
promises.push(
|
||||
promise.then(e => {
|
||||
if (e) e.zIndex = i;
|
||||
|
|
@ -44,6 +75,39 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
|||
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.
|
||||
* 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 = {
|
||||
...CONST.MEASURED_TEMPLATE_TYPES,
|
||||
CIRCLE: 'circle',
|
||||
CONE: 'cone',
|
||||
RECTANGLE: 'rectangle',
|
||||
LINE: 'line',
|
||||
EMANATION: 'emanation',
|
||||
INFRONT: 'inFront'
|
||||
};
|
||||
|
|
@ -241,8 +245,8 @@ export const defaultRestOptions = {
|
|||
type: 'friendly'
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
hitPoints: {
|
||||
applyTo: healingTypes.hitPoints.id,
|
||||
value: {
|
||||
custom: {
|
||||
|
|
@ -251,7 +255,7 @@ export const defaultRestOptions = {
|
|||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -275,8 +279,8 @@ export const defaultRestOptions = {
|
|||
type: 'self'
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
stress: {
|
||||
applyTo: healingTypes.stress.id,
|
||||
value: {
|
||||
custom: {
|
||||
|
|
@ -285,7 +289,7 @@ export const defaultRestOptions = {
|
|||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -310,8 +314,8 @@ export const defaultRestOptions = {
|
|||
type: 'friendly'
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
armor: {
|
||||
applyTo: healingTypes.armor.id,
|
||||
value: {
|
||||
custom: {
|
||||
|
|
@ -320,7 +324,7 @@ export const defaultRestOptions = {
|
|||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -344,8 +348,8 @@ export const defaultRestOptions = {
|
|||
type: 'self'
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
hope: {
|
||||
applyTo: healingTypes.hope.id,
|
||||
value: {
|
||||
custom: {
|
||||
|
|
@ -354,7 +358,7 @@ export const defaultRestOptions = {
|
|||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
prepareWithFriends: {
|
||||
|
|
@ -368,8 +372,8 @@ export const defaultRestOptions = {
|
|||
type: 'self'
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
hope: {
|
||||
applyTo: healingTypes.hope.id,
|
||||
value: {
|
||||
custom: {
|
||||
|
|
@ -378,7 +382,7 @@ export const defaultRestOptions = {
|
|||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -405,8 +409,8 @@ export const defaultRestOptions = {
|
|||
type: 'friendly'
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
hitPoints: {
|
||||
applyTo: healingTypes.hitPoints.id,
|
||||
value: {
|
||||
custom: {
|
||||
|
|
@ -415,7 +419,7 @@ export const defaultRestOptions = {
|
|||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -439,8 +443,8 @@ export const defaultRestOptions = {
|
|||
type: 'self'
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
stress: {
|
||||
applyTo: healingTypes.stress.id,
|
||||
value: {
|
||||
custom: {
|
||||
|
|
@ -449,7 +453,7 @@ export const defaultRestOptions = {
|
|||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -474,8 +478,8 @@ export const defaultRestOptions = {
|
|||
type: 'friendly'
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
armor: {
|
||||
applyTo: healingTypes.armor.id,
|
||||
value: {
|
||||
custom: {
|
||||
|
|
@ -484,7 +488,7 @@ export const defaultRestOptions = {
|
|||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -508,8 +512,8 @@ export const defaultRestOptions = {
|
|||
type: 'self'
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
hope: {
|
||||
applyTo: healingTypes.hope.id,
|
||||
value: {
|
||||
custom: {
|
||||
|
|
@ -518,7 +522,7 @@ export const defaultRestOptions = {
|
|||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
prepareWithFriends: {
|
||||
|
|
@ -532,8 +536,8 @@ export const defaultRestOptions = {
|
|||
type: 'self'
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
hope: {
|
||||
applyTo: healingTypes.hope.id,
|
||||
value: {
|
||||
custom: {
|
||||
|
|
@ -542,7 +546,7 @@ export const defaultRestOptions = {
|
|||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -954,3 +958,129 @@ export const sceneRangeMeasurementSetting = {
|
|||
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'
|
||||
}
|
||||
};
|
||||
|
||||
export const fallAndCollisionDamage = {
|
||||
veryClose: {
|
||||
id: 'veryClose',
|
||||
label: 'DAGGERHEART.CONFIG.fallAndCollision.veryClose.label',
|
||||
chatTitle: 'DAGGERHEART.CONFIG.fallAndCollision.veryClose.chatTitle',
|
||||
damageFormula: '1d10 + 3'
|
||||
},
|
||||
close: {
|
||||
id: 'veryClose',
|
||||
label: 'DAGGERHEART.CONFIG.fallAndCollision.close.label',
|
||||
chatTitle: 'DAGGERHEART.CONFIG.fallAndCollision.close.chatTitle',
|
||||
damageFormula: '1d20 + 5'
|
||||
},
|
||||
far: {
|
||||
id: 'veryClose',
|
||||
label: 'DAGGERHEART.CONFIG.fallAndCollision.far.label',
|
||||
chatTitle: 'DAGGERHEART.CONFIG.fallAndCollision.far.chatTitle',
|
||||
damageFormula: '1d100 + 15'
|
||||
},
|
||||
collision: {
|
||||
id: 'veryClose',
|
||||
label: 'DAGGERHEART.CONFIG.fallAndCollision.collision.label',
|
||||
chatTitle: 'DAGGERHEART.CONFIG.fallAndCollision.collision.chatTitle',
|
||||
damageFormula: '1d20 + 5'
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export const hooksConfig = {
|
||||
effectDisplayToggle: 'DHEffectDisplayToggle',
|
||||
lockedTooltipDismissed: 'DHLockedTooltipDismissed'
|
||||
lockedTooltipDismissed: 'DHLockedTooltipDismissed',
|
||||
tagTeamStart: 'DHTagTeamRollStart'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ export const armorFeatures = {
|
|||
type: 'hostile'
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
stress: {
|
||||
applyTo: 'stress',
|
||||
value: {
|
||||
custom: {
|
||||
|
|
@ -24,7 +24,7 @@ export const armorFeatures = {
|
|||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -489,15 +489,18 @@ export const weaponFeatures = {
|
|||
description: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.effects.barrier.description',
|
||||
img: 'icons/skills/melee/shield-block-bash-blue.webp',
|
||||
changes: [
|
||||
{
|
||||
key: 'system.armorScore',
|
||||
mode: 2,
|
||||
value: 'ITEM.@system.tier + 1'
|
||||
},
|
||||
{
|
||||
key: 'system.evasion',
|
||||
mode: 2,
|
||||
value: '-1'
|
||||
},
|
||||
{
|
||||
key: 'Armor',
|
||||
type: 'armor',
|
||||
typeData: {
|
||||
type: 'armor',
|
||||
max: 'ITEM.@system.tier + 1'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -732,8 +735,8 @@ export const weaponFeatures = {
|
|||
type: 'hostile'
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
stress: {
|
||||
applyTo: 'stress',
|
||||
value: {
|
||||
custom: {
|
||||
|
|
@ -742,7 +745,7 @@ export const weaponFeatures = {
|
|||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
@ -789,11 +792,6 @@ export const weaponFeatures = {
|
|||
description: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.description',
|
||||
img: 'icons/skills/melee/sword-shield-stylized-white.webp',
|
||||
changes: [
|
||||
{
|
||||
key: 'system.armorScore',
|
||||
mode: 2,
|
||||
value: '1'
|
||||
},
|
||||
{
|
||||
key: 'system.bonuses.damage.primaryWeapon.bonus',
|
||||
mode: 2,
|
||||
|
|
@ -808,6 +806,22 @@ export const weaponFeatures = {
|
|||
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'
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
hitPoints: {
|
||||
applyTo: 'hitPoints',
|
||||
value: {
|
||||
custom: {
|
||||
|
|
@ -924,7 +938,7 @@ export const weaponFeatures = {
|
|||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -1191,9 +1205,13 @@ export const weaponFeatures = {
|
|||
img: 'icons/skills/melee/shield-block-gray-orange.webp',
|
||||
changes: [
|
||||
{
|
||||
key: 'system.armorScore',
|
||||
mode: 2,
|
||||
value: 'ITEM.@system.tier'
|
||||
key: 'Armor',
|
||||
type: 'armor',
|
||||
value: 0,
|
||||
typeData: {
|
||||
type: 'armor',
|
||||
max: 'ITEM.@system.tier'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ export const gameSettings = {
|
|||
LevelTiers: 'LevelTiers',
|
||||
Countdowns: 'Countdowns',
|
||||
LastMigrationVersion: 'LastMigrationVersion',
|
||||
TagTeamRoll: 'TagTeamRoll',
|
||||
SpotlightRequestQueue: 'SpotlightRequestQueue',
|
||||
CompendiumBrowserSettings: 'CompendiumBrowserSettings'
|
||||
CompendiumBrowserSettings: 'CompendiumBrowserSettings',
|
||||
SpotlightTracker: 'SpotlightTracker'
|
||||
};
|
||||
|
||||
export const actionAutomationChoices = {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
export { default as DhCombat } from './combat.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 RegisteredTriggers } from './registeredTriggers.mjs';
|
||||
export { default as CompendiumBrowserSettings } from './compendiumBrowserSettings.mjs';
|
||||
export { default as TagTeamData } from './tagTeamData.mjs';
|
||||
export { default as SpotlightTracker } from './spotlightTracker.mjs';
|
||||
|
||||
export * as countdowns from './countdowns.mjs';
|
||||
export * as actions from './action/_module.mjs';
|
||||
|
|
|
|||
|
|
@ -26,23 +26,23 @@ export default class DHAttackAction extends DHDamageAction {
|
|||
return {
|
||||
value: {
|
||||
multiplier: 'prof',
|
||||
dice: this.item?.system?.attack.damage.parts[0].value.dice,
|
||||
bonus: this.item?.system?.attack.damage.parts[0].value.bonus ?? 0
|
||||
dice: this.item?.system?.attack.damage.parts.hitPoints.value.dice,
|
||||
bonus: this.item?.system?.attack.damage.parts.hitPoints.value.bonus ?? 0
|
||||
},
|
||||
type: this.item?.system?.attack.damage.parts[0].type,
|
||||
type: this.item?.system?.attack.damage.parts.hitPoints.type,
|
||||
base: true
|
||||
};
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
return hitPointsPart.value.getFormula();
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
return hitPointsPart.valueAlt.getFormula();
|
||||
|
|
@ -50,9 +50,8 @@ export default class DHAttackAction extends DHDamageAction {
|
|||
|
||||
async 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;
|
||||
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
|
||||
* @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.");
|
||||
|
||||
let config = this.prepareConfig(event);
|
||||
let config = this.prepareConfig(event, configOptions);
|
||||
if (!config) return;
|
||||
|
||||
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 (this.chatDisplay && !config.actionChatMessageHandled) await this.toChat();
|
||||
if (this.chatDisplay && !config.skips.createMessage && !config.actionChatMessageHandled) await this.toChat();
|
||||
|
||||
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
|
||||
* @returns {object}
|
||||
*/
|
||||
prepareBaseConfig(event) {
|
||||
prepareBaseConfig(event, configOptions = {}) {
|
||||
const isActor = this.item instanceof CONFIG.Actor.documentClass;
|
||||
const actionTitle = game.i18n.localize(this.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,
|
||||
onSave: this.save?.damageMod,
|
||||
isDirect: !!this.damage?.direct,
|
||||
selectedRollMode: game.settings.get('core', 'rollMode'),
|
||||
selectedMessageMode: game.settings.get('core', 'messageMode'),
|
||||
data: this.getRollData(),
|
||||
evaluate: this.hasRoll,
|
||||
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);
|
||||
|
|
@ -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
|
||||
* @returns {object}
|
||||
*/
|
||||
prepareConfig(event) {
|
||||
const config = this.prepareBaseConfig(event);
|
||||
prepareConfig(event, configOptions = {}) {
|
||||
const config = this.prepareBaseConfig(event, configOptions);
|
||||
for (const clsField of Object.values(this.schema.fields)) {
|
||||
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) {
|
||||
if (!actor) return [];
|
||||
|
||||
return Array.from(await actor.allApplicableEffects()).filter(effect => {
|
||||
/* Effects on weapons only ever apply for the weapon itself */
|
||||
if (effect.parent.type === 'weapon') {
|
||||
/* Unless they're secondary - then they apply only to other primary weapons */
|
||||
if (effect.parent.system.secondary) {
|
||||
if (effectParent?.type !== 'weapon' || effectParent?.system.secondary) return false;
|
||||
} else if (effectParent?.id !== effect.parent.id) return false;
|
||||
}
|
||||
return Array.from(await actor.allApplicableEffects({ noTransferArmor: true, noSelfArmor: true })).filter(
|
||||
effect => {
|
||||
/* Effects on weapons only ever apply for the weapon itself */
|
||||
if (effect.parent.type === 'weapon') {
|
||||
/* Unless they're secondary - then they apply only to other primary weapons */
|
||||
if (effect.parent.system.secondary) {
|
||||
if (effectParent?.type !== 'weapon' || effectParent?.system.secondary) return false;
|
||||
} else if (effectParent?.id !== effect.parent.id) return false;
|
||||
}
|
||||
|
||||
return !effect.isSuppressed;
|
||||
});
|
||||
return !effect.isSuppressed;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -326,6 +337,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
|||
* @param {boolean} successCost
|
||||
*/
|
||||
async consume(config, successCost = false) {
|
||||
config.resourceUpdates = new ResourceUpdateMap(config.actionActor);
|
||||
await this.workflow.get('cost')?.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() {
|
||||
return this.damage?.parts?.length && this.type !== 'healing';
|
||||
return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type !== 'healing';
|
||||
}
|
||||
|
||||
get hasHealing() {
|
||||
return this.damage?.parts?.length && this.type === 'healing';
|
||||
return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type === 'healing';
|
||||
}
|
||||
|
||||
get hasSave() {
|
||||
|
|
@ -378,6 +390,15 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
|||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -2,84 +2,4 @@ import DHBaseAction from './baseAction.mjs';
|
|||
|
||||
export default class DhBeastformAction extends DHBaseAction {
|
||||
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 BeastformEffect from './beastformEffect.mjs';
|
||||
import HordeEffect from './hordeEffect.mjs';
|
||||
export { changeTypes, changeEffects } from './changeTypes/_module.mjs';
|
||||
|
||||
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)
|
||||
*/
|
||||
|
||||
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() {
|
||||
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 {
|
||||
...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({
|
||||
enabled: new fields.BooleanField({
|
||||
required: true,
|
||||
|
|
@ -41,10 +80,57 @@ export default class BaseEffect extends foundry.abstract.TypeDataModel {
|
|||
initial: CONFIG.DH.GENERAL.range.melee.id,
|
||||
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() {
|
||||
return {
|
||||
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() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
characterTokenData: new fields.SchemaField({
|
||||
usesDynamicToken: new fields.BooleanField({ initial: false }),
|
||||
tokenImg: new fields.FilePathField({
|
||||
|
|
@ -24,7 +25,7 @@ export default class BeastformEffect extends BaseEffect {
|
|||
width: new fields.NumberField({ integer: false, nullable: true })
|
||||
})
|
||||
}),
|
||||
advantageOn: new fields.ArrayField(new fields.StringField()),
|
||||
advantageOn: new fields.TypedObjectField(new fields.SchemaField({ value: new fields.StringField() })),
|
||||
featureIds: new fields.ArrayField(new fields.StringField()),
|
||||
effectIds: new fields.ArrayField(new fields.StringField())
|
||||
};
|
||||
|
|
@ -99,7 +100,7 @@ export default class BeastformEffect extends BaseEffect {
|
|||
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'
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
hitPoints: {
|
||||
type: ['physical'],
|
||||
value: {
|
||||
multiplier: 'flat'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
|
@ -265,12 +265,12 @@ export default class DhpAdversary extends DhCreature {
|
|||
}
|
||||
|
||||
// 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)) {
|
||||
if (!action.damage) continue;
|
||||
|
||||
// Parse damage, and convert all formula matches in the descriptions to the new damage
|
||||
try {
|
||||
const result = this.#adjustActionDamage(action, { ...damageMeta, type: 'action' });
|
||||
if (!result) continue;
|
||||
|
||||
for (const { previousFormula, formula } of Object.values(result)) {
|
||||
const oldFormulaRegexp = new RegExp(
|
||||
previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?')
|
||||
|
|
@ -372,16 +372,14 @@ export default class DhpAdversary extends DhCreature {
|
|||
/**
|
||||
* Updates damage to reflect a specific value.
|
||||
* @throws if damage structure is invalid for conversion
|
||||
* @returns the converted formula and value as a simplified term
|
||||
* @returns the converted formula and value as a simplified term, or null if it doesn't deal HP damage
|
||||
*/
|
||||
#adjustActionDamage(action, damageMeta) {
|
||||
// The current algorithm only returns a value if there is a single damage part
|
||||
const hpDamageParts = action.damage.parts.filter(d => d.applyTo === 'hitPoints');
|
||||
if (hpDamageParts.length !== 1) throw new Error('incorrect number of hp parts');
|
||||
if (!action.damage?.parts.hitPoints) return null;
|
||||
|
||||
const result = {};
|
||||
for (const property of ['value', 'valueAlt']) {
|
||||
const data = hpDamageParts[0][property];
|
||||
const data = action.damage.parts.hitPoints[property];
|
||||
const previousFormula = data.custom.enabled
|
||||
? data.custom.formula
|
||||
: [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0]
|
||||
|
|
|
|||
|
|
@ -189,21 +189,6 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
|
|||
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) {
|
||||
const allowed = await super._preUpdate(changes, options, userId);
|
||||
if (allowed === false) return;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import DhCreature from './creature.mjs';
|
|||
import { attributeField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs';
|
||||
import { ActionField } from '../fields/actionField.mjs';
|
||||
import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs';
|
||||
import { getArmorSources } from '../../helpers/utils.mjs';
|
||||
|
||||
export default class DhCharacter extends DhCreature {
|
||||
/**@override */
|
||||
|
|
@ -41,17 +42,16 @@ export default class DhCharacter extends DhCreature {
|
|||
label: 'DAGGERHEART.GENERAL.proficiency'
|
||||
}),
|
||||
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({
|
||||
severe: new fields.NumberField({
|
||||
integer: true,
|
||||
initial: 0,
|
||||
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
|
||||
}),
|
||||
major: new fields.NumberField({
|
||||
integer: true,
|
||||
initial: 0,
|
||||
label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold'
|
||||
}),
|
||||
severe: new fields.NumberField({
|
||||
integer: true,
|
||||
initial: 0,
|
||||
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
|
||||
})
|
||||
}),
|
||||
experiences: new fields.TypedObjectField(
|
||||
|
|
@ -96,8 +96,8 @@ export default class DhCharacter extends DhCreature {
|
|||
trait: 'strength'
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
hitPoints: {
|
||||
type: ['physical'],
|
||||
value: {
|
||||
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() {
|
||||
const ancestryFeatures = [],
|
||||
communityFeatures = [],
|
||||
|
|
@ -588,6 +683,10 @@ export default class DhCharacter extends DhCreature {
|
|||
|
||||
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;
|
||||
|
||||
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 = {
|
||||
major: armor
|
||||
? armor.system.baseThresholds.major + this.levelData.level.current
|
||||
major: this.armor
|
||||
? this.armor.system.baseThresholds.major + this.levelData.level.current
|
||||
: this.levelData.level.current,
|
||||
severe: armor
|
||||
? armor.system.baseThresholds.severe + this.levelData.level.current
|
||||
severe: this.armor
|
||||
? this.armor.system.baseThresholds.severe + this.levelData.level.current
|
||||
: 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.resources.armor = {
|
||||
...this.armorScore,
|
||||
label: 'DAGGERHEART.GENERAL.armor',
|
||||
value: this.armor?.system?.marks?.value ?? 0,
|
||||
max: this.armorScore,
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -81,15 +81,15 @@ export default class DhCompanion extends DhCreature {
|
|||
bonus: 0
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
hitPoints: {
|
||||
type: ['physical'],
|
||||
value: {
|
||||
dice: 'd6',
|
||||
multiplier: 'prof'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
|
@ -135,7 +135,9 @@ export default class DhCompanion extends DhCreature {
|
|||
break;
|
||||
case 'vicious':
|
||||
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 {
|
||||
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(() => {
|
||||
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 ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
|
||||
import TagTeamData from '../tagTeamData.mjs';
|
||||
|
||||
export default class DhParty extends BaseDataActor {
|
||||
/**@inheritdoc */
|
||||
|
|
@ -14,7 +15,8 @@ export default class DhParty extends BaseDataActor {
|
|||
handfuls: new fields.NumberField({ initial: 1, integer: true }),
|
||||
bags: 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) {
|
||||
super._onDelete(options, userId);
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
|
|||
return {
|
||||
title: new fields.StringField(),
|
||||
actionDescription: new fields.HTMLField(),
|
||||
roll: new fields.ObjectField(),
|
||||
targets: targetsField(),
|
||||
hasRoll: new fields.BooleanField({ initial: false }),
|
||||
hasDamage: new fields.BooleanField({ initial: false }),
|
||||
|
|
@ -41,7 +40,6 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
|
|||
hasSave: new fields.BooleanField({ initial: false }),
|
||||
hasTarget: new fields.BooleanField({ initial: false }),
|
||||
isDirect: new fields.BooleanField({ initial: false }),
|
||||
isCritical: new fields.BooleanField({ initial: false }),
|
||||
onSave: new fields.StringField(),
|
||||
source: new fields.SchemaField({
|
||||
actor: new fields.StringField(),
|
||||
|
|
@ -55,6 +53,19 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
|
|||
};
|
||||
}
|
||||
|
||||
get roll() {
|
||||
switch (this.parent.type) {
|
||||
case 'adversaryRoll':
|
||||
return this.parent.rolls.find(x => x instanceof game.system.api.dice.D20Roll);
|
||||
case 'dualityRoll':
|
||||
return this.parent.rolls.find(x => x instanceof game.system.api.dice.DualityRoll);
|
||||
case 'fateRoll':
|
||||
return this.parent.rolls.find(x => x instanceof game.system.api.dice.FateRoll);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
get actionActor() {
|
||||
if (!this.source.actor) return null;
|
||||
return fromUuidSync(this.source.actor);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export { ActionCollection } from './actionField.mjs';
|
||||
export { default as IterableTypedObjectField } from './iterableTypedObjectField.mjs';
|
||||
export { default as FormulaField } from './formulaField.mjs';
|
||||
export { default as ForeignDocumentUUIDField } from './foreignDocumentUUIDField.mjs';
|
||||
export { default as ForeignDocumentUUIDArrayField } from './foreignDocumentUUIDArrayField.mjs';
|
||||
export { default as TriggerField } from './triggerField.mjs';
|
||||
export { default as MappingField } from './mappingField.mjs';
|
||||
export * as ActionFields from './action/_module.mjs';
|
||||
|
|
|
|||
|
|
@ -28,8 +28,21 @@ export default class BeastformField extends fields.SchemaField {
|
|||
{ 1: game.i18n.localize('DAGGERHEART.GENERAL.Tiers.1') }
|
||||
);
|
||||
},
|
||||
hint: 'DAGGERHEART.ACTIONS.Config.beastform.exactHint'
|
||||
label: 'DAGGERHEART.ACTIONS.Config.beastform.exact.label',
|
||||
hint: 'DAGGERHEART.ACTIONS.Config.beastform.exact.hint'
|
||||
})
|
||||
}),
|
||||
modifications: new fields.SchemaField({
|
||||
traitBonuses: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
bonus: new fields.NumberField({
|
||||
integer: true,
|
||||
initial: 1,
|
||||
min: 1,
|
||||
label: 'DAGGERHEART.ACTIONS.Config.beastform.modifications.traitBonuses.bonus'
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
};
|
||||
super(beastformFields, options, context);
|
||||
|
|
@ -66,15 +79,9 @@ export default class BeastformField extends fields.SchemaField {
|
|||
) ?? 1;
|
||||
|
||||
config.tierLimit = this.beastform.tierAccess.exact ?? actorTier;
|
||||
config.modifications = this.beastform.modifications;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO by Harry
|
||||
* @param {*} selectedForm
|
||||
* @param {*} evolvedData
|
||||
* @param {*} hybridData
|
||||
* @returns
|
||||
*/
|
||||
static async transform(selectedForm, evolvedData, hybridData) {
|
||||
const formData = evolvedData?.form ?? selectedForm;
|
||||
const beastformEffect = formData.effects.find(x => x.type === 'beastform');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import FormulaField from '../formulaField.mjs';
|
||||
import { setsEqual } from '../../../helpers/utils.mjs';
|
||||
import IterableTypedObjectField from '../iterableTypedObjectField.mjs';
|
||||
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
|
|
@ -12,7 +13,7 @@ export default class DamageField extends fields.SchemaField {
|
|||
/** @inheritDoc */
|
||||
constructor(options, context = {}) {
|
||||
const damageFields = {
|
||||
parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)),
|
||||
parts: new IterableTypedObjectField(DHDamageData),
|
||||
includeBase: new fields.BooleanField({
|
||||
initial: false,
|
||||
label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label'
|
||||
|
|
@ -48,11 +49,14 @@ export default class DamageField extends fields.SchemaField {
|
|||
|
||||
formulas = DamageField.formatFormulas.call(this, formulas, config);
|
||||
|
||||
messageId = config.message?._id ?? messageId;
|
||||
const message = game.messages.get(messageId);
|
||||
const damageConfig = {
|
||||
dialog: {},
|
||||
...config,
|
||||
roll: formulas,
|
||||
dialog: {},
|
||||
data: this.getRollData()
|
||||
data: this.getRollData(),
|
||||
isCritical: Boolean(message?.system.roll?.isCritical)
|
||||
};
|
||||
delete damageConfig.evaluate;
|
||||
|
||||
|
|
@ -60,7 +64,7 @@ export default class DamageField extends fields.SchemaField {
|
|||
damageConfig.dialog.configure = false;
|
||||
if (config.hasSave) config.onSave = damageConfig.onSave = this.save.damageMod;
|
||||
|
||||
damageConfig.source.message = config.message?._id ?? messageId;
|
||||
damageConfig.source.message = messageId;
|
||||
damageConfig.directDamage = !!damageConfig.source?.message;
|
||||
|
||||
// if(damageConfig.source?.message && game.modules.get('dice-so-nice')?.active)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export default class EffectsField extends fields.ArrayField {
|
|||
static async execute(config, targets = null, force = false) {
|
||||
if (!config.hasEffect) return;
|
||||
let message = config.message ?? ui.chat.collection.get(config.parent?._id);
|
||||
if (!message) {
|
||||
if (!message && !config.skips.createMessage) {
|
||||
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
|
||||
roll._evaluated = true;
|
||||
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} actor Actor Document
|
||||
*/
|
||||
static async applyEffect(effect, actor) {
|
||||
const existingEffect = actor.effects.find(e => e.origin === effect.uuid);
|
||||
if (existingEffect) {
|
||||
return effect.update(
|
||||
foundry.utils.mergeObject({
|
||||
...effect.constructor.getInitialDuration(),
|
||||
disabled: false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise, create a new effect on the target
|
||||
const effectData = foundry.utils.mergeObject({
|
||||
...(effect.toObject?.() ?? effect),
|
||||
disabled: false,
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export default class SaveField extends fields.SchemaField {
|
|||
if (!config.hasSave) return;
|
||||
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('');
|
||||
roll._evaluated = true;
|
||||
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 { itemAbleRollParse } from '../../helpers/utils.mjs';
|
||||
import MappingField from './mappingField.mjs';
|
||||
|
||||
/**
|
||||
* Specialized collection type for stored actions.
|
||||
|
|
@ -11,9 +10,9 @@ export class ActionCollection extends Collection {
|
|||
constructor(model, entries) {
|
||||
super();
|
||||
this.#model = model;
|
||||
for (const entry of entries) {
|
||||
if (!(entry instanceof game.system.api.models.actions.actionsTypes.base)) continue;
|
||||
this.set(entry._id, entry);
|
||||
for (const [key, value] of entries) {
|
||||
if (!(value instanceof game.system.api.models.actions.actionsTypes.base)) continue;
|
||||
this.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +60,7 @@ export class ActionCollection extends Collection {
|
|||
/**
|
||||
* Field that stores actions.
|
||||
*/
|
||||
export class ActionsField extends MappingField {
|
||||
export class ActionsField extends foundry.data.fields.TypedObjectField {
|
||||
constructor(options) {
|
||||
super(new ActionField(), options);
|
||||
}
|
||||
|
|
@ -70,7 +69,7 @@ export class ActionsField extends MappingField {
|
|||
|
||||
/** @inheritDoc */
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -88,10 +87,11 @@ export class ActionField extends foundry.data.fields.ObjectField {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_cleanType(value, options) {
|
||||
_cleanType(value, options, _state) {
|
||||
if (!(typeof value === 'object')) value = {};
|
||||
value = super._cleanType(value, options, _state);
|
||||
const cls = this.getModel(value);
|
||||
if (cls) return cls.cleanData(value, options);
|
||||
if (cls) return cls.cleanData(value, options, _state);
|
||||
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 {any} fieldData The value of this field within the source data.
|
||||
*/
|
||||
migrateSource(sourceData, fieldData) {
|
||||
const cls = this.getModel(fieldData);
|
||||
if (cls) cls.migrateDataSafe(fieldData);
|
||||
_migrate(sourceData, _fieldData) {
|
||||
const source = sourceData ?? this.options.initial;
|
||||
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);
|
||||
}
|
||||
|
||||
delete() {
|
||||
async delete() {
|
||||
if (!this.inCollection) return this.item;
|
||||
const action = foundry.utils.getProperty(this.item, `system.${this.systemPath}`)?.get(this.id);
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ class ResourcesField extends fields.TypedObjectField {
|
|||
return key in CONFIG.DH.RESOURCE[this.actorType].all;
|
||||
}
|
||||
|
||||
_cleanType(value, options) {
|
||||
value = super._cleanType(value, options);
|
||||
_cleanType(value, options, _state) {
|
||||
value = super._cleanType(value, options, _state);
|
||||
|
||||
// If not partial, ensure all data exists
|
||||
if (!options.partial) {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export default class ForeignDocumentUUIDArrayField extends foundry.data.fields.A
|
|||
|
||||
/** @inheritdoc */
|
||||
initialize(value, model, options = {}) {
|
||||
const v = super.initialize(value, model, options);
|
||||
const v = super.initialize(value ?? [], model, options);
|
||||
return () => {
|
||||
const data = v.map(entry => (typeof entry === 'function' ? entry() : entry));
|
||||
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(),
|
||||
tier: new fields.NumberField({ required: true, integer: true, initial: 1, min: 1 }),
|
||||
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(
|
||||
new fields.SchemaField({
|
||||
value: new fields.StringField({
|
||||
|
|
@ -28,14 +35,7 @@ export default class DHArmor extends AttachableItem {
|
|||
effectIds: 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);
|
||||
changes.system.actions = actionIds.reduce((acc, id) => {
|
||||
acc[`-=${id}`] = null;
|
||||
acc[id] = _del;
|
||||
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.
|
||||
* @returns {string[]} An array of localized tag strings.
|
||||
*/
|
||||
_getTags() {
|
||||
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}`
|
||||
];
|
||||
|
||||
|
|
@ -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.
|
||||
*/
|
||||
_getLabels() {
|
||||
const labels = [];
|
||||
if (this.baseScore)
|
||||
labels.push(`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`);
|
||||
const labels = [`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.armor.max}`];
|
||||
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 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') {
|
||||
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];
|
||||
}
|
||||
|
||||
if (changed.system?.actions) {
|
||||
const triggersToRemove = Object.keys(changed.system.actions).reduce((acc, key) => {
|
||||
if (!changed.system.actions[key]) {
|
||||
const strippedKey = key.replace('-=', '');
|
||||
acc.push(...this.actions.get(strippedKey).triggers.map(x => x.trigger));
|
||||
const action = changed.system.actions[key];
|
||||
if (action && Object.keys(action).length === 0) {
|
||||
acc.push(...this.actions.get(key).triggers.map(x => x.trigger));
|
||||
}
|
||||
|
||||
return acc;
|
||||
|
|
|
|||
|
|
@ -99,17 +99,22 @@ export default class DHBeastform extends BaseDataItem {
|
|||
|
||||
get beastformAttackData() {
|
||||
const effect = this.parent.effects.find(x => x.type === 'beastform');
|
||||
return DHBeastform.getBeastformAttackData(effect);
|
||||
}
|
||||
|
||||
static getBeastformAttackData(effect) {
|
||||
if (!effect) return null;
|
||||
|
||||
const traitBonus = effect.changes.find(x => x.key === `system.traits.${this.mainTrait}.value`)?.value ?? 0;
|
||||
const evasionBonus = effect.changes.find(x => x.key === 'system.evasion')?.value ?? 0;
|
||||
const mainTrait = effect.system.changes.find(x => x.key === 'system.rules.attack.roll.trait')?.value;
|
||||
const traitBonus = effect.system.changes.find(x => x.key === `system.traits.${mainTrait}.value`)?.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 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 {
|
||||
trait: game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.mainTrait].label),
|
||||
trait: game.i18n.localize(CONFIG.DH.ACTOR.abilities[mainTrait]?.label),
|
||||
traitBonus: traitBonus ? Number(traitBonus).signedString() : '',
|
||||
evasionBonus: evasionBonus ? Number(evasionBonus).signedString() : '',
|
||||
damageDice: damageDice,
|
||||
|
|
@ -169,17 +174,17 @@ export default class DHBeastform extends BaseDataItem {
|
|||
|
||||
const beastformEffect = this.parent.effects.find(x => x.type === 'beastform');
|
||||
await beastformEffect.updateSource({
|
||||
changes: [
|
||||
...beastformEffect.changes,
|
||||
{
|
||||
key: 'system.advantageSources',
|
||||
mode: 2,
|
||||
value: Object.values(this.advantageOn)
|
||||
.map(x => x.value)
|
||||
.join(', ')
|
||||
}
|
||||
],
|
||||
system: {
|
||||
changes: [
|
||||
...beastformEffect.system.changes,
|
||||
{
|
||||
key: 'system.advantageSources',
|
||||
mode: 2,
|
||||
value: Object.values(this.advantageOn)
|
||||
.map(x => x.value)
|
||||
.join(', ')
|
||||
}
|
||||
],
|
||||
characterTokenData: {
|
||||
usesDynamicToken: this.parent.parent.prototypeToken.ring.enabled,
|
||||
tokenImg: this.parent.parent.prototypeToken.texture.src,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ export default class DHSubclass extends BaseDataItem {
|
|||
}
|
||||
|
||||
async _preCreate(data, options, user) {
|
||||
const allowed = await super._preCreate(data, options, user);
|
||||
if (allowed === false) return;
|
||||
|
||||
if (this.actor?.type === 'character') {
|
||||
const dataUuid = data.uuid ?? data._stats.compendiumSource ?? `Item.${data._id}`;
|
||||
if (this.actor.system.class.subclass) {
|
||||
|
|
@ -86,9 +89,6 @@ export default class DHSubclass extends BaseDataItem {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const allowed = await super._preCreate(data, options, user);
|
||||
if (allowed === false) return;
|
||||
}
|
||||
|
||||
/**@inheritdoc */
|
||||
|
|
|
|||
|
|
@ -63,15 +63,15 @@ export default class DHWeapon extends AttachableItem {
|
|||
type: 'attack'
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
hitPoints: {
|
||||
type: ['physical'],
|
||||
value: {
|
||||
multiplier: 'prof',
|
||||
dice: 'd8'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
|
@ -148,7 +148,7 @@ export default class DHWeapon extends AttachableItem {
|
|||
|
||||
await this.parent.deleteEmbeddedDocuments('ActiveEffect', removedEffectsUpdate);
|
||||
changes.system.actions = removedActionsUpdate.reduce((acc, id) => {
|
||||
acc[`-=${id}`] = null;
|
||||
acc[id] = _del;
|
||||
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({
|
||||
enabled: new fields.BooleanField({
|
||||
nullable: false,
|
||||
|
|
|
|||
9
module/data/spotlightTracker.mjs
Normal file
9
module/data/spotlightTracker.mjs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export default class SpotlightTracker extends foundry.abstract.DataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
return {
|
||||
spotlightedTokens: new fields.SetField(new fields.DocumentUUIDField())
|
||||
};
|
||||
}
|
||||
}
|
||||
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 })
|
||||
})
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -4,3 +4,4 @@ export { default as DamageRoll } from './damageRoll.mjs';
|
|||
export { default as DHRoll } from './dhRoll.mjs';
|
||||
export { default as DualityRoll } from './dualityRoll.mjs';
|
||||
export { default as FateRoll } from './fateRoll.mjs';
|
||||
export { diceTypes } from './die/_module.mjs';
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export default class D20Roll extends DHRoll {
|
|||
get isCritical() {
|
||||
if (!this.d20._evaluated) return;
|
||||
|
||||
const criticalThreshold = this.options.actionType === 'reaction' ? 20 : this.data.system.criticalThreshold;
|
||||
const criticalThreshold = this.options.actionType === 'reaction' ? 20 : this.data.criticalThreshold;
|
||||
return this.d20.total >= criticalThreshold;
|
||||
}
|
||||
|
||||
|
|
@ -207,7 +207,7 @@ export default class D20Roll extends DHRoll {
|
|||
rerolls: dice.results.filter(x => x.rerolled)
|
||||
}
|
||||
}));
|
||||
data.isCritical = config.isCritical = roll.isCritical;
|
||||
data.isCritical = roll.isCritical;
|
||||
data.extra = roll.dice
|
||||
.filter(d => !roll.baseTerms.includes(d))
|
||||
.map(d => {
|
||||
|
|
@ -217,49 +217,11 @@ export default class D20Roll extends DHRoll {
|
|||
results: d.results
|
||||
};
|
||||
});
|
||||
data.modifierTotal = this.calculateTotalModifiers(roll);
|
||||
data.modifierTotal = roll.modifierTotal;
|
||||
return data;
|
||||
}
|
||||
|
||||
resetFormula() {
|
||||
return (this._formula = this.constructor.getFormula(this.terms));
|
||||
}
|
||||
|
||||
static async reroll(rollString, _target, message) {
|
||||
let parsedRoll = game.system.api.dice.D20Roll.fromData(rollString);
|
||||
parsedRoll = await parsedRoll.reroll();
|
||||
const newRoll = game.system.api.dice.D20Roll.postEvaluate(parsedRoll, {
|
||||
targets: message.system.targets,
|
||||
roll: {
|
||||
advantage: message.system.roll.advantage?.type,
|
||||
difficulty: message.system.roll.difficulty ? Number(message.system.roll.difficulty) : null
|
||||
}
|
||||
});
|
||||
|
||||
if (game.modules.get('dice-so-nice')?.active) {
|
||||
await game.dice3d.showForRoll(parsedRoll, game.user, true);
|
||||
}
|
||||
|
||||
const rerolled = {
|
||||
any: true,
|
||||
rerolls: [
|
||||
...(message.system.roll.dice[0].rerolled?.rerolls?.length > 0
|
||||
? [message.system.roll.dice[0].rerolled?.rerolls]
|
||||
: []),
|
||||
rollString.terms[0].results
|
||||
]
|
||||
};
|
||||
return {
|
||||
newRoll: {
|
||||
...newRoll,
|
||||
dice: [
|
||||
{
|
||||
...newRoll.dice[0],
|
||||
rerolled: rerolled
|
||||
}
|
||||
]
|
||||
},
|
||||
parsedRoll
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import DamageDialog from '../applications/dialogs/damageDialog.mjs';
|
||||
import { parseRallyDice } from '../helpers/utils.mjs';
|
||||
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
|
||||
import DHRoll from './dhRoll.mjs';
|
||||
|
||||
export default class DamageRoll extends DHRoll {
|
||||
|
|
@ -8,6 +7,10 @@ export default class DamageRoll extends DHRoll {
|
|||
super(formula, data, options);
|
||||
}
|
||||
|
||||
get isCritical() {
|
||||
return !!this.options.isCritical;
|
||||
}
|
||||
|
||||
static DefaultDialog = DamageDialog;
|
||||
|
||||
static async buildEvaluate(roll, config = {}, message = {}) {
|
||||
|
|
@ -34,7 +37,7 @@ export default class DamageRoll extends DHRoll {
|
|||
static async buildPost(roll, config, message) {
|
||||
const chatMessage = 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) {
|
||||
const pool = foundry.dice.terms.PoolTerm.fromRolls(
|
||||
Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll))
|
||||
|
|
@ -139,57 +142,53 @@ export default class DamageRoll extends DHRoll {
|
|||
}
|
||||
|
||||
constructFormula(config) {
|
||||
this.options.roll.forEach((part, index) => {
|
||||
this.options.isCritical = config.isCritical;
|
||||
for (const [index, part] of this.options.roll.entries()) {
|
||||
part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data));
|
||||
this.constructFormulaPart(config, part, index);
|
||||
});
|
||||
part.roll.terms = Roll.parse(part.roll.formula, config.data);
|
||||
if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
|
||||
part.modifiers = this.applyBaseBonus(part);
|
||||
this.addModifiers(part);
|
||||
part.modifiers?.forEach(m => {
|
||||
part.roll.terms.push(...this.formatModifier(m.value));
|
||||
});
|
||||
}
|
||||
|
||||
/* To Remove When Reaction System */
|
||||
if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
|
||||
for (const mod in config.modifiers) {
|
||||
const modifier = config.modifiers[mod];
|
||||
if (modifier.beforeCrit === true && (modifier.enabled || modifier.value)) modifier.callback(part);
|
||||
}
|
||||
}
|
||||
|
||||
if (part.extraFormula) {
|
||||
part.roll.terms.push(
|
||||
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
|
||||
...this.constructor.parse(part.extraFormula, this.options.data)
|
||||
);
|
||||
}
|
||||
|
||||
if (config.isCritical && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
|
||||
const total = part.roll.dice.reduce((acc, term) => acc + term._faces * term._number, 0);
|
||||
if (total > 0) {
|
||||
part.roll.terms.push(...this.formatModifier(total));
|
||||
}
|
||||
}
|
||||
|
||||
/* To Remove When Reaction System */
|
||||
if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
|
||||
for (const mod in config.modifiers) {
|
||||
const modifier = config.modifiers[mod];
|
||||
if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(part);
|
||||
}
|
||||
}
|
||||
|
||||
part.roll._formula = this.constructor.getFormula(part.roll.terms);
|
||||
}
|
||||
return this.options.roll;
|
||||
}
|
||||
|
||||
constructFormulaPart(config, part, index) {
|
||||
part.roll.terms = Roll.parse(part.roll.formula, config.data);
|
||||
|
||||
if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
|
||||
part.modifiers = this.applyBaseBonus(part);
|
||||
this.addModifiers(part);
|
||||
part.modifiers?.forEach(m => {
|
||||
part.roll.terms.push(...this.formatModifier(m.value));
|
||||
});
|
||||
}
|
||||
|
||||
/* To Remove When Reaction System */
|
||||
if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
|
||||
for (const mod in config.modifiers) {
|
||||
const modifier = config.modifiers[mod];
|
||||
if (modifier.beforeCrit === true && (modifier.enabled || modifier.value)) modifier.callback(part);
|
||||
}
|
||||
}
|
||||
|
||||
if (part.extraFormula) {
|
||||
part.roll.terms.push(
|
||||
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
|
||||
...this.constructor.parse(part.extraFormula, this.options.data)
|
||||
);
|
||||
}
|
||||
|
||||
if (config.isCritical && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
|
||||
const total = part.roll.dice.reduce((acc, term) => acc + term._faces * term._number, 0);
|
||||
if (total > 0) {
|
||||
part.roll.terms.push(...this.formatModifier(total));
|
||||
}
|
||||
}
|
||||
|
||||
/* To Remove When Reaction System */
|
||||
if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
|
||||
for (const mod in config.modifiers) {
|
||||
const modifier = config.modifiers[mod];
|
||||
if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(part);
|
||||
}
|
||||
}
|
||||
|
||||
return (part.roll._formula = this.constructor.getFormula(part.roll.terms));
|
||||
}
|
||||
|
||||
/* To Remove When Reaction System */
|
||||
static temporaryModifierBuilder(config) {
|
||||
const mods = {};
|
||||
|
|
@ -197,7 +196,7 @@ export default class DamageRoll extends DHRoll {
|
|||
if (config.data.parent.appliedEffects) {
|
||||
// Bardic Rally
|
||||
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) });
|
||||
return a;
|
||||
}, []);
|
||||
|
|
@ -281,10 +280,7 @@ export default class DamageRoll extends DHRoll {
|
|||
return mods;
|
||||
}
|
||||
|
||||
static async reroll(target, message) {
|
||||
const { damageType, part, dice, result } = target.dataset;
|
||||
const rollPart = message.system.damage[damageType].parts[part];
|
||||
|
||||
static async reroll(rollPart, dice, result) {
|
||||
let diceIndex = 0;
|
||||
let parsedRoll = game.system.api.dice.DamageRoll.fromData({
|
||||
...rollPart.roll,
|
||||
|
|
@ -353,29 +349,6 @@ export default class DamageRoll extends DHRoll {
|
|||
};
|
||||
});
|
||||
|
||||
const updateMessage = game.messages.get(message._id);
|
||||
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
|
||||
}
|
||||
});
|
||||
return { parsedRoll, rerolledDice };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ export default class DHRoll extends Roll {
|
|||
return game.i18n.localize('DAGGERHEART.GENERAL.Roll.basic');
|
||||
}
|
||||
|
||||
get modifierTotal() {
|
||||
return this.constructor.calculateTotalModifiers(this);
|
||||
}
|
||||
|
||||
static messageType = 'adversaryRoll';
|
||||
|
||||
static CHAT_TEMPLATE = 'systems/daggerheart/templates/ui/chat/roll.hbs';
|
||||
|
|
@ -21,6 +25,9 @@ export default class DHRoll extends Roll {
|
|||
static async build(config = {}, message = {}) {
|
||||
const roll = await this.buildConfigure(config, message);
|
||||
if (!roll) return;
|
||||
|
||||
if (config.skips?.createMessage) config.messageRoll = roll;
|
||||
|
||||
await this.buildEvaluate(roll, config, (message = {}));
|
||||
await this.buildPost(roll, config, (message = {}));
|
||||
return config;
|
||||
|
|
@ -30,12 +37,6 @@ export default class DHRoll extends Roll {
|
|||
config.hooks = [...this.getHooks(), ''];
|
||||
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) {
|
||||
if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null;
|
||||
}
|
||||
|
|
@ -120,14 +121,10 @@ export default class DHRoll extends Roll {
|
|||
rolls: [roll]
|
||||
};
|
||||
|
||||
config.selectedRollMode ??= game.settings.get('core', 'rollMode');
|
||||
config.selectedMessageMode ??= game.settings.get('core', 'messageMode');
|
||||
|
||||
if (roll._evaluated) {
|
||||
const message = await cls.create(msgData, { rollMode: config.selectedRollMode });
|
||||
|
||||
if (config.tagTeamSelected) {
|
||||
game.system.api.applications.dialogs.TagTeamDialog.assignRoll(message.speakerActor, message);
|
||||
}
|
||||
const message = await cls.create(msgData, { messageMode: config.selectedMessageMode });
|
||||
|
||||
if (roll.formula !== '' && game.modules.get('dice-so-nice')?.active) {
|
||||
await game.dice3d.waitFor3DAnimationByMessageID(message.id);
|
||||
|
|
@ -145,6 +142,7 @@ export default class DHRoll extends Roll {
|
|||
const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options });
|
||||
return foundry.applications.handlebars.renderTemplate(template, {
|
||||
...chatData,
|
||||
roll: this,
|
||||
parent: chatData.parent,
|
||||
targetMode: chatData.targetMode,
|
||||
metagamingSettings
|
||||
|
|
@ -248,16 +246,21 @@ export default class DHRoll extends Roll {
|
|||
return (this._formula = this.constructor.getFormula(this.terms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total modifiers of any rolls, including non-dh rolls.
|
||||
* This exists because damage rolls still may receive base roll classes
|
||||
*/
|
||||
static calculateTotalModifiers(roll) {
|
||||
let modifierTotal = 0;
|
||||
for (let i = 0; i < roll.terms.length; i++) {
|
||||
if (
|
||||
roll.terms[i] instanceof foundry.dice.terms.NumericTerm &&
|
||||
!!roll.terms[i - 1] &&
|
||||
roll.terms[i - 1] instanceof foundry.dice.terms.OperatorTerm
|
||||
)
|
||||
modifierTotal += Number(`${roll.terms[i - 1].operator}${roll.terms[i].total}`);
|
||||
if (!roll.terms[i].isDeterministic) continue;
|
||||
const termTotal = roll.terms[i].total;
|
||||
if (typeof termTotal === 'number') {
|
||||
const multiplier = roll.terms[i - 1]?.operator === " - " ? -1 : 1;
|
||||
modifierTotal += multiplier * termTotal;
|
||||
}
|
||||
}
|
||||
|
||||
return modifierTotal;
|
||||
}
|
||||
|
||||
|
|
@ -269,12 +272,12 @@ export default class DHRoll extends Roll {
|
|||
const changeKeys = this.getActionChangeKeys();
|
||||
return (
|
||||
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] = {
|
||||
id: effect.id,
|
||||
name: effect.name,
|
||||
description: effect.description,
|
||||
changes: effect.changes,
|
||||
changes: effect.system.changes,
|
||||
origEffect: effect,
|
||||
selected: !effect.disabled
|
||||
};
|
||||
|
|
|
|||
9
module/dice/die/_module.mjs
Normal file
9
module/dice/die/_module.mjs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import DualityDie from './dualityDie.mjs';
|
||||
import AdvantageDie from './advantageDie.mjs';
|
||||
import DisadvantageDie from './disadvantageDie.mjs';
|
||||
|
||||
export const diceTypes = {
|
||||
DualityDie,
|
||||
AdvantageDie,
|
||||
DisadvantageDie
|
||||
};
|
||||
7
module/dice/die/advantageDie.mjs
Normal file
7
module/dice/die/advantageDie.mjs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default class AdvantageDie extends foundry.dice.terms.Die {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.modifiers = [];
|
||||
}
|
||||
}
|
||||
7
module/dice/die/disadvantageDie.mjs
Normal file
7
module/dice/die/disadvantageDie.mjs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default class DisadvantageDie extends foundry.dice.terms.Die {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.modifiers = [];
|
||||
}
|
||||
}
|
||||
62
module/dice/die/dualityDie.mjs
Normal file
62
module/dice/die/dualityDie.mjs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { ResourceUpdateMap } from '../../data/action/baseAction.mjs';
|
||||
|
||||
export default class DualityDie extends foundry.dice.terms.Die {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.modifiers = [];
|
||||
}
|
||||
|
||||
#getDualityState(roll) {
|
||||
if (!roll) return null;
|
||||
return roll.withHope ? 1 : roll.withFear ? -1 : 0;
|
||||
}
|
||||
|
||||
#updateResources(oldDuality, newDuality, actor) {
|
||||
const { hopeFear } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
|
||||
if (game.user.isGM ? !hopeFear.gm : !hopeFear.players) return;
|
||||
|
||||
const updates = [];
|
||||
const hope = (newDuality >= 0 ? 1 : 0) - (oldDuality >= 0 ? 1 : 0);
|
||||
const stress = (newDuality === 0 ? 1 : 0) - (oldDuality === 0 ? 1 : 0);
|
||||
const fear = (newDuality === -1 ? 1 : 0) - (oldDuality === -1 ? 1 : 0);
|
||||
|
||||
if (hope !== 0) updates.push({ key: 'hope', value: hope, total: -1 * hope, enabled: true });
|
||||
if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, total: stress, enabled: true });
|
||||
if (fear !== 0) updates.push({ key: 'fear', value: fear, total: -1 * fear, enabled: true });
|
||||
|
||||
const resourceUpdates = new ResourceUpdateMap(actor);
|
||||
resourceUpdates.addResources(updates);
|
||||
resourceUpdates.updateResources();
|
||||
}
|
||||
|
||||
async reroll(modifier, options) {
|
||||
const oldDuality = this.#getDualityState(options.liveRoll.roll);
|
||||
await super.reroll(modifier, options);
|
||||
|
||||
if (options?.liveRoll) {
|
||||
/* Can't currently test since DiceSoNice is not v14. Might need to set the appearance earlier if a roll is triggered by super.reroll */
|
||||
if (game.modules.get('dice-so-nice')?.active) {
|
||||
const diceSoNiceRoll = {
|
||||
_evaluated: true,
|
||||
dice: [this],
|
||||
options: { appearance: {} }
|
||||
};
|
||||
|
||||
const preset = await getDiceSoNicePreset(diceSoNice[key], faces);
|
||||
diceSoNiceRoll.dice[0].options.appearance = preset.appearance;
|
||||
diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile;
|
||||
|
||||
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
|
||||
} else {
|
||||
foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
|
||||
}
|
||||
|
||||
await options.liveRoll.roll._evaluate();
|
||||
if (options.liveRoll.isReaction) return;
|
||||
|
||||
const newDuality = this.#getDualityState(options.liveRoll.roll);
|
||||
this.#updateResources(oldDuality, newDuality, options.liveRoll.actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
|
||||
import D20Roll from './d20Roll.mjs';
|
||||
import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
|
||||
import { getDiceSoNicePresets } from '../config/generalConfig.mjs';
|
||||
import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
|
||||
|
||||
export default class DualityRoll extends D20Roll {
|
||||
_advantageFaces = 6;
|
||||
|
|
@ -26,27 +24,31 @@ export default class DualityRoll extends D20Roll {
|
|||
}
|
||||
|
||||
get dHope() {
|
||||
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
||||
if (!(this.dice[0] instanceof game.system.api.dice.diceTypes.DualityDie)) this.createBaseDice();
|
||||
return this.dice[0];
|
||||
}
|
||||
|
||||
set dHope(faces) {
|
||||
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
||||
this.dice[0].faces = this.getFaces(faces);
|
||||
// TODO this should not be asymmetrical with the getter. updateRollConfiguration() should use dHope.faces
|
||||
this.dHope.faces = this.getFaces(faces);
|
||||
}
|
||||
|
||||
get dFear() {
|
||||
if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
||||
if (!(this.dice[1] instanceof game.system.api.dice.diceTypes.DualityDie)) this.createBaseDice();
|
||||
return this.dice[1];
|
||||
}
|
||||
|
||||
set dFear(faces) {
|
||||
if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
||||
this.dice[1].faces = this.getFaces(faces);
|
||||
// TODO this should not be asymmetrical with the getter. updateRollConfiguration() should use dFear.faces
|
||||
this.dFear.faces = this.getFaces(faces);
|
||||
}
|
||||
|
||||
get dAdvantage() {
|
||||
return this.dice[2];
|
||||
return this.dice[2] instanceof game.system.api.dice.diceTypes.AdvantageDie ? this.dice[2] : null;
|
||||
}
|
||||
|
||||
get dDisadvantage() {
|
||||
return this.dice[2] instanceof game.system.api.dice.diceTypes.DisadvantageDie ? this.dice[2] : null;
|
||||
}
|
||||
|
||||
get advantageFaces() {
|
||||
|
|
@ -65,9 +67,14 @@ export default class DualityRoll extends D20Roll {
|
|||
this._advantageNumber = Number(value);
|
||||
}
|
||||
|
||||
get extraDice() {
|
||||
const { DualityDie, AdvantageDie, DisadvantageDie } = game.system.api.dice.diceTypes;
|
||||
return this.dice.filter(x => ![DualityDie, AdvantageDie, DisadvantageDie].some(die => x instanceof die));
|
||||
}
|
||||
|
||||
setRallyChoices() {
|
||||
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) });
|
||||
return a;
|
||||
}, []);
|
||||
|
|
@ -118,22 +125,28 @@ export default class DualityRoll extends D20Roll {
|
|||
|
||||
/** @inheritDoc */
|
||||
static fromData(data) {
|
||||
data.terms[0].class = foundry.dice.terms.Die.name;
|
||||
data.terms[2].class = foundry.dice.terms.Die.name;
|
||||
data.terms[0].class = 'DualityDie';
|
||||
data.terms[2].class = 'DualityDie';
|
||||
if (data.options.roll.advantage?.type && data.terms[4]?.faces) {
|
||||
data.terms[4].class = data.options.roll.advantage.type === 1 ? 'AdvantageDie' : 'DisadvantageDie';
|
||||
}
|
||||
return super.fromData(data);
|
||||
}
|
||||
|
||||
createBaseDice() {
|
||||
if (this.dice[0] instanceof foundry.dice.terms.Die && this.dice[1] instanceof foundry.dice.terms.Die) {
|
||||
if (
|
||||
this.dice[0] instanceof game.system.api.dice.diceTypes.DualityDie &&
|
||||
this.dice[1] instanceof game.system.api.dice.diceTypes.DualityDie
|
||||
) {
|
||||
this.terms = [this.terms[0], this.terms[1], this.terms[2]];
|
||||
return;
|
||||
}
|
||||
|
||||
this.terms[0] = new foundry.dice.terms.Die({
|
||||
this.terms[0] = new game.system.api.dice.diceTypes.DualityDie({
|
||||
faces: this.data.rules.dualityRoll?.defaultHopeDice ?? 12
|
||||
});
|
||||
this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' });
|
||||
this.terms[2] = new foundry.dice.terms.Die({
|
||||
this.terms[2] = new game.system.api.dice.diceTypes.DualityDie({
|
||||
faces: this.data.rules.dualityRoll?.defaultFearDice ?? 12
|
||||
});
|
||||
}
|
||||
|
|
@ -179,7 +192,7 @@ export default class DualityRoll extends D20Roll {
|
|||
static async buildConfigure(config = {}, message = {}) {
|
||||
config.dialog ??= {};
|
||||
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;
|
||||
return a;
|
||||
}, false);
|
||||
|
|
@ -305,7 +318,6 @@ export default class DualityRoll extends D20Roll {
|
|||
!config.source?.actor ||
|
||||
(game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) ||
|
||||
config.actionType === 'reaction' ||
|
||||
config.tagTeamSelected ||
|
||||
config.skips?.resources
|
||||
)
|
||||
return;
|
||||
|
|
@ -346,7 +358,6 @@ export default class DualityRoll extends D20Roll {
|
|||
if (
|
||||
automationSettings.countdownAutomation &&
|
||||
config.actionType !== 'reaction' &&
|
||||
!config.tagTeamSelected &&
|
||||
!config.skips?.updateCountdowns
|
||||
) {
|
||||
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
|
||||
|
|
@ -373,61 +384,4 @@ export default class DualityRoll extends D20Roll {
|
|||
if (currentCombatant?.actorId == config.data.id) ui.combat.setCombatantSpotlight(currentCombatant.id);
|
||||
}
|
||||
}
|
||||
|
||||
static async reroll(rollString, target, message) {
|
||||
let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollString, evaluated: false });
|
||||
const term = parsedRoll.terms[target.dataset.dieIndex];
|
||||
await term.reroll(`/r1=${term.total}`);
|
||||
const result = await parsedRoll.evaluate();
|
||||
|
||||
if (game.modules.get('dice-so-nice')?.active) {
|
||||
const diceSoNiceRoll = {
|
||||
_evaluated: true,
|
||||
dice: [
|
||||
new foundry.dice.terms.Die({
|
||||
...term,
|
||||
faces: term._faces,
|
||||
results: term.results.filter(x => !x.rerolled)
|
||||
})
|
||||
],
|
||||
options: { appearance: {} }
|
||||
};
|
||||
|
||||
const diceSoNicePresets = await getDiceSoNicePresets(result, `d${term._faces}`, `d${term._faces}`);
|
||||
const type = target.dataset.type;
|
||||
if (diceSoNicePresets[type]) {
|
||||
diceSoNiceRoll.dice[0].options = diceSoNicePresets[type];
|
||||
}
|
||||
|
||||
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
|
||||
}
|
||||
|
||||
const newRoll = game.system.api.dice.DualityRoll.postEvaluate(parsedRoll, {
|
||||
targets: message.system.targets,
|
||||
roll: {
|
||||
advantage: message.system.roll.advantage?.type,
|
||||
difficulty: message.system.roll.difficulty ? Number(message.system.roll.difficulty) : null
|
||||
}
|
||||
});
|
||||
|
||||
const extraIndex = newRoll.advantage ? 3 : 2;
|
||||
newRoll.extra = newRoll.extra.slice(extraIndex);
|
||||
|
||||
const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
|
||||
|
||||
const actor = message.system.source.actor ? await foundry.utils.fromUuid(message.system.source.actor) : null;
|
||||
const config = {
|
||||
source: { actor: message.system.source.actor ?? '' },
|
||||
targets: message.system.targets,
|
||||
tagTeamSelected: Object.values(tagTeamSettings.members).some(x => x.messageId === message._id),
|
||||
roll: newRoll,
|
||||
rerolledRoll: message.system.roll,
|
||||
resourceUpdates: new ResourceUpdateMap(actor)
|
||||
};
|
||||
|
||||
await DualityRoll.addDualityResourceUpdates(config);
|
||||
await config.resourceUpdates.updateResources();
|
||||
|
||||
return { newRoll, parsedRoll };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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