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
|
|
@ -293,7 +293,8 @@ 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/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'
|
||||
]);
|
||||
|
|
|
|||
115
lang/en.json
115
lang/en.json
|
|
@ -163,6 +163,12 @@
|
|||
"Or": "Or",
|
||||
"Description": "Description",
|
||||
"Features": "Features",
|
||||
"proficiency": "Proficiency",
|
||||
"unarmored": "Unarmored",
|
||||
"Experience": {
|
||||
"Single": "Experience",
|
||||
"plural": "Experiences"
|
||||
},
|
||||
"Adversary": {
|
||||
"Singular": "Adversary",
|
||||
"Plural": "Adversaries"
|
||||
|
|
@ -357,39 +363,43 @@
|
|||
"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."
|
||||
}
|
||||
|
|
@ -775,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",
|
||||
|
|
@ -937,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",
|
||||
|
|
|
|||
|
|
@ -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 { level: level, value: value };
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
return context;
|
||||
}
|
||||
|
||||
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]);
|
||||
});
|
||||
});
|
||||
|
||||
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'
|
||||
},
|
||||
{ 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
|
||||
});
|
||||
next: {
|
||||
disabled: !this.levelup.currentLevelFinished,
|
||||
label: next
|
||||
? game.i18n.format('DAGGERHEART.Application.LevelUp.navigateLevel', { level: next })
|
||||
: '',
|
||||
toSummary: !next,
|
||||
show: this.tabGroups.primary !== 'summary'
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {})
|
||||
};
|
||||
|
||||
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;
|
||||
}, {});
|
||||
|
||||
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 {
|
||||
data: data,
|
||||
activeLevel: this.activeLevel,
|
||||
changedLevel: this.actor.system.levelData.changedLevel,
|
||||
completedSelection: selectedChoices === 2
|
||||
...domain,
|
||||
selected: key === data.secondaryData,
|
||||
disabled: (data.secondaryData && key !== data.secondaryData) || alreadySelected
|
||||
};
|
||||
}) ?? [],
|
||||
compendium: 'classes',
|
||||
limit: 1
|
||||
};
|
||||
}
|
||||
|
||||
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])];
|
||||
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);
|
||||
}
|
||||
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
|
||||
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
|
||||
}
|
||||
};
|
||||
updates[1] = {
|
||||
path: [arrayPart, '1'].join('.'),
|
||||
value: {
|
||||
level: this.activeLevel,
|
||||
value: {
|
||||
class: multiclassData.class,
|
||||
subclass: multiclassData.subclass,
|
||||
domain: multiclassData.domain,
|
||||
level: this.activeLevel
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
} 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;
|
||||
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;
|
||||
}, {});
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
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';
|
||||
|
|
@ -268,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'),
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
@ -215,9 +225,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))
|
||||
};
|
||||
|
|
@ -479,7 +489,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 });
|
||||
}
|
||||
|
||||
|
|
@ -531,21 +541,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) {
|
||||
|
|
@ -576,7 +586,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(
|
||||
|
|
@ -592,7 +602,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 = {
|
||||
|
|
@ -633,7 +643,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) {
|
||||
|
|
@ -781,7 +796,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();
|
||||
}
|
||||
|
|
@ -866,6 +881,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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,19 @@ export const gameSettings = {
|
|||
AbilityArray: 'AbilityArray',
|
||||
RangeMeasurement: 'RangeMeasurement'
|
||||
},
|
||||
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'
|
||||
}
|
||||
};
|
||||
|
|
|
|||
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 })
|
||||
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(
|
||||
const resourceField = max =>
|
||||
new fields.SchemaField({
|
||||
multiclass: new fields.BooleanField(),
|
||||
feature: new fields.StringField({})
|
||||
})
|
||||
),
|
||||
multiclass: new fields.TypedObjectField(new fields.BooleanField())
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,11 @@ export default class DhpActor extends Actor {
|
|||
|
||||
// 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,48 +24,105 @@ 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;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
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
|
||||
)
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
selections.push({
|
||||
...checkbox,
|
||||
level: Number(levelKey),
|
||||
optionKey: optionKey,
|
||||
checkboxNr: Number(checkboxNr),
|
||||
itemUuid
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
levelups[levelKey] = {
|
||||
achievements: {
|
||||
...level.achievements,
|
||||
domainCards: achievementDomainCards
|
||||
},
|
||||
selections: selections
|
||||
};
|
||||
}
|
||||
|
||||
await this.update({
|
||||
system: {
|
||||
levelData: {
|
||||
level: {
|
||||
current: this.system.levelData.level.changed
|
||||
},
|
||||
levelups: levelups
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async diceRoll(modifier, shiftKey) {
|
||||
if (this.type === 'pc') {
|
||||
return await this.dualityRoll(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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 `<tag title="${tagData.title || tagData.value}"
|
||||
contenteditable='false'
|
||||
spellcheck='false'
|
||||
tabIndex="${this.settings.a11y.focusableTags ? 0 : -1}"
|
||||
class="${this.settings.classNames.tag} ${tagData.class ? tagData.class : ''}"
|
||||
${this.getAttributes(tagData)}>
|
||||
<x class="${this.settings.classNames.tagX}" role='button' aria-label='remove tag'></x>
|
||||
<div>
|
||||
<span class="${this.settings.classNames.tagText}">${tagData[this.settings.tagTextProp] || tagData.value}</span>
|
||||
${tagData.src ? `<img src="${tagData.src}"></i>` : ''}
|
||||
</div>
|
||||
</tag>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const onSelect = async event => {
|
||||
const inputElement = event.detail.tagify.DOM.originalInput;
|
||||
const selectedOptions = event.detail?.value ? JSON.parse(event.detail.value) : [];
|
||||
|
||||
const unusedDropDownItems = event.detail.tagify.suggestedListItems;
|
||||
const missingOptions = Object.keys(options).filter(x => !unusedDropDownItems.find(item => item.value === x));
|
||||
const removedItem = missingOptions.find(x => !selectedOptions.find(item => item.value === x));
|
||||
const addedItem = removedItem
|
||||
? null
|
||||
: selectedOptions.find(x => !missingOptions.find(item => item === x.value));
|
||||
|
||||
const changedItem = { option: removedItem ?? addedItem.value, removed: Boolean(removedItem) };
|
||||
|
||||
onChange(selectedOptions, changedItem, inputElement);
|
||||
};
|
||||
tagifyElement.on('change', onSelect);
|
||||
};
|
||||
|
||||
export const getDeleteKeys = (property, innerProperty, innerPropertyDefaultValue) => {
|
||||
return Object.keys(property).reduce((acc, key) => {
|
||||
if (innerProperty) {
|
||||
if (innerPropertyDefaultValue !== undefined) {
|
||||
acc[`${key}`] = {
|
||||
[innerProperty]: innerPropertyDefaultValue
|
||||
};
|
||||
} else {
|
||||
acc[`${key}.-=${innerProperty}`] = null;
|
||||
}
|
||||
} else {
|
||||
acc[`-=${key}`] = null;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
/* Drop Shadows */
|
||||
/* Background */
|
||||
/* Duality */
|
||||
/* Fear */
|
||||
@import '../node_modules/@yaireo/tagify/dist/tagify.css';
|
||||
.daggerheart.sheet.class .editor {
|
||||
height: 500px;
|
||||
|
|
@ -2750,11 +2751,222 @@ div.daggerheart.views.multiclass {
|
|||
.item-button .item-icon.checked {
|
||||
opacity: 1;
|
||||
}
|
||||
.theme-light .daggerheart.levelup .tiers-container .tier-container {
|
||||
background-image: url('../assets/parchments/dh-parchment-light.png');
|
||||
}
|
||||
.daggerheart.levelup .window-content {
|
||||
max-height: 960px;
|
||||
overflow: auto;
|
||||
}
|
||||
.daggerheart.levelup div[data-application-part='form'] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.daggerheart.levelup section .section-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.daggerheart.levelup .levelup-navigation-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 22px;
|
||||
height: 36px;
|
||||
}
|
||||
.daggerheart.levelup .levelup-navigation-container nav {
|
||||
flex: 1;
|
||||
}
|
||||
.daggerheart.levelup .levelup-navigation-container nav .levelup-tab-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.daggerheart.levelup .levelup-navigation-container .levelup-navigation-actions {
|
||||
width: 306px;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
gap: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.daggerheart.levelup .levelup-navigation-container .levelup-navigation-actions * {
|
||||
width: calc(50% - 8px);
|
||||
}
|
||||
.daggerheart.levelup .tiers-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
.daggerheart.levelup .tiers-container .tier-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
background-image: url('../assets/parchments/dh-parchment-dark.png');
|
||||
}
|
||||
.daggerheart.levelup .tiers-container .tier-container.inactive {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
.daggerheart.levelup .tiers-container .tier-container legend {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.daggerheart.levelup .tiers-container .tier-container .checkbox-group-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 3fr;
|
||||
gap: 4px;
|
||||
}
|
||||
.daggerheart.levelup .tiers-container .tier-container .checkbox-group-container .checkboxes-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
gap: 4px;
|
||||
}
|
||||
.daggerheart.levelup .tiers-container .tier-container .checkbox-group-container .checkboxes-container .checkbox-grouping-coontainer {
|
||||
display: flex;
|
||||
height: min-content;
|
||||
}
|
||||
.daggerheart.levelup .tiers-container .tier-container .checkbox-group-container .checkboxes-container .checkbox-grouping-coontainer.multi {
|
||||
border: 2px solid grey;
|
||||
padding: 2.4px 2.5px 0;
|
||||
border-radius: 4px;
|
||||
gap: 2px;
|
||||
}
|
||||
.daggerheart.levelup .tiers-container .tier-container .checkbox-group-container .checkboxes-container .checkbox-grouping-coontainer.multi .selection-checkbox {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
.daggerheart.levelup .tiers-container .tier-container .checkbox-group-container .checkboxes-container .checkbox-grouping-coontainer .selection-checkbox {
|
||||
margin: 0;
|
||||
}
|
||||
.daggerheart.levelup .tiers-container .tier-container .checkbox-group-container .checkbox-group-label {
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
}
|
||||
.daggerheart.levelup .levelup-selections-container .achievement-experience-cards {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.daggerheart.levelup .levelup-selections-container .achievement-experience-cards .achievement-experience-card {
|
||||
border: 1px solid;
|
||||
border-radius: 4px;
|
||||
padding-right: 4px;
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.daggerheart.levelup .levelup-selections-container .achievement-experience-cards .achievement-experience-card .achievement-experience-marker {
|
||||
border: 1px solid;
|
||||
border-radius: 50%;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
.daggerheart.levelup .levelup-selections-container .levelup-card-selection {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 40px;
|
||||
}
|
||||
.daggerheart.levelup .levelup-selections-container .levelup-card-selection .card-preview-container {
|
||||
width: calc(100% * (1 / 5));
|
||||
}
|
||||
.daggerheart.levelup .levelup-selections-container .levelup-card-selection .levelup-domains-selection-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.daggerheart.levelup .levelup-selections-container .levelup-card-selection .levelup-domains-selection-container .levelup-domain-selection-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
.daggerheart.levelup .levelup-selections-container .levelup-card-selection .levelup-domains-selection-container .levelup-domain-selection-container.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.4;
|
||||
}
|
||||
.daggerheart.levelup .levelup-selections-container .levelup-card-selection .levelup-domains-selection-container .levelup-domain-selection-container .levelup-domain-label {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 4px;
|
||||
background: grey;
|
||||
padding: 0 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.daggerheart.levelup .levelup-selections-container .levelup-card-selection .levelup-domains-selection-container .levelup-domain-selection-container img {
|
||||
height: 124px;
|
||||
}
|
||||
.daggerheart.levelup .levelup-selections-container .levelup-card-selection .levelup-domains-selection-container .levelup-domain-selection-container .levelup-domain-selected {
|
||||
position: absolute;
|
||||
height: 54px;
|
||||
width: 54px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid;
|
||||
font-size: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-image: url(../assets/parchments/dh-parchment-light.png);
|
||||
color: var(--color-dark-5);
|
||||
top: calc(50% - 29px);
|
||||
}
|
||||
.daggerheart.levelup .levelup-selections-container .levelup-card-selection .levelup-domains-selection-container .levelup-domain-selection-container .levelup-domain-selected i {
|
||||
position: relative;
|
||||
right: 2px;
|
||||
}
|
||||
.daggerheart.levelup .levelup-selections-container .levelup-selections-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.daggerheart.levelup .levelup-summary-container .level-achievements-container,
|
||||
.daggerheart.levelup .levelup-summary-container .level-advancements-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.daggerheart.levelup .levelup-summary-container .level-achievements-container h2,
|
||||
.daggerheart.levelup .levelup-summary-container .level-advancements-container h2,
|
||||
.daggerheart.levelup .levelup-summary-container .level-achievements-container h3,
|
||||
.daggerheart.levelup .levelup-summary-container .level-advancements-container h3,
|
||||
.daggerheart.levelup .levelup-summary-container .level-achievements-container h4,
|
||||
.daggerheart.levelup .levelup-summary-container .level-advancements-container h4,
|
||||
.daggerheart.levelup .levelup-summary-container .level-achievements-container h5,
|
||||
.daggerheart.levelup .levelup-summary-container .level-advancements-container h5 {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.daggerheart.levelup .levelup-summary-container .increase-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.daggerheart.levelup .levelup-summary-container .summary-selection-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.daggerheart.levelup .levelup-summary-container .summary-selection-container .summary-selection {
|
||||
border: 2px solid;
|
||||
border-radius: 6px;
|
||||
padding: 0 4px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.daggerheart.levelup .levelup-footer {
|
||||
display: flex;
|
||||
}
|
||||
:root {
|
||||
--primary-color-fear: rgba(9, 71, 179, 0.75);
|
||||
--secondary-color-fear: rgba(9, 71, 179, 0.75);
|
||||
--shadow-text-stroke: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
|
||||
--fear-animation: background 0.3s ease, box-shadow .3s ease, border-color .3s ease, opacity .3s ease;
|
||||
--fear-animation: background 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease, opacity 0.3s ease;
|
||||
}
|
||||
#resources {
|
||||
min-height: calc(var(--header-height) + 4rem);
|
||||
|
|
@ -2785,7 +2997,7 @@ div.daggerheart.views.multiclass {
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 3rem;
|
||||
background-color: var(--primary-color-fear);
|
||||
background-color: var(rgba(9, 71, 179, 0.75));
|
||||
-webkit-box-shadow: 0px 0px 5px 1px rgba(0, 0, 0, 0.75);
|
||||
box-shadow: 0px 0px 5px 1px rgba(0, 0, 0, 0.75);
|
||||
color: #d3d3d3;
|
||||
|
|
@ -2850,7 +3062,7 @@ div.daggerheart.views.multiclass {
|
|||
#resources .window-content #resource-fear.isGM i:hover {
|
||||
font-size: var(--font-size-20);
|
||||
}
|
||||
#resources button[data-action="close"] {
|
||||
#resources button[data-action='close'] {
|
||||
display: none;
|
||||
}
|
||||
#resources:not(:hover):not(.minimized) {
|
||||
|
|
@ -3280,6 +3492,93 @@ div.daggerheart.views.multiclass {
|
|||
.system-daggerheart.theme-light .tagify__dropdown .tagify__dropdown__item--active {
|
||||
color: #efe6d8;
|
||||
}
|
||||
.theme-light .application .component.dh-style.card-preview-container {
|
||||
background-image: url('../assets/parchments/dh-parchment-light.png');
|
||||
}
|
||||
.theme-light .application .component.dh-style.card-preview-container .preview-text-container {
|
||||
background-image: url(../assets/parchments/dh-parchment-dark.png);
|
||||
}
|
||||
.theme-light .application .component.dh-style.card-preview-container .preview-selected-icon-container {
|
||||
background-image: url(../assets/parchments/dh-parchment-dark.png);
|
||||
color: var(--color-light-5);
|
||||
}
|
||||
.application .component.dh-style.card-preview-container {
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
border: 2px solid var(--color-tabs-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
aspect-ratio: 0.75;
|
||||
background-image: url('../assets/parchments/dh-parchment-dark.png');
|
||||
}
|
||||
.application .component.dh-style.card-preview-container.selectable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.application .component.dh-style.card-preview-container.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.4;
|
||||
}
|
||||
.application .component.dh-style.card-preview-container .preview-image-outer-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.application .component.dh-style.card-preview-container .preview-image-container {
|
||||
flex: 1;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
.application .component.dh-style.card-preview-container .preview-text-container {
|
||||
flex: 1;
|
||||
border-radius: 0 0 4px 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
color: var(--color-text-selection-bg);
|
||||
background-image: url(../assets/parchments/dh-parchment-light.png);
|
||||
}
|
||||
.application .component.dh-style.card-preview-container .preview-empty-container {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
.application .component.dh-style.card-preview-container .preview-empty-container .preview-empty-inner-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.application .component.dh-style.card-preview-container .preview-empty-container .preview-empty-inner-container .preview-add-icon {
|
||||
font-size: 48px;
|
||||
}
|
||||
.application .component.dh-style.card-preview-container .preview-empty-container .preview-empty-inner-container .preview-empty-subtext {
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
font-size: 18px;
|
||||
font-variant: small-caps;
|
||||
text-align: center;
|
||||
}
|
||||
.application .component.dh-style.card-preview-container .preview-selected-icon-container {
|
||||
position: absolute;
|
||||
height: 54px;
|
||||
width: 54px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid;
|
||||
font-size: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-image: url(../assets/parchments/dh-parchment-light.png);
|
||||
color: var(--color-dark-5);
|
||||
}
|
||||
.application .component.dh-style.card-preview-container .preview-selected-icon-container i {
|
||||
position: relative;
|
||||
right: 2px;
|
||||
}
|
||||
.sheet.daggerheart.dh-style .tab-navigation {
|
||||
margin: 5px 0;
|
||||
height: 40px;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
@import './application.less';
|
||||
@import './sheets/sheets.less';
|
||||
@import './dialog.less';
|
||||
@import './levelup.less';
|
||||
@import '../node_modules/@yaireo/tagify/dist/tagify.css';
|
||||
@import './resources.less';
|
||||
|
||||
|
|
|
|||
|
|
@ -288,3 +288,105 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.theme-light .application .component.dh-style.card-preview-container {
|
||||
background-image: url('../assets/parchments/dh-parchment-light.png');
|
||||
|
||||
.preview-text-container {
|
||||
background-image: url(../assets/parchments/dh-parchment-dark.png);
|
||||
}
|
||||
|
||||
.preview-selected-icon-container {
|
||||
background-image: url(../assets/parchments/dh-parchment-dark.png);
|
||||
color: var(--color-light-5);
|
||||
}
|
||||
}
|
||||
|
||||
.application .component.dh-style.card-preview-container {
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
border: 2px solid var(--color-tabs-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
aspect-ratio: 0.75;
|
||||
background-image: url('../assets/parchments/dh-parchment-dark.png');
|
||||
|
||||
&.selectable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.preview-image-outer-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-image-container {
|
||||
flex: 1;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.preview-text-container {
|
||||
flex: 1;
|
||||
border-radius: 0 0 4px 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
color: var(--color-text-selection-bg);
|
||||
background-image: url(../assets/parchments/dh-parchment-light.png);
|
||||
}
|
||||
|
||||
.preview-empty-container {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
|
||||
.preview-empty-inner-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.preview-add-icon {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.preview-empty-subtext {
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
font-size: 18px;
|
||||
font-variant: small-caps;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview-selected-icon-container {
|
||||
position: absolute;
|
||||
height: 54px;
|
||||
width: 54px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid;
|
||||
font-size: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-image: url(../assets/parchments/dh-parchment-light.png);
|
||||
color: var(--color-dark-5);
|
||||
|
||||
i {
|
||||
position: relative;
|
||||
right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/cinzeldecorative/v17/daaHSScvJGqLYhG8nNt8KPPswUAPniZoaelD.ttf) format('truetype');
|
||||
src: url(https://fonts.gstatic.com/s/cinzeldecorative/v17/daaHSScvJGqLYhG8nNt8KPPswUAPniZoaelD.ttf)
|
||||
format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
@import '../utils/fonts.less';
|
||||
|
||||
.application.sheet.daggerheart.dh-style.domain-card {
|
||||
|
||||
section.tab {
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
|
|
|
|||
261
styles/levelup.less
Normal file
261
styles/levelup.less
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
.theme-light {
|
||||
.daggerheart.levelup {
|
||||
.tiers-container {
|
||||
.tier-container {
|
||||
background-image: url('../assets/parchments/dh-parchment-light.png');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.daggerheart.levelup {
|
||||
.window-content {
|
||||
max-height: 960px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
div[data-application-part='form'] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
section {
|
||||
.section-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.levelup-navigation-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 22px;
|
||||
height: 36px;
|
||||
|
||||
nav {
|
||||
flex: 1;
|
||||
|
||||
.levelup-tab-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.levelup-navigation-actions {
|
||||
width: 306px;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
gap: 16px;
|
||||
margin-right: 4px;
|
||||
|
||||
* {
|
||||
width: calc(50% - 8px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tiers-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
.tier-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
background-image: url('../assets/parchments/dh-parchment-dark.png');
|
||||
|
||||
&.inactive {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
legend {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.checkbox-group-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 3fr;
|
||||
gap: 4px;
|
||||
|
||||
.checkboxes-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
gap: 4px;
|
||||
|
||||
.checkbox-grouping-coontainer {
|
||||
display: flex;
|
||||
height: min-content;
|
||||
|
||||
&.multi {
|
||||
border: 2px solid grey;
|
||||
padding: 2.4px 2.5px 0;
|
||||
border-radius: 4px;
|
||||
gap: 2px;
|
||||
|
||||
.selection-checkbox {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.selection-checkbox {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-group-label {
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.levelup-selections-container {
|
||||
.achievement-experience-cards {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.achievement-experience-card {
|
||||
border: 1px solid;
|
||||
border-radius: 4px;
|
||||
padding-right: 4px;
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.achievement-experience-marker {
|
||||
border: 1px solid;
|
||||
border-radius: 50%;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.levelup-card-selection {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 40px;
|
||||
|
||||
.card-preview-container {
|
||||
width: calc(100% * (1 / 5));
|
||||
}
|
||||
|
||||
.levelup-domains-selection-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.levelup-domain-selection-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.levelup-domain-label {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 4px;
|
||||
background: grey;
|
||||
padding: 0 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 124px;
|
||||
}
|
||||
|
||||
.levelup-domain-selected {
|
||||
position: absolute;
|
||||
height: 54px;
|
||||
width: 54px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid;
|
||||
font-size: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-image: url(../assets/parchments/dh-parchment-light.png);
|
||||
color: var(--color-dark-5);
|
||||
top: calc(50% - 29px);
|
||||
|
||||
i {
|
||||
position: relative;
|
||||
right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.levelup-selections-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.levelup-summary-container {
|
||||
.level-achievements-container,
|
||||
.level-advancements-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.increase-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.summary-selection-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.summary-selection {
|
||||
border: 2px solid;
|
||||
border-radius: 6px;
|
||||
padding: 0 4px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.levelup-footer {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
:root {
|
||||
--primary-color-fear: rgba(9, 71, 179, .75);
|
||||
--secondary-color-fear: rgba(9, 71, 179, .75);
|
||||
--shadow-text-stroke: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
|
||||
--fear-animation : background .3s ease, box-shadow .3s ease, border-color .3s ease, opacity .3s ease;
|
||||
--fear-animation: background 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease, opacity 0.3s ease;
|
||||
}
|
||||
|
||||
#resources {
|
||||
|
|
@ -10,37 +8,39 @@
|
|||
min-width: 4rem;
|
||||
color: #d3d3d3;
|
||||
transition: var(--fear-animation);
|
||||
header, .controls, .window-resize-handle {
|
||||
header,
|
||||
.controls,
|
||||
.window-resize-handle {
|
||||
transition: var(--fear-animation);
|
||||
}
|
||||
.window-content {
|
||||
padding: .5rem;
|
||||
padding: 0.5rem;
|
||||
#resource-fear {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: .5rem 0.25rem;
|
||||
gap: 0.5rem 0.25rem;
|
||||
flex-wrap: wrap;
|
||||
i {
|
||||
font-size: var(--font-size-18);
|
||||
// flex: 1 1 calc(25% - 0.25rem);
|
||||
border: 1px solid rgba(0,0,0,.5);
|
||||
border: 1px solid rgba(0, 0, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 3rem;
|
||||
background-color: var(--primary-color-fear);
|
||||
-webkit-box-shadow: 0px 0px 5px 1px rgba(0,0,0,.75);
|
||||
box-shadow: 0px 0px 5px 1px rgba(0,0,0,.75);
|
||||
background-color: var(@primary-color-fear);
|
||||
-webkit-box-shadow: 0px 0px 5px 1px rgba(0, 0, 0, 0.75);
|
||||
box-shadow: 0px 0px 5px 1px rgba(0, 0, 0, 0.75);
|
||||
color: #d3d3d3;
|
||||
flex-grow: 0;
|
||||
&.inactive {
|
||||
filter: grayscale(1) !important;
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
.controls, .resource-bar {
|
||||
.controls,
|
||||
.resource-bar {
|
||||
border: 2px solid rgb(153 122 79);
|
||||
background-color: rgb(24 22 46);
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
font-size: 1.5rem;
|
||||
}
|
||||
&.disabled {
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
.resource-bar {
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
font-size: var(--font-size-20);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: .25rem .5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
flex: 1;
|
||||
text-shadow: var(--shadow-text-stroke);
|
||||
&:before {
|
||||
|
|
@ -88,7 +88,6 @@
|
|||
z-index: 1;
|
||||
}
|
||||
&.fear {
|
||||
|
||||
}
|
||||
}
|
||||
&.isGM {
|
||||
|
|
@ -101,14 +100,16 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
button[data-action="close"] {
|
||||
button[data-action='close'] {
|
||||
display: none;
|
||||
}
|
||||
&:not(:hover):not(.minimized) {
|
||||
background: transparent;
|
||||
box-shadow: unset;
|
||||
border-color: transparent;
|
||||
header, .controls, .window-resize-handle {
|
||||
header,
|
||||
.controls,
|
||||
.window-resize-handle {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,3 +32,6 @@
|
|||
@criticalAccent: #66159c;
|
||||
@criticalBackgroundStart: rgba(37, 8, 37, 0.6);
|
||||
@criticalBackgroundEnd: rgba(128, 0, 128, 0.6);
|
||||
|
||||
/* Fear */
|
||||
@primary-color-fear: rgba(9, 71, 179, 0.75);
|
||||
|
|
|
|||
13
templates/components/card-preview.hbs
Normal file
13
templates/components/card-preview.hbs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<div class="component dh-style card-preview-container {{#if (and this.compendium (not this.img))}}selectable{{/if}}" data-action="viewCompendium" data-compendium="{{this.compendium}}" data-path="{{this.path}}" data-limit="{{this.limit}}">
|
||||
{{#if this.img}}
|
||||
<img class="preview-image-container" src="{{this.img}}" />
|
||||
<div class="preview-text-container">{{this.name}}</div>
|
||||
{{else}}
|
||||
<div class="preview-empty-container">
|
||||
<div class="preview-empty-inner-container">
|
||||
<i class="preview-add-icon fa-solid fa-plus"></i>
|
||||
<div class="preview-empty-subtext">{{this.emptySubtext}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
|
@ -103,7 +103,7 @@
|
|||
<div class="form-group">
|
||||
<label>{{localize "DAGGERHEART.Sheets.Adversary.HP"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" name="system.resources.health.max" value="{{source.system.resources.health.max}}" data-dtype="Number" />
|
||||
<input type="text" name="system.resources.hitPoints.max" value="{{source.system.resources.hitPoints.max}}" data-dtype="Number" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
|
|
|||
|
|
@ -7,22 +7,19 @@
|
|||
{{#each this.attributes as |attribute key|}}
|
||||
<div class="attribute">
|
||||
<div class="attribute-banner">
|
||||
<img class="attribute-roll" data-action="attributeRoll" data-attribute="{{key}}" data-value="{{attribute.data.value}}" src="icons/svg/d12-grey.svg" />
|
||||
<img class="attribute-roll" data-action="attributeRoll" data-attribute="{{key}}" data-value="{{attribute.value}}" src="icons/svg/d12-grey.svg" />
|
||||
<div class="attribute-text">{{key}}</div>
|
||||
</div>
|
||||
<div class="attribute-mark {{#if (and (not attribute.levelMark) (and (not (includes attribute.levelMarks ../document.system.levelData.currentLevel)) (gt ../document.system.availableAttributeMarks.length 0)))}}selectable{{/if}}" data-action="toggleAttributeMark" data-attribute="{{key}}">
|
||||
<i class="fa-solid fa-check {{#if attribute.levelMark}}selected{{/if}}"></i>
|
||||
</div>
|
||||
<div class="attribute-image">
|
||||
{{#if ../editAttributes}}
|
||||
<select class="attribute-value{{#if (lt attribute.data.base 0)}} negative{{/if}}{{#if (and (not attribute.data.base) (not ../abilityScoresFinished))}} unselected{{/if}}" data-attribute="{{key}}">
|
||||
{{#if (not (eq attribute.data.base 0))}}<option value="">{{attribute.data.base}}</option>{{/if}}
|
||||
<select class="attribute-value{{#if (lt attribute.base 0)}} negative{{/if}}{{#if (and (not attribute.base) (not ../abilityScoresFinished))}} unselected{{/if}}" data-attribute="{{key}}">
|
||||
{{#if (not (eq attribute.base 0))}}<option value="">{{attribute.base}}</option>{{/if}}
|
||||
{{#each ../abilityScoreArray as |option|}}
|
||||
<option value="{{option.value}}"{{#if (eq option.value attribute.data.base)}} selected="selected"{{/if}}>{{option.name}}</option>
|
||||
<option value="{{option.value}}"{{#if (eq option.value attribute.base)}} selected="selected"{{/if}}>{{option.name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
{{else}}
|
||||
<div class="attribute-text {{#if (lt attribute.data.value 0)}}negative{{/if}}">{{attribute.data.value}}</div>
|
||||
<div class="attribute-text {{#if (lt attribute.value 0)}}negative{{/if}}">{{attribute.value}}</div>
|
||||
{{/if}}
|
||||
<img src="systems/daggerheart/assets/AttributeShield.svg" />
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<div class="defense-row">
|
||||
<div class="defense-section">
|
||||
<div class="defense-container">
|
||||
<div class="defense-value">{{document.system.evasion}}</div>
|
||||
<div class="defense-value">{{document.system.evasion.value}}</div>
|
||||
<img src="systems/daggerheart/assets/AttributeShield.svg" />
|
||||
<div class="defense-banner">{{localize "DAGGERHEART.Sheets.PC.Defense.Evasion"}}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
<i class="fa-solid fa-caret-left"></i>
|
||||
<div class="health-category">{{localize "DAGGERHEART.Sheets.PC.Health.Severe"}}</div>
|
||||
</div>
|
||||
<i data-action="makeDeathMove" class="fas fa-skull death-save {{#if (lt resources.health.value document.system.resources.health.max)}}disabled{{/if}}" title="{{localize "DAGGERHEART.Sheets.PC.Health.DeathMoveTooltip"}}"></i>
|
||||
<i data-action="makeDeathMove" class="fas fa-skull death-save {{#if (lt resources.hitPoints.value document.system.resources.hitPoints.max)}}disabled{{/if}}" title="{{localize "DAGGERHEART.Sheets.PC.Health.DeathMoveTooltip"}}"></i>
|
||||
</div>
|
||||
<div class="flexrow" style="flex-wrap: nowrap; align-items: center;">
|
||||
<div class="flexcol flex0">
|
||||
|
|
@ -30,12 +30,12 @@
|
|||
</div>
|
||||
<div class="flexcol">
|
||||
<div class="flexrow" style="flex-wrap: nowrap;">
|
||||
{{#times document.system.resources.health.max}}
|
||||
{{#times document.system.resources.hitPoints.max}}
|
||||
{{#with (add this 1)}}
|
||||
<input class="resource-box" type="checkbox" data-action="toggleHP" data-value="{{this}}" {{ checked (gte ../../document.system.resources.health.value this) }} />
|
||||
<input class="resource-box" type="checkbox" data-action="toggleHP" data-value="{{this}}" {{ checked (gte ../../document.system.resources.hitPoints.value this) }} />
|
||||
{{/with}}
|
||||
{{/times}}
|
||||
{{#times (subtract 12 document.system.resources.health.max)}}
|
||||
{{#times (subtract 12 document.system.resources.hitPoints.max)}}
|
||||
<input class="resource-box disabled" type="checkbox" disabled />
|
||||
{{/times}}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -41,13 +41,13 @@
|
|||
<button data-action="takeLongRest" title="{{localize "DAGGERHEART.Sheets.PC.LongRest"}}"><i class="fa-solid fa-bed"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-container {{#if document.system.canLevelUp}}levelup{{/if}}">
|
||||
<div class="level-container {{#if document.system.levelData.canLevelUp}}levelup{{/if}}">
|
||||
<div class="level-value-container">
|
||||
<input class="level-value {{#if document.system.canLevelUp}}levelup{{/if}}" name="system.levelData.changedLevel" value="{{document.system.levelData.changedLevel}}" type="text" data-dtype="Number" />
|
||||
{{#if document.system.canLevelUp}}<div class="levelup-marker {{#if (gte document.system.levels.current 10)}}double-digit{{/if}}">*</div>{{/if}}
|
||||
<input class="level-value {{#if document.system.levelData.canLevelUp}}levelup{{/if}}" value="{{document.system.levelData.level.changed}}" type="text" data-dtype="Number" />
|
||||
{{#if document.system.levelData.canLevelUp}}<div class="levelup-marker">*</div>{{/if}}
|
||||
</div>
|
||||
<img src="systems/daggerheart/assets/AttributeShield.svg" />
|
||||
<div class="level-title {{#if document.system.canLevelUp}}levelup{{/if}}">{{localize "DAGGERHEART.Sheets.PC.Level"}}</div>
|
||||
<div class="level-title {{#if document.system.levelData.canLevelUp}}levelup{{/if}}">{{localize "DAGGERHEART.Sheets.PC.Level"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flexrow">
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
<div class="flex-col">
|
||||
<div class="levelup-title-container">Level {{activeLevel}}</div>
|
||||
<div class="levelup-section">
|
||||
{{#each data}}
|
||||
{{> "systems/daggerheart/templates/views/parts/level.hbs" data=this }}
|
||||
{{/each}}
|
||||
{{!-- {{#each levelupConfig as |configData key|}}
|
||||
{{> "systems/daggerheart/templates/views/parts/level.hbs" configData=configData levelData=(lookup ../levelData key) completedSelection=../completedSelection activeTier=../activeTier activeLevel=../activeLevel category=key }}
|
||||
{{/each}} --}}
|
||||
</div>
|
||||
<footer>
|
||||
|
||||
{{#if (eq activeLevel changedLevel )}}
|
||||
<button data-action="finishLevelup" {{#if (not completedSelection)}}disabled{{/if}}>{{localize "DAGGERHEART.Application.LevelUp.TakeLevelUp"}}</button>
|
||||
{{else}}
|
||||
<button data-action="advanceLevel" {{#if (not completedSelection)}}disabled{{/if}}>{{localize "DAGGERHEART.Application.LevelUp.AdvanceLevel" level=(add activeLevel 1)}}</button>
|
||||
{{/if}}
|
||||
<button data-action="close">{{localize "DAGGERHEART.Application.Cancel"}}</button>
|
||||
</footer>
|
||||
</div>
|
||||
11
templates/views/levelup/parts/selectable-card-preview.hbs
Normal file
11
templates/views/levelup/parts/selectable-card-preview.hbs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<div class="component dh-style card-preview-container selectable {{#if this.disabled}}disabled{{/if}}" data-action="selectPreview" data-path="{{this.path}}" data-uuid="{{this.uuid}}" {{#if selected}}data-selected="true"{{/if}}>
|
||||
<div class="preview-image-outer-container">
|
||||
<img class="preview-image-container" src="{{this.img}}" />
|
||||
{{#if this.selected}}
|
||||
<div class="preview-selected-icon-container">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="preview-text-container">{{this.name}}</div>
|
||||
</div>
|
||||
40
templates/views/levelup/tabs/advancements.hbs
Normal file
40
templates/views/levelup/tabs/advancements.hbs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<section
|
||||
class='tab {{tabs.advancements.cssClass}} {{tabs.advancements.id}}'
|
||||
data-tab='{{tabs.advancements.id}}'
|
||||
data-group='{{tabs.advancements.group}}'
|
||||
>
|
||||
<div class="section-container">
|
||||
<div class="tiers-container">
|
||||
{{#each this.levelup.tiersForRendering as |tier key|}}
|
||||
<fieldset class="tier-container {{#if (not tier.active)}}inactive{{/if}}">
|
||||
<legend>{{tier.name}}</legend>
|
||||
|
||||
{{#each tier.groups}}
|
||||
<div class="checkbox-group-container">
|
||||
<div class="checkboxes-container">
|
||||
{{#each this.checkboxGroups}}
|
||||
<div class="checkbox-grouping-coontainer {{#if this.multi}}multi{{/if}}">
|
||||
{{#each this.checkboxes}}
|
||||
<input
|
||||
type="checkbox" class="selection-checkbox{{#if (gt this.cost 1)}} multi{{/if}}" {{checked this.selected}} {{#if this.disabled}}disabled{{/if}}
|
||||
data-tier="{{this.tier}}"
|
||||
data-level="{{this.level}}"
|
||||
data-option="{{this.type}}"
|
||||
data-checkbox-nr="{{this.checkboxNr}}"
|
||||
data-cost="{{this.minCost}}"
|
||||
data-amount="{{this.amount}}"
|
||||
data-value="{{this.value}}"
|
||||
data-type="{{this.type}}"
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="checkbox-group-label">{{this.label}}</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</fieldset>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
95
templates/views/levelup/tabs/selections.hbs
Normal file
95
templates/views/levelup/tabs/selections.hbs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<section
|
||||
class='tab {{tabs.selections.cssClass}} {{tabs.selections.id}}'
|
||||
data-tab='{{tabs.selections.id}}'
|
||||
data-group='{{tabs.selections.group}}'
|
||||
>
|
||||
<div class="section-container levelup-selections-container">
|
||||
{{#if (gt this.newExperiences.length 0)}}
|
||||
<div>
|
||||
<h3>{{localize "DAGGERHEART.Application.LevelUp.summary.newExperiences"}}</h3>
|
||||
<div class="achievement-experience-cards">
|
||||
{{#each this.newExperiences}}
|
||||
<div class="achievement-experience-card">
|
||||
<div class="flexrow">
|
||||
<input type="text" name="{{concat "levelup.levels." this.level ".achievements.experiences." this.key ".name"}}" value="{{this.name}}" placeholder="{{localize "DAGGERHEART.Application.LevelUp.summary.experiencePlaceholder"}}" />
|
||||
<div class="flex0">{{signedNumber this.modifier}}</div>
|
||||
</div>
|
||||
<div class="achievement-experience-marker">
|
||||
{{#if this.name}}<i class="fa-solid fa-check"></i>{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.traits.active}}
|
||||
<div>
|
||||
<h3 class="levelup-selections-title">
|
||||
<div>{{localize "DAGGERHEART.Application.LevelUp.summary.traits"}}</div>
|
||||
<div>{{this.traits.progress.selected}}/{{this.traits.progress.max}}</div>
|
||||
</h3>
|
||||
|
||||
<input type="text" class="levelup-trait-increases" value="{{this.traits.values}}" />
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.experienceIncreases.active}}
|
||||
<div>
|
||||
<h3 class="levelup-selections-title">
|
||||
<div>{{localize "DAGGERHEART.Application.LevelUp.summary.experienceIncreases"}}</div>
|
||||
<div>{{this.experienceIncreases.progress.selected}}/{{this.experienceIncreases.progress.max}}</div>
|
||||
</h3>
|
||||
|
||||
<input type="text" class="levelup-experience-increases" value="{{this.experienceIncreases.values}}" />
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if (gt this.domainCards.length 0)}}
|
||||
<div>
|
||||
<h3>{{localize "DAGGERHEART.Application.LevelUp.summary.domainCards"}}</h3>
|
||||
|
||||
<div class="levelup-card-selection domain-cards">
|
||||
{{#each this.domainCards}}
|
||||
{{> "systems/daggerheart/templates/components/card-preview.hbs" this }}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if (gt this.subclassCards.length 0)}}
|
||||
<div>
|
||||
<h3>{{localize "DAGGERHEART.Application.LevelUp.summary.subclass"}}</h3>
|
||||
|
||||
<div class="levelup-card-selection subclass-cards">
|
||||
{{#each this.subclassCards}}
|
||||
{{> "systems/daggerheart/templates/views/levelup/parts/selectable-card-preview.hbs" img=this.img name=this.name path=this.path selected=this.selected uuid=this.uuid disabled=this.disabled }}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.multiclass}}
|
||||
<div>
|
||||
<h3>{{localize "DAGGERHEART.Application.LevelUp.summary.multiclass"}}</h3>
|
||||
|
||||
<div class="levelup-card-selection multiclass-cards" data-path="{{this.multiclass.path}}" data-tier="{{this.multiclass.tier}}" data-min-cost="{{this.multiclass.minCost}}" data-amount="{{this.multiclass.amount}}" data-value="{{this.multiclass.value}}" data-type="{{this.multiclass.type}}">
|
||||
{{> "systems/daggerheart/templates/components/card-preview.hbs" this.multiclass }}
|
||||
<div class="levelup-domains-selection-container">
|
||||
{{#each this.multiclass.domains}}
|
||||
<div class="levelup-domain-selection-container {{#if this.disabled}}disabled{{/if}}" {{#if (not this.disabled)}}data-action="selectDomain" data-uuid="{{../multiclass.uuid}}" data-domain="{{this.id}}" data-path="{{../multiclass.path}}" {{/if}}>
|
||||
<div class="levelup-domain-label">{{localize this.label}}</div>
|
||||
<img src="{{this.src}}" />
|
||||
{{#if this.selected}}
|
||||
<div class="levelup-domain-selected">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
||||
134
templates/views/levelup/tabs/summary.hbs
Normal file
134
templates/views/levelup/tabs/summary.hbs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<section
|
||||
class='tab {{tabs.summary.cssClass}} {{tabs.summary.id}}'
|
||||
data-tab='{{tabs.summary.id}}'
|
||||
data-group='{{tabs.summary.group}}'
|
||||
>
|
||||
<div class="section-container levelup-summary-container">
|
||||
<fieldset>
|
||||
<legend>{{localize "DAGGERHEART.Application.LevelUp.summary.levelAchievements"}}</legend>
|
||||
|
||||
<div class="level-achievements-container">
|
||||
{{#if this.achievements.proficiency.shown}}
|
||||
<div>
|
||||
<div class="increase-container">
|
||||
{{localize "DAGGERHEART.Application.LevelUp.summary.proficiencyIncrease" proficiency=this.achievements.proficiency.old }}
|
||||
<i class="fa-solid fa-arrow-right-long"></i>
|
||||
{{this.achievements.proficiency.new}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div>
|
||||
<h5 class="summary-section">{{localize "DAGGERHEART.Application.LevelUp.summary.damageThresholds"}}{{#if this.levelAchievements.damageThresholds.unarmored}}({{localize "DAGGERHEART.General.unarmored"}}){{/if}}</h5>
|
||||
<div class="increase-container">
|
||||
{{localize "DAGGERHEART.Application.LevelUp.summary.damageThresholdMajorIncrease" threshold=this.achievements.damageThresholds.major.old }}
|
||||
<i class="fa-solid fa-arrow-right-long"></i>
|
||||
{{this.achievements.damageThresholds.major.new}}
|
||||
</div>
|
||||
<div class="increase-container">
|
||||
{{localize "DAGGERHEART.Application.LevelUp.summary.damageThresholdSevereIncrease" threshold=this.achievements.damageThresholds.severe.old }}
|
||||
<i class="fa-solid fa-arrow-right-long"></i>
|
||||
{{this.achievements.damageThresholds.severe.new}}
|
||||
</div>
|
||||
</div>
|
||||
{{#if this.achievements.domainCards.shown}}
|
||||
<div>
|
||||
<h5>{{localize "DAGGERHEART.Application.LevelUp.summary.domainCards"}}</h5>
|
||||
<div class="summary-selection-container">
|
||||
{{#each this.achievements.domainCards.values}}
|
||||
<div class="summary-selection">{{this.name}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.achievements.experiences.shown}}
|
||||
<div>
|
||||
<h5>{{localize "DAGGERHEART.Application.LevelUp.summary.newExperiences"}}</h5>
|
||||
<div class="summary-selection-container">
|
||||
{{#each this.achievements.experiences.values}}
|
||||
<div class="summary-selection">{{this.name}} {{signedNumber this.modifier}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>{{localize "DAGGERHEART.Application.LevelUp.summary.levelAdvancements"}}</legend>
|
||||
|
||||
<div class="level-advancements-container">
|
||||
{{#if this.advancements.statistics.shown}}
|
||||
<div>
|
||||
{{#if this.advancements.statistics.proficiency.shown}}
|
||||
<div class="increase-container">
|
||||
{{localize "DAGGERHEART.Application.LevelUp.summary.proficiencyIncrease" proficiency=this.advancements.statistics.proficiency.old }}
|
||||
<i class="fa-solid fa-arrow-right-long"></i>
|
||||
{{this.advancements.statistics.proficiency.new}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.advancements.statistics.hitPoints.shown}}
|
||||
<div class="increase-container">
|
||||
{{localize "DAGGERHEART.Application.LevelUp.summary.hpIncrease" hitPoints=this.advancements.statistics.hitPoints.old }}
|
||||
<i class="fa-solid fa-arrow-right-long"></i>
|
||||
{{this.advancements.statistics.hitPoints.new}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.advancements.statistics.stress.shown}}
|
||||
<div class="increase-container">
|
||||
{{localize "DAGGERHEART.Application.LevelUp.summary.stressIncrease" stress=this.advancements.statistics.stress.old }}
|
||||
<i class="fa-solid fa-arrow-right-long"></i>
|
||||
{{this.advancements.statistics.stress.new}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.advancements.statistics.evasion.shown}}
|
||||
<div class="increase-container">
|
||||
{{localize "DAGGERHEART.Application.LevelUp.summary.evasionIncrease" evasion=this.advancements.statistics.evasion.old }}
|
||||
<i class="fa-solid fa-arrow-right-long"></i>
|
||||
{{this.advancements.statistics.evasion.new}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.advancements.traits}}
|
||||
<div>
|
||||
<h5>{{localize "DAGGERHEART.Application.LevelUp.summary.traits"}}</h5>
|
||||
<div class="summary-selection-container">
|
||||
{{#each this.advancements.traits}}
|
||||
<div class="summary-selection">{{this}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.advancements.domainCards}}
|
||||
<div>
|
||||
<h5>{{localize "DAGGERHEART.Application.LevelUp.summary.domainCards"}}</h5>
|
||||
<div class="summary-selection-container">
|
||||
{{#each this.advancements.domainCards}}
|
||||
<div class="summary-selection">{{this.name}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.advancements.experiences}}
|
||||
<div>
|
||||
<h5>{{localize "DAGGERHEART.Application.LevelUp.summary.experienceIncreases"}}</h5>
|
||||
<div class="summary-selection-container">
|
||||
{{#each this.advancements.experiences}}
|
||||
<div class="summary-selection">{{this.name}} {{signedNumber this.modifier}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<footer class="levelup-footer">
|
||||
<button data-action="save" {{#if (not this.levelup.allLevelsFinished)}}disabled{{/if}}>{{localize "Finish Levelup"}}</button>
|
||||
</footer>
|
||||
</div>
|
||||
</section>
|
||||
40
templates/views/levelup/tabs/tab-navigation.hbs
Normal file
40
templates/views/levelup/tabs/tab-navigation.hbs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<section class='tab-navigation'>
|
||||
<line-div></line-div>
|
||||
<div class="levelup-navigation-container">
|
||||
{{#if this.showTabs}}
|
||||
<nav class='feature-tab sheet-tabs tabs' data-group='primary'>
|
||||
{{#each tabs as |tab|}}
|
||||
{{#if (not (eq tab.id 'summary'))}}
|
||||
<div class="levelup-tab-container">
|
||||
<a class='{{tab.id}} {{tab.cssClass}}' data-action='tab' data-group='{{tab.group}}' data-tab='{{tab.id}}'>
|
||||
{{localize tab.label}}
|
||||
</a>
|
||||
{{#if tab.progress}}
|
||||
<div>{{tab.progress.selected}}/{{tab.progress.max}}</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</nav>
|
||||
{{/if}}
|
||||
<div class="levelup-navigation-actions {{#if (not this.showTabs)}}test{{/if}}">
|
||||
{{#if this.navigate.previous.fromSummary}}
|
||||
<button data-action="activatePart" data-part="advancements">{{localize "DAGGERHEART.Application.LevelUp.navigateToLevelup"}}</button>
|
||||
{{else}}
|
||||
{{#if (not this.navigate.previous.disabled)}}
|
||||
<button data-action="updateCurrentLevel" >{{this.navigate.previous.label}}</button>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if this.navigate.next.show}}
|
||||
{{#if this.navigate.next.toSummary}}
|
||||
<button data-action="activatePart" data-part="summary" {{#if this.navigate.next.disabled}}disabled{{/if}}>{{localize "DAGGERHEART.Application.LevelUp.navigateToSummary"}}</button>
|
||||
{{else}}
|
||||
<button data-action="updateCurrentLevel" data-forward="true" {{#if this.navigate.next.disabled}}disabled{{/if}}>{{this.navigate.next.label}}</button>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div></div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<line-div></line-div>
|
||||
</section>
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
<div class="levelup-container {{#if (not data.active)}}disabled{{/if}}">
|
||||
<fieldset class="levelup-inner-container">
|
||||
<legend class="levelup-legend">
|
||||
{{data.label}}
|
||||
</legend>
|
||||
|
||||
<div class="levelup-info">{{data.info}}</div>
|
||||
<div class="levelup-pretext">{{data.pretext}}</div>
|
||||
<div class="levelup-body">
|
||||
{{#each data.choices as |choice choiceKey|}}
|
||||
<div class="levelup-choice-row">
|
||||
<div class="levelup-choice-row-inner">
|
||||
{{#each choice.values as |value valueKey|}}
|
||||
{{#times choice.cost}}
|
||||
<div class="levelup-choice-input-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
data-action="toggleBox"
|
||||
data-path="{{../path}}"
|
||||
data-level-attribute="{{@../key}}"
|
||||
{{checked ../selected}}
|
||||
{{#if ../disabled}}disabled{{/if}}
|
||||
/>
|
||||
{{#if (lt (add this 1) ../../cost)}}
|
||||
<i class="fa-solid fa-link"></i>
|
||||
{{/if}}
|
||||
{{#if ../locked}}
|
||||
<i class="fa-solid fa-lock"></i>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/times}}
|
||||
{{/each}}
|
||||
</div>
|
||||
<div>{{localize choice.description}}</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="levelup-posttext">{{data.posttext}}</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
|
@ -1,52 +1,47 @@
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
import readline from "readline";
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import readline from 'readline';
|
||||
|
||||
console.log("Reforging Symlinks");
|
||||
console.log('Reforging Symlinks');
|
||||
|
||||
const askQuestion = (question) => {
|
||||
const askQuestion = question => {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
return new Promise((resolve) =>
|
||||
rl.question(question, (answer) => {
|
||||
return new Promise(resolve =>
|
||||
rl.question(question, answer => {
|
||||
rl.close();
|
||||
resolve(answer);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const installPath = await askQuestion("Enter your Foundry install path: ");
|
||||
const installPath = await askQuestion('Enter your Foundry install path: ');
|
||||
|
||||
// Determine if it's an Electron install (nested structure)
|
||||
const nested = fs.existsSync(path.join(installPath, "resources", "app"));
|
||||
const fileRoot = nested
|
||||
? path.join(installPath, "resources", "app")
|
||||
: installPath;
|
||||
const nested = fs.existsSync(path.join(installPath, 'resources', 'app'));
|
||||
const fileRoot = nested ? path.join(installPath, 'resources', 'app') : installPath;
|
||||
|
||||
try {
|
||||
await fs.promises.mkdir("foundry");
|
||||
await fs.promises.mkdir('foundry');
|
||||
} catch (e) {
|
||||
if (e.code !== "EEXIST") throw e;
|
||||
if (e.code !== 'EEXIST') throw e;
|
||||
}
|
||||
|
||||
// JavaScript files
|
||||
for (const p of ["client", "common", "tsconfig.json"]) {
|
||||
for (const p of ['client', 'common', 'tsconfig.json']) {
|
||||
try {
|
||||
await fs.promises.symlink(path.join(fileRoot, p), path.join("foundry", p));
|
||||
await fs.promises.symlink(path.join(fileRoot, p), path.join('foundry', p));
|
||||
} catch (e) {
|
||||
if (e.code !== "EEXIST") throw e;
|
||||
if (e.code !== 'EEXIST') throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Language files
|
||||
try {
|
||||
await fs.promises.symlink(
|
||||
path.join(fileRoot, "public", "lang"),
|
||||
path.join("foundry", "lang")
|
||||
);
|
||||
await fs.promises.symlink(path.join(fileRoot, 'public', 'lang'), path.join('foundry', 'lang'));
|
||||
} catch (e) {
|
||||
if (e.code !== "EEXIST") throw e;
|
||||
if (e.code !== 'EEXIST') throw e;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue