Added DomainCard selection

This commit is contained in:
WBHarry 2025-05-30 14:16:15 +02:00
parent 07c533c82c
commit 66defbffce
18 changed files with 823 additions and 132 deletions

View file

@ -11,13 +11,16 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const playerLevelupData = actor.system.levelData;
this.levelup = new DhLevelup(DhLevelup.initializeData(this.levelTiers, playerLevelupData, actor.system.level));
this._dragDrop = this._createDragDropHandlers();
}
get title() {
return `${this.actor.name} - Level Up`;
return game.i18n.format('DAGGERHEART.Application.LevelUp.Title', { actor: this.actor.name });
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'levelup'],
position: { width: 1000, height: 'auto' },
window: {
@ -25,31 +28,188 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
},
actions: {
save: this.save
}
},
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/sheets/global/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) {
const context = await super._prepareContext(_options);
context.levelup = this.levelup;
context.tabs = this._getTabs(this.constructor.TABS);
return context;
}
async _preparePartContext(partId, context) {
switch (partId) {
case 'selections':
context.advancementChoices = this.levelup.selectionData.reduce((acc, data) => {
const advancementChoice = {
...data,
path: `tiers.${data.tier}.levels.${data.level}.optionSelections.${data.optionKey}.${data.checkboxNr}.data`
};
if (acc[data.type]) acc[data.type].push(advancementChoice);
else acc[data.type] = [advancementChoice];
return acc;
}, {});
context.newExperiences = this.levelup.allInitialAchievements.newExperiences;
const allDomainCards = {
...context.advancementChoices.domainCard,
...this.levelup.domainCards
};
const allDomainCardValues = Object.values(allDomainCards);
context.domainCards = [];
for (var domainCard of allDomainCardValues) {
const uuid = domainCard.data ?? domainCard.uuid;
const card = uuid ? await foundry.utils.fromUuid(uuid) : { path: domainCard.path };
context.domainCards.push({
...(card.toObject?.() ?? card),
emptySubtext: game.i18n.format(
'DAGGERHEART.Application.LevelUp.Selections.emptyDomainCardHint',
{ level: domainCard.level }
),
limit: domainCard.level
});
}
break;
case 'summary':
const actorArmor = this.actor.system.armor;
const { current: currentLevel, changed: changedLevel } = this.actor.system.levelData.level;
context.levelAchievements = {
statisticIncreases: {
proficiency: {
old: this.actor.system.proficiency,
new: this.actor.system.proficiency + this.levelup.allInitialAchievements.proficiency
},
damageThresholds: {
major: {
old: this.actor.system.damageThresholds.major,
new: this.actor.system.damageThresholds.major + changedLevel - currentLevel
},
severe: {
old: this.actor.system.damageThresholds.severe,
new:
this.actor.system.damageThresholds.severe +
(actorArmor ? changedLevel - currentLevel : (changedLevel - currentLevel) * 2)
},
unarmored: !actorArmor
}
}
};
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).find('.selection-checkbox').on('change', this.selectionClick.bind(this));
htmlElement
.querySelectorAll('.selection-checkbox')
.forEach(element => element.addEventListener('change', this.selectionClick.bind(this)));
this._dragDrop.forEach(d => d.bind(htmlElement));
}
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.currentTarget.parentElement?.classList?.contains('domain-cards')) {
if (item.type === 'domainCard') {
if (!this.actor.system.class.system.domains.includes(item.system.domain)) {
// Also needs to check for multiclass adding a new domain
ui.notifications.error(
game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.domainCardWrongDomain')
);
return;
}
if (item.system.level > Number(event.currentTarget.dataset.limit)) {
ui.notifications.error(
game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.domainCardToHighLevel')
);
return;
}
const achievementCard = event.currentTarget.dataset.path.startsWith('domainCards');
await this.levelup.updateSource({ [event.currentTarget.dataset.path]: item.uuid });
this.render();
}
}
}
async selectionClick(event) {
event.stopPropagation();
const button = event.currentTarget;
// const advancementSelections = this.getAdvancementSelectionUpdates(button);
if (!button.checked) {
await this.levelup.updateSource({
[`tiers.${button.dataset.tier}.levels.${button.dataset.level}.optionSelections.${button.dataset.option}.-=${button.dataset.checkboxNr}`]:
@ -101,12 +261,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
}
static async save() {
await this.actor.update({
'system.levelData': {
'level.current': this.actor.system.levelData.level.changed,
'selections': this.levelup.playerData
}
});
await this.actor.levelUp(this.levelup.selectionData);
this.close();
}

View file

@ -182,9 +182,9 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) {
htmlElement
.querySelectorAll('.experience-value')
.forEach(element => element.addEventListener('change', this.experienceValueChange.bind(this)));
htmlElement
.querySelectorAll('[data-item]')
.forEach(element => element.addEventListener.on('change', this.itemUpdate.bind(this)));
// htmlElement
// .querySelectorAll('[data-item]')
// .forEach(element => element.addEventListener.on('change', this.itemUpdate.bind(this)));
htmlElement.querySelector('.level-value').addEventListener('change', this.onLevelChange.bind(this));
}

View file

@ -61,19 +61,31 @@ class DhLevelOption extends foundry.abstract.DataModel {
export const LevelOptionType = {
trait: {
id: 'trait',
label: 'Character Trait'
label: 'Character Trait',
dataPath: ''
},
hitPoint: {
id: 'hitPoint',
label: 'Hit Points'
label: 'Hit Points',
dataPath: 'resources.hitPoints',
dataPathData: {
property: 'max',
dependencies: ['value']
}
},
stress: {
id: 'stress',
label: 'Stress'
label: 'Stress',
dataPath: 'resources.stress',
dataPathData: {
property: 'max',
dependencies: ['value']
}
},
evasion: {
id: 'evasion',
label: 'Evasion'
label: 'Evasion',
dataPath: 'evasion'
},
proficiency: {
id: 'proficiency',
@ -108,8 +120,8 @@ export const defaultLevelTiers = {
},
initialAchievements: {
experience: {
nr: 2,
modifier: 1
nr: 1,
modifier: 2
},
proficiency: 1
},
@ -171,8 +183,8 @@ export const defaultLevelTiers = {
},
initialAchievements: {
experience: {
nr: 2,
modifier: 1
nr: 1,
modifier: 2
},
proficiency: 1
},
@ -252,8 +264,8 @@ export const defaultLevelTiers = {
},
initialAchievements: {
experience: {
nr: 2,
modifier: 1
nr: 1,
modifier: 2
},
proficiency: 1
},

View file

@ -7,17 +7,63 @@ export class DhLevelup extends foundry.abstract.DataModel {
const tierKeys = Object.keys(levelTierData.tiers);
const maxLevel = levelTierData.tiers[tierKeys[tierKeys.length - 1]].levels.end;
return {
tiers: tierKeys.reduce((acc, key) => {
acc[key] = DhLevelupTier.initializeData(
levelTierData.tiers[key],
maxLevel,
pcLevelData.selections.filter(x => x.tier === Number(key)),
pcLevelData.level.changed
);
const totalLevelProgression = [];
for (var level = pcLevelData.level.current + 1; level <= pcLevelData.level.changed; level++) {
totalLevelProgression.push(level);
}
const tiers = tierKeys.reduce((acc, key) => {
acc[key] = DhLevelupTier.initializeData(
levelTierData.tiers[key],
maxLevel,
pcLevelData.selections.filter(x => x.tier === Number(key)),
pcLevelData.level.changed
);
return acc;
}, {});
const allInitialAchievements = Object.values(tiers).reduce(
(acc, tier) => {
const levelThreshold = Math.min(...tier.belongingLevels);
if (totalLevelProgression.includes(levelThreshold)) {
acc.proficiency += tier.initialAchievements.proficiency;
[...Array(tier.initialAchievements.experience.nr).keys()].forEach(_ => {
acc.newExperiences[foundry.utils.randomID()] = {
name: '',
modifier: tier.initialAchievements.experience.modifier
};
});
}
return acc;
}, {}),
},
{ newExperiences: {}, proficiency: 0 }
);
const domainCards = Object.keys(tiers).reduce((acc, tierKey) => {
const tier = tiers[tierKey];
for (var level of tier.belongingLevels) {
if (level <= pcLevelData.level.changed) {
for (var domainCardSlot = 1; domainCardSlot <= tier.domainCardByLevel; domainCardSlot++) {
const cardId = foundry.utils.randomID();
acc[cardId] = {
uuid: null,
tier: tierKey,
level: level,
domainCardSlot: domainCardSlot,
path: `domainCards.${cardId}.uuid`
};
}
}
}
return acc;
}, {});
return {
tiers: tiers,
maxSelections: [...Array(pcLevelData.level.changed).keys()].reduce((acc, index) => {
const level = index + 1;
const availableChoices = availableChoicesPerLevel[level];
@ -26,7 +72,12 @@ export class DhLevelup extends foundry.abstract.DataModel {
}
return acc;
}, 0)
}, 0),
allInitialAchievements: {
newExperiences: allInitialAchievements.newExperiences,
proficiency: allInitialAchievements.proficiency
},
domainCards: domainCards
};
}
@ -35,7 +86,28 @@ export class DhLevelup extends foundry.abstract.DataModel {
return {
tiers: new fields.TypedObjectField(new fields.EmbeddedDataField(DhLevelupTier)),
maxSelections: new fields.NumberField({ required: true, integer: true })
maxSelections: new fields.NumberField({ required: true, integer: true }),
allInitialAchievements: new fields.SchemaField({
newExperiences: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({ required: true }),
modifier: new fields.NumberField({ required: true, integer: true })
})
),
proficiency: new fields.NumberField({ required: true, integer: true })
}),
domainCards: new fields.TypedObjectField(
new fields.SchemaField({
uuid: new fields.StringField({ required: true, nullable: true, initial: null }),
tier: new fields.NumberField({ required: true, integer: true }),
level: new fields.NumberField({ required: true, integer: true }),
domainCardSlot: new fields.NumberField({ required: true, integer: true }),
path: new fields.StringField({ required: true })
})
)
// advancementSelections: new fields.SchemaField({
// experiences: new fields.SetField(new fields.StringField()),
// }),
};
}
@ -57,7 +129,7 @@ export class DhLevelup extends foundry.abstract.DataModel {
);
}
get playerData() {
get selectionData() {
return Object.keys(this.tiers).flatMap(tierKey => {
const tier = this.tiers[tierKey];
return Object.keys(tier.levels).flatMap(levelKey => {
@ -66,15 +138,19 @@ export class DhLevelup extends foundry.abstract.DataModel {
const selection = level.optionSelections[optionSelectionKey];
const optionSelect = tier.options[optionSelectionKey];
return Object.keys(selection).map(checkboxNr => ({
tier: Number(tierKey),
level: Number(levelKey),
optionKey: optionSelectionKey,
type: optionSelect.type,
checkboxNr: Number(checkboxNr),
value: optionSelect.value,
amount: optionSelect.amount
}));
return Object.keys(selection).map(checkboxNr => {
const selectionObj = selection[checkboxNr];
return {
tier: Number(tierKey),
level: Number(levelKey),
optionKey: optionSelectionKey,
type: optionSelect.type,
checkboxNr: Number(checkboxNr),
value: optionSelect.value,
amount: optionSelect.amount,
data: selectionObj.data
};
});
});
});
});
@ -108,6 +184,8 @@ class DhLevelupTier extends foundry.abstract.DataModel {
return acc;
}, {}),
belongingLevels: belongingLevels,
initialAchievements: levelTier.initialAchievements,
domainCardByLevel: levelTier.domainCardByLevel,
levels: levels
};
}
@ -121,10 +199,22 @@ class DhLevelupTier extends foundry.abstract.DataModel {
active: new fields.BooleanField({ required: true, initial: true }),
options: new fields.TypedObjectField(new fields.EmbeddedDataField(DhLevelupTierOption)),
belongingLevels: new fields.ArrayField(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 })
}),
domainCardByLevel: new fields.NumberField({ required: true, integer: true }),
levels: new fields.TypedObjectField(new fields.EmbeddedDataField(DhLevelupLevel))
};
}
get initialAchievementData() {
return this.active ? this.initialAchievements : null;
}
get selections() {
const allSelections = Object.keys(this.levels).reduce(
(acc, key) => {
@ -223,7 +313,8 @@ class DhLevelupLevel extends foundry.abstract.DataModel {
new fields.SchemaField({
selected: new fields.BooleanField({ required: true, initial: true }),
minCost: new fields.NumberField({ required: true, integer: true }),
locked: new fields.BooleanField({ required: true, initial: false })
locked: new fields.BooleanField({ required: true, initial: false }),
data: new fields.StringField()
})
)
)

View file

@ -7,10 +7,7 @@ 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 })
bonus: new fields.NumberField({ initial: 0, integer: true })
})
});
@ -52,11 +49,7 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
presence: attributeField(),
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 })
}),
proficiency: new fields.NumberField({ required: true, initial: 1, integer: true }),
evasion: new fields.NumberField({ initial: 0, integer: true }),
experiences: new fields.ArrayField(
new fields.SchemaField({
@ -347,7 +340,15 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
this.evasion = this.class?.system?.evasion ?? 0;
// this.armor.value = this.activeArmor?.baseScore ?? 0;
// this.damageThresholds = this.computeDamageThresholds();
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
};
this.applyLevels();
this.applyEffects();

View file

@ -45,6 +45,15 @@ export default class DhpActor extends Actor {
}
}
async levelUp(levelupData) {
await this.actor.update({
'system.levelData': {
'level.current': this.system.levelData.level.changed,
'selections': levelupData
}
});
}
async diceRoll(modifier, shiftKey) {
if (this.type === 'pc') {
return await this.dualityRoll(modifier, shiftKey);

View file

@ -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 = `<input type="range" name="${name}" value="${value}" min="${min}" max="${max}" step="${step}"/>
<span class="range-value">${value}</span>`;
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) {