mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-01-12 03:31:07 +01:00
Levelup Remake (#100)
* Set up DhLevelTier datamodel * Added Levelup data model and started at the render * Fixed data handling in the LevelUp view * Added back the save function * Finalised levelup selections and propagating to PC * Added level advancement selection data * Added DomainCard selection * Css merge commit * Added PC level/delevel benefits of leveling up * Fixed sticky previous selections on continous leveling * Fixed up Summary. Fixed multiclass/subclass blocking on selection * Removed unused level.hbs * Fixed attribute base for PC * Improved naming of attribute properties * Renamed/structured resources/evasion/proficiency * Improved trait marking * Rework to level up once at a time * Added markers * Removed tabs when in Summary * Fixed multilevel buttons * Improved multiclass/subclass recognition * Fixed tagify error on selection * Review fixes
This commit is contained in:
parent
47a6abddfb
commit
a92221778e
41 changed files with 3279 additions and 1283 deletions
340
module/data/levelTier.mjs
Normal file
340
module/data/levelTier.mjs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
311
module/data/levelup.mjs
Normal file
311
module/data/levelup.mjs
Normal file
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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,42 +232,6 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
|
|||
}
|
||||
}
|
||||
|
||||
get inventoryWeapons() {
|
||||
const inventoryWeaponFirst = this.parent.items.find(x => x.type === 'weapon' && x.system.inventoryWeapon === 1);
|
||||
const inventoryWeaponSecond = this.parent.items.find(
|
||||
x => x.type === 'weapon' && x.system.inventoryWeapon === 2
|
||||
);
|
||||
return {
|
||||
first: this.#weaponData(inventoryWeaponFirst),
|
||||
second: this.#weaponData(inventoryWeaponSecond)
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
@ -367,141 +282,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) {
|
||||
|
|
@ -509,10 +320,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({
|
||||
|
|
@ -539,10 +350,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;
|
||||
|
|
@ -550,3 +357,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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue