diff --git a/daggerheart.mjs b/daggerheart.mjs
index bcb058cb..58600ad4 100644
--- a/daggerheart.mjs
+++ b/daggerheart.mjs
@@ -3,11 +3,10 @@ import * as applications from './module/applications/_module.mjs';
import * as models from './module/data/_module.mjs';
import * as documents from './module/documents/_module.mjs';
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
-import DhpCombatTracker from './module/ui/combatTracker.mjs';
+import DhCombatTracker from './module/ui/combatTracker.mjs';
import { GMUpdateEvent, handleSocketEvent, socketEvent } from './module/helpers/socket.mjs';
import { registerDHSettings } from './module/applications/settings.mjs';
import DhpChatLog from './module/ui/chatLog.mjs';
-import DhpPlayers from './module/ui/players.mjs';
import DhpRuler from './module/ui/ruler.mjs';
import DhpTokenRuler from './module/ui/tokenRuler.mjs';
import { dualityRollEnricher } from './module/enrichers/DualityRollEnricher.mjs';
@@ -65,11 +64,11 @@ Hooks.once('init', () => {
Actors.registerSheet(SYSTEM.id, applications.DhpEnvironment, { types: ['environment'], makeDefault: true });
CONFIG.Combat.dataModels = {
- base: models.DhpCombat
+ base: models.DhCombat
};
CONFIG.Combatant.dataModels = {
- base: models.DhpCombatant
+ base: models.DhCombatant
};
CONFIG.ChatMessage.dataModels = models.messages.config;
@@ -77,7 +76,7 @@ Hooks.once('init', () => {
CONFIG.Canvas.rulerClass = DhpRuler;
CONFIG.Combat.documentClass = documents.DhpCombat;
- CONFIG.ui.combat = DhpCombatTracker;
+ CONFIG.ui.combat = DhCombatTracker;
CONFIG.ui.chat = DhpChatLog;
// CONFIG.ui.players = DhpPlayers;
CONFIG.Token.rulerClass = DhpTokenRuler;
@@ -97,8 +96,8 @@ Hooks.once('init', () => {
Hooks.on('ready', () => {
ui.resources = new CONFIG.ui.resources();
- ui.resources.render({force: true});
-})
+ ui.resources.render({ force: true });
+});
Hooks.once('dicesoniceready', () => {});
@@ -201,8 +200,8 @@ Hooks.on('chatMessage', (_, message) => {
const title = attributeValue
? game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', {
- ability: game.i18n.localize(abilities[attributeValue].label)
- })
+ ability: game.i18n.localize(abilities[attributeValue].label)
+ })
: game.i18n.localize('DAGGERHEART.General.Duality');
const hopeAndFearRoll = `1${rollCommand.hope ?? 'd12'}+1${rollCommand.fear ?? 'd12'}`;
@@ -221,9 +220,9 @@ Hooks.on('chatMessage', (_, message) => {
roll,
attribute: attribute
? {
- value: attribute.data.value,
- label: `${game.i18n.localize(abilities[attributeValue].label)} ${attribute.data.value >= 0 ? `+` : ``}${attribute.data.value}`
- }
+ value: attribute.data.value,
+ label: `${game.i18n.localize(abilities[attributeValue].label)} ${attribute.data.value >= 0 ? `+` : ``}${attribute.data.value}`
+ }
: undefined,
title
});
@@ -280,7 +279,9 @@ const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/sheets/pc/sections/loadout.hbs',
'systems/daggerheart/templates/sheets/pc/parts/heritageCard.hbs',
'systems/daggerheart/templates/sheets/pc/parts/advancementCard.hbs',
- 'systems/daggerheart/templates/views/parts/level.hbs',
- 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs'
+ 'systems/daggerheart/templates/components/card-preview.hbs',
+ 'systems/daggerheart/templates/views/levelup/parts/selectable-card-preview.hbs',
+ 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs',
+ 'systems/daggerheart/templates/ui/combat/combatTrackerSection.hbs'
]);
};
diff --git a/lang/en.json b/lang/en.json
index 75a803e1..39d5225c 100755
--- a/lang/en.json
+++ b/lang/en.json
@@ -61,6 +61,13 @@
"outline": "Outline",
"edge": "Edge"
}
+ },
+ "VariantRules": {
+ "title": "Variant Rules",
+ "label": "Variant Rules",
+ "hint": "Apply variant rules from the Daggerheart system",
+ "name": "Variant Rules",
+ "actionTokens": "Action Tokens"
}
},
"Automation": {
@@ -101,6 +108,12 @@
"Hint": "Enable measuring of ranges with the ruler according to set distances."
}
},
+ "VariantRules": {
+ "ActionTokens": {
+ "Name": "Action Tokens",
+ "Hint": "Give each player action tokens to use in combat"
+ }
+ },
"DualityRollColor": {
"Name": "Duality Roll Colour Scheme",
"Hint": "The display type for Duality Rolls",
@@ -150,6 +163,20 @@
"Or": "Or",
"Description": "Description",
"Features": "Features",
+ "proficiency": "Proficiency",
+ "unarmored": "Unarmored",
+ "Experience": {
+ "Single": "Experience",
+ "plural": "Experiences"
+ },
+ "Adversary": {
+ "Singular": "Adversary",
+ "Plural": "Adversaries"
+ },
+ "Character": {
+ "Singular": "Character",
+ "Plural": "Characters"
+ },
"RefreshType": {
"Session": "Session",
"Shortrest": "Short Rest",
@@ -329,40 +356,50 @@
"grimoire": "Grimoire"
}
},
+ "Combat": {
+ "giveSpotlight": "Give The Spotlight",
+ "requestSpotlight": "Request The Spotlight",
+ "requestingSpotlight": "Requesting The Spotlight",
+ "combatStarted": "Active"
+ },
"LevelUp": {
- "Tier1": {
- "Label": "Level 2-4",
- "InfoLabel": "At Level 2, take an additional Experience.",
- "Pretext": "When you level up, record it on your character sheet, then choose two available options from the list below and mark them.",
- "Posttext": "Then increase your Severe Damage Threshold by +2 and choose a new Domain Deck card at your Level or lower."
+ "Options": {
+ "trait": "Gain a +1 bonus to two unmarked character traits and mark them.",
+ "hitPoint": "Permanently gain one Hit Point slot.",
+ "stress": "Permanently gain one Stress slot.",
+ "experience": "Permanently gain a +1 bonus to two experiences.",
+ "domainCard": "Choose an additional domain card of your level or lower from a domain you have access to (up to level {maxLevel})",
+ "evasion": "Permanently gain a +1 bonus to your Evasion.",
+ "subclass": "Take an upgraded subclass card. Then cross out the multiclass option for this tier.",
+ "proficiency": "Increase your Proficiency by +1.",
+ "multiclass": "Multiclass: Choose an additional class for your character, then cross out an unused “Take an upgraded subclass card” and the other multiclass option on this sheet."
},
"Tier2": {
- "Label": "Level 5-7",
- "InfoLabel": "At Level 5, take an additional Experience and clear all marks on Character Traits.",
- "Pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.",
- "Posttext": "Then, increase your Damage Thresholds: Major by +1 and Severe by +3. Then choose a new Domain Deck card at your Level or lower. If your loadout is full, you may choose a card to swap."
+ "Label": "Levels 2-4",
+ "InfoLabel": "At Level 2, gain an additional Experience at +2 and gain a +1 bonus to your Proficiency.",
+ "Pretext": "Choose two options from the list below",
+ "Posttext": "Take an additional domain card of your level or lower from a domain you have access to."
},
"Tier3": {
- "Label": "Level 8-10",
+ "Label": "Levels 5-7",
+ "InfoLabel": "At Level 5, take an additional Experience and clear all marks on Character Traits.",
+ "Pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.",
+ "Posttext": "Take an additional domain card of your level or lower from a domain you have access to."
+ },
+ "Tier4": {
+ "Label": "Levels 8-10",
"InfoLabel": "At Level 8, take an additional Experience and clear all marks on Character Traits.",
"Pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.",
- "Posttext": "Then, increase your Damage Thresholds: Minor by +1, Major by +2, and Severe by +4. Then choose a new Domain Deck card at your Level or lower. If your loadout is full, you may choose a card to swap."
+ "Posttext": "Take an additional domain card of your level or lower from a domain you have access to."
},
"ChoiceDescriptions": {
- "Attributes": "Increase two unmarked Character Traits by +1 and mark them.",
- "HitPointSlots": "Permanently add one Hit Point Slot.",
- "StressSlots": "Permanently add one Stress Slot.",
- "Experiences": "Increase two Experiences by +1.",
- "Proficiency": "Increase your Proficiency by +1",
- "ArmorOrEvasionSlot": "Permanently add one Armor Slot or take +1 to your Evasion.",
- "MajorDamageThreshold2": "Increase your Major Damage Threshold by +2.",
- "SevereDamageThreshold2": "Increase your Severe Damage Threshold by +2.",
- "MinorDamageThreshold2": "Increase your Minor Damage Threshold by +2.",
- "SevereDamageThreshold3": "Increase your Severe Damage Threshold by +3.",
- "Major2OrSevere4DamageThreshold": "Increase your Major Damage Threshold by +2 or Severe Damage Threshold by +4",
- "Minor1OrMajor1DamageThreshold": "Increase your Minor or Major Damage Threshold by +1.",
- "SevereDamageThreshold4": "Increase your Severe Damage Threshold by +4.",
- "MajorDamageThreshold1": "Increase your Major Damage Threshold by +1.",
+ "Attributes": "Gain a +1 bonus to two unmarked character traits and mark them.",
+ "HitPointSlots": "Permanently gain one Hit Point slot.",
+ "StressSlots": "Permanently gain one Stress slot.",
+ "Experiences": "Permanently gain a +1 bonus to two experiences.",
+ "DomainCard": "Choose an additional domain card of your level or lower from a domain you have access to (up to level {maxLevel})",
+ "Evasion": "Permanently gain a +1 bonus to your Evasion.",
+ "Proficiency": "Increase your Proficiency by +1.",
"Subclass": "Take an upgraded subclass card. Then cross out the multiclass option for this tier.",
"Multiclass": "Multiclass: Choose an additional class for your character, then cross out an unused “Take an upgraded subclass card” and the other multiclass option on this sheet."
}
@@ -748,8 +785,54 @@
"TakeDowntime": "Take Downtime"
},
"LevelUp": {
- "AdvanceLevel": "Continue To Level {level}",
- "TakeLevelUp": "Finish Level Up"
+ "Title": "{actor} Level Up",
+ "Tabs": {
+ "advancement": "Level Advancement",
+ "selections": "Advancement Choices",
+ "summary": "Summary"
+ },
+ "navigateLevel": "To Level {level}",
+ "navigateToLevelup": "Return To Levelup",
+ "navigateToSummary": "To Summary",
+ "TakeLevelUp": "Finish Level Up",
+ "Delevel": {
+ "title": "Go back to previous level",
+ "content": "Returning to the previous level selection will remove all selections made for this level. Do you want to proceed?"
+ },
+ "Selections": {
+ "emptyDomainCardHint": "Domain Card Level {level} or below"
+ },
+ "summary": {
+ "levelAchievements": "Level Achievements",
+ "levelAdvancements": "Level Advancements",
+ "proficiencyIncrease": "Proficiency Increased: {proficiency}",
+ "hpIncrease": "Hit Points Increased: {hitPoints}",
+ "stressIncrease": "Stress Increased: {stress}",
+ "evasionIncrease": "Evasion Increased: {evasion}",
+ "damageThresholdMajorIncrease": "Major: {threshold}",
+ "damageThresholdSevereIncrease": "Severe: {threshold}",
+ "newExperiences": "New Experiences",
+ "experiencePlaceholder": "A new experience..",
+ "domainCards": "Domain Cards",
+ "subclass": "Subclass",
+ "multiclass": "Multiclass",
+ "traits": "Increased Traits",
+ "experienceIncreases": "Experience Increases",
+ "damageThresholds": "Damage Thresholds"
+ },
+ "notifications": {
+ "info": {
+ "insufficentAdvancements": "You don't have enough advancements left.",
+ "insufficientTierAdvancements": "You have no available advancements for this tier."
+ },
+ "error": {
+ "domainCardWrongDomain": "You don't have access to that Domain",
+ "domainCardToHighLevel": "The Domain Card is too high level to be selected",
+ "domainCardDuplicate": "You already have that domain card!",
+ "noSelectionsLeft": "Nothing more to select!",
+ "alreadySelectedClass": "You already have that class!"
+ }
+ }
},
"DeathMove": {
"Title": "{actor} - Death Move",
@@ -910,7 +993,10 @@
},
"NewItem": "New Item",
"NewScar": "New Scar",
- "DeleteConfirmation": "Are you sure you want to delete the item - {item}?"
+ "DeleteConfirmation": "Are you sure you want to delete the item - {item}?",
+ "Errors": {
+ "missingClassOrSubclass": "The character doesn't have a class and subclass"
+ }
},
"Adversary": {
"Description": "Description",
diff --git a/module/applications/levelup.mjs b/module/applications/levelup.mjs
index 90500e2a..7e69a6a1 100644
--- a/module/applications/levelup.mjs
+++ b/module/applications/levelup.mjs
@@ -1,371 +1,673 @@
-import SelectDialog from '../dialogs/selectDialog.mjs';
-import { getTier } from '../helpers/utils.mjs';
-import DhpMulticlassDialog from './multiclassDialog.mjs';
+import { abilities } from '../config/actorConfig.mjs';
+import { domains } from '../config/domainConfig.mjs';
+import { DhLevelup } from '../data/levelup.mjs';
+import { getDeleteKeys, tagifyElement } from '../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
-export default class DhpLevelup extends HandlebarsApplicationMixin(ApplicationV2) {
+export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actor) {
super({});
this.actor = actor;
- this.data = foundry.utils.deepClone(actor.system.levelData);
- this.activeLevel = actor.system.levelData.currentLevel + 1;
+ this.levelTiers = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.LevelTiers);
+
+ const playerLevelupData = actor.system.levelData;
+ this.levelup = new DhLevelup(DhLevelup.initializeData(this.levelTiers, playerLevelupData, actor.system.level));
+
+ this._dragDrop = this._createDragDropHandlers();
+ this.tabGroups.primary = 'advancements';
}
get title() {
- return `${this.actor.name} - Level Up`;
+ return game.i18n.format('DAGGERHEART.Application.LevelUp.Title', { actor: this.actor.name });
}
static DEFAULT_OPTIONS = {
- classes: ['daggerheart', 'views', 'levelup'],
- position: { width: 1200, height: 'auto' },
+ tag: 'form',
+ classes: ['daggerheart', 'levelup'],
+ position: { width: 1000, height: 'auto' },
window: {
resizable: true
},
-
actions: {
- toggleBox: this.toggleBox,
- advanceLevel: this.advanceLevel,
- finishLevelup: this.finishLevelup
- }
+ save: this.save,
+ viewCompendium: this.viewCompendium,
+ selectPreview: this.selectPreview,
+ selectDomain: this.selectDomain,
+ updateCurrentLevel: this.updateCurrentLevel,
+ activatePart: this.activatePart
+ },
+ form: {
+ handler: this.updateForm,
+ submitOnChange: true,
+ closeOnSubmit: false
+ },
+ dragDrop: [{ dragSelector: null, dropSelector: '.levelup-card-selection .card-preview-container' }]
};
static PARTS = {
- form: {
- id: 'levelup',
- template: 'systems/daggerheart/templates/views/levelup.hbs'
+ tabs: { template: 'systems/daggerheart/templates/views/levelup/tabs/tab-navigation.hbs' },
+ advancements: { template: 'systems/daggerheart/templates/views/levelup/tabs/advancements.hbs' },
+ selections: { template: 'systems/daggerheart/templates/views/levelup/tabs/selections.hbs' },
+ summary: { template: 'systems/daggerheart/templates/views/levelup/tabs/summary.hbs' }
+ };
+
+ static TABS = {
+ advancements: {
+ active: true,
+ cssClass: '',
+ group: 'primary',
+ id: 'advancements',
+ icon: null,
+ label: 'DAGGERHEART.Application.LevelUp.Tabs.advancement'
+ },
+ selections: {
+ active: false,
+ cssClass: '',
+ group: 'primary',
+ id: 'selections',
+ icon: null,
+ label: 'DAGGERHEART.Application.LevelUp.Tabs.selections'
+ },
+ summary: {
+ active: false,
+ cssClass: '',
+ group: 'primary',
+ id: 'summary',
+ icon: null,
+ label: 'DAGGERHEART.Application.LevelUp.Tabs.summary'
}
};
async _prepareContext(_options) {
- let selectedChoices = 0,
- multiclassing = {},
- subclassing = {};
- const leveledTiers = Object.keys(this.data.levelups).reduce(
- (acc, levelKey) => {
- const levelData = this.data.levelups[levelKey];
- ['tier1', 'tier2', 'tier3'].forEach(tierKey => {
- let tierUpdate = {};
- const tierData = levelData[tierKey];
- if (tierData) {
- tierUpdate = Object.keys(tierData).reduce((acc, propertyKey) => {
- const values = tierData[propertyKey];
- const level = Number.parseInt(levelKey);
+ const context = await super._prepareContext(_options);
+ context.levelup = this.levelup;
+ context.tabs = this._getTabs(this.constructor.TABS);
- acc[propertyKey] = Object.values(values).map(value => {
- if (value && level === this.activeLevel) selectedChoices++;
- if (propertyKey === 'multiclass') multiclassing[levelKey] = true;
- if (propertyKey === 'subclass') subclassing[tierKey] = true;
+ return context;
+ }
- return { level: level, value: value };
- });
-
- return acc;
- }, {});
+ async _preparePartContext(partId, context) {
+ const currentLevel = this.levelup.levels[this.levelup.currentLevel];
+ switch (partId) {
+ case 'tabs':
+ const previous =
+ this.levelup.currentLevel === this.levelup.startLevel ? null : this.levelup.currentLevel - 1;
+ const next = this.levelup.currentLevel === this.levelup.endLevel ? null : this.levelup.currentLevel + 1;
+ context.navigate = {
+ previous: {
+ disabled: !previous,
+ label: previous
+ ? game.i18n.format('DAGGERHEART.Application.LevelUp.navigateLevel', { level: previous })
+ : '',
+ fromSummary: this.tabGroups.primary === 'summary'
+ },
+ next: {
+ disabled: !this.levelup.currentLevelFinished,
+ label: next
+ ? game.i18n.format('DAGGERHEART.Application.LevelUp.navigateLevel', { level: next })
+ : '',
+ toSummary: !next,
+ show: this.tabGroups.primary !== 'summary'
}
+ };
- Object.keys(tierUpdate).forEach(propertyKey => {
- const property = tierUpdate[propertyKey];
- const propertyValues = foundry.utils.getProperty(acc, `${tierKey}.${propertyKey}`) ?? [];
- foundry.utils.setProperty(acc, `${tierKey}.${propertyKey}`, [...propertyValues, ...property]);
+ const { selections } = currentLevel.nrSelections;
+ context.tabs.advancements.progress = { selected: selections, max: currentLevel.maxSelections };
+ context.showTabs = this.tabGroups.primary !== 'summary';
+ break;
+ case 'selections':
+ const advancementChoices = Object.keys(currentLevel.choices).reduce((acc, choiceKey) => {
+ Object.keys(currentLevel.choices[choiceKey]).forEach(checkboxNr => {
+ const checkbox = currentLevel.choices[choiceKey][checkboxNr];
+ const data = {
+ ...checkbox,
+ path: `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}`,
+ level: this.levelup.currentLevel
+ };
+
+ if (!acc[choiceKey]) acc[choiceKey] = [];
+ acc[choiceKey].push(data);
});
- });
-
- return acc;
- },
- { tier1: {}, tier2: {}, tier3: {} }
- );
-
- const activeTier = getTier(this.activeLevel);
- const data = Object.keys(SYSTEM.ACTOR.levelupData).reduce((acc, tierKey) => {
- const tier = SYSTEM.ACTOR.levelupData[tierKey];
- acc[tierKey] = {
- label: game.i18n.localize(tier.label),
- info: game.i18n.localize(tier.info),
- pretext: game.i18n.localize(tier.pretext),
- postext: game.i18n.localize(tier.posttext),
- active: tierKey <= activeTier,
- choices: Object.keys(tier.choices).reduce((acc, propertyKey) => {
- const property = tier.choices[propertyKey];
- acc[propertyKey] = { description: property.description, cost: property.cost ?? 1, values: [] };
- for (var i = 0; i < property.maxChoices; i++) {
- const leveledValue = leveledTiers[tierKey][propertyKey]?.[i];
- const subclassLock =
- propertyKey === 'subclass' &&
- Object.keys(multiclassing).find(x => getTier(Number.parseInt(x)) === tierKey);
- const subclassMulticlassLock = propertyKey === 'multiclass' && subclassing[tierKey];
- const multiclassLock =
- propertyKey === 'multiclass' &&
- Object.keys(multiclassing).length > 0 &&
- !(
- leveledValue &&
- Object.keys(multiclassing).find(x => Number.parseInt(x) === leveledValue.level)
- );
- const locked =
- (leveledValue && leveledValue.level !== this.activeLevel) ||
- subclassLock ||
- subclassMulticlassLock ||
- multiclassLock;
- const disabled =
- tierKey > activeTier ||
- (selectedChoices === 2 && !(leveledValue && leveledValue.level === this.activeLevel)) ||
- locked;
-
- acc[propertyKey].values.push({
- selected: leveledValue?.value !== undefined,
- path: `levelups.${this.activeLevel}.${tierKey}.${propertyKey}.${i}`,
- description: game.i18n.localize(property.description),
- disabled: disabled,
- locked: locked
- });
- }
return acc;
- }, {})
+ }, {});
+
+ const traits = Object.values(advancementChoices.trait ?? {});
+ const traitValues = traits.filter(trait => trait.data.length > 0).flatMap(trait => trait.data);
+ context.traits = {
+ values: traitValues,
+ active: traits.length > 0,
+ progress: {
+ selected: traitValues.length,
+ max: traits.reduce((acc, exp) => acc + exp.amount, 0)
+ }
+ };
+
+ const experienceIncreases = Object.values(advancementChoices.experience ?? {});
+ const experienceIncreaseValues = experienceIncreases
+ .filter(exp => exp.data.length > 0)
+ .flatMap(exp =>
+ exp.data.map(data => this.actor.system.experiences.find(x => x.id === data).description)
+ );
+ context.experienceIncreases = {
+ values: experienceIncreaseValues,
+ active: experienceIncreases.length > 0,
+ progress: {
+ selected: experienceIncreaseValues.length,
+ max: experienceIncreases.reduce((acc, exp) => acc + exp.amount, 0)
+ }
+ };
+
+ context.newExperiences = Object.keys(currentLevel.achievements.experiences).map(key => {
+ const experience = currentLevel.achievements.experiences[key];
+ return {
+ ...experience,
+ level: this.levelup.currentLevel,
+ key: key
+ };
+ });
+
+ const allDomainCards = {
+ ...advancementChoices.domainCard,
+ ...currentLevel.achievements.domainCards
+ };
+ const allDomainCardKeys = Object.keys(allDomainCards);
+
+ context.domainCards = [];
+ for (var key of allDomainCardKeys) {
+ const domainCard = allDomainCards[key];
+ if (domainCard.level > this.levelup.endLevel) continue;
+
+ const uuid = domainCard.data?.length > 0 ? domainCard.data[0] : domainCard.uuid;
+ const card = uuid ? await foundry.utils.fromUuid(uuid) : {};
+
+ context.domainCards.push({
+ ...(card.toObject?.() ?? card),
+ emptySubtext: game.i18n.format(
+ 'DAGGERHEART.Application.LevelUp.Selections.emptyDomainCardHint',
+ { level: domainCard.level }
+ ),
+ path: domainCard.data
+ ? `${domainCard.path}.data`
+ : `levels.${domainCard.level}.achievements.domainCards.${key}.uuid`,
+ limit: domainCard.level,
+ compendium: 'domains'
+ });
+ }
+
+ const subclassSelections = advancementChoices.subclass?.flatMap(x => x.data) ?? [];
+
+ const multiclassSubclass = this.actor.system.multiclass?.system?.subclasses?.[0];
+ const possibleSubclasses = [
+ this.actor.system.subclass,
+ ...(multiclassSubclass ? [multiclassSubclass] : [])
+ ];
+ const selectedSubclasses = possibleSubclasses.filter(x => subclassSelections.includes(x.uuid));
+ context.subclassCards = [];
+ if (advancementChoices.subclass?.length > 0) {
+ for (var subclass of possibleSubclasses) {
+ const data = await foundry.utils.fromUuid(subclass.uuid);
+ const selected = selectedSubclasses.some(x => x.uuid === data.uuid);
+ context.subclassCards.push({
+ ...data.toObject(),
+ uuid: data.uuid,
+ selected: selected
+ });
+ }
+ }
+
+ const multiclasses = Object.values(advancementChoices.multiclass ?? {});
+ if (multiclasses?.[0]) {
+ const data = multiclasses[0];
+ const multiclass = data.data.length > 0 ? await foundry.utils.fromUuid(data.data[0]) : {};
+
+ context.multiclass = {
+ ...data,
+ ...(multiclass.toObject?.() ?? multiclass),
+ uuid: multiclass.uuid,
+ domains:
+ multiclass?.system?.domains.map(key => {
+ const domain = domains[key];
+ const alreadySelected = this.actor.system.class.system.domains.includes(key);
+
+ return {
+ ...domain,
+ selected: key === data.secondaryData,
+ disabled: (data.secondaryData && key !== data.secondaryData) || alreadySelected
+ };
+ }) ?? [],
+ compendium: 'classes',
+ limit: 1
+ };
+ }
+
+ break;
+ case 'summary':
+ const { current: currentActorLevel, changed: changedActorLevel } = this.actor.system.levelData.level;
+ const actorArmor = this.actor.system.armor;
+ const levelKeys = Object.keys(this.levelup.levels);
+ let achivementProficiency = 0;
+ const achievementCards = [];
+ let achievementExperiences = [];
+ for (var levelKey of levelKeys) {
+ const level = this.levelup.levels[levelKey];
+ if (Number(levelKey) < this.levelup.startLevel) continue;
+
+ achivementProficiency += level.achievements.proficiency ?? 0;
+ const cards = level.achievements.domainCards ? Object.values(level.achievements.domainCards) : null;
+ if (cards) {
+ for (var card of cards) {
+ const itemCard = await foundry.utils.fromUuid(card.uuid);
+ achievementCards.push(itemCard);
+ }
+ }
+
+ achievementExperiences = level.achievements.experiences
+ ? Object.values(level.achievements.experiences).reduce((acc, experience) => {
+ if (experience.name) acc.push(experience);
+ return acc;
+ }, [])
+ : [];
+ }
+
+ context.achievements = {
+ proficiency: {
+ old: this.actor.system.proficiency.value,
+ new: this.actor.system.proficiency.value + achivementProficiency,
+ shown: achivementProficiency > 0
+ },
+ damageThresholds: {
+ major: {
+ old: this.actor.system.damageThresholds.major,
+ new: this.actor.system.damageThresholds.major + changedActorLevel - currentActorLevel
+ },
+ severe: {
+ old: this.actor.system.damageThresholds.severe,
+ new:
+ this.actor.system.damageThresholds.severe +
+ (actorArmor
+ ? changedActorLevel - currentActorLevel
+ : (changedActorLevel - currentActorLevel) * 2)
+ },
+ unarmored: !actorArmor
+ },
+ domainCards: {
+ values: achievementCards,
+ shown: achievementCards.length > 0
+ },
+ experiences: {
+ values: achievementExperiences
+ }
+ };
+
+ const advancement = {};
+ for (var levelKey of levelKeys) {
+ const level = this.levelup.levels[levelKey];
+ if (Number(levelKey) < this.levelup.startLevel) continue;
+
+ for (var choiceKey of Object.keys(level.choices)) {
+ const choice = level.choices[choiceKey];
+ for (var checkbox of Object.values(choice)) {
+ switch (choiceKey) {
+ case 'proficiency':
+ case 'hitPoint':
+ case 'stress':
+ case 'evasion':
+ advancement[choiceKey] = advancement[choiceKey]
+ ? advancement[choiceKey] + Number(checkbox.value)
+ : Number(checkbox.value);
+ break;
+ case 'domainCard':
+ if (!advancement[choiceKey]) advancement[choiceKey] = [];
+ if (checkbox.data.length === 1) {
+ const choiceItem = await foundry.utils.fromUuid(checkbox.data[0]);
+ advancement[choiceKey].push(choiceItem.toObject());
+ }
+ break;
+ case 'experience':
+ if (!advancement[choiceKey]) advancement[choiceKey] = [];
+ const data = checkbox.data.map(
+ data =>
+ this.actor.system.experiences.find(x => x.id === data)?.description ?? ''
+ );
+ advancement[choiceKey].push({ data: data, value: checkbox.value });
+ break;
+ }
+ }
+ }
+ }
+
+ context.advancements = {
+ statistics: {
+ proficiency: {
+ old: context.achievements.proficiency.new,
+ new: context.achievements.proficiency.new + (advancement.proficiency ?? 0)
+ },
+ hitPoints: {
+ old: this.actor.system.resources.hitPoints.max,
+ new: this.actor.system.resources.hitPoints.max + (advancement.hitPoint ?? 0)
+ },
+ stress: {
+ old: this.actor.system.resources.stress.max,
+ new: this.actor.system.resources.stress.max + (advancement.stress ?? 0)
+ },
+ evasion: {
+ old: this.actor.system.evasion.value,
+ new: this.actor.system.evasion.value + (advancement.evasion ?? 0)
+ }
+ },
+ traits:
+ advancement.trait?.flatMap(x =>
+ x.data.map(data => game.i18n.localize(abilities[data].label))
+ ) ?? [],
+ domainCards: advancement.domainCard ?? [],
+ experiences:
+ advancement.experience?.flatMap(x => x.data.map(data => ({ name: data, modifier: x.value }))) ??
+ []
+ };
+
+ context.advancements.statistics.proficiency.shown =
+ context.advancements.statistics.proficiency.new > context.advancements.statistics.proficiency.old;
+ context.advancements.statistics.hitPoints.shown =
+ context.advancements.statistics.hitPoints.new > context.advancements.statistics.hitPoints.old;
+ context.advancements.statistics.stress.shown =
+ context.advancements.statistics.stress.new > context.advancements.statistics.stress.old;
+ context.advancements.statistics.evasion.shown =
+ context.advancements.statistics.evasion.new > context.advancements.statistics.evasion.old;
+ context.advancements.statistics.shown =
+ context.advancements.statistics.proficiency.shown ||
+ context.advancements.statistics.hitPoints.shown ||
+ context.advancements.statistics.stress.shown ||
+ context.advancements.statistics.evasion.shown;
+
+ break;
+ }
+
+ return context;
+ }
+
+ _getTabs(tabs) {
+ for (const v of Object.values(tabs)) {
+ v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active;
+ v.cssClass = v.active ? 'active' : '';
+ }
+
+ return tabs;
+ }
+
+ _createDragDropHandlers() {
+ return this.options.dragDrop.map(d => {
+ d.callbacks = {
+ drop: this._onDrop.bind(this)
};
+ return new foundry.applications.ux.DragDrop.implementation(d);
+ });
+ }
+
+ _attachPartListeners(partId, htmlElement, options) {
+ super._attachPartListeners(partId, htmlElement, options);
+ htmlElement
+ .querySelectorAll('.selection-checkbox')
+ .forEach(element => element.addEventListener('change', this.selectionClick.bind(this)));
+
+ const traitsTagify = htmlElement.querySelector('.levelup-trait-increases');
+ if (traitsTagify) {
+ tagifyElement(traitsTagify, abilities, this.tagifyUpdate('trait').bind(this));
+ }
+
+ const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases');
+ if (experienceIncreaseTagify) {
+ tagifyElement(
+ experienceIncreaseTagify,
+ this.actor.system.experiences.reduce((acc, experience) => {
+ acc[experience.id] = { label: experience.description };
+
+ return acc;
+ }, {}),
+ this.tagifyUpdate('experience').bind(this)
+ );
+ }
+
+ this._dragDrop.forEach(d => d.bind(htmlElement));
+ }
+
+ tagifyUpdate =
+ type =>
+ async (_, { option, removed }) => {
+ const updatePath = Object.keys(this.levelup.levels[this.levelup.currentLevel].choices).reduce(
+ (acc, choiceKey) => {
+ const choice = this.levelup.levels[this.levelup.currentLevel].choices[choiceKey];
+ Object.keys(choice).forEach(checkboxNr => {
+ const checkbox = choice[checkboxNr];
+ if (
+ choiceKey === type &&
+ (removed ? checkbox.data.includes(option) : checkbox.data.length < checkbox.amount)
+ ) {
+ acc = `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}.data`;
+ }
+ });
+
+ return acc;
+ },
+ null
+ );
+
+ if (!updatePath) {
+ ui.notifications.error(
+ game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.noSelectionsLeft')
+ );
+ return;
+ }
+
+ const currentData = foundry.utils.getProperty(this.levelup, updatePath);
+ const updatedData = removed ? currentData.filter(x => x !== option) : [...currentData, option];
+ await this.levelup.updateSource({ [updatePath]: updatedData });
+ this.render();
+ };
+
+ static async updateForm(event, _, formData) {
+ const { levelup } = foundry.utils.expandObject(formData.object);
+ await this.levelup.updateSource(levelup);
+ this.render();
+ }
+
+ async _onDrop(event) {
+ const data = foundry.applications.ux.TextEditor.getDragEventData(event);
+ const item = await fromUuid(data.uuid);
+ if (event.target.closest('.domain-cards')) {
+ const target = event.target.closest('.card-preview-container');
+ if (item.type === 'domainCard') {
+ if (
+ !this.actor.system.class.system.domains.includes(item.system.domain) &&
+ this.levelup.classUpgradeChoices?.multiclass?.domain !== item.system.domain
+ ) {
+ ui.notifications.error(
+ game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.domainCardWrongDomain')
+ );
+ return;
+ }
+
+ if (item.system.level > Number(target.dataset.limit)) {
+ ui.notifications.error(
+ game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.domainCardToHighLevel')
+ );
+ return;
+ }
+
+ if (
+ Object.values(this.levelup.levels).some(level => {
+ const achievementExists = Object.values(level.achievements.domainCards).some(
+ card => card.uuid === item.uuid
+ );
+ const advancementExists = Object.keys(level.choices).some(choiceKey => {
+ if (choiceKey !== 'domainCard') return false;
+ const choice = level.choices[choiceKey];
+ return Object.values(choice).some(checkbox => checkbox.data.includes(item.uuid));
+ });
+
+ return achievementExists || advancementExists;
+ })
+ ) {
+ ui.notifications.error(
+ game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.domainCardDuplicate')
+ );
+ return;
+ }
+
+ await this.levelup.updateSource({ [target.dataset.path]: item.uuid });
+ this.render();
+ }
+ } else if (event.target.closest('.multiclass-cards')) {
+ const target = event.target.closest('.multiclass-cards');
+ if (item.type === 'class') {
+ if (item.name === this.actor.system.class.name) {
+ ui.notifications.error(
+ game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.alreadySelectedClass')
+ );
+ return;
+ }
+
+ await this.levelup.updateSource({
+ multiclass: {
+ class: item.uuid,
+ level: this.levelup.currentLevel,
+ tier: Number(target.dataset.tier)
+ },
+ [target.dataset.path]: {
+ tier: Number(target.dataset.tier),
+ minCost: Number(target.dataset.minCost),
+ amount: target.dataset.amount ? Number(target.dataset.amount) : null,
+ value: target.dataset.value,
+ type: target.dataset.type,
+ data: item.uuid,
+ secondaryData: null
+ }
+ });
+ this.render();
+ }
+ }
+ }
+
+ async selectionClick(event) {
+ event.stopPropagation();
+ const button = event.currentTarget;
+
+ const update = {};
+ if (!button.checked) {
+ if (button.dataset.cost > 1) {
+ // Simple handling that doesn't cover potential Custom LevelTiers.
+ update[`levels.${this.levelup.currentLevel}.choices.-=${button.dataset.option}`] = null;
+ } else {
+ update[
+ `levels.${this.levelup.currentLevel}.choices.${button.dataset.option}.-=${button.dataset.checkboxNr}`
+ ] = null;
+ }
+ } else {
+ if (!this.levelup.levels[this.levelup.currentLevel].nrSelections.available) {
+ ui.notifications.info(
+ game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.info.insufficentAdvancements')
+ );
+ this.render();
+ return;
+ }
+
+ update[
+ `levels.${this.levelup.currentLevel}.choices.${button.dataset.option}.${button.dataset.checkboxNr}`
+ ] = {
+ tier: Number(button.dataset.tier),
+ minCost: Number(button.dataset.cost),
+ amount: button.dataset.amount ? Number(button.dataset.amount) : null,
+ value: button.dataset.value,
+ type: button.dataset.type
+ };
+ }
+
+ await this.levelup.updateSource(update);
+ this.render();
+ }
+
+ static async viewCompendium(_, button) {
+ (await game.packs.get(`daggerheart.${button.dataset.compendium}`))?.render(true);
+ }
+
+ static async selectPreview(_, button) {
+ const remove = button.dataset.selected;
+ const selectionData = Object.values(this.levelup.selectionData);
+ const option = remove
+ ? selectionData.find(x => x.type === 'subclass' && x.data.includes(button.dataset.uuid))
+ : selectionData.find(x => x.type === 'subclass' && x.data.length === 0);
+ if (!option) return;
+
+ const path = `tiers.${option.tier}.levels.${option.level}.optionSelections.${option.optionKey}.${option.checkboxNr}.data`;
+ await this.levelup.updateSource({ [path]: remove ? [] : button.dataset.uuid });
+ this.render();
+ }
+
+ static async selectDomain(_, button) {
+ const option = foundry.utils.getProperty(this.levelup, button.dataset.path);
+ const domain = option.secondaryData ? null : button.dataset.domain;
+
+ await this.levelup.updateSource({
+ multiclass: { domain },
+ [`${button.dataset.path}.secondaryData`]: domain
+ });
+ this.render();
+ }
+
+ static async updateCurrentLevel(_, button) {
+ if (!button.dataset.forward) {
+ const confirmed = await foundry.applications.api.DialogV2.confirm({
+ window: {
+ title: game.i18n.localize('DAGGERHEART.Application.LevelUp.Delevel.title')
+ },
+ content: game.i18n.format('DAGGERHEART.Application.LevelUp.Delevel.content')
+ });
+
+ if (!confirmed) return;
+
+ await this.levelup.updateSource({
+ currentLevel: Math.min(this.levelup.currentLevel - 1, this.levelup.startLevel),
+ levels: Object.keys(this.levelup.levels).reduce((acc, key) => {
+ const level = this.levelup.levels[key];
+ if (Number(key) === this.levelup.currentLevel) {
+ acc[key] = {
+ achievements: {
+ experiences: getDeleteKeys(level.achievements.experiences, 'name', ''),
+ domainCards: getDeleteKeys(level.achievements.domainCards, 'uuid', null)
+ },
+ choices: getDeleteKeys(level.choices)
+ };
+ }
+ return acc;
+ }, {})
+ });
+ } else {
+ await this.levelup.updateSource({
+ currentLevel: Math.min(this.levelup.currentLevel + 1, this.levelup.endLevel)
+ });
+ }
+
+ this.tabGroups.primary = 'advancements';
+ this.render();
+ }
+
+ static activatePart(_, button) {
+ this.tabGroups.primary = button.dataset.part;
+ this.render();
+ }
+
+ static async save() {
+ const levelupData = Object.keys(this.levelup.levels).reduce((acc, level) => {
+ if (level >= this.levelup.startLevel) {
+ acc[level] = this.levelup.levels[level].toObject();
+ }
return acc;
}, {});
- return {
- data: data,
- activeLevel: this.activeLevel,
- changedLevel: this.actor.system.levelData.changedLevel,
- completedSelection: selectedChoices === 2
- };
- }
-
- static async toggleBox(_, button) {
- const path = button.dataset.path;
- if (foundry.utils.getProperty(this.data, path)) {
- const pathParts = path.split('.');
- const arrayPart = pathParts.slice(0, pathParts.length - 1).join('.');
- let array = foundry.utils.getProperty(this.data, arrayPart);
- if (button.dataset.levelAttribute === 'multiclass') {
- array = [];
- } else {
- delete array[Number.parseInt(pathParts[pathParts.length - 1])];
- }
- foundry.utils.setProperty(this.data, arrayPart, array);
- } else {
- const updates = [{ path: path, value: { level: this.activeLevel } }];
- const levelChoices = SYSTEM.ACTOR.levelChoices[button.dataset.levelAttribute];
- if (button.dataset.levelAttribute === 'subclass') {
- if (!this.actor.system.multiclassSubclass) {
- updates[0].value.value = {
- multiclass: false,
- feature: this.actor.system.subclass.system.specializationFeature.unlocked
- ? 'mastery'
- : 'specialization'
- };
- } else {
- const choices = [
- { name: this.actor.system.subclass.name, value: this.actor.system.subclass.uuid },
- {
- name: this.actor.system.multiclassSubclass.name,
- value: this.actor.system.multiclassSubclass.uuid
- }
- ];
- const indexes = await SelectDialog.selectItem({
- actor: this.actor,
- choices: choices,
- title: levelChoices.title,
- nrChoices: 1
- });
- if (indexes.length === 0) {
- this.render();
- return;
- }
- const multiclassSubclass = choices[indexes[0]].name === this.actor.system.multiclassSubclass.name;
- updates[0].value.value = {
- multiclass: multiclassSubclass,
- feature: this.actor.system.multiclassSubclass.system.specializationFeature.unlocked
- ? 'mastery'
- : 'specialization'
- };
- }
- } else if (button.dataset.levelAttribute === 'multiclass') {
- const multiclassAwait = new Promise(resolve => {
- new DhpMulticlassDialog(this.actor.name, this.actor.system.class, resolve).render(true);
- });
- const multiclassData = await multiclassAwait;
- if (!multiclassData) {
- this.render();
- return;
- }
-
- const pathParts = path.split('.');
- const arrayPart = pathParts.slice(0, pathParts.length - 1).join('.');
- updates[0] = {
- path: [arrayPart, '0'].join('.'),
- value: {
- level: this.activeLevel,
- value: {
- class: multiclassData.class,
- subclass: multiclassData.subclass,
- domain: multiclassData.domain,
- level: this.activeLevel
- }
- }
- };
- updates[1] = {
- path: [arrayPart, '1'].join('.'),
- value: {
- level: this.activeLevel,
- value: {
- class: multiclassData.class,
- subclass: multiclassData.subclass,
- domain: multiclassData.domain,
- level: this.activeLevel
- }
- }
- };
- } else {
- if (levelChoices.choices.length > 0) {
- if (typeof levelChoices.choices === 'string') {
- const choices = foundry.utils
- .getProperty(this.actor, levelChoices.choices)
- .map(x => ({ name: x.description, value: x.id }));
- const indexes = await SelectDialog.selectItem({
- actor: this.actor,
- choices: choices,
- title: levelChoices.title,
- nrChoices: levelChoices.nrChoices
- });
- if (indexes.length === 0) {
- this.render();
- return;
- }
- updates[0].value.value = choices
- .filter((_, index) => indexes.includes(index))
- .map(x => x.value);
- } else {
- const indexes = await SelectDialog.selectItem({
- actor: this.actor,
- choices: levelChoices.choices,
- title: levelChoices.title,
- nrChoices: levelChoices.nrChoices
- });
- if (indexes.length === 0) {
- this.render();
- return;
- }
- updates[0].value.value = levelChoices.choices[indexes[0]].path;
- }
- }
- }
-
- const update = updates.reduce((acc, x) => {
- acc[x.path] = x.value;
-
- return acc;
- }, {});
-
- this.data = foundry.utils.mergeObject(this.data, update);
- }
-
- this.render();
- }
-
- static advanceLevel() {
- this.activeLevel += 1;
- this.render();
- }
-
- static async finishLevelup() {
- this.data.currentLevel = this.data.changedLevel;
- let multiclass = null;
- for (var level in this.data.levelups) {
- for (var tier in this.data.levelups[level]) {
- for (var category in this.data.levelups[level][tier]) {
- for (var value in this.data.levelups[level][tier][category]) {
- if (category === 'multiclass') {
- multiclass = this.data.levelups[level][tier][category][value].value;
- this.data.levelups[level][tier][category][value] = true;
- } else {
- this.data.levelups[level][tier][category][value] =
- this.data.levelups[level][tier][category][value].value ?? true;
- }
- }
- }
- }
- }
-
- const tiersMoved =
- getTier(this.actor.system.levelData.changedLevel, true) -
- getTier(this.actor.system.levelData.currentLevel, true);
- const experiences = Array.from(Array(tiersMoved), (_, index) => ({
- id: foundry.utils.randomID(),
- level: this.actor.system.experiences.length + index * 3,
- description: '',
- value: 1
- }));
-
- await this.actor.update(
- {
- system: {
- levelData: this.data,
- experiences: [...this.actor.system.experiences, ...experiences]
- }
- },
- { diff: false }
- );
-
- if (!this.actor.multiclass && multiclass) {
- const multiclassClass = (await fromUuid(multiclass.class.uuid)).toObject();
- multiclassClass.system.domains = [multiclass.domain.id];
- multiclassClass.system.multiclass = multiclass.level;
-
- const multiclassFeatures = [];
- for (var i = 0; i < multiclassClass.system.features.length; i++) {
- const feature = (await fromUuid(multiclassClass.system.features[i].uuid)).toObject();
- feature.system.multiclass = multiclass.level;
- multiclassFeatures.push(feature);
- }
-
- const multiclassSubclass = (await fromUuid(multiclass.subclass.uuid)).toObject();
- multiclassSubclass.system.multiclass = multiclass.level;
-
- const multiclassSubclassFeatures = {};
- const features = [
- multiclassSubclass.system.foundationFeature,
- multiclassSubclass.system.specializationFeature,
- multiclassSubclass.system.masteryFeature
- ];
- for (var i = 0; i < features.length; i++) {
- const path = i === 0 ? 'foundationFeature' : i === 1 ? 'specializationFeature' : 'masteryFeature';
- const feature = features[i];
- for (var ability of feature.abilities) {
- const data = (await fromUuid(ability.uuid)).toObject();
- if (i > 0) data.system.disabled = true;
- data.system.multiclass = multiclass.level;
- if (!multiclassSubclassFeatures[path]) multiclassSubclassFeatures[path] = [data];
- else multiclassSubclassFeatures[path].push(data);
- // data.uuid = feature.uuid;
-
- // const abilityData = await this._onDropItemCreate(data);
- // ability.uuid = abilityData[0].uuid;
-
- // createdItems.push(abilityData);
- }
- }
-
- for (let subclassFeaturesKey in multiclassSubclassFeatures) {
- const values = multiclassSubclassFeatures[subclassFeaturesKey];
- const abilityResults = await this.actor.createEmbeddedDocuments('Item', values);
- for (var i = 0; i < abilityResults.length; i++) {
- multiclassSubclass.system[subclassFeaturesKey].abilities[i].uuid = abilityResults[i].uuid;
- }
- }
-
- await this.actor.createEmbeddedDocuments('Item', [
- multiclassClass,
- ...multiclassFeatures,
- multiclassSubclass
- ]);
- }
-
+ await this.actor.levelUp(levelupData);
this.close();
}
}
diff --git a/module/applications/resources.mjs b/module/applications/resources.mjs
index 86f85178..bbd47fc5 100644
--- a/module/applications/resources.mjs
+++ b/module/applications/resources.mjs
@@ -1,4 +1,3 @@
-
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
/**
@@ -10,100 +9,101 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
*/
export default class Resources extends HandlebarsApplicationMixin(ApplicationV2) {
- constructor(options={}) {
- super(options);
- }
-
- /** @inheritDoc */
- static DEFAULT_OPTIONS = {
- id: "resources",
- classes: [],
- tag: "div",
- window: {
- frame: true,
- title: "Fear",
- positioned: true,
- resizable: true
- },
- actions: {
- setFear: Resources.setFear,
- increaseFear: Resources.increaseFear
- },
- position: {
- width: 222,
- height: 222,
- // top: "200px",
- // left: "120px"
+ constructor(options = {}) {
+ super(options);
}
- };
- /** @override */
- static PARTS = {
- resources: {
- root: true,
- template: "systems/daggerheart/templates/views/resources.hbs"
- // template: "templates/ui/players.hbs"
+ /** @inheritDoc */
+ static DEFAULT_OPTIONS = {
+ id: 'resources',
+ classes: [],
+ tag: 'div',
+ window: {
+ frame: true,
+ title: 'Fear',
+ positioned: true,
+ resizable: true,
+ minimizable: false
+ },
+ actions: {
+ setFear: Resources.setFear,
+ increaseFear: Resources.increaseFear
+ },
+ position: {
+ width: 222,
+ height: 222
+ // top: "200px",
+ // left: "120px"
+ }
+ };
+
+ /** @override */
+ static PARTS = {
+ resources: {
+ root: true,
+ template: 'systems/daggerheart/templates/views/resources.hbs'
+ }
+ };
+
+ get currentFear() {
+ return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear);
}
- };
- get currentFear() {
- return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear);
- }
+ get maxFear() {
+ return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.MaxFear);
+ }
- get maxFear() {
- return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.MaxFear);
- }
+ /* -------------------------------------------- */
+ /* Rendering */
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
- /* Rendering */
- /* -------------------------------------------- */
+ /** @override */
+ async _prepareContext(_options) {
+ const display = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.DisplayFear),
+ current = this.currentFear,
+ max = this.maxFear,
+ percent = (current / max) * 100,
+ isGM = game.user.isGM;
+ // Return the data for rendering
+ return { display, current, max, percent, isGM };
+ }
- /** @override */
- async _prepareContext(_options) {
- const display = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.DisplayFear),
- current = this.currentFear,
- max = this.maxFear,
- percent = (current / max) * 100,
- isGM = game.user.isGM;
- // Return the data for rendering
- return {display, current, max, percent, isGM};
- }
+ /** @override */
+ async _preFirstRender(context, options) {
+ options.position = game.user.getFlag(SYSTEM.id, 'app.resources.position') ?? Resources.DEFAULT_OPTIONS.position;
+ }
- /** @override */
- async _preFirstRender(context, options) {
- options.position = game.user.getFlag(SYSTEM.id, 'app.resources.position') ?? Resources.DEFAULT_OPTIONS.position;
- }
+ /** @override */
+ async _preRender(context, options) {
+ if (this.currentFear > this.maxFear)
+ await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, this.maxFear);
+ }
- /** @override */
- async _preRender(context, options) {
- if(this.currentFear > this.maxFear) await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, this.maxFear);
- }
+ _onPosition(position) {
+ game.user.setFlag(SYSTEM.id, 'app.resources.position', position);
+ }
- _onPosition(position) {
- game.user.setFlag(SYSTEM.id, 'app.resources.position', position);
- }
+ async close(options = {}) {
+ if (!options.allowed) return;
+ else super.close(options);
+ }
- async close(options={}) {
- if(!options.allowed) return;
- else super.close(options);
- }
+ static async setFear(event, target) {
+ if (!game.user.isGM) return;
+ const fearCount = Number(target.dataset.index ?? 0);
+ await this.updateFear(this.currentFear === fearCount + 1 ? fearCount : fearCount + 1);
+ }
- static async setFear(event, target) {
- if(!game.user.isGM) return;
- const fearCount = Number(target.dataset.index ?? 0);
- await this.updateFear(this.currentFear === fearCount + 1 ? fearCount : fearCount + 1);
- }
+ static async increaseFear(event, target) {
+ let value = target.dataset.increment ?? 0,
+ operator = value.split('')[0] ?? null;
+ value = Number(value);
+ await this.updateFear(operator ? this.currentFear + value : value);
+ }
- static async increaseFear(event, target) {
- let value = target.dataset.increment ?? 0,
- operator = value.split('')[0] ?? null;
- value = Number(value);
- await this.updateFear(operator ? this.currentFear + value : value);
- }
-
- async updateFear(value) {
- if(!game.user.isGM) return;
- value = Math.max(0, Math.min(this.maxFear, value));
- await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, value);
- }
-}
\ No newline at end of file
+ async updateFear(value) {
+ if (!game.user.isGM) return;
+ value = Math.max(0, Math.min(this.maxFear, value));
+ await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, value);
+ }
+}
diff --git a/module/applications/settings.mjs b/module/applications/settings.mjs
index 4a885d17..a7eed1b9 100644
--- a/module/applications/settings.mjs
+++ b/module/applications/settings.mjs
@@ -1,5 +1,9 @@
+import { DualityRollColor } from '../config/settingsConfig.mjs';
+import { defaultLevelTiers, DhLevelTiers } from '../data/levelTier.mjs';
import DhAppearance from '../data/settings/Appearance.mjs';
import DHAppearanceSettings from './settings/appearanceSettings.mjs';
+import DhVariantRules from '../data/settings/VariantRules.mjs';
+import DHVariantRuleSettings from './settings/variantRuleSettings.mjs';
class DhpAutomationSettings extends FormApplication {
constructor(object = {}, options = {}) {
@@ -181,7 +185,8 @@ export const registerDHSettings = () => {
type: Number,
default: 0,
onChange: () => {
- if(ui.resources) ui.resources.render({force: true});
+ if (ui.resources) ui.resources.render({ force: true });
+ ui.combat.render({ force: true });
}
});
@@ -193,7 +198,7 @@ export const registerDHSettings = () => {
type: Number,
default: 12,
onChange: () => {
- if(ui.resources) ui.resources.render({force: true});
+ if (ui.resources) ui.resources.render({ force: true });
}
});
@@ -204,15 +209,15 @@ export const registerDHSettings = () => {
config: true,
type: String,
choices: {
- 'token': 'Tokens',
- 'bar': 'Bar',
- 'hide': 'Hide'
+ token: 'Tokens',
+ bar: 'Bar',
+ hide: 'Hide'
},
default: 'token',
onChange: value => {
- if(ui.resources) {
- if(value === 'hide') ui.resources.close({allowed: true});
- else ui.resources.render({force: true});
+ if (ui.resources) {
+ if (value === 'hide') ui.resources.close({ allowed: true });
+ else ui.resources.render({ force: true });
}
}
});
@@ -251,6 +256,13 @@ export const registerDHSettings = () => {
}
});
+ game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.variantRules, {
+ scope: 'world',
+ config: false,
+ type: DhVariantRules,
+ default: DhVariantRules.defaultSchema
+ });
+
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance, {
scope: 'client',
config: false,
@@ -258,6 +270,23 @@ export const registerDHSettings = () => {
default: DhAppearance.defaultSchema
});
+ game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.DualityRollColor, {
+ name: game.i18n.localize('DAGGERHEART.Settings.DualityRollColor.Name'),
+ hint: game.i18n.localize('DAGGERHEART.Settings.DualityRollColor.Hint'),
+ scope: 'world',
+ config: true,
+ type: Number,
+ choices: Object.values(DualityRollColor),
+ default: DualityRollColor.colorful.value
+ });
+
+ game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.LevelTiers, {
+ scope: 'world',
+ config: false,
+ type: DhLevelTiers,
+ default: defaultLevelTiers
+ });
+
game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.Automation.Name, {
name: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Name'),
label: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Label'),
@@ -291,4 +320,13 @@ export const registerDHSettings = () => {
type: DHAppearanceSettings,
restricted: false
});
+
+ game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.VariantRules.Name, {
+ name: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.title'),
+ label: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.label'),
+ hint: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.hint'),
+ icon: SYSTEM.SETTINGS.menu.VariantRules.Icon,
+ type: DHVariantRuleSettings,
+ restricted: false
+ });
};
diff --git a/module/applications/settings/variantRuleSettings.mjs b/module/applications/settings/variantRuleSettings.mjs
new file mode 100644
index 00000000..101d3e42
--- /dev/null
+++ b/module/applications/settings/variantRuleSettings.mjs
@@ -0,0 +1,59 @@
+import DhVariantRules from '../../data/settings/VariantRules.mjs';
+
+const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
+
+export default class DHVariantRuleSettings extends HandlebarsApplicationMixin(ApplicationV2) {
+ constructor() {
+ super({});
+
+ this.settings = new DhVariantRules(
+ game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.variantRules).toObject()
+ );
+ }
+
+ get title() {
+ return game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.name');
+ }
+
+ static DEFAULT_OPTIONS = {
+ tag: 'form',
+ id: 'daggerheart-appearance-settings',
+ classes: ['daggerheart', 'setting', 'dh-style'],
+ position: { width: '600', height: 'auto' },
+ actions: {
+ reset: this.reset,
+ save: this.save
+ },
+ form: { handler: this.updateData, submitOnChange: true }
+ };
+
+ static PARTS = {
+ main: {
+ template: 'systems/daggerheart/templates/settings/variant-rules.hbs'
+ }
+ };
+
+ async _prepareContext(_options) {
+ const context = await super._prepareContext(_options);
+ context.settingFields = this.settings;
+
+ return context;
+ }
+
+ static async updateData(event, element, formData) {
+ const updatedSettings = foundry.utils.expandObject(formData.object);
+
+ await this.settings.updateSource(updatedSettings);
+ this.render();
+ }
+
+ static async reset() {
+ this.settings = new DhVariantRules();
+ this.render();
+ }
+
+ static async save() {
+ await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.variantRules, this.settings.toObject());
+ this.close();
+ }
+}
diff --git a/module/applications/sheets/adversary.mjs b/module/applications/sheets/adversary.mjs
index 2087ea0a..b9180bf3 100644
--- a/module/applications/sheets/adversary.mjs
+++ b/module/applications/sheets/adversary.mjs
@@ -362,7 +362,7 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) {
name: x.actor.name,
img: x.actor.img,
difficulty: x.actor.system.difficulty,
- evasion: x.actor.system.evasion
+ evasion: x.actor.system.evasion.value
}));
const cls = getDocumentClass('ChatMessage');
diff --git a/module/applications/sheets/pc.mjs b/module/applications/sheets/pc.mjs
index e76dd9f3..b38195d5 100644
--- a/module/applications/sheets/pc.mjs
+++ b/module/applications/sheets/pc.mjs
@@ -1,10 +1,10 @@
import { capitalize } from '../../helpers/utils.mjs';
import DhpDeathMove from '../deathMove.mjs';
import DhpDowntime from '../downtime.mjs';
-import DhpLevelup from '../levelup.mjs';
import AncestrySelectionDialog from '../ancestrySelectionDialog.mjs';
import DaggerheartSheet from './daggerheart-sheet.mjs';
import { abilities } from '../../config/actorConfig.mjs';
+import DhlevelUp from '../levelup.mjs';
const { ActorSheetV2 } = foundry.applications.sheets;
const { TextEditor } = foundry.applications.ux;
@@ -167,13 +167,23 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) {
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
- $(htmlElement).find('.attribute-value').on('change', this.attributeChange.bind(this));
- $(htmlElement).find('.tab-selector').on('click', this.tabSwitch.bind(this));
- $(htmlElement).find('.level-title.levelup').on('click', this.openLevelUp.bind(this));
- $(htmlElement).find('.feature-input').on('change', this.onFeatureInputBlur.bind(this));
- $(htmlElement).find('.experience-description').on('change', this.experienceDescriptionChange.bind(this));
- $(htmlElement).find('.experience-value').on('change', this.experienceValueChange.bind(this));
- $(htmlElement).find('[data-item]').on('change', this.itemUpdate.bind(this));
+ htmlElement
+ .querySelectorAll('.attribute-value')
+ .forEach(element => element.addEventListener('change', this.attributeChange.bind(this)));
+ htmlElement
+ .querySelectorAll('.tab-selector')
+ .forEach(element => element.addEventListener('click', this.tabSwitch.bind(this)));
+ htmlElement.querySelector('.level-title.levelup')?.addEventListener('click', this.openLevelUp.bind(this));
+ htmlElement
+ .querySelectorAll('.feature-input')
+ .forEach(element => element.addEventListener('change', this.onFeatureInputBlur.bind(this)));
+ htmlElement
+ .querySelectorAll('.experience-description')
+ .forEach(element => element.addEventListener('change', this.experienceDescriptionChange.bind(this)));
+ htmlElement
+ .querySelectorAll('.experience-value')
+ .forEach(element => element.addEventListener('change', this.experienceValueChange.bind(this)));
+ htmlElement.querySelector('.level-value').addEventListener('change', this.onLevelChange.bind(this));
}
async _prepareContext(_options) {
@@ -188,7 +198,7 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) {
context.storyEditor = this.storyEditor;
context.multiclassFeatureSetSelected = this.multiclassFeatureSetSelected;
- const selectedAttributes = Object.values(this.document.system.attributes).map(x => x.data.base);
+ const selectedAttributes = Object.values(this.document.system.traits).map(x => x.base);
context.abilityScoreArray = JSON.parse(
await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.AbilityArray)
).reduce((acc, x) => {
@@ -204,7 +214,7 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) {
if (!context.abilityScoreArray.includes(0)) context.abilityScoreArray.push({ name: 0, value: 0 });
context.abilityScoresFinished = context.abilityScoreArray.every(x => x.value === 0);
- //FIXME:
+ //FIXME:
context.domains = this.document.system.class
? {
first: this.document.system.class.system.domains[0]
@@ -216,9 +226,9 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) {
}
: {};
- context.attributes = Object.keys(this.document.system.attributes).reduce((acc, key) => {
+ context.attributes = Object.keys(this.document.system.traits).reduce((acc, key) => {
acc[key] = {
- ...this.document.system.attributes[key],
+ ...this.document.system.traits[key],
name: game.i18n.localize(SYSTEM.ACTOR.abilities[key].name),
verbs: SYSTEM.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x))
};
@@ -480,7 +490,7 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) {
}
async attributeChange(event) {
- const path = `system.attributes.${event.currentTarget.dataset.attribute}.data.base`;
+ const path = `system.traits.${event.currentTarget.dataset.attribute}.base`;
await this.document.update({ [path]: event.currentTarget.value });
}
@@ -532,21 +542,21 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) {
}
static async toggleAttributeMark(_, button) {
- const attribute = this.document.system.attributes[button.dataset.attribute];
+ const attribute = this.document.system.traits[button.dataset.attribute];
const newMark = this.document.system.availableAttributeMarks
- .filter(x => x > Math.max.apply(null, this.document.system.attributes[button.dataset.attribute].levelMarks))
+ .filter(x => x > Math.max.apply(null, this.document.system.traits[button.dataset.attribute].levelMarks))
.sort((a, b) => (a > b ? 1 : -1))[0];
if (attribute.levelMark || !newMark) return;
- const path = `system.attributes.${button.dataset.attribute}.levelMarks`;
+ const path = `system.traits.${button.dataset.attribute}.levelMarks`;
await this.document.update({ [path]: [...attribute.levelMarks, newMark] });
}
static async toggleHP(_, button) {
const healthValue = Number.parseInt(button.dataset.value);
- const newValue = this.document.system.resources.health.value >= healthValue ? healthValue - 1 : healthValue;
- await this.document.update({ 'system.resources.health.value': newValue });
+ const newValue = this.document.system.resources.hitPoints.value >= healthValue ? healthValue - 1 : healthValue;
+ await this.document.update({ 'system.resources.hitPoints.value': newValue });
}
static async toggleStress(_, button) {
@@ -577,7 +587,7 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) {
type: weapon.system.damage.type,
bonusDamage: this.document.system.bonuses.damage
};
- const modifier = this.document.system.attributes[weapon.system.trait].data.value;
+ const modifier = this.document.system.traits[weapon.system.trait].value;
const { roll, hope, fear, advantage, disadvantage, modifiers, bonusDamageString } =
await this.document.dualityRoll(
@@ -593,7 +603,7 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) {
name: x.actor.name,
img: x.actor.img,
difficulty: x.actor.system.difficulty,
- evasion: x.actor.system.evasion
+ evasion: x.actor.system.evasion.value
}));
const systemData = {
@@ -634,7 +644,12 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) {
}
openLevelUp() {
- new DhpLevelup(this.document).render(true);
+ if (!this.document.system.class || !this.document.system.subclass) {
+ ui.notifications.error(game.i18n.localize('DAGGERHEART.Sheets.PC.Errors.missingClassOrSubclass'));
+ return;
+ }
+
+ new DhlevelUp(this.document).render(true);
}
static domainCardsTab(toVault) {
@@ -782,7 +797,7 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) {
}
static async makeDeathMove() {
- if (this.document.system.resources.health.value === this.document.system.resources.health.max) {
+ if (this.document.system.resources.hitPoints.value === this.document.system.resources.hitPoints.max) {
await new DhpDeathMove(this.document).render(true);
await this.minimize();
}
@@ -867,6 +882,11 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) {
await item.update({ [name]: event.currentTarget.value });
}
+ async onLevelChange(event) {
+ await this.document.updateLevel(Number(event.currentTarget.value));
+ this.render();
+ }
+
static async deleteItem(_, button) {
const item = await fromUuid($(button).closest('[data-item-id]')[0].dataset.itemId);
await item.delete();
@@ -1090,7 +1110,7 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) {
await itemObject.update({ 'system.active': true });
break;
case 'inventory-weapon-section':
- /* FIXME inventoryWeapon is no longer a field
+ /* FIXME inventoryWeapon is no longer a field
const existingInventoryWeapon = this.document.items.find(x => x.system.inventoryWeapon);
await existingInventoryWeapon?.update({ 'system.inventoryWeapon': false });
await itemObject.update({ 'system.inventoryWeapon': true });
diff --git a/module/config/actorConfig.mjs b/module/config/actorConfig.mjs
index 241d3302..4db5ca9c 100644
--- a/module/config/actorConfig.mjs
+++ b/module/config/actorConfig.mjs
@@ -52,31 +52,31 @@ export const abilities = {
export const featureProperties = {
agility: {
name: 'DAGGERHEART.Abilities.agility.name',
- path: actor => actor.system.attributes.agility.data.value
+ path: actor => actor.system.traits.agility.data.value
},
strength: {
name: 'DAGGERHEART.Abilities.strength.name',
- path: actor => actor.system.attributes.strength.data.value
+ path: actor => actor.system.traits.strength.data.value
},
finesse: {
name: 'DAGGERHEART.Abilities.finesse.name',
- path: actor => actor.system.attributes.finesse.data.value
+ path: actor => actor.system.traits.finesse.data.value
},
instinct: {
name: 'DAGGERHEART.Abilities.instinct.name',
- path: actor => actor.system.attributes.instinct.data.value
+ path: actor => actor.system.traits.instinct.data.value
},
presence: {
name: 'DAGGERHEART.Abilities.presence.name',
- path: actor => actor.system.attributes.presence.data.value
+ path: actor => actor.system.traits.presence.data.value
},
knowledge: {
name: 'DAGGERHEART.Abilities.knowledge.name',
- path: actor => actor.system.attributes.knowledge.data.value
+ path: actor => actor.system.traits.knowledge.data.value
},
spellcastingTrait: {
name: 'DAGGERHEART.FeatureProperty.SpellcastingTrait',
- path: actor => actor.system.attributes[actor.system.subclass.system.spellcastingTrait].data.value
+ path: actor => actor.system.traits[actor.system.subclass.system.spellcastingTrait].data.value
}
};
diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs
index 26de2a48..3a33e61b 100644
--- a/module/config/settingsConfig.mjs
+++ b/module/config/settingsConfig.mjs
@@ -10,6 +10,10 @@ export const menu = {
Range: {
Name: 'GameSettingsRange',
Icon: 'fa-solid fa-ruler'
+ },
+ VariantRules: {
+ Name: 'GameSettingsVariantrules',
+ Icon: 'fa-solid fa-scale-balanced'
}
};
@@ -27,5 +31,19 @@ export const gameSettings = {
AbilityArray: 'AbilityArray',
RangeMeasurement: 'RangeMeasurement'
},
- appearance: 'Appearance'
+ DualityRollColor: 'DualityRollColor',
+ LevelTiers: 'LevelTiers',
+ appearance: 'Appearance',
+ variantRules: 'VariantRules'
+};
+
+export const DualityRollColor = {
+ colorful: {
+ value: 0,
+ label: 'DAGGERHEART.Settings.DualityRollColor.Options.Colorful'
+ },
+ normal: {
+ value: 1,
+ label: 'DAGGERHEART.Settings.DualityRollColor.Options.Normal'
+ }
};
diff --git a/module/data/_module.mjs b/module/data/_module.mjs
index 198f6547..b70b5a23 100644
--- a/module/data/_module.mjs
+++ b/module/data/_module.mjs
@@ -1,8 +1,10 @@
export { default as DhpPC } from './pc.mjs';
-export { default as DhpCombat } from './combat.mjs';
-export { default as DhpCombatant } from './combatant.mjs';
+export { default as DhClass } from './item/class.mjs';
+export { default as DhSubclass } from './item/subclass.mjs';
+export { default as DhCombat } from './combat.mjs';
+export { default as DhCombatant } from './combatant.mjs';
export { default as DhpAdversary } from './adversary.mjs';
export { default as DhpEnvironment } from './environment.mjs';
-export * as items from "./item/_module.mjs";
-export * as messages from "./chat-message/_modules.mjs";
\ No newline at end of file
+export * as items from './item/_module.mjs';
+export * as messages from './chat-message/_modules.mjs';
diff --git a/module/data/combat.mjs b/module/data/combat.mjs
index 3ad52b8b..e0490286 100644
--- a/module/data/combat.mjs
+++ b/module/data/combat.mjs
@@ -1,9 +1,6 @@
-export default class DhpCombat extends foundry.abstract.TypeDataModel {
+export default class DhCombat extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
- return {
- actions: new fields.NumberField({ initial: 0, integer: true }),
- activeCombatant: new fields.StringField({})
- };
+ return {};
}
}
diff --git a/module/data/combatant.mjs b/module/data/combatant.mjs
index 60c32db6..cae5d08f 100644
--- a/module/data/combatant.mjs
+++ b/module/data/combatant.mjs
@@ -1,8 +1,11 @@
-export default class DhpCombatant extends foundry.abstract.TypeDataModel {
+export default class DhCombatant extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
- active: new fields.BooleanField({ initial: false })
+ spotlight: new fields.SchemaField({
+ requesting: new fields.BooleanField({ required: true, initial: false })
+ }),
+ actionTokens: new fields.NumberField({ required: true, integer: true, initial: 3 })
};
}
}
diff --git a/module/data/levelTier.mjs b/module/data/levelTier.mjs
new file mode 100644
index 00000000..6cf11252
--- /dev/null
+++ b/module/data/levelTier.mjs
@@ -0,0 +1,340 @@
+export class DhLevelTiers extends foundry.abstract.DataModel {
+ static defineSchema() {
+ const fields = foundry.data.fields;
+
+ return {
+ tiers: new fields.TypedObjectField(new fields.EmbeddedDataField(DhLevelTier))
+ };
+ }
+
+ get availableChoicesPerLevel() {
+ return Object.values(this.tiers).reduce((acc, tier) => {
+ for (var level = tier.levels.start; level < tier.levels.end + 1; level++) {
+ acc[level] = tier.availableOptions;
+ }
+
+ return acc;
+ }, {});
+ }
+}
+
+class DhLevelTier extends foundry.abstract.DataModel {
+ static defineSchema() {
+ const fields = foundry.data.fields;
+
+ return {
+ tier: new fields.NumberField({ required: true, integer: true }),
+ name: new fields.StringField({ required: true }),
+ levels: new fields.SchemaField({
+ start: new fields.NumberField({ required: true, integer: true }),
+ end: new fields.NumberField({ required: true, integer: true })
+ }),
+ initialAchievements: new fields.SchemaField({
+ experience: new fields.SchemaField({
+ nr: new fields.NumberField({ required: true, initial: 1 }),
+ modifier: new fields.NumberField({ required: true, initial: 2 })
+ }),
+ proficiency: new fields.NumberField({ integer: true, initial: 1 })
+ }),
+ availableOptions: new fields.NumberField({ required: true, initial: 2 }),
+ domainCardByLevel: new fields.NumberField({ initial: 1 }),
+ options: new fields.TypedObjectField(new fields.EmbeddedDataField(DhLevelOption))
+ };
+ }
+}
+
+class DhLevelOption extends foundry.abstract.DataModel {
+ static defineSchema() {
+ const fields = foundry.data.fields;
+
+ return {
+ label: new fields.StringField({ required: true }),
+ checkboxSelections: new fields.NumberField({ required: true, integer: true, initial: 1 }),
+ minCost: new fields.NumberField({ required: true, integer: true, initial: 1 }),
+ type: new fields.StringField({ required: true, choices: LevelOptionType }),
+ value: new fields.NumberField({ integer: true }),
+ amount: new fields.NumberField({ integer: true })
+ };
+ }
+}
+
+export const LevelOptionType = {
+ trait: {
+ id: 'trait',
+ label: 'Character Trait',
+ dataPath: ''
+ },
+ hitPoint: {
+ id: 'hitPoint',
+ label: 'Hit Points',
+ dataPath: 'resources.hitPoints',
+ dataPathData: {
+ property: 'max',
+ dependencies: ['value']
+ }
+ },
+ stress: {
+ id: 'stress',
+ label: 'Stress',
+ dataPath: 'resources.stress',
+ dataPathData: {
+ property: 'max',
+ dependencies: ['value']
+ }
+ },
+ evasion: {
+ id: 'evasion',
+ label: 'Evasion',
+ dataPath: 'evasion'
+ },
+ proficiency: {
+ id: 'proficiency',
+ label: 'Proficiency'
+ },
+ experience: {
+ id: 'experience',
+ label: 'Experience'
+ },
+ domainCard: {
+ id: 'domainCard',
+ label: 'Domain Card'
+ },
+ subclass: {
+ id: 'subclass',
+ label: 'Subclass'
+ },
+ multiclass: {
+ id: 'multiclass',
+ label: 'Multiclass'
+ }
+};
+
+export const defaultLevelTiers = {
+ tiers: {
+ 2: {
+ tier: 2,
+ name: 'Tier 2',
+ levels: {
+ start: 2,
+ end: 4
+ },
+ initialAchievements: {
+ experience: {
+ nr: 1,
+ modifier: 2
+ },
+ proficiency: 1
+ },
+ availableOptions: 2,
+ domainCardByLevel: 1,
+ options: {
+ trait: {
+ label: 'DAGGERHEART.LevelUp.Options.trait',
+ checkboxSelections: 3,
+ minCost: 1,
+ type: LevelOptionType.trait.id,
+ amount: 2
+ },
+ hitPoint: {
+ label: 'DAGGERHEART.LevelUp.Options.hitPoint',
+ checkboxSelections: 2,
+ minCost: 1,
+ type: LevelOptionType.hitPoint.id,
+ value: 1,
+ value: 1
+ },
+ stress: {
+ label: 'DAGGERHEART.LevelUp.Options.stress',
+ checkboxSelections: 2,
+ minCost: 1,
+ type: LevelOptionType.stress.id,
+ value: 1
+ },
+ experience: {
+ label: 'DAGGERHEART.LevelUp.Options.experience',
+ checkboxSelections: 1,
+ minCost: 1,
+ type: LevelOptionType.experience.id,
+ value: 1,
+ amount: 2
+ },
+ domainCard: {
+ label: 'DAGGERHEART.LevelUp.Options.domainCard',
+ checkboxSelections: 1,
+ minCost: 1,
+ type: LevelOptionType.domainCard.id,
+ amount: 1
+ },
+ evasion: {
+ label: 'DAGGERHEART.LevelUp.Options.evasion',
+ checkboxSelections: 1,
+ minCost: 1,
+ type: LevelOptionType.evasion.id,
+ value: 1
+ }
+ }
+ },
+ 3: {
+ tier: 3,
+ name: 'Tier 3',
+ levels: {
+ start: 5,
+ end: 7
+ },
+ initialAchievements: {
+ experience: {
+ nr: 1,
+ modifier: 2
+ },
+ proficiency: 1
+ },
+ availableOptions: 2,
+ domainCardByLevel: 1,
+ options: {
+ trait: {
+ label: 'DAGGERHEART.LevelUp.Options.trait',
+ checkboxSelections: 3,
+ minCost: 1,
+ type: LevelOptionType.trait.id,
+ amount: 2
+ },
+ hitPoint: {
+ label: 'DAGGERHEART.LevelUp.Options.hitPoint',
+ checkboxSelections: 2,
+ minCost: 1,
+ type: LevelOptionType.hitPoint.id,
+ value: 1
+ },
+ stress: {
+ label: 'DAGGERHEART.LevelUp.Options.stress',
+ checkboxSelections: 2,
+ minCost: 1,
+ type: LevelOptionType.stress.id,
+ value: 1
+ },
+ experience: {
+ label: 'DAGGERHEART.LevelUp.Options.experience',
+ checkboxSelections: 1,
+ minCost: 1,
+ type: LevelOptionType.experience.id,
+ value: 1,
+ amount: 2
+ },
+ domainCard: {
+ label: 'DAGGERHEART.LevelUp.Options.domainCard',
+ checkboxSelections: 1,
+ minCost: 1,
+ type: LevelOptionType.domainCard.id,
+ amount: 1
+ },
+ evasion: {
+ label: 'DAGGERHEART.LevelUp.Options.evasion',
+ checkboxSelections: 1,
+ minCost: 1,
+ type: LevelOptionType.evasion.id,
+ value: 1
+ },
+ subclass: {
+ label: 'DAGGERHEART.LevelUp.Options.subclass',
+ checkboxSelections: 1,
+ minCost: 1,
+ type: LevelOptionType.subclass.id
+ },
+ proficiency: {
+ label: 'DAGGERHEART.LevelUp.Options.proficiency',
+ checkboxSelections: 2,
+ minCost: 2,
+ type: LevelOptionType.proficiency.id,
+ value: 1
+ },
+ multiclass: {
+ label: 'DAGGERHEART.LevelUp.Options.multiclass',
+ checkboxSelections: 2,
+ minCost: 2,
+ type: LevelOptionType.multiclass.id
+ }
+ }
+ },
+ 4: {
+ tier: 4,
+ name: 'Tier 4',
+ levels: {
+ start: 8,
+ end: 10
+ },
+ initialAchievements: {
+ experience: {
+ nr: 1,
+ modifier: 2
+ },
+ proficiency: 1
+ },
+ availableOptions: 2,
+ domainCardByLevel: 1,
+ options: {
+ trait: {
+ label: 'DAGGERHEART.LevelUp.Options.trait',
+ checkboxSelections: 3,
+ minCost: 1,
+ type: LevelOptionType.trait.id,
+ amount: 2
+ },
+ hitPoint: {
+ label: 'DAGGERHEART.LevelUp.Options.hitPoint',
+ checkboxSelections: 2,
+ minCost: 1,
+ type: LevelOptionType.hitPoint.id,
+ value: 1
+ },
+ stress: {
+ label: 'DAGGERHEART.LevelUp.Options.stress',
+ checkboxSelections: 2,
+ minCost: 1,
+ type: LevelOptionType.stress.id,
+ value: 1
+ },
+ experience: {
+ label: 'DAGGERHEART.LevelUp.Options.experience',
+ checkboxSelections: 1,
+ minCost: 1,
+ type: LevelOptionType.experience.id,
+ value: 1,
+ amount: 2
+ },
+ domainCard: {
+ label: 'DAGGERHEART.LevelUp.Options.domainCard',
+ checkboxSelections: 1,
+ minCost: 1,
+ type: LevelOptionType.domainCard.id,
+ amount: 1
+ },
+ evasion: {
+ label: 'DAGGERHEART.LevelUp.Options.evasion',
+ checkboxSelections: 1,
+ minCost: 1,
+ type: LevelOptionType.evasion.id,
+ value: 1
+ },
+ subclass: {
+ label: 'DAGGERHEART.LevelUp.Options.subclass',
+ checkboxSelections: 1,
+ minCost: 1,
+ type: LevelOptionType.subclass.id
+ },
+ proficiency: {
+ label: 'DAGGERHEART.LevelUp.Options.proficiency',
+ checkboxSelections: 2,
+ minCost: 2,
+ type: LevelOptionType.proficiency.id,
+ value: 1
+ },
+ multiclass: {
+ label: 'DAGGERHEART.LevelUp.Options.multiclass',
+ checkboxSelections: 2,
+ minCost: 2,
+ type: LevelOptionType.multiclass.id
+ }
+ }
+ }
+ }
+};
diff --git a/module/data/levelup.mjs b/module/data/levelup.mjs
new file mode 100644
index 00000000..0f9204e0
--- /dev/null
+++ b/module/data/levelup.mjs
@@ -0,0 +1,311 @@
+import { chunkify } from '../helpers/utils.mjs';
+import { LevelOptionType } from './levelTier.mjs';
+
+export class DhLevelup extends foundry.abstract.DataModel {
+ static initializeData(levelTierData, pcLevelData) {
+ const startLevel = pcLevelData.level.current + 1;
+ const currentLevel = pcLevelData.level.current + 1;
+ const endLevel = pcLevelData.level.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 domainCards = [...Array(tier.domainCardByLevel).keys()].reduce((acc, _) => {
+ const id = foundry.utils.randomID();
+ acc[id] = { uuid: null, itemUuid: null, level: i };
+ return acc;
+ }, {});
+
+ levels[i] = DhLevelupLevel.initializeData(pcLevelData.levelups[i], tier.availableOptions, {
+ ...initialAchievements,
+ experiences,
+ domainCards
+ });
+ }
+
+ belongingLevels.push(i);
+ }
+
+ tiers[key] = {
+ name: tier.name,
+ belongingLevels: belongingLevels,
+ options: Object.keys(tier.options).reduce((acc, key) => {
+ acc[key] = tier.options[key].toObject();
+ return acc;
+ }, {})
+ };
+ });
+
+ return {
+ tiers,
+ levels,
+ startLevel,
+ currentLevel,
+ 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':
+ return checkbox.amount ? checkbox.data.length === checkbox.amount : checkbox.data.length === 1;
+ case 'multiclass':
+ const classSelected = checkbox.data.length === 1;
+ const domainSelected = checkbox.secondaryData;
+ return classSelected && domainSelected;
+ 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 classUpgradeChoices() {
+ let subclass = null;
+ 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 ?? null,
+ tier: checkbox.tier,
+ level: levelKey
+ };
+ }
+ if (checkbox.type === 'subclass') {
+ subclass = {
+ tier: checkbox.tier,
+ level: levelKey
+ };
+ }
+ });
+ });
+ });
+ return { subclass, 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, subclass } = this.classUpgradeChoices;
+ return tierKeys.map(tierKey => {
+ const tier = this.tiers[tierKey];
+ const multiclassInTier = multiclass?.tier === Number(tierKey);
+ const subclassInTier = subclass?.tier === Number(tierKey);
+
+ return {
+ name: 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;
+ });
+ return {
+ label: game.i18n.localize(option.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.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
+ };
+ }
+}
diff --git a/module/data/pc.mjs b/module/data/pc.mjs
index 4905f6e5..7589321a 100644
--- a/module/data/pc.mjs
+++ b/module/data/pc.mjs
@@ -1,50 +1,29 @@
-import { getPathValue, getTier } from '../helpers/utils.mjs';
+import { getPathValue } from '../helpers/utils.mjs';
+import { LevelOptionType } from './levelTier.mjs';
const fields = foundry.data.fields;
const attributeField = () =>
new fields.SchemaField({
- data: new fields.SchemaField({
- value: new fields.NumberField({ initial: 0, integer: true }),
- base: new fields.NumberField({ initial: 0, integer: true }),
- bonus: new fields.NumberField({ initial: 0, integer: true }),
- actualValue: new fields.NumberField({ initial: 0, integer: true }),
- overrideValue: new fields.NumberField({ initial: 0, integer: true })
- }),
- levelMarks: new fields.ArrayField(new fields.NumberField({ nullable: true, initial: null, integer: true })),
- levelMark: new fields.NumberField({ nullable: true, initial: null, integer: true })
+ bonus: new fields.NumberField({ initial: 0, integer: true }),
+ base: new fields.NumberField({ initial: 0, integer: true }),
+ tierMarked: new fields.BooleanField({ required: true, initial: false })
});
-const levelUpTier = () => ({
- attributes: new fields.TypedObjectField(new fields.BooleanField()),
- hitPointSlots: new fields.TypedObjectField(new fields.BooleanField()),
- stressSlots: new fields.TypedObjectField(new fields.BooleanField()),
- experiences: new fields.TypedObjectField(new fields.ArrayField(new fields.StringField({}))),
- proficiency: new fields.TypedObjectField(new fields.BooleanField()),
- armorOrEvasionSlot: new fields.TypedObjectField(new fields.StringField({})),
- subclass: new fields.TypedObjectField(
- new fields.SchemaField({
- multiclass: new fields.BooleanField(),
- feature: new fields.StringField({})
- })
- ),
- multiclass: new fields.TypedObjectField(new fields.BooleanField())
-});
+const resourceField = max =>
+ new fields.SchemaField({
+ value: new fields.NumberField({ initial: 0, integer: true }),
+ bonus: new fields.NumberField({ initial: 0, integer: true }),
+ min: new fields.NumberField({ initial: 0, integer: true }),
+ baseMax: new fields.NumberField({ initial: max, integer: true })
+ });
export default class DhpPC extends foundry.abstract.TypeDataModel {
static defineSchema() {
return {
resources: new fields.SchemaField({
- health: new fields.SchemaField({
- value: new fields.NumberField({ initial: 0, integer: true }),
- min: new fields.NumberField({ initial: 0, integer: true }),
- max: new fields.NumberField({ initial: 6, integer: true })
- }),
- stress: new fields.SchemaField({
- value: new fields.NumberField({ initial: 0, integer: true }),
- min: new fields.NumberField({ initial: 0, integer: true }),
- max: new fields.NumberField({ initial: 6, integer: true })
- }),
+ hitPoints: resourceField(6),
+ stress: resourceField(6),
hope: new fields.SchemaField({
value: new fields.NumberField({ initial: -1, integer: true }), // FIXME. Logic is gte and needs -1 in PC/Hope. Change to 0
min: new fields.NumberField({ initial: 0, integer: true })
@@ -61,7 +40,7 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
})
)
}),
- attributes: new fields.SchemaField({
+ traits: new fields.SchemaField({
agility: attributeField(),
strength: attributeField(),
finesse: attributeField(),
@@ -70,22 +49,22 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
knowledge: attributeField()
}),
proficiency: new fields.SchemaField({
- value: new fields.NumberField({ initial: 1, integer: true }),
- min: new fields.NumberField({ initial: 1, integer: true }),
- max: new fields.NumberField({ initial: 6, integer: true })
+ base: new fields.NumberField({ required: true, initial: 1, integer: true }),
+ bonus: new fields.NumberField({ required: true, initial: 0, integer: true })
+ }),
+ evasion: new fields.SchemaField({
+ bonus: new fields.NumberField({ initial: 0, integer: true })
}),
- evasion: new fields.NumberField({ initial: 0, integer: true }),
experiences: new fields.ArrayField(
new fields.SchemaField({
id: new fields.StringField({ required: true }),
- level: new fields.NumberField({ required: true, integer: true }),
description: new fields.StringField({}),
value: new fields.NumberField({ integer: true, nullable: true, initial: null })
}),
{
initial: [
- { id: foundry.utils.randomID(), level: 1, description: '', value: 2 },
- { id: foundry.utils.randomID(), level: 1, description: '', value: 2 }
+ { id: foundry.utils.randomID(), description: '', value: 2 },
+ { id: foundry.utils.randomID(), description: '', value: 2 }
]
}
),
@@ -100,30 +79,6 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
maxLoadout: new fields.NumberField({ initial: 2, integer: true }),
maxCards: new fields.NumberField({ initial: 2, integer: true })
}),
- levelData: new fields.SchemaField({
- currentLevel: new fields.NumberField({ initial: 1, integer: true }),
- changedLevel: new fields.NumberField({ initial: 1, integer: true }),
- levelups: new fields.TypedObjectField(
- new fields.SchemaField({
- level: new fields.NumberField({ required: true, integer: true }),
- tier1: new fields.SchemaField({
- ...levelUpTier()
- }),
- tier2: new fields.SchemaField(
- {
- ...levelUpTier()
- },
- { nullable: true, initial: null }
- ),
- tier3: new fields.SchemaField(
- {
- ...levelUpTier()
- },
- { nullable: true, initial: null }
- )
- })
- )
- }),
story: new fields.SchemaField({
background: new fields.HTMLField(),
appearance: new fields.HTMLField(),
@@ -140,15 +95,11 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
armorMarks: new fields.SchemaField({
max: new fields.NumberField({ initial: 6, integer: true }),
value: new fields.NumberField({ initial: 0, integer: true })
- })
+ }),
+ levelData: new fields.EmbeddedDataField(DhPCLevelData)
};
}
- get canLevelUp() {
- // return Object.values(this.levels.data).some(x => !x.completed);
- return this.levelData.currentLevel !== this.levelData.changedLevel;
- }
-
get tier() {
return this.#getTier(this.levelData.currentLevel);
}
@@ -281,31 +232,6 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
}
}
- get totalAttributeMarks() {
- return Object.keys(this.levelData.levelups).reduce((nr, level) => {
- const nrAttributeMarks = Object.keys(this.levelData.levelups[level]).reduce((nr, tier) => {
- nr += Object.keys(this.levelData.levelups[level][tier]?.attributes ?? {}).length * 2;
-
- return nr;
- }, 0);
-
- nr.push(...Array(nrAttributeMarks).fill(Number.parseInt(level)));
-
- return nr;
- }, []);
- }
-
- get availableAttributeMarks() {
- const attributeMarks = Object.keys(this.attributes).flatMap(y => this.attributes[y].levelMarks);
- return this.totalAttributeMarks.reduce((acc, attribute) => {
- if (!attributeMarks.findSplice(x => x === attribute)) {
- acc.push(attribute);
- }
-
- return acc;
- }, []);
- }
-
get effects() {
return this.parent.items.reduce((acc, item) => {
const effects = item.system.effectData;
@@ -357,141 +283,37 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
: null;
}
+ prepareBaseData() {
+ this.resources.hitPoints.max = this.resources.hitPoints.baseMax + this.resources.hitPoints.bonus;
+ this.resources.stress.max = this.resources.stress.baseMax + this.resources.stress.bonus;
+ this.evasion.value = (this.class?.system?.evasion ?? 0) + this.evasion.bonus;
+ this.proficiency.value = this.proficiency.base + this.proficiency.bonus;
+
+ for (var attributeKey in this.traits) {
+ const attribute = this.traits[attributeKey];
+ attribute.value = attribute.base + attribute.bonus;
+ }
+ }
+
prepareDerivedData() {
this.resources.hope.max = 6 - this.story.scars.length;
if (this.resources.hope.value >= this.resources.hope.max) {
this.resources.hope.value = Math.max(this.resources.hope.max - 1, 0);
}
- for (var attributeKey in this.attributes) {
- const attribute = this.attributes[attributeKey];
+ const armor = this.armor;
+ this.damageThresholds = {
+ major: armor
+ ? armor.system.baseThresholds.major + this.levelData.level.current
+ : this.levelData.level.current,
+ severe: armor
+ ? armor.system.baseThresholds.severe + this.levelData.level.current
+ : this.levelData.level.current * 2
+ };
- attribute.levelMark = attribute.levelMarks.find(x => this.isSameTier(x)) ?? null;
-
- const actualValue = attribute.data.base + attribute.levelMarks.length + attribute.data.bonus;
- attribute.data.actualValue = actualValue;
- attribute.data.value = attribute.data.overrideValue
- ? attribute.data.overrideValue
- : attribute.data.actualValue;
- }
-
- this.evasion = this.class?.system?.evasion ?? 0;
- // this.armor.value = this.activeArmor?.baseScore ?? 0;
- this.damageThresholds = this.computeDamageThresholds();
-
- this.applyLevels();
this.applyEffects();
}
- computeDamageThresholds() {
- // TODO: missing weapon features and domain cards calculation
- if (!this.armor) {
- return {
- major: this.levelData.currentLevel,
- severe: this.levelData.currentLevel * 2
- };
- }
- const {
- baseThresholds: { major = 0, severe = 0 }
- } = this.armor.system;
- return {
- major: major + this.levelData.currentLevel,
- severe: severe + this.levelData.currentLevel
- };
- }
-
- applyLevels() {
- let healthBonus = 0,
- stressBonus = 0,
- proficiencyBonus = 0,
- evasionBonus = 0,
- armorBonus = 0;
- let experienceBonuses = {};
- let advancementFirst = null,
- advancementSecond = null;
- for (var level in this.levelData.levelups) {
- var levelData = this.levelData.levelups[level];
- for (var tier in levelData) {
- var tierData = levelData[tier];
- if (tierData) {
- healthBonus += Object.keys(tierData.hitPointSlots).length;
- stressBonus += Object.keys(tierData.stressSlots).length;
- proficiencyBonus += Object.keys(tierData.proficiency).length;
- advancementFirst =
- Object.keys(tierData.subclass).length > 0 && level >= 5 && level <= 7
- ? { ...tierData.subclass[0], tier: getTier(Number.parseInt(level), true) }
- : advancementFirst;
- advancementSecond =
- Object.keys(tierData.subclass).length > 0 && level >= 8 && level <= 10
- ? { ...tierData.subclass[0], tier: getTier(Number.parseInt(level), true) }
- : advancementSecond;
-
- for (var index in Object.keys(tierData.experiences)) {
- for (var experienceKey in tierData.experiences[index]) {
- var experience = tierData.experiences[index][experienceKey];
- experienceBonuses[experience] = experienceBonuses[experience]
- ? experienceBonuses[experience] + 1
- : 1;
- }
- }
-
- evasionBonus += Object.keys(tierData.armorOrEvasionSlot).filter(
- x => tierData.armorOrEvasionSlot[x] === 'evasion'
- ).length;
- armorBonus += Object.keys(tierData.armorOrEvasionSlot).filter(
- x => tierData.armorOrEvasionSlot[x] === 'armor'
- ).length;
- }
- }
- }
-
- this.resources.health.max += healthBonus;
- this.resources.stress.max += stressBonus;
- this.proficiency.value += proficiencyBonus;
- this.evasion += evasionBonus;
- this.armorMarks = {
- max: this.armor ? this.armor.system.marks.max + armorBonus : 0,
- value: this.armor ? this.armor.system.marks.value : 0
- };
-
- this.experiences = this.experiences.map(x => ({ ...x, value: x.value + (experienceBonuses[x.id] ?? 0) }));
-
- const subclassFeatures = this.subclassFeatures;
- if (advancementFirst) {
- if (advancementFirst.multiclass) {
- this.multiclassSubclass.system[`${advancementFirst.feature}Feature`].unlocked = true;
- this.multiclassSubclass.system[`${advancementFirst.feature}Feature`].tier = advancementFirst.tier;
- subclassFeatures.multiclassSubclass[advancementFirst.feature].forEach(x => (x.system.disabled = false));
- } else {
- this.subclass.system[`${advancementFirst.feature}Feature`].unlocked = true;
- this.subclass.system[`${advancementFirst.feature}Feature`].tier = advancementFirst.tier;
- subclassFeatures.subclass[advancementFirst.feature].forEach(x => (x.system.disabled = false));
- }
- }
- if (advancementSecond) {
- if (advancementSecond.multiclass) {
- this.multiclassSubclass.system[`${advancementSecond.feature}Feature`].unlocked = true;
- this.multiclassSubclass.system[`${advancementSecond.feature}Feature`].tier = advancementSecond.tier;
- subclassFeatures.multiclassSubclass[advancementSecond.feature].forEach(
- x => (x.system.disabled = false)
- );
- } else {
- this.subclass.system[`${advancementSecond.feature}Feature`].unlocked = true;
- this.subclass.system[`${advancementSecond.feature}Feature`].tier = advancementSecond.tier;
- subclassFeatures.subclass[advancementSecond.feature].forEach(x => (x.system.disabled = false));
- }
- }
-
- //General progression
- for (var i = 0; i < this.levelData.currentLevel; i++) {
- const tier = getTier(i + 1);
- if (tier !== 'tier0') {
- this.domainData.maxLoadout = Math.min(this.domainData.maxLoadout + 1, 5);
- this.domainData.maxCards += 1;
- }
- }
- }
-
applyEffects() {
const effects = this.effects;
for (var key in effects) {
@@ -499,10 +321,10 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
for (var effect of effectType) {
switch (key) {
case SYSTEM.EFFECTS.effectTypes.health.id:
- this.resources.health.max += effect.value.valueData.value;
+ this.resources.hitPoints.bonus += effect.value.valueData.value;
break;
case SYSTEM.EFFECTS.effectTypes.stress.id:
- this.resources.stress.max += effect.value.valueData.value;
+ this.resources.stress.bonus += effect.value.valueData.value;
break;
case SYSTEM.EFFECTS.effectTypes.damage.id:
this.bonuses.damage.push({
@@ -529,10 +351,6 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
return twoHanded ? 'twoHanded' : oneHanded ? 'oneHanded' : null;
}
- isSameTier(level) {
- return this.#getTier(this.levelData.currentLevel) === this.#getTier(level);
- }
-
#getTier(level) {
if (level >= 8) return 3;
else if (level >= 5) return 2;
@@ -540,3 +358,55 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
else return 0;
}
}
+
+class DhPCLevelData extends foundry.abstract.DataModel {
+ static defineSchema() {
+ return {
+ level: new fields.SchemaField({
+ current: new fields.NumberField({ required: true, integer: true, initial: 1 }),
+ changed: new fields.NumberField({ required: true, integer: true, initial: 1 })
+ }),
+ levelups: new fields.TypedObjectField(
+ new fields.SchemaField({
+ 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.ArrayField(
+ new fields.SchemaField({
+ uuid: new fields.StringField({ required: true }),
+ itemUuid: new fields.StringField({ required: true })
+ })
+ ),
+ proficiency: new fields.NumberField({ integer: true })
+ },
+ { nullable: true, initial: null }
+ ),
+ selections: new fields.ArrayField(
+ new fields.SchemaField({
+ tier: new fields.NumberField({ required: true, integer: true }),
+ level: new fields.NumberField({ required: true, integer: true }),
+ optionKey: new fields.StringField({ required: true }),
+ type: new fields.StringField({ required: true, choices: LevelOptionType }),
+ checkboxNr: new fields.NumberField({ required: true, integer: true }),
+ value: new fields.NumberField({ integer: true }),
+ minCost: new fields.NumberField({ integer: true }),
+ amount: new fields.NumberField({ integer: true }),
+ data: new fields.ArrayField(new fields.StringField({ required: true })),
+ secondaryData: new fields.StringField(),
+ itemUuid: new fields.StringField({ required: true })
+ })
+ )
+ })
+ )
+ };
+ }
+
+ get canLevelUp() {
+ return this.level.current < this.level.changed;
+ }
+}
diff --git a/module/data/settings/VariantRules.mjs b/module/data/settings/VariantRules.mjs
new file mode 100644
index 00000000..2a1f948d
--- /dev/null
+++ b/module/data/settings/VariantRules.mjs
@@ -0,0 +1,13 @@
+export default class DhVariantRules extends foundry.abstract.DataModel {
+ static defineSchema() {
+ const fields = foundry.data.fields;
+ return {
+ actionTokens: new fields.SchemaField({
+ enabled: new fields.BooleanField({ required: true, initial: false }),
+ tokens: new fields.NumberField({ required: true, integer: true, initial: 3 })
+ })
+ };
+ }
+
+ static defaultSchema = {};
+}
diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs
index 3d38c485..bc116550 100644
--- a/module/documents/actor.mjs
+++ b/module/documents/actor.mjs
@@ -6,13 +6,16 @@ import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
export default class DhpActor extends Actor {
async _preCreate(data, options, user) {
- if ( (await super._preCreate(data, options, user)) === false ) return false;
-
+ if ((await super._preCreate(data, options, user)) === false) return false;
+
// Configure prototype token settings
const prototypeToken = {};
- if ( this.type === "pc" ) Object.assign(prototypeToken, {
- sight: { enabled: true }, actorLink: true, disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY
- });
+ if (this.type === 'pc')
+ Object.assign(prototypeToken, {
+ sight: { enabled: true },
+ actorLink: true,
+ disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY
+ });
this.updateSource({ prototypeToken });
}
@@ -21,46 +24,103 @@ export default class DhpActor extends Actor {
}
async _preUpdate(changed, options, user) {
- //Level Down
- if (
- changed.system?.levelData?.changedLevel &&
- this.system.levelData.currentLevel > changed.system.levelData.changedLevel
- ) {
- changed.system.levelData.currentLevel = changed.system.levelData.changedLevel;
- changed.system.levelData.levelups = Object.keys(this.system.levelData.levelups).reduce((acc, x) => {
- if (x > changed.system.levelData.currentLevel) {
- acc[`-=${x}`] = null;
+ super._preUpdate(changed, options, user);
+ }
+
+ async updateLevel(newLevel) {
+ if (this.type !== 'pc' || newLevel === this.system.levelData.level.changed) return;
+
+ if (newLevel > this.system.levelData.level.current) {
+ await this.update({ 'system.levelData.level.changed': newLevel });
+ } else {
+ const updatedLevelups = Object.keys(this.system.levelData.levelups).reduce((acc, level) => {
+ if (Number(level) > newLevel) acc[`-=${level}`] = null;
+
+ return acc;
+ }, {});
+
+ const domainCards = Object.keys(this.system.levelData.levelups)
+ .filter(x => x > newLevel)
+ .flatMap(levelKey => {
+ const level = this.system.levelData.levelups[levelKey];
+ const achievementCards = level.achievements.domainCards.map(x => x.itemUuid);
+ const advancementCards = level.selections.filter(x => x.type === 'domainCard').map(x => x.itemUuid);
+ return [...achievementCards, ...advancementCards];
+ });
+
+ for (var domainCard of domainCards) {
+ const itemCard = await this.items.find(x => x.uuid === domainCard);
+ itemCard.delete();
+ }
+
+ await this.update({
+ system: {
+ levelData: {
+ level: {
+ current: newLevel,
+ changed: newLevel
+ },
+ levelups: updatedLevelups
+ }
}
+ });
+ }
+ }
- return acc;
- }, {});
+ async levelUp(levelupData) {
+ const levelups = {};
+ for (var levelKey of Object.keys(levelupData)) {
+ const level = levelupData[levelKey];
+ const achievementDomainCards = [];
+ for (var card of Object.values(level.achievements.domainCards)) {
+ const item = await foundry.utils.fromUuid(card.uuid);
+ const embeddedItem = await this.createEmbeddedDocuments('Item', [item.toObject()]);
+ card.itemUuid = embeddedItem[0].uuid;
+ achievementDomainCards.push(card);
+ }
- changed.system.attributes = Object.keys(this.system.attributes).reduce((acc, key) => {
- acc[key] = {
- levelMarks: this.system.attributes[key].levelMarks.filter(
- x => x <= changed.system.levelData.currentLevel
- )
- };
+ const selections = [];
+ for (var optionKey of Object.keys(level.choices)) {
+ const selection = level.choices[optionKey];
+ for (var checkboxNr of Object.keys(selection)) {
+ const checkbox = selection[checkboxNr];
+ let itemUuid = null;
- return acc;
- }, {});
+ if (checkbox.type === 'domainCard') {
+ const item = await foundry.utils.fromUuid(checkbox.data[0]);
+ const embeddedItem = await this.createEmbeddedDocuments('Item', [item.toObject()]);
+ itemUuid = embeddedItem[0].uuid;
+ }
- changed.system.experiences = this.system.experiences.filter(
- x => x.level <= changed.system.levelData.currentLevel
- );
-
- if (
- this.system.multiclass &&
- this.system.multiclass.system.multiclass > changed.system.levelData.changedLevel
- ) {
- const multiclassFeatures = this.items.filter(x => x.system.multiclass);
- for (var feature of multiclassFeatures) {
- await feature.delete();
+ selections.push({
+ ...checkbox,
+ level: Number(levelKey),
+ optionKey: optionKey,
+ checkboxNr: Number(checkboxNr),
+ itemUuid
+ });
}
}
+
+ levelups[levelKey] = {
+ achievements: {
+ ...level.achievements,
+ domainCards: achievementDomainCards
+ },
+ selections: selections
+ };
}
- super._preUpdate(changed, options, user);
+ await this.update({
+ system: {
+ levelData: {
+ level: {
+ current: this.system.levelData.level.changed
+ },
+ levelups: levelups
+ }
+ }
+ });
}
async diceRoll(modifier, shiftKey) {
@@ -286,9 +346,9 @@ export default class DhpActor extends Actor {
: 0;
const update = {
- 'system.resources.health.value': Math.min(
- this.system.resources.health.value + hpDamage,
- this.system.resources.health.max
+ 'system.resources.hitPoints.value': Math.min(
+ this.system.resources.hitPoints.value + hpDamage,
+ this.system.resources.hitPoints.max
)
};
@@ -311,9 +371,9 @@ export default class DhpActor extends Actor {
switch (type) {
case SYSTEM.GENERAL.healingTypes.health.id:
update = {
- 'system.resources.health.value': Math.min(
- this.system.resources.health.value + healing,
- this.system.resources.health.max
+ 'system.resources.hitPoints.value': Math.min(
+ this.system.resources.hitPoints.value + healing,
+ this.system.resources.hitPoints.max
)
};
break;
diff --git a/module/documents/combat.mjs b/module/documents/combat.mjs
index c7905605..3ad3189e 100644
--- a/module/documents/combat.mjs
+++ b/module/documents/combat.mjs
@@ -1,44 +1,19 @@
-import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs';
-
export default class DhpCombat extends Combat {
- _sortCombatants(a, b) {
- if (a.isNPC !== b.isNPC) {
- const aVal = a.isNPC ? 0 : 1;
- const bVal = b.isNPC ? 0 : 1;
+ async startCombat() {
+ this._playCombatSound('startEncounter');
+ const updateData = { round: 1, turn: null };
+ Hooks.callAll('combatStart', this, updateData);
+ await this.update(updateData);
+ return this;
+ }
- return aVal - bVal;
+ _sortCombatants(a, b) {
+ const aNPC = Number(a.isNPC);
+ const bNPC = Number(b.isNPC);
+ if (aNPC !== bNPC) {
+ return aNPC - bNPC;
}
return a.name.localeCompare(b.name);
}
-
- async useActionToken(combatantId) {
- const automateActionPoints = await game.settings.get(
- SYSTEM.id,
- SYSTEM.SETTINGS.gameSettings.Automation.ActionPoints
- );
-
- if (game.user.isGM) {
- if (this.system.actions < 1) return;
-
- const update = automateActionPoints
- ? { 'system.activeCombatant': combatantId, 'system.actions': Math.max(this.system.actions - 1, 0) }
- : { 'system.activeCombatant': combatantId };
-
- await this.update(update);
- } else {
- const update = automateActionPoints
- ? { 'system.activeCombatant': combatantId, 'system.actions': this.system.actions + 1 }
- : { 'system.activeCombatant': combatantId };
-
- await game.socket.emit(`system.${SYSTEM.id}`, {
- action: socketEvent.GMUpdate,
- data: {
- action: GMUpdateEvent.UpdateDocument,
- uuid: this.uuid,
- update: update
- }
- });
- }
- }
}
diff --git a/module/helpers/handlebarsHelper.mjs b/module/helpers/handlebarsHelper.mjs
index 8a099175..87d1fb7f 100644
--- a/module/helpers/handlebarsHelper.mjs
+++ b/module/helpers/handlebarsHelper.mjs
@@ -3,22 +3,19 @@ import { getWidthOfText } from './utils.mjs';
export default class RegisterHandlebarsHelpers {
static registerHelpers() {
Handlebars.registerHelper({
- looseEq: this.looseEq,
times: this.times,
join: this.join,
add: this.add,
subtract: this.subtract,
objectSelector: this.objectSelector,
includes: this.includes,
- simpleEditor: this.simpleEditor,
- debug: this.debug
+ debug: this.debug,
+ signedNumber: this.signedNumber,
+ switch: this.switch,
+ case: this.case
});
}
- static looseEq(a, b) {
- return a == b;
- }
-
static times(nr, block) {
var accum = '';
for (var i = 0; i < nr; ++i) accum += block.fn(i);
@@ -77,33 +74,25 @@ export default class RegisterHandlebarsHelpers {
return new Handlebars.SafeString(html);
}
- static rangePicker(options) {
- let { name, value, min, max, step } = options.hash;
- name = name || 'range';
- value = value ?? '';
- if (Number.isNaN(value)) value = '';
- const html = `
- ${value}`;
- return new Handlebars.SafeString(html);
- }
-
static includes(list, item) {
return list.includes(item);
}
- static simpleEditor(content, options) {
- const {
- target,
- editable = true,
- button,
- engine = 'tinymce',
- collaborate = false,
- class: cssClass
- } = options.hash;
- const config = { name: target, value: content, button, collaborate, editable, engine };
- const element = foundry.applications.fields.createEditorInput(config);
- if (cssClass) element.querySelector('.editor-content').classList.add(cssClass);
- return new Handlebars.SafeString(element.outerHTML);
+ static signedNumber(number) {
+ return number >= 0 ? `+${number}` : number;
+ }
+
+ static switch(value, options) {
+ this.switch_value = value;
+ this.switch_break = false;
+ return options.fn(this);
+ }
+
+ static case(value, options) {
+ if (value == this.switch_value) {
+ this.switch_break = true;
+ return options.fn(this);
+ }
}
static debug(a) {
diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs
index 753974f5..3fbe89c3 100644
--- a/module/helpers/utils.mjs
+++ b/module/helpers/utils.mjs
@@ -1,4 +1,5 @@
import { getDiceSoNicePresets } from '../config/generalConfig.mjs';
+import Tagify from '@yaireo/tagify';
export const loadCompendiumOptions = async compendiums => {
const compendiumValues = [];
@@ -131,3 +132,93 @@ export const setDiceSoNiceForDualityRoll = (rollResult, advantage, disadvantage)
rollResult.dice[2].options.appearance = diceSoNicePresets.disadvantage;
}
};
+
+export const chunkify = (array, chunkSize, mappingFunc) => {
+ var chunkifiedArray = [];
+ for (let i = 0; i < array.length; i += chunkSize) {
+ const chunk = array.slice(i, i + chunkSize);
+ if (mappingFunc) {
+ chunkifiedArray.push(mappingFunc(chunk));
+ } else {
+ chunkifiedArray.push(chunk);
+ }
+ }
+
+ return chunkifiedArray;
+};
+
+export const tagifyElement = (element, options, onChange, tagifyOptions = {}) => {
+ const { maxTags } = tagifyOptions;
+ const tagifyElement = new Tagify(element, {
+ tagTextProp: 'name',
+ enforceWhitelist: true,
+ whitelist: Object.keys(options).map(key => {
+ const option = options[key];
+ return {
+ value: key,
+ name: game.i18n.localize(option.label),
+ src: option.src
+ };
+ }),
+ maxTags: maxTags,
+ dropdown: {
+ mapValueTo: 'name',
+ searchKeys: ['name'],
+ enabled: 0,
+ maxItems: 20,
+ closeOnSelect: true,
+ highlightFirst: false
+ },
+ templates: {
+ tag(tagData) {
+ return `` : ''}
+