daggerheart/module/data/levelup.mjs
WBHarry 187ee3e1bd
Refactor/84 data models structure (#131)
* - Move all DataModel item files to a new 'items' subfolder for better organization
- Add _module.mjs file to simplify imports
- Update all import paths
- Rename class for use the new acronym DH

* FIX: remove unnecessary import

* FEAT: BaseDataItem class
add TODO comments for future improvements
FIX: Remove effect field on template
FIX: remove unused DhpEffects file

* FEAT: new FormulaField class
FEAT: add getRollData on BaseDataItem Class
FEAT: weapon
FIX: remove inventoryWeapon field on Weapon Data Model

* FEAT: add class prepareBaseData for domains

* FEAT: new ForeignDocumentUUIDField
FIX: Remove unnecessary fields
FEAT: use ForeignDocumentUUIDField in the Item Class DataModel

* FIX: remove wrong option in String Field

* FIX: remove unused import

* FIX: ADD htmlFields description in manifest

* FIX: minor fixes

* REFACTOR: rename folder `data/items` -> `data/item`
REFACTOR: rename folder `data/messages` -> `data/chat-message`.

* FIX: imports
FIX: items sheet new paths
FIX: ItemDataModelMetadata type jsdoc

* FEAT: formatting code
FIX: fix fields used
FEAT: add jsdoc

* 110 - Class Data Model (#111)

* Added PreCreate/Create/Delete logic for Class/Subclass and set it as foreignUUID fields in PC

* Moved methods into TypedModelData

* Simplified Subclass

* Fixed up data model and a basic placeholder template (#117)

* 118 - adversary data model (#119)

* Fixed datamodel and set up basic template in new style

* Added in a temp attack button, because why not

* Restored HitPoints counting up

* 113 - Character Data Model (#114)

* Improved Character datamodel

* Removed additional unneccessary getters

* Preliminary cleanup in the class sheet

* Cleanup of 'pc' references

* Corrected Duality rolling from Character

* Fix to damage roll

* Added a basic BaseDataActor data model

* Gathered exports

* getRollData recursion fix

* Feature/112 items use action datamodel (#127)

* Create new actions classes

* actions types - attack roll

* fixes before merge

* First PR

* Add daggerheart.css to gitignore

* Update ToDo

* Remove console log

* Fixed chat /dr roll

* Remove jQuery

* Fixed so the different chat themes work again

* Fixed duality roll buttons

* Fix to advantage/disadvantage shortcut

* Extand action to other item types

* Roll fixes

* Fixes to adversary rolls

* resources

* Fixed adversary dice

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>

* Feature/116-implementation-of-pseudo-documents (#125)

* FEAT: add baseDataModel logic

* FEAT: new PseudoDocumentsField
FIX: BasePseudoDocument 's getEmbeddedDocument

* FEAT: PseudoDocument class

* FEAT: add TypedPseudoDocument
REFACTOR: PreudoDocument
FIX: Typos Bug

* FIX: CONFIG types

* FEAT: basic PseudoDocumentSheet

* FIX: remove schema
ADD: input of example

---------

Co-authored-by: Joaquin Pereyra <joaquinpereyra98@users.noreply.github.com>
Co-authored-by: WBHarry <williambjrklund@gmail.com>

* Levelup Followup (#126)

* Levelup applies bonuses to character

* Added visualisation of domain card levels

* Fixed domaincard level max for selections in a tier

* A trait can now only be level up once within the same tier

---------

Co-authored-by: Joaquin Pereyra <joaquinpereyra98@users.noreply.github.com>
Co-authored-by: joaquinpereyra98 <24190917+joaquinpereyra98@users.noreply.github.com>
Co-authored-by: Dapoulp <74197441+Dapoulp@users.noreply.github.com>
2025-06-13 14:17:13 +02:00

350 lines
15 KiB
JavaScript

import { abilities } from '../config/actorConfig.mjs';
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.data.length === (checkbox.amount ?? 1);
case 'multiclass':
const classSelected = checkbox.data.length === 1;
const domainSelected = checkbox.secondaryData.domain;
const subclassSelected = checkbox.secondaryData.subclass;
return classSelected && domainSelected && subclassSelected;
default:
return true;
}
});
});
const experiencesSelected = !this.levels[levelKey].achievements.experiences
? true
: Object.values(this.levels[levelKey].achievements.experiences).every(exp => exp.name);
const domainCardsSelected = Object.values(this.levels[levelKey].achievements.domainCards)
.filter(x => x.level <= this.endLevel)
.every(card => card.uuid);
const allAchievementsSelected = experiencesSelected && domainCardsSelected;
return allSelectionsMade && allChoicesMade && allAchievementsSelected;
}
get currentLevelFinished() {
return this.#levelFinished(this.currentLevel);
}
get allLevelsFinished() {
return Object.keys(this.levels)
.filter(level => Number(level) >= this.startLevel)
.every(this.#levelFinished.bind(this));
}
get unmarkedTraits() {
const possibleLevels = Object.values(this.tiers).reduce((acc, tier) => {
if (tier.belongingLevels.includes(this.currentLevel)) acc = tier.belongingLevels;
return acc;
}, []);
return Object.keys(this.levels)
.filter(key => possibleLevels.some(x => x === Number(key)))
.reduce(
(acc, levelKey) => {
const level = this.levels[levelKey];
Object.values(level.choices).forEach(choice =>
Object.values(choice).forEach(checkbox => {
if (
checkbox.type === 'trait' &&
checkbox.data.length > 0 &&
Number(levelKey) !== this.currentLevel
) {
checkbox.data.forEach(data => delete acc[data]);
}
})
);
return acc;
},
{ ...abilities }
);
}
get classUpgradeChoices() {
let subclasses = [];
let multiclass = null;
Object.keys(this.levels).forEach(levelKey => {
const level = this.levels[levelKey];
Object.values(level.choices).forEach(choice => {
Object.values(choice).forEach(checkbox => {
if (checkbox.type === 'multiclass') {
multiclass = {
class: checkbox.data.length > 0 ? checkbox.data[0] : null,
domain: checkbox.secondaryData.domain ?? null,
subclass: checkbox.secondaryData.subclass ?? null,
tier: checkbox.tier,
level: levelKey
};
}
if (checkbox.type === 'subclass') {
subclasses.push({
tier: checkbox.tier,
level: levelKey
});
}
});
});
});
return { subclasses, multiclass };
}
get tiersForRendering() {
const tierKeys = Object.keys(this.tiers);
const selections = Object.keys(this.levels).reduce(
(acc, key) => {
const level = this.levels[key];
Object.keys(level.choices).forEach(optionKey => {
const choice = level.choices[optionKey];
Object.keys(choice).forEach(checkboxNr => {
const checkbox = choice[checkboxNr];
if (!acc[checkbox.tier][optionKey]) acc[checkbox.tier][optionKey] = {};
Object.keys(choice).forEach(checkboxNr => {
acc[checkbox.tier][optionKey][checkboxNr] = { ...checkbox, level: Number(key) };
});
});
});
return acc;
},
tierKeys.reduce((acc, key) => {
acc[key] = {};
return acc;
}, {})
);
const { multiclass, subclasses } = this.classUpgradeChoices;
return tierKeys.map((tierKey, tierIndex) => {
const tier = this.tiers[tierKey];
const multiclassInTier = multiclass?.tier === Number(tierKey);
const subclassInTier = subclasses.some(x => x.tier === Number(tierKey));
return {
name: tier.name,
active: this.currentLevel >= Math.min(...tier.belongingLevels),
groups: Object.keys(tier.options).map(optionKey => {
const option = tier.options[optionKey];
const checkboxes = [...Array(option.checkboxSelections).keys()].flatMap(index => {
const checkboxNr = index + 1;
const checkboxData = selections[tierKey]?.[optionKey]?.[checkboxNr];
const checkbox = { ...option, checkboxNr, tier: tierKey };
if (checkboxData) {
checkbox.level = checkboxData.level;
checkbox.selected = true;
checkbox.disabled = checkbox.level !== this.currentLevel;
}
if (optionKey === 'multiclass') {
if ((multiclass && !multiclassInTier) || subclassInTier) {
checkbox.disabled = true;
}
}
if (optionKey === 'subclass' && multiclassInTier) {
checkbox.disabled = true;
}
return checkbox;
});
let label = game.i18n.localize(option.label);
if (optionKey === 'domainCard') {
const maxLevel = tier.belongingLevels[tier.belongingLevels.length - 1];
label = game.i18n.format(option.label, { maxLevel });
}
return {
label: label,
checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => {
const anySelected = chunkedBoxes.some(x => x.selected);
const anyDisabled = chunkedBoxes.some(x => x.disabled);
return {
multi: option.minCost > 1,
checkboxes: chunkedBoxes.map(x => ({
...x,
selected: anySelected,
disabled: anyDisabled
}))
};
})
};
})
};
});
}
}
export class DhLevelupLevel extends foundry.abstract.DataModel {
static initializeData(levelData = { selections: [] }, maxSelections, achievements) {
return {
maxSelections: maxSelections,
achievements: {
experiences: levelData.achievements?.experiences ?? achievements.experiences ?? {},
domainCards: levelData.achievements?.domainCards
? levelData.achievements.domainCards.reduce((acc, card, index) => {
acc[index] = { ...card };
return acc;
}, {})
: (achievements.domainCards ?? {}),
proficiency: levelData.achievements?.proficiency ?? achievements.proficiency ?? null
},
choices: levelData.selections.reduce((acc, data) => {
if (!acc[data.optionKey]) acc[data.optionKey] = {};
acc[data.optionKey][data.checkboxNr] = { ...data };
return acc;
}, {})
};
}
static defineSchema() {
const fields = foundry.data.fields;
return {
maxSelections: new fields.NumberField({ required: true, integer: true }),
achievements: new fields.SchemaField({
experiences: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({ required: true }),
modifier: new fields.NumberField({ required: true, integer: true })
})
),
domainCards: new fields.TypedObjectField(
new fields.SchemaField({
uuid: new fields.StringField({ required: true, nullable: true, initial: null }),
itemUuid: new fields.StringField({ required: true }),
level: new fields.NumberField({ required: true, integer: true })
})
),
proficiency: new fields.NumberField({ integer: true })
}),
choices: new fields.TypedObjectField(
new fields.TypedObjectField(
new fields.SchemaField({
tier: new fields.NumberField({ required: true, integer: true }),
minCost: new fields.NumberField({ required: true, integer: true }),
amount: new fields.NumberField({ integer: true }),
value: new fields.StringField(),
data: new fields.ArrayField(new fields.StringField()),
secondaryData: new fields.TypedObjectField(new fields.StringField()),
type: new fields.StringField({ required: true })
})
)
)
};
}
get nrSelections() {
const selections = Object.keys(this.choices).reduce((acc, choiceKey) => {
const choice = this.choices[choiceKey];
acc += Object.values(choice).reduce((acc, x) => acc + x.minCost, 0);
return acc;
}, 0);
return {
selections: selections,
available: this.maxSelections - selections
};
}
}