mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-03-08 06:56:12 +01:00
Compare commits
101 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1f5102af1 | ||
|
|
a4f8c67707 | ||
|
|
83c3da0130 | ||
|
|
92d8c2ca18 | ||
|
|
17aa0680d2 | ||
|
|
5732639391 | ||
|
|
9bfe3505bf | ||
|
|
9cba77ec11 | ||
|
|
0675e1f019 | ||
|
|
1212bd01f8 | ||
|
|
3267f3f531 | ||
|
|
986544a653 | ||
|
|
5459581f7f | ||
|
|
0d0b5125ba | ||
|
|
c48842dd2d | ||
|
|
e79ccd34e9 | ||
|
|
4324c3abf2 | ||
|
|
1b09b44d6c | ||
|
|
340abbc98c | ||
|
|
56cc16b39a | ||
|
|
267de9a8cf | ||
|
|
9296b8fcc2 | ||
|
|
ca434d33f1 | ||
|
|
b64a9002ea | ||
|
|
472f876ea3 | ||
|
|
7022630316 | ||
|
|
e0b3d33f80 | ||
|
|
60cd28ae82 | ||
|
|
12bcd6e34e | ||
|
|
6cbe770880 | ||
|
|
95d4003045 | ||
|
|
fa19339868 | ||
|
|
17ec77a349 | ||
|
|
a65514b1c1 | ||
|
|
b23b6c75fb | ||
|
|
7c86417752 | ||
|
|
c7431d16a7 | ||
|
|
5413730108 | ||
|
|
d96e72505a | ||
|
|
f9f252c7a6 | ||
|
|
78012be6e4 | ||
|
|
4ad8b960b5 | ||
|
|
f7e4c5346e | ||
|
|
44131d21a6 | ||
|
|
202e624a06 | ||
|
|
5e7201bfe9 | ||
|
|
cad3f533ad | ||
|
|
c3653e1b30 | ||
|
|
c1f7866594 | ||
|
|
0d2495c143 | ||
|
|
cab185df66 | ||
|
|
735ed4c214 | ||
|
|
c8d1ea1460 | ||
|
|
c1924534da | ||
|
|
31c70469ef | ||
|
|
491d921a9b | ||
|
|
668dbdf8f4 | ||
|
|
ab538df3aa | ||
|
|
483caa1062 | ||
|
|
22d446f360 | ||
|
|
94efbeada3 | ||
|
|
1bc9e07098 | ||
|
|
b374070809 | ||
|
|
7bf0f0fbee | ||
|
|
3f4d1cd292 | ||
|
|
a18393a9d0 | ||
|
|
076b7f01fa | ||
|
|
bb43cb57dc | ||
|
|
0baed9234b | ||
|
|
6321c7c508 | ||
|
|
bae9470a4f | ||
|
|
f1ebb7d1e1 | ||
|
|
ce96ffa0a3 | ||
|
|
c42f876d4f | ||
|
|
a78ef1f70c | ||
|
|
fdb6412c8c | ||
|
|
2757a97244 | ||
|
|
d43a4994ad | ||
|
|
37ae40be8b | ||
|
|
cb998860d9 | ||
|
|
bdb8997324 | ||
|
|
0d84516813 | ||
|
|
cbd268ea1f | ||
|
|
edc3017b39 | ||
|
|
21ef288283 | ||
|
|
fa3b3fa0a8 | ||
|
|
68b35970f2 | ||
|
|
1f7d4d6f1e | ||
|
|
c90875fa7c | ||
|
|
38fb00bd10 | ||
|
|
2aba7cf921 | ||
|
|
f659d08d58 | ||
|
|
6e5d7fb34c | ||
|
|
3725fc29ef | ||
|
|
cc998bffa7 | ||
|
|
77bac647a8 | ||
|
|
9d75157e17 | ||
|
|
3103a40c26 | ||
|
|
4ce8fbb84c | ||
|
|
3b27803e24 | ||
|
|
98cf6fa6de |
259 changed files with 6091 additions and 1348 deletions
3
.editorconfig
Normal file
3
.editorconfig
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[*]
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = spaces
|
||||||
108
daggerheart.mjs
108
daggerheart.mjs
|
|
@ -3,13 +3,15 @@ import * as applications from './module/applications/_module.mjs';
|
||||||
import * as data from './module/data/_module.mjs';
|
import * as data from './module/data/_module.mjs';
|
||||||
import * as models from './module/data/_module.mjs';
|
import * as models from './module/data/_module.mjs';
|
||||||
import * as documents from './module/documents/_module.mjs';
|
import * as documents from './module/documents/_module.mjs';
|
||||||
|
import * as collections from './module/documents/collections/_module.mjs';
|
||||||
import * as dice from './module/dice/_module.mjs';
|
import * as dice from './module/dice/_module.mjs';
|
||||||
import * as fields from './module/data/fields/_module.mjs';
|
import * as fields from './module/data/fields/_module.mjs';
|
||||||
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
|
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
|
||||||
import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs';
|
import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs';
|
||||||
import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs';
|
import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs';
|
||||||
import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll } from './module/dice/_module.mjs';
|
import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll } from './module/dice/_module.mjs';
|
||||||
import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs';
|
import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs';
|
||||||
|
import { enrichedFateRoll, getFateTypeData } from './module/enrichers/FateRollEnricher.mjs';
|
||||||
import {
|
import {
|
||||||
handlebarsRegistration,
|
handlebarsRegistration,
|
||||||
runMigrations,
|
runMigrations,
|
||||||
|
|
@ -24,16 +26,18 @@ import TokenManager from './module/documents/tokenManager.mjs';
|
||||||
CONFIG.DH = SYSTEM;
|
CONFIG.DH = SYSTEM;
|
||||||
CONFIG.TextEditor.enrichers.push(...enricherConfig);
|
CONFIG.TextEditor.enrichers.push(...enricherConfig);
|
||||||
|
|
||||||
CONFIG.Dice.rolls = [BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll];
|
CONFIG.Dice.rolls = [BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll];
|
||||||
CONFIG.Dice.daggerheart = {
|
CONFIG.Dice.daggerheart = {
|
||||||
DHRoll: DHRoll,
|
DHRoll: DHRoll,
|
||||||
DualityRoll: DualityRoll,
|
DualityRoll: DualityRoll,
|
||||||
D20Roll: D20Roll,
|
D20Roll: D20Roll,
|
||||||
DamageRoll: DamageRoll
|
DamageRoll: DamageRoll,
|
||||||
|
FateRoll: FateRoll
|
||||||
};
|
};
|
||||||
|
|
||||||
CONFIG.Actor.documentClass = documents.DhpActor;
|
CONFIG.Actor.documentClass = documents.DhpActor;
|
||||||
CONFIG.Actor.dataModels = models.actors.config;
|
CONFIG.Actor.dataModels = models.actors.config;
|
||||||
|
CONFIG.Actor.collection = collections.DhActorCollection;
|
||||||
|
|
||||||
CONFIG.Item.documentClass = documents.DHItem;
|
CONFIG.Item.documentClass = documents.DHItem;
|
||||||
CONFIG.Item.dataModels = models.items.config;
|
CONFIG.Item.dataModels = models.items.config;
|
||||||
|
|
@ -56,6 +60,9 @@ CONFIG.Canvas.layers.tokens.layerClass = DhTokenLayer;
|
||||||
|
|
||||||
CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate;
|
CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate;
|
||||||
|
|
||||||
|
CONFIG.RollTable.documentClass = documents.DhRollTable;
|
||||||
|
CONFIG.RollTable.resultTemplate = 'systems/daggerheart/templates/ui/chat/table-result.hbs';
|
||||||
|
|
||||||
CONFIG.Scene.documentClass = documents.DhScene;
|
CONFIG.Scene.documentClass = documents.DhScene;
|
||||||
|
|
||||||
CONFIG.Token.documentClass = documents.DhToken;
|
CONFIG.Token.documentClass = documents.DhToken;
|
||||||
|
|
@ -103,7 +110,7 @@ Hooks.once('init', () => {
|
||||||
type: game.i18n.localize(typePath)
|
type: game.i18n.localize(typePath)
|
||||||
});
|
});
|
||||||
|
|
||||||
const { Items, Actors } = foundry.documents.collections;
|
const { Items, Actors, RollTables } = foundry.documents.collections;
|
||||||
Items.unregisterSheet('core', foundry.applications.sheets.ItemSheetV2);
|
Items.unregisterSheet('core', foundry.applications.sheets.ItemSheetV2);
|
||||||
Items.registerSheet(SYSTEM.id, applications.sheets.items.Ancestry, {
|
Items.registerSheet(SYSTEM.id, applications.sheets.items.Ancestry, {
|
||||||
types: ['ancestry'],
|
types: ['ancestry'],
|
||||||
|
|
@ -188,6 +195,12 @@ Hooks.once('init', () => {
|
||||||
label: sheetLabel('TYPES.Actor.party')
|
label: sheetLabel('TYPES.Actor.party')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
RollTables.unregisterSheet('core', foundry.applications.sheets.RollTableSheet);
|
||||||
|
RollTables.registerSheet(SYSTEM.id, applications.sheets.rollTables.RollTableSheet, {
|
||||||
|
types: ['base'],
|
||||||
|
makeDefault: true
|
||||||
|
});
|
||||||
|
|
||||||
DocumentSheetConfig.unregisterSheet(
|
DocumentSheetConfig.unregisterSheet(
|
||||||
CONFIG.ActiveEffect.documentClass,
|
CONFIG.ActiveEffect.documentClass,
|
||||||
'core',
|
'core',
|
||||||
|
|
@ -229,6 +242,41 @@ Hooks.on('setup', () => {
|
||||||
systemEffect: true
|
systemEffect: true
|
||||||
}))
|
}))
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const damageThresholds = ['damageThresholds.major', 'damageThresholds.severe'];
|
||||||
|
const traits = Object.keys(game.system.api.data.actors.DhCharacter.schema.fields.traits.fields).map(
|
||||||
|
trait => `traits.${trait}.value`
|
||||||
|
);
|
||||||
|
const resistance = Object.values(game.system.api.data.actors.DhCharacter.schema.fields.resistance.fields).flatMap(
|
||||||
|
type => Object.keys(type.fields).map(x => `resistance.${type.name}.${x}`)
|
||||||
|
);
|
||||||
|
const actorCommon = {
|
||||||
|
bar: ['resources.stress'],
|
||||||
|
value: [...resistance, 'advantageSources', 'disadvantageSources']
|
||||||
|
};
|
||||||
|
CONFIG.Actor.trackableAttributes = {
|
||||||
|
character: {
|
||||||
|
bar: [...actorCommon.bar, 'resources.hitPoints', 'resources.hope'],
|
||||||
|
value: [
|
||||||
|
...actorCommon.value,
|
||||||
|
...traits,
|
||||||
|
...damageThresholds,
|
||||||
|
'proficiency',
|
||||||
|
'evasion',
|
||||||
|
'armorScore',
|
||||||
|
'scars',
|
||||||
|
'levelData.level.current'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
adversary: {
|
||||||
|
bar: [...actorCommon.bar, 'resources.hitPoints'],
|
||||||
|
value: [...actorCommon.value, ...damageThresholds, 'criticalThreshold', 'difficulty']
|
||||||
|
},
|
||||||
|
companion: {
|
||||||
|
bar: [...actorCommon.bar],
|
||||||
|
value: [...actorCommon.value, 'evasion', 'levelData.level.current']
|
||||||
|
}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
Hooks.on('ready', async () => {
|
Hooks.on('ready', async () => {
|
||||||
|
|
@ -296,13 +344,15 @@ Hooks.on('chatMessage', (_, message) => {
|
||||||
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
|
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
|
||||||
: undefined;
|
: undefined;
|
||||||
const difficulty = rollCommand.difficulty;
|
const difficulty = rollCommand.difficulty;
|
||||||
|
const grantResources = rollCommand.grantResources;
|
||||||
|
|
||||||
const target = getCommandTarget({ allowNull: true });
|
const target = getCommandTarget({ allowNull: true });
|
||||||
const title = traitValue
|
const title =
|
||||||
? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
(flavor ?? traitValue)
|
||||||
ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label)
|
? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
||||||
})
|
ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label)
|
||||||
: game.i18n.localize('DAGGERHEART.GENERAL.duality');
|
})
|
||||||
|
: game.i18n.localize('DAGGERHEART.GENERAL.duality');
|
||||||
|
|
||||||
enrichedDualityRoll({
|
enrichedDualityRoll({
|
||||||
reaction,
|
reaction,
|
||||||
|
|
@ -312,7 +362,36 @@ Hooks.on('chatMessage', (_, message) => {
|
||||||
title,
|
title,
|
||||||
label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'),
|
label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'),
|
||||||
actionType: null,
|
actionType: null,
|
||||||
advantage
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -341,10 +420,7 @@ const updateActorsRangeDependentEffects = async token => {
|
||||||
// Get required distance and special case 5 feet to test adjacency
|
// Get required distance and special case 5 feet to test adjacency
|
||||||
const required = rangeMeasurement[range];
|
const required = rangeMeasurement[range];
|
||||||
const reverse = type === CONFIG.DH.GENERAL.rangeInclusion.outsideRange.id;
|
const reverse = type === CONFIG.DH.GENERAL.rangeInclusion.outsideRange.id;
|
||||||
const inRange =
|
const inRange = userTarget.distanceTo(token.object) <= required;
|
||||||
required === 5
|
|
||||||
? userTarget.isAdjacentWith(token.object)
|
|
||||||
: userTarget.distanceTo(token.object) <= required;
|
|
||||||
if (reverse ? inRange : !inRange) {
|
if (reverse ? inRange : !inRange) {
|
||||||
enabledEffect = false;
|
enabledEffect = false;
|
||||||
break;
|
break;
|
||||||
|
|
@ -381,8 +457,8 @@ Hooks.on('targetToken', () => {
|
||||||
debouncedRangeEffectCall();
|
debouncedRangeEffectCall();
|
||||||
});
|
});
|
||||||
|
|
||||||
Hooks.on('refreshToken', (_, options) => {
|
Hooks.on('refreshToken', (token, options) => {
|
||||||
if (options.refreshPosition) {
|
if (options.refreshPosition && !token._original) {
|
||||||
debouncedRangeEffectCall();
|
debouncedRangeEffectCall();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
205
lang/en.json
205
lang/en.json
|
|
@ -192,6 +192,9 @@
|
||||||
},
|
},
|
||||||
"age": "Age",
|
"age": "Age",
|
||||||
"backgroundQuestions": "Backgrounds",
|
"backgroundQuestions": "Backgrounds",
|
||||||
|
"burden": {
|
||||||
|
"ignore": { "label": "Burden: Ignore", "hint": "Ignore burden rules" }
|
||||||
|
},
|
||||||
"companionFeatures": "Companion Features",
|
"companionFeatures": "Companion Features",
|
||||||
"connections": "Connections",
|
"connections": "Connections",
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
|
|
@ -214,6 +217,12 @@
|
||||||
"maxEvasionBonus": "Max Evasion Increase",
|
"maxEvasionBonus": "Max Evasion Increase",
|
||||||
"maxHPBonus": "Max HP Increase",
|
"maxHPBonus": "Max HP Increase",
|
||||||
"pronouns": "Pronouns",
|
"pronouns": "Pronouns",
|
||||||
|
"roll": {
|
||||||
|
"guaranteedCritical": {
|
||||||
|
"label": "Guaranteed Critical",
|
||||||
|
"hint": "Set to 1 to always roll a critical"
|
||||||
|
}
|
||||||
|
},
|
||||||
"story": {
|
"story": {
|
||||||
"backgroundTitle": "Background",
|
"backgroundTitle": "Background",
|
||||||
"characteristics": "Characteristics",
|
"characteristics": "Characteristics",
|
||||||
|
|
@ -237,10 +246,13 @@
|
||||||
"confirmText": "Would you like to level up your companion {name} by {levelChange} levels at this time? (You can do it manually later)"
|
"confirmText": "Would you like to level up your companion {name} by {levelChange} levels at this time? (You can do it manually later)"
|
||||||
},
|
},
|
||||||
"viewLevelups": "View Levelups",
|
"viewLevelups": "View Levelups",
|
||||||
|
"resetCharacter": "Reset Character",
|
||||||
"viewParty": "View Party",
|
"viewParty": "View Party",
|
||||||
"InvalidOldCharacterImportTitle": "Old Character Import",
|
"InvalidOldCharacterImportTitle": "Old Character Import",
|
||||||
"InvalidOldCharacterImportText": "Character data exported prior to system version 1.1 will not generate a complete character. Do you wish to continue?",
|
"InvalidOldCharacterImportText": "Character data exported prior to system version 1.1 will not generate a complete character. Do you wish to continue?",
|
||||||
"cancelBeastform": "Cancel Beastform"
|
"cancelBeastform": "Cancel Beastform",
|
||||||
|
"resetCharacterConfirmationTitle": "Reset Character",
|
||||||
|
"resetCharacterConfirmationContent": "You are reseting all character data except name and portrait. Are you sure?"
|
||||||
},
|
},
|
||||||
"Companion": {
|
"Companion": {
|
||||||
"FIELDS": {
|
"FIELDS": {
|
||||||
|
|
@ -314,6 +326,8 @@
|
||||||
"selectPrimaryWeapon": "Select Primary Weapon",
|
"selectPrimaryWeapon": "Select Primary Weapon",
|
||||||
"selectSecondaryWeapon": "Select Secondary Weapon",
|
"selectSecondaryWeapon": "Select Secondary Weapon",
|
||||||
"selectSubclass": "Select Subclass",
|
"selectSubclass": "Select Subclass",
|
||||||
|
"setupSkipTitle": "Skipping Character Setup",
|
||||||
|
"setupSkipContent": "You are skipping the Character Setup by adding this manually. The character setup is the blinking arrows in the top-right. Are you sure you want to continue?",
|
||||||
"startingItems": "Starting Items",
|
"startingItems": "Starting Items",
|
||||||
"story": "Story",
|
"story": "Story",
|
||||||
"storyExplanation": "Select which background and connection prompts you want to copy into your character's background.",
|
"storyExplanation": "Select which background and connection prompts you want to copy into your character's background.",
|
||||||
|
|
@ -325,6 +339,12 @@
|
||||||
"title": "{actor} - Character Setup",
|
"title": "{actor} - Character Setup",
|
||||||
"traitIncreases": "Trait Increases"
|
"traitIncreases": "Trait Increases"
|
||||||
},
|
},
|
||||||
|
"CharacterReset": {
|
||||||
|
"title": "Reset Character",
|
||||||
|
"alwaysDeleteSection": "Deleted Data",
|
||||||
|
"optionalDeleteSection": "Optional Data",
|
||||||
|
"headerTitle": "Select which data you'd like to keep"
|
||||||
|
},
|
||||||
"CombatTracker": {
|
"CombatTracker": {
|
||||||
"combatStarted": "Active",
|
"combatStarted": "Active",
|
||||||
"giveSpotlight": "Give The Spotlight",
|
"giveSpotlight": "Give The Spotlight",
|
||||||
|
|
@ -332,6 +352,12 @@
|
||||||
"requestSpotlight": "Request The Spotlight",
|
"requestSpotlight": "Request The Spotlight",
|
||||||
"openCountdowns": "Countdowns"
|
"openCountdowns": "Countdowns"
|
||||||
},
|
},
|
||||||
|
"CompendiumBrowserSettings": {
|
||||||
|
"title": "Enable Compendiums",
|
||||||
|
"enableSource": "Enable Source",
|
||||||
|
"disableSource": "Disable Source",
|
||||||
|
"worldCompendiums": "World Compendiums"
|
||||||
|
},
|
||||||
"ContextMenu": {
|
"ContextMenu": {
|
||||||
"disableEffect": "Disable Effect",
|
"disableEffect": "Disable Effect",
|
||||||
"enableEffect": "Enable Effect",
|
"enableEffect": "Enable Effect",
|
||||||
|
|
@ -432,9 +458,13 @@
|
||||||
"name": "Clear Stress"
|
"name": "Clear Stress"
|
||||||
},
|
},
|
||||||
"prepare": {
|
"prepare": {
|
||||||
"description": "Describe how you are preparing for the next day's adventure, then gain a Hope. If you choose to Prepare with one or more members of your party, you may each take two Hope.",
|
"description": "Describe how you are preparing for the next day's adventure, then gain a Hope.",
|
||||||
"name": "Prepare"
|
"name": "Prepare"
|
||||||
},
|
},
|
||||||
|
"prepareWithFriends": {
|
||||||
|
"description": "You prepare with one or more members of your party, and you each gain 2 Hope.",
|
||||||
|
"name": "Prepare (together)"
|
||||||
|
},
|
||||||
"repairArmor": {
|
"repairArmor": {
|
||||||
"description": "Describe how you spend time repairing your armor and clear all of its Armor Slots. You may also do this to an ally's armor instead.",
|
"description": "Describe how you spend time repairing your armor and clear all of its Armor Slots. You may also do this to an ally's armor instead.",
|
||||||
"name": "Repair Armor"
|
"name": "Repair Armor"
|
||||||
|
|
@ -465,7 +495,11 @@
|
||||||
},
|
},
|
||||||
"prepare": {
|
"prepare": {
|
||||||
"name": "Prepare",
|
"name": "Prepare",
|
||||||
"description": "Describe how you prepare yourself for the path ahead, then gain a Hope. If you choose to Prepare with one or more members of your party, you each gain 2 Hope."
|
"description": "Describe how you prepare yourself for the path ahead, then gain a Hope."
|
||||||
|
},
|
||||||
|
"prepareWithFriends": {
|
||||||
|
"name": "Prepare (together)",
|
||||||
|
"description": "You prepare with one or more members of your party, and you each gain 2 Hope."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"refreshable": {
|
"refreshable": {
|
||||||
|
|
@ -477,7 +511,9 @@
|
||||||
"tokenHUD": {
|
"tokenHUD": {
|
||||||
"genericEffects": "Foundry Effects",
|
"genericEffects": "Foundry Effects",
|
||||||
"depositPartyTokens": "Deposit Party Tokens",
|
"depositPartyTokens": "Deposit Party Tokens",
|
||||||
"retrievePartyTokens": "Retrieve Party Tokens"
|
"retrievePartyTokens": "Retrieve Party Tokens",
|
||||||
|
"depositCompanionTokens": "Deposit Companion Token",
|
||||||
|
"retrieveCompanionTokens": "Retrieve Companion Token"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ImageSelect": {
|
"ImageSelect": {
|
||||||
|
|
@ -607,6 +643,7 @@
|
||||||
},
|
},
|
||||||
"RerollDialog": {
|
"RerollDialog": {
|
||||||
"title": "Reroll",
|
"title": "Reroll",
|
||||||
|
"damageTitle": "Reroll Damage",
|
||||||
"deselectDiceNotification": "Deselect one of the selected dice first",
|
"deselectDiceNotification": "Deselect one of the selected dice first",
|
||||||
"acceptCurrentRolls": "Accept Current Rolls"
|
"acceptCurrentRolls": "Accept Current Rolls"
|
||||||
},
|
},
|
||||||
|
|
@ -614,6 +651,13 @@
|
||||||
"title": "{name} Resource",
|
"title": "{name} Resource",
|
||||||
"rerollDice": "Reroll Dice"
|
"rerollDice": "Reroll Dice"
|
||||||
},
|
},
|
||||||
|
"RiskItAllDialog": {
|
||||||
|
"title": "{name} - Risk It All",
|
||||||
|
"subtitle": "Clear Stress and Hit Points",
|
||||||
|
"remainingTitle": "Remaining Points",
|
||||||
|
"clearResource": "Clear {resource}",
|
||||||
|
"finalTitle": "Final Character Resources"
|
||||||
|
},
|
||||||
"TagTeamSelect": {
|
"TagTeamSelect": {
|
||||||
"title": "Tag Team Roll",
|
"title": "Tag Team Roll",
|
||||||
"leaderTitle": "Initiating Character",
|
"leaderTitle": "Initiating Character",
|
||||||
|
|
@ -961,6 +1005,10 @@
|
||||||
"outsideRange": "Outside Range"
|
"outsideRange": "Outside Range"
|
||||||
},
|
},
|
||||||
"Condition": {
|
"Condition": {
|
||||||
|
"deathMove": {
|
||||||
|
"name": "Death Move",
|
||||||
|
"description": "The character is about to make a Death Move"
|
||||||
|
},
|
||||||
"dead": {
|
"dead": {
|
||||||
"name": "Dead",
|
"name": "Dead",
|
||||||
"description": "The character is dead"
|
"description": "The character is dead"
|
||||||
|
|
@ -983,7 +1031,8 @@
|
||||||
},
|
},
|
||||||
"vulnerable": {
|
"vulnerable": {
|
||||||
"name": "Vulnerable",
|
"name": "Vulnerable",
|
||||||
"description": "While a creature is Vulnerable, all rolls targeting them have advantage.\nA creature who is already Vulnerable can’t be made to take the condition again."
|
"description": "While a creature is Vulnerable, all rolls targeting them have advantage.\nA creature who is already Vulnerable can’t be made to take the condition again.",
|
||||||
|
"autoAppliedByLabel": "Max Stress"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CountdownType": {
|
"CountdownType": {
|
||||||
|
|
@ -1012,15 +1061,15 @@
|
||||||
"DeathMoves": {
|
"DeathMoves": {
|
||||||
"avoidDeath": {
|
"avoidDeath": {
|
||||||
"name": "Avoid Death",
|
"name": "Avoid Death",
|
||||||
"description": "You drop unconscious temporarily and work with the GM to describe how the situation gets much worse because of it. Then roll your Fear die; if its value is equal to or under your Level, take a Scar."
|
"description": "Your character avoids death and faces the consequences. They temporarily drop unconscious, and then you work with the GM to describe how the situation worsens. While unconscious, your character can't move or act, and they can't be targeted by an attack. They return to consciousness when an ally clears 1 or more of their marked Hit Points or when the party finishes a long rest. After your character falls unconscious, roll your Hope Die. If its value is equal to or less than your character's level, they gain a scar: permanently cross out a Hope slot and work with the GM to determine its lasting narrative impact and how, if possible, it can be restored. If you ever cross out your last Hope slot, your character's journey ends."
|
||||||
},
|
},
|
||||||
"riskItAll": {
|
"riskItAll": {
|
||||||
"name": "Risk It All",
|
"name": "Risk It All",
|
||||||
"description": "Roll your Duality Dice. If Hope is higher, you stay on your feet and clear an amount of Hit Points and/or Stress equal to the value of the Hope die (divide the Hope die value up between these however you’d prefer). If your Fear die is higher, you cross through the veil of death. If the Duality Dice are tied, you stay on your feet and clear all Hit Points and Stress."
|
"description": "Roll your Duality Dice. If the Hope Die is higher, your character stays on their feet and clears a number of Hit Points or Stress equal to the value of the Hope Die (you can divide the Hope Die value between Hit Points and Stress however you'd prefer). If the Fear Die is higher, your character crosses through the veil of death. If the Duality Dice show matching results, your character stays up and clears all Hit Points and Stress."
|
||||||
},
|
},
|
||||||
"blazeOfGlory": {
|
"blazeOfGlory": {
|
||||||
"name": "Blaze Of Glory",
|
"name": "Blaze Of Glory",
|
||||||
"description": "With Blaze of Glory, the player is accepting death for the character. Take one action (at GM discretion), which becomes an automatic critical success, then cross through the veil of death."
|
"description": "Your character embraces death and goes out in a blaze of glory. Take one final action. It automatically critically succeeds (with GM approval), and then you cross through the veil of death. NOTE: A Blaze of Glory effect has been added to your character. Any Duality Roll will automatically be a critical."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DomainCardTypes": {
|
"DomainCardTypes": {
|
||||||
|
|
@ -1118,12 +1167,12 @@
|
||||||
},
|
},
|
||||||
"far": {
|
"far": {
|
||||||
"name": "Far",
|
"name": "Far",
|
||||||
"description": "means a distance where one can see the appearance of a person or object, but probably not in great detail-- across a small battlefield or down a large corridor. This is usually about 30-100 feet away. While under danger, a PC will likely have to make an Agility check to get here safely. Anything on a battle map that is within the length of a standard piece of paper (~10-11 inches) can usually be considered far.",
|
"description": "means a distance where one can see the appearance of a person or object, but probably not in great detail-- across a small battlefield or down a large corridor. This is usually about 30-100 feet away. While under danger, a PC will likely have to make an Agility roll to get here safely. Anything on a battle map that is within the length of a standard piece of paper (~10-11 inches) can usually be considered far.",
|
||||||
"short": "Far"
|
"short": "Far"
|
||||||
},
|
},
|
||||||
"veryFar": {
|
"veryFar": {
|
||||||
"name": "Very Far",
|
"name": "Very Far",
|
||||||
"description": "means a distance where you can see the shape of a person or object, but probably not make outany details-- across a large battlefield or down a long street, generally about 100-300 feet away. While under danger, a PC likely has to make an Agility check to get here safely. Anything on a battle map that is beyond far distance, but still within sight of the characters can usually be considered very far.",
|
"description": "means a distance where you can see the shape of a person or object, but probably not make outany details-- across a large battlefield or down a long street, generally about 100-300 feet away. While under danger, a PC likely has to make an Agility roll to get here safely. Anything on a battle map that is beyond far distance, but still within sight of the characters can usually be considered very far.",
|
||||||
"short": "V. Far"
|
"short": "V. Far"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1246,6 +1295,7 @@
|
||||||
"triggerTexts": {
|
"triggerTexts": {
|
||||||
"strangePatternsContentTitle": "Matched {nr} times.",
|
"strangePatternsContentTitle": "Matched {nr} times.",
|
||||||
"strangePatternsContentSubTitle": "Increase hope and stress to a total of {nr}.",
|
"strangePatternsContentSubTitle": "Increase hope and stress to a total of {nr}.",
|
||||||
|
"strangePatternsActionExplanation": "Left click to increase, right click to decrease",
|
||||||
"ferocityContent": "Spend 2 Hope to gain {bonus} bonus Evasion until after the next attack against you?",
|
"ferocityContent": "Spend 2 Hope to gain {bonus} bonus Evasion until after the next attack against you?",
|
||||||
"ferocityEffectDescription": "Your evasion is increased by {bonus}. This bonus lasts until after the next attack made against you."
|
"ferocityEffectDescription": "Your evasion is increased by {bonus}. This bonus lasts until after the next attack made against you."
|
||||||
},
|
},
|
||||||
|
|
@ -1815,6 +1865,16 @@
|
||||||
"singular": "Adversary",
|
"singular": "Adversary",
|
||||||
"plural": "Adversaries"
|
"plural": "Adversaries"
|
||||||
},
|
},
|
||||||
|
"Attack": {
|
||||||
|
"hpDamageMultiplier": {
|
||||||
|
"label": "HP Damage Multiplier",
|
||||||
|
"hint": "Multiply any damage you deal by this number"
|
||||||
|
},
|
||||||
|
"hpDamageTakenMultiplier": {
|
||||||
|
"label": "HP Damage Taken Multiplier",
|
||||||
|
"hint": "Multiply any damage dealt to you by this number"
|
||||||
|
}
|
||||||
|
},
|
||||||
"Bonuses": {
|
"Bonuses": {
|
||||||
"rest": {
|
"rest": {
|
||||||
"downtimeAction": "Downtime Action",
|
"downtimeAction": "Downtime Action",
|
||||||
|
|
@ -1999,16 +2059,40 @@
|
||||||
"reaction": "Reaction Roll"
|
"reaction": "Reaction Roll"
|
||||||
},
|
},
|
||||||
"Rules": {
|
"Rules": {
|
||||||
|
"conditionImmunities": {
|
||||||
|
"hidden": "Condition Immunity: Hidden",
|
||||||
|
"restrained": "Condition Immunity: Restrained",
|
||||||
|
"vulnerable": "Condition Immunity: Vulnerable"
|
||||||
|
},
|
||||||
"damageReduction": {
|
"damageReduction": {
|
||||||
|
"disabledArmor": { "label": "Disabled Armorslots" },
|
||||||
"increasePerArmorMark": {
|
"increasePerArmorMark": {
|
||||||
"label": "Damage Reduction per Armor Slot",
|
"label": "Damage Reduction per Armor Slot",
|
||||||
"hint": "A used armor slot normally reduces damage by one step. This value increases the number of steps damage is reduced by."
|
"hint": "A used armor slot normally reduces damage by one step. This value increases the number of steps damage is reduced by."
|
||||||
},
|
},
|
||||||
|
"magical": {
|
||||||
|
"label": "Daamge Reduction: Only Magical",
|
||||||
|
"hint": "Armor can only be used to reduce magical damage"
|
||||||
|
},
|
||||||
"maxArmorMarkedBonus": "Max Armor Used",
|
"maxArmorMarkedBonus": "Max Armor Used",
|
||||||
"maxArmorMarkedStress": {
|
"maxArmorMarkedStress": {
|
||||||
"label": "Max Armor Used With Stress",
|
"label": "Max Armor Used With Stress",
|
||||||
"hint": "If this value is set you can use up to that much stress to spend additional Armor Marks beyond your normal maximum."
|
"hint": "If this value is set you can use up to that much stress to spend additional Armor Marks beyond your normal maximum."
|
||||||
},
|
},
|
||||||
|
"reduceSeverity": {
|
||||||
|
"magical": {
|
||||||
|
"label": "Reduce Damage Severity: Magical",
|
||||||
|
"hint": "Lowers any magical damage received by the set amount of severity degrees"
|
||||||
|
},
|
||||||
|
"physical": {
|
||||||
|
"label": "Reduce Damage Severity: Physical",
|
||||||
|
"hint": "Lowers any physical damage received by the set amount of severity degrees"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"physical": {
|
||||||
|
"label": "Damage Reduction: Only Physical",
|
||||||
|
"hint": "Armor can only be used to reduce physical damage"
|
||||||
|
},
|
||||||
"stress": {
|
"stress": {
|
||||||
"any": {
|
"any": {
|
||||||
"label": "Stress Damage Reduction: Any",
|
"label": "Stress Damage Reduction: Any",
|
||||||
|
|
@ -2026,6 +2110,12 @@
|
||||||
"label": "Stress Damage Reduction: Minor",
|
"label": "Stress Damage Reduction: Minor",
|
||||||
"hint": "The cost in stress you can pay to reduce minor damage to none."
|
"hint": "The cost in stress you can pay to reduce minor damage to none."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"thresholdImmunities": {
|
||||||
|
"minor": {
|
||||||
|
"label": "Threshold Immunities: Minor",
|
||||||
|
"hint": "Automatically ignores minor damage when set to 1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"attack": {
|
"attack": {
|
||||||
|
|
@ -2061,6 +2151,7 @@
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"main": "Data",
|
"main": "Data",
|
||||||
"information": "Information",
|
"information": "Information",
|
||||||
|
"itemFeatures": "Item Features",
|
||||||
"notes": "Notes",
|
"notes": "Notes",
|
||||||
"inventory": "Inventory",
|
"inventory": "Inventory",
|
||||||
"loadout": "Loadout",
|
"loadout": "Loadout",
|
||||||
|
|
@ -2085,7 +2176,6 @@
|
||||||
"tier4": "tier 4",
|
"tier4": "tier 4",
|
||||||
"domains": "Domains",
|
"domains": "Domains",
|
||||||
"downtime": "Downtime",
|
"downtime": "Downtime",
|
||||||
"itemFeatures": "Item Features",
|
|
||||||
"roll": "Roll",
|
"roll": "Roll",
|
||||||
"rules": "Rules",
|
"rules": "Rules",
|
||||||
"partyMembers": "Party Members",
|
"partyMembers": "Party Members",
|
||||||
|
|
@ -2094,7 +2184,10 @@
|
||||||
"questions": "Questions",
|
"questions": "Questions",
|
||||||
"configuration": "Configuration",
|
"configuration": "Configuration",
|
||||||
"base": "Base",
|
"base": "Base",
|
||||||
"triggers": "Triggers"
|
"triggers": "Triggers",
|
||||||
|
"deathMoves": "Deathmoves",
|
||||||
|
"sources": "Sources",
|
||||||
|
"packs": "Packs"
|
||||||
},
|
},
|
||||||
"Tiers": {
|
"Tiers": {
|
||||||
"singular": "Tier",
|
"singular": "Tier",
|
||||||
|
|
@ -2118,6 +2211,7 @@
|
||||||
"armorSlots": "Armor Slots",
|
"armorSlots": "Armor Slots",
|
||||||
"artistAttribution": "Artwork By: {artist}",
|
"artistAttribution": "Artwork By: {artist}",
|
||||||
"attack": "Attack",
|
"attack": "Attack",
|
||||||
|
"automation": "Automation",
|
||||||
"basics": "Basics",
|
"basics": "Basics",
|
||||||
"bonus": "Bonus",
|
"bonus": "Bonus",
|
||||||
"burden": "Burden",
|
"burden": "Burden",
|
||||||
|
|
@ -2125,6 +2219,7 @@
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"criticalSuccess": "Critical Success",
|
"criticalSuccess": "Critical Success",
|
||||||
"criticalShort": "Critical",
|
"criticalShort": "Critical",
|
||||||
|
"currentLevel": "Current Level",
|
||||||
"custom": "Custom",
|
"custom": "Custom",
|
||||||
"d20Roll": "D20 Roll",
|
"d20Roll": "D20 Roll",
|
||||||
"damage": "Damage",
|
"damage": "Damage",
|
||||||
|
|
@ -2136,6 +2231,7 @@
|
||||||
"dropActorsHere": "Drop Actors here",
|
"dropActorsHere": "Drop Actors here",
|
||||||
"dropFeaturesHere": "Drop Features here",
|
"dropFeaturesHere": "Drop Features here",
|
||||||
"duality": "Duality",
|
"duality": "Duality",
|
||||||
|
"dualityDice": "Duality Dice",
|
||||||
"dualityRoll": "Duality Roll",
|
"dualityRoll": "Duality Roll",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"evasion": "Evasion",
|
"evasion": "Evasion",
|
||||||
|
|
@ -2145,11 +2241,14 @@
|
||||||
"plural": "Experiences"
|
"plural": "Experiences"
|
||||||
},
|
},
|
||||||
"failure": "Failure",
|
"failure": "Failure",
|
||||||
|
"fate": "Fate",
|
||||||
|
"fateRoll": "Fate Roll",
|
||||||
"fear": "Fear",
|
"fear": "Fear",
|
||||||
"features": "Features",
|
"features": "Features",
|
||||||
"formula": "Formula",
|
"formula": "Formula",
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"gm": "GM",
|
"gm": "GM",
|
||||||
|
"guaranteedCriticalSuccess": "Guaranteed Critical Success",
|
||||||
"healing": "Healing",
|
"healing": "Healing",
|
||||||
"healingRoll": "Healing Roll",
|
"healingRoll": "Healing Roll",
|
||||||
"hit": {
|
"hit": {
|
||||||
|
|
@ -2193,6 +2292,7 @@
|
||||||
"single": "Player",
|
"single": "Player",
|
||||||
"plurial": "Players"
|
"plurial": "Players"
|
||||||
},
|
},
|
||||||
|
"portrait": "Portrait",
|
||||||
"proficiency": "Proficiency",
|
"proficiency": "Proficiency",
|
||||||
"quantity": "Quantity",
|
"quantity": "Quantity",
|
||||||
"range": "Range",
|
"range": "Range",
|
||||||
|
|
@ -2209,6 +2309,7 @@
|
||||||
"rollWith": "{roll} Roll",
|
"rollWith": "{roll} Roll",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"scalable": "Scalable",
|
"scalable": "Scalable",
|
||||||
|
"scars": "Scars",
|
||||||
"situationalBonus": "Situational Bonus",
|
"situationalBonus": "Situational Bonus",
|
||||||
"spent": "Spent",
|
"spent": "Spent",
|
||||||
"step": "Step",
|
"step": "Step",
|
||||||
|
|
@ -2224,6 +2325,7 @@
|
||||||
"single": "Target",
|
"single": "Target",
|
||||||
"plural": "Targets"
|
"plural": "Targets"
|
||||||
},
|
},
|
||||||
|
"thingsAndThing": "{things} and {thing}",
|
||||||
"title": "Title",
|
"title": "Title",
|
||||||
"tokenSize": "Token Size",
|
"tokenSize": "Token Size",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
|
|
@ -2262,7 +2364,8 @@
|
||||||
},
|
},
|
||||||
"Ancestry": {
|
"Ancestry": {
|
||||||
"primaryFeature": "Primary Feature",
|
"primaryFeature": "Primary Feature",
|
||||||
"secondaryFeature": "Secondary Feature"
|
"secondaryFeature": "Secondary Feature",
|
||||||
|
"featuresLabel": "Ancestry Features"
|
||||||
},
|
},
|
||||||
"Armor": {
|
"Armor": {
|
||||||
"baseScore": "Base Score",
|
"baseScore": "Base Score",
|
||||||
|
|
@ -2315,7 +2418,12 @@
|
||||||
"evolvedImagePlaceholder": "The image for the form selected for evolution will be used"
|
"evolvedImagePlaceholder": "The image for the form selected for evolution will be used"
|
||||||
},
|
},
|
||||||
"Class": {
|
"Class": {
|
||||||
|
"startingEvasionScore": "Starting Evasion Score",
|
||||||
|
"startingHitPoints": "Starting Hit Points",
|
||||||
|
"classItems": "Class Items",
|
||||||
|
"hopeFeatureLabel": "{class}'s Hope Feature",
|
||||||
"hopeFeatures": "Hope Features",
|
"hopeFeatures": "Hope Features",
|
||||||
|
"classFeature": "Class Feature",
|
||||||
"classFeatures": "Class Features",
|
"classFeatures": "Class Features",
|
||||||
"guide": {
|
"guide": {
|
||||||
"suggestedEquipment": "Suggested Equipments",
|
"suggestedEquipment": "Suggested Equipments",
|
||||||
|
|
@ -2328,6 +2436,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Community": {
|
||||||
|
"featuresLabel": "Community Feature"
|
||||||
|
},
|
||||||
"Consumable": {
|
"Consumable": {
|
||||||
"consumeOnUse": "Consume On Use",
|
"consumeOnUse": "Consume On Use",
|
||||||
"destroyOnEmpty": "Destroy On Empty"
|
"destroyOnEmpty": "Destroy On Empty"
|
||||||
|
|
@ -2343,7 +2454,11 @@
|
||||||
"masteryTitle": "Mastery"
|
"masteryTitle": "Mastery"
|
||||||
},
|
},
|
||||||
"Subclass": {
|
"Subclass": {
|
||||||
"spellcastingTrait": "Spellcasting Trait"
|
"spellcastingTrait": "Spellcasting Trait",
|
||||||
|
"spellcastTrait": "Spellcast Trait",
|
||||||
|
"foundationFeatures": "Foundation Features",
|
||||||
|
"specializationFeature": "Specialization Feature",
|
||||||
|
"masteryFeature": "Mastery Feature"
|
||||||
},
|
},
|
||||||
"Weapon": {
|
"Weapon": {
|
||||||
"weaponType": "Weapon Type",
|
"weaponType": "Weapon Type",
|
||||||
|
|
@ -2351,6 +2466,12 @@
|
||||||
"secondaryWeapon": "Secondary Weapon"
|
"secondaryWeapon": "Secondary Weapon"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ROLLTABLES": {
|
||||||
|
"FIELDS": {
|
||||||
|
"formulaName": { "label": "Formula Name" }
|
||||||
|
},
|
||||||
|
"formula": "Formula"
|
||||||
|
},
|
||||||
"SETTINGS": {
|
"SETTINGS": {
|
||||||
"Appearance": {
|
"Appearance": {
|
||||||
"FIELDS": {
|
"FIELDS": {
|
||||||
|
|
@ -2366,6 +2487,14 @@
|
||||||
"hideAttribution": {
|
"hideAttribution": {
|
||||||
"label": "Hide Attribution"
|
"label": "Hide Attribution"
|
||||||
},
|
},
|
||||||
|
"showTokenDistance": {
|
||||||
|
"label": "Show Token Distance on Hover",
|
||||||
|
"choices": {
|
||||||
|
"always": "Always",
|
||||||
|
"encounters": "Encounters",
|
||||||
|
"never": "Never"
|
||||||
|
}
|
||||||
|
},
|
||||||
"expandedTitle": "Auto-expand Descriptions",
|
"expandedTitle": "Auto-expand Descriptions",
|
||||||
"extendCharacterDescriptions": {
|
"extendCharacterDescriptions": {
|
||||||
"label": "Characters"
|
"label": "Characters"
|
||||||
|
|
@ -2417,13 +2546,21 @@
|
||||||
"overlay": { "label": "Overlay Effect" },
|
"overlay": { "label": "Overlay Effect" },
|
||||||
"characterDefault": { "label": "Character Default Defeated Status" },
|
"characterDefault": { "label": "Character Default Defeated Status" },
|
||||||
"adversaryDefault": { "label": "Adversary Default Defeated Status" },
|
"adversaryDefault": { "label": "Adversary Default Defeated Status" },
|
||||||
"companionDefault": { "label": "Companion Default Defeated Status" }
|
"companionDefault": { "label": "Companion Default Defeated Status" },
|
||||||
|
"deathMove": { "label": "Death Move" },
|
||||||
|
"dead": { "label": "Dead" },
|
||||||
|
"defeated": { "label": "Defeated" },
|
||||||
|
"unconscious": { "label": "Unconscious" }
|
||||||
},
|
},
|
||||||
"hopeFear": {
|
"hopeFear": {
|
||||||
"label": "Hope & Fear",
|
"label": "Hope & Fear",
|
||||||
"gm": { "label": "GM" },
|
"gm": { "label": "GM" },
|
||||||
"players": { "label": "Players" }
|
"players": { "label": "Players" }
|
||||||
},
|
},
|
||||||
|
"vulnerableAutomation": {
|
||||||
|
"label": "Vulnerable Automation",
|
||||||
|
"hint": "Automatically apply the Vulnerable condition when a actor reaches max stress"
|
||||||
|
},
|
||||||
"countdownAutomation": {
|
"countdownAutomation": {
|
||||||
"label": "Countdown Automation",
|
"label": "Countdown Automation",
|
||||||
"hint": "Automatically progress countdowns based on their progression settings"
|
"hint": "Automatically progress countdowns based on their progression settings"
|
||||||
|
|
@ -2450,10 +2587,6 @@
|
||||||
"label": "Show Resource Change Scrolltexts",
|
"label": "Show Resource Change Scrolltexts",
|
||||||
"hint": "When a character is damaged, uses armor etc, a scrolling text will briefly appear by the token to signify this."
|
"hint": "When a character is damaged, uses armor etc, a scrolling text will briefly appear by the token to signify this."
|
||||||
},
|
},
|
||||||
"playerCanEditSheet": {
|
|
||||||
"label": "Players Can Manually Edit Character Settings",
|
|
||||||
"hint": "Players are allowed to access the manual Character Settings and change their statistics beyond the rules."
|
|
||||||
},
|
|
||||||
"roll": {
|
"roll": {
|
||||||
"roll": {
|
"roll": {
|
||||||
"label": "Roll",
|
"label": "Roll",
|
||||||
|
|
@ -2506,9 +2639,13 @@
|
||||||
"itemFeatures": "Item Features",
|
"itemFeatures": "Item Features",
|
||||||
"nrChoices": "# Moves Per Rest",
|
"nrChoices": "# Moves Per Rest",
|
||||||
"resetMovesTitle": "Reset {type} Downtime Moves",
|
"resetMovesTitle": "Reset {type} Downtime Moves",
|
||||||
|
"resetItemFeaturesTitle": "Reset {type}",
|
||||||
"resetMovesText": "Are you sure you want to reset?",
|
"resetMovesText": "Are you sure you want to reset?",
|
||||||
|
"deleteItemTitle": "Delete Homebrew Item",
|
||||||
|
"deleteItemText": "Are you sure you want to delete the item?",
|
||||||
"FIELDS": {
|
"FIELDS": {
|
||||||
"maxFear": { "label": "Max Fear" },
|
"maxFear": { "label": "Max Fear" },
|
||||||
|
"maxHope": { "label": "Max Hope" },
|
||||||
"traitArray": { "label": "Initial Trait Modifiers" },
|
"traitArray": { "label": "Initial Trait Modifiers" },
|
||||||
"maxLoadout": {
|
"maxLoadout": {
|
||||||
"label": "Max Cards in Loadout",
|
"label": "Max Cards in Loadout",
|
||||||
|
|
@ -2656,7 +2793,16 @@
|
||||||
"currentTarget": "Current"
|
"currentTarget": "Current"
|
||||||
},
|
},
|
||||||
"deathMove": {
|
"deathMove": {
|
||||||
"title": "Death Move"
|
"title": "Death Move",
|
||||||
|
"gainScar": "You gained a scar.",
|
||||||
|
"avoidScar": "You have avoided a new scar.",
|
||||||
|
"journeysEnd": "You have {scars} Scars and have crossed out your last Hope slot. Your character's journey ends.",
|
||||||
|
"riskItAllCritical": "Critical Rolled, clearing all marked Stress and Hit Points.",
|
||||||
|
"riskItAllFailure": "The fear die rolled higher. You have crossed through the veil of death.",
|
||||||
|
"blazeOfGlory": "Blaze of Glory Effect Added!",
|
||||||
|
"riskItAllDialogButton": "Clear Stress And Hit Points.",
|
||||||
|
"riskItAllSuccessWithEnoughHope": "The Hope value is more than the marked Stress and Hit Points. Both are cleared fully.",
|
||||||
|
"riskItAllSuccess": "The hope die rolled higher, clear up to {hope} Stress And Hit Points."
|
||||||
},
|
},
|
||||||
"dicePool": {
|
"dicePool": {
|
||||||
"title": "Dice Pool"
|
"title": "Dice Pool"
|
||||||
|
|
@ -2665,7 +2811,7 @@
|
||||||
"title": "Domain Card"
|
"title": "Domain Card"
|
||||||
},
|
},
|
||||||
"dualityRoll": {
|
"dualityRoll": {
|
||||||
"abilityCheckTitle": "{ability} Check"
|
"abilityCheckTitle": "{ability} Roll"
|
||||||
},
|
},
|
||||||
"effectSummary": {
|
"effectSummary": {
|
||||||
"title": "Effects Applied",
|
"title": "Effects Applied",
|
||||||
|
|
@ -2680,7 +2826,7 @@
|
||||||
"selectLeader": "Select a Leader",
|
"selectLeader": "Select a Leader",
|
||||||
"selectMember": "Select a Member",
|
"selectMember": "Select a Member",
|
||||||
"rerollTitle": "Reroll Group Roll",
|
"rerollTitle": "Reroll Group Roll",
|
||||||
"rerollContent": "Are you sure you want to reroll your {trait} check?",
|
"rerollContent": "Are you sure you want to reroll your {trait} roll?",
|
||||||
"rerollTooltip": "Reroll",
|
"rerollTooltip": "Reroll",
|
||||||
"wholePartySelected": "The whole party is selected"
|
"wholePartySelected": "The whole party is selected"
|
||||||
},
|
},
|
||||||
|
|
@ -2728,6 +2874,7 @@
|
||||||
"ItemBrowser": {
|
"ItemBrowser": {
|
||||||
"title": "Daggerheart Compendium Browser",
|
"title": "Daggerheart Compendium Browser",
|
||||||
"hint": "Select a Folder in sidebar to start browsing through the compendium",
|
"hint": "Select a Folder in sidebar to start browsing through the compendium",
|
||||||
|
"browserSettings": "Browser Settings",
|
||||||
"searchPlaceholder": "Search...",
|
"searchPlaceholder": "Search...",
|
||||||
"columnName": "Name",
|
"columnName": "Name",
|
||||||
"tooltipFilters": "Filters",
|
"tooltipFilters": "Filters",
|
||||||
|
|
@ -2778,7 +2925,9 @@
|
||||||
"noAssignedPlayerCharacter": "You have no assigned character.",
|
"noAssignedPlayerCharacter": "You have no assigned character.",
|
||||||
"noSelectedToken": "You have no selected token",
|
"noSelectedToken": "You have no selected token",
|
||||||
"onlyUseableByPC": "This can only be used with a PC token",
|
"onlyUseableByPC": "This can only be used with a PC token",
|
||||||
"dualityParsing": "Duality roll not properly formated",
|
"dualityParsing": "Duality roll not properly formatted",
|
||||||
|
"fateParsing": "Fate roll not properly formatted",
|
||||||
|
"fateTypeParsing": "Fate roll not properly formatted, bad fate type. Valid types are 'Hope' and 'Fear'",
|
||||||
"attributeFaulty": "The supplied Attribute doesn't exist",
|
"attributeFaulty": "The supplied Attribute doesn't exist",
|
||||||
"domainCardWrongDomain": "You don't have access to that Domain",
|
"domainCardWrongDomain": "You don't have access to that Domain",
|
||||||
"domainCardToHighLevel": "The Domain Card is too high level to be selected",
|
"domainCardToHighLevel": "The Domain Card is too high level to be selected",
|
||||||
|
|
@ -2842,14 +2991,18 @@
|
||||||
"documentIsMissing": "The {documentType} is missing from the world.",
|
"documentIsMissing": "The {documentType} is missing from the world.",
|
||||||
"tokenActorMissing": "{name} is missing an Actor",
|
"tokenActorMissing": "{name} is missing an Actor",
|
||||||
"tokenActorsMissing": "[{names}] missing Actors",
|
"tokenActorsMissing": "[{names}] missing Actors",
|
||||||
"domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used"
|
"domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used",
|
||||||
|
"knowTheTide": "Know The Tide gained a token",
|
||||||
|
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}"
|
||||||
},
|
},
|
||||||
"Sidebar": {
|
"Sidebar": {
|
||||||
"actorDirectory": {
|
"actorDirectory": {
|
||||||
"tier": "Tier {tier} {type}",
|
"tier": "Tier {tier} {type}",
|
||||||
"character": "Level {level} Character",
|
"character": "Level {level} Character",
|
||||||
"companion": "Level {level} - {partner}",
|
"companion": "Level {level} - {partner}",
|
||||||
"companionNoPartner": "No Partner"
|
"companionNoPartner": "No Partner",
|
||||||
|
"duplicateToNewTier": "Duplicate to New Tier",
|
||||||
|
"pickTierTitle": "Pick a new tier for this adversary"
|
||||||
},
|
},
|
||||||
"daggerheartMenu": {
|
"daggerheartMenu": {
|
||||||
"title": "Daggerheart Menu",
|
"title": "Daggerheart Menu",
|
||||||
|
|
@ -2881,7 +3034,7 @@
|
||||||
"rulesOn": "Rules On",
|
"rulesOn": "Rules On",
|
||||||
"rulesOff": "Rules Off",
|
"rulesOff": "Rules Off",
|
||||||
"remainingUses": "Uses refresh on {type}",
|
"remainingUses": "Uses refresh on {type}",
|
||||||
"rightClickExtand": "Right-Click to extand",
|
"rightClickExtend": "Right-Click to extend",
|
||||||
"companionPartnerLevelBlock": "The companion needs an assigned partner to level up.",
|
"companionPartnerLevelBlock": "The companion needs an assigned partner to level up.",
|
||||||
"configureAttribution": "Configure Attribution",
|
"configureAttribution": "Configure Attribution",
|
||||||
"deleteItem": "Delete Item",
|
"deleteItem": "Delete Item",
|
||||||
|
|
|
||||||
143
module/applications/dialogs/CompendiumBrowserSettings.mjs
Normal file
143
module/applications/dialogs/CompendiumBrowserSettings.mjs
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
|
|
||||||
|
export default class CompendiumBrowserSettings extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.browserSettings = game.settings
|
||||||
|
.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings)
|
||||||
|
.toObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
tag: 'div',
|
||||||
|
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'compendium-brower-settings'],
|
||||||
|
window: {
|
||||||
|
icon: 'fa-solid fa-book',
|
||||||
|
title: 'DAGGERHEART.APPLICATIONS.CompendiumBrowserSettings.title'
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
width: 500
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
toggleSource: CompendiumBrowserSettings.#toggleSource,
|
||||||
|
finish: CompendiumBrowserSettings.#finish
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
static PARTS = {
|
||||||
|
packs: {
|
||||||
|
id: 'packs',
|
||||||
|
template: 'systems/daggerheart/templates/dialogs/compendiumBrowserSettingsDialog/packs.hbs'
|
||||||
|
},
|
||||||
|
footer: { template: 'systems/daggerheart/templates/dialogs/compendiumBrowserSettingsDialog/footer.hbs' }
|
||||||
|
};
|
||||||
|
|
||||||
|
static #browserPackTypes = ['Actor', 'Item'];
|
||||||
|
|
||||||
|
_attachPartListeners(partId, htmlElement, options) {
|
||||||
|
super._attachPartListeners(partId, htmlElement, options);
|
||||||
|
|
||||||
|
for (const element of htmlElement.querySelectorAll('.pack-checkbox'))
|
||||||
|
element.addEventListener('change', this.toggleTypedPack.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**@inheritdoc */
|
||||||
|
async _prepareContext(_options) {
|
||||||
|
const context = await super._prepareContext(_options);
|
||||||
|
|
||||||
|
const excludedSourceData = this.browserSettings.excludedSources;
|
||||||
|
const excludedPackData = this.browserSettings.excludedPacks;
|
||||||
|
context.typePackCollections = game.packs.reduce((acc, pack) => {
|
||||||
|
const { type, label, packageType, packageName: basePackageName, id } = pack.metadata;
|
||||||
|
if (!CompendiumBrowserSettings.#browserPackTypes.includes(type)) return acc;
|
||||||
|
|
||||||
|
const isWorldPack = packageType === 'world';
|
||||||
|
const packageName = isWorldPack ? 'world' : basePackageName;
|
||||||
|
const sourceChecked =
|
||||||
|
!excludedSourceData[packageName] ||
|
||||||
|
!excludedSourceData[packageName].excludedDocumentTypes.includes(type);
|
||||||
|
|
||||||
|
const sourceLabel =
|
||||||
|
game.modules.get(packageName)?.title ??
|
||||||
|
(isWorldPack
|
||||||
|
? game.i18n.localize('DAGGERHEART.APPLICATIONS.CompendiumBrowserSettings.worldCompendiums')
|
||||||
|
: game.system.title);
|
||||||
|
if (!acc[type]) acc[type] = { label: game.i18n.localize(`DOCUMENT.${type}s`), sources: {} };
|
||||||
|
if (!acc[type].sources[packageName])
|
||||||
|
acc[type].sources[packageName] = { label: sourceLabel, checked: sourceChecked, packs: [] };
|
||||||
|
|
||||||
|
const checked = !excludedPackData[id] || !excludedPackData[id].excludedDocumentTypes.includes(type);
|
||||||
|
|
||||||
|
acc[type].sources[packageName].packs.push({
|
||||||
|
pack: id,
|
||||||
|
type,
|
||||||
|
label: id === game.system.id ? game.system.title : game.i18n.localize(label),
|
||||||
|
checked: checked
|
||||||
|
});
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
static #toggleSource(event, button) {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const { type, source } = button.dataset;
|
||||||
|
const currentlyExcluded = this.browserSettings.excludedSources[source]
|
||||||
|
? this.browserSettings.excludedSources[source].excludedDocumentTypes.includes(type)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
if (!this.browserSettings.excludedSources[source])
|
||||||
|
this.browserSettings.excludedSources[source] = { excludedDocumentTypes: [] };
|
||||||
|
this.browserSettings.excludedSources[source].excludedDocumentTypes = currentlyExcluded
|
||||||
|
? this.browserSettings.excludedSources[source].excludedDocumentTypes.filter(x => x !== type)
|
||||||
|
: [...(this.browserSettings.excludedSources[source]?.excludedDocumentTypes ?? []), type];
|
||||||
|
|
||||||
|
const toggleIcon = button.querySelector('a > i');
|
||||||
|
toggleIcon.classList.toggle('fa-toggle-off');
|
||||||
|
toggleIcon.classList.toggle('fa-toggle-on');
|
||||||
|
button.closest('.source-container').querySelector('.checks-container').classList.toggle('collapsed');
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTypedPack(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const { type, pack } = event.target.dataset;
|
||||||
|
const currentlyExcluded = this.browserSettings.excludedPacks[pack]
|
||||||
|
? this.browserSettings.excludedPacks[pack].excludedDocumentTypes.includes(type)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
if (!this.browserSettings.excludedPacks[pack])
|
||||||
|
this.browserSettings.excludedPacks[pack] = { excludedDocumentTypes: [] };
|
||||||
|
this.browserSettings.excludedPacks[pack].excludedDocumentTypes = currentlyExcluded
|
||||||
|
? this.browserSettings.excludedPacks[pack].excludedDocumentTypes.filter(x => x !== type)
|
||||||
|
: [...(this.browserSettings.excludedPacks[pack]?.excludedDocumentTypes ?? []), type];
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #finish() {
|
||||||
|
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings);
|
||||||
|
await settings.updateSource(this.browserSettings);
|
||||||
|
await game.settings.set(
|
||||||
|
CONFIG.DH.id,
|
||||||
|
CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings,
|
||||||
|
settings.toObject()
|
||||||
|
);
|
||||||
|
|
||||||
|
this.updated = true;
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async configure() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const app = new this();
|
||||||
|
app.addEventListener('close', () => resolve(app.updated), { once: true });
|
||||||
|
app.render({ force: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export { default as AttributionDialog } from './attributionDialog.mjs';
|
export { default as AttributionDialog } from './attributionDialog.mjs';
|
||||||
export { default as BeastformDialog } from './beastformDialog.mjs';
|
export { default as BeastformDialog } from './beastformDialog.mjs';
|
||||||
|
export { default as CharacterResetDialog } from './characterResetDialog.mjs';
|
||||||
export { default as d20RollDialog } from './d20RollDialog.mjs';
|
export { default as d20RollDialog } from './d20RollDialog.mjs';
|
||||||
export { default as DamageDialog } from './damageDialog.mjs';
|
export { default as DamageDialog } from './damageDialog.mjs';
|
||||||
export { default as DamageReductionDialog } from './damageReductionDialog.mjs';
|
export { default as DamageReductionDialog } from './damageReductionDialog.mjs';
|
||||||
|
|
@ -14,3 +15,5 @@ export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs';
|
||||||
export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs';
|
export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs';
|
||||||
export { default as GroupRollDialog } from './group-roll-dialog.mjs';
|
export { default as GroupRollDialog } from './group-roll-dialog.mjs';
|
||||||
export { default as TagTeamDialog } from './tagTeamDialog.mjs';
|
export { default as TagTeamDialog } from './tagTeamDialog.mjs';
|
||||||
|
export { default as RiskItAllDialog } from './riskItAllDialog.mjs';
|
||||||
|
export { default as CompendiumBrowserSettingsDialog } from './CompendiumBrowserSettings.mjs';
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,11 @@ export default class AttributionDialog extends HandlebarsApplicationMixin(Applic
|
||||||
const after = label.slice(matchIndex + search.length, label.length);
|
const after = label.slice(matchIndex + search.length, label.length);
|
||||||
|
|
||||||
const element = document.createElement('li');
|
const element = document.createElement('li');
|
||||||
element.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
|
element.innerHTML =
|
||||||
|
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
|
||||||
|
' ',
|
||||||
|
' '
|
||||||
|
);
|
||||||
if (item.hint) {
|
if (item.hint) {
|
||||||
element.dataset.tooltip = game.i18n.localize(item.hint);
|
element.dataset.tooltip = game.i18n.localize(item.hint);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
105
module/applications/dialogs/characterResetDialog.mjs
Normal file
105
module/applications/dialogs/characterResetDialog.mjs
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
|
|
||||||
|
export default class CharacterResetDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
constructor(actor, options = {}) {
|
||||||
|
super(options);
|
||||||
|
|
||||||
|
this.actor = actor;
|
||||||
|
this.data = {
|
||||||
|
delete: {
|
||||||
|
class: { keep: false, label: 'TYPES.Item.class' },
|
||||||
|
subclass: { keep: false, label: 'TYPES.Item.subclass' },
|
||||||
|
ancestry: { keep: false, label: 'TYPES.Item.ancestry' },
|
||||||
|
community: { keep: false, label: 'TYPES.Item.community' }
|
||||||
|
},
|
||||||
|
optional: {
|
||||||
|
portrait: { keep: true, label: 'DAGGERHEART.GENERAL.portrait' },
|
||||||
|
name: { keep: true, label: 'Name' },
|
||||||
|
biography: { keep: true, label: 'DAGGERHEART.GENERAL.Tabs.biography' },
|
||||||
|
inventory: { keep: true, label: 'DAGGERHEART.GENERAL.inventory' }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
tag: 'form',
|
||||||
|
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'character-reset'],
|
||||||
|
window: {
|
||||||
|
icon: 'fa-solid fa-arrow-rotate-left',
|
||||||
|
title: 'DAGGERHEART.APPLICATIONS.CharacterReset.title'
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
finishSelection: this.#finishSelection
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
handler: this.updateData,
|
||||||
|
submitOnChange: true,
|
||||||
|
submitOnClose: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
static PARTS = {
|
||||||
|
resourceDice: {
|
||||||
|
id: 'resourceDice',
|
||||||
|
template: 'systems/daggerheart/templates/dialogs/characterReset.hbs'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async _prepareContext(_options) {
|
||||||
|
const context = await super._prepareContext(_options);
|
||||||
|
context.data = this.data;
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async updateData(event, _, formData) {
|
||||||
|
const { data } = foundry.utils.expandObject(formData.object);
|
||||||
|
|
||||||
|
this.data = foundry.utils.mergeObject(this.data, data);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
static getUpdateData() {
|
||||||
|
const update = {};
|
||||||
|
if (!this.data.optional.portrait) update.if(!this.data.optional.biography);
|
||||||
|
|
||||||
|
if (!this.data.optional.inventory) return update;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #finishSelection() {
|
||||||
|
const update = {};
|
||||||
|
if (!this.data.optional.name.keep) {
|
||||||
|
const defaultName = game.system.api.documents.DhpActor.defaultName({ type: 'character' });
|
||||||
|
foundry.utils.setProperty(update, 'name', defaultName);
|
||||||
|
foundry.utils.setProperty(update, 'prototypeToken.name', defaultName);
|
||||||
|
}
|
||||||
|
|
||||||
|
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', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.data.optional.biography.keep)
|
||||||
|
foundry.utils.setProperty(update, 'system.biography', this.actor.system.biography);
|
||||||
|
|
||||||
|
if (this.data.optional.inventory.keep) foundry.utils.setProperty(update, 'system.gold', this.actor.system.gold);
|
||||||
|
|
||||||
|
const { system, ...rest } = update;
|
||||||
|
await this.actor.update({
|
||||||
|
...rest,
|
||||||
|
'==system': system ?? {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const inventoryItemTypes = ['weapon', 'armor', 'consumable', 'loot'];
|
||||||
|
await this.actor.deleteEmbeddedDocuments(
|
||||||
|
'Item',
|
||||||
|
this.actor.items
|
||||||
|
.filter(x => !inventoryItemTypes.includes(x.type) || !this.data.optional.inventory.keep)
|
||||||
|
.map(x => x.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -109,11 +109,17 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
context.roll = this.roll;
|
context.roll = this.roll;
|
||||||
context.rollType = this.roll?.constructor.name;
|
context.rollType = this.roll?.constructor.name;
|
||||||
context.rallyDie = this.roll.rallyChoices;
|
context.rallyDie = this.roll.rallyChoices;
|
||||||
const experiences = this.config.data?.system?.experiences || {};
|
|
||||||
|
const actorExperiences = this.config.data?.system?.experiences || {};
|
||||||
|
const companionExperiences = this.config.roll.companionRoll
|
||||||
|
? (this.config.data?.companion?.system.experiences ?? {})
|
||||||
|
: null;
|
||||||
|
const experiences = companionExperiences ?? actorExperiences;
|
||||||
context.experiences = Object.keys(experiences).map(id => ({
|
context.experiences = Object.keys(experiences).map(id => ({
|
||||||
id,
|
id,
|
||||||
...experiences[id]
|
...experiences[id]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
context.selectedExperiences = this.config.experiences;
|
context.selectedExperiences = this.config.experiences;
|
||||||
context.advantage = this.config.roll?.advantage;
|
context.advantage = this.config.roll?.advantage;
|
||||||
context.disadvantage = this.config.roll?.disadvantage;
|
context.disadvantage = this.config.roll?.disadvantage;
|
||||||
|
|
@ -123,7 +129,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
context.formula = this.roll.constructFormula(this.config);
|
context.formula = this.roll.constructFormula(this.config);
|
||||||
if (this.actor?.system?.traits) context.abilities = this.getTraitModifiers();
|
if (this.actor?.system?.traits) context.abilities = this.getTraitModifiers();
|
||||||
|
|
||||||
context.showReaction = !this.config.roll?.type || context.rollType === 'DualityRoll';
|
context.showReaction = !this.config.skips?.reaction && context.rollType === 'DualityRoll';
|
||||||
context.reactionOverride = this.reactionOverride;
|
context.reactionOverride = this.reactionOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,9 +165,10 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
}
|
}
|
||||||
if (rest.hasOwnProperty('trait')) {
|
if (rest.hasOwnProperty('trait')) {
|
||||||
this.config.roll.trait = rest.trait;
|
this.config.roll.trait = rest.trait;
|
||||||
this.config.title = game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
if (!this.config.source.item)
|
||||||
ability: game.i18n.localize(abilities[this.config.roll.trait]?.label)
|
this.config.title = game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
||||||
});
|
ability: game.i18n.localize(abilities[this.config.roll.trait]?.label)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.config.extraFormula = rest.extraFormula;
|
this.config.extraFormula = rest.extraFormula;
|
||||||
this.render();
|
this.render();
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
import { enrichedFateRoll } from '../../enrichers/FateRollEnricher.mjs';
|
||||||
|
import { enrichedDualityRoll } from '../../enrichers/DualityRollEnricher.mjs';
|
||||||
|
|
||||||
export default class DhpDeathMove extends HandlebarsApplicationMixin(ApplicationV2) {
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
constructor(actor) {
|
constructor(actor) {
|
||||||
super({});
|
super({});
|
||||||
|
|
||||||
this.actor = actor;
|
this.actor = actor;
|
||||||
this.selectedMove = null;
|
this.selectedMove = null;
|
||||||
|
this.showRiskItAllButton = false;
|
||||||
|
this.riskItAllButtonLabel = '';
|
||||||
|
this.riskItAllHope = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
|
|
@ -38,6 +43,115 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleAvoidDeath(useAutomation) {
|
||||||
|
const target = this.actor.uuid;
|
||||||
|
const config = await enrichedFateRoll({
|
||||||
|
target,
|
||||||
|
title: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.avoidDeath.name'),
|
||||||
|
label: `${game.i18n.localize('DAGGERHEART.GENERAL.hope')} ${game.i18n.localize('DAGGERHEART.GENERAL.fateRoll')}`,
|
||||||
|
fateType: 'Hope'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!config.roll.fate) return;
|
||||||
|
if (!useAutomation) return '';
|
||||||
|
|
||||||
|
let returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.avoidScar');
|
||||||
|
if (config.roll.fate.value <= this.actor.system.levelData.level.current) {
|
||||||
|
const newScarAmount = this.actor.system.scars + 1;
|
||||||
|
await this.actor.update({
|
||||||
|
system: {
|
||||||
|
scars: newScarAmount
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newScarAmount >= this.actor.system.resources.hope.max) {
|
||||||
|
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id);
|
||||||
|
return game.i18n.format('DAGGERHEART.UI.Chat.deathMove.journeysEnd', { scars: newScarAmount });
|
||||||
|
}
|
||||||
|
|
||||||
|
returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.gainScar');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.unconscious.id);
|
||||||
|
return returnMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleRiskItAll(useAutomation) {
|
||||||
|
const config = await enrichedDualityRoll({
|
||||||
|
reaction: true,
|
||||||
|
traitValue: null,
|
||||||
|
target: this.actor,
|
||||||
|
difficulty: null,
|
||||||
|
title: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.riskItAll.name'),
|
||||||
|
label: game.i18n.localize('DAGGERHEART.GENERAL.dualityDice'),
|
||||||
|
actionType: null,
|
||||||
|
advantage: null,
|
||||||
|
grantResources: false,
|
||||||
|
customConfig: { skips: { resources: true, reaction: true } }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!config.roll.result) return;
|
||||||
|
if (!useAutomation) return '';
|
||||||
|
|
||||||
|
const clearAllStressAndHitpointsUpdates = [
|
||||||
|
{ key: 'hitPoints', clear: true },
|
||||||
|
{ key: 'stress', clear: true }
|
||||||
|
];
|
||||||
|
|
||||||
|
let chatMessage = '';
|
||||||
|
if (config.roll.isCritical) {
|
||||||
|
config.resourceUpdates.addResources(clearAllStressAndHitpointsUpdates);
|
||||||
|
chatMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.riskItAllCritical');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.roll.result.duality == 1) {
|
||||||
|
if (
|
||||||
|
config.roll.hope.value >=
|
||||||
|
this.actor.system.resources.hitPoints.value + this.actor.system.resources.stress.value
|
||||||
|
) {
|
||||||
|
config.resourceUpdates.addResources(clearAllStressAndHitpointsUpdates);
|
||||||
|
chatMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.riskItAllSuccessWithEnoughHope');
|
||||||
|
} else {
|
||||||
|
chatMessage = game.i18n.format('DAGGERHEART.UI.Chat.deathMove.riskItAllSuccess', {
|
||||||
|
hope: config.roll.hope.value
|
||||||
|
});
|
||||||
|
this.showRiskItAllButton = true;
|
||||||
|
this.riskItAllHope = config.roll.hope.value;
|
||||||
|
this.riskItAllButtonLabel = game.i18n.format('DAGGERHEART.UI.Chat.deathMove.riskItAllDialogButton');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.roll.result.duality == -1) {
|
||||||
|
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id);
|
||||||
|
chatMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.riskItAllFailure');
|
||||||
|
}
|
||||||
|
|
||||||
|
await config.resourceUpdates.updateResources();
|
||||||
|
return chatMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleBlazeOfGlory(useAutomation) {
|
||||||
|
if (!useAutomation) return '';
|
||||||
|
|
||||||
|
this.actor.createEmbeddedDocuments('ActiveEffect', [
|
||||||
|
{
|
||||||
|
name: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.name'),
|
||||||
|
description: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.description'),
|
||||||
|
img: CONFIG.DH.GENERAL.deathMoves.blazeOfGlory.img,
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
key: 'system.rules.roll.guaranteedCritical',
|
||||||
|
mode: 2,
|
||||||
|
value: 'true'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id);
|
||||||
|
return game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.blazeOfGlory');
|
||||||
|
}
|
||||||
|
|
||||||
static selectMove(_, button) {
|
static selectMove(_, button) {
|
||||||
const move = button.dataset.move;
|
const move = button.dataset.move;
|
||||||
this.selectedMove = CONFIG.DH.GENERAL.deathMoves[move];
|
this.selectedMove = CONFIG.DH.GENERAL.deathMoves[move];
|
||||||
|
|
@ -46,23 +160,52 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application
|
||||||
}
|
}
|
||||||
|
|
||||||
static async takeMove() {
|
static async takeMove() {
|
||||||
|
this.close();
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
const deathMoveAutomation = game.settings.get(
|
||||||
|
CONFIG.DH.id,
|
||||||
|
CONFIG.DH.SETTINGS.gameSettings.Automation
|
||||||
|
).deathMoveAutomation;
|
||||||
|
if (CONFIG.DH.GENERAL.deathMoves.blazeOfGlory === this.selectedMove) {
|
||||||
|
result = await this.handleBlazeOfGlory(deathMoveAutomation.blazeOfGlory);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CONFIG.DH.GENERAL.deathMoves.avoidDeath === this.selectedMove) {
|
||||||
|
result = await this.handleAvoidDeath(deathMoveAutomation.avoidDeath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CONFIG.DH.GENERAL.deathMoves.riskItAll === this.selectedMove) {
|
||||||
|
result = await this.handleRiskItAll(deathMoveAutomation.riskItAll);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result === undefined) return;
|
||||||
|
|
||||||
|
const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
|
||||||
|
.expandRollMessage?.desc;
|
||||||
const cls = getDocumentClass('ChatMessage');
|
const cls = getDocumentClass('ChatMessage');
|
||||||
|
|
||||||
const msg = {
|
const msg = {
|
||||||
user: game.user.id,
|
user: game.user.id,
|
||||||
content: await foundry.applications.handlebars.renderTemplate(
|
content: await foundry.applications.handlebars.renderTemplate(
|
||||||
'systems/daggerheart/templates/ui/chat/deathMove.hbs',
|
'systems/daggerheart/templates/ui/chat/deathMove.hbs',
|
||||||
{
|
{
|
||||||
player: this.actor.name,
|
player: this.actor.name,
|
||||||
actor: { name: this.actor.name, img: this.actor.img },
|
actor: this.actor,
|
||||||
|
actorId: this.actor._id,
|
||||||
author: game.users.get(game.user.id),
|
author: game.users.get(game.user.id),
|
||||||
title: game.i18n.localize(this.selectedMove.name),
|
title: game.i18n.localize(this.selectedMove.name),
|
||||||
img: this.selectedMove.img,
|
img: this.selectedMove.img,
|
||||||
description: game.i18n.localize(this.selectedMove.description)
|
description: game.i18n.localize(this.selectedMove.description),
|
||||||
|
result: result,
|
||||||
|
open: autoExpandDescription ? 'open' : '',
|
||||||
|
showRiskItAllButton: this.showRiskItAllButton,
|
||||||
|
riskItAllButtonLabel: this.riskItAllButtonLabel,
|
||||||
|
riskItAllHope: this.riskItAllHope
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
title: game.i18n.localize(
|
title: game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.title'),
|
||||||
'DAGGERHEART.UI.Chat.deathMove.title'
|
|
||||||
),
|
|
||||||
speaker: cls.getSpeaker(),
|
speaker: cls.getSpeaker(),
|
||||||
flags: {
|
flags: {
|
||||||
daggerheart: {
|
daggerheart: {
|
||||||
|
|
@ -72,7 +215,5 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application
|
||||||
};
|
};
|
||||||
|
|
||||||
cls.create(msg);
|
cls.create(msg);
|
||||||
|
|
||||||
this.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,9 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
|
||||||
.filter(x => x.testUserPermission(game.user, 'LIMITED'))
|
.filter(x => x.testUserPermission(game.user, 'LIMITED'))
|
||||||
.filter(x => x.uuid !== this.actor.uuid);
|
.filter(x => x.uuid !== this.actor.uuid);
|
||||||
|
|
||||||
|
const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
|
||||||
|
.expandRollMessage?.desc;
|
||||||
|
|
||||||
const cls = getDocumentClass('ChatMessage');
|
const cls = getDocumentClass('ChatMessage');
|
||||||
const msg = {
|
const msg = {
|
||||||
user: game.user.id,
|
user: game.user.id,
|
||||||
|
|
@ -216,7 +219,8 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
|
||||||
actor: { name: this.actor.name, img: this.actor.img },
|
actor: { name: this.actor.name, img: this.actor.img },
|
||||||
moves: moves,
|
moves: moves,
|
||||||
characters: characters,
|
characters: characters,
|
||||||
selfId: this.actor.uuid
|
selfId: this.actor.uuid,
|
||||||
|
open: autoExpandDescription ? 'open' : ''
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
flags: {
|
flags: {
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,11 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
element.appendChild(img);
|
element.appendChild(img);
|
||||||
|
|
||||||
const label = document.createElement('span');
|
const label = document.createElement('span');
|
||||||
label.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
|
label.innerHTML =
|
||||||
|
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
|
||||||
|
' ',
|
||||||
|
' '
|
||||||
|
);
|
||||||
element.appendChild(label);
|
element.appendChild(label);
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
|
|
@ -119,7 +123,11 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
element.appendChild(img);
|
element.appendChild(img);
|
||||||
|
|
||||||
const label = document.createElement('span');
|
const label = document.createElement('span');
|
||||||
label.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
|
label.innerHTML =
|
||||||
|
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
|
||||||
|
' ',
|
||||||
|
' '
|
||||||
|
);
|
||||||
element.appendChild(label);
|
element.appendChild(label);
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
|
|
|
||||||
94
module/applications/dialogs/riskItAllDialog.mjs
Normal file
94
module/applications/dialogs/riskItAllDialog.mjs
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
export default class RiskItAllDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
constructor(actor, resourceValue) {
|
||||||
|
super({});
|
||||||
|
|
||||||
|
this.actor = actor;
|
||||||
|
this.resourceValue = resourceValue;
|
||||||
|
this.choices = {
|
||||||
|
hitPoints: 0,
|
||||||
|
stress: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
return game.i18n.format('DAGGERHEART.APPLICATIONS.RiskItAllDialog.title', { name: this.actor.name });
|
||||||
|
}
|
||||||
|
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
classes: ['daggerheart', 'dh-style', 'dialog', 'views', 'risk-it-all'],
|
||||||
|
position: { width: 280, height: 'auto' },
|
||||||
|
window: { icon: 'fa-solid fa-dice fa-xl' },
|
||||||
|
actions: {
|
||||||
|
finish: RiskItAllDialog.#finish
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
application: {
|
||||||
|
id: 'risk-it-all',
|
||||||
|
template: 'systems/daggerheart/templates/dialogs/riskItAllDialog.hbs'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_attachPartListeners(partId, htmlElement, options) {
|
||||||
|
super._attachPartListeners(partId, htmlElement, options);
|
||||||
|
|
||||||
|
for (const input of htmlElement.querySelectorAll('.resource-container input'))
|
||||||
|
input.addEventListener('change', this.updateChoice.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
async _prepareContext(_options) {
|
||||||
|
const context = await super._prepareContext(_options);
|
||||||
|
context.resourceValue = this.resourceValue;
|
||||||
|
context.maxHitPointsValue = Math.min(this.resourceValue, this.actor.system.resources.hitPoints.max);
|
||||||
|
context.maxStressValue = Math.min(this.resourceValue, this.actor.system.resources.stress.max);
|
||||||
|
context.remainingResource = this.resourceValue - this.choices.hitPoints - this.choices.stress;
|
||||||
|
context.unfinished = context.remainingResource !== 0;
|
||||||
|
|
||||||
|
context.choices = this.choices;
|
||||||
|
context.final = {
|
||||||
|
hitPoints: {
|
||||||
|
value: this.actor.system.resources.hitPoints.value - this.choices.hitPoints,
|
||||||
|
max: this.actor.system.resources.hitPoints.max
|
||||||
|
},
|
||||||
|
stress: {
|
||||||
|
value: this.actor.system.resources.stress.value - this.choices.stress,
|
||||||
|
max: this.actor.system.resources.stress.max
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
context;
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChoice(event) {
|
||||||
|
let value = Number.parseInt(event.target.value);
|
||||||
|
const choiceKey = event.target.dataset.choice;
|
||||||
|
const actorValue = this.actor.system.resources[choiceKey].value;
|
||||||
|
const remaining = this.resourceValue - this.choices.hitPoints - this.choices.stress;
|
||||||
|
const changeAmount = value - this.choices[choiceKey];
|
||||||
|
|
||||||
|
/* If trying to increase beyond remaining resource points, just increase to max available */
|
||||||
|
if (remaining - changeAmount < 0) value = this.choices[choiceKey] + remaining;
|
||||||
|
else if (actorValue - value < 0) value = actorValue;
|
||||||
|
|
||||||
|
this.choices[choiceKey] = value;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #finish() {
|
||||||
|
const resourceUpdate = Object.keys(this.choices).reduce((acc, resourceKey) => {
|
||||||
|
const value = this.actor.system.resources[resourceKey].value - this.choices[resourceKey];
|
||||||
|
acc[resourceKey] = { value };
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
await this.actor.update({
|
||||||
|
'system.resources': resourceUpdate
|
||||||
|
});
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
|
||||||
classes: ['daggerheart'],
|
classes: ['daggerheart'],
|
||||||
actions: {
|
actions: {
|
||||||
combat: DHTokenHUD.#onToggleCombat,
|
combat: DHTokenHUD.#onToggleCombat,
|
||||||
togglePartyTokens: DHTokenHUD.#togglePartyTokens
|
togglePartyTokens: DHTokenHUD.#togglePartyTokens,
|
||||||
|
toggleCompanions: DHTokenHUD.#toggleCompanions
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -26,7 +27,7 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
|
||||||
context.partyOnCanvas =
|
context.partyOnCanvas =
|
||||||
this.actor.type === 'party' &&
|
this.actor.type === 'party' &&
|
||||||
this.actor.system.partyMembers.some(member => member.getActiveTokens().length > 0);
|
this.actor.system.partyMembers.some(member => member.getActiveTokens().length > 0);
|
||||||
context.icons.toggleParty = 'systems/daggerheart/assets/icons/arrow-dunk.png';
|
context.icons.toggleClowncar = 'systems/daggerheart/assets/icons/arrow-dunk.png';
|
||||||
context.actorType = this.actor.type;
|
context.actorType = this.actor.type;
|
||||||
context.usesEffects = this.actor.type !== 'party';
|
context.usesEffects = this.actor.type !== 'party';
|
||||||
context.canToggleCombat = DHTokenHUD.#nonCombatTypes.includes(this.actor.type)
|
context.canToggleCombat = DHTokenHUD.#nonCombatTypes.includes(this.actor.type)
|
||||||
|
|
@ -56,6 +57,9 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
|
||||||
}, {})
|
}, {})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
context.hasCompanion = this.actor.system.companion;
|
||||||
|
context.companionOnCanvas = context.hasCompanion && this.actor.system.companion.getActiveTokens().length > 0;
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,8 +105,24 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
|
||||||
: 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositPartyTokens'
|
: 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositPartyTokens'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.toggleClowncar(this.actor.system.partyMembers);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #toggleCompanions(_, button) {
|
||||||
|
const icon = button.querySelector('img');
|
||||||
|
icon.classList.toggle('flipped');
|
||||||
|
button.dataset.tooltip = game.i18n.localize(
|
||||||
|
icon.classList.contains('flipped')
|
||||||
|
? 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.retrieveCompanionTokens'
|
||||||
|
: 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositCompanionTokens'
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.toggleClowncar([this.actor.system.companion]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleClowncar(actors) {
|
||||||
const animationDuration = 500;
|
const animationDuration = 500;
|
||||||
const activeTokens = this.actor.system.partyMembers.flatMap(member => member.getActiveTokens());
|
const activeTokens = actors.flatMap(member => member.getActiveTokens());
|
||||||
const { x: actorX, y: actorY } = this.document;
|
const { x: actorX, y: actorY } = this.document;
|
||||||
if (activeTokens.length > 0) {
|
if (activeTokens.length > 0) {
|
||||||
for (let token of activeTokens) {
|
for (let token of activeTokens) {
|
||||||
|
|
@ -114,14 +134,15 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const activeScene = game.scenes.find(x => x.id === game.user.viewedScene);
|
const activeScene = game.scenes.find(x => x.id === game.user.viewedScene);
|
||||||
const partyTokenData = [];
|
const tokenData = [];
|
||||||
for (let member of this.actor.system.partyMembers) {
|
for (let member of actors) {
|
||||||
const data = await member.getTokenDocument();
|
const data = await member.getTokenDocument();
|
||||||
partyTokenData.push(data.toObject());
|
tokenData.push(data.toObject());
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTokens = await activeScene.createEmbeddedDocuments(
|
const newTokens = await activeScene.createEmbeddedDocuments(
|
||||||
'Token',
|
'Token',
|
||||||
partyTokenData.map(tokenData => ({
|
tokenData.map(tokenData => ({
|
||||||
...tokenData,
|
...tokenData,
|
||||||
alpha: 0,
|
alpha: 0,
|
||||||
x: actorX,
|
x: actorX,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import BaseLevelUp from './levelup.mjs';
|
import BaseLevelUp from './levelup.mjs';
|
||||||
import { defaultCompanionTier, LevelOptionType } from '../../data/levelTier.mjs';
|
import { defaultCompanionTier, LevelOptionType } from '../../data/levelTier.mjs';
|
||||||
import { DhLevelup } from '../../data/levelup.mjs';
|
import { DhCompanionLevelup as DhLevelup } from '../../data/companionLevelup.mjs';
|
||||||
import { diceTypes, range } from '../../config/generalConfig.mjs';
|
import { diceTypes, range } from '../../config/generalConfig.mjs';
|
||||||
|
|
||||||
export default class DhCompanionLevelUp extends BaseLevelUp {
|
export default class DhCompanionLevelUp extends BaseLevelUp {
|
||||||
|
|
@ -9,7 +9,9 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
|
||||||
|
|
||||||
this.levelTiers = this.addBonusChoices(defaultCompanionTier);
|
this.levelTiers = this.addBonusChoices(defaultCompanionTier);
|
||||||
const playerLevelupData = actor.system.levelData;
|
const playerLevelupData = actor.system.levelData;
|
||||||
this.levelup = new DhLevelup(DhLevelup.initializeData(this.levelTiers, playerLevelupData));
|
this.levelup = new DhLevelup(
|
||||||
|
DhLevelup.initializeData(this.levelTiers, playerLevelupData, actor.system.levelupChoicesLeft)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _preparePartContext(partId, context) {
|
async _preparePartContext(partId, context) {
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,10 @@ export default class DhlevelUpViewMode extends HandlebarsApplicationMixin(Applic
|
||||||
return checkbox;
|
return checkbox;
|
||||||
});
|
});
|
||||||
|
|
||||||
let label = game.i18n.localize(option.label);
|
let label =
|
||||||
|
optionKey === 'domainCard'
|
||||||
|
? game.i18n.format(option.label, { maxLevel: tier.levels.end })
|
||||||
|
: game.i18n.localize(option.label);
|
||||||
return {
|
return {
|
||||||
label: label,
|
label: label,
|
||||||
checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => {
|
checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => {
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,15 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
|
||||||
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
|
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
|
||||||
const item = await foundry.utils.fromUuid(data.uuid);
|
const item = await foundry.utils.fromUuid(data.uuid);
|
||||||
if (item instanceof game.system.api.documents.DhpActor && item.type === 'environment') {
|
if (item instanceof game.system.api.documents.DhpActor && item.type === 'environment') {
|
||||||
|
let sceneUuid = data.uuid;
|
||||||
|
if (item.pack) {
|
||||||
|
const inWorldActor = await game.system.api.documents.DhpActor.create([item.toObject()]);
|
||||||
|
if (!inWorldActor.length) return;
|
||||||
|
sceneUuid = inWorldActor[0].uuid;
|
||||||
|
}
|
||||||
|
|
||||||
await this.daggerheartFlag.updateSource({
|
await this.daggerheartFlag.updateSource({
|
||||||
sceneEnvironments: [...this.daggerheartFlag.sceneEnvironments, data.uuid]
|
sceneEnvironments: [...this.daggerheartFlag.sceneEnvironments, sceneUuid]
|
||||||
});
|
});
|
||||||
this.render({ internalRefresh: true });
|
this.render({ internalRefresh: true });
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +103,15 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
|
||||||
|
|
||||||
/** @override */
|
/** @override */
|
||||||
async _processSubmitData(event, form, submitData, options) {
|
async _processSubmitData(event, form, submitData, options) {
|
||||||
submitData.flags.daggerheart = this.daggerheartFlag.toObject();
|
if (!submitData.flags) submitData.flags = {};
|
||||||
|
submitData.flags.daggerheart = foundry.utils.mergeObject(
|
||||||
|
this.daggerheartFlag.toObject(),
|
||||||
|
submitData.flags.daggerheart
|
||||||
|
);
|
||||||
|
submitData.flags.daggerheart.sceneEnvironments = submitData.flags.daggerheart.sceneEnvironments.filter(x =>
|
||||||
|
foundry.utils.fromUuidSync(x)
|
||||||
|
);
|
||||||
|
|
||||||
for (const key of Object.keys(this.document._source.flags.daggerheart?.sceneEnvironments ?? {})) {
|
for (const key of Object.keys(this.document._source.flags.daggerheart?.sceneEnvironments ?? {})) {
|
||||||
if (!submitData.flags.daggerheart.sceneEnvironments[key]) {
|
if (!submitData.flags.daggerheart.sceneEnvironments[key]) {
|
||||||
submitData.flags.daggerheart.sceneEnvironments[`-=${key}`] = null;
|
submitData.flags.daggerheart.sceneEnvironments[`-=${key}`] = null;
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export default class DhAutomationSettings extends HandlebarsApplicationMixin(App
|
||||||
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
|
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
|
||||||
header: { template: 'systems/daggerheart/templates/settings/automation-settings/header.hbs' },
|
header: { template: 'systems/daggerheart/templates/settings/automation-settings/header.hbs' },
|
||||||
general: { template: 'systems/daggerheart/templates/settings/automation-settings/general.hbs' },
|
general: { template: 'systems/daggerheart/templates/settings/automation-settings/general.hbs' },
|
||||||
rules: { template: 'systems/daggerheart/templates/settings/automation-settings/rules.hbs' },
|
rules: { template: 'systems/daggerheart/templates/settings/automation-settings/deathMoves.hbs' },
|
||||||
roll: { template: 'systems/daggerheart/templates/settings/automation-settings/roll.hbs' },
|
roll: { template: 'systems/daggerheart/templates/settings/automation-settings/roll.hbs' },
|
||||||
footer: { template: 'systems/daggerheart/templates/settings/automation-settings/footer.hbs' }
|
footer: { template: 'systems/daggerheart/templates/settings/automation-settings/footer.hbs' }
|
||||||
};
|
};
|
||||||
|
|
@ -42,7 +42,7 @@ export default class DhAutomationSettings extends HandlebarsApplicationMixin(App
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
static TABS = {
|
static TABS = {
|
||||||
main: {
|
main: {
|
||||||
tabs: [{ id: 'general' }, { id: 'rules' }, { id: 'roll' }],
|
tabs: [{ id: 'general' }, { id: 'deathMoves' }, { id: 'roll' }],
|
||||||
initial: 'general',
|
initial: 'general',
|
||||||
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
addItem: this.addItem,
|
addItem: this.addItem,
|
||||||
editItem: this.editItem,
|
editItem: this.editItem,
|
||||||
removeItem: this.removeItem,
|
removeItem: this.removeItem,
|
||||||
resetMoves: this.resetMoves,
|
resetDowntimeMoves: this.resetDowntimeMoves,
|
||||||
|
resetItemFeatures: this.resetItemFeatures,
|
||||||
addDomain: this.addDomain,
|
addDomain: this.addDomain,
|
||||||
toggleSelectedDomain: this.toggleSelectedDomain,
|
toggleSelectedDomain: this.toggleSelectedDomain,
|
||||||
deleteDomain: this.deleteDomain,
|
deleteDomain: this.deleteDomain,
|
||||||
|
|
@ -102,6 +103,12 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
? { id: this.selected.adversaryType, ...this.settings.adversaryTypes[this.selected.adversaryType] }
|
? { id: this.selected.adversaryType, ...this.settings.adversaryTypes[this.selected.adversaryType] }
|
||||||
: null;
|
: null;
|
||||||
break;
|
break;
|
||||||
|
case 'downtime':
|
||||||
|
context.restOptions = {
|
||||||
|
shortRest: CONFIG.DH.GENERAL.defaultRestOptions.shortRest(),
|
||||||
|
longRest: CONFIG.DH.GENERAL.defaultRestOptions.longRest()
|
||||||
|
};
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
|
|
@ -164,7 +171,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'),
|
name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'),
|
||||||
img: 'icons/magic/life/cross-worn-green.webp',
|
img: 'icons/magic/life/cross-worn-green.webp',
|
||||||
description: '',
|
description: '',
|
||||||
actions: []
|
actions: [],
|
||||||
|
effects: []
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (['armorFeatures', 'weaponFeatures'].includes(type)) {
|
} else if (['armorFeatures', 'weaponFeatures'].includes(type)) {
|
||||||
|
|
@ -179,6 +187,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,20 +228,32 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
static async removeItem(_, target) {
|
static async removeItem(_, target) {
|
||||||
|
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
||||||
|
window: {
|
||||||
|
title: game.i18n.localize(`DAGGERHEART.SETTINGS.Homebrew.deleteItemTitle`)
|
||||||
|
},
|
||||||
|
content: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.deleteItemText')
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
const { type, id } = target.dataset;
|
const { type, id } = target.dataset;
|
||||||
const isDowntime = ['shortRest', 'longRest'].includes(type);
|
const isDowntime = ['shortRest', 'longRest'].includes(type);
|
||||||
const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`;
|
const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`;
|
||||||
await this.settings.updateSource({
|
await this.settings.updateSource({
|
||||||
[`${path}.-=${id}`]: null
|
[`${path}.-=${id}`]: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
static async resetMoves(_, target) {
|
static async resetDowntimeMoves(_, target) {
|
||||||
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
||||||
window: {
|
window: {
|
||||||
title: game.i18n.format('DAGGERHEART.SETTINGS.Homebrew.resetMovesTitle', {
|
title: game.i18n.format('DAGGERHEART.SETTINGS.Homebrew.resetMovesTitle', {
|
||||||
|
|
@ -266,7 +287,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
...move,
|
...move,
|
||||||
name: game.i18n.localize(move.name),
|
name: game.i18n.localize(move.name),
|
||||||
description: game.i18n.localize(move.description),
|
description: game.i18n.localize(move.description),
|
||||||
actions: move.actions.reduce((acc, key) => {
|
actions: Object.keys(move.actions).reduce((acc, key) => {
|
||||||
const action = move.actions[key];
|
const action = move.actions[key];
|
||||||
acc[key] = {
|
acc[key] = {
|
||||||
...action,
|
...action,
|
||||||
|
|
@ -293,6 +314,31 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async resetItemFeatures(_, target) {
|
||||||
|
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
||||||
|
window: {
|
||||||
|
title: game.i18n.format('DAGGERHEART.SETTINGS.Homebrew.resetItemFeaturesTitle', {
|
||||||
|
type: game.i18n.localize(`DAGGERHEART.GENERAL.${target.dataset.type}`)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
content: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.resetMovesText')
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
await this.settings.updateSource({
|
||||||
|
[`itemFeatures.${target.dataset.type}`]: Object.keys(
|
||||||
|
this.settings.itemFeatures[target.dataset.type]
|
||||||
|
).reduce((acc, key) => {
|
||||||
|
acc[`-=${key}`] = null;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
|
});
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
static async addDomain(event) {
|
static async addDomain(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const content = new foundry.data.fields.StringField({
|
const content = new foundry.data.fields.StringField({
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
||||||
async _prepareContext(_options) {
|
async _prepareContext(_options) {
|
||||||
const context = await super._prepareContext(_options, 'action');
|
const context = await super._prepareContext(_options, 'action');
|
||||||
context.source = this.action.toObject(true);
|
context.source = this.action.toObject(true);
|
||||||
|
context.action = this.action;
|
||||||
|
|
||||||
context.summons = [];
|
context.summons = [];
|
||||||
for (const summon of context.source.summon ?? []) {
|
for (const summon of context.source.summon ?? []) {
|
||||||
|
|
@ -313,7 +314,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
||||||
const index = Number.parseInt(button.dataset.index);
|
const index = Number.parseInt(button.dataset.index);
|
||||||
const toggle = (element, codeMirror) => {
|
const toggle = (element, codeMirror) => {
|
||||||
codeMirror.classList.toggle('revealed');
|
codeMirror.classList.toggle('revealed');
|
||||||
const button = element.querySelector('a > i');
|
const button = element.querySelector('.expand-trigger > i');
|
||||||
button.classList.toggle('fa-angle-up');
|
button.classList.toggle('fa-angle-up');
|
||||||
button.classList.toggle('fa-angle-down');
|
button.classList.toggle('fa-angle-down');
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,22 +4,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
const ignoredActorKeys = ['config', 'DhEnvironment'];
|
this.changeChoices = DhActiveEffectConfig.getChangeChoices();
|
||||||
this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => {
|
|
||||||
if (!ignoredActorKeys.includes(key)) {
|
|
||||||
const model = game.system.api.models.actors[key];
|
|
||||||
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model);
|
|
||||||
// As per DHToken._getTrackedAttributesFromSchema, attributes.bar have a max version as well.
|
|
||||||
const maxAttributes = attributes.bar.map(x => [...x, 'max']);
|
|
||||||
attributes.value.push(...maxAttributes);
|
|
||||||
const group = game.i18n.localize(model.metadata.label);
|
|
||||||
const choices = CONFIG.Token.documentClass
|
|
||||||
.getTrackedAttributeChoices(attributes, model)
|
|
||||||
.map(x => ({ ...x, group: group }));
|
|
||||||
acc.push(...choices);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
|
|
@ -50,6 +35,69 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get ChangeChoices for the changes autocomplete. Static for use in this class aswell as in settings-active-effect-config.mjs
|
||||||
|
* @returns {ChangeChoice { value: string, label: string, hint: string, group: string }[]}
|
||||||
|
*/
|
||||||
|
static getChangeChoices() {
|
||||||
|
const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty'];
|
||||||
|
|
||||||
|
const getAllLeaves = (root, group, parentPath = '') => {
|
||||||
|
const leaves = [];
|
||||||
|
const rootKey = `${parentPath ? `${parentPath}.` : ''}${root.name}`;
|
||||||
|
for (const field of Object.values(root.fields)) {
|
||||||
|
if (field instanceof foundry.data.fields.SchemaField)
|
||||||
|
leaves.push(...getAllLeaves(field, group, rootKey));
|
||||||
|
else
|
||||||
|
leaves.push({
|
||||||
|
value: `${rootKey}.${field.name}`,
|
||||||
|
label: game.i18n.localize(field.label),
|
||||||
|
hint: game.i18n.localize(field.hint),
|
||||||
|
group
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return leaves;
|
||||||
|
};
|
||||||
|
return Object.keys(game.system.api.models.actors).reduce((acc, key) => {
|
||||||
|
if (ignoredActorKeys.includes(key)) return acc;
|
||||||
|
|
||||||
|
const model = game.system.api.models.actors[key];
|
||||||
|
const group = game.i18n.localize(model.metadata.label);
|
||||||
|
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model.metadata.type);
|
||||||
|
|
||||||
|
const getTranslations = path => {
|
||||||
|
if (path === 'resources.hope.max')
|
||||||
|
return {
|
||||||
|
label: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxHope.label'),
|
||||||
|
hint: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const field = model.schema.getField(path);
|
||||||
|
return {
|
||||||
|
label: field ? game.i18n.localize(field.label) : path,
|
||||||
|
hint: field ? game.i18n.localize(field.hint) : ''
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const bars = attributes.bar.flatMap(x => {
|
||||||
|
const joined = `${x.join('.')}.max`;
|
||||||
|
return { value: joined, ...getTranslations(joined), group };
|
||||||
|
});
|
||||||
|
const values = attributes.value.flatMap(x => {
|
||||||
|
const joined = x.join('.');
|
||||||
|
return { value: joined, ...getTranslations(joined), group };
|
||||||
|
});
|
||||||
|
|
||||||
|
const bonuses = getAllLeaves(model.schema.fields.bonuses, group);
|
||||||
|
const rules = getAllLeaves(model.schema.fields.rules, group);
|
||||||
|
|
||||||
|
acc.push(...bars, ...values, ...rules, ...bonuses);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
_attachPartListeners(partId, htmlElement, options) {
|
_attachPartListeners(partId, htmlElement, options) {
|
||||||
super._attachPartListeners(partId, htmlElement, options);
|
super._attachPartListeners(partId, htmlElement, options);
|
||||||
const changeChoices = this.changeChoices;
|
const changeChoices = this.changeChoices;
|
||||||
|
|
@ -68,14 +116,18 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
|
||||||
},
|
},
|
||||||
render: function (item, search) {
|
render: function (item, search) {
|
||||||
const label = game.i18n.localize(item.label);
|
const label = game.i18n.localize(item.label);
|
||||||
const matchIndex = label.toLowerCase().indexOf(search);
|
const matchIndex = label.toLowerCase().indexOf(search.toLowerCase());
|
||||||
|
|
||||||
const beforeText = label.slice(0, matchIndex);
|
const beforeText = label.slice(0, matchIndex);
|
||||||
const matchText = label.slice(matchIndex, matchIndex + search.length);
|
const matchText = label.slice(matchIndex, matchIndex + search.length);
|
||||||
const after = label.slice(matchIndex + search.length, label.length);
|
const after = label.slice(matchIndex + search.length, label.length);
|
||||||
|
|
||||||
const element = document.createElement('li');
|
const element = document.createElement('li');
|
||||||
element.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
|
element.innerHTML =
|
||||||
|
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
|
||||||
|
' ',
|
||||||
|
' '
|
||||||
|
);
|
||||||
if (item.hint) {
|
if (item.hint) {
|
||||||
element.dataset.tooltip = game.i18n.localize(item.hint);
|
element.dataset.tooltip = game.i18n.localize(item.hint);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,7 @@ export default class SettingActiveEffectConfig extends HandlebarsApplicationMixi
|
||||||
super({});
|
super({});
|
||||||
|
|
||||||
this.effect = foundry.utils.deepClone(effect);
|
this.effect = foundry.utils.deepClone(effect);
|
||||||
const ignoredActorKeys = ['config', 'DhEnvironment'];
|
this.changeChoices = game.system.api.applications.sheetConfigs.ActiveEffectConfig.getChangeChoices();
|
||||||
this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => {
|
|
||||||
if (!ignoredActorKeys.includes(key)) {
|
|
||||||
const model = game.system.api.models.actors[key];
|
|
||||||
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model);
|
|
||||||
const group = game.i18n.localize(model.metadata.label);
|
|
||||||
const choices = CONFIG.Token.documentClass
|
|
||||||
.getTrackedAttributeChoices(attributes, model)
|
|
||||||
.map(x => ({ ...x, group: group }));
|
|
||||||
acc.push(...choices);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
|
|
@ -103,7 +91,11 @@ export default class SettingActiveEffectConfig extends HandlebarsApplicationMixi
|
||||||
const after = label.slice(matchIndex + search.length, label.length);
|
const after = label.slice(matchIndex + search.length, label.length);
|
||||||
|
|
||||||
const element = document.createElement('li');
|
const element = document.createElement('li');
|
||||||
element.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
|
element.innerHTML =
|
||||||
|
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
|
||||||
|
' ',
|
||||||
|
' '
|
||||||
|
);
|
||||||
if (item.hint) {
|
if (item.hint) {
|
||||||
element.dataset.tooltip = game.i18n.localize(item.hint);
|
element.dataset.tooltip = game.i18n.localize(item.hint);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,9 +73,11 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async updateData(event, element, formData) {
|
static async updateData(_event, _element, formData) {
|
||||||
const data = foundry.utils.expandObject(formData.object);
|
const data = foundry.utils.expandObject(formData.object);
|
||||||
foundry.utils.mergeObject(this.move, data);
|
await this.updateMove({
|
||||||
|
[`${this.movePath}`]: data
|
||||||
|
});
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
@ -135,9 +137,7 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.settings.updateSource({ [`${this.actionsPath}.${action.id}`]: action });
|
await this.updateMove({ [`${this.actionsPath}.${action.id}`]: action });
|
||||||
this.move = foundry.utils.getProperty(this.settings, this.movePath);
|
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -150,13 +150,12 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
|
||||||
await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect);
|
await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect);
|
||||||
if (!updatedEffect) return;
|
if (!updatedEffect) return;
|
||||||
|
|
||||||
await this.settings.updateSource({
|
await this.updateMove({
|
||||||
[`${this.movePath}.effects`]: this.move.effects.reduce((acc, effect, index) => {
|
[`${this.movePath}.effects`]: this.move.effects.reduce((acc, effect, index) => {
|
||||||
acc.push(index === effectIndex ? { ...updatedEffect, id: effect.id } : effect);
|
acc.push(index === effectIndex ? { ...updatedEffect, id: effect.id } : effect);
|
||||||
return acc;
|
return acc;
|
||||||
}, [])
|
}, [])
|
||||||
});
|
});
|
||||||
this.move = foundry.utils.getProperty(this.settings, this.movePath);
|
|
||||||
this.render();
|
this.render();
|
||||||
} else {
|
} else {
|
||||||
const action = this.move.actions.get(id);
|
const action = this.move.actions.get(id);
|
||||||
|
|
@ -171,13 +170,13 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
|
||||||
: existingEffectIndex === -1
|
: existingEffectIndex === -1
|
||||||
? [...currentEffects, effectData]
|
? [...currentEffects, effectData]
|
||||||
: currentEffects.with(existingEffectIndex, effectData);
|
: currentEffects.with(existingEffectIndex, effectData);
|
||||||
await this.settings.updateSource({
|
await this.updateMove({
|
||||||
[`${this.movePath}.effects`]: updatedEffects
|
[`${this.movePath}.effects`]: updatedEffects
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.settings.updateSource({ [`${this.actionsPath}.${id}`]: updatedMove });
|
await this.updateMove({ [`${this.actionsPath}.${id}`]: updatedMove });
|
||||||
this.move = foundry.utils.getProperty(this.settings, this.movePath);
|
|
||||||
this.render();
|
this.render();
|
||||||
return updatedEffects;
|
return updatedEffects;
|
||||||
}).render(true);
|
}).render(true);
|
||||||
|
|
@ -199,33 +198,36 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.settings.updateSource({
|
await this.updateMove({
|
||||||
[this.movePath]: {
|
[this.movePath]: {
|
||||||
effects: move.effects.filter(x => x.id !== id),
|
effects: move.effects.filter(x => x.id !== id),
|
||||||
actions: move.actions
|
actions: move.actions
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await this.settings.updateSource({ [`${this.actionsPath}.-=${target.dataset.id}`]: null });
|
await this.updateMove({ [`${this.actionsPath}.-=${target.dataset.id}`]: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.move = foundry.utils.getProperty(this.settings, this.movePath);
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
static async addEffect(_, target) {
|
static async addEffect() {
|
||||||
const currentEffects = foundry.utils.getProperty(this.settings, `${this.movePath}.effects`);
|
const currentEffects = foundry.utils.getProperty(this.settings, `${this.movePath}.effects`);
|
||||||
await this.settings.updateSource({
|
|
||||||
|
await this.updateMove({
|
||||||
[`${this.movePath}.effects`]: [
|
[`${this.movePath}.effects`]: [
|
||||||
...currentEffects,
|
...currentEffects,
|
||||||
game.system.api.data.activeEffects.BaseEffect.getDefaultObject()
|
game.system.api.data.activeEffects.BaseEffect.getDefaultObject()
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
this.move = foundry.utils.getProperty(this.settings, this.movePath);
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateMove(update) {
|
||||||
|
await this.settings.updateSource(update);
|
||||||
|
this.move = foundry.utils.getProperty(this.settings, this.movePath);
|
||||||
|
}
|
||||||
|
|
||||||
static resetMoves() {}
|
static resetMoves() {}
|
||||||
|
|
||||||
_filterTabs(tabs) {
|
_filterTabs(tabs) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export * as actors from './actors/_module.mjs';
|
export * as actors from './actors/_module.mjs';
|
||||||
export * as api from './api/_modules.mjs';
|
export * as api from './api/_modules.mjs';
|
||||||
export * as items from './items/_module.mjs';
|
export * as items from './items/_module.mjs';
|
||||||
|
export * as rollTables from './rollTables/_module.mjs';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import DHBaseActorSheet from '../api/base-actor.mjs';
|
import DHBaseActorSheet from '../api/base-actor.mjs';
|
||||||
import DhpDeathMove from '../../dialogs/deathMove.mjs';
|
import DhDeathMove from '../../dialogs/deathMove.mjs';
|
||||||
import { abilities } from '../../../config/actorConfig.mjs';
|
import { abilities } from '../../../config/actorConfig.mjs';
|
||||||
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
|
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
|
||||||
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
|
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
|
||||||
|
|
@ -27,6 +27,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
makeDeathMove: CharacterSheet.#makeDeathMove,
|
makeDeathMove: CharacterSheet.#makeDeathMove,
|
||||||
levelManagement: CharacterSheet.#levelManagement,
|
levelManagement: CharacterSheet.#levelManagement,
|
||||||
viewLevelups: CharacterSheet.#viewLevelups,
|
viewLevelups: CharacterSheet.#viewLevelups,
|
||||||
|
resetCharacter: CharacterSheet.#resetCharacter,
|
||||||
toggleEquipItem: CharacterSheet.#toggleEquipItem,
|
toggleEquipItem: CharacterSheet.#toggleEquipItem,
|
||||||
toggleResourceDice: CharacterSheet.#toggleResourceDice,
|
toggleResourceDice: CharacterSheet.#toggleResourceDice,
|
||||||
handleResourceDice: CharacterSheet.#handleResourceDice,
|
handleResourceDice: CharacterSheet.#handleResourceDice,
|
||||||
|
|
@ -42,6 +43,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
icon: 'fa-solid fa-angles-up',
|
icon: 'fa-solid fa-angles-up',
|
||||||
label: 'DAGGERHEART.ACTORS.Character.viewLevelups',
|
label: 'DAGGERHEART.ACTORS.Character.viewLevelups',
|
||||||
action: 'viewLevelups'
|
action: 'viewLevelups'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'fa-solid fa-arrow-rotate-left',
|
||||||
|
label: 'DAGGERHEART.ACTORS.Character.resetCharacter',
|
||||||
|
action: 'resetCharacter'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -220,13 +226,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
async _preparePartContext(partId, context, options) {
|
async _preparePartContext(partId, context, options) {
|
||||||
context = await super._preparePartContext(partId, context, options);
|
context = await super._preparePartContext(partId, context, options);
|
||||||
switch (partId) {
|
switch (partId) {
|
||||||
case 'header':
|
|
||||||
const { playerCanEditSheet, levelupAuto } = game.settings.get(
|
|
||||||
CONFIG.DH.id,
|
|
||||||
CONFIG.DH.SETTINGS.gameSettings.Automation
|
|
||||||
);
|
|
||||||
context.showSettings = game.user.isGM || !levelupAuto || (levelupAuto && playerCanEditSheet);
|
|
||||||
break;
|
|
||||||
case 'loadout':
|
case 'loadout':
|
||||||
await this._prepareLoadoutContext(context, options);
|
await this._prepareLoadoutContext(context, options);
|
||||||
break;
|
break;
|
||||||
|
|
@ -666,12 +665,19 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
new LevelupViewMode(this.document).render({ force: true });
|
new LevelupViewMode(this.document).render({ force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the character data and removes all embedded documents.
|
||||||
|
*/
|
||||||
|
static async #resetCharacter() {
|
||||||
|
new game.system.api.applications.dialogs.CharacterResetDialog(this.document).render({ force: true });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the Death Move interface for the character.
|
* Opens the Death Move interface for the character.
|
||||||
* @type {ApplicationClickAction}
|
* @type {ApplicationClickAction}
|
||||||
*/
|
*/
|
||||||
static async #makeDeathMove() {
|
static async #makeDeathMove() {
|
||||||
await new DhpDeathMove(this.document).render({ force: true });
|
await new DhDeathMove(this.document).render({ force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -728,9 +734,9 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
/* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */
|
/* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */
|
||||||
const costResources = result.costs
|
const costResources =
|
||||||
.filter(x => x.enabled)
|
result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) ||
|
||||||
.map(cost => ({ ...cost, value: -cost.value, total: -cost.total }));
|
{};
|
||||||
config.resourceUpdates.addResources(costResources);
|
config.resourceUpdates.addResources(costResources);
|
||||||
await config.resourceUpdates.updateResources();
|
await config.resourceUpdates.updateResources();
|
||||||
}
|
}
|
||||||
|
|
@ -956,6 +962,18 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onDropItem(event, item) {
|
async _onDropItem(event, item) {
|
||||||
|
const setupCriticalItemTypes = ['class', 'subclass', 'ancestry', 'community'];
|
||||||
|
if (this.document.system.needsCharacterSetup && setupCriticalItemTypes.includes(item.type)) {
|
||||||
|
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
||||||
|
window: {
|
||||||
|
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.CharacterCreation.setupSkipTitle')
|
||||||
|
},
|
||||||
|
content: game.i18n.localize('DAGGERHEART.APPLICATIONS.CharacterCreation.setupSkipContent')
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.document.uuid === item.parent?.uuid) {
|
if (this.document.uuid === item.parent?.uuid) {
|
||||||
return super._onDropItem(event, item);
|
return super._onDropItem(event, item);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,15 +38,6 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @inheritDoc */
|
|
||||||
async _onRender(context, options) {
|
|
||||||
await super._onRender(context, options);
|
|
||||||
|
|
||||||
this.element
|
|
||||||
.querySelector('.level-value')
|
|
||||||
?.addEventListener('change', event => this.document.updateLevel(Number(event.currentTarget.value)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
/* Application Clicks Actions */
|
/* Application Clicks Actions */
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
@ -71,10 +62,10 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
|
||||||
title: `${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}: ${this.actor.name}`,
|
title: `${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}: ${this.actor.name}`,
|
||||||
headerTitle: `Companion ${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}`,
|
headerTitle: `Companion ${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}`,
|
||||||
roll: {
|
roll: {
|
||||||
trait: partner.system.spellcastModifierTrait?.key
|
trait: partner.system.spellcastModifierTrait?.key,
|
||||||
|
companionRoll: true
|
||||||
},
|
},
|
||||||
hasRoll: true,
|
hasRoll: true
|
||||||
data: partner.getRollData()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await partner.diceRoll(config);
|
const result = await partner.diceRoll(config);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import DaggerheartMenu from '../../sidebar/tabs/daggerheartMenu.mjs';
|
||||||
import { socketEvent } from '../../../systemRegistration/socket.mjs';
|
import { socketEvent } from '../../../systemRegistration/socket.mjs';
|
||||||
import GroupRollDialog from '../../dialogs/group-roll-dialog.mjs';
|
import GroupRollDialog from '../../dialogs/group-roll-dialog.mjs';
|
||||||
import DhpActor from '../../../documents/actor.mjs';
|
import DhpActor from '../../../documents/actor.mjs';
|
||||||
import DHItem from '../../../documents/item.mjs';
|
|
||||||
|
|
||||||
export default class Party extends DHBaseActorSheet {
|
export default class Party extends DHBaseActorSheet {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
|
|
@ -269,15 +268,6 @@ export default class Party extends DHBaseActorSheet {
|
||||||
).render({ force: true });
|
).render({ force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the set of ContextMenu options for Consumable and Loot.
|
|
||||||
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
|
|
||||||
* @this {CharacterSheet}
|
|
||||||
* @protected
|
|
||||||
*/
|
|
||||||
static #getItemContextOptions() {
|
|
||||||
return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true });
|
|
||||||
}
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
/* Filter Tracking */
|
/* Filter Tracking */
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
|
||||||
|
|
@ -431,18 +431,18 @@ export default function DHApplicationMixin(Base) {
|
||||||
{
|
{
|
||||||
name: 'disableEffect',
|
name: 'disableEffect',
|
||||||
icon: 'fa-solid fa-lightbulb',
|
icon: 'fa-solid fa-lightbulb',
|
||||||
condition: target => {
|
condition: element => {
|
||||||
const doc = getDocFromElementSync(target);
|
const target = element.closest('[data-item-uuid]');
|
||||||
return doc && !doc.disabled;
|
return !target.dataset.disabled && target.dataset.itemType !== 'beastform';
|
||||||
},
|
},
|
||||||
callback: async target => (await getDocFromElement(target)).update({ disabled: true })
|
callback: async target => (await getDocFromElement(target)).update({ disabled: true })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'enableEffect',
|
name: 'enableEffect',
|
||||||
icon: 'fa-regular fa-lightbulb',
|
icon: 'fa-regular fa-lightbulb',
|
||||||
condition: target => {
|
condition: element => {
|
||||||
const doc = getDocFromElementSync(target);
|
const target = element.closest('[data-item-uuid]');
|
||||||
return doc && doc.disabled;
|
return target.dataset.disabled && target.dataset.itemType !== 'beastform';
|
||||||
},
|
},
|
||||||
callback: async target => (await getDocFromElement(target)).update({ disabled: false })
|
callback: async target => (await getDocFromElement(target)).update({ disabled: false })
|
||||||
}
|
}
|
||||||
|
|
@ -536,6 +536,10 @@ export default function DHApplicationMixin(Base) {
|
||||||
options.push({
|
options.push({
|
||||||
name: 'CONTROLS.CommonDelete',
|
name: 'CONTROLS.CommonDelete',
|
||||||
icon: 'fa-solid fa-trash',
|
icon: 'fa-solid fa-trash',
|
||||||
|
condition: element => {
|
||||||
|
const target = element.closest('[data-item-uuid]');
|
||||||
|
return target.dataset.itemType !== 'beastform';
|
||||||
|
},
|
||||||
callback: async (target, event) => {
|
callback: async (target, event) => {
|
||||||
const doc = await getDocFromElement(target);
|
const doc = await getDocFromElement(target);
|
||||||
if (event.shiftKey) return doc.delete();
|
if (event.shiftKey) return doc.delete();
|
||||||
|
|
@ -600,7 +604,7 @@ export default function DHApplicationMixin(Base) {
|
||||||
{
|
{
|
||||||
relativeTo: isAction ? doc.parent : doc,
|
relativeTo: isAction ? doc.parent : doc,
|
||||||
rollData: doc.getRollData?.(),
|
rollData: doc.getRollData?.(),
|
||||||
secrets: isAction ? doc.parent.isOwner : doc.isOwner
|
secrets: isAction ? doc.parent.parent.isOwner : doc.isOwner
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
|
||||||
],
|
],
|
||||||
dragDrop: [
|
dragDrop: [
|
||||||
{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null },
|
{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null },
|
||||||
{ dragSelector: ".currency[data-currency] .drag-handle", dropSelector: null }
|
{ dragSelector: '.currency[data-currency] .drag-handle', dropSelector: null }
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -92,7 +92,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
|
||||||
value: context.source.system.gold[key]
|
value: context.source.system.gold[key]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
context.inventory.hasCurrency = Object.values(context.inventory.currencies).some((c) => c.enabled);
|
context.inventory.hasCurrency = Object.values(context.inventory.currencies).some(c => c.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
|
|
@ -270,7 +270,9 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
|
||||||
currency
|
currency
|
||||||
});
|
});
|
||||||
if (quantity) {
|
if (quantity) {
|
||||||
originActor.update({ [`system.gold.${currency}`]: Math.max(0, originActor.system.gold[currency] - quantity) });
|
originActor.update({
|
||||||
|
[`system.gold.${currency}`]: Math.max(0, originActor.system.gold[currency] - quantity)
|
||||||
|
});
|
||||||
this.document.update({ [`system.gold.${currency}`]: this.document.system.gold[currency] + quantity });
|
this.document.update({ [`system.gold.${currency}`]: this.document.system.gold[currency] + quantity });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -292,6 +294,15 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
|
||||||
|
|
||||||
/* Handling transfer of inventoryItems */
|
/* Handling transfer of inventoryItems */
|
||||||
if (item.system.metadata.isInventoryItem) {
|
if (item.system.metadata.isInventoryItem) {
|
||||||
|
if (!this.document.testUserPermission(game.user, 'OWNER', { exact: true })) {
|
||||||
|
return ui.notifications.error(
|
||||||
|
game.i18n.format('DAGGERHEART.UI.Notifications.lackingItemTransferPermission', {
|
||||||
|
user: game.user.name,
|
||||||
|
target: this.document.name
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (item.system.metadata.isQuantifiable) {
|
if (item.system.metadata.isQuantifiable) {
|
||||||
const actorItem = originActor.items.get(data.originId);
|
const actorItem = originActor.items.get(data.originId);
|
||||||
const quantityTransfered = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
|
const quantityTransfered = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
|
||||||
|
|
@ -300,14 +311,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (quantityTransfered) {
|
if (quantityTransfered) {
|
||||||
if (quantityTransfered === actorItem.system.quantity) {
|
|
||||||
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
|
|
||||||
} else {
|
|
||||||
await actorItem.update({
|
|
||||||
'system.quantity': actorItem.system.quantity - quantityTransfered
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingItem = this.document.items.find(x => itemIsIdentical(x, item));
|
const existingItem = this.document.items.find(x => itemIsIdentical(x, item));
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
await existingItem.update({
|
await existingItem.update({
|
||||||
|
|
@ -325,10 +328,18 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (quantityTransfered === actorItem.system.quantity) {
|
||||||
|
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
|
||||||
|
} else {
|
||||||
|
await actorItem.update({
|
||||||
|
'system.quantity': actorItem.system.quantity - quantityTransfered
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
|
|
||||||
await this.document.createEmbeddedDocuments('Item', [item.toObject()]);
|
await this.document.createEmbeddedDocuments('Item', [item.toObject()]);
|
||||||
|
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -339,7 +350,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
|
||||||
*/
|
*/
|
||||||
async _onDragStart(event) {
|
async _onDragStart(event) {
|
||||||
// Handle drag/dropping currencies
|
// Handle drag/dropping currencies
|
||||||
const currencyEl = event.currentTarget.closest(".currency[data-currency]");
|
const currencyEl = event.currentTarget.closest('.currency[data-currency]');
|
||||||
if (currencyEl) {
|
if (currencyEl) {
|
||||||
const currency = currencyEl.dataset.currency;
|
const currency = currencyEl.dataset.currency;
|
||||||
const data = { type: 'Currency', currency, originActor: this.document.uuid };
|
const data = { type: 'Currency', currency, originActor: this.document.uuid };
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
export default function ItemAttachmentSheet(Base) {
|
export default function ItemAttachmentSheet(Base) {
|
||||||
return class extends Base {
|
return class extends Base {
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
...super.DEFAULT_OPTIONS,
|
|
||||||
dragDrop: [
|
dragDrop: [
|
||||||
...(super.DEFAULT_OPTIONS.dragDrop || []),
|
...(super.DEFAULT_OPTIONS.dragDrop || []),
|
||||||
{ dragSelector: null, dropSelector: '.attachments-section' }
|
{ dragSelector: null, dropSelector: '.attachments-section' }
|
||||||
|
|
|
||||||
1
module/applications/sheets/rollTables/_module.mjs
Normal file
1
module/applications/sheets/rollTables/_module.mjs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as RollTableSheet } from './rollTable.mjs';
|
||||||
191
module/applications/sheets/rollTables/rollTable.mjs
Normal file
191
module/applications/sheets/rollTables/rollTable.mjs
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
export default class DhRollTableSheet extends foundry.applications.sheets.RollTableSheet {
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
...super.DEFAULT_OPTIONS,
|
||||||
|
actions: {
|
||||||
|
changeMode: DhRollTableSheet.#onChangeMode,
|
||||||
|
drawResult: DhRollTableSheet.#onDrawResult,
|
||||||
|
resetResults: DhRollTableSheet.#onResetResults,
|
||||||
|
addFormula: DhRollTableSheet.#addFormula,
|
||||||
|
removeFormula: DhRollTableSheet.#removeFormula
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static buildParts() {
|
||||||
|
const { footer, header, sheet, results, ...parts } = super.PARTS;
|
||||||
|
return {
|
||||||
|
sheet: {
|
||||||
|
...sheet,
|
||||||
|
template: 'systems/daggerheart/templates/sheets/rollTable/sheet.hbs'
|
||||||
|
},
|
||||||
|
header: { template: 'systems/daggerheart/templates/sheets/rollTable/header.hbs' },
|
||||||
|
...parts,
|
||||||
|
results: {
|
||||||
|
template: 'systems/daggerheart/templates/sheets/rollTable/results.hbs',
|
||||||
|
templates: ['templates/sheets/roll-table/result-details.hbs'],
|
||||||
|
scrollable: ['table[data-results] tbody']
|
||||||
|
},
|
||||||
|
summary: { template: 'systems/daggerheart/templates/sheets/rollTable/summary.hbs' },
|
||||||
|
footer
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static PARTS = DhRollTableSheet.buildParts();
|
||||||
|
|
||||||
|
async _preRender(context, options) {
|
||||||
|
await super._preRender(context, options);
|
||||||
|
|
||||||
|
if (!options.internalRefresh)
|
||||||
|
this.daggerheartFlag = new game.system.api.data.DhRollTable(this.document.flags.daggerheart);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* root PART has a blank element on _attachPartListeners, so it cannot be used to set the eventListeners for the view mode */
|
||||||
|
async _onRender(context, options) {
|
||||||
|
super._onRender(context, options);
|
||||||
|
|
||||||
|
for (const element of this.element.querySelectorAll('.system-update-field'))
|
||||||
|
element.addEventListener('change', this.updateSystemField.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
async _preparePartContext(partId, context, options) {
|
||||||
|
context = await super._preparePartContext(partId, context, options);
|
||||||
|
|
||||||
|
switch (partId) {
|
||||||
|
case 'sheet':
|
||||||
|
context.altFormula = this.daggerheartFlag.altFormula;
|
||||||
|
context.usesAltFormula = Object.keys(this.daggerheartFlag.altFormula).length > 0;
|
||||||
|
context.altFormulaOptions = {
|
||||||
|
'': { name: this.daggerheartFlag.formulaName },
|
||||||
|
...this.daggerheartFlag.altFormula
|
||||||
|
};
|
||||||
|
context.activeAltFormula = this.daggerheartFlag.activeAltFormula;
|
||||||
|
context.selectedFormula = this.daggerheartFlag.getActiveFormula(this.document.formula);
|
||||||
|
context.results = this.getExtendedResults(context.results);
|
||||||
|
break;
|
||||||
|
case 'header':
|
||||||
|
context.altFormula = this.daggerheartFlag.altFormula;
|
||||||
|
context.usesAltFormula = Object.keys(this.daggerheartFlag.altFormula).length > 0;
|
||||||
|
context.altFormulaOptions = {
|
||||||
|
'': { name: this.daggerheartFlag.formulaName },
|
||||||
|
...this.daggerheartFlag.altFormula
|
||||||
|
};
|
||||||
|
context.activeAltFormula = this.daggerheartFlag.activeAltFormula;
|
||||||
|
break;
|
||||||
|
case 'summary':
|
||||||
|
context.systemFields = this.daggerheartFlag.schema.fields;
|
||||||
|
context.altFormula = this.daggerheartFlag.altFormula;
|
||||||
|
context.formulaName = this.daggerheartFlag.formulaName;
|
||||||
|
break;
|
||||||
|
case 'results':
|
||||||
|
context.results = this.getExtendedResults(context.results);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
getExtendedResults(results) {
|
||||||
|
const bodyDarkMode = document.body.classList.contains('theme-dark');
|
||||||
|
const elementLightMode = this.element.classList.contains('theme-light');
|
||||||
|
const elementDarkMode = this.element.classList.contains('theme-dark');
|
||||||
|
const isDarkMode = elementDarkMode || (!elementLightMode && bodyDarkMode);
|
||||||
|
|
||||||
|
return results.map(x => ({
|
||||||
|
...x,
|
||||||
|
displayImg: isDarkMode && x.img === 'icons/svg/d20-black.svg' ? 'icons/svg/d20.svg' : x.img
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
/* Flag SystemData update methods */
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
async updateSystemField(event) {
|
||||||
|
const { dataset, value } = event.target;
|
||||||
|
await this.daggerheartFlag.updateSource({ [dataset.path]: value });
|
||||||
|
this.render({ internalRefresh: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
getSystemFlagUpdate() {
|
||||||
|
const deleteUpdate = Object.keys(this.document._source.flags.daggerheart?.altFormula ?? {}).reduce(
|
||||||
|
(acc, formulaKey) => {
|
||||||
|
if (!this.daggerheartFlag.altFormula[formulaKey]) acc.altFormula[`-=${formulaKey}`] = null;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ altFormula: {} }
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ['flags.daggerheart']: foundry.utils.mergeObject(this.daggerheartFlag.toObject(), deleteUpdate) };
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #addFormula() {
|
||||||
|
await this.daggerheartFlag.updateSource({
|
||||||
|
[`altFormula.${foundry.utils.randomID()}`]: game.system.api.data.DhRollTable.getDefaultFormula()
|
||||||
|
});
|
||||||
|
this.render({ internalRefresh: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #removeFormula(_event, target) {
|
||||||
|
await this.daggerheartFlag.updateSource({
|
||||||
|
[`altFormula.-=${target.dataset.key}`]: null
|
||||||
|
});
|
||||||
|
this.render({ internalRefresh: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
/* Extended RollTable methods */
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternate between view and edit modes.
|
||||||
|
* @this {RollTableSheet}
|
||||||
|
* @type {ApplicationClickAction}
|
||||||
|
*/
|
||||||
|
static async #onChangeMode() {
|
||||||
|
this.mode = this.isEditMode ? 'view' : 'edit';
|
||||||
|
await this.document.update(this.getSystemFlagUpdate());
|
||||||
|
await this.render({ internalRefresh: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
async _processSubmitData(event, form, submitData, options) {
|
||||||
|
/* RollTable sends an empty dummy event when swapping from view/edit first time */
|
||||||
|
if (Object.keys(submitData).length) {
|
||||||
|
if (!submitData.flags) submitData.flags = { daggerheart: {} };
|
||||||
|
submitData.flags.daggerheart = this.getSystemFlagUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
super._processSubmitData(event, form, submitData, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
static async #onResetResults() {
|
||||||
|
await this.document.update(this.getSystemFlagUpdate());
|
||||||
|
await this.document.resetResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roll and draw a TableResult.
|
||||||
|
* @this {RollTableSheet}
|
||||||
|
* @type {ApplicationClickAction}
|
||||||
|
*/
|
||||||
|
static async #onDrawResult(_event, button) {
|
||||||
|
if (this.form) await this.submit({ operation: { render: false } });
|
||||||
|
button.disabled = true;
|
||||||
|
const table = this.document;
|
||||||
|
|
||||||
|
await this.document.update(this.getSystemFlagUpdate());
|
||||||
|
|
||||||
|
/* Sending in the currently selectd activeFormula to table.roll to use as the formula */
|
||||||
|
const selectedFormula = this.daggerheartFlag.getActiveFormula(this.document.formula);
|
||||||
|
const tableRoll = await table.roll({ selectedFormula });
|
||||||
|
const draws = table.getResultsForRoll(tableRoll.roll.total);
|
||||||
|
if (draws.length > 0) {
|
||||||
|
if (game.settings.get('core', 'animateRollTable')) await this._animateRoll(draws);
|
||||||
|
await table.draw(tableRoll);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reenable the button if drawing with replacement since the draw won't trigger a sheet re-render
|
||||||
|
if (table.replacement) button.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -43,4 +43,54 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs.
|
||||||
event.dataTransfer.setDragImage(preview, w / 2, h / 2);
|
event.dataTransfer.setDragImage(preview, w / 2, h / 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getEntryContextOptions() {
|
||||||
|
const options = super._getEntryContextOptions();
|
||||||
|
options.push({
|
||||||
|
name: 'DAGGERHEART.UI.Sidebar.actorDirectory.duplicateToNewTier',
|
||||||
|
icon: `<i class="fa-solid fa-arrow-trend-up" inert></i>`,
|
||||||
|
condition: li => {
|
||||||
|
const actor = game.actors.get(li.dataset.entryId);
|
||||||
|
return actor?.type === 'adversary' && actor.system.type !== 'social';
|
||||||
|
},
|
||||||
|
callback: async li => {
|
||||||
|
const actor = game.actors.get(li.dataset.entryId);
|
||||||
|
if (!actor) throw new Error('Unexpected missing actor');
|
||||||
|
|
||||||
|
const tiers = [1, 2, 3, 4].filter(t => t !== actor.system.tier);
|
||||||
|
const content = document.createElement('div');
|
||||||
|
const select = document.createElement('select');
|
||||||
|
select.name = 'tier';
|
||||||
|
select.append(
|
||||||
|
...tiers.map(t => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = t;
|
||||||
|
option.textContent = game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${t}`);
|
||||||
|
return option;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
content.append(select);
|
||||||
|
|
||||||
|
const tier = await foundry.applications.api.Dialog.input({
|
||||||
|
classes: ['dh-style', 'dialog'],
|
||||||
|
window: { title: 'DAGGERHEART.UI.Sidebar.actorDirectory.pickTierTitle' },
|
||||||
|
content,
|
||||||
|
ok: {
|
||||||
|
label: 'Create Adversary',
|
||||||
|
callback: (event, button, dialog) => Number(button.form.elements.tier.value)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tier === actor.system.tier) {
|
||||||
|
ui.notifications.warn('This actor is already at this tier');
|
||||||
|
} else if (tier) {
|
||||||
|
const source = actor.system.adjustForTier(tier);
|
||||||
|
await Actor.create(source);
|
||||||
|
ui.notifications.info(`Tier ${tier} ${actor.name} created`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { refreshIsAllowed } from '../../../helpers/utils.mjs';
|
import { RefreshFeatures } from '../../../helpers/utils.mjs';
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
const { AbstractSidebarTab } = foundry.applications.sidebar;
|
const { AbstractSidebarTab } = foundry.applications.sidebar;
|
||||||
|
|
@ -54,73 +54,6 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRefreshables(types) {
|
|
||||||
const refreshedActors = {};
|
|
||||||
for (let actor of game.actors) {
|
|
||||||
if (['character', 'adversary'].includes(actor.type) && actor.prototypeToken.actorLink) {
|
|
||||||
const updates = {};
|
|
||||||
for (let item of actor.items) {
|
|
||||||
if (item.system.metadata?.hasResource && refreshIsAllowed(types, item.system.resource?.recovery)) {
|
|
||||||
if (!refreshedActors[actor.id])
|
|
||||||
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
|
|
||||||
refreshedActors[actor.id].refreshed.add(
|
|
||||||
game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[item.system.resource.recovery].label)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
|
|
||||||
|
|
||||||
const increasing =
|
|
||||||
item.system.resource.progression === CONFIG.DH.ITEM.itemResourceProgression.increasing.id;
|
|
||||||
updates[item.id].system = {
|
|
||||||
...updates[item.id].system,
|
|
||||||
'resource.value': increasing
|
|
||||||
? 0
|
|
||||||
: Roll.replaceFormulaData(item.system.resource.max, actor.getRollData())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (item.system.metadata?.hasActions) {
|
|
||||||
const refreshTypes = new Set();
|
|
||||||
const actions = item.system.actions.filter(action => {
|
|
||||||
if (refreshIsAllowed(types, action.uses.recovery)) {
|
|
||||||
refreshTypes.add(action.uses.recovery);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
if (actions.length === 0) continue;
|
|
||||||
|
|
||||||
if (!refreshedActors[actor.id])
|
|
||||||
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
|
|
||||||
refreshedActors[actor.id].refreshed.add(
|
|
||||||
...refreshTypes.map(type => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[type].label))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
|
|
||||||
|
|
||||||
updates[item.id].system = {
|
|
||||||
...updates[item.id].system,
|
|
||||||
...actions.reduce(
|
|
||||||
(acc, action) => {
|
|
||||||
acc.actions[action.id] = { 'uses.value': 0 };
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{ actions: updates[item.id].system.actions ?? {} }
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let key in updates) {
|
|
||||||
const update = updates[key];
|
|
||||||
await actor.items.get(key).update(update);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return refreshedActors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
/* Application Clicks Actions */
|
/* Application Clicks Actions */
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
@ -133,30 +66,9 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
|
||||||
|
|
||||||
static async #refreshActors() {
|
static async #refreshActors() {
|
||||||
const refreshKeys = Object.keys(this.refreshSelections).filter(key => this.refreshSelections[key].selected);
|
const refreshKeys = Object.keys(this.refreshSelections).filter(key => this.refreshSelections[key].selected);
|
||||||
await this.getRefreshables(refreshKeys);
|
await RefreshFeatures(refreshKeys);
|
||||||
const types = refreshKeys.map(x => this.refreshSelections[x].label).join(', ');
|
|
||||||
ui.notifications.info(
|
|
||||||
game.i18n.format('DAGGERHEART.UI.Notifications.gmMenuRefresh', {
|
|
||||||
types: `[${types}]`
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this.refreshSelections = DaggerheartMenu.defaultRefreshSelections();
|
this.refreshSelections = DaggerheartMenu.defaultRefreshSelections();
|
||||||
|
|
||||||
const cls = getDocumentClass('ChatMessage');
|
|
||||||
const msg = {
|
|
||||||
user: game.user.id,
|
|
||||||
content: await foundry.applications.handlebars.renderTemplate(
|
|
||||||
'systems/daggerheart/templates/ui/chat/refreshMessage.hbs',
|
|
||||||
{
|
|
||||||
types: types
|
|
||||||
}
|
|
||||||
),
|
|
||||||
title: game.i18n.localize('DAGGERHEART.UI.Chat.refreshMessage.title'),
|
|
||||||
speaker: cls.getSpeaker()
|
|
||||||
};
|
|
||||||
|
|
||||||
cls.create(msg);
|
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
||||||
html.querySelectorAll('.group-roll-header-expand-section').forEach(element =>
|
html.querySelectorAll('.group-roll-header-expand-section').forEach(element =>
|
||||||
element.addEventListener('click', this.groupRollExpandSection)
|
element.addEventListener('click', this.groupRollExpandSection)
|
||||||
);
|
);
|
||||||
|
html.querySelectorAll('.risk-it-all-button').forEach(element =>
|
||||||
|
element.addEventListener('click', event => this.riskItAllClearStressAndHitPoints(event, data))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
setupHooks() {
|
setupHooks() {
|
||||||
|
|
@ -94,15 +97,17 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
||||||
|
|
||||||
/** Ensure the chat theme inherits the interface theme */
|
/** Ensure the chat theme inherits the interface theme */
|
||||||
_replaceHTML(result, content, options) {
|
_replaceHTML(result, content, options) {
|
||||||
const themedElement = result.log?.querySelector(".chat-log");
|
const themedElement = result.log?.querySelector('.chat-log');
|
||||||
themedElement?.classList.remove("themed", "theme-light", "theme-dark");
|
themedElement?.classList.remove('themed', 'theme-light', 'theme-dark');
|
||||||
super._replaceHTML(result, content, options);
|
super._replaceHTML(result, content, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Remove chat log theme from notifications area */
|
/** Remove chat log theme from notifications area */
|
||||||
async _onFirstRender(result, content) {
|
async _onFirstRender(result, content) {
|
||||||
await super._onFirstRender(result, content);
|
await super._onFirstRender(result, content);
|
||||||
document.querySelector("#chat-notifications .chat-log")?.classList.remove("themed", "theme-light", "theme-dark")
|
document
|
||||||
|
.querySelector('#chat-notifications .chat-log')
|
||||||
|
?.classList.remove('themed', 'theme-light', 'theme-dark');
|
||||||
}
|
}
|
||||||
|
|
||||||
async onRollSimple(event, message) {
|
async onRollSimple(event, message) {
|
||||||
|
|
@ -383,4 +388,10 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
||||||
});
|
});
|
||||||
event.target.closest('.group-roll-section').querySelector('.group-roll-content').classList.toggle('closed');
|
event.target.closest('.group-roll-section').querySelector('.group-roll-content').classList.toggle('closed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async riskItAllClearStressAndHitPoints(event, data) {
|
||||||
|
const resourceValue = event.target.dataset.resourceValue;
|
||||||
|
const actor = game.actors.get(event.target.dataset.actorId);
|
||||||
|
new game.system.api.applications.dialogs.RiskItAllDialog(actor, resourceValue).render({ force: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
|
||||||
this.combats
|
this.combats
|
||||||
.find(x => x.active)
|
.find(x => x.active)
|
||||||
?.system?.extendedBattleToggles?.reduce((acc, toggle) => (acc ?? 0) + toggle.category, null) ?? null;
|
?.system?.extendedBattleToggles?.reduce((acc, toggle) => (acc ?? 0) + toggle.category, null) ?? null;
|
||||||
const maxBP = CONFIG.DH.ENCOUNTER.BaseBPPerEncounter(context.characters.length) + modifierBP;
|
const maxBP = CONFIG.DH.ENCOUNTER.BaseBPPerEncounter(context.allCharacters.length) + modifierBP;
|
||||||
const currentBP = AdversaryBPPerEncounter(context.adversaries, context.characters);
|
const currentBP = AdversaryBPPerEncounter(context.adversaries, context.allCharacters);
|
||||||
|
|
||||||
Object.assign(context, {
|
Object.assign(context, {
|
||||||
fear: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
|
fear: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
|
||||||
|
|
@ -73,9 +73,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
|
||||||
Object.assign(context, {
|
Object.assign(context, {
|
||||||
actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens,
|
actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens,
|
||||||
adversaries,
|
adversaries,
|
||||||
characters: characters
|
allCharacters: characters,
|
||||||
?.filter(x => !x.isNPC)
|
characters: characters.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0),
|
||||||
.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0),
|
|
||||||
spotlightRequests
|
spotlightRequests
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,8 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleHidden(token, focused) {
|
toggleHidden(token, focused) {
|
||||||
|
if (!this.element) return;
|
||||||
|
|
||||||
const effects = DhEffectsDisplay.getTokenEffects(focused ? token : null);
|
const effects = DhEffectsDisplay.getTokenEffects(focused ? token : null);
|
||||||
this.element.hidden = effects.length === 0;
|
this.element.hidden = effects.length === 0;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,6 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
|
||||||
position: {
|
position: {
|
||||||
width: 222,
|
width: 222,
|
||||||
height: 222
|
height: 222
|
||||||
// top: "200px",
|
|
||||||
// left: "120px"
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -66,7 +64,7 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
|
||||||
max = this.maxFear,
|
max = this.maxFear,
|
||||||
percent = (current / max) * 100,
|
percent = (current / max) * 100,
|
||||||
isGM = game.user.isGM;
|
isGM = game.user.isGM;
|
||||||
// Return the data for rendering
|
|
||||||
return { display, current, max, percent, isGM };
|
return { display, current, max, percent, isGM };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -17,6 +19,15 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig;
|
this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig;
|
||||||
this.presets = {};
|
this.presets = {};
|
||||||
this.compendiumBrowserTypeKey = 'compendiumBrowserDefault';
|
this.compendiumBrowserTypeKey = 'compendiumBrowserDefault';
|
||||||
|
|
||||||
|
this.setupHooks = Hooks.on(socketEvent.Refresh, ({ refreshType }) => {
|
||||||
|
if (refreshType === RefreshType.CompendiumBrowser) {
|
||||||
|
if (this.rendered) {
|
||||||
|
this.render();
|
||||||
|
this.loadItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
|
|
@ -35,7 +46,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
selectFolder: this.selectFolder,
|
selectFolder: this.selectFolder,
|
||||||
expandContent: this.expandContent,
|
expandContent: this.expandContent,
|
||||||
resetFilters: this.resetFilters,
|
resetFilters: this.resetFilters,
|
||||||
sortList: this.sortList
|
sortList: this.sortList,
|
||||||
|
openSettings: this.openSettings
|
||||||
},
|
},
|
||||||
position: {
|
position: {
|
||||||
left: 100,
|
left: 100,
|
||||||
|
|
@ -157,6 +169,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
context.formatChoices = this.formatChoices;
|
context.formatChoices = this.formatChoices;
|
||||||
context.items = this.items;
|
context.items = this.items;
|
||||||
context.presets = this.presets;
|
context.presets = this.presets;
|
||||||
|
context.isGM = game.user.isGM;
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,6 +228,10 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
loadItems() {
|
loadItems() {
|
||||||
let loadTimeout = this.toggleLoader(true);
|
let loadTimeout = this.toggleLoader(true);
|
||||||
|
|
||||||
|
const browserSettings = game.settings.get(
|
||||||
|
CONFIG.DH.id,
|
||||||
|
CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings
|
||||||
|
);
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
|
||||||
game.packs.forEach(pack => {
|
game.packs.forEach(pack => {
|
||||||
|
|
@ -227,7 +245,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
|
||||||
Promise.all(promises).then(async result => {
|
Promise.all(promises).then(async result => {
|
||||||
this.items = ItemBrowser.sortBy(
|
this.items = ItemBrowser.sortBy(
|
||||||
result.flatMap(r => r),
|
result.flatMap(r => r).filter(r => !browserSettings.isEntryExcluded.bind(browserSettings)(r)),
|
||||||
'name'
|
'name'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -512,6 +530,22 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
itemListContainer.replaceChildren(...newOrder);
|
itemListContainer.replaceChildren(...newOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async openSettings() {
|
||||||
|
const settingsUpdated = await game.system.api.applications.dialogs.CompendiumBrowserSettingsDialog.configure();
|
||||||
|
if (settingsUpdated) {
|
||||||
|
if (this.rendered) {
|
||||||
|
this.render();
|
||||||
|
this.loadItems();
|
||||||
|
}
|
||||||
|
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
||||||
|
action: socketEvent.Refresh,
|
||||||
|
data: {
|
||||||
|
refreshType: RefreshType.CompendiumBrowser
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_createDragProcess() {
|
_createDragProcess() {
|
||||||
new foundry.applications.ux.DragDrop.implementation({
|
new foundry.applications.ux.DragDrop.implementation({
|
||||||
dragSelector: '.item-container',
|
dragSelector: '.item-container',
|
||||||
|
|
@ -571,4 +605,9 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
headerActions.append(button);
|
headerActions.append(button);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async close(options = {}) {
|
||||||
|
Hooks.off(socketEvent.Refresh, this.setupHooks);
|
||||||
|
await super.close(options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,8 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi
|
||||||
if (scene.flags.daggerheart.sceneEnvironments[0] !== environment.uuid) {
|
if (scene.flags.daggerheart.sceneEnvironments[0] !== environment.uuid) {
|
||||||
const newEnvironments = scene.flags.daggerheart.sceneEnvironments;
|
const newEnvironments = scene.flags.daggerheart.sceneEnvironments;
|
||||||
const newFirst = newEnvironments.splice(
|
const newFirst = newEnvironments.splice(
|
||||||
newEnvironments.findIndex(x => x === environment.uuid)
|
newEnvironments.findIndex(x => x === environment.uuid),
|
||||||
|
1
|
||||||
)[0];
|
)[0];
|
||||||
newEnvironments.unshift(newFirst);
|
newEnvironments.unshift(newFirst);
|
||||||
emitAsGM(
|
emitAsGM(
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,9 @@ export default class DhMeasuredTemplate extends foundry.canvas.placeables.Measur
|
||||||
|
|
||||||
static getRangeLabels(distanceValue, settings) {
|
static getRangeLabels(distanceValue, settings) {
|
||||||
let result = { distance: distanceValue, units: '' };
|
let result = { distance: distanceValue, units: '' };
|
||||||
const sceneRangeMeasurement = canvas.scene.flags.daggerheart?.rangeMeasurement;
|
if (!settings.enabled) return result;
|
||||||
|
|
||||||
|
const sceneRangeMeasurement = canvas.scene.flags.daggerheart?.rangeMeasurement;
|
||||||
const { disable, custom } = CONFIG.DH.GENERAL.sceneRangeMeasurementSetting;
|
const { disable, custom } = CONFIG.DH.GENERAL.sceneRangeMeasurementSetting;
|
||||||
if (sceneRangeMeasurement?.setting === disable.id) {
|
if (sceneRangeMeasurement?.setting === disable.id) {
|
||||||
result.distance = distanceValue;
|
result.distance = distanceValue;
|
||||||
|
|
@ -27,31 +28,9 @@ export default class DhMeasuredTemplate extends foundry.canvas.placeables.Measur
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const melee = sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.melee : settings.melee;
|
const ranges = sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement : settings;
|
||||||
const veryClose =
|
const distanceKey = ['melee', 'veryClose', 'close', 'far'].find(r => ranges[r] >= distanceValue);
|
||||||
sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.veryClose : settings.veryClose;
|
result.distance = game.i18n.localize(`DAGGERHEART.CONFIG.Range.${distanceKey ?? 'veryFar'}.name`);
|
||||||
const close = sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.close : settings.close;
|
|
||||||
const far = sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.far : settings.far;
|
|
||||||
if (distanceValue <= melee) {
|
|
||||||
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.melee.name');
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (distanceValue <= veryClose) {
|
|
||||||
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.veryClose.name');
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (distanceValue <= close) {
|
|
||||||
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.close.name');
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (distanceValue <= far) {
|
|
||||||
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.far.name');
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (distanceValue > far) {
|
|
||||||
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.veryFar.name');
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import DhMeasuredTemplate from './measuredTemplate.mjs';
|
||||||
|
|
||||||
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
async _draw(options) {
|
async _draw(options) {
|
||||||
|
|
@ -52,30 +54,111 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
||||||
if (this === target) return 0;
|
if (this === target) return 0;
|
||||||
|
|
||||||
const originPoint = this.center;
|
const originPoint = this.center;
|
||||||
const destinationPoint = target.center;
|
const targetPoint = target.center;
|
||||||
|
const thisBounds = this.bounds;
|
||||||
|
const targetBounds = target.bounds;
|
||||||
|
const adjacencyBuffer = canvas.grid.distance * 1.75; // handles diagonals with one square elevation difference
|
||||||
|
|
||||||
|
// Figure out the elevation difference.
|
||||||
|
// This intends to return "grid distance" for adjacent ones, so we add that number if not overlapping.
|
||||||
|
const sizePerUnit = canvas.grid.size / canvas.grid.distance;
|
||||||
|
const thisHeight = Math.max(thisBounds.width, thisBounds.height) / sizePerUnit;
|
||||||
|
const targetHeight = Math.max(targetBounds.width, targetBounds.height) / sizePerUnit;
|
||||||
|
const thisElevation = [this.document.elevation, this.document.elevation + thisHeight];
|
||||||
|
const targetElevation = [target.document.elevation, target.document.elevation + targetHeight];
|
||||||
|
const isSameAltitude =
|
||||||
|
thisElevation[0] < targetElevation[1] && // bottom of this must be at or below the top of target
|
||||||
|
thisElevation[1] > targetElevation[0]; // top of this must be at or above the bottom of target
|
||||||
|
const [lower, higher] = [targetElevation, thisElevation].sort((a, b) => a[1] - b[1]);
|
||||||
|
const elevation = isSameAltitude ? 0 : higher[0] - lower[1] + canvas.grid.distance;
|
||||||
|
|
||||||
// Compute for gridless. This version returns circular edge to edge + grid distance,
|
// Compute for gridless. This version returns circular edge to edge + grid distance,
|
||||||
// so that tokens that are touching return 5.
|
// so that tokens that are touching return 5.
|
||||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||||
const boundsCorrection = canvas.grid.distance / canvas.grid.size;
|
const boundsCorrection = canvas.grid.distance / canvas.grid.size;
|
||||||
const originRadius = (this.bounds.width * boundsCorrection) / 2;
|
const originRadius = (thisBounds.width * boundsCorrection) / 2;
|
||||||
const targetRadius = (target.bounds.width * boundsCorrection) / 2;
|
const targetRadius = (targetBounds.width * boundsCorrection) / 2;
|
||||||
const distance = canvas.grid.measurePath([originPoint, destinationPoint]).distance;
|
const measuredDistance = canvas.grid.measurePath([
|
||||||
return distance - originRadius - targetRadius + canvas.grid.distance;
|
{ ...originPoint, elevation: 0 },
|
||||||
|
{ ...targetPoint, elevation }
|
||||||
|
]).distance;
|
||||||
|
const distance = Math.floor(measuredDistance - originRadius - targetRadius + canvas.grid.distance);
|
||||||
|
return Math.min(distance, distance > adjacencyBuffer ? Infinity : canvas.grid.distance);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute what the closest grid space of each token is, then compute that distance
|
// Compute what the closest grid space of each token is, then compute that distance
|
||||||
const originEdge = this.#getEdgeBoundary(this.bounds, originPoint, destinationPoint);
|
const originEdge = this.#getEdgeBoundary(thisBounds, originPoint, targetPoint);
|
||||||
const targetEdge = this.#getEdgeBoundary(target.bounds, originPoint, destinationPoint);
|
const targetEdge = this.#getEdgeBoundary(targetBounds, originPoint, targetPoint);
|
||||||
const adjustedOriginPoint = canvas.grid.getTopLeftPoint({
|
const adjustedOriginPoint = originEdge
|
||||||
x: originEdge.x + Math.sign(originPoint.x - originEdge.x),
|
? canvas.grid.getTopLeftPoint({
|
||||||
y: originEdge.y + Math.sign(originPoint.y - originEdge.y)
|
x: originEdge.x + Math.sign(originPoint.x - originEdge.x),
|
||||||
});
|
y: originEdge.y + Math.sign(originPoint.y - originEdge.y)
|
||||||
const adjustDestinationPoint = canvas.grid.getTopLeftPoint({
|
})
|
||||||
x: targetEdge.x + Math.sign(destinationPoint.x - targetEdge.x),
|
: originPoint;
|
||||||
y: targetEdge.y + Math.sign(destinationPoint.y - targetEdge.y)
|
const adjustDestinationPoint = targetEdge
|
||||||
});
|
? canvas.grid.getTopLeftPoint({
|
||||||
return canvas.grid.measurePath([adjustedOriginPoint, adjustDestinationPoint]).distance;
|
x: targetEdge.x + Math.sign(targetPoint.x - targetEdge.x),
|
||||||
|
y: targetEdge.y + Math.sign(targetPoint.y - targetEdge.y)
|
||||||
|
})
|
||||||
|
: targetPoint;
|
||||||
|
const distance = canvas.grid.measurePath([
|
||||||
|
{ ...adjustedOriginPoint, elevation: 0 },
|
||||||
|
{ ...adjustDestinationPoint, elevation }
|
||||||
|
]).distance;
|
||||||
|
return Math.min(distance, distance > adjacencyBuffer ? Infinity : canvas.grid.distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onHoverIn(event, options) {
|
||||||
|
super._onHoverIn(event, options);
|
||||||
|
|
||||||
|
// Check if the setting is enabled
|
||||||
|
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).showTokenDistance;
|
||||||
|
if (setting === 'never' || (setting === 'encounters' && !game.combat?.started)) return;
|
||||||
|
|
||||||
|
// Check if this token isn't invisible and is actually being hovered
|
||||||
|
const isTokenValid =
|
||||||
|
this.visible &&
|
||||||
|
this.hover &&
|
||||||
|
!this.isPreview &&
|
||||||
|
!this.document.isSecret &&
|
||||||
|
!this.controlled &&
|
||||||
|
!this.animation;
|
||||||
|
if (!isTokenValid) return;
|
||||||
|
|
||||||
|
// Ensure we have a single controlled token
|
||||||
|
const originToken = canvas.tokens.controlled[0];
|
||||||
|
if (!originToken || canvas.tokens.controlled.length > 1) return;
|
||||||
|
|
||||||
|
// Determine the actual range
|
||||||
|
const ranges = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement;
|
||||||
|
const distanceResult = DhMeasuredTemplate.getRangeLabels(originToken.distanceTo(this), ranges);
|
||||||
|
const distanceLabel = `${distanceResult.distance} ${distanceResult.units}`.trim();
|
||||||
|
|
||||||
|
// Create the element
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.id = 'token-hover-distance';
|
||||||
|
element.classList.add('waypoint-label', 'last');
|
||||||
|
const ruler = document.createElement('i');
|
||||||
|
ruler.classList.add('fa-solid', 'fa-ruler');
|
||||||
|
element.appendChild(ruler);
|
||||||
|
const labelEl = document.createElement('span');
|
||||||
|
labelEl.classList.add('total-measurement');
|
||||||
|
labelEl.textContent = distanceLabel;
|
||||||
|
element.appendChild(labelEl);
|
||||||
|
|
||||||
|
// Position the element and add to the DOM
|
||||||
|
const center = this.getCenterPoint();
|
||||||
|
element.style.setProperty('--transformY', 'calc(-100% - 10px)');
|
||||||
|
element.style.setProperty('--position-y', `${this.y}px`);
|
||||||
|
element.style.setProperty('--position-x', `${center.x}px`);
|
||||||
|
element.style.setProperty('--ui-scale', String(canvas.dimensions.uiScale));
|
||||||
|
document.querySelector('#token-hover-distance')?.remove();
|
||||||
|
document.querySelector('#measurement').appendChild(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onHoverOut(...args) {
|
||||||
|
super._onHoverOut(...args);
|
||||||
|
document.querySelector('#token-hover-distance')?.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the point at which a line starting at origin and ending at destination intersects the edge of the bounds */
|
/** Returns the point at which a line starting at origin and ending at destination intersects the edge of the bounds */
|
||||||
|
|
@ -100,11 +183,6 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Tests if the token is at least adjacent with another, with some leeway for diagonals */
|
|
||||||
isAdjacentWith(token) {
|
|
||||||
return this.distanceTo(token) <= canvas.grid.distance * 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
_drawBar(number, bar, data) {
|
_drawBar(number, bar, data) {
|
||||||
const val = Number(data.value);
|
const val = Number(data.value);
|
||||||
|
|
|
||||||
|
|
@ -494,3 +494,275 @@ export const subclassFeatureLabels = {
|
||||||
2: 'DAGGERHEART.ITEMS.DomainCard.specializationTitle',
|
2: 'DAGGERHEART.ITEMS.DomainCard.specializationTitle',
|
||||||
3: 'DAGGERHEART.ITEMS.DomainCard.masteryTitle'
|
3: 'DAGGERHEART.ITEMS.DomainCard.masteryTitle'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} TierData
|
||||||
|
* @property {number} difficulty
|
||||||
|
* @property {number} majorThreshold
|
||||||
|
* @property {number} severeThreshold
|
||||||
|
* @property {number} hp
|
||||||
|
* @property {number} stress
|
||||||
|
* @property {number} attack
|
||||||
|
* @property {number[]} damage
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Record<string, Record<2 | 3 | 4, TierData>}
|
||||||
|
* Scaling data used to change an adversary's tier. Each rank is applied incrementally.
|
||||||
|
*/
|
||||||
|
export const adversaryScalingData = {
|
||||||
|
bruiser: {
|
||||||
|
2: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 5,
|
||||||
|
severeThreshold: 10,
|
||||||
|
hp: 1,
|
||||||
|
stress: 2,
|
||||||
|
attack: 2,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 7,
|
||||||
|
severeThreshold: 15,
|
||||||
|
hp: 1,
|
||||||
|
stress: 0,
|
||||||
|
attack: 2,
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 12,
|
||||||
|
severeThreshold: 25,
|
||||||
|
hp: 1,
|
||||||
|
stress: 0,
|
||||||
|
attack: 2,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
horde: {
|
||||||
|
2: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 5,
|
||||||
|
severeThreshold: 8,
|
||||||
|
hp: 2,
|
||||||
|
stress: 0,
|
||||||
|
attack: 0,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 5,
|
||||||
|
severeThreshold: 12,
|
||||||
|
hp: 0,
|
||||||
|
stress: 1,
|
||||||
|
attack: 1,
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 10,
|
||||||
|
severeThreshold: 15,
|
||||||
|
hp: 2,
|
||||||
|
stress: 0,
|
||||||
|
attack: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leader: {
|
||||||
|
2: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 6,
|
||||||
|
severeThreshold: 10,
|
||||||
|
hp: 0,
|
||||||
|
stress: 0,
|
||||||
|
attack: 1,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 6,
|
||||||
|
severeThreshold: 15,
|
||||||
|
hp: 1,
|
||||||
|
stress: 0,
|
||||||
|
attack: 2,
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 12,
|
||||||
|
severeThreshold: 25,
|
||||||
|
hp: 1,
|
||||||
|
stress: 1,
|
||||||
|
attack: 3,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
minion: {
|
||||||
|
2: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 0,
|
||||||
|
severeThreshold: 0,
|
||||||
|
hp: 0,
|
||||||
|
stress: 0,
|
||||||
|
attack: 1,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 0,
|
||||||
|
severeThreshold: 0,
|
||||||
|
hp: 0,
|
||||||
|
stress: 1,
|
||||||
|
attack: 1,
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 0,
|
||||||
|
severeThreshold: 0,
|
||||||
|
hp: 0,
|
||||||
|
stress: 0,
|
||||||
|
attack: 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ranged: {
|
||||||
|
2: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 3,
|
||||||
|
severeThreshold: 6,
|
||||||
|
hp: 1,
|
||||||
|
stress: 0,
|
||||||
|
attack: 1,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 7,
|
||||||
|
severeThreshold: 14,
|
||||||
|
hp: 1,
|
||||||
|
stress: 1,
|
||||||
|
attack: 2,
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 5,
|
||||||
|
severeThreshold: 10,
|
||||||
|
hp: 1,
|
||||||
|
stress: 1,
|
||||||
|
attack: 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
skulk: {
|
||||||
|
2: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 3,
|
||||||
|
severeThreshold: 8,
|
||||||
|
hp: 1,
|
||||||
|
stress: 1,
|
||||||
|
attack: 1,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 8,
|
||||||
|
severeThreshold: 12,
|
||||||
|
hp: 1,
|
||||||
|
stress: 1,
|
||||||
|
attack: 1,
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 8,
|
||||||
|
severeThreshold: 10,
|
||||||
|
hp: 1,
|
||||||
|
stress: 1,
|
||||||
|
attack: 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
solo: {
|
||||||
|
2: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 5,
|
||||||
|
severeThreshold: 10,
|
||||||
|
hp: 0,
|
||||||
|
stress: 1,
|
||||||
|
attack: 2,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 7,
|
||||||
|
severeThreshold: 15,
|
||||||
|
hp: 2,
|
||||||
|
stress: 1,
|
||||||
|
attack: 2,
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 12,
|
||||||
|
severeThreshold: 25,
|
||||||
|
hp: 0,
|
||||||
|
stress: 1,
|
||||||
|
attack: 3,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
standard: {
|
||||||
|
2: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 3,
|
||||||
|
severeThreshold: 8,
|
||||||
|
hp: 0,
|
||||||
|
stress: 0,
|
||||||
|
attack: 1,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 7,
|
||||||
|
severeThreshold: 15,
|
||||||
|
hp: 1,
|
||||||
|
stress: 1,
|
||||||
|
attack: 1,
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 10,
|
||||||
|
severeThreshold: 15,
|
||||||
|
hp: 0,
|
||||||
|
stress: 1,
|
||||||
|
attack: 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
support: {
|
||||||
|
2: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 3,
|
||||||
|
severeThreshold: 8,
|
||||||
|
hp: 1,
|
||||||
|
stress: 1,
|
||||||
|
attack: 1,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 7,
|
||||||
|
severeThreshold: 12,
|
||||||
|
hp: 0,
|
||||||
|
stress: 0,
|
||||||
|
attack: 1,
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
difficulty: 2,
|
||||||
|
majorThreshold: 8,
|
||||||
|
severeThreshold: 10,
|
||||||
|
hp: 1,
|
||||||
|
stress: 1,
|
||||||
|
attack: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scaling data used for an adversary's damage.
|
||||||
|
* Tier 4 is missing certain adversary types and therefore skews upwards.
|
||||||
|
* We manually set tier 4 data to hopefully lead to better results
|
||||||
|
*/
|
||||||
|
export const adversaryExpectedDamage = {
|
||||||
|
basic: {
|
||||||
|
1: { mean: 7.321428571428571, deviation: 1.962519002770912 },
|
||||||
|
2: { mean: 12.444444444444445, deviation: 2.0631069425529676 },
|
||||||
|
3: { mean: 15.722222222222221, deviation: 2.486565208464823 },
|
||||||
|
4: { mean: 26, deviation: 5.2 }
|
||||||
|
},
|
||||||
|
minion: {
|
||||||
|
1: { mean: 2.142857142857143, deviation: 1.0690449676496976 },
|
||||||
|
2: { mean: 5, deviation: 0.816496580927726 },
|
||||||
|
3: { mean: 6.5, deviation: 2.1213203435596424 },
|
||||||
|
4: { mean: 11, deviation: 1 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@ export const defeatedConditions = () => {
|
||||||
acc[key] = {
|
acc[key] = {
|
||||||
...choice,
|
...choice,
|
||||||
img: defeated[`${choice.id}Icon`],
|
img: defeated[`${choice.id}Icon`],
|
||||||
description: `DAGGERHEART.CONFIG.Condition.${choice.id}.description`
|
description: game.i18n.localize(`DAGGERHEART.CONFIG.Condition.${choice.id}.description`)
|
||||||
};
|
};
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
|
|
@ -179,6 +179,10 @@ export const defeatedConditions = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defeatedConditionChoices = {
|
export const defeatedConditionChoices = {
|
||||||
|
deathMove: {
|
||||||
|
id: 'deathMove',
|
||||||
|
name: 'DAGGERHEART.CONFIG.Condition.deathMove.name'
|
||||||
|
},
|
||||||
defeated: {
|
defeated: {
|
||||||
id: 'defeated',
|
id: 'defeated',
|
||||||
name: 'DAGGERHEART.CONFIG.Condition.defeated.name'
|
name: 'DAGGERHEART.CONFIG.Condition.defeated.name'
|
||||||
|
|
@ -198,7 +202,8 @@ export const conditions = () => ({
|
||||||
id: 'vulnerable',
|
id: 'vulnerable',
|
||||||
name: 'DAGGERHEART.CONFIG.Condition.vulnerable.name',
|
name: 'DAGGERHEART.CONFIG.Condition.vulnerable.name',
|
||||||
img: 'icons/magic/control/silhouette-fall-slip-prone.webp',
|
img: 'icons/magic/control/silhouette-fall-slip-prone.webp',
|
||||||
description: 'DAGGERHEART.CONFIG.Condition.vulnerable.description'
|
description: 'DAGGERHEART.CONFIG.Condition.vulnerable.description',
|
||||||
|
autoApplyFlagId: 'auto-vulnerable'
|
||||||
},
|
},
|
||||||
hidden: {
|
hidden: {
|
||||||
id: 'hidden',
|
id: 'hidden',
|
||||||
|
|
@ -232,6 +237,7 @@ export const defaultRestOptions = {
|
||||||
actionType: 'action',
|
actionType: 'action',
|
||||||
chatDisplay: false,
|
chatDisplay: false,
|
||||||
target: {
|
target: {
|
||||||
|
amount: 1,
|
||||||
type: 'friendly'
|
type: 'friendly'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
|
|
@ -248,7 +254,8 @@ export const defaultRestOptions = {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
effects: []
|
||||||
},
|
},
|
||||||
clearStress: {
|
clearStress: {
|
||||||
id: 'clearStress',
|
id: 'clearStress',
|
||||||
|
|
@ -281,7 +288,8 @@ export const defaultRestOptions = {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
effects: []
|
||||||
},
|
},
|
||||||
repairArmor: {
|
repairArmor: {
|
||||||
id: 'repairArmor',
|
id: 'repairArmor',
|
||||||
|
|
@ -298,6 +306,7 @@ export const defaultRestOptions = {
|
||||||
actionType: 'action',
|
actionType: 'action',
|
||||||
chatDisplay: false,
|
chatDisplay: false,
|
||||||
target: {
|
target: {
|
||||||
|
amount: 1,
|
||||||
type: 'friendly'
|
type: 'friendly'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
|
|
@ -314,7 +323,8 @@ export const defaultRestOptions = {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
effects: []
|
||||||
},
|
},
|
||||||
prepare: {
|
prepare: {
|
||||||
id: 'prepare',
|
id: 'prepare',
|
||||||
|
|
@ -322,7 +332,57 @@ export const defaultRestOptions = {
|
||||||
icon: 'fa-solid fa-dumbbell',
|
icon: 'fa-solid fa-dumbbell',
|
||||||
img: 'icons/skills/trades/academics-merchant-scribe.webp',
|
img: 'icons/skills/trades/academics-merchant-scribe.webp',
|
||||||
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.description'),
|
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.description'),
|
||||||
actions: {}
|
actions: {
|
||||||
|
prepare: {
|
||||||
|
type: 'healing',
|
||||||
|
systemPath: 'restMoves.shortRest.moves.prepare.actions',
|
||||||
|
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.name'),
|
||||||
|
img: 'icons/skills/trades/academics-merchant-scribe.webp',
|
||||||
|
actionType: 'action',
|
||||||
|
chatDisplay: false,
|
||||||
|
target: {
|
||||||
|
type: 'self'
|
||||||
|
},
|
||||||
|
damage: {
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
applyTo: healingTypes.hope.id,
|
||||||
|
value: {
|
||||||
|
custom: {
|
||||||
|
enabled: true,
|
||||||
|
formula: '1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prepareWithFriends: {
|
||||||
|
type: 'healing',
|
||||||
|
systemPath: 'restMoves.shortRest.moves.prepare.actions',
|
||||||
|
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepareWithFriends.name'),
|
||||||
|
img: 'icons/skills/trades/academics-merchant-scribe.webp',
|
||||||
|
actionType: 'action',
|
||||||
|
chatDisplay: false,
|
||||||
|
target: {
|
||||||
|
type: 'self'
|
||||||
|
},
|
||||||
|
damage: {
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
applyTo: healingTypes.hope.id,
|
||||||
|
value: {
|
||||||
|
custom: {
|
||||||
|
enabled: true,
|
||||||
|
formula: '2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
effects: []
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
longRest: () => ({
|
longRest: () => ({
|
||||||
|
|
@ -341,6 +401,7 @@ export const defaultRestOptions = {
|
||||||
actionType: 'action',
|
actionType: 'action',
|
||||||
chatDisplay: false,
|
chatDisplay: false,
|
||||||
target: {
|
target: {
|
||||||
|
amount: 1,
|
||||||
type: 'friendly'
|
type: 'friendly'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
|
|
@ -357,7 +418,8 @@ export const defaultRestOptions = {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
effects: []
|
||||||
},
|
},
|
||||||
clearStress: {
|
clearStress: {
|
||||||
id: 'clearStress',
|
id: 'clearStress',
|
||||||
|
|
@ -390,7 +452,8 @@ export const defaultRestOptions = {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
effects: []
|
||||||
},
|
},
|
||||||
repairArmor: {
|
repairArmor: {
|
||||||
id: 'repairArmor',
|
id: 'repairArmor',
|
||||||
|
|
@ -407,6 +470,7 @@ export const defaultRestOptions = {
|
||||||
actionType: 'action',
|
actionType: 'action',
|
||||||
chatDisplay: false,
|
chatDisplay: false,
|
||||||
target: {
|
target: {
|
||||||
|
amount: 1,
|
||||||
type: 'friendly'
|
type: 'friendly'
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
|
|
@ -423,7 +487,8 @@ export const defaultRestOptions = {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
effects: []
|
||||||
},
|
},
|
||||||
prepare: {
|
prepare: {
|
||||||
id: 'prepare',
|
id: 'prepare',
|
||||||
|
|
@ -431,7 +496,57 @@ export const defaultRestOptions = {
|
||||||
icon: 'fa-solid fa-dumbbell',
|
icon: 'fa-solid fa-dumbbell',
|
||||||
img: 'icons/skills/trades/academics-merchant-scribe.webp',
|
img: 'icons/skills/trades/academics-merchant-scribe.webp',
|
||||||
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.description'),
|
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.description'),
|
||||||
actions: {}
|
actions: {
|
||||||
|
prepare: {
|
||||||
|
type: 'healing',
|
||||||
|
systemPath: 'restMoves.longRest.moves.prepare.actions',
|
||||||
|
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.name'),
|
||||||
|
img: 'icons/skills/trades/academics-merchant-scribe.webp',
|
||||||
|
actionType: 'action',
|
||||||
|
chatDisplay: false,
|
||||||
|
target: {
|
||||||
|
type: 'self'
|
||||||
|
},
|
||||||
|
damage: {
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
applyTo: healingTypes.hope.id,
|
||||||
|
value: {
|
||||||
|
custom: {
|
||||||
|
enabled: true,
|
||||||
|
formula: '1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prepareWithFriends: {
|
||||||
|
type: 'healing',
|
||||||
|
systemPath: 'restMoves.longRest.moves.prepare.actions',
|
||||||
|
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepareWithFriends.name'),
|
||||||
|
img: 'icons/skills/trades/academics-merchant-scribe.webp',
|
||||||
|
actionType: 'action',
|
||||||
|
chatDisplay: false,
|
||||||
|
target: {
|
||||||
|
type: 'self'
|
||||||
|
},
|
||||||
|
damage: {
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
applyTo: healingTypes.hope.id,
|
||||||
|
value: {
|
||||||
|
custom: {
|
||||||
|
enabled: true,
|
||||||
|
formula: '2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
effects: []
|
||||||
},
|
},
|
||||||
workOnAProject: {
|
workOnAProject: {
|
||||||
id: 'workOnAProject',
|
id: 'workOnAProject',
|
||||||
|
|
@ -439,7 +554,8 @@ export const defaultRestOptions = {
|
||||||
icon: 'fa-solid fa-diagram-project',
|
icon: 'fa-solid fa-diagram-project',
|
||||||
img: 'icons/skills/social/thumbsup-approval-like.webp',
|
img: 'icons/skills/social/thumbsup-approval-like.webp',
|
||||||
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.workOnAProject.description'),
|
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.workOnAProject.description'),
|
||||||
actions: {}
|
actions: {},
|
||||||
|
effects: []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -467,9 +467,7 @@ export const allArmorFeatures = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const orderedArmorFeatures = () => {
|
export const orderedArmorFeatures = () => {
|
||||||
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures
|
const allFeatures = allArmorFeatures();
|
||||||
.armorFeatures;
|
|
||||||
const allFeatures = { ...armorFeatures, ...homebrewFeatures };
|
|
||||||
const all = Object.keys(allFeatures).map(key => {
|
const all = Object.keys(allFeatures).map(key => {
|
||||||
const feature = allFeatures[key];
|
const feature = allFeatures[key];
|
||||||
return {
|
return {
|
||||||
|
|
@ -1404,9 +1402,7 @@ export const allWeaponFeatures = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const orderedWeaponFeatures = () => {
|
export const orderedWeaponFeatures = () => {
|
||||||
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures
|
const allFeatures = allWeaponFeatures();
|
||||||
.weaponFeatures;
|
|
||||||
const allFeatures = { ...weaponFeatures, ...homebrewFeatures };
|
|
||||||
const all = Object.keys(allFeatures).map(key => {
|
const all = Object.keys(allFeatures).map(key => {
|
||||||
const feature = allFeatures[key];
|
const feature = allFeatures[key];
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export const gameSettings = {
|
||||||
LastMigrationVersion: 'LastMigrationVersion',
|
LastMigrationVersion: 'LastMigrationVersion',
|
||||||
TagTeamRoll: 'TagTeamRoll',
|
TagTeamRoll: 'TagTeamRoll',
|
||||||
SpotlightRequestQueue: 'SpotlightRequestQueue',
|
SpotlightRequestQueue: 'SpotlightRequestQueue',
|
||||||
|
CompendiumBrowserSettings: 'CompendiumBrowserSettings'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actionAutomationChoices = {
|
export const actionAutomationChoices = {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
export { default as DhCombat } from './combat.mjs';
|
export { default as DhCombat } from './combat.mjs';
|
||||||
export { default as DhCombatant } from './combatant.mjs';
|
export { default as DhCombatant } from './combatant.mjs';
|
||||||
export { default as DhTagTeamRoll } from './tagTeamRoll.mjs';
|
export { default as DhTagTeamRoll } from './tagTeamRoll.mjs';
|
||||||
|
export { default as DhRollTable } from './rollTable.mjs';
|
||||||
export { default as RegisteredTriggers } from './registeredTriggers.mjs';
|
export { default as RegisteredTriggers } from './registeredTriggers.mjs';
|
||||||
|
export { default as CompendiumBrowserSettings } from './compendiumBrowserSettings.mjs';
|
||||||
|
|
||||||
export * as countdowns from './countdowns.mjs';
|
export * as countdowns from './countdowns.mjs';
|
||||||
export * as actions from './action/_module.mjs';
|
export * as actions from './action/_module.mjs';
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,20 @@ export default class DHAttackAction extends DHDamageAction {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get damageFormula() {
|
||||||
|
const hitPointsPart = this.damage.parts.find(x => x.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
|
||||||
|
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);
|
||||||
|
if (!hitPointsPart) return '0';
|
||||||
|
|
||||||
|
return hitPointsPart.valueAlt.getFormula();
|
||||||
|
}
|
||||||
|
|
||||||
async use(event, options) {
|
async use(event, options) {
|
||||||
const result = await super.use(event, options);
|
const result = await super.use(event, options);
|
||||||
if (!result.message) return;
|
if (!result.message) return;
|
||||||
|
|
|
||||||
|
|
@ -114,9 +114,24 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
* Return Item the action is attached too.
|
* Return Item the action is attached too.
|
||||||
*/
|
*/
|
||||||
get item() {
|
get item() {
|
||||||
|
if (!this.parent.parent && this.systemPath)
|
||||||
|
return foundry.utils.getProperty(this.parent, this.systemPath).get(this.id);
|
||||||
|
|
||||||
return this.parent.parent;
|
return this.parent.parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get applyEffects() {
|
||||||
|
if (this.item.systemPath) {
|
||||||
|
const itemEffectIds = this.item.effects.map(x => x._id);
|
||||||
|
const movePathSplit = this.item.systemPath.split('.');
|
||||||
|
movePathSplit.pop();
|
||||||
|
const move = foundry.utils.getProperty(this.parent, movePathSplit.join('.'));
|
||||||
|
return new Collection(itemEffectIds.map(id => [id, move.effects.find(x => x.id === id)]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.item.effects;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the first Actor parent found.
|
* Return the first Actor parent found.
|
||||||
*/
|
*/
|
||||||
|
|
@ -125,7 +140,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
? this.item
|
? this.item
|
||||||
: this.item?.parent instanceof DhpActor
|
: this.item?.parent instanceof DhpActor
|
||||||
? this.item.parent
|
? this.item.parent
|
||||||
: this.item?.actor;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getRollType(parent) {
|
static getRollType(parent) {
|
||||||
|
|
@ -214,7 +229,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
|
|
||||||
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;
|
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;
|
||||||
|
|
||||||
if (this.chatDisplay) await this.toChat();
|
if (this.chatDisplay && !config.actionChatMessageHandled) await this.toChat();
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
@ -225,9 +240,13 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
* @returns {object}
|
* @returns {object}
|
||||||
*/
|
*/
|
||||||
prepareBaseConfig(event) {
|
prepareBaseConfig(event) {
|
||||||
|
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} - `;
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
event,
|
event,
|
||||||
title: `${this.item instanceof CONFIG.Actor.documentClass ? '' : `${this.item.name}: `}${game.i18n.localize(this.name)}`,
|
title: `${itemTitle}${actionTitle}`,
|
||||||
source: {
|
source: {
|
||||||
item: this.item._id,
|
item: this.item._id,
|
||||||
originItem: this.originItem,
|
originItem: this.originItem,
|
||||||
|
|
@ -241,6 +260,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
hasHealing: this.hasHealing,
|
hasHealing: this.hasHealing,
|
||||||
hasEffect: this.hasEffect,
|
hasEffect: this.hasEffect,
|
||||||
hasSave: this.hasSave,
|
hasSave: this.hasSave,
|
||||||
|
onSave: this.save?.damageMod,
|
||||||
isDirect: !!this.damage?.direct,
|
isDirect: !!this.damage?.direct,
|
||||||
selectedRollMode: game.settings.get('core', 'rollMode'),
|
selectedRollMode: game.settings.get('core', 'rollMode'),
|
||||||
data: this.getRollData(),
|
data: this.getRollData(),
|
||||||
|
|
@ -376,14 +396,14 @@ export class ResourceUpdateMap extends Map {
|
||||||
if (!resource.key) continue;
|
if (!resource.key) continue;
|
||||||
|
|
||||||
const existing = this.get(resource.key);
|
const existing = this.get(resource.key);
|
||||||
if (existing) {
|
if (!existing || resource.clear) {
|
||||||
|
this.set(resource.key, resource);
|
||||||
|
} else if (!existing?.clear) {
|
||||||
this.set(resource.key, {
|
this.set(resource.key, {
|
||||||
...existing,
|
...existing,
|
||||||
value: existing.value + (resource.value ?? 0),
|
value: existing.value + (resource.value ?? 0),
|
||||||
total: existing.total + (resource.total ?? 0)
|
total: existing.total + (resource.total ?? 0)
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
this.set(resource.key, resource);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import DHAdversarySettings from '../../applications/sheets-configs/adversary-settings.mjs';
|
import DHAdversarySettings from '../../applications/sheets-configs/adversary-settings.mjs';
|
||||||
import { ActionField } from '../fields/actionField.mjs';
|
import { ActionField } from '../fields/actionField.mjs';
|
||||||
import BaseDataActor, { commonActorRules } from './base.mjs';
|
import { commonActorRules } from './base.mjs';
|
||||||
|
import DhCreature from './creature.mjs';
|
||||||
import { resourceField, bonusField } from '../fields/actorField.mjs';
|
import { resourceField, bonusField } from '../fields/actorField.mjs';
|
||||||
|
import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs';
|
||||||
|
import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs';
|
||||||
|
|
||||||
export default class DhpAdversary extends BaseDataActor {
|
export default class DhpAdversary extends DhCreature {
|
||||||
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary'];
|
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary'];
|
||||||
|
|
||||||
static get metadata() {
|
static get metadata() {
|
||||||
|
|
@ -40,7 +43,14 @@ export default class DhpAdversary extends BaseDataActor {
|
||||||
integer: true,
|
integer: true,
|
||||||
label: 'DAGGERHEART.GENERAL.hordeHp'
|
label: 'DAGGERHEART.GENERAL.hordeHp'
|
||||||
}),
|
}),
|
||||||
criticalThreshold: new fields.NumberField({ required: true, integer: true, min: 1, max: 20, initial: 20 }),
|
criticalThreshold: new fields.NumberField({
|
||||||
|
required: true,
|
||||||
|
integer: true,
|
||||||
|
min: 1,
|
||||||
|
max: 20,
|
||||||
|
initial: 20,
|
||||||
|
label: 'DAGGERHEART.ACTIONS.Settings.criticalThreshold'
|
||||||
|
}),
|
||||||
damageThresholds: new fields.SchemaField({
|
damageThresholds: new fields.SchemaField({
|
||||||
major: new fields.NumberField({
|
major: new fields.NumberField({
|
||||||
required: true,
|
required: true,
|
||||||
|
|
@ -180,6 +190,10 @@ export default class DhpAdversary extends BaseDataActor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prepareDerivedData() {
|
||||||
|
this.attack.roll.isStandardAttack = true;
|
||||||
|
}
|
||||||
|
|
||||||
_getTags() {
|
_getTags() {
|
||||||
const tags = [
|
const tags = [
|
||||||
game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${this.tier}`),
|
game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${this.tier}`),
|
||||||
|
|
@ -188,4 +202,211 @@ export default class DhpAdversary extends BaseDataActor {
|
||||||
];
|
];
|
||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns source data for this actor adjusted to a new tier, which can be used to create a new actor. */
|
||||||
|
adjustForTier(tier) {
|
||||||
|
const source = this.parent.toObject(true);
|
||||||
|
|
||||||
|
/** @type {(2 | 3 | 4)[]} */
|
||||||
|
const tiers = new Array(Math.abs(tier - this.tier))
|
||||||
|
.fill(0)
|
||||||
|
.map((_, idx) => idx + Math.min(tier, this.tier) + 1);
|
||||||
|
if (tier < this.tier) tiers.reverse();
|
||||||
|
const typeData = adversaryScalingData[source.system.type] ?? adversaryScalingData[source.system.standard];
|
||||||
|
const tierEntries = tiers.map(t => ({ tier: t, ...typeData[t] }));
|
||||||
|
|
||||||
|
// Apply simple tier changes
|
||||||
|
const scale = tier > this.tier ? 1 : -1;
|
||||||
|
for (const entry of tierEntries) {
|
||||||
|
source.system.difficulty += scale * entry.difficulty;
|
||||||
|
source.system.damageThresholds.major += scale * entry.majorThreshold;
|
||||||
|
source.system.damageThresholds.severe += scale * entry.severeThreshold;
|
||||||
|
source.system.resources.hitPoints.max += scale * entry.hp;
|
||||||
|
source.system.resources.stress.max += scale * entry.stress;
|
||||||
|
source.system.attack.roll.bonus += scale * entry.attack;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the mean and standard deviation of expected damage in the previous and new tier
|
||||||
|
// The data we have is for attack scaling, but we reuse this for action scaling later
|
||||||
|
const expectedDamageData = adversaryExpectedDamage[source.system.type] ?? adversaryExpectedDamage.basic;
|
||||||
|
const damageMeta = {
|
||||||
|
currentDamageRange: { tier: source.system.tier, ...expectedDamageData[source.system.tier] },
|
||||||
|
newDamageRange: { tier, ...expectedDamageData[tier] },
|
||||||
|
type: 'attack'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update damage of base attack
|
||||||
|
try {
|
||||||
|
this.#adjustActionDamage(source.system.attack, damageMeta);
|
||||||
|
} catch (err) {
|
||||||
|
ui.notifications.warn('Failed to convert attack damage of adversary');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update damage of each item action, making sure to also update the description if possible
|
||||||
|
const damageRegex = /@Damage\[([^\[\]]*)\]({[^}]*})?/g;
|
||||||
|
for (const item of source.items) {
|
||||||
|
// Replace damage inlines with new formulas
|
||||||
|
for (const withDescription of [item.system, ...Object.values(item.system.actions)]) {
|
||||||
|
withDescription.description = withDescription.description.replace(damageRegex, (match, inner) => {
|
||||||
|
const { value: formula } = parseInlineParams(inner);
|
||||||
|
if (!formula || !type) return match;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const adjusted = this.#calculateAdjustedDamage(formula, { ...damageMeta, type: 'action' });
|
||||||
|
const newFormula = [
|
||||||
|
adjusted.diceQuantity ? `${adjusted.diceQuantity}d${adjusted.faces}` : null,
|
||||||
|
adjusted.bonus
|
||||||
|
]
|
||||||
|
.filter(p => !!p)
|
||||||
|
.join('+');
|
||||||
|
return match.replace(formula, newFormula);
|
||||||
|
} catch {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update damage in item actions
|
||||||
|
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' });
|
||||||
|
for (const { previousFormula, formula } of Object.values(result)) {
|
||||||
|
const oldFormulaRegexp = new RegExp(
|
||||||
|
previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?')
|
||||||
|
);
|
||||||
|
item.system.description = item.system.description.replace(oldFormulaRegexp, formula);
|
||||||
|
action.description = action.description.replace(oldFormulaRegexp, formula);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
ui.notifications.warn(`Failed to convert action damage for item ${item.name}`);
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally set the tier of the source data, now that everything is complete
|
||||||
|
source.system.tier = tier;
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a damage object to a new damage range
|
||||||
|
* @returns {{ diceQuantity: number; faces: number; bonus: number }} the adjusted result as a combined term
|
||||||
|
* @throws error if the formula is the wrong type
|
||||||
|
*/
|
||||||
|
#calculateAdjustedDamage(formula, { currentDamageRange, newDamageRange, type }) {
|
||||||
|
const terms = parseTermsFromSimpleFormula(formula);
|
||||||
|
const flatTerms = terms.filter(t => t.diceQuantity === 0);
|
||||||
|
const diceTerms = terms.filter(t => t.diceQuantity > 0);
|
||||||
|
if (flatTerms.length > 1 || diceTerms.length > 1) {
|
||||||
|
throw new Error('invalid formula for conversion');
|
||||||
|
}
|
||||||
|
const value = {
|
||||||
|
...(diceTerms[0] ?? { diceQuantity: 0, faces: 1 }),
|
||||||
|
bonus: flatTerms[0]?.bonus ?? 0
|
||||||
|
};
|
||||||
|
const previousExpected = calculateExpectedValue(value);
|
||||||
|
if (previousExpected === 0) return value; // nothing to do
|
||||||
|
|
||||||
|
const dieSizes = [4, 6, 8, 10, 12, 20];
|
||||||
|
const steps = newDamageRange.tier - currentDamageRange.tier;
|
||||||
|
const increasing = steps > 0;
|
||||||
|
const deviation = (previousExpected - currentDamageRange.mean) / currentDamageRange.deviation;
|
||||||
|
const expected = Math.max(1, newDamageRange.mean + newDamageRange.deviation * deviation);
|
||||||
|
|
||||||
|
// If this was just a flat number, convert to the expected damage and exit
|
||||||
|
if (value.diceQuantity === 0) {
|
||||||
|
value.bonus = Math.round(expected);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getExpectedDie = () => calculateExpectedValue({ diceQuantity: 1, faces: value.faces }) || 1;
|
||||||
|
const getBaseAverage = () => calculateExpectedValue({ ...value, bonus: 0 });
|
||||||
|
|
||||||
|
// Check the number of base overages over the expected die. In the end, if the bonus inflates too much, we add a die
|
||||||
|
const baseOverages = Math.floor(value.bonus / getExpectedDie());
|
||||||
|
|
||||||
|
// Prestep. Change number of dice for attacks, bump up/down for actions
|
||||||
|
// We never bump up to d20, though we might bump down from it
|
||||||
|
if (type === 'attack') {
|
||||||
|
const minimum = increasing ? value.diceQuantity : 0;
|
||||||
|
value.diceQuantity = Math.max(minimum, newDamageRange.tier);
|
||||||
|
} else {
|
||||||
|
const currentIdx = dieSizes.indexOf(value.faces);
|
||||||
|
value.faces = dieSizes[Math.clamp(currentIdx + steps, 0, 4)];
|
||||||
|
}
|
||||||
|
|
||||||
|
value.bonus = Math.round(expected - getBaseAverage());
|
||||||
|
|
||||||
|
// Attempt to handle negative values.
|
||||||
|
// If we can do it with only step downs, do so. Otherwise remove tier dice, and try again
|
||||||
|
if (value.bonus < 0) {
|
||||||
|
let stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity);
|
||||||
|
const currentIdx = dieSizes.indexOf(value.faces);
|
||||||
|
|
||||||
|
// If step downs alone don't suffice, change the flat modifier, then calculate steps required again
|
||||||
|
// If this isn't sufficient, the result will be slightly off. This is unlikely to happen
|
||||||
|
if (type !== 'attack' && stepsRequired > currentIdx && value.diceQuantity > 0) {
|
||||||
|
value.diceQuantity -= increasing ? 1 : Math.abs(steps);
|
||||||
|
value.bonus = Math.round(expected - getBaseAverage());
|
||||||
|
if (value.bonus >= 0) return value; // complete
|
||||||
|
}
|
||||||
|
|
||||||
|
stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity);
|
||||||
|
value.faces = dieSizes[Math.max(0, currentIdx - stepsRequired)];
|
||||||
|
value.bonus = Math.max(0, Math.round(expected - getBaseAverage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If value is really high, we add a number of dice based on the number of overages
|
||||||
|
// This attempts to preserve a similar amount of variance when increasing an action
|
||||||
|
const overagesToRemove = Math.floor(value.bonus / getExpectedDie()) - baseOverages;
|
||||||
|
if (type !== 'attack' && increasing && overagesToRemove > 0) {
|
||||||
|
value.diceQuantity += overagesToRemove;
|
||||||
|
value.bonus = Math.round(expected - getBaseAverage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates damage to reflect a specific value.
|
||||||
|
* @throws if damage structure is invalid for conversion
|
||||||
|
* @returns the converted formula and value as a simplified term
|
||||||
|
*/
|
||||||
|
#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');
|
||||||
|
|
||||||
|
const result = {};
|
||||||
|
for (const property of ['value', 'valueAlt']) {
|
||||||
|
const data = hpDamageParts[0][property];
|
||||||
|
const previousFormula = data.custom.enabled
|
||||||
|
? data.custom.formula
|
||||||
|
: [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0]
|
||||||
|
.filter(p => !!p)
|
||||||
|
.join('+');
|
||||||
|
const value = this.#calculateAdjustedDamage(previousFormula, damageMeta);
|
||||||
|
const formula = [value.diceQuantity ? `${value.diceQuantity}d${value.faces}` : null, value.bonus]
|
||||||
|
.filter(p => !!p)
|
||||||
|
.join('+');
|
||||||
|
if (value.diceQuantity) {
|
||||||
|
data.custom.enabled = false;
|
||||||
|
data.bonus = value.bonus;
|
||||||
|
data.dice = `d${value.faces}`;
|
||||||
|
data.flatMultiplier = value.diceQuantity;
|
||||||
|
} else if (!value.diceQuantity) {
|
||||||
|
data.custom.enabled = true;
|
||||||
|
data.custom.formula = formula;
|
||||||
|
}
|
||||||
|
|
||||||
|
result[property] = { previousFormula, formula, value };
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,21 +27,64 @@ const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Common rules applying to Characters and Adversaries */
|
/* Common rules applying to Characters and Adversaries */
|
||||||
export const commonActorRules = (extendedData = { damageReduction: {} }) => ({
|
export const commonActorRules = (extendedData = { damageReduction: {}, attack: { damage: {} } }) => ({
|
||||||
conditionImmunities: new fields.SchemaField({
|
conditionImmunities: new fields.SchemaField({
|
||||||
hidden: new fields.BooleanField({ initial: false }),
|
hidden: new fields.BooleanField({
|
||||||
restrained: new fields.BooleanField({ initial: false }),
|
initial: false,
|
||||||
vulnerable: new fields.BooleanField({ initial: false })
|
label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.hidden'
|
||||||
|
}),
|
||||||
|
restrained: new fields.BooleanField({
|
||||||
|
initial: false,
|
||||||
|
label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.restrained'
|
||||||
|
}),
|
||||||
|
vulnerable: new fields.BooleanField({
|
||||||
|
initial: false,
|
||||||
|
label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.vulnerable'
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
damageReduction: new fields.SchemaField({
|
damageReduction: new fields.SchemaField({
|
||||||
thresholdImmunities: new fields.SchemaField({
|
thresholdImmunities: new fields.SchemaField({
|
||||||
minor: new fields.BooleanField({ initial: false })
|
minor: new fields.BooleanField({
|
||||||
|
initial: false,
|
||||||
|
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.thresholdImmunities.minor.label',
|
||||||
|
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.thresholdImmunities.minor.hint'
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
reduceSeverity: new fields.SchemaField({
|
reduceSeverity: new fields.SchemaField({
|
||||||
magical: new fields.NumberField({ initial: 0, min: 0 }),
|
magical: new fields.NumberField({
|
||||||
physical: new fields.NumberField({ initial: 0, min: 0 })
|
initial: 0,
|
||||||
|
min: 0,
|
||||||
|
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.reduceSeverity.magical.label',
|
||||||
|
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.reduceSeverity.magical.hint'
|
||||||
|
}),
|
||||||
|
physical: new fields.NumberField({
|
||||||
|
initial: 0,
|
||||||
|
min: 0,
|
||||||
|
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.reduceSeverity.physical.label',
|
||||||
|
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.reduceSeverity.physical.hint'
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
...extendedData.damageReduction
|
...(extendedData.damageReduction ?? {})
|
||||||
|
}),
|
||||||
|
attack: new fields.SchemaField({
|
||||||
|
...extendedData.attack,
|
||||||
|
damage: new fields.SchemaField({
|
||||||
|
hpDamageMultiplier: new fields.NumberField({
|
||||||
|
required: true,
|
||||||
|
nullable: false,
|
||||||
|
initial: 1,
|
||||||
|
label: 'DAGGERHEART.GENERAL.Attack.hpDamageMultiplier.label',
|
||||||
|
hint: 'DAGGERHEART.GENERAL.Attack.hpDamageMultiplier.hint'
|
||||||
|
}),
|
||||||
|
hpDamageTakenMultiplier: new fields.NumberField({
|
||||||
|
required: true,
|
||||||
|
nullable: false,
|
||||||
|
initial: 1,
|
||||||
|
label: 'DAGGERHEART.GENERAL.Attack.hpDamageTakenMultiplier.label',
|
||||||
|
hint: 'DAGGERHEART.GENERAL.Attack.hpDamageTakenMultiplier.hint'
|
||||||
|
}),
|
||||||
|
...(extendedData.attack?.damage ?? {})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { burden } from '../../config/generalConfig.mjs';
|
import { burden } from '../../config/generalConfig.mjs';
|
||||||
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
|
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
|
||||||
import DhLevelData from '../levelData.mjs';
|
import DhLevelData from '../levelData.mjs';
|
||||||
import BaseDataActor, { commonActorRules } from './base.mjs';
|
import { commonActorRules } from './base.mjs';
|
||||||
|
import DhCreature from './creature.mjs';
|
||||||
import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs';
|
import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs';
|
||||||
import { ActionField } from '../fields/actionField.mjs';
|
import { ActionField } from '../fields/actionField.mjs';
|
||||||
import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs';
|
import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs';
|
||||||
|
|
||||||
export default class DhCharacter extends BaseDataActor {
|
export default class DhCharacter extends DhCreature {
|
||||||
/**@override */
|
/**@override */
|
||||||
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Character'];
|
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Character'];
|
||||||
|
|
||||||
|
|
@ -35,7 +36,18 @@ export default class DhCharacter extends BaseDataActor {
|
||||||
'DAGGERHEART.ACTORS.Character.maxHPBonus'
|
'DAGGERHEART.ACTORS.Character.maxHPBonus'
|
||||||
),
|
),
|
||||||
stress: resourceField(6, 0, 'DAGGERHEART.GENERAL.stress', true),
|
stress: resourceField(6, 0, 'DAGGERHEART.GENERAL.stress', true),
|
||||||
hope: resourceField(6, 2, 'DAGGERHEART.GENERAL.hope')
|
hope: new fields.SchemaField(
|
||||||
|
{
|
||||||
|
value: new fields.NumberField({
|
||||||
|
initial: 2,
|
||||||
|
min: 0,
|
||||||
|
integer: true,
|
||||||
|
label: 'DAGGERHEART.GENERAL.hope'
|
||||||
|
}),
|
||||||
|
isReversed: new fields.BooleanField({ initial: false })
|
||||||
|
},
|
||||||
|
{ label: 'DAGGERHEART.GENERAL.hope' }
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
traits: new fields.SchemaField({
|
traits: new fields.SchemaField({
|
||||||
agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'),
|
agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'),
|
||||||
|
|
@ -78,12 +90,7 @@ export default class DhCharacter extends BaseDataActor {
|
||||||
bags: new fields.NumberField({ initial: 0, integer: true }),
|
bags: new fields.NumberField({ initial: 0, integer: true }),
|
||||||
chests: new fields.NumberField({ initial: 0, integer: true })
|
chests: new fields.NumberField({ initial: 0, integer: true })
|
||||||
}),
|
}),
|
||||||
scars: new fields.TypedObjectField(
|
scars: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.scars' }),
|
||||||
new fields.SchemaField({
|
|
||||||
name: new fields.StringField({}),
|
|
||||||
description: new fields.StringField()
|
|
||||||
})
|
|
||||||
),
|
|
||||||
biography: new fields.SchemaField({
|
biography: new fields.SchemaField({
|
||||||
background: new fields.HTMLField(),
|
background: new fields.HTMLField(),
|
||||||
connections: new fields.HTMLField(),
|
connections: new fields.HTMLField(),
|
||||||
|
|
@ -125,14 +132,6 @@ export default class DhCharacter extends BaseDataActor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
advantageSources: new fields.ArrayField(new fields.StringField(), {
|
|
||||||
label: 'DAGGERHEART.ACTORS.Character.advantageSources.label',
|
|
||||||
hint: 'DAGGERHEART.ACTORS.Character.advantageSources.hint'
|
|
||||||
}),
|
|
||||||
disadvantageSources: new fields.ArrayField(new fields.StringField(), {
|
|
||||||
label: 'DAGGERHEART.ACTORS.Character.disadvantageSources.label',
|
|
||||||
hint: 'DAGGERHEART.ACTORS.Character.disadvantageSources.hint'
|
|
||||||
}),
|
|
||||||
levelData: new fields.EmbeddedDataField(DhLevelData),
|
levelData: new fields.EmbeddedDataField(DhLevelData),
|
||||||
bonuses: new fields.SchemaField({
|
bonuses: new fields.SchemaField({
|
||||||
roll: new fields.SchemaField({
|
roll: new fields.SchemaField({
|
||||||
|
|
@ -219,8 +218,16 @@ export default class DhCharacter extends BaseDataActor {
|
||||||
rules: new fields.SchemaField({
|
rules: new fields.SchemaField({
|
||||||
...commonActorRules({
|
...commonActorRules({
|
||||||
damageReduction: {
|
damageReduction: {
|
||||||
magical: new fields.BooleanField({ initial: false }),
|
magical: new fields.BooleanField({
|
||||||
physical: new fields.BooleanField({ initial: false }),
|
initial: false,
|
||||||
|
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.magical.label',
|
||||||
|
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.magical.hint'
|
||||||
|
}),
|
||||||
|
physical: new fields.BooleanField({
|
||||||
|
initial: false,
|
||||||
|
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.physical.label',
|
||||||
|
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.physical.hint'
|
||||||
|
}),
|
||||||
maxArmorMarked: new fields.SchemaField({
|
maxArmorMarked: new fields.SchemaField({
|
||||||
value: new fields.NumberField({
|
value: new fields.NumberField({
|
||||||
required: true,
|
required: true,
|
||||||
|
|
@ -250,36 +257,39 @@ export default class DhCharacter extends BaseDataActor {
|
||||||
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.label',
|
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.label',
|
||||||
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint'
|
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint'
|
||||||
}),
|
}),
|
||||||
disabledArmor: new fields.BooleanField({ intial: false })
|
disabledArmor: new fields.BooleanField({
|
||||||
|
intial: false,
|
||||||
|
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.disabledArmor.label'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
attack: {
|
||||||
|
damage: {
|
||||||
|
diceIndex: new fields.NumberField({
|
||||||
|
integer: true,
|
||||||
|
min: 0,
|
||||||
|
max: 5,
|
||||||
|
initial: 0,
|
||||||
|
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.label',
|
||||||
|
hint: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.hint'
|
||||||
|
}),
|
||||||
|
bonus: new fields.NumberField({
|
||||||
|
required: true,
|
||||||
|
initial: 0,
|
||||||
|
min: 0,
|
||||||
|
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.bonus.label'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
roll: new fields.SchemaField({
|
||||||
|
trait: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
choices: CONFIG.DH.ACTOR.abilities,
|
||||||
|
nullable: true,
|
||||||
|
initial: null,
|
||||||
|
label: 'DAGGERHEART.GENERAL.Rules.attack.roll.trait.label'
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
attack: new fields.SchemaField({
|
|
||||||
damage: new fields.SchemaField({
|
|
||||||
diceIndex: new fields.NumberField({
|
|
||||||
integer: true,
|
|
||||||
min: 0,
|
|
||||||
max: 5,
|
|
||||||
initial: 0,
|
|
||||||
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.label',
|
|
||||||
hint: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.hint'
|
|
||||||
}),
|
|
||||||
bonus: new fields.NumberField({
|
|
||||||
required: true,
|
|
||||||
initial: 0,
|
|
||||||
min: 0,
|
|
||||||
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.bonus.label'
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
roll: new fields.SchemaField({
|
|
||||||
trait: new fields.StringField({
|
|
||||||
required: true,
|
|
||||||
choices: CONFIG.DH.ACTOR.abilities,
|
|
||||||
nullable: true,
|
|
||||||
initial: null,
|
|
||||||
label: 'DAGGERHEART.GENERAL.Rules.attack.roll.trait.label'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
dualityRoll: new fields.SchemaField({
|
dualityRoll: new fields.SchemaField({
|
||||||
defaultHopeDice: new fields.NumberField({
|
defaultHopeDice: new fields.NumberField({
|
||||||
nullable: false,
|
nullable: false,
|
||||||
|
|
@ -298,9 +308,14 @@ export default class DhCharacter extends BaseDataActor {
|
||||||
label: 'DAGGERHEART.ACTORS.Character.defaultFearDice'
|
label: 'DAGGERHEART.ACTORS.Character.defaultFearDice'
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
runeWard: new fields.BooleanField({ initial: false }),
|
|
||||||
burden: new fields.SchemaField({
|
burden: new fields.SchemaField({
|
||||||
ignore: new fields.BooleanField()
|
ignore: new fields.BooleanField({ label: 'DAGGERHEART.ACTORS.Character.burden.ignore.label' })
|
||||||
|
}),
|
||||||
|
roll: new fields.SchemaField({
|
||||||
|
guaranteedCritical: new fields.BooleanField({
|
||||||
|
label: 'DAGGERHEART.ACTORS.Character.roll.guaranteedCritical.label',
|
||||||
|
hint: 'DAGGERHEART.ACTORS.Character.roll.guaranteedCritical.hint'
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
@ -363,7 +378,7 @@ export default class DhCharacter extends BaseDataActor {
|
||||||
const modifiers = subClasses
|
const modifiers = subClasses
|
||||||
?.map(sc => ({ ...this.traits[sc.system.spellcastingTrait], key: sc.system.spellcastingTrait }))
|
?.map(sc => ({ ...this.traits[sc.system.spellcastingTrait], key: sc.system.spellcastingTrait }))
|
||||||
.filter(x => x);
|
.filter(x => x);
|
||||||
return modifiers.sort((a, b) => a.value - b.value)[0];
|
return modifiers.sort((a, b) => (b.value ?? 0) - (a.value ?? 0))[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
get spellcastModifier() {
|
get spellcastModifier() {
|
||||||
|
|
@ -544,7 +559,18 @@ export default class DhCharacter extends BaseDataActor {
|
||||||
}
|
}
|
||||||
|
|
||||||
get deathMoveViable() {
|
get deathMoveViable() {
|
||||||
return this.resources.hitPoints.max > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.max;
|
const { characterDefault } = game.settings.get(
|
||||||
|
CONFIG.DH.id,
|
||||||
|
CONFIG.DH.SETTINGS.gameSettings.Automation
|
||||||
|
).defeated;
|
||||||
|
const deathMoveOutcomeStatuses = Object.keys(CONFIG.DH.GENERAL.defeatedConditionChoices).filter(
|
||||||
|
key => key !== characterDefault
|
||||||
|
);
|
||||||
|
const deathMoveNotResolved = this.parent.statuses.every(status => !deathMoveOutcomeStatuses.includes(status));
|
||||||
|
|
||||||
|
const allHitPointsMarked =
|
||||||
|
this.resources.hitPoints.max > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.max;
|
||||||
|
return deathMoveNotResolved && allHitPointsMarked;
|
||||||
}
|
}
|
||||||
|
|
||||||
get armorApplicableDamageTypes() {
|
get armorApplicableDamageTypes() {
|
||||||
|
|
@ -642,8 +668,15 @@ export default class DhCharacter extends BaseDataActor {
|
||||||
? armor.system.baseThresholds.severe + this.levelData.level.current
|
? armor.system.baseThresholds.severe + this.levelData.level.current
|
||||||
: this.levelData.level.current * 2
|
: this.levelData.level.current * 2
|
||||||
};
|
};
|
||||||
this.resources.hope.max -= Object.keys(this.scars).length;
|
|
||||||
|
const globalHopeMax = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxHope;
|
||||||
|
this.resources.hope.max = globalHopeMax;
|
||||||
this.resources.hitPoints.max += this.class.value?.system?.hitPoints ?? 0;
|
this.resources.hitPoints.max += this.class.value?.system?.hitPoints ?? 0;
|
||||||
|
|
||||||
|
/* Companion Related Data */
|
||||||
|
this.companionData = {
|
||||||
|
levelupChoices: this.levelData.level.current - 1
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareDerivedData() {
|
prepareDerivedData() {
|
||||||
|
|
@ -661,6 +694,7 @@ export default class DhCharacter extends BaseDataActor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.resources.hope.max -= this.scars;
|
||||||
this.resources.hope.value = Math.min(baseHope, this.resources.hope.max);
|
this.resources.hope.value = Math.min(baseHope, this.resources.hope.max);
|
||||||
this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait;
|
this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait;
|
||||||
|
|
||||||
|
|
@ -699,6 +733,30 @@ export default class DhCharacter extends BaseDataActor {
|
||||||
changes.system.experiences[experience].core = true;
|
changes.system.experiences[experience].core = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Scars can alter the amount of current hope */
|
||||||
|
if (changes.system?.scars) {
|
||||||
|
const diff = this.system.scars - changes.system.scars;
|
||||||
|
const newHopeMax = this.system.resources.hope.max + diff;
|
||||||
|
const newHopeValue = Math.min(newHopeMax, this.system.resources.hope.value);
|
||||||
|
if (newHopeValue != this.system.resources.hope.value) {
|
||||||
|
if (!changes.system.resources) changes.system.resources = { hope: { value: 0 } };
|
||||||
|
changes.system.resources.hope = {
|
||||||
|
...changes.system.resources.hope,
|
||||||
|
value: changes.system.resources.hope.value + newHopeValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force companion data prep */
|
||||||
|
if (this.companion) {
|
||||||
|
if (
|
||||||
|
changes.system?.levelData?.level?.current !== undefined &&
|
||||||
|
changes.system.levelData.level.current !== this._source.levelData.level.current
|
||||||
|
) {
|
||||||
|
this.companion.update(this.companion.toObject(), { diff: false, recursive: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _preDelete() {
|
async _preDelete() {
|
||||||
|
|
@ -714,4 +772,11 @@ export default class DhCharacter extends BaseDataActor {
|
||||||
t => !!t
|
t => !!t
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static migrateData(source) {
|
||||||
|
if (typeof source.scars === 'object') source.scars = 0;
|
||||||
|
if (source.resources?.hope?.max) source.scars = Math.max(6 - source.resources.hope.max, 0);
|
||||||
|
|
||||||
|
return super.migrateData(source);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import BaseDataActor from './base.mjs';
|
import DhCreature from './creature.mjs';
|
||||||
import DhLevelData from '../levelData.mjs';
|
import DhLevelData from '../levelData.mjs';
|
||||||
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
|
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
|
||||||
import { ActionField } from '../fields/actionField.mjs';
|
import { ActionField } from '../fields/actionField.mjs';
|
||||||
|
|
@ -6,7 +6,7 @@ import { adjustDice, adjustRange } from '../../helpers/utils.mjs';
|
||||||
import DHCompanionSettings from '../../applications/sheets-configs/companion-settings.mjs';
|
import DHCompanionSettings from '../../applications/sheets-configs/companion-settings.mjs';
|
||||||
import { resourceField, bonusField } from '../fields/actorField.mjs';
|
import { resourceField, bonusField } from '../fields/actorField.mjs';
|
||||||
|
|
||||||
export default class DhCompanion extends BaseDataActor {
|
export default class DhCompanion extends DhCreature {
|
||||||
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Companion'];
|
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Companion'];
|
||||||
|
|
||||||
/**@inheritdoc */
|
/**@inheritdoc */
|
||||||
|
|
@ -53,9 +53,18 @@ export default class DhCompanion extends BaseDataActor {
|
||||||
),
|
),
|
||||||
rules: new fields.SchemaField({
|
rules: new fields.SchemaField({
|
||||||
conditionImmunities: new fields.SchemaField({
|
conditionImmunities: new fields.SchemaField({
|
||||||
hidden: new fields.BooleanField({ initial: false }),
|
hidden: new fields.BooleanField({
|
||||||
restrained: new fields.BooleanField({ initial: false }),
|
initial: false,
|
||||||
vulnerable: new fields.BooleanField({ initial: false })
|
label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.hidden'
|
||||||
|
}),
|
||||||
|
restrained: new fields.BooleanField({
|
||||||
|
initial: false,
|
||||||
|
label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.restrained'
|
||||||
|
}),
|
||||||
|
vulnerable: new fields.BooleanField({
|
||||||
|
initial: false,
|
||||||
|
label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.vulnerable'
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
attack: new ActionField({
|
attack: new ActionField({
|
||||||
|
|
@ -109,6 +118,10 @@ export default class DhCompanion extends BaseDataActor {
|
||||||
return this.partner?.system?.proficiency ?? 1;
|
return this.partner?.system?.proficiency ?? 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canLevelUp() {
|
||||||
|
return this.levelupChoicesLeft > 0;
|
||||||
|
}
|
||||||
|
|
||||||
isItemValid() {
|
isItemValid() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +140,7 @@ export default class DhCompanion extends BaseDataActor {
|
||||||
if (selection.data[0] === 'damage') {
|
if (selection.data[0] === 'damage') {
|
||||||
this.attack.damage.parts[0].value.dice = adjustDice(this.attack.damage.parts[0].value.dice);
|
this.attack.damage.parts[0].value.dice = adjustDice(this.attack.damage.parts[0].value.dice);
|
||||||
} else {
|
} else {
|
||||||
this.attack.range = adjustRange(this.attack.range);
|
this.attack.range = adjustRange(this.attack.range).id;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'stress':
|
case 'stress':
|
||||||
|
|
@ -147,6 +160,17 @@ export default class DhCompanion extends BaseDataActor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prepareDerivedData() {
|
||||||
|
/* Partner Related Setup */
|
||||||
|
if (this.partner) {
|
||||||
|
this.levelData.level.changed = this.partner.system.levelData.level.current;
|
||||||
|
this.levelupChoicesLeft = Object.values(this.levelData.levelups).reduce((acc, curr) => {
|
||||||
|
acc = Math.max(acc - curr.selections.length, 0);
|
||||||
|
return acc;
|
||||||
|
}, this.partner.system.companionData.levelupChoices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async _preUpdate(changes, options, userId) {
|
async _preUpdate(changes, options, userId) {
|
||||||
const allowed = await super._preUpdate(changes, options, userId);
|
const allowed = await super._preUpdate(changes, options, userId);
|
||||||
if (allowed === false) return;
|
if (allowed === false) return;
|
||||||
|
|
@ -162,6 +186,16 @@ export default class DhCompanion extends BaseDataActor {
|
||||||
changes.system.experiences[experience].core = true;
|
changes.system.experiences[experience].core = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Force partner data prep */
|
||||||
|
if (this.partner) {
|
||||||
|
if (
|
||||||
|
changes.system?.levelData?.level?.current !== undefined &&
|
||||||
|
changes.system.levelData.level.current !== this._source.levelData.level.current
|
||||||
|
) {
|
||||||
|
this.partner.update(this.partner.toObject(), { diff: false, recursive: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _preDelete() {
|
async _preDelete() {
|
||||||
|
|
|
||||||
61
module/data/actor/creature.mjs
Normal file
61
module/data/actor/creature.mjs
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import BaseDataActor from './base.mjs';
|
||||||
|
|
||||||
|
export default class DhCreature extends BaseDataActor {
|
||||||
|
/**@inheritdoc */
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...super.defineSchema(),
|
||||||
|
advantageSources: new fields.ArrayField(new fields.StringField(), {
|
||||||
|
label: 'DAGGERHEART.ACTORS.Character.advantageSources.label',
|
||||||
|
hint: 'DAGGERHEART.ACTORS.Character.advantageSources.hint'
|
||||||
|
}),
|
||||||
|
disadvantageSources: new fields.ArrayField(new fields.StringField(), {
|
||||||
|
label: 'DAGGERHEART.ACTORS.Character.disadvantageSources.label',
|
||||||
|
hint: 'DAGGERHEART.ACTORS.Character.disadvantageSources.hint'
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get isAutoVulnerableActive() {
|
||||||
|
const vulnerableAppliedByOther = this.parent.effects.some(
|
||||||
|
x => x.statuses.has('vulnerable') && !x.flags.daggerheart?.autoApplyFlagId
|
||||||
|
);
|
||||||
|
return !vulnerableAppliedByOther;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _preUpdate(changes, options, userId) {
|
||||||
|
const allowed = await super._preUpdate(changes, options, userId);
|
||||||
|
if (allowed === false) return;
|
||||||
|
|
||||||
|
const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
|
||||||
|
if (
|
||||||
|
automationSettings.vulnerableAutomation &&
|
||||||
|
this.parent.type !== 'companion' &&
|
||||||
|
changes.system?.resources?.stress?.value
|
||||||
|
) {
|
||||||
|
const { name, description, img, autoApplyFlagId } = CONFIG.DH.GENERAL.conditions().vulnerable;
|
||||||
|
const autoEffects = this.parent.effects.filter(
|
||||||
|
x => x.flags.daggerheart?.autoApplyFlagId === autoApplyFlagId
|
||||||
|
);
|
||||||
|
if (changes.system.resources.stress.value >= this.resources.stress.max) {
|
||||||
|
if (!autoEffects.length)
|
||||||
|
this.parent.createEmbeddedDocuments('ActiveEffect', [
|
||||||
|
{
|
||||||
|
name: game.i18n.localize(name),
|
||||||
|
description: game.i18n.localize(description),
|
||||||
|
img: img,
|
||||||
|
statuses: ['vulnerable'],
|
||||||
|
flags: { daggerheart: { autoApplyFlagId } }
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
} else if (this.resources.stress.value >= this.resources.stress.max) {
|
||||||
|
this.parent.deleteEmbeddedDocuments(
|
||||||
|
'ActiveEffect',
|
||||||
|
autoEffects.map(x => x.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ export const config = {
|
||||||
adversaryRoll: DHActorRoll,
|
adversaryRoll: DHActorRoll,
|
||||||
damageRoll: DHActorRoll,
|
damageRoll: DHActorRoll,
|
||||||
dualityRoll: DHActorRoll,
|
dualityRoll: DHActorRoll,
|
||||||
|
fateRoll: DHActorRoll,
|
||||||
groupRoll: DHGroupRoll,
|
groupRoll: DHGroupRoll,
|
||||||
systemMessage: DHSystemMessage
|
systemMessage: DHSystemMessage
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
return {
|
return {
|
||||||
title: new fields.StringField(),
|
title: new fields.StringField(),
|
||||||
|
actionDescription: new fields.HTMLField(),
|
||||||
roll: new fields.ObjectField(),
|
roll: new fields.ObjectField(),
|
||||||
targets: targetsField(),
|
targets: targetsField(),
|
||||||
hasRoll: new fields.BooleanField({ initial: false }),
|
hasRoll: new fields.BooleanField({ initial: false }),
|
||||||
|
|
|
||||||
370
module/data/companionLevelup.mjs
Normal file
370
module/data/companionLevelup.mjs
Normal file
|
|
@ -0,0 +1,370 @@
|
||||||
|
import { abilities } from '../config/actorConfig.mjs';
|
||||||
|
import { chunkify } from '../helpers/utils.mjs';
|
||||||
|
import { LevelOptionType } from './levelTier.mjs';
|
||||||
|
|
||||||
|
export class DhCompanionLevelup extends foundry.abstract.DataModel {
|
||||||
|
static initializeData(levelTierData, pcLevelData, origChoicesLeft) {
|
||||||
|
let choicesLeft = origChoicesLeft;
|
||||||
|
|
||||||
|
const { current, changed } = pcLevelData.level;
|
||||||
|
const bonusChoicesOnly = current === changed;
|
||||||
|
const startLevel = bonusChoicesOnly ? current : current + 1;
|
||||||
|
const endLevel = bonusChoicesOnly ? startLevel : changed;
|
||||||
|
|
||||||
|
const tiers = {};
|
||||||
|
const levels = {};
|
||||||
|
const tierKeys = Object.keys(levelTierData.tiers);
|
||||||
|
tierKeys.forEach(key => {
|
||||||
|
const tier = levelTierData.tiers[key];
|
||||||
|
const belongingLevels = [];
|
||||||
|
for (var i = tier.levels.start; i <= tier.levels.end; i++) {
|
||||||
|
if (i <= endLevel) {
|
||||||
|
const initialAchievements = i === tier.levels.start ? tier.initialAchievements : {};
|
||||||
|
const experiences = initialAchievements.experience
|
||||||
|
? [...Array(initialAchievements.experience.nr).keys()].reduce((acc, _) => {
|
||||||
|
acc[foundry.utils.randomID()] = {
|
||||||
|
name: '',
|
||||||
|
modifier: initialAchievements.experience.modifier
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const currentChoices = pcLevelData.levelups[i]?.selections?.length;
|
||||||
|
const maxSelections =
|
||||||
|
i === endLevel
|
||||||
|
? choicesLeft + (currentChoices ?? 0)
|
||||||
|
: (currentChoices ?? tier.maxSelections[i]);
|
||||||
|
if (!pcLevelData.levelups[i]) choicesLeft -= maxSelections;
|
||||||
|
|
||||||
|
levels[i] = DhLevelupLevel.initializeData(pcLevelData.levelups[i], maxSelections, {
|
||||||
|
...initialAchievements,
|
||||||
|
experiences,
|
||||||
|
domainCards: {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
belongingLevels.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Improve. Temporary handling for Companion new experiences */
|
||||||
|
Object.keys(tier.extraAchievements ?? {}).forEach(key => {
|
||||||
|
const level = Number(key);
|
||||||
|
if (level >= startLevel && level <= endLevel) {
|
||||||
|
const levelExtras = tier.extraAchievements[level];
|
||||||
|
if (levelExtras.experience) {
|
||||||
|
levels[level].achievements.experiences[foundry.utils.randomID()] = {
|
||||||
|
name: '',
|
||||||
|
modifier: levelExtras.experience.modifier
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tiers[key] = {
|
||||||
|
name: tier.name,
|
||||||
|
belongingLevels: belongingLevels,
|
||||||
|
options: Object.keys(tier.options).reduce((acc, key) => {
|
||||||
|
acc[key] = tier.options[key].toObject?.() ?? tier.options[key];
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
tiers,
|
||||||
|
levels,
|
||||||
|
startLevel,
|
||||||
|
currentLevel: startLevel,
|
||||||
|
endLevel
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
return {
|
||||||
|
tiers: new fields.TypedObjectField(
|
||||||
|
new fields.SchemaField({
|
||||||
|
name: new fields.StringField({ required: true }),
|
||||||
|
belongingLevels: new fields.ArrayField(new fields.NumberField({ required: true, integer: true })),
|
||||||
|
options: new fields.TypedObjectField(
|
||||||
|
new fields.SchemaField({
|
||||||
|
label: new fields.StringField({ required: true }),
|
||||||
|
checkboxSelections: new fields.NumberField({ required: true, integer: true }),
|
||||||
|
minCost: new fields.NumberField({ required: true, integer: true }),
|
||||||
|
type: new fields.StringField({ required: true, choices: LevelOptionType }),
|
||||||
|
value: new fields.NumberField({ integer: true }),
|
||||||
|
amount: new fields.NumberField({ integer: true })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
),
|
||||||
|
levels: new fields.TypedObjectField(new fields.EmbeddedDataField(DhLevelupLevel)),
|
||||||
|
startLevel: new fields.NumberField({ required: true, integer: true }),
|
||||||
|
currentLevel: new fields.NumberField({ required: true, integer: true }),
|
||||||
|
endLevel: new fields.NumberField({ required: true, integer: true })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#levelFinished(levelKey) {
|
||||||
|
const allSelectionsMade = this.levels[levelKey].nrSelections.available === 0;
|
||||||
|
const allChoicesMade = Object.keys(this.levels[levelKey].choices).every(choiceKey => {
|
||||||
|
const choice = this.levels[levelKey].choices[choiceKey];
|
||||||
|
return Object.values(choice).every(checkbox => {
|
||||||
|
switch (choiceKey) {
|
||||||
|
case 'trait':
|
||||||
|
case 'experience':
|
||||||
|
case 'domainCard':
|
||||||
|
case 'subclass':
|
||||||
|
case 'vicious':
|
||||||
|
return checkbox.data.length === (checkbox.amount ?? 1);
|
||||||
|
case 'multiclass':
|
||||||
|
const classSelected = checkbox.data.length === 1;
|
||||||
|
const domainSelected = checkbox.secondaryData.domain;
|
||||||
|
const subclassSelected = checkbox.secondaryData.subclass;
|
||||||
|
return classSelected && domainSelected && subclassSelected;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const experiencesSelected = !this.levels[levelKey].achievements.experiences
|
||||||
|
? true
|
||||||
|
: Object.values(this.levels[levelKey].achievements.experiences).every(exp => exp.name);
|
||||||
|
const domainCardsSelected = Object.values(this.levels[levelKey].achievements.domainCards)
|
||||||
|
.filter(x => x.level <= this.endLevel)
|
||||||
|
.every(card => card.uuid);
|
||||||
|
const allAchievementsSelected = experiencesSelected && domainCardsSelected;
|
||||||
|
|
||||||
|
return allSelectionsMade && allChoicesMade && allAchievementsSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentLevelFinished() {
|
||||||
|
return this.#levelFinished(this.currentLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
get allLevelsFinished() {
|
||||||
|
return Object.keys(this.levels)
|
||||||
|
.filter(level => Number(level) >= this.startLevel)
|
||||||
|
.every(this.#levelFinished.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
get unmarkedTraits() {
|
||||||
|
const possibleLevels = Object.values(this.tiers).reduce((acc, tier) => {
|
||||||
|
if (tier.belongingLevels.includes(this.currentLevel)) acc = tier.belongingLevels;
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return Object.keys(this.levels)
|
||||||
|
.filter(key => possibleLevels.some(x => x === Number(key)))
|
||||||
|
.reduce(
|
||||||
|
(acc, levelKey) => {
|
||||||
|
const level = this.levels[levelKey];
|
||||||
|
Object.values(level.choices).forEach(choice =>
|
||||||
|
Object.values(choice).forEach(checkbox => {
|
||||||
|
if (
|
||||||
|
checkbox.type === 'trait' &&
|
||||||
|
checkbox.data.length > 0 &&
|
||||||
|
Number(levelKey) !== this.currentLevel
|
||||||
|
) {
|
||||||
|
checkbox.data.forEach(data => delete acc[data]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ ...abilities }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get classUpgradeChoices() {
|
||||||
|
let subclasses = [];
|
||||||
|
let multiclass = null;
|
||||||
|
Object.keys(this.levels).forEach(levelKey => {
|
||||||
|
const level = this.levels[levelKey];
|
||||||
|
Object.values(level.choices).forEach(choice => {
|
||||||
|
Object.values(choice).forEach(checkbox => {
|
||||||
|
if (checkbox.type === 'multiclass') {
|
||||||
|
multiclass = {
|
||||||
|
class: checkbox.data.length > 0 ? checkbox.data[0] : null,
|
||||||
|
domain: checkbox.secondaryData.domain ?? null,
|
||||||
|
subclass: checkbox.secondaryData.subclass ?? null,
|
||||||
|
tier: checkbox.tier,
|
||||||
|
level: levelKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (checkbox.type === 'subclass') {
|
||||||
|
subclasses.push({
|
||||||
|
tier: checkbox.tier,
|
||||||
|
level: levelKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return { subclasses, multiclass };
|
||||||
|
}
|
||||||
|
|
||||||
|
get tiersForRendering() {
|
||||||
|
const tierKeys = Object.keys(this.tiers);
|
||||||
|
const selections = Object.keys(this.levels).reduce(
|
||||||
|
(acc, key) => {
|
||||||
|
const level = this.levels[key];
|
||||||
|
Object.keys(level.choices).forEach(optionKey => {
|
||||||
|
const choice = level.choices[optionKey];
|
||||||
|
Object.keys(choice).forEach(checkboxNr => {
|
||||||
|
const checkbox = choice[checkboxNr];
|
||||||
|
if (!acc[checkbox.tier][optionKey]) acc[checkbox.tier][optionKey] = {};
|
||||||
|
Object.keys(choice).forEach(checkboxNr => {
|
||||||
|
acc[checkbox.tier][optionKey][checkboxNr] = { ...checkbox, level: Number(key) };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
tierKeys.reduce((acc, key) => {
|
||||||
|
acc[key] = {};
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { multiclass, subclasses } = this.classUpgradeChoices;
|
||||||
|
return tierKeys.map((tierKey, tierIndex) => {
|
||||||
|
const tier = this.tiers[tierKey];
|
||||||
|
const multiclassInTier = multiclass?.tier === Number(tierKey);
|
||||||
|
const subclassInTier = subclasses.some(x => x.tier === Number(tierKey));
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: game.i18n.localize(tier.name),
|
||||||
|
active: this.currentLevel >= Math.min(...tier.belongingLevels),
|
||||||
|
groups: Object.keys(tier.options).map(optionKey => {
|
||||||
|
const option = tier.options[optionKey];
|
||||||
|
|
||||||
|
const checkboxes = [...Array(option.checkboxSelections).keys()].flatMap(index => {
|
||||||
|
const checkboxNr = index + 1;
|
||||||
|
const checkboxData = selections[tierKey]?.[optionKey]?.[checkboxNr];
|
||||||
|
const checkbox = { ...option, checkboxNr, tier: tierKey };
|
||||||
|
|
||||||
|
if (checkboxData) {
|
||||||
|
checkbox.level = checkboxData.level;
|
||||||
|
checkbox.selected = true;
|
||||||
|
checkbox.disabled = checkbox.level !== this.currentLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optionKey === 'multiclass') {
|
||||||
|
if ((multiclass && !multiclassInTier) || subclassInTier) {
|
||||||
|
checkbox.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optionKey === 'subclass' && multiclassInTier) {
|
||||||
|
checkbox.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkbox;
|
||||||
|
});
|
||||||
|
|
||||||
|
let label = game.i18n.localize(option.label);
|
||||||
|
if (optionKey === 'domainCard') {
|
||||||
|
const maxLevel = tier.belongingLevels[tier.belongingLevels.length - 1];
|
||||||
|
label = game.i18n.format(option.label, { maxLevel });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: label,
|
||||||
|
checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => {
|
||||||
|
const anySelected = chunkedBoxes.some(x => x.selected);
|
||||||
|
const anyDisabled = chunkedBoxes.some(x => x.disabled);
|
||||||
|
return {
|
||||||
|
multi: option.minCost > 1,
|
||||||
|
checkboxes: chunkedBoxes.map(x => ({
|
||||||
|
...x,
|
||||||
|
selected: anySelected,
|
||||||
|
disabled: anyDisabled
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DhLevelupLevel extends foundry.abstract.DataModel {
|
||||||
|
static initializeData(levelData = { selections: [] }, maxSelections, achievements) {
|
||||||
|
return {
|
||||||
|
maxSelections: maxSelections,
|
||||||
|
achievements: {
|
||||||
|
experiences: levelData.achievements?.experiences ?? achievements.experiences ?? {},
|
||||||
|
domainCards: levelData.achievements?.domainCards
|
||||||
|
? levelData.achievements.domainCards.reduce((acc, card, index) => {
|
||||||
|
acc[index] = { ...card };
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
|
: (achievements.domainCards ?? {}),
|
||||||
|
proficiency: levelData.achievements?.proficiency ?? achievements.proficiency ?? null
|
||||||
|
},
|
||||||
|
choices: levelData.selections.reduce((acc, data) => {
|
||||||
|
if (!acc[data.optionKey]) acc[data.optionKey] = {};
|
||||||
|
acc[data.optionKey][data.checkboxNr] = { ...data };
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
return {
|
||||||
|
maxSelections: new fields.NumberField({ required: true, integer: true }),
|
||||||
|
achievements: new fields.SchemaField({
|
||||||
|
experiences: new fields.TypedObjectField(
|
||||||
|
new fields.SchemaField({
|
||||||
|
name: new fields.StringField({ required: true }),
|
||||||
|
modifier: new fields.NumberField({ required: true, integer: true })
|
||||||
|
})
|
||||||
|
),
|
||||||
|
domainCards: new fields.TypedObjectField(
|
||||||
|
new fields.SchemaField({
|
||||||
|
uuid: new fields.StringField({ required: true, nullable: true, initial: null }),
|
||||||
|
itemUuid: new fields.StringField({ required: true }),
|
||||||
|
level: new fields.NumberField({ required: true, integer: true })
|
||||||
|
})
|
||||||
|
),
|
||||||
|
proficiency: new fields.NumberField({ integer: true })
|
||||||
|
}),
|
||||||
|
choices: new fields.TypedObjectField(
|
||||||
|
new fields.TypedObjectField(
|
||||||
|
new fields.SchemaField({
|
||||||
|
tier: new fields.NumberField({ required: true, integer: true }),
|
||||||
|
minCost: new fields.NumberField({ required: true, integer: true }),
|
||||||
|
amount: new fields.NumberField({ integer: true }),
|
||||||
|
value: new fields.StringField(),
|
||||||
|
data: new fields.ArrayField(new fields.StringField()),
|
||||||
|
secondaryData: new fields.TypedObjectField(new fields.StringField()),
|
||||||
|
type: new fields.StringField({ required: true })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get nrSelections() {
|
||||||
|
const selections = Object.keys(this.choices).reduce((acc, choiceKey) => {
|
||||||
|
const choice = this.choices[choiceKey];
|
||||||
|
acc += Object.values(choice).reduce((acc, x) => acc + x.minCost, 0);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
selections: selections,
|
||||||
|
available: this.maxSelections - selections
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
36
module/data/compendiumBrowserSettings.mjs
Normal file
36
module/data/compendiumBrowserSettings.mjs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
export default class CompendiumBrowserSettings extends foundry.abstract.DataModel {
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
return {
|
||||||
|
excludedSources: new fields.TypedObjectField(
|
||||||
|
new fields.SchemaField({
|
||||||
|
excludedDocumentTypes: new fields.ArrayField(
|
||||||
|
new fields.StringField({ required: true, choices: CONST.SYSTEM_SPECIFIC_COMPENDIUM_TYPES })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
),
|
||||||
|
excludedPacks: new fields.TypedObjectField(
|
||||||
|
new fields.SchemaField({
|
||||||
|
excludedDocumentTypes: new fields.ArrayField(
|
||||||
|
new fields.StringField({ required: true, choices: CONST.SYSTEM_SPECIFIC_COMPENDIUM_TYPES })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isEntryExcluded(item) {
|
||||||
|
const pack = game.packs.get(item.pack);
|
||||||
|
if (!pack) return false;
|
||||||
|
|
||||||
|
const packageName = pack.metadata.packageType === 'world' ? 'world' : pack.metadata.packageName;
|
||||||
|
const excludedSourceData = this.excludedSources[packageName];
|
||||||
|
if (excludedSourceData && excludedSourceData.excludedDocumentTypes.includes(pack.metadata.type)) return true;
|
||||||
|
|
||||||
|
const excludedPackData = this.excludedPacks[item.pack];
|
||||||
|
if (excludedPackData && excludedPackData.excludedDocumentTypes.includes(pack.metadata.type)) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -68,6 +68,8 @@ export default class DamageField extends fields.SchemaField {
|
||||||
|
|
||||||
const damageResult = await CONFIG.Dice.daggerheart.DamageRoll.build(damageConfig);
|
const damageResult = await CONFIG.Dice.daggerheart.DamageRoll.build(damageConfig);
|
||||||
if (!damageResult) return false;
|
if (!damageResult) return false;
|
||||||
|
if (damageResult.actionChatMessageHandled) config.actionChatMessageHandled = true;
|
||||||
|
|
||||||
config.damage = damageResult.damage;
|
config.damage = damageResult.damage;
|
||||||
config.message ??= damageConfig.message;
|
config.message ??= damageConfig.message;
|
||||||
}
|
}
|
||||||
|
|
@ -105,12 +107,22 @@ export default class DamageField extends fields.SchemaField {
|
||||||
damagePromises.push(
|
damagePromises.push(
|
||||||
actor.takeHealing(config.damage).then(updates => targetDamage.push({ token, updates }))
|
actor.takeHealing(config.damage).then(updates => targetDamage.push({ token, updates }))
|
||||||
);
|
);
|
||||||
else
|
else {
|
||||||
|
const configDamage = foundry.utils.deepClone(config.damage);
|
||||||
|
const hpDamageMultiplier = config.actionActor?.system.rules?.attack?.damage?.hpDamageMultiplier ?? 1;
|
||||||
|
const hpDamageTakenMultiplier = actor.system.rules?.attack?.damage?.hpDamageTakenMultiplier;
|
||||||
|
if (configDamage.hitPoints) {
|
||||||
|
for (const part of configDamage.hitPoints.parts) {
|
||||||
|
part.total = Math.ceil(part.total * hpDamageMultiplier * hpDamageTakenMultiplier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
damagePromises.push(
|
damagePromises.push(
|
||||||
actor
|
actor
|
||||||
.takeDamage(config.damage, config.isDirect)
|
.takeDamage(configDamage, config.isDirect)
|
||||||
.then(updates => targetDamage.push({ token, updates }))
|
.then(updates => targetDamage.push({ token, updates }))
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.all(damagePromises).then(async _ => {
|
Promise.all(damagePromises).then(async _ => {
|
||||||
|
|
@ -153,7 +165,8 @@ export default class DamageField extends fields.SchemaField {
|
||||||
if (data.hasRoll && part.resultBased && data.roll.result.duality === -1) return part.valueAlt;
|
if (data.hasRoll && part.resultBased && data.roll.result.duality === -1) return part.valueAlt;
|
||||||
|
|
||||||
const isAdversary = this.actor.type === 'adversary';
|
const isAdversary = this.actor.type === 'adversary';
|
||||||
if (isAdversary && this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) {
|
const isHorde = this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id;
|
||||||
|
if (isAdversary && isHorde && this.roll?.isStandardAttack) {
|
||||||
const hasHordeDamage = this.actor.effects.find(x => x.type === 'horde');
|
const hasHordeDamage = this.actor.effects.find(x => x.type === 'horde');
|
||||||
if (hasHordeDamage && !hasHordeDamage.disabled) return part.valueAlt;
|
if (hasHordeDamage && !hasHordeDamage.disabled) return part.valueAlt;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ export default class EffectsField extends fields.ArrayField {
|
||||||
});
|
});
|
||||||
|
|
||||||
effects.forEach(async e => {
|
effects.forEach(async e => {
|
||||||
const effect = this.item.effects.get(e._id);
|
const effect = (this.item.applyEffects ?? this.item.effects).get(e._id);
|
||||||
if (!token.actor || !effect) return;
|
if (!token.actor || !effect) return;
|
||||||
await EffectsField.applyEffect(effect, token.actor);
|
await EffectsField.applyEffect(effect, token.actor);
|
||||||
});
|
});
|
||||||
|
|
@ -96,7 +96,7 @@ export default class EffectsField extends fields.ArrayField {
|
||||||
content: await foundry.applications.handlebars.renderTemplate(
|
content: await foundry.applications.handlebars.renderTemplate(
|
||||||
'systems/daggerheart/templates/ui/chat/effectSummary.hbs',
|
'systems/daggerheart/templates/ui/chat/effectSummary.hbs',
|
||||||
{
|
{
|
||||||
effects: this.effects.map(e => this.item.effects.get(e._id)),
|
effects: this.effects.map(e => (this.item.applyEffects ?? this.item.effects).get(e._id)),
|
||||||
targets: messageTargets
|
targets: messageTargets
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -123,7 +123,7 @@ export default class EffectsField extends fields.ArrayField {
|
||||||
|
|
||||||
// Otherwise, create a new effect on the target
|
// Otherwise, create a new effect on the target
|
||||||
const effectData = foundry.utils.mergeObject({
|
const effectData = foundry.utils.mergeObject({
|
||||||
...effect.toObject(),
|
...(effect.toObject?.() ?? effect),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
transfer: false,
|
transfer: false,
|
||||||
origin: effect.uuid
|
origin: effect.uuid
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,9 @@ export function ActionMixin(Base) {
|
||||||
}
|
}
|
||||||
|
|
||||||
get uuid() {
|
get uuid() {
|
||||||
return `${this.item.uuid}.${this.documentName}.${this.id}`;
|
const isItem = this.item instanceof game.system.api.documents.DHItem;
|
||||||
|
const isActor = this.item instanceof game.system.api.documents.DhpActor;
|
||||||
|
return isItem || isActor ? `${this.item.uuid}.${this.documentName}.${this.id}` : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get sheet() {
|
get sheet() {
|
||||||
|
|
@ -260,6 +262,9 @@ export function ActionMixin(Base) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async toChat(origin) {
|
async toChat(origin) {
|
||||||
|
const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
|
||||||
|
.expandRollMessage?.desc;
|
||||||
|
|
||||||
const cls = getDocumentClass('ChatMessage');
|
const cls = getDocumentClass('ChatMessage');
|
||||||
const systemData = {
|
const systemData = {
|
||||||
title: game.i18n.localize('DAGGERHEART.CONFIG.FeatureForm.action'),
|
title: game.i18n.localize('DAGGERHEART.CONFIG.FeatureForm.action'),
|
||||||
|
|
@ -288,7 +293,7 @@ export function ActionMixin(Base) {
|
||||||
system: systemData,
|
system: systemData,
|
||||||
content: await foundry.applications.handlebars.renderTemplate(
|
content: await foundry.applications.handlebars.renderTemplate(
|
||||||
'systems/daggerheart/templates/ui/chat/action.hbs',
|
'systems/daggerheart/templates/ui/chat/action.hbs',
|
||||||
systemData
|
{ ...systemData, open: autoExpandDescription ? 'open' : '' }
|
||||||
),
|
),
|
||||||
flags: {
|
flags: {
|
||||||
daggerheart: {
|
daggerheart: {
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,20 @@ const attributeField = label =>
|
||||||
});
|
});
|
||||||
|
|
||||||
const resourceField = (max = 0, initial = 0, label, reverse = false, maxLabel) =>
|
const resourceField = (max = 0, initial = 0, label, reverse = false, maxLabel) =>
|
||||||
new fields.SchemaField({
|
new fields.SchemaField(
|
||||||
value: new fields.NumberField({ initial: initial, min: 0, integer: true, label }),
|
{
|
||||||
max: new fields.NumberField({
|
value: new fields.NumberField({ initial: initial, min: 0, integer: true, label }),
|
||||||
initial: max,
|
max: new fields.NumberField({
|
||||||
integer: true,
|
initial: max,
|
||||||
label:
|
integer: true,
|
||||||
maxLabel ?? game.i18n.format('DAGGERHEART.GENERAL.maxWithThing', { thing: game.i18n.localize(label) })
|
label:
|
||||||
}),
|
maxLabel ??
|
||||||
isReversed: new fields.BooleanField({ initial: reverse })
|
game.i18n.format('DAGGERHEART.GENERAL.maxWithThing', { thing: game.i18n.localize(label) })
|
||||||
});
|
}),
|
||||||
|
isReversed: new fields.BooleanField({ initial: reverse })
|
||||||
|
},
|
||||||
|
{ label }
|
||||||
|
);
|
||||||
|
|
||||||
const stressDamageReductionRule = localizationPath =>
|
const stressDamageReductionRule = localizationPath =>
|
||||||
new fields.SchemaField({
|
new fields.SchemaField({
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import BaseDataItem from './base.mjs';
|
import BaseDataItem from './base.mjs';
|
||||||
import ItemLinkFields from '../../data/fields/itemLinkFields.mjs';
|
import ItemLinkFields from '../../data/fields/itemLinkFields.mjs';
|
||||||
|
import { getFeaturesHTMLData } from '../../helpers/utils.mjs';
|
||||||
|
|
||||||
export default class DHAncestry extends BaseDataItem {
|
export default class DHAncestry extends BaseDataItem {
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
|
|
@ -19,7 +20,6 @@ export default class DHAncestry extends BaseDataItem {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/**@override */
|
/**@override */
|
||||||
|
|
@ -42,4 +42,18 @@ export default class DHAncestry extends BaseDataItem {
|
||||||
get secondaryFeature() {
|
get secondaryFeature() {
|
||||||
return this.features.find(x => x.type === CONFIG.DH.ITEM.featureSubTypes.secondary)?.item;
|
return this.features.find(x => x.type === CONFIG.DH.ITEM.featureSubTypes.secondary)?.item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**@inheritdoc */
|
||||||
|
async getDescriptionData() {
|
||||||
|
const baseDescription = this.description;
|
||||||
|
const features = await getFeaturesHTMLData(this.features);
|
||||||
|
|
||||||
|
if (!features.length) return { prefix: null, value: baseDescription, suffix: null };
|
||||||
|
const suffix = await foundry.applications.handlebars.renderTemplate(
|
||||||
|
'systems/daggerheart/templates/sheets/items/description.hbs',
|
||||||
|
{ label: 'DAGGERHEART.ITEMS.Ancestry.featuresLabel', features }
|
||||||
|
);
|
||||||
|
|
||||||
|
return { prefix: null, value: baseDescription, suffix };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,7 @@ export default class DHArmor extends AttachableItem {
|
||||||
armorFeatures: new fields.ArrayField(
|
armorFeatures: new fields.ArrayField(
|
||||||
new fields.SchemaField({
|
new fields.SchemaField({
|
||||||
value: new fields.StringField({
|
value: new fields.StringField({
|
||||||
required: true,
|
required: true
|
||||||
choices: CONFIG.DH.ITEM.allArmorFeatures,
|
|
||||||
blank: true
|
|
||||||
}),
|
}),
|
||||||
effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
|
effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
|
||||||
actionIds: new fields.ArrayField(new fields.StringField({ required: true }))
|
actionIds: new fields.ArrayField(new fields.StringField({ required: true }))
|
||||||
|
|
@ -58,12 +56,11 @@ export default class DHArmor extends AttachableItem {
|
||||||
async getDescriptionData() {
|
async getDescriptionData() {
|
||||||
const baseDescription = this.description;
|
const baseDescription = this.description;
|
||||||
const allFeatures = CONFIG.DH.ITEM.allArmorFeatures();
|
const allFeatures = CONFIG.DH.ITEM.allArmorFeatures();
|
||||||
const features = this.armorFeatures.map(x => allFeatures[x.value]);
|
const features = this.armorFeatures.map(x => allFeatures[x.value]).filter(x => x);
|
||||||
if (!features.length) return { prefix: null, value: baseDescription, suffix: null };
|
|
||||||
|
|
||||||
const prefix = await foundry.applications.handlebars.renderTemplate(
|
const prefix = await foundry.applications.handlebars.renderTemplate(
|
||||||
'systems/daggerheart/templates/sheets/items/armor/description.hbs',
|
'systems/daggerheart/templates/sheets/items/armor/description.hbs',
|
||||||
{ features }
|
{ item: this.parent, features }
|
||||||
);
|
);
|
||||||
|
|
||||||
return { prefix, value: baseDescription, suffix: null };
|
return { prefix, value: baseDescription, suffix: null };
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
|
||||||
return await foundry.applications.ux.TextEditor.implementation.enrichHTML(fullDescription, {
|
return await foundry.applications.ux.TextEditor.implementation.enrichHTML(fullDescription, {
|
||||||
relativeTo: this,
|
relativeTo: this,
|
||||||
rollData: this.getRollData(),
|
rollData: this.getRollData(),
|
||||||
secrets: this.isOwner
|
secrets: this.parent.isOwner
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -253,4 +253,20 @@ export default class DHBeastform extends BaseDataItem {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onCreate(_data, _options, userId) {
|
||||||
|
if (!this.actor && game.user.id === userId) {
|
||||||
|
const hasBeastformEffect = this.parent.effects.some(x => x.type === 'beastform');
|
||||||
|
if (!hasBeastformEffect)
|
||||||
|
this.parent.createEmbeddedDocuments('ActiveEffect', [
|
||||||
|
{
|
||||||
|
type: 'beastform',
|
||||||
|
name: game.i18n.localize('DAGGERHEART.ITEMS.Beastform.beastformEffect'),
|
||||||
|
img: 'icons/creatures/abilities/paw-print-pair-purple.webp'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import BaseDataItem from './base.mjs';
|
||||||
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
|
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
|
||||||
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
|
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
|
||||||
import ItemLinkFields from '../fields/itemLinkFields.mjs';
|
import ItemLinkFields from '../fields/itemLinkFields.mjs';
|
||||||
import { addLinkedItemsDiff, updateLinkedItemApps } from '../../helpers/utils.mjs';
|
import { addLinkedItemsDiff, getFeaturesHTMLData, updateLinkedItemApps } from '../../helpers/utils.mjs';
|
||||||
|
|
||||||
export default class DHClass extends BaseDataItem {
|
export default class DHClass extends BaseDataItem {
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
|
|
@ -163,4 +163,56 @@ export default class DHClass extends BaseDataItem {
|
||||||
|
|
||||||
updateLinkedItemApps(options, this.parent.sheet);
|
updateLinkedItemApps(options, this.parent.sheet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**@inheritdoc */
|
||||||
|
async getDescriptionData() {
|
||||||
|
const baseDescription = this.description;
|
||||||
|
|
||||||
|
const getDomainLabel = domain => {
|
||||||
|
const data = CONFIG.DH.DOMAIN.allDomains()[domain];
|
||||||
|
return data ? game.i18n.localize(data.label) : '';
|
||||||
|
};
|
||||||
|
let domainsLabel = '';
|
||||||
|
if (this.domains.length) {
|
||||||
|
if (this.domains.length === 1) domainsLabel = getDomainLabel(this.domains[0]);
|
||||||
|
else {
|
||||||
|
const firstDomains = this.domains
|
||||||
|
.slice(0, this.domains.length - 1)
|
||||||
|
.map(getDomainLabel)
|
||||||
|
.join(', ');
|
||||||
|
const lastDomain = getDomainLabel(this.domains[this.domains.length - 1]);
|
||||||
|
domainsLabel = game.i18n.format('DAGGERHEART.GENERAL.thingsAndThing', {
|
||||||
|
things: firstDomains,
|
||||||
|
thing: lastDomain
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const classItems = [];
|
||||||
|
for (const itemData of this.inventory.choiceB) {
|
||||||
|
const linkData = [
|
||||||
|
undefined,
|
||||||
|
'UUID', // type
|
||||||
|
itemData.uuid // target
|
||||||
|
];
|
||||||
|
const contentLink = await foundry.applications.ux.TextEditor.implementation._createContentLink(linkData);
|
||||||
|
classItems.push(contentLink.outerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hopeFeatures = await getFeaturesHTMLData(this.hopeFeatures);
|
||||||
|
const classFeatures = await getFeaturesHTMLData(this.classFeatures);
|
||||||
|
|
||||||
|
const suffix = await foundry.applications.handlebars.renderTemplate(
|
||||||
|
'systems/daggerheart/templates/sheets/items/class/description.hbs',
|
||||||
|
{
|
||||||
|
class: this.parent,
|
||||||
|
domains: domainsLabel,
|
||||||
|
classItems,
|
||||||
|
hopeFeatures,
|
||||||
|
classFeatures
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return { prefix: null, value: baseDescription, suffix };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { getFeaturesHTMLData } from '../../helpers/utils.mjs';
|
||||||
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
|
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
|
||||||
import BaseDataItem from './base.mjs';
|
import BaseDataItem from './base.mjs';
|
||||||
|
|
||||||
|
|
@ -24,4 +25,17 @@ export default class DHCommunity extends BaseDataItem {
|
||||||
/**@override */
|
/**@override */
|
||||||
static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/items/village.svg';
|
static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/items/village.svg';
|
||||||
|
|
||||||
|
/**@inheritdoc */
|
||||||
|
async getDescriptionData() {
|
||||||
|
const baseDescription = this.description;
|
||||||
|
const features = await getFeaturesHTMLData(this.features);
|
||||||
|
|
||||||
|
if (!features.length) return { prefix: null, value: baseDescription, suffix: null };
|
||||||
|
const suffix = await foundry.applications.handlebars.renderTemplate(
|
||||||
|
'systems/daggerheart/templates/sheets/items/description.hbs',
|
||||||
|
{ label: 'DAGGERHEART.ITEMS.Community.featuresLabel', features }
|
||||||
|
);
|
||||||
|
|
||||||
|
return { prefix: null, value: baseDescription, suffix };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,8 +94,10 @@ export default class DHDomainCard extends BaseDataItem {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.actor.system.loadoutSlot.available) {
|
if (!this.actor.system.loadoutSlot.available && !this.loadoutIgnore) {
|
||||||
data.system.inVault = true;
|
data.system.inVault = true;
|
||||||
|
await this.updateSource({ inVault: true });
|
||||||
|
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.loadoutMaxReached'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { getFeaturesHTMLData } from '../../helpers/utils.mjs';
|
||||||
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
|
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
|
||||||
import ItemLinkFields from '../fields/itemLinkFields.mjs';
|
import ItemLinkFields from '../fields/itemLinkFields.mjs';
|
||||||
import BaseDataItem from './base.mjs';
|
import BaseDataItem from './base.mjs';
|
||||||
|
|
@ -89,4 +90,28 @@ export default class DHSubclass extends BaseDataItem {
|
||||||
const allowed = await super._preCreate(data, options, user);
|
const allowed = await super._preCreate(data, options, user);
|
||||||
if (allowed === false) return;
|
if (allowed === false) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**@inheritdoc */
|
||||||
|
async getDescriptionData() {
|
||||||
|
const baseDescription = this.description;
|
||||||
|
|
||||||
|
const spellcastTrait = this.spellcastingTrait
|
||||||
|
? game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.spellcastingTrait].label)
|
||||||
|
: null;
|
||||||
|
const foundationFeatures = await getFeaturesHTMLData(this.foundationFeatures);
|
||||||
|
const specializationFeatures = await getFeaturesHTMLData(this.specializationFeatures);
|
||||||
|
const masteryFeatures = await getFeaturesHTMLData(this.masteryFeatures);
|
||||||
|
|
||||||
|
const suffix = await foundry.applications.handlebars.renderTemplate(
|
||||||
|
'systems/daggerheart/templates/sheets/items/subclass/description.hbs',
|
||||||
|
{
|
||||||
|
spellcastTrait,
|
||||||
|
foundationFeatures,
|
||||||
|
specializationFeatures,
|
||||||
|
masteryFeatures
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return { prefix: null, value: baseDescription, suffix };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,7 @@ export default class DHWeapon extends AttachableItem {
|
||||||
weaponFeatures: new fields.ArrayField(
|
weaponFeatures: new fields.ArrayField(
|
||||||
new fields.SchemaField({
|
new fields.SchemaField({
|
||||||
value: new fields.StringField({
|
value: new fields.StringField({
|
||||||
required: true,
|
required: true
|
||||||
choices: CONFIG.DH.ITEM.allWeaponFeatures,
|
|
||||||
blank: true
|
|
||||||
}),
|
}),
|
||||||
effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
|
effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
|
||||||
actionIds: new fields.ArrayField(new fields.StringField({ required: true }))
|
actionIds: new fields.ArrayField(new fields.StringField({ required: true }))
|
||||||
|
|
@ -113,13 +111,26 @@ export default class DHWeapon extends AttachableItem {
|
||||||
/**@inheritdoc */
|
/**@inheritdoc */
|
||||||
async getDescriptionData() {
|
async getDescriptionData() {
|
||||||
const baseDescription = this.description;
|
const baseDescription = this.description;
|
||||||
|
|
||||||
|
const tier = game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${this.tier}`);
|
||||||
|
const trait = game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.attack.roll.trait].label);
|
||||||
|
const range = game.i18n.localize(`DAGGERHEART.CONFIG.Range.${this.attack.range}.name`);
|
||||||
|
const damage = Roll.replaceFormulaData(this.attack.damageFormula, this.parent.parent ?? this.parent);
|
||||||
|
const burden = game.i18n.localize(CONFIG.DH.GENERAL.burden[this.burden].label);
|
||||||
|
|
||||||
const allFeatures = CONFIG.DH.ITEM.allWeaponFeatures();
|
const allFeatures = CONFIG.DH.ITEM.allWeaponFeatures();
|
||||||
const features = this.weaponFeatures.map(x => allFeatures[x.value]);
|
const features = this.weaponFeatures.map(x => allFeatures[x.value]).filter(x => x);
|
||||||
if (!features.length) return { prefix: null, value: baseDescription, suffix: null };
|
|
||||||
|
|
||||||
const prefix = await foundry.applications.handlebars.renderTemplate(
|
const prefix = await foundry.applications.handlebars.renderTemplate(
|
||||||
'systems/daggerheart/templates/sheets/items/weapon/description.hbs',
|
'systems/daggerheart/templates/sheets/items/weapon/description.hbs',
|
||||||
{ features }
|
{
|
||||||
|
features,
|
||||||
|
tier,
|
||||||
|
trait,
|
||||||
|
range,
|
||||||
|
damage,
|
||||||
|
burden
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return { prefix, value: baseDescription, suffix: null };
|
return { prefix, value: baseDescription, suffix: null };
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,12 @@ export default class DhLevelData extends foundry.abstract.DataModel {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
level: new fields.SchemaField({
|
level: new fields.SchemaField({
|
||||||
current: new fields.NumberField({ required: true, integer: true, initial: 1 }),
|
current: new fields.NumberField({
|
||||||
|
required: true,
|
||||||
|
integer: true,
|
||||||
|
initial: 1,
|
||||||
|
label: 'DAGGERHEART.GENERAL.currentLevel'
|
||||||
|
}),
|
||||||
changed: new fields.NumberField({ required: true, integer: true, initial: 1 }),
|
changed: new fields.NumberField({ required: true, integer: true, initial: 1 }),
|
||||||
bonuses: new fields.TypedObjectField(new fields.NumberField({ integer: true, nullable: false }))
|
bonuses: new fields.TypedObjectField(new fields.NumberField({ integer: true, nullable: false }))
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export default class RegisteredTriggers extends Map {
|
||||||
}
|
}
|
||||||
|
|
||||||
registerItemTriggers(item, registerOverride) {
|
registerItemTriggers(item, registerOverride) {
|
||||||
|
if (!item.actor || !item._stats.createdTime) return;
|
||||||
for (const action of item.system.actions ?? []) {
|
for (const action of item.system.actions ?? []) {
|
||||||
if (!action.actor) continue;
|
if (!action.actor) continue;
|
||||||
|
|
||||||
|
|
@ -71,10 +72,21 @@ export default class RegisteredTriggers extends Map {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unregisterSceneEnvironmentTriggers(flagSystemData) {
|
||||||
|
const sceneData = new game.system.api.data.scenes.DHScene(flagSystemData);
|
||||||
|
for (const environment of sceneData.sceneEnvironments) {
|
||||||
|
if (!environment || environment.pack) continue;
|
||||||
|
this.unregisterItemTriggers(environment.system.features);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
unregisterSceneTriggers(scene) {
|
unregisterSceneTriggers(scene) {
|
||||||
|
this.unregisterSceneEnvironmentTriggers(scene.flags.daggerheart);
|
||||||
|
|
||||||
for (const triggerKey of Object.keys(CONFIG.DH.TRIGGER.triggers)) {
|
for (const triggerKey of Object.keys(CONFIG.DH.TRIGGER.triggers)) {
|
||||||
const existingTrigger = this.get(triggerKey);
|
const existingTrigger = this.get(triggerKey);
|
||||||
if (!existingTrigger) continue;
|
if (!existingTrigger) continue;
|
||||||
|
|
||||||
const filtered = new Map();
|
const filtered = new Map();
|
||||||
for (const [uuid, data] of existingTrigger.entries()) {
|
for (const [uuid, data] of existingTrigger.entries()) {
|
||||||
if (!uuid.startsWith(scene.uuid)) filtered.set(uuid, data);
|
if (!uuid.startsWith(scene.uuid)) filtered.set(uuid, data);
|
||||||
|
|
@ -83,14 +95,17 @@ export default class RegisteredTriggers extends Map {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerSceneEnvironmentTriggers(flagSystemData) {
|
||||||
|
const sceneData = new game.system.api.data.scenes.DHScene(flagSystemData);
|
||||||
|
for (const environment of sceneData.sceneEnvironments) {
|
||||||
|
for (const feature of environment.system.features) {
|
||||||
|
if (feature) this.registerItemTriggers(feature, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
registerSceneTriggers(scene) {
|
registerSceneTriggers(scene) {
|
||||||
/* TODO: Finish sceneEnvironment registration and unreg */
|
this.registerSceneEnvironmentTriggers(scene.flags.daggerheart);
|
||||||
// const systemData = new game.system.api.data.scenes.DHScene(scene.flags.daggerheart);
|
|
||||||
// for (const environment of systemData.sceneEnvironments) {
|
|
||||||
// for (const feature of environment.system.features) {
|
|
||||||
// if(feature) this.registerItemTriggers(feature, true);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
for (const actor of scene.tokens.filter(x => x.actor).map(x => x.actor)) {
|
for (const actor of scene.tokens.filter(x => x.actor).map(x => x.actor)) {
|
||||||
if (actor.prototypeToken.actorLink) continue;
|
if (actor.prototypeToken.actorLink) continue;
|
||||||
|
|
@ -107,13 +122,11 @@ export default class RegisteredTriggers extends Map {
|
||||||
if (!triggerSettings.enabled) return updates;
|
if (!triggerSettings.enabled) return updates;
|
||||||
|
|
||||||
const dualityTrigger = this.get(trigger);
|
const dualityTrigger = this.get(trigger);
|
||||||
if (dualityTrigger) {
|
if (dualityTrigger?.size) {
|
||||||
const tokenBoundActors = ['adversary', 'environment'];
|
const triggerActors = ['character', 'adversary', 'environment'];
|
||||||
const triggerActors = ['character', ...tokenBoundActors];
|
|
||||||
for (let [itemUuid, { actor: actorUuid, triggeringActorType, commands }] of dualityTrigger.entries()) {
|
for (let [itemUuid, { actor: actorUuid, triggeringActorType, commands }] of dualityTrigger.entries()) {
|
||||||
const actor = await foundry.utils.fromUuid(actorUuid);
|
const actor = await foundry.utils.fromUuid(actorUuid);
|
||||||
if (!actor || !triggerActors.includes(actor.type)) continue;
|
if (!actor || !triggerActors.includes(actor.type)) continue;
|
||||||
if (tokenBoundActors.includes(actor.type) && !actor.getActiveTokens().length) continue;
|
|
||||||
|
|
||||||
const triggerData = CONFIG.DH.TRIGGER.triggers[trigger];
|
const triggerData = CONFIG.DH.TRIGGER.triggers[trigger];
|
||||||
if (triggerData.usesActor && triggeringActorType !== 'any') {
|
if (triggerData.usesActor && triggeringActorType !== 'any') {
|
||||||
|
|
|
||||||
38
module/data/rollTable.mjs
Normal file
38
module/data/rollTable.mjs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import FormulaField from './fields/formulaField.mjs';
|
||||||
|
|
||||||
|
//Extra definitions for RollTable
|
||||||
|
export default class DhRollTable extends foundry.abstract.TypeDataModel {
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
return {
|
||||||
|
formulaName: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
nullable: false,
|
||||||
|
initial: 'Roll Formula',
|
||||||
|
label: 'DAGGERHEART.ROLLTABLES.FIELDS.formulaName.label'
|
||||||
|
}),
|
||||||
|
altFormula: new fields.TypedObjectField(
|
||||||
|
new fields.SchemaField({
|
||||||
|
name: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
nullable: false,
|
||||||
|
initial: 'Roll Formula',
|
||||||
|
label: 'DAGGERHEART.ROLLTABLES.FIELDS.formulaName.label'
|
||||||
|
}),
|
||||||
|
formula: new FormulaField({ label: 'Formula Roll', initial: '1d20' })
|
||||||
|
})
|
||||||
|
),
|
||||||
|
activeAltFormula: new fields.StringField({ nullable: true, initial: null })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveFormula(baseFormula) {
|
||||||
|
return this.activeAltFormula ? (this.altFormula[this.activeAltFormula]?.formula ?? baseFormula) : baseFormula;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDefaultFormula = () => ({
|
||||||
|
name: game.i18n.localize('Roll Formula'),
|
||||||
|
formula: '1d20'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -37,11 +37,30 @@ export default class DhAppearance extends foundry.abstract.DataModel {
|
||||||
extendEnvironmentDescriptions: new BooleanField(),
|
extendEnvironmentDescriptions: new BooleanField(),
|
||||||
extendItemDescriptions: new BooleanField(),
|
extendItemDescriptions: new BooleanField(),
|
||||||
expandRollMessage: new SchemaField({
|
expandRollMessage: new SchemaField({
|
||||||
desc: new BooleanField(),
|
desc: new BooleanField({ initial: true }),
|
||||||
roll: new BooleanField(),
|
roll: new BooleanField(),
|
||||||
damage: new BooleanField(),
|
damage: new BooleanField(),
|
||||||
target: new BooleanField()
|
target: new BooleanField()
|
||||||
}),
|
}),
|
||||||
|
showTokenDistance: new StringField({
|
||||||
|
required: true,
|
||||||
|
choices: {
|
||||||
|
always: {
|
||||||
|
value: 'always',
|
||||||
|
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.showTokenDistance.choices.always'
|
||||||
|
},
|
||||||
|
encounters: {
|
||||||
|
value: 'encounters',
|
||||||
|
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.showTokenDistance.choices.encounters'
|
||||||
|
},
|
||||||
|
never: {
|
||||||
|
value: 'never',
|
||||||
|
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.showTokenDistance.choices.never'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nullable: false,
|
||||||
|
initial: 'always'
|
||||||
|
}),
|
||||||
hideAttribution: new BooleanField(),
|
hideAttribution: new BooleanField(),
|
||||||
showGenericStatusEffects: new BooleanField({ initial: true })
|
showGenericStatusEffects: new BooleanField({ initial: true })
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ export default class DhAutomation extends foundry.abstract.DataModel {
|
||||||
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hopeFear.players.label'
|
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hopeFear.players.label'
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
vulnerableAutomation: new fields.BooleanField({
|
||||||
|
initial: true,
|
||||||
|
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.vulnerableAutomation.label'
|
||||||
|
}),
|
||||||
countdownAutomation: new fields.BooleanField({
|
countdownAutomation: new fields.BooleanField({
|
||||||
required: true,
|
required: true,
|
||||||
initial: true,
|
initial: true,
|
||||||
|
|
@ -55,15 +59,27 @@ export default class DhAutomation extends foundry.abstract.DataModel {
|
||||||
initial: true,
|
initial: true,
|
||||||
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.resourceScrollTexts.label'
|
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.resourceScrollTexts.label'
|
||||||
}),
|
}),
|
||||||
playerCanEditSheet: new fields.BooleanField({
|
deathMoveAutomation: new fields.SchemaField({
|
||||||
required: true,
|
avoidDeath: new fields.BooleanField({
|
||||||
initial: false,
|
required: true,
|
||||||
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.playerCanEditSheet.label'
|
initial: true,
|
||||||
|
label: 'DAGGERHEART.CONFIG.DeathMoves.avoidDeath.name'
|
||||||
|
}),
|
||||||
|
riskItAll: new fields.BooleanField({
|
||||||
|
required: true,
|
||||||
|
initial: true,
|
||||||
|
label: 'DAGGERHEART.CONFIG.DeathMoves.riskItAll.name'
|
||||||
|
}),
|
||||||
|
blazeOfGlory: new fields.BooleanField({
|
||||||
|
required: true,
|
||||||
|
initial: true,
|
||||||
|
label: 'DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.name'
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
defeated: new fields.SchemaField({
|
defeated: new fields.SchemaField({
|
||||||
enabled: new fields.BooleanField({
|
enabled: new fields.BooleanField({
|
||||||
required: true,
|
required: true,
|
||||||
initial: false,
|
initial: true,
|
||||||
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.enabled.label'
|
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.enabled.label'
|
||||||
}),
|
}),
|
||||||
overlay: new fields.BooleanField({
|
overlay: new fields.BooleanField({
|
||||||
|
|
@ -74,7 +90,7 @@ export default class DhAutomation extends foundry.abstract.DataModel {
|
||||||
characterDefault: new fields.StringField({
|
characterDefault: new fields.StringField({
|
||||||
required: true,
|
required: true,
|
||||||
choices: CONFIG.DH.GENERAL.defeatedConditionChoices,
|
choices: CONFIG.DH.GENERAL.defeatedConditionChoices,
|
||||||
initial: CONFIG.DH.GENERAL.defeatedConditionChoices.unconscious.id,
|
initial: CONFIG.DH.GENERAL.defeatedConditionChoices.deathMove.id,
|
||||||
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.characterDefault.label'
|
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.characterDefault.label'
|
||||||
}),
|
}),
|
||||||
adversaryDefault: new fields.StringField({
|
adversaryDefault: new fields.StringField({
|
||||||
|
|
@ -89,23 +105,29 @@ export default class DhAutomation extends foundry.abstract.DataModel {
|
||||||
initial: CONFIG.DH.GENERAL.defeatedConditionChoices.defeated.id,
|
initial: CONFIG.DH.GENERAL.defeatedConditionChoices.defeated.id,
|
||||||
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.companionDefault.label'
|
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.companionDefault.label'
|
||||||
}),
|
}),
|
||||||
|
deathMoveIcon: new fields.FilePathField({
|
||||||
|
initial: 'icons/magic/life/heart-cross-purple-orange.webp',
|
||||||
|
categories: ['IMAGE'],
|
||||||
|
base64: false,
|
||||||
|
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.deathMove.label'
|
||||||
|
}),
|
||||||
deadIcon: new fields.FilePathField({
|
deadIcon: new fields.FilePathField({
|
||||||
initial: 'icons/magic/death/grave-tombstone-glow-teal.webp',
|
initial: 'icons/magic/death/grave-tombstone-glow-teal.webp',
|
||||||
categories: ['IMAGE'],
|
categories: ['IMAGE'],
|
||||||
base64: false,
|
base64: false,
|
||||||
label: 'Dead'
|
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.dead.label'
|
||||||
}),
|
}),
|
||||||
defeatedIcon: new fields.FilePathField({
|
defeatedIcon: new fields.FilePathField({
|
||||||
initial: 'icons/magic/control/fear-fright-mask-orange.webp',
|
initial: 'icons/magic/control/fear-fright-mask-orange.webp',
|
||||||
categories: ['IMAGE'],
|
categories: ['IMAGE'],
|
||||||
base64: false,
|
base64: false,
|
||||||
label: 'Defeated'
|
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.defeated.label'
|
||||||
}),
|
}),
|
||||||
unconsciousIcon: new fields.FilePathField({
|
unconsciousIcon: new fields.FilePathField({
|
||||||
initial: 'icons/magic/control/sleep-bubble-purple.webp',
|
initial: 'icons/magic/control/sleep-bubble-purple.webp',
|
||||||
categories: ['IMAGE'],
|
categories: ['IMAGE'],
|
||||||
base64: false,
|
base64: false,
|
||||||
label: 'Unconcious'
|
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.unconscious.label'
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
roll: new fields.SchemaField({
|
roll: new fields.SchemaField({
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,20 @@ const currencyField = (initial, label, icon) =>
|
||||||
icon: new foundry.data.fields.StringField({ required: true, nullable: false, blank: true, initial: icon })
|
icon: new foundry.data.fields.StringField({ required: true, nullable: false, blank: true, initial: icon })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const restMoveField = () =>
|
||||||
|
new foundry.data.fields.SchemaField({
|
||||||
|
name: new foundry.data.fields.StringField({ required: true }),
|
||||||
|
icon: new foundry.data.fields.StringField({ required: true }),
|
||||||
|
img: new foundry.data.fields.FilePathField({
|
||||||
|
initial: 'icons/magic/life/cross-worn-green.webp',
|
||||||
|
categories: ['IMAGE'],
|
||||||
|
base64: false
|
||||||
|
}),
|
||||||
|
description: new foundry.data.fields.HTMLField(),
|
||||||
|
actions: new ActionsField(),
|
||||||
|
effects: new foundry.data.fields.ArrayField(new foundry.data.fields.ObjectField())
|
||||||
|
});
|
||||||
|
|
||||||
export default class DhHomebrew extends foundry.abstract.DataModel {
|
export default class DhHomebrew extends foundry.abstract.DataModel {
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
@ -23,6 +37,13 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
|
||||||
initial: 12,
|
initial: 12,
|
||||||
label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxFear.label'
|
label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxFear.label'
|
||||||
}),
|
}),
|
||||||
|
maxHope: new fields.NumberField({
|
||||||
|
required: true,
|
||||||
|
integer: true,
|
||||||
|
min: 0,
|
||||||
|
initial: 6,
|
||||||
|
label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxHope.label'
|
||||||
|
}),
|
||||||
maxLoadout: new fields.NumberField({
|
maxLoadout: new fields.NumberField({
|
||||||
required: true,
|
required: true,
|
||||||
integer: true,
|
integer: true,
|
||||||
|
|
@ -98,37 +119,11 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
|
||||||
restMoves: new fields.SchemaField({
|
restMoves: new fields.SchemaField({
|
||||||
longRest: new fields.SchemaField({
|
longRest: new fields.SchemaField({
|
||||||
nrChoices: new fields.NumberField({ required: true, integer: true, min: 1, initial: 2 }),
|
nrChoices: new fields.NumberField({ required: true, integer: true, min: 1, initial: 2 }),
|
||||||
moves: new fields.TypedObjectField(
|
moves: new fields.TypedObjectField(restMoveField(), { initial: defaultRestOptions.longRest() })
|
||||||
new fields.SchemaField({
|
|
||||||
name: new fields.StringField({ required: true }),
|
|
||||||
icon: new fields.StringField({ required: true }),
|
|
||||||
img: new fields.FilePathField({
|
|
||||||
initial: 'icons/magic/life/cross-worn-green.webp',
|
|
||||||
categories: ['IMAGE'],
|
|
||||||
base64: false
|
|
||||||
}),
|
|
||||||
description: new fields.HTMLField(),
|
|
||||||
actions: new ActionsField()
|
|
||||||
}),
|
|
||||||
{ initial: defaultRestOptions.longRest() }
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
shortRest: new fields.SchemaField({
|
shortRest: new fields.SchemaField({
|
||||||
nrChoices: new fields.NumberField({ required: true, integer: true, min: 1, initial: 2 }),
|
nrChoices: new fields.NumberField({ required: true, integer: true, min: 1, initial: 2 }),
|
||||||
moves: new fields.TypedObjectField(
|
moves: new fields.TypedObjectField(restMoveField(), { initial: defaultRestOptions.shortRest() })
|
||||||
new fields.SchemaField({
|
|
||||||
name: new fields.StringField({ required: true }),
|
|
||||||
icon: new fields.StringField({ required: true }),
|
|
||||||
img: new fields.FilePathField({
|
|
||||||
initial: 'icons/magic/life/cross-worn-green.webp',
|
|
||||||
categories: ['IMAGE'],
|
|
||||||
base64: false
|
|
||||||
}),
|
|
||||||
description: new fields.HTMLField(),
|
|
||||||
actions: new ActionsField()
|
|
||||||
}),
|
|
||||||
{ initial: defaultRestOptions.shortRest() }
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
domains: new fields.TypedObjectField(
|
domains: new fields.TypedObjectField(
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,4 @@ export { default as D20Roll } from './d20Roll.mjs';
|
||||||
export { default as DamageRoll } from './damageRoll.mjs';
|
export { default as DamageRoll } from './damageRoll.mjs';
|
||||||
export { default as DHRoll } from './dhRoll.mjs';
|
export { default as DHRoll } from './dhRoll.mjs';
|
||||||
export { default as DualityRoll } from './dualityRoll.mjs';
|
export { default as DualityRoll } from './dualityRoll.mjs';
|
||||||
|
export { default as FateRoll } from './fateRoll.mjs';
|
||||||
|
|
|
||||||
|
|
@ -99,11 +99,14 @@ export default class D20Roll extends DHRoll {
|
||||||
|
|
||||||
this.options.roll.modifiers = this.applyBaseBonus();
|
this.options.roll.modifiers = this.applyBaseBonus();
|
||||||
|
|
||||||
|
const actorExperiences = this.options.roll.companionRoll
|
||||||
|
? (this.options.data?.companion?.system.experiences ?? {})
|
||||||
|
: (this.options.data.system?.experiences ?? {});
|
||||||
this.options.experiences?.forEach(m => {
|
this.options.experiences?.forEach(m => {
|
||||||
if (this.options.data.system?.experiences?.[m])
|
if (actorExperiences[m])
|
||||||
this.options.roll.modifiers.push({
|
this.options.roll.modifiers.push({
|
||||||
label: this.options.data.system.experiences[m].name,
|
label: actorExperiences[m].name,
|
||||||
value: this.options.data.system.experiences[m].value
|
value: actorExperiences[m].value
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import DamageDialog from '../applications/dialogs/damageDialog.mjs';
|
import DamageDialog from '../applications/dialogs/damageDialog.mjs';
|
||||||
|
import { parseRallyDice } from '../helpers/utils.mjs';
|
||||||
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
|
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
|
||||||
import DHRoll from './dhRoll.mjs';
|
import DHRoll from './dhRoll.mjs';
|
||||||
|
|
||||||
|
|
@ -33,7 +34,7 @@ export default class DamageRoll extends DHRoll {
|
||||||
static async buildPost(roll, config, message) {
|
static async buildPost(roll, config, message) {
|
||||||
const chatMessage = config.source?.message
|
const chatMessage = config.source?.message
|
||||||
? ui.chat.collection.get(config.source.message)
|
? ui.chat.collection.get(config.source.message)
|
||||||
: getDocumentClass('ChatMessage').applyRollMode({}, config.rollMode);
|
: getDocumentClass('ChatMessage').applyRollMode({}, config.rollMode ?? CONST.DICE_ROLL_MODES.PUBLIC);
|
||||||
if (game.modules.get('dice-so-nice')?.active) {
|
if (game.modules.get('dice-so-nice')?.active) {
|
||||||
const pool = foundry.dice.terms.PoolTerm.fromRolls(
|
const pool = foundry.dice.terms.PoolTerm.fromRolls(
|
||||||
Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll))
|
Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll))
|
||||||
|
|
@ -46,9 +47,14 @@ export default class DamageRoll extends DHRoll {
|
||||||
chatMessage.whisper?.length > 0 ? chatMessage.whisper : null,
|
chatMessage.whisper?.length > 0 ? chatMessage.whisper : null,
|
||||||
chatMessage.blind
|
chatMessage.blind
|
||||||
);
|
);
|
||||||
|
config.mute = true;
|
||||||
}
|
}
|
||||||
await super.buildPost(roll, config, message);
|
await super.buildPost(roll, config, message);
|
||||||
if (config.source?.message) chatMessage.update({ 'system.damage': config.damage });
|
if (config.source?.message) {
|
||||||
|
chatMessage.update({ 'system.damage': config.damage });
|
||||||
|
|
||||||
|
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static unifyDamageRoll(rolls) {
|
static unifyDamageRoll(rolls) {
|
||||||
|
|
@ -192,7 +198,7 @@ export default class DamageRoll extends DHRoll {
|
||||||
// Bardic Rally
|
// Bardic Rally
|
||||||
const rallyChoices = config.data?.parent?.appliedEffects.reduce((a, c) => {
|
const rallyChoices = config.data?.parent?.appliedEffects.reduce((a, c) => {
|
||||||
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
|
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
|
||||||
if (change) a.push({ value: c.id, label: change.value });
|
if (change) a.push({ value: c.id, label: parseRallyDice(change.value, c) });
|
||||||
return a;
|
return a;
|
||||||
}, []);
|
}, []);
|
||||||
if (rallyChoices.length) {
|
if (rallyChoices.length) {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
|
||||||
export default class DHRoll extends Roll {
|
export default class DHRoll extends Roll {
|
||||||
baseTerms = [];
|
baseTerms = [];
|
||||||
constructor(formula, data = {}, options = {}) {
|
constructor(formula, data = {}, options = {}) {
|
||||||
super(formula, data, options);
|
super(formula, data, foundry.utils.mergeObject(options, { roll: [] }, { overwrite: false }));
|
||||||
options.bonusEffects = this.bonusEffectBuilder();
|
options.bonusEffects = this.bonusEffectBuilder();
|
||||||
if (!this.data || !Object.keys(this.data).length) this.data = options.data;
|
if (!this.data || !Object.keys(this.data).length) this.data = options.data;
|
||||||
}
|
}
|
||||||
|
|
@ -96,6 +96,19 @@ export default class DHRoll extends Roll {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async toMessage(roll, config) {
|
static async toMessage(roll, config) {
|
||||||
|
const item = config.data.parent?.items?.get?.(config.source.item) ?? null;
|
||||||
|
const action = item ? item.system.actions.get(config.source.action) : null;
|
||||||
|
let actionDescription = null;
|
||||||
|
if (action?.chatDisplay) {
|
||||||
|
actionDescription = action
|
||||||
|
? await foundry.applications.ux.TextEditor.implementation.enrichHTML(action.description, {
|
||||||
|
relativeTo: config.data,
|
||||||
|
rollData: config.data.getRollData?.() ?? {}
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
config.actionChatMessageHandled = true;
|
||||||
|
}
|
||||||
|
|
||||||
const cls = getDocumentClass('ChatMessage'),
|
const cls = getDocumentClass('ChatMessage'),
|
||||||
msgData = {
|
msgData = {
|
||||||
type: this.messageType,
|
type: this.messageType,
|
||||||
|
|
@ -103,7 +116,7 @@ export default class DHRoll extends Roll {
|
||||||
title: roll.title,
|
title: roll.title,
|
||||||
speaker: cls.getSpeaker({ actor: roll.data?.parent }),
|
speaker: cls.getSpeaker({ actor: roll.data?.parent }),
|
||||||
sound: config.mute ? null : CONFIG.sounds.dice,
|
sound: config.mute ? null : CONFIG.sounds.dice,
|
||||||
system: config,
|
system: { ...config, actionDescription },
|
||||||
rolls: [roll]
|
rolls: [roll]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
|
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
|
||||||
import D20Roll from './d20Roll.mjs';
|
import D20Roll from './d20Roll.mjs';
|
||||||
import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
|
import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
|
||||||
import { getDiceSoNicePresets } from '../config/generalConfig.mjs';
|
import { getDiceSoNicePresets } from '../config/generalConfig.mjs';
|
||||||
import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
|
import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
|
||||||
|
|
||||||
|
|
@ -12,6 +12,7 @@ export default class DualityRoll extends D20Roll {
|
||||||
constructor(formula, data = {}, options = {}) {
|
constructor(formula, data = {}, options = {}) {
|
||||||
super(formula, data, options);
|
super(formula, data, options);
|
||||||
this.rallyChoices = this.setRallyChoices();
|
this.rallyChoices = this.setRallyChoices();
|
||||||
|
this.guaranteedCritical = options.guaranteedCritical;
|
||||||
}
|
}
|
||||||
|
|
||||||
static messageType = 'dualityRoll';
|
static messageType = 'dualityRoll';
|
||||||
|
|
@ -25,29 +26,23 @@ export default class DualityRoll extends D20Roll {
|
||||||
}
|
}
|
||||||
|
|
||||||
get dHope() {
|
get dHope() {
|
||||||
// if ( !(this.terms[0] instanceof foundry.dice.terms.Die) ) return;
|
|
||||||
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
||||||
return this.dice[0];
|
return this.dice[0];
|
||||||
// return this.#hopeDice;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set dHope(faces) {
|
set dHope(faces) {
|
||||||
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
||||||
this.terms[0].faces = this.getFaces(faces);
|
this.dice[0].faces = this.getFaces(faces);
|
||||||
// this.#hopeDice = `d${face}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get dFear() {
|
get dFear() {
|
||||||
// if ( !(this.terms[1] instanceof foundry.dice.terms.Die) ) return;
|
|
||||||
if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
||||||
return this.dice[1];
|
return this.dice[1];
|
||||||
// return this.#fearDice;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set dFear(faces) {
|
set dFear(faces) {
|
||||||
if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
||||||
this.dice[1].faces = this.getFaces(faces);
|
this.dice[1].faces = this.getFaces(faces);
|
||||||
// this.#fearDice = `d${face}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get dAdvantage() {
|
get dAdvantage() {
|
||||||
|
|
@ -73,7 +68,7 @@ export default class DualityRoll extends D20Roll {
|
||||||
setRallyChoices() {
|
setRallyChoices() {
|
||||||
return this.data?.parent?.appliedEffects.reduce((a, c) => {
|
return this.data?.parent?.appliedEffects.reduce((a, c) => {
|
||||||
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
|
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
|
||||||
if (change) a.push({ value: c.id, label: change.value });
|
if (change) a.push({ value: c.id, label: parseRallyDice(change.value, c) });
|
||||||
return a;
|
return a;
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
@ -90,26 +85,29 @@ export default class DualityRoll extends D20Roll {
|
||||||
}
|
}
|
||||||
|
|
||||||
get isCritical() {
|
get isCritical() {
|
||||||
|
if (this.guaranteedCritical) return true;
|
||||||
if (!this.dHope._evaluated || !this.dFear._evaluated) return;
|
if (!this.dHope._evaluated || !this.dFear._evaluated) return;
|
||||||
return this.dHope.total === this.dFear.total;
|
return this.dHope.total === this.dFear.total;
|
||||||
}
|
}
|
||||||
|
|
||||||
get withHope() {
|
get withHope() {
|
||||||
if (!this._evaluated) return;
|
if (!this._evaluated || this.guaranteedCritical) return;
|
||||||
return this.dHope.total > this.dFear.total;
|
return this.dHope.total > this.dFear.total;
|
||||||
}
|
}
|
||||||
|
|
||||||
get withFear() {
|
get withFear() {
|
||||||
if (!this._evaluated) return;
|
if (!this._evaluated || this.guaranteedCritical) return;
|
||||||
return this.dHope.total < this.dFear.total;
|
return this.dHope.total < this.dFear.total;
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalLabel() {
|
get totalLabel() {
|
||||||
const label = this.withHope
|
const label = this.guaranteedCritical
|
||||||
? 'DAGGERHEART.GENERAL.hope'
|
? 'DAGGERHEART.GENERAL.guaranteedCriticalSuccess'
|
||||||
: this.withFear
|
: this.isCritical
|
||||||
? 'DAGGERHEART.GENERAL.fear'
|
? 'DAGGERHEART.GENERAL.criticalSuccess'
|
||||||
: 'DAGGERHEART.GENERAL.criticalSuccess';
|
: this.withHope
|
||||||
|
? 'DAGGERHEART.GENERAL.hope'
|
||||||
|
: 'DAGGERHEART.GENERAL.fear';
|
||||||
|
|
||||||
return game.i18n.localize(label);
|
return game.i18n.localize(label);
|
||||||
}
|
}
|
||||||
|
|
@ -178,6 +176,21 @@ export default class DualityRoll extends D20Roll {
|
||||||
return modifiers;
|
return modifiers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
if (change) a = true;
|
||||||
|
return a;
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
if (config.guaranteedCritical) {
|
||||||
|
config.dialog.configure = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.buildConfigure(config, message);
|
||||||
|
}
|
||||||
|
|
||||||
getActionChangeKeys() {
|
getActionChangeKeys() {
|
||||||
const changeKeys = new Set([`system.bonuses.roll.${this.options.actionType}`]);
|
const changeKeys = new Set([`system.bonuses.roll.${this.options.actionType}`]);
|
||||||
|
|
||||||
|
|
@ -223,7 +236,7 @@ export default class DualityRoll extends D20Roll {
|
||||||
|
|
||||||
data.hope = {
|
data.hope = {
|
||||||
dice: roll.dHope.denomination,
|
dice: roll.dHope.denomination,
|
||||||
value: roll.dHope.total,
|
value: this.guaranteedCritical ? 0 : roll.dHope.total,
|
||||||
rerolled: {
|
rerolled: {
|
||||||
any: roll.dHope.results.some(x => x.rerolled),
|
any: roll.dHope.results.some(x => x.rerolled),
|
||||||
rerolls: roll.dHope.results.filter(x => x.rerolled)
|
rerolls: roll.dHope.results.filter(x => x.rerolled)
|
||||||
|
|
@ -231,7 +244,7 @@ export default class DualityRoll extends D20Roll {
|
||||||
};
|
};
|
||||||
data.fear = {
|
data.fear = {
|
||||||
dice: roll.dFear.denomination,
|
dice: roll.dFear.denomination,
|
||||||
value: roll.dFear.total,
|
value: this.guaranteedCritical ? 0 : roll.dFear.total,
|
||||||
rerolled: {
|
rerolled: {
|
||||||
any: roll.dFear.results.some(x => x.rerolled),
|
any: roll.dFear.results.some(x => x.rerolled),
|
||||||
rerolls: roll.dFear.results.filter(x => x.rerolled)
|
rerolls: roll.dFear.results.filter(x => x.rerolled)
|
||||||
|
|
@ -243,7 +256,7 @@ export default class DualityRoll extends D20Roll {
|
||||||
};
|
};
|
||||||
data.result = {
|
data.result = {
|
||||||
duality: roll.withHope ? 1 : roll.withFear ? -1 : 0,
|
duality: roll.withHope ? 1 : roll.withFear ? -1 : 0,
|
||||||
total: roll.dHope.total + roll.dFear.total,
|
total: this.guaranteedCritical ? 0 : roll.dHope.total + roll.dFear.total,
|
||||||
label: roll.totalLabel
|
label: roll.totalLabel
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -261,7 +274,7 @@ export default class DualityRoll extends D20Roll {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async handleTriggers(roll, config) {
|
static async handleTriggers(roll, config) {
|
||||||
if (!config.source?.actor) return;
|
if (!config.source?.actor || config.skips?.triggers) return;
|
||||||
|
|
||||||
const updates = [];
|
const updates = [];
|
||||||
const dualityUpdates = await game.system.registeredTriggers.runTrigger(
|
const dualityUpdates = await game.system.registeredTriggers.runTrigger(
|
||||||
|
|
@ -396,7 +409,9 @@ export default class DualityRoll extends D20Roll {
|
||||||
difficulty: message.system.roll.difficulty ? Number(message.system.roll.difficulty) : null
|
difficulty: message.system.roll.difficulty ? Number(message.system.roll.difficulty) : null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
newRoll.extra = newRoll.extra.slice(2);
|
|
||||||
|
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 tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
|
||||||
|
|
||||||
|
|
|
||||||
85
module/dice/fateRoll.mjs
Normal file
85
module/dice/fateRoll.mjs
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
|
||||||
|
import D20Roll from './d20Roll.mjs';
|
||||||
|
import { setDiceSoNiceForHopeFateRoll, setDiceSoNiceForFearFateRoll } from '../helpers/utils.mjs';
|
||||||
|
|
||||||
|
export default class FateRoll extends D20Roll {
|
||||||
|
constructor(formula, data = {}, options = {}) {
|
||||||
|
super(formula, data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
static messageType = 'fateRoll';
|
||||||
|
|
||||||
|
static DefaultDialog = D20RollDialog;
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
return game.i18n.localize(`DAGGERHEART.GENERAL.fateRoll`);
|
||||||
|
}
|
||||||
|
|
||||||
|
get dHope() {
|
||||||
|
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
get dFear() {
|
||||||
|
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
||||||
|
return this.dice[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
set dFear(faces) {
|
||||||
|
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
||||||
|
this.dice[0].faces = this.getFaces(faces);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isCritical() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fateDie() {
|
||||||
|
return this.data.fateType;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getHooks(hooks) {
|
||||||
|
return [...(hooks ?? []), 'Fate'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
|
static fromData(data) {
|
||||||
|
data.terms[0].class = foundry.dice.terms.Die.name;
|
||||||
|
return super.fromData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
createBaseDice() {
|
||||||
|
if (this.dice[0] instanceof foundry.dice.terms.Die) {
|
||||||
|
this.terms = [this.terms[0]];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.terms[0] = new foundry.dice.terms.Die({ faces: 12 });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async buildEvaluate(roll, config = {}, message = {}) {
|
||||||
|
await super.buildEvaluate(roll, config, message);
|
||||||
|
|
||||||
|
if (roll.fateDie === 'Hope') {
|
||||||
|
await setDiceSoNiceForHopeFateRoll(roll, config.roll.fate.dice);
|
||||||
|
} else {
|
||||||
|
await setDiceSoNiceForFearFateRoll(roll, config.roll.fate.dice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static postEvaluate(roll, config = {}) {
|
||||||
|
const data = super.postEvaluate(roll, config);
|
||||||
|
|
||||||
|
data.fate = {
|
||||||
|
dice: roll.fateDie === 'Hope' ? roll.dHope.denomination : roll.dFear.denomination,
|
||||||
|
value: roll.fateDie === 'Hope' ? roll.dHope.total : roll.dFear.total,
|
||||||
|
fateDie: roll.fateDie
|
||||||
|
};
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ export { default as DhpCombat } from './combat.mjs';
|
||||||
export { default as DHCombatant } from './combatant.mjs';
|
export { default as DHCombatant } from './combatant.mjs';
|
||||||
export { default as DhActiveEffect } from './activeEffect.mjs';
|
export { default as DhActiveEffect } from './activeEffect.mjs';
|
||||||
export { default as DhChatMessage } from './chatMessage.mjs';
|
export { default as DhChatMessage } from './chatMessage.mjs';
|
||||||
|
export { default as DhRollTable } from './rollTable.mjs';
|
||||||
export { default as DhScene } from './scene.mjs';
|
export { default as DhScene } from './scene.mjs';
|
||||||
export { default as DhToken } from './token.mjs';
|
export { default as DhToken } from './token.mjs';
|
||||||
export { default as DhTooltipManager } from './tooltipManager.mjs';
|
export { default as DhTooltipManager } from './tooltipManager.mjs';
|
||||||
|
|
|
||||||
|
|
@ -61,14 +61,15 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
||||||
update.img = 'icons/magic/life/heart-cross-blue.webp';
|
update.img = 'icons/magic/life/heart-cross-blue.webp';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const statuses = Object.keys(data.statuses ?? {});
|
||||||
const immuneStatuses =
|
const immuneStatuses =
|
||||||
data.statuses?.filter(
|
statuses.filter(
|
||||||
status =>
|
status =>
|
||||||
this.parent.system.rules?.conditionImmunities &&
|
this.parent.system.rules?.conditionImmunities &&
|
||||||
this.parent.system.rules.conditionImmunities[status]
|
this.parent.system.rules.conditionImmunities[status]
|
||||||
) ?? [];
|
) ?? [];
|
||||||
if (immuneStatuses.length > 0) {
|
if (immuneStatuses.length > 0) {
|
||||||
update.statuses = data.statuses.filter(x => !immuneStatuses.includes(x));
|
update.statuses = statuses.filter(x => !immuneStatuses.includes(x));
|
||||||
const conditions = CONFIG.DH.GENERAL.conditions();
|
const conditions = CONFIG.DH.GENERAL.conditions();
|
||||||
const scrollingTexts = immuneStatuses.map(status => ({
|
const scrollingTexts = immuneStatuses.map(status => ({
|
||||||
text: game.i18n.format('DAGGERHEART.ACTIVEEFFECT.immuneStatusText', {
|
text: game.i18n.format('DAGGERHEART.ACTIVEEFFECT.immuneStatusText', {
|
||||||
|
|
@ -113,6 +114,11 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
||||||
super.applyField(model, change, field);
|
super.applyField(model, change, field);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_applyLegacy(actor, change, changes) {
|
||||||
|
change.value = DhActiveEffect.getChangeValue(actor, change, change.effect);
|
||||||
|
super._applyLegacy(actor, change, changes);
|
||||||
|
}
|
||||||
|
|
||||||
/** */
|
/** */
|
||||||
static getChangeValue(model, change, effect) {
|
static getChangeValue(model, change, effect) {
|
||||||
let value = change.value;
|
let value = change.value;
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,11 @@ export default class DhpActor extends Actor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.system.companion) {
|
||||||
|
this.system.companion.updateLevel(usedLevel);
|
||||||
|
}
|
||||||
|
|
||||||
this.sheet.render();
|
this.sheet.render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -607,7 +612,7 @@ export default class DhpActor extends Actor {
|
||||||
if (!updates.length) return;
|
if (!updates.length) return;
|
||||||
|
|
||||||
const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
|
const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
|
||||||
if (hpDamage) {
|
if (hpDamage?.value) {
|
||||||
hpDamage.value = this.convertDamageToThreshold(hpDamage.value);
|
hpDamage.value = this.convertDamageToThreshold(hpDamage.value);
|
||||||
if (
|
if (
|
||||||
this.type === 'character' &&
|
this.type === 'character' &&
|
||||||
|
|
@ -764,16 +769,24 @@ export default class DhpActor extends Actor {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
const valueFunc = (base, resource, baseMax) => {
|
||||||
|
if (resource.clear) return baseMax && base.inverted ? baseMax : 0;
|
||||||
|
|
||||||
|
return (base.value ?? base) + resource.value;
|
||||||
|
};
|
||||||
switch (r.key) {
|
switch (r.key) {
|
||||||
case 'fear':
|
case 'fear':
|
||||||
ui.resources.updateFear(
|
ui.resources.updateFear(
|
||||||
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) + r.value
|
valueFunc(
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
|
||||||
|
r
|
||||||
|
)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'armor':
|
case 'armor':
|
||||||
if (this.system.armor?.system?.marks) {
|
if (this.system.armor?.system?.marks) {
|
||||||
updates.armor.resources['system.marks.value'] = Math.max(
|
updates.armor.resources['system.marks.value'] = Math.max(
|
||||||
Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore),
|
Math.min(valueFunc(this.system.armor.system.marks, r), this.system.armorScore),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -782,7 +795,7 @@ export default class DhpActor extends Actor {
|
||||||
if (this.system.resources?.[r.key]) {
|
if (this.system.resources?.[r.key]) {
|
||||||
updates.actor.resources[`system.resources.${r.key}.value`] = Math.max(
|
updates.actor.resources[`system.resources.${r.key}.value`] = Math.max(
|
||||||
Math.min(
|
Math.min(
|
||||||
this.system.resources[r.key].value + r.value,
|
valueFunc(this.system.resources[r.key], r, this.system.resources[r.key].max),
|
||||||
this.system.resources[r.key].max
|
this.system.resources[r.key].max
|
||||||
),
|
),
|
||||||
0
|
0
|
||||||
|
|
@ -841,8 +854,8 @@ export default class DhpActor extends Actor {
|
||||||
|
|
||||||
async toggleDefeated(defeatedState) {
|
async toggleDefeated(defeatedState) {
|
||||||
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated;
|
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated;
|
||||||
const { unconscious, defeated, dead } = CONFIG.DH.GENERAL.conditions();
|
const { deathMove, unconscious, defeated, dead } = CONFIG.DH.GENERAL.conditions();
|
||||||
const defeatedConditions = new Set([unconscious.id, defeated.id, dead.id]);
|
const defeatedConditions = new Set([deathMove.id, unconscious.id, defeated.id, dead.id]);
|
||||||
if (!defeatedState) {
|
if (!defeatedState) {
|
||||||
for (let defeatedId of defeatedConditions) {
|
for (let defeatedId of defeatedConditions) {
|
||||||
await this.toggleStatusEffect(defeatedId, { overlay: settings.overlay, active: defeatedState });
|
await this.toggleStatusEffect(defeatedId, { overlay: settings.overlay, active: defeatedState });
|
||||||
|
|
@ -856,6 +869,18 @@ export default class DhpActor extends Actor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setDeathMoveDefeated(defeatedIconId) {
|
||||||
|
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated;
|
||||||
|
const actorDefault = settings[`${this.type}Default`];
|
||||||
|
if (!settings.enabled || !settings.enabled || !actorDefault || actorDefault === defeatedIconId) return;
|
||||||
|
|
||||||
|
for (let defeatedId of Object.keys(CONFIG.DH.GENERAL.defeatedConditionChoices)) {
|
||||||
|
await this.toggleStatusEffect(defeatedId, { overlay: settings.overlay, active: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defeatedIconId) await this.toggleStatusEffect(defeatedIconId, { overlay: settings.overlay, active: true });
|
||||||
|
}
|
||||||
|
|
||||||
queueScrollText(scrollingTextData) {
|
queueScrollText(scrollingTextData) {
|
||||||
this.#scrollTextQueue.push(...scrollingTextData.map(data => () => createScrollText(this, data)));
|
this.#scrollTextQueue.push(...scrollingTextData.map(data => () => createScrollText(this, data)));
|
||||||
if (!this.#scrollTextInterval) {
|
if (!this.#scrollTextInterval) {
|
||||||
|
|
@ -909,10 +934,23 @@ export default class DhpActor extends Actor {
|
||||||
|
|
||||||
/** Get active effects */
|
/** Get active effects */
|
||||||
getActiveEffects() {
|
getActiveEffects() {
|
||||||
|
const conditions = CONFIG.DH.GENERAL.conditions();
|
||||||
const statusMap = new Map(foundry.CONFIG.statusEffects.map(status => [status.id, status]));
|
const statusMap = new Map(foundry.CONFIG.statusEffects.map(status => [status.id, status]));
|
||||||
|
const autoVulnerableActive = this.system.isAutoVulnerableActive;
|
||||||
return this.effects
|
return this.effects
|
||||||
.filter(x => !x.disabled)
|
.filter(x => !x.disabled)
|
||||||
.reduce((acc, effect) => {
|
.reduce((acc, effect) => {
|
||||||
|
/* Could be generalized if needed. Currently just related to Vulnerable */
|
||||||
|
const isAutoVulnerableEffect =
|
||||||
|
effect.flags.daggerheart?.autoApplyFlagId === conditions.vulnerable.autoApplyFlagId;
|
||||||
|
if (isAutoVulnerableEffect) {
|
||||||
|
if (!autoVulnerableActive) return acc;
|
||||||
|
|
||||||
|
effect.appliedBy = game.i18n.localize('DAGGERHEART.CONFIG.Condition.vulnerable.autoAppliedByLabel');
|
||||||
|
effect.isLockedCondition = true;
|
||||||
|
effect.condition = 'vulnerable';
|
||||||
|
}
|
||||||
|
|
||||||
acc.push(effect);
|
acc.push(effect);
|
||||||
|
|
||||||
const currentStatusActiveEffects = acc.filter(
|
const currentStatusActiveEffects = acc.filter(
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,15 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.type === 'fateRoll') {
|
||||||
|
html.classList.add('fate');
|
||||||
|
if (this.system.roll?.fate.fateDie == 'Hope') {
|
||||||
|
html.classList.add('hope');
|
||||||
|
}
|
||||||
|
if (this.system.roll?.fate.fateDie == 'Fear') {
|
||||||
|
html.classList.add('fear');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const autoExpandRoll = game.settings.get(
|
const autoExpandRoll = game.settings.get(
|
||||||
CONFIG.DH.id,
|
CONFIG.DH.id,
|
||||||
|
|
@ -101,6 +110,8 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
||||||
} else if (s.classList.contains('damage-section'))
|
} else if (s.classList.contains('damage-section'))
|
||||||
s.classList.toggle('expanded', autoExpandRoll.damage);
|
s.classList.toggle('expanded', autoExpandRoll.damage);
|
||||||
else if (s.classList.contains('target-section')) s.classList.toggle('expanded', autoExpandRoll.target);
|
else if (s.classList.contains('target-section')) s.classList.toggle('expanded', autoExpandRoll.target);
|
||||||
|
else if (s.classList.contains('description-section'))
|
||||||
|
s.classList.toggle('expanded', autoExpandRoll.desc);
|
||||||
});
|
});
|
||||||
if (itemDesc && autoExpandRoll.desc) itemDesc.setAttribute('open', '');
|
if (itemDesc && autoExpandRoll.desc) itemDesc.setAttribute('open', '');
|
||||||
}
|
}
|
||||||
|
|
@ -179,7 +190,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
||||||
config = foundry.utils.deepClone(this.system);
|
config = foundry.utils.deepClone(this.system);
|
||||||
config.event = event;
|
config.event = event;
|
||||||
|
|
||||||
if (this.system.onSave) {
|
if (config.hasSave) {
|
||||||
const pendingingSaves = targets.filter(t => t.saved.success === null);
|
const pendingingSaves = targets.filter(t => t.saved.success === null);
|
||||||
if (pendingingSaves.length) {
|
if (pendingingSaves.length) {
|
||||||
const confirm = await foundry.applications.api.DialogV2.confirm({
|
const confirm = await foundry.applications.api.DialogV2.confirm({
|
||||||
|
|
|
||||||
1
module/documents/collections/_module.mjs
Normal file
1
module/documents/collections/_module.mjs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as DhActorCollection } from './actorCollection.mjs';
|
||||||
14
module/documents/collections/actorCollection.mjs
Normal file
14
module/documents/collections/actorCollection.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
export default class DhActorCollection extends foundry.documents.collections.Actors {
|
||||||
|
/** Ensure companions are initialized after all other subtypes. */
|
||||||
|
_initialize() {
|
||||||
|
super._initialize();
|
||||||
|
const companions = [];
|
||||||
|
for (const actor of this.values()) {
|
||||||
|
if (actor.type === 'companion') companions.push(actor);
|
||||||
|
}
|
||||||
|
for (const actor of companions) {
|
||||||
|
this.delete(actor.id);
|
||||||
|
this.set(actor.id, actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -185,7 +185,10 @@ export default class DHItem extends foundry.documents.Item {
|
||||||
tags: this._getTags()
|
tags: this._getTags()
|
||||||
},
|
},
|
||||||
actions: item.system.actionsList,
|
actions: item.system.actionsList,
|
||||||
description: this.system.description
|
description: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.system.description, {
|
||||||
|
relativeTo: this.parent,
|
||||||
|
rollData: this.parent?.getRollData() ?? {}
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
const msg = {
|
const msg = {
|
||||||
|
|
|
||||||
122
module/documents/rollTable.mjs
Normal file
122
module/documents/rollTable.mjs
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
export default class DhRollTable extends foundry.documents.RollTable {
|
||||||
|
async roll({ selectedFormula, roll, recursive = true, _depth = 0 } = {}) {
|
||||||
|
// Prevent excessive recursion
|
||||||
|
if (_depth > 5) {
|
||||||
|
throw new Error(`Maximum recursion depth exceeded when attempting to draw from RollTable ${this.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formula = selectedFormula ?? this.formula;
|
||||||
|
|
||||||
|
// If there is no formula, automatically calculate an even distribution
|
||||||
|
if (!this.formula) {
|
||||||
|
await this.normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference the provided roll formula
|
||||||
|
roll = roll instanceof Roll ? roll : Roll.create(formula);
|
||||||
|
let results = [];
|
||||||
|
|
||||||
|
// Ensure that at least one non-drawn result remains
|
||||||
|
const available = this.results.filter(r => !r.drawn);
|
||||||
|
if (!available.length) {
|
||||||
|
ui.notifications.warn(game.i18n.localize('TABLE.NoAvailableResults'));
|
||||||
|
return { roll, results };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that results are available within the minimum/maximum range
|
||||||
|
const minRoll = (await roll.reroll({ minimize: true })).total;
|
||||||
|
const maxRoll = (await roll.reroll({ maximize: true })).total;
|
||||||
|
const availableRange = available.reduce(
|
||||||
|
(range, result) => {
|
||||||
|
const r = result.range;
|
||||||
|
if (!range[0] || r[0] < range[0]) range[0] = r[0];
|
||||||
|
if (!range[1] || r[1] > range[1]) range[1] = r[1];
|
||||||
|
return range;
|
||||||
|
},
|
||||||
|
[null, null]
|
||||||
|
);
|
||||||
|
if (availableRange[0] > maxRoll || availableRange[1] < minRoll) {
|
||||||
|
ui.notifications.warn('No results can possibly be drawn from this table and formula.');
|
||||||
|
return { roll, results };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue rolling until one or more results are recovered
|
||||||
|
let iter = 0;
|
||||||
|
while (!results.length) {
|
||||||
|
if (iter >= 10000) {
|
||||||
|
ui.notifications.error(
|
||||||
|
`Failed to draw an available entry from Table ${this.name}, maximum iteration reached`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
roll = await roll.reroll();
|
||||||
|
results = this.getResultsForRoll(roll.total);
|
||||||
|
iter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw results recursively from any inner Roll Tables
|
||||||
|
if (recursive) {
|
||||||
|
const inner = [];
|
||||||
|
for (const result of results) {
|
||||||
|
const { type, documentUuid } = result;
|
||||||
|
const documentName = foundry.utils.parseUuid(documentUuid)?.type;
|
||||||
|
if (type === 'document' && documentName === 'RollTable') {
|
||||||
|
const innerTable = await fromUuid(documentUuid);
|
||||||
|
if (innerTable) {
|
||||||
|
const innerRoll = await innerTable.roll({ _depth: _depth + 1 });
|
||||||
|
inner.push(...innerRoll.results);
|
||||||
|
}
|
||||||
|
} else inner.push(result);
|
||||||
|
}
|
||||||
|
results = inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the Roll and the results
|
||||||
|
return { roll, results };
|
||||||
|
}
|
||||||
|
|
||||||
|
async toMessage(results, { roll, messageData = {}, messageOptions = {} } = {}) {
|
||||||
|
messageOptions.rollMode ??= game.settings.get('core', 'rollMode');
|
||||||
|
|
||||||
|
// Construct chat data
|
||||||
|
messageData = foundry.utils.mergeObject(
|
||||||
|
{
|
||||||
|
author: game.user.id,
|
||||||
|
speaker: foundry.documents.ChatMessage.implementation.getSpeaker(),
|
||||||
|
rolls: [],
|
||||||
|
sound: roll ? CONFIG.sounds.dice : null,
|
||||||
|
flags: { 'core.RollTable': this.id }
|
||||||
|
},
|
||||||
|
messageData
|
||||||
|
);
|
||||||
|
if (roll) messageData.rolls.push(roll);
|
||||||
|
|
||||||
|
// Render the chat card which combines the dice roll with the drawn results
|
||||||
|
const detailsPromises = await Promise.allSettled(results.map(r => r.getHTML()));
|
||||||
|
const flavorKey = `TABLE.DrawFlavor${results.length > 1 ? 'Plural' : ''}`;
|
||||||
|
const flavor = game.i18n.format(flavorKey, {
|
||||||
|
number: results.length,
|
||||||
|
name: foundry.utils.escapeHTML(this.name)
|
||||||
|
});
|
||||||
|
messageData.content = await foundry.applications.handlebars.renderTemplate(CONFIG.RollTable.resultTemplate, {
|
||||||
|
description: await TextEditor.implementation.enrichHTML(this.description, {
|
||||||
|
documents: true,
|
||||||
|
secrets: this.isOwner
|
||||||
|
}),
|
||||||
|
flavor: flavor,
|
||||||
|
results: results.map((result, i) => {
|
||||||
|
const r = result.toObject(false);
|
||||||
|
r.details = detailsPromises[i].value ?? '';
|
||||||
|
const useTableIcon =
|
||||||
|
result.icon === CONFIG.RollTable.resultIcon && this.img !== this.constructor.DEFAULT_ICON;
|
||||||
|
r.icon = useTableIcon ? this.img : result.icon;
|
||||||
|
return r;
|
||||||
|
}),
|
||||||
|
rollHTML: this.displayRoll && roll ? await roll.render() : null,
|
||||||
|
table: this
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the chat message
|
||||||
|
return foundry.documents.ChatMessage.implementation.create(messageData, messageOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -51,6 +51,27 @@ export default class DhScene extends Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _preUpdate(changes, options, user) {
|
||||||
|
const allowed = await super._preUpdate(changes, options, user);
|
||||||
|
if (allowed === false) return false;
|
||||||
|
|
||||||
|
if (changes.flags?.daggerheart) {
|
||||||
|
if (this._source.flags.daggerheart) {
|
||||||
|
const unregisterTriggerData = (this._source.flags.daggerheart.sceneEnvironments ?? []).reduce(
|
||||||
|
(acc, env) => {
|
||||||
|
if (!changes.flags.daggerheart.sceneEnvironments.includes(env)) acc.sceneEnvironments.push(env);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ ...this._source.flags.daggerheart, sceneEnvironments: [] }
|
||||||
|
);
|
||||||
|
game.system.registeredTriggers.unregisterSceneEnvironmentTriggers(unregisterTriggerData);
|
||||||
|
}
|
||||||
|
|
||||||
|
game.system.registeredTriggers.registerSceneEnvironmentTriggers(changes.flags.daggerheart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_onDelete(options, userId) {
|
_onDelete(options, userId) {
|
||||||
super._onDelete(options, userId);
|
super._onDelete(options, userId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,78 +1,30 @@
|
||||||
export default class DHToken extends CONFIG.Token.documentClass {
|
export default class DHToken extends CONFIG.Token.documentClass {
|
||||||
/**
|
/**@inheritdoc */
|
||||||
* Inspect the Actor data model and identify the set of attributes which could be used for a Token Bar.
|
static getTrackedAttributeChoices(attributes, typeKey) {
|
||||||
* @param {object} attributes The tracked attributes which can be chosen from
|
|
||||||
* @returns {object} A nested object of attribute choices to display
|
|
||||||
*/
|
|
||||||
static getTrackedAttributeChoices(attributes, model) {
|
|
||||||
attributes = attributes || this.getTrackedAttributes();
|
attributes = attributes || this.getTrackedAttributes();
|
||||||
const barGroup = game.i18n.localize('TOKEN.BarAttributes');
|
const barGroup = game.i18n.localize('TOKEN.BarAttributes');
|
||||||
const valueGroup = game.i18n.localize('TOKEN.BarValues');
|
const valueGroup = game.i18n.localize('TOKEN.BarValues');
|
||||||
|
const actorModel = typeKey ? game.system.api.data.actors[`Dh${typeKey.capitalize()}`] : null;
|
||||||
|
const getLabel = path => {
|
||||||
|
const label = actorModel?.schema.getField(path)?.label;
|
||||||
|
return label ? game.i18n.localize(label) : path;
|
||||||
|
};
|
||||||
|
|
||||||
const bars = attributes.bar.map(v => {
|
const bars = attributes.bar.map(v => {
|
||||||
const a = v.join('.');
|
const a = v.join('.');
|
||||||
const modelLabel = model ? game.i18n.localize(model.schema.getField(`${a}.value`).label) : null;
|
return { group: barGroup, value: a, label: getLabel(a) };
|
||||||
return { group: barGroup, value: a, label: modelLabel ? modelLabel : a };
|
|
||||||
});
|
});
|
||||||
bars.sort((a, b) => a.label.compare(b.label));
|
bars.sort((a, b) => a.value.compare(b.value));
|
||||||
|
|
||||||
const invalidAttributes = [
|
const values = attributes.value.map(v => {
|
||||||
'gold',
|
|
||||||
'levelData',
|
|
||||||
'actions',
|
|
||||||
'biography',
|
|
||||||
'class',
|
|
||||||
'multiclass',
|
|
||||||
'companion',
|
|
||||||
'notes',
|
|
||||||
'partner',
|
|
||||||
'description',
|
|
||||||
'impulses',
|
|
||||||
'tier',
|
|
||||||
'type'
|
|
||||||
];
|
|
||||||
const values = attributes.value.reduce((acc, v) => {
|
|
||||||
const a = v.join('.');
|
const a = v.join('.');
|
||||||
if (invalidAttributes.some(x => a.startsWith(x))) return acc;
|
return { group: valueGroup, value: a, label: getLabel(a) };
|
||||||
|
});
|
||||||
const field = model ? model.schema.getField(a) : null;
|
|
||||||
const modelLabel = field ? game.i18n.localize(field.label) : null;
|
|
||||||
const hint = field ? game.i18n.localize(field.hint) : null;
|
|
||||||
acc.push({ group: valueGroup, value: a, label: modelLabel ? modelLabel : a, hint: hint });
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
values.sort((a, b) => a.label.compare(b.label));
|
|
||||||
|
|
||||||
|
values.sort((a, b) => a.value.compare(b.value));
|
||||||
return bars.concat(values);
|
return bars.concat(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
static _getTrackedAttributesFromSchema(schema, _path = []) {
|
|
||||||
const attributes = { bar: [], value: [] };
|
|
||||||
for (const [name, field] of Object.entries(schema.fields)) {
|
|
||||||
const p = _path.concat([name]);
|
|
||||||
if (field instanceof foundry.data.fields.NumberField) attributes.value.push(p);
|
|
||||||
if (field instanceof foundry.data.fields.BooleanField && field.options.isAttributeChoice)
|
|
||||||
attributes.value.push(p);
|
|
||||||
if (field instanceof foundry.data.fields.StringField) attributes.value.push(p);
|
|
||||||
if (field instanceof foundry.data.fields.ArrayField) attributes.value.push(p);
|
|
||||||
const isSchema = field instanceof foundry.data.fields.SchemaField;
|
|
||||||
const isModel = field instanceof foundry.data.fields.EmbeddedDataField;
|
|
||||||
|
|
||||||
if (isSchema || isModel) {
|
|
||||||
const schema = isModel ? field.model.schema : field;
|
|
||||||
const isBar = schema.has && schema.has('value') && schema.has('max');
|
|
||||||
if (isBar) attributes.bar.push(p);
|
|
||||||
else {
|
|
||||||
const inner = this.getTrackedAttributes(schema, p);
|
|
||||||
attributes.bar.push(...inner.bar);
|
|
||||||
attributes.value.push(...inner.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return attributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
_shouldRecordMovementHistory() {
|
_shouldRecordMovementHistory() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -269,7 +221,7 @@ export default class DHToken extends CONFIG.Token.documentClass {
|
||||||
|
|
||||||
// Hexagon symmetry
|
// Hexagon symmetry
|
||||||
if (columns) {
|
if (columns) {
|
||||||
const rowData = BaseToken.#getHexagonalShape(height, width, shape, false);
|
const rowData = DHToken.#getHexagonalShape(height, width, shape, false);
|
||||||
if (!rowData) return null;
|
if (!rowData) return null;
|
||||||
|
|
||||||
// Transpose the offsets/points of the shape in row orientation
|
// Transpose the offsets/points of the shape in row orientation
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { abilities } from '../config/actorConfig.mjs';
|
||||||
import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs';
|
import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs';
|
||||||
|
|
||||||
export default function DhDualityRollEnricher(match, _options) {
|
export default function DhDualityRollEnricher(match, _options) {
|
||||||
const roll = rollCommandToJSON(match[1], match[0]);
|
const roll = rollCommandToJSON(match[0]);
|
||||||
if (!roll) return match[0];
|
if (!roll) return match[0];
|
||||||
|
|
||||||
return getDualityMessage(roll.result, roll.flavor);
|
return getDualityMessage(roll.result, roll.flavor);
|
||||||
|
|
@ -47,6 +47,7 @@ function getDualityMessage(roll, flavor) {
|
||||||
${roll?.trait && abilities[roll.trait] ? `data-trait="${roll.trait}"` : ''}
|
${roll?.trait && abilities[roll.trait] ? `data-trait="${roll.trait}"` : ''}
|
||||||
${roll?.advantage ? 'data-advantage="true"' : ''}
|
${roll?.advantage ? 'data-advantage="true"' : ''}
|
||||||
${roll?.disadvantage ? 'data-disadvantage="true"' : ''}
|
${roll?.disadvantage ? 'data-disadvantage="true"' : ''}
|
||||||
|
${roll?.grantResources ? 'data-grant-resources="true"' : ''}
|
||||||
>
|
>
|
||||||
${roll?.reaction ? '<i class="fa-solid fa-reply"></i>' : '<i class="fa-solid fa-circle-half-stroke"></i>'}
|
${roll?.reaction ? '<i class="fa-solid fa-reply"></i>' : '<i class="fa-solid fa-circle-half-stroke"></i>'}
|
||||||
${label}
|
${label}
|
||||||
|
|
@ -63,7 +64,8 @@ export const renderDualityButton = async event => {
|
||||||
traitValue = button.dataset.trait?.toLowerCase(),
|
traitValue = button.dataset.trait?.toLowerCase(),
|
||||||
target = getCommandTarget({ allowNull: true }),
|
target = getCommandTarget({ allowNull: true }),
|
||||||
difficulty = button.dataset.difficulty,
|
difficulty = button.dataset.difficulty,
|
||||||
advantage = button.dataset.advantage ? Number(button.dataset.advantage) : undefined;
|
advantage = button.dataset.advantage ? Number(button.dataset.advantage) : undefined,
|
||||||
|
grantResources = Boolean(button.dataset?.grantResources);
|
||||||
|
|
||||||
await enrichedDualityRoll(
|
await enrichedDualityRoll(
|
||||||
{
|
{
|
||||||
|
|
@ -73,36 +75,48 @@ export const renderDualityButton = async event => {
|
||||||
difficulty,
|
difficulty,
|
||||||
title: button.dataset.title,
|
title: button.dataset.title,
|
||||||
label: button.dataset.label,
|
label: button.dataset.label,
|
||||||
advantage
|
advantage,
|
||||||
|
grantResources
|
||||||
},
|
},
|
||||||
event
|
event
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const enrichedDualityRoll = async (
|
export const enrichedDualityRoll = async (
|
||||||
{ reaction, traitValue, target, difficulty, title, label, advantage },
|
{ reaction, traitValue, target, difficulty, title, label, advantage, grantResources, customConfig },
|
||||||
event
|
event
|
||||||
) => {
|
) => {
|
||||||
|
const shouldGrantResources = grantResources === undefined ? true : grantResources;
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
event: event ?? {},
|
event: event ?? {},
|
||||||
title: title,
|
title: title,
|
||||||
headerTitle: label,
|
headerTitle: label,
|
||||||
|
actionType: reaction ? 'reaction' : null,
|
||||||
roll: {
|
roll: {
|
||||||
trait: traitValue && target ? traitValue : null,
|
trait: traitValue && target ? traitValue : null,
|
||||||
difficulty: difficulty,
|
difficulty: difficulty,
|
||||||
advantage,
|
advantage
|
||||||
type: reaction ? 'reaction' : null
|
// type: reaction ? 'reaction' : null //not needed really but keeping it for troubleshooting
|
||||||
|
},
|
||||||
|
skips: {
|
||||||
|
resources: !shouldGrantResources,
|
||||||
|
triggers: !shouldGrantResources
|
||||||
},
|
},
|
||||||
type: 'trait',
|
type: 'trait',
|
||||||
hasRoll: true
|
hasRoll: true,
|
||||||
|
...(customConfig ?? {})
|
||||||
};
|
};
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
await target.diceRoll(config);
|
const result = await target.diceRoll(config);
|
||||||
|
if (!result) return;
|
||||||
|
result.resourceUpdates.updateResources();
|
||||||
} else {
|
} else {
|
||||||
// For no target, call DualityRoll directly with basic data
|
// For no target, call DualityRoll directly with basic data
|
||||||
config.data = { experiences: {}, traits: {}, rules: {} };
|
config.data = { experiences: {}, traits: {}, rules: {} };
|
||||||
config.source = { actor: null };
|
config.source = { actor: null };
|
||||||
await CONFIG.Dice.daggerheart.DualityRoll.build(config);
|
await CONFIG.Dice.daggerheart.DualityRoll.build(config);
|
||||||
}
|
}
|
||||||
|
return config;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
80
module/enrichers/FateRollEnricher.mjs
Normal file
80
module/enrichers/FateRollEnricher.mjs
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs';
|
||||||
|
|
||||||
|
export default function DhFateRollEnricher(match, _options) {
|
||||||
|
const roll = rollCommandToJSON(match[0]);
|
||||||
|
if (!roll) return match[0];
|
||||||
|
|
||||||
|
return getFateMessage(roll.result, roll?.flavor);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFateTypeData(fateTypeValue) {
|
||||||
|
const value = fateTypeValue ? fateTypeValue.capitalize() : 'Hope';
|
||||||
|
const lowercased = fateTypeValue?.toLowerCase?.() ?? 'hope';
|
||||||
|
switch (lowercased) {
|
||||||
|
case 'hope':
|
||||||
|
case 'fear':
|
||||||
|
return { value, label: game.i18n.localize(`DAGGERHEART.GENERAL.${lowercased}`) };
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFateMessage(roll, flavor) {
|
||||||
|
const fateTypeData = getFateTypeData(roll?.type);
|
||||||
|
|
||||||
|
if (!fateTypeData)
|
||||||
|
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
|
||||||
|
|
||||||
|
const { value: fateType, label: fateTypeLabel } = fateTypeData;
|
||||||
|
const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll');
|
||||||
|
|
||||||
|
const fateElement = document.createElement('span');
|
||||||
|
fateElement.innerHTML = `
|
||||||
|
<button type="button" class="fate-roll-button${roll?.inline ? ' inline' : ''}"
|
||||||
|
data-title="${title}"
|
||||||
|
data-label="${fateTypeLabel}"
|
||||||
|
data-fateType="${fateType}"
|
||||||
|
>
|
||||||
|
${title}
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return fateElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const renderFateButton = async event => {
|
||||||
|
const button = event.currentTarget,
|
||||||
|
target = getCommandTarget({ allowNull: true });
|
||||||
|
|
||||||
|
const fateTypeData = getFateTypeData(button.dataset?.fatetype);
|
||||||
|
|
||||||
|
if (!fateTypeData) ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
|
||||||
|
const { value: fateType, label: fateTypeLabel } = fateTypeData;
|
||||||
|
|
||||||
|
await enrichedFateRoll(
|
||||||
|
{
|
||||||
|
target,
|
||||||
|
title: button.dataset.title,
|
||||||
|
label: button.dataset.label,
|
||||||
|
fateType: fateType
|
||||||
|
},
|
||||||
|
event
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const enrichedFateRoll = async ({ target, title, label, fateType }, event) => {
|
||||||
|
const config = {
|
||||||
|
event: event ?? {},
|
||||||
|
title: title,
|
||||||
|
headerTitle: label,
|
||||||
|
roll: {},
|
||||||
|
hasRoll: true,
|
||||||
|
fateType: fateType,
|
||||||
|
skips: { reaction: true }
|
||||||
|
};
|
||||||
|
|
||||||
|
config.data = { experiences: {}, traits: {}, fateType: fateType };
|
||||||
|
config.source = { actor: target?.uuid };
|
||||||
|
await CONFIG.Dice.daggerheart.FateRoll.build(config);
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
@ -4,6 +4,7 @@ export default function DhTemplateEnricher(match, _options) {
|
||||||
const params = parseInlineParams(match[1]);
|
const params = parseInlineParams(match[1]);
|
||||||
const { type, angle = CONFIG.MeasuredTemplate.defaults.angle, inline = false } = params;
|
const { type, angle = CONFIG.MeasuredTemplate.defaults.angle, inline = false } = params;
|
||||||
const direction = Number(params.direction) || 0;
|
const direction = Number(params.direction) || 0;
|
||||||
|
params.range = params.range?.toLowerCase();
|
||||||
const range =
|
const range =
|
||||||
params.range && Number.isNaN(Number(params.range))
|
params.range && Number.isNaN(Number(params.range))
|
||||||
? Object.values(CONFIG.DH.GENERAL.templateRanges).find(
|
? Object.values(CONFIG.DH.GENERAL.templateRanges).find(
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { default as DhDamageEnricher, renderDamageButton } from './DamageEnricher.mjs';
|
import { default as DhDamageEnricher, renderDamageButton } from './DamageEnricher.mjs';
|
||||||
import { default as DhDualityRollEnricher, renderDualityButton } from './DualityRollEnricher.mjs';
|
import { default as DhDualityRollEnricher, renderDualityButton } from './DualityRollEnricher.mjs';
|
||||||
|
import { default as DhFateRollEnricher, renderFateButton } from './FateRollEnricher.mjs';
|
||||||
import { default as DhEffectEnricher } from './EffectEnricher.mjs';
|
import { default as DhEffectEnricher } from './EffectEnricher.mjs';
|
||||||
import { default as DhTemplateEnricher, renderMeasuredTemplate } from './TemplateEnricher.mjs';
|
import { default as DhTemplateEnricher, renderMeasuredTemplate } from './TemplateEnricher.mjs';
|
||||||
import { default as DhLookupEnricher } from './LookupEnricher.mjs';
|
import { default as DhLookupEnricher } from './LookupEnricher.mjs';
|
||||||
|
|
||||||
export { DhDamageEnricher, DhDualityRollEnricher, DhEffectEnricher, DhTemplateEnricher };
|
export { DhDamageEnricher, DhDualityRollEnricher, DhEffectEnricher, DhTemplateEnricher, DhFateRollEnricher };
|
||||||
|
|
||||||
export const enricherConfig = [
|
export const enricherConfig = [
|
||||||
{
|
{
|
||||||
|
|
@ -15,6 +16,10 @@ export const enricherConfig = [
|
||||||
pattern: /\[\[\/dr\s?(.*?)\]\]({[^}]*})?/g,
|
pattern: /\[\[\/dr\s?(.*?)\]\]({[^}]*})?/g,
|
||||||
enricher: DhDualityRollEnricher
|
enricher: DhDualityRollEnricher
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
pattern: /\[\[\/fr\s?(.*?)\]\]({[^}]*})?/g,
|
||||||
|
enricher: DhFateRollEnricher
|
||||||
|
},
|
||||||
{
|
{
|
||||||
pattern: /@Effect\[([^\[\]]*)\]({[^}]*})?/g,
|
pattern: /@Effect\[([^\[\]]*)\]({[^}]*})?/g,
|
||||||
enricher: DhEffectEnricher
|
enricher: DhEffectEnricher
|
||||||
|
|
@ -38,6 +43,10 @@ export const enricherRenderSetup = element => {
|
||||||
.querySelectorAll('.duality-roll-button')
|
.querySelectorAll('.duality-roll-button')
|
||||||
.forEach(element => element.addEventListener('click', renderDualityButton));
|
.forEach(element => element.addEventListener('click', renderDualityButton));
|
||||||
|
|
||||||
|
element
|
||||||
|
.querySelectorAll('.fate-roll-button')
|
||||||
|
.forEach(element => element.addEventListener('click', renderFateButton));
|
||||||
|
|
||||||
element
|
element
|
||||||
.querySelectorAll('.measured-template-button')
|
.querySelectorAll('.measured-template-button')
|
||||||
.forEach(element => element.addEventListener('click', renderMeasuredTemplate));
|
.forEach(element => element.addEventListener('click', renderMeasuredTemplate));
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
import { diceTypes, getDiceSoNicePresets, range } from '../config/generalConfig.mjs';
|
import { diceTypes, getDiceSoNicePresets, getDiceSoNicePreset, range } from '../config/generalConfig.mjs';
|
||||||
import Tagify from '@yaireo/tagify';
|
import Tagify from '@yaireo/tagify';
|
||||||
|
|
||||||
export const capitalize = string => {
|
export const capitalize = string => {
|
||||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function rollCommandToJSON(text, raw) {
|
export function rollCommandToJSON(text) {
|
||||||
if (!text) return {};
|
if (!text) return {};
|
||||||
|
|
||||||
const flavorMatch = raw?.match(/{(.*)}$/);
|
const flavorMatch = text?.match(/{(.*)}$/);
|
||||||
const flavor = flavorMatch ? flavorMatch[1] : null;
|
const flavor = flavorMatch ? flavorMatch[1] : null;
|
||||||
|
|
||||||
// Match key="quoted string" OR key=unquotedValue
|
// Match key="quoted string" OR key=unquotedValue
|
||||||
const PAIR_RE = /(\w+)=("(?:[^"\\]|\\.)*"|\S+)/g;
|
const PAIR_RE = /(\w+)\s*=\s*("(?:[^"\\]|\\.)*"|[^\]\}\s]+)/g; //updated regex to allow escaped quotes in quoted strings and avoid matching closing brackets/braces
|
||||||
const result = {};
|
const result = {};
|
||||||
for (const [, key, raw] of text.matchAll(PAIR_RE)) {
|
for (const [, key, raw] of text.matchAll(PAIR_RE)) {
|
||||||
let value;
|
let value;
|
||||||
|
|
@ -31,7 +31,7 @@ export function rollCommandToJSON(text, raw) {
|
||||||
}
|
}
|
||||||
result[key] = value;
|
result[key] = value;
|
||||||
}
|
}
|
||||||
return Object.keys(result).length > 0 ? { result, flavor } : null;
|
return { result, flavor };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getCommandTarget = (options = {}) => {
|
export const getCommandTarget = (options = {}) => {
|
||||||
|
|
@ -69,6 +69,20 @@ export const setDiceSoNiceForDualityRoll = async (rollResult, advantageState, ho
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setDiceSoNiceForHopeFateRoll = async (rollResult, hopeFaces) => {
|
||||||
|
if (!game.modules.get('dice-so-nice')?.active) return;
|
||||||
|
const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
|
||||||
|
const diceSoNicePresets = await getDiceSoNicePreset(diceSoNice.hope, hopeFaces);
|
||||||
|
rollResult.dice[0].options = diceSoNicePresets;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setDiceSoNiceForFearFateRoll = async (rollResult, fearFaces) => {
|
||||||
|
if (!game.modules.get('dice-so-nice')?.active) return;
|
||||||
|
const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
|
||||||
|
const diceSoNicePresets = await getDiceSoNicePreset(diceSoNice.fear, fearFaces);
|
||||||
|
rollResult.dice[0].options = diceSoNicePresets;
|
||||||
|
};
|
||||||
|
|
||||||
export const chunkify = (array, chunkSize, mappingFunc) => {
|
export const chunkify = (array, chunkSize, mappingFunc) => {
|
||||||
var chunkifiedArray = [];
|
var chunkifiedArray = [];
|
||||||
for (let i = 0; i < array.length; i += chunkSize) {
|
for (let i = 0; i < array.length; i += chunkSize) {
|
||||||
|
|
@ -105,8 +119,8 @@ export const tagifyElement = (element, baseOptions, onChange, tagifyOptions = {}
|
||||||
}),
|
}),
|
||||||
maxTags: typeof maxTags === 'function' ? maxTags() : maxTags,
|
maxTags: typeof maxTags === 'function' ? maxTags() : maxTags,
|
||||||
dropdown: {
|
dropdown: {
|
||||||
|
searchKeys: ['value', 'name'],
|
||||||
mapValueTo: 'name',
|
mapValueTo: 'name',
|
||||||
searchKeys: ['value'],
|
|
||||||
enabled: 0,
|
enabled: 0,
|
||||||
maxItems: 100,
|
maxItems: 100,
|
||||||
closeOnSelect: true,
|
closeOnSelect: true,
|
||||||
|
|
@ -458,7 +472,7 @@ export function refreshIsAllowed(allowedTypes, typeToCheck) {
|
||||||
case CONFIG.DH.GENERAL.refreshTypes.scene.id:
|
case CONFIG.DH.GENERAL.refreshTypes.scene.id:
|
||||||
case CONFIG.DH.GENERAL.refreshTypes.session.id:
|
case CONFIG.DH.GENERAL.refreshTypes.session.id:
|
||||||
case CONFIG.DH.GENERAL.refreshTypes.longRest.id:
|
case CONFIG.DH.GENERAL.refreshTypes.longRest.id:
|
||||||
return allowedTypes.includes(typeToCheck);
|
return allowedTypes.includes?.(typeToCheck) ?? allowedTypes.has(typeToCheck);
|
||||||
case CONFIG.DH.GENERAL.refreshTypes.shortRest.id:
|
case CONFIG.DH.GENERAL.refreshTypes.shortRest.id:
|
||||||
return allowedTypes.some(
|
return allowedTypes.some(
|
||||||
x =>
|
x =>
|
||||||
|
|
@ -481,3 +495,183 @@ export function htmlToText(html) {
|
||||||
|
|
||||||
return tempDivElement.textContent || tempDivElement.innerText || '';
|
return tempDivElement.textContent || tempDivElement.innerText || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getFeaturesHTMLData(features) {
|
||||||
|
const result = [];
|
||||||
|
for (const feature of features) {
|
||||||
|
if (feature) {
|
||||||
|
const base = feature.item ?? feature;
|
||||||
|
const item = base.system ? base : await foundry.utils.fromUuid(base.uuid);
|
||||||
|
const itemDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||||
|
item.system.description
|
||||||
|
);
|
||||||
|
result.push({ label: item.name, description: itemDescription });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a simple flavor-less formula with only +/- operators, returns a list of damage partial terms.
|
||||||
|
* All subtracted terms become negative terms.
|
||||||
|
* If there are no dice, it returns 0d1 for that term.
|
||||||
|
*/
|
||||||
|
export function parseTermsFromSimpleFormula(formula) {
|
||||||
|
const roll = formula instanceof Roll ? formula : new Roll(formula);
|
||||||
|
|
||||||
|
// Parse from right to left so that when we hit an operator, we already have the term.
|
||||||
|
return roll.terms.reduceRight((result, term) => {
|
||||||
|
// Ignore + terms, we assume + by default
|
||||||
|
if (term.expression === ' + ') return result;
|
||||||
|
|
||||||
|
// - terms modify the last term we parsed
|
||||||
|
if (term.expression === ' - ') {
|
||||||
|
const termToModify = result[0];
|
||||||
|
if (termToModify) {
|
||||||
|
if (termToModify.bonus) termToModify.bonus *= -1;
|
||||||
|
if (termToModify.dice) termToModify.dice *= -1;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.unshift({
|
||||||
|
bonus: term instanceof foundry.dice.terms.NumericTerm ? term.number : 0,
|
||||||
|
diceQuantity: term instanceof foundry.dice.terms.Die ? term.number : 0,
|
||||||
|
faces: term.faces ?? 1
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the expectede value from a formula or the results of parseTermsFromSimpleFormula.
|
||||||
|
* @returns {number} the average result of rolling the given dice
|
||||||
|
*/
|
||||||
|
export function calculateExpectedValue(formulaOrTerms) {
|
||||||
|
const terms = Array.isArray(formulaOrTerms)
|
||||||
|
? formulaOrTerms
|
||||||
|
: typeof formulaOrTerms === 'string'
|
||||||
|
? parseTermsFromSimpleFormula(formulaOrTerms)
|
||||||
|
: [formulaOrTerms];
|
||||||
|
return terms.reduce((r, t) => r + (t.bonus ?? 0) + (t.diceQuantity ? (t.diceQuantity * (t.faces + 1)) / 2 : 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRallyDice(value, effect) {
|
||||||
|
const legacyStartsWithPrefix = value.toLowerCase().startsWith('d');
|
||||||
|
const workingValue = legacyStartsWithPrefix ? value.slice(1) : value;
|
||||||
|
const dataParsedValue = itemAbleRollParse(workingValue, effect.parent);
|
||||||
|
|
||||||
|
return `d${game.system.api.documents.DhActiveEffect.effectSafeEval(dataParsedValue)}`;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Refreshes character and/or adversary resources.
|
||||||
|
* @param { string[] } refreshTypes Which type of features to refresh using IDs from CONFIG.DH.GENERAL.refreshTypes
|
||||||
|
* @param { string[] = ['character', 'adversary'] } actorTypes Which actor types should refresh their features. Defaults to character and adversary.
|
||||||
|
* @param { boolean = true } sendRefreshMessage If a chat message should be created detailing the refresh
|
||||||
|
* @return { Actor[] } The actors that had their features refreshed
|
||||||
|
*/
|
||||||
|
export async function RefreshFeatures(
|
||||||
|
refreshTypes = [],
|
||||||
|
actorTypes = ['character', 'adversary'],
|
||||||
|
sendNotificationMessage = true,
|
||||||
|
sendRefreshMessage = true
|
||||||
|
) {
|
||||||
|
const refreshedActors = {};
|
||||||
|
for (let actor of game.actors) {
|
||||||
|
if (actorTypes.includes(actor.type) && actor.prototypeToken.actorLink) {
|
||||||
|
const updates = {};
|
||||||
|
for (let item of actor.items) {
|
||||||
|
if (
|
||||||
|
item.system.metadata?.hasResource &&
|
||||||
|
refreshIsAllowed(refreshTypes, item.system.resource?.recovery)
|
||||||
|
) {
|
||||||
|
if (!refreshedActors[actor.id])
|
||||||
|
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
|
||||||
|
refreshedActors[actor.id].refreshed.add(
|
||||||
|
game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[item.system.resource.recovery].label)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
|
||||||
|
|
||||||
|
const increasing =
|
||||||
|
item.system.resource.progression === CONFIG.DH.ITEM.itemResourceProgression.increasing.id;
|
||||||
|
updates[item.id].system = {
|
||||||
|
...updates[item.id].system,
|
||||||
|
'resource.value': increasing
|
||||||
|
? 0
|
||||||
|
: game.system.api.documents.DhActiveEffect.effectSafeEval(
|
||||||
|
Roll.replaceFormulaData(item.system.resource.max, actor.getRollData())
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (item.system.metadata?.hasActions) {
|
||||||
|
const usedTypes = new Set();
|
||||||
|
const actions = item.system.actions.filter(action => {
|
||||||
|
if (refreshIsAllowed(refreshTypes, action.uses.recovery)) {
|
||||||
|
usedTypes.add(action.uses.recovery);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (actions.length === 0) continue;
|
||||||
|
|
||||||
|
if (!refreshedActors[actor.id])
|
||||||
|
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
|
||||||
|
refreshedActors[actor.id].refreshed.add(
|
||||||
|
...usedTypes.map(type => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[type].label))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
|
||||||
|
|
||||||
|
updates[item.id].system = {
|
||||||
|
...updates[item.id].system,
|
||||||
|
...actions.reduce(
|
||||||
|
(acc, action) => {
|
||||||
|
acc.actions[action.id] = { 'uses.value': 0 };
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ actions: updates[item.id].system.actions ?? {} }
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let key in updates) {
|
||||||
|
const update = updates[key];
|
||||||
|
await actor.items.get(key).update(update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const types = refreshTypes.map(x => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[x].label)).join(', ');
|
||||||
|
|
||||||
|
if (sendNotificationMessage) {
|
||||||
|
ui.notifications.info(
|
||||||
|
game.i18n.format('DAGGERHEART.UI.Notifications.gmMenuRefresh', {
|
||||||
|
types: `[${types}]`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendRefreshMessage) {
|
||||||
|
const cls = getDocumentClass('ChatMessage');
|
||||||
|
const msg = {
|
||||||
|
user: game.user.id,
|
||||||
|
content: await foundry.applications.handlebars.renderTemplate(
|
||||||
|
'systems/daggerheart/templates/ui/chat/refreshMessage.hbs',
|
||||||
|
{
|
||||||
|
types: types
|
||||||
|
}
|
||||||
|
),
|
||||||
|
title: game.i18n.localize('DAGGERHEART.UI.Chat.refreshMessage.title'),
|
||||||
|
speaker: cls.getSpeaker()
|
||||||
|
};
|
||||||
|
|
||||||
|
cls.create(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return refreshedActors;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ export const preloadHandlebarsTemplates = async function () {
|
||||||
'systems/daggerheart/templates/dialogs/downtime/activities.hbs',
|
'systems/daggerheart/templates/dialogs/downtime/activities.hbs',
|
||||||
'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs',
|
'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs',
|
||||||
'systems/daggerheart/templates/ui/chat/parts/roll-part.hbs',
|
'systems/daggerheart/templates/ui/chat/parts/roll-part.hbs',
|
||||||
|
'systems/daggerheart/templates/ui/chat/parts/description-part.hbs',
|
||||||
'systems/daggerheart/templates/ui/chat/parts/damage-part.hbs',
|
'systems/daggerheart/templates/ui/chat/parts/damage-part.hbs',
|
||||||
'systems/daggerheart/templates/ui/chat/parts/target-part.hbs',
|
'systems/daggerheart/templates/ui/chat/parts/target-part.hbs',
|
||||||
'systems/daggerheart/templates/ui/chat/parts/button-part.hbs',
|
'systems/daggerheart/templates/ui/chat/parts/button-part.hbs',
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,42 @@ export async function runMigrations() {
|
||||||
|
|
||||||
lastMigrationVersion = '1.2.7';
|
lastMigrationVersion = '1.2.7';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (foundry.utils.isNewerVersion('1.5.5', lastMigrationVersion)) {
|
||||||
|
/* Clear out Environments that were added directly from compendium */
|
||||||
|
for (const scene of game.scenes) {
|
||||||
|
if (!scene.flags.daggerheart) continue;
|
||||||
|
const systemData = new game.system.api.data.scenes.DHScene(scene.flags.daggerheart);
|
||||||
|
const sceneEnvironments = systemData.sceneEnvironments;
|
||||||
|
|
||||||
|
const newEnvironments = sceneEnvironments.filter(x => !x?.pack);
|
||||||
|
if (newEnvironments.length !== sceneEnvironments.length)
|
||||||
|
await scene.update({ 'flags.daggerheart.sceneEnvironments': newEnvironments });
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.nav.render(true);
|
||||||
|
|
||||||
|
lastMigrationVersion = '1.5.5';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundry.utils.isNewerVersion('1.6.0', lastMigrationVersion)) {
|
||||||
|
/* Delevel any companions that are higher level than their partner character */
|
||||||
|
for (const companion of game.actors.filter(x => x.type === 'companion')) {
|
||||||
|
if (companion.system.levelData.level.current <= 1) continue;
|
||||||
|
|
||||||
|
if (!companion.system.partner) {
|
||||||
|
await companion.updateLevel(1);
|
||||||
|
} else {
|
||||||
|
const endLevel = companion.system.partner.system.levelData.level.current;
|
||||||
|
if (endLevel < companion.system.levelData.level.current) {
|
||||||
|
companion.system.levelData.level.changed = companion.system.levelData.level.current;
|
||||||
|
await companion.updateLevel(endLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMigrationVersion = '1.6.0';
|
||||||
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion);
|
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
DhHomebrewSettings,
|
DhHomebrewSettings,
|
||||||
DhVariantRuleSettings
|
DhVariantRuleSettings
|
||||||
} from '../applications/settings/_module.mjs';
|
} from '../applications/settings/_module.mjs';
|
||||||
import { DhTagTeamRoll } from '../data/_module.mjs';
|
import { CompendiumBrowserSettings, DhTagTeamRoll } from '../data/_module.mjs';
|
||||||
|
|
||||||
export const registerDHSettings = () => {
|
export const registerDHSettings = () => {
|
||||||
registerMenuSettings();
|
registerMenuSettings();
|
||||||
|
|
@ -126,7 +126,7 @@ const registerNonConfigSettings = () => {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
onChange: () => {
|
onChange: () => {
|
||||||
if (ui.resources) ui.resources.render({ force: true });
|
if (ui.resources) ui.resources.render();
|
||||||
ui.combat.render({ force: true });
|
ui.combat.render({ force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -142,6 +142,12 @@ const registerNonConfigSettings = () => {
|
||||||
config: false,
|
config: false,
|
||||||
type: DhTagTeamRoll
|
type: DhTagTeamRoll
|
||||||
});
|
});
|
||||||
|
|
||||||
|
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings, {
|
||||||
|
scope: 'client',
|
||||||
|
config: false,
|
||||||
|
type: CompendiumBrowserSettings
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
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