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:
WBHarry 2025-06-07 01:50:50 +02:00 committed by GitHub
parent 47a6abddfb
commit a92221778e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 3279 additions and 1283 deletions

View file

@ -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'
]);

View file

@ -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",

File diff suppressed because it is too large Load diff

View file

@ -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'),

View file

@ -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');

View file

@ -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();

View file

@ -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
}
};

View file

@ -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
View 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
View 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
};
}
}

View file

@ -1,50 +1,29 @@
import { getPathValue, getTier } from '../helpers/utils.mjs';
import { getPathValue } from '../helpers/utils.mjs';
import { LevelOptionType } from './levelTier.mjs';
const fields = foundry.data.fields;
const attributeField = () =>
new fields.SchemaField({
data: new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true }),
base: new fields.NumberField({ initial: 0, integer: true }),
bonus: new fields.NumberField({ initial: 0, integer: true }),
actualValue: new fields.NumberField({ initial: 0, integer: true }),
overrideValue: new fields.NumberField({ initial: 0, integer: true })
}),
levelMarks: new fields.ArrayField(new fields.NumberField({ nullable: true, initial: null, integer: true })),
levelMark: new fields.NumberField({ nullable: true, initial: null, integer: true })
bonus: new fields.NumberField({ initial: 0, integer: true }),
base: new fields.NumberField({ initial: 0, integer: true }),
tierMarked: new fields.BooleanField({ required: true, initial: false })
});
const levelUpTier = () => ({
attributes: new fields.TypedObjectField(new fields.BooleanField()),
hitPointSlots: new fields.TypedObjectField(new fields.BooleanField()),
stressSlots: new fields.TypedObjectField(new fields.BooleanField()),
experiences: new fields.TypedObjectField(new fields.ArrayField(new fields.StringField({}))),
proficiency: new fields.TypedObjectField(new fields.BooleanField()),
armorOrEvasionSlot: new fields.TypedObjectField(new fields.StringField({})),
subclass: new fields.TypedObjectField(
new fields.SchemaField({
multiclass: new fields.BooleanField(),
feature: new fields.StringField({})
})
),
multiclass: new fields.TypedObjectField(new fields.BooleanField())
});
const resourceField = max =>
new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true }),
bonus: new fields.NumberField({ initial: 0, integer: true }),
min: new fields.NumberField({ initial: 0, integer: true }),
baseMax: new fields.NumberField({ initial: max, integer: true })
});
export default class DhpPC extends foundry.abstract.TypeDataModel {
static defineSchema() {
return {
resources: new fields.SchemaField({
health: new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true }),
min: new fields.NumberField({ initial: 0, integer: true }),
max: new fields.NumberField({ initial: 6, integer: true })
}),
stress: new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true }),
min: new fields.NumberField({ initial: 0, integer: true }),
max: new fields.NumberField({ initial: 6, integer: true })
}),
hitPoints: resourceField(6),
stress: resourceField(6),
hope: new fields.SchemaField({
value: new fields.NumberField({ initial: -1, integer: true }), // FIXME. Logic is gte and needs -1 in PC/Hope. Change to 0
min: new fields.NumberField({ initial: 0, integer: true })
@ -61,7 +40,7 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
})
)
}),
attributes: new fields.SchemaField({
traits: new fields.SchemaField({
agility: attributeField(),
strength: attributeField(),
finesse: attributeField(),
@ -70,22 +49,22 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
knowledge: attributeField()
}),
proficiency: new fields.SchemaField({
value: new fields.NumberField({ initial: 1, integer: true }),
min: new fields.NumberField({ initial: 1, integer: true }),
max: new fields.NumberField({ initial: 6, integer: true })
base: new fields.NumberField({ required: true, initial: 1, integer: true }),
bonus: new fields.NumberField({ required: true, initial: 0, integer: true })
}),
evasion: new fields.SchemaField({
bonus: new fields.NumberField({ initial: 0, integer: true })
}),
evasion: new fields.NumberField({ initial: 0, integer: true }),
experiences: new fields.ArrayField(
new fields.SchemaField({
id: new fields.StringField({ required: true }),
level: new fields.NumberField({ required: true, integer: true }),
description: new fields.StringField({}),
value: new fields.NumberField({ integer: true, nullable: true, initial: null })
}),
{
initial: [
{ id: foundry.utils.randomID(), level: 1, description: '', value: 2 },
{ id: foundry.utils.randomID(), level: 1, description: '', value: 2 }
{ id: foundry.utils.randomID(), description: '', value: 2 },
{ id: foundry.utils.randomID(), description: '', value: 2 }
]
}
),
@ -100,30 +79,6 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
maxLoadout: new fields.NumberField({ initial: 2, integer: true }),
maxCards: new fields.NumberField({ initial: 2, integer: true })
}),
levelData: new fields.SchemaField({
currentLevel: new fields.NumberField({ initial: 1, integer: true }),
changedLevel: new fields.NumberField({ initial: 1, integer: true }),
levelups: new fields.TypedObjectField(
new fields.SchemaField({
level: new fields.NumberField({ required: true, integer: true }),
tier1: new fields.SchemaField({
...levelUpTier()
}),
tier2: new fields.SchemaField(
{
...levelUpTier()
},
{ nullable: true, initial: null }
),
tier3: new fields.SchemaField(
{
...levelUpTier()
},
{ nullable: true, initial: null }
)
})
)
}),
story: new fields.SchemaField({
background: new fields.HTMLField(),
appearance: new fields.HTMLField(),
@ -140,15 +95,11 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
armorMarks: new fields.SchemaField({
max: new fields.NumberField({ initial: 6, integer: true }),
value: new fields.NumberField({ initial: 0, integer: true })
})
}),
levelData: new fields.EmbeddedDataField(DhPCLevelData)
};
}
get canLevelUp() {
// return Object.values(this.levels.data).some(x => !x.completed);
return this.levelData.currentLevel !== this.levelData.changedLevel;
}
get tier() {
return this.#getTier(this.levelData.currentLevel);
}
@ -281,42 +232,6 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
}
}
get inventoryWeapons() {
const inventoryWeaponFirst = this.parent.items.find(x => x.type === 'weapon' && x.system.inventoryWeapon === 1);
const inventoryWeaponSecond = this.parent.items.find(
x => x.type === 'weapon' && x.system.inventoryWeapon === 2
);
return {
first: this.#weaponData(inventoryWeaponFirst),
second: this.#weaponData(inventoryWeaponSecond)
};
}
get totalAttributeMarks() {
return Object.keys(this.levelData.levelups).reduce((nr, level) => {
const nrAttributeMarks = Object.keys(this.levelData.levelups[level]).reduce((nr, tier) => {
nr += Object.keys(this.levelData.levelups[level][tier]?.attributes ?? {}).length * 2;
return nr;
}, 0);
nr.push(...Array(nrAttributeMarks).fill(Number.parseInt(level)));
return nr;
}, []);
}
get availableAttributeMarks() {
const attributeMarks = Object.keys(this.attributes).flatMap(y => this.attributes[y].levelMarks);
return this.totalAttributeMarks.reduce((acc, attribute) => {
if (!attributeMarks.findSplice(x => x === attribute)) {
acc.push(attribute);
}
return acc;
}, []);
}
get effects() {
return this.parent.items.reduce((acc, item) => {
const effects = item.system.effectData;
@ -367,141 +282,37 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
: null;
}
prepareBaseData() {
this.resources.hitPoints.max = this.resources.hitPoints.baseMax + this.resources.hitPoints.bonus;
this.resources.stress.max = this.resources.stress.baseMax + this.resources.stress.bonus;
this.evasion.value = (this.class?.system?.evasion ?? 0) + this.evasion.bonus;
this.proficiency.value = this.proficiency.base + this.proficiency.bonus;
for (var attributeKey in this.traits) {
const attribute = this.traits[attributeKey];
attribute.value = attribute.base + attribute.bonus;
}
}
prepareDerivedData() {
this.resources.hope.max = 6 - this.story.scars.length;
if (this.resources.hope.value >= this.resources.hope.max) {
this.resources.hope.value = Math.max(this.resources.hope.max - 1, 0);
}
for (var attributeKey in this.attributes) {
const attribute = this.attributes[attributeKey];
const armor = this.armor;
this.damageThresholds = {
major: armor
? armor.system.baseThresholds.major + this.levelData.level.current
: this.levelData.level.current,
severe: armor
? armor.system.baseThresholds.severe + this.levelData.level.current
: this.levelData.level.current * 2
};
attribute.levelMark = attribute.levelMarks.find(x => this.isSameTier(x)) ?? null;
const actualValue = attribute.data.base + attribute.levelMarks.length + attribute.data.bonus;
attribute.data.actualValue = actualValue;
attribute.data.value = attribute.data.overrideValue
? attribute.data.overrideValue
: attribute.data.actualValue;
}
this.evasion = this.class?.system?.evasion ?? 0;
// this.armor.value = this.activeArmor?.baseScore ?? 0;
this.damageThresholds = this.computeDamageThresholds();
this.applyLevels();
this.applyEffects();
}
computeDamageThresholds() {
// TODO: missing weapon features and domain cards calculation
if (!this.armor) {
return {
major: this.levelData.currentLevel,
severe: this.levelData.currentLevel * 2
};
}
const {
baseThresholds: { major = 0, severe = 0 }
} = this.armor.system;
return {
major: major + this.levelData.currentLevel,
severe: severe + this.levelData.currentLevel
};
}
applyLevels() {
let healthBonus = 0,
stressBonus = 0,
proficiencyBonus = 0,
evasionBonus = 0,
armorBonus = 0;
let experienceBonuses = {};
let advancementFirst = null,
advancementSecond = null;
for (var level in this.levelData.levelups) {
var levelData = this.levelData.levelups[level];
for (var tier in levelData) {
var tierData = levelData[tier];
if (tierData) {
healthBonus += Object.keys(tierData.hitPointSlots).length;
stressBonus += Object.keys(tierData.stressSlots).length;
proficiencyBonus += Object.keys(tierData.proficiency).length;
advancementFirst =
Object.keys(tierData.subclass).length > 0 && level >= 5 && level <= 7
? { ...tierData.subclass[0], tier: getTier(Number.parseInt(level), true) }
: advancementFirst;
advancementSecond =
Object.keys(tierData.subclass).length > 0 && level >= 8 && level <= 10
? { ...tierData.subclass[0], tier: getTier(Number.parseInt(level), true) }
: advancementSecond;
for (var index in Object.keys(tierData.experiences)) {
for (var experienceKey in tierData.experiences[index]) {
var experience = tierData.experiences[index][experienceKey];
experienceBonuses[experience] = experienceBonuses[experience]
? experienceBonuses[experience] + 1
: 1;
}
}
evasionBonus += Object.keys(tierData.armorOrEvasionSlot).filter(
x => tierData.armorOrEvasionSlot[x] === 'evasion'
).length;
armorBonus += Object.keys(tierData.armorOrEvasionSlot).filter(
x => tierData.armorOrEvasionSlot[x] === 'armor'
).length;
}
}
}
this.resources.health.max += healthBonus;
this.resources.stress.max += stressBonus;
this.proficiency.value += proficiencyBonus;
this.evasion += evasionBonus;
this.armorMarks = {
max: this.armor ? this.armor.system.marks.max + armorBonus : 0,
value: this.armor ? this.armor.system.marks.value : 0
};
this.experiences = this.experiences.map(x => ({ ...x, value: x.value + (experienceBonuses[x.id] ?? 0) }));
const subclassFeatures = this.subclassFeatures;
if (advancementFirst) {
if (advancementFirst.multiclass) {
this.multiclassSubclass.system[`${advancementFirst.feature}Feature`].unlocked = true;
this.multiclassSubclass.system[`${advancementFirst.feature}Feature`].tier = advancementFirst.tier;
subclassFeatures.multiclassSubclass[advancementFirst.feature].forEach(x => (x.system.disabled = false));
} else {
this.subclass.system[`${advancementFirst.feature}Feature`].unlocked = true;
this.subclass.system[`${advancementFirst.feature}Feature`].tier = advancementFirst.tier;
subclassFeatures.subclass[advancementFirst.feature].forEach(x => (x.system.disabled = false));
}
}
if (advancementSecond) {
if (advancementSecond.multiclass) {
this.multiclassSubclass.system[`${advancementSecond.feature}Feature`].unlocked = true;
this.multiclassSubclass.system[`${advancementSecond.feature}Feature`].tier = advancementSecond.tier;
subclassFeatures.multiclassSubclass[advancementSecond.feature].forEach(
x => (x.system.disabled = false)
);
} else {
this.subclass.system[`${advancementSecond.feature}Feature`].unlocked = true;
this.subclass.system[`${advancementSecond.feature}Feature`].tier = advancementSecond.tier;
subclassFeatures.subclass[advancementSecond.feature].forEach(x => (x.system.disabled = false));
}
}
//General progression
for (var i = 0; i < this.levelData.currentLevel; i++) {
const tier = getTier(i + 1);
if (tier !== 'tier0') {
this.domainData.maxLoadout = Math.min(this.domainData.maxLoadout + 1, 5);
this.domainData.maxCards += 1;
}
}
}
applyEffects() {
const effects = this.effects;
for (var key in effects) {
@ -509,10 +320,10 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
for (var effect of effectType) {
switch (key) {
case SYSTEM.EFFECTS.effectTypes.health.id:
this.resources.health.max += effect.value.valueData.value;
this.resources.hitPoints.bonus += effect.value.valueData.value;
break;
case SYSTEM.EFFECTS.effectTypes.stress.id:
this.resources.stress.max += effect.value.valueData.value;
this.resources.stress.bonus += effect.value.valueData.value;
break;
case SYSTEM.EFFECTS.effectTypes.damage.id:
this.bonuses.damage.push({
@ -539,10 +350,6 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
return twoHanded ? 'twoHanded' : oneHanded ? 'oneHanded' : null;
}
isSameTier(level) {
return this.#getTier(this.levelData.currentLevel) === this.#getTier(level);
}
#getTier(level) {
if (level >= 8) return 3;
else if (level >= 5) return 2;
@ -550,3 +357,55 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
else return 0;
}
}
class DhPCLevelData extends foundry.abstract.DataModel {
static defineSchema() {
return {
level: new fields.SchemaField({
current: new fields.NumberField({ required: true, integer: true, initial: 1 }),
changed: new fields.NumberField({ required: true, integer: true, initial: 1 })
}),
levelups: new fields.TypedObjectField(
new fields.SchemaField({
achievements: new fields.SchemaField(
{
experiences: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({ required: true }),
modifier: new fields.NumberField({ required: true, integer: true })
})
),
domainCards: new fields.ArrayField(
new fields.SchemaField({
uuid: new fields.StringField({ required: true }),
itemUuid: new fields.StringField({ required: true })
})
),
proficiency: new fields.NumberField({ integer: true })
},
{ nullable: true, initial: null }
),
selections: new fields.ArrayField(
new fields.SchemaField({
tier: new fields.NumberField({ required: true, integer: true }),
level: new fields.NumberField({ required: true, integer: true }),
optionKey: new fields.StringField({ required: true }),
type: new fields.StringField({ required: true, choices: LevelOptionType }),
checkboxNr: new fields.NumberField({ required: true, integer: true }),
value: new fields.NumberField({ integer: true }),
minCost: new fields.NumberField({ integer: true }),
amount: new fields.NumberField({ integer: true }),
data: new fields.ArrayField(new fields.StringField({ required: true })),
secondaryData: new fields.StringField(),
itemUuid: new fields.StringField({ required: true })
})
)
})
)
};
}
get canLevelUp() {
return this.level.current < this.level.changed;
}
}

View file

@ -6,13 +6,16 @@ import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
export default class DhpActor extends Actor {
async _preCreate(data, options, user) {
if ( (await super._preCreate(data, options, user)) === false ) return false;
if ((await super._preCreate(data, options, user)) === false) return false;
// Configure prototype token settings
const prototypeToken = {};
if ( this.type === "pc" ) Object.assign(prototypeToken, {
sight: { enabled: true }, actorLink: true, disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY
});
if (this.type === 'pc')
Object.assign(prototypeToken, {
sight: { enabled: true },
actorLink: true,
disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY
});
this.updateSource({ prototypeToken });
}
@ -21,46 +24,103 @@ export default class DhpActor extends Actor {
}
async _preUpdate(changed, options, user) {
//Level Down
if (
changed.system?.levelData?.changedLevel &&
this.system.levelData.currentLevel > changed.system.levelData.changedLevel
) {
changed.system.levelData.currentLevel = changed.system.levelData.changedLevel;
changed.system.levelData.levelups = Object.keys(this.system.levelData.levelups).reduce((acc, x) => {
if (x > changed.system.levelData.currentLevel) {
acc[`-=${x}`] = null;
super._preUpdate(changed, options, user);
}
async updateLevel(newLevel) {
if (this.type !== 'pc' || newLevel === this.system.levelData.level.changed) return;
if (newLevel > this.system.levelData.level.current) {
await this.update({ 'system.levelData.level.changed': newLevel });
} else {
const updatedLevelups = Object.keys(this.system.levelData.levelups).reduce((acc, level) => {
if (Number(level) > newLevel) acc[`-=${level}`] = null;
return acc;
}, {});
const domainCards = Object.keys(this.system.levelData.levelups)
.filter(x => x > newLevel)
.flatMap(levelKey => {
const level = this.system.levelData.levelups[levelKey];
const achievementCards = level.achievements.domainCards.map(x => x.itemUuid);
const advancementCards = level.selections.filter(x => x.type === 'domainCard').map(x => x.itemUuid);
return [...achievementCards, ...advancementCards];
});
for (var domainCard of domainCards) {
const itemCard = await this.items.find(x => x.uuid === domainCard);
itemCard.delete();
}
await this.update({
system: {
levelData: {
level: {
current: newLevel,
changed: newLevel
},
levelups: updatedLevelups
}
}
});
}
}
return acc;
}, {});
async levelUp(levelupData) {
const levelups = {};
for (var levelKey of Object.keys(levelupData)) {
const level = levelupData[levelKey];
const achievementDomainCards = [];
for (var card of Object.values(level.achievements.domainCards)) {
const item = await foundry.utils.fromUuid(card.uuid);
const embeddedItem = await this.createEmbeddedDocuments('Item', [item.toObject()]);
card.itemUuid = embeddedItem[0].uuid;
achievementDomainCards.push(card);
}
changed.system.attributes = Object.keys(this.system.attributes).reduce((acc, key) => {
acc[key] = {
levelMarks: this.system.attributes[key].levelMarks.filter(
x => x <= changed.system.levelData.currentLevel
)
};
const selections = [];
for (var optionKey of Object.keys(level.choices)) {
const selection = level.choices[optionKey];
for (var checkboxNr of Object.keys(selection)) {
const checkbox = selection[checkboxNr];
let itemUuid = null;
return acc;
}, {});
if (checkbox.type === 'domainCard') {
const item = await foundry.utils.fromUuid(checkbox.data[0]);
const embeddedItem = await this.createEmbeddedDocuments('Item', [item.toObject()]);
itemUuid = embeddedItem[0].uuid;
}
changed.system.experiences = this.system.experiences.filter(
x => x.level <= changed.system.levelData.currentLevel
);
if (
this.system.multiclass &&
this.system.multiclass.system.multiclass > changed.system.levelData.changedLevel
) {
const multiclassFeatures = this.items.filter(x => x.system.multiclass);
for (var feature of multiclassFeatures) {
await feature.delete();
selections.push({
...checkbox,
level: Number(levelKey),
optionKey: optionKey,
checkboxNr: Number(checkboxNr),
itemUuid
});
}
}
levelups[levelKey] = {
achievements: {
...level.achievements,
domainCards: achievementDomainCards
},
selections: selections
};
}
super._preUpdate(changed, options, user);
await this.update({
system: {
levelData: {
level: {
current: this.system.levelData.level.changed
},
levelups: levelups
}
}
});
}
async diceRoll(modifier, shiftKey) {
@ -286,9 +346,9 @@ export default class DhpActor extends Actor {
: 0;
const update = {
'system.resources.health.value': Math.min(
this.system.resources.health.value + hpDamage,
this.system.resources.health.max
'system.resources.hitPoints.value': Math.min(
this.system.resources.hitPoints.value + hpDamage,
this.system.resources.hitPoints.max
)
};
@ -311,9 +371,9 @@ export default class DhpActor extends Actor {
switch (type) {
case SYSTEM.GENERAL.healingTypes.health.id:
update = {
'system.resources.health.value': Math.min(
this.system.resources.health.value + healing,
this.system.resources.health.max
'system.resources.hitPoints.value': Math.min(
this.system.resources.hitPoints.value + healing,
this.system.resources.hitPoints.max
)
};
break;

View file

@ -3,22 +3,19 @@ import { getWidthOfText } from './utils.mjs';
export default class RegisterHandlebarsHelpers {
static registerHelpers() {
Handlebars.registerHelper({
looseEq: this.looseEq,
times: this.times,
join: this.join,
add: this.add,
subtract: this.subtract,
objectSelector: this.objectSelector,
includes: this.includes,
simpleEditor: this.simpleEditor,
debug: this.debug
debug: this.debug,
signedNumber: this.signedNumber,
switch: this.switch,
case: this.case
});
}
static looseEq(a, b) {
return a == b;
}
static times(nr, block) {
var accum = '';
for (var i = 0; i < nr; ++i) accum += block.fn(i);
@ -77,33 +74,25 @@ export default class RegisterHandlebarsHelpers {
return new Handlebars.SafeString(html);
}
static rangePicker(options) {
let { name, value, min, max, step } = options.hash;
name = name || 'range';
value = value ?? '';
if (Number.isNaN(value)) value = '';
const html = `<input type="range" name="${name}" value="${value}" min="${min}" max="${max}" step="${step}"/>
<span class="range-value">${value}</span>`;
return new Handlebars.SafeString(html);
}
static includes(list, item) {
return list.includes(item);
}
static simpleEditor(content, options) {
const {
target,
editable = true,
button,
engine = 'tinymce',
collaborate = false,
class: cssClass
} = options.hash;
const config = { name: target, value: content, button, collaborate, editable, engine };
const element = foundry.applications.fields.createEditorInput(config);
if (cssClass) element.querySelector('.editor-content').classList.add(cssClass);
return new Handlebars.SafeString(element.outerHTML);
static signedNumber(number) {
return number >= 0 ? `+${number}` : number;
}
static switch(value, options) {
this.switch_value = value;
this.switch_break = false;
return options.fn(this);
}
static case(value, options) {
if (value == this.switch_value) {
this.switch_break = true;
return options.fn(this);
}
}
static debug(a) {

View file

@ -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;
}, {});
};

View file

@ -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;

View file

@ -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';

View file

@ -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;
}
}
}

View file

@ -1,51 +1,51 @@
@import '../utils/colors.less';
@import '../utils/fonts.less';
.sheet.daggerheart.dh-style.item {
.tab.features {
padding: 0 10px;
max-height: 265px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: light-dark(@dark-blue, @golden) transparent;
.feature-list {
display: flex;
flex-direction: column;
list-style: none;
padding: 0;
margin: 0;
width: 100%;
.feature-item {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0px;
}
.feature-line {
display: grid;
align-items: center;
grid-template-columns: 1fr 4fr 1fr;
h4 {
font-family: @font-body;
font-weight: lighter;
color: light-dark(@dark, @beige);
}
.image {
height: 40px;
width: 40px;
object-fit: cover;
border-radius: 6px;
border: none;
}
.controls {
display: flex;
justify-content: center;
gap: 10px;
a {
text-shadow: none;
}
}
}
}
}
}
}
@import '../utils/colors.less';
@import '../utils/fonts.less';
.sheet.daggerheart.dh-style.item {
.tab.features {
padding: 0 10px;
max-height: 265px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: light-dark(@dark-blue, @golden) transparent;
.feature-list {
display: flex;
flex-direction: column;
list-style: none;
padding: 0;
margin: 0;
width: 100%;
.feature-item {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0px;
}
.feature-line {
display: grid;
align-items: center;
grid-template-columns: 1fr 4fr 1fr;
h4 {
font-family: @font-body;
font-weight: lighter;
color: light-dark(@dark, @beige);
}
.image {
height: 40px;
width: 40px;
object-fit: cover;
border-radius: 6px;
border: none;
}
.controls {
display: flex;
justify-content: center;
gap: 10px;
a {
text-shadow: none;
}
}
}
}
}
}
}

View file

@ -1,152 +1,152 @@
@import '../utils/colors.less';
.application.sheet.daggerheart.dh-style {
.item-sheet-header {
display: flex;
.profile {
height: 150px;
width: 150px;
object-fit: cover;
border-right: 1px solid light-dark(@dark-blue, @golden);
border-bottom: 1px solid light-dark(@dark-blue, @golden);
box-sizing: border-box;
cursor: pointer;
}
.item-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
margin-top: 36px;
text-align: center;
width: 80%;
.item-name input[type='text'] {
font-size: 32px;
height: 42px;
text-align: center;
width: 90%;
transition: all 0.3s ease;
outline: 2px solid transparent;
border: 1px solid transparent;
&:hover[type='text'],
&:focus[type='text'] {
box-shadow: none;
outline: 2px solid light-dark(@dark-blue, @golden);
}
}
.item-description {
display: flex;
flex-direction: column;
gap: 10px;
}
h3 {
font-size: 1rem;
}
}
}
.item-card-header {
display: flex;
flex-direction: column;
justify-content: start;
text-align: center;
.profile {
height: 300px;
width: 100%;
object-fit: cover;
mask-image: linear-gradient(0deg, transparent 0%, black 10%);
cursor: pointer;
}
.item-icons-list {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
top: 50px;
right: 10px;
.item-icon {
display: flex;
align-items: center;
justify-content: end;
text-align: center;
padding-right: 8px;
max-width: 50px;
height: 50px;
font-size: 1.2rem;
background: light-dark(@light-black, @semi-transparent-dark-blue);
border: 4px double light-dark(@beige, @golden);
color: light-dark(@beige, @golden);
border-radius: 999px;
transition: all 0.3s ease;
.recall-label {
font-size: 14px;
opacity: 0;
margin-right: 0.3rem;
transition: all 0.3s ease;
}
i {
font-size: 0.8rem;
}
&:hover {
max-width: 300px;
padding: 0 10px;
border-radius: 60px;
.recall-label {
opacity: 1;
}
}
}
}
.item-info {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
top: -25px;
gap: 5px;
margin-bottom: -20px;
.item-name {
input[type='text'] {
font-size: 32px;
height: 42px;
text-align: center;
width: 90%;
transition: all 0.3s ease;
outline: 2px solid transparent;
border: 1px solid transparent;
&:hover[type='text'],
&:focus[type='text'] {
box-shadow: none;
outline: 2px solid light-dark(@dark-blue, @golden);
}
}
}
.item-description {
display: flex;
flex-direction: column;
gap: 10px;
}
h3 {
font-size: 1rem;
}
}
}
}
@import '../utils/colors.less';
.application.sheet.daggerheart.dh-style {
.item-sheet-header {
display: flex;
.profile {
height: 150px;
width: 150px;
object-fit: cover;
border-right: 1px solid light-dark(@dark-blue, @golden);
border-bottom: 1px solid light-dark(@dark-blue, @golden);
box-sizing: border-box;
cursor: pointer;
}
.item-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
margin-top: 36px;
text-align: center;
width: 80%;
.item-name input[type='text'] {
font-size: 32px;
height: 42px;
text-align: center;
width: 90%;
transition: all 0.3s ease;
outline: 2px solid transparent;
border: 1px solid transparent;
&:hover[type='text'],
&:focus[type='text'] {
box-shadow: none;
outline: 2px solid light-dark(@dark-blue, @golden);
}
}
.item-description {
display: flex;
flex-direction: column;
gap: 10px;
}
h3 {
font-size: 1rem;
}
}
}
.item-card-header {
display: flex;
flex-direction: column;
justify-content: start;
text-align: center;
.profile {
height: 300px;
width: 100%;
object-fit: cover;
mask-image: linear-gradient(0deg, transparent 0%, black 10%);
cursor: pointer;
}
.item-icons-list {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
top: 50px;
right: 10px;
.item-icon {
display: flex;
align-items: center;
justify-content: end;
text-align: center;
padding-right: 8px;
max-width: 50px;
height: 50px;
font-size: 1.2rem;
background: light-dark(@light-black, @semi-transparent-dark-blue);
border: 4px double light-dark(@beige, @golden);
color: light-dark(@beige, @golden);
border-radius: 999px;
transition: all 0.3s ease;
.recall-label {
font-size: 14px;
opacity: 0;
margin-right: 0.3rem;
transition: all 0.3s ease;
}
i {
font-size: 0.8rem;
}
&:hover {
max-width: 300px;
padding: 0 10px;
border-radius: 60px;
.recall-label {
opacity: 1;
}
}
}
}
.item-info {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
top: -25px;
gap: 5px;
margin-bottom: -20px;
.item-name {
input[type='text'] {
font-size: 32px;
height: 42px;
text-align: center;
width: 90%;
transition: all 0.3s ease;
outline: 2px solid transparent;
border: 1px solid transparent;
&:hover[type='text'],
&:focus[type='text'] {
box-shadow: none;
outline: 2px solid light-dark(@dark-blue, @golden);
}
}
}
.item-description {
display: flex;
flex-direction: column;
gap: 10px;
}
h3 {
font-size: 1rem;
}
}
}
}

View file

@ -1,80 +1,80 @@
@import '../utils/colors.less';
@import '../utils/fonts.less';
.application.sheet.dh-style .window-header {
background: transparent;
border-bottom: none;
justify-content: end;
h1 {
color: light-dark(@dark-blue, @beige);
font-family: @font-body;
}
button {
background: light-dark(transparent, @deep-black);
color: light-dark(@dark-blue, @beige);
border: 1px solid light-dark(@dark-blue, transparent);
padding: 0;
&:hover {
border: 1px solid light-dark(@dark-blue, @golden);
color: light-dark(@dark-blue, @golden);
}
}
}
.application.sheet.dh-style:not(.minimized) {
.window-title,
.window-icon {
display: none;
opacity: 0;
transition: opacity 0.3s ease;
}
}
.application.sheet.dh-style.minimized {
.window-content {
display: none;
opacity: 0;
transition: opacity 0.1s ease;
}
}
.application.sheet.dh-style:not(.minimized) {
.window-content {
opacity: 1;
transition: opacity 0.3s ease;
}
}
.application.sheet.dh-style .window-content {
overflow: initial;
backdrop-filter: none;
padding: 0;
}
.theme-dark {
.application.sheet.dh-style {
backdrop-filter: blur(4px);
}
}
.theme-light {
.application.sheet.dh-style {
background-image: url('../assets/parchments/dh-parchment-light.png');
background-repeat: no-repeat;
background-position: center;
}
}
.application.sheet.daggerheart.dh-style {
.window-content {
position: relative;
top: -36px;
.tab {
padding: 0 10px;
}
}
}
@import '../utils/colors.less';
@import '../utils/fonts.less';
.application.sheet.dh-style .window-header {
background: transparent;
border-bottom: none;
justify-content: end;
h1 {
color: light-dark(@dark-blue, @beige);
font-family: @font-body;
}
button {
background: light-dark(transparent, @deep-black);
color: light-dark(@dark-blue, @beige);
border: 1px solid light-dark(@dark-blue, transparent);
padding: 0;
&:hover {
border: 1px solid light-dark(@dark-blue, @golden);
color: light-dark(@dark-blue, @golden);
}
}
}
.application.sheet.dh-style:not(.minimized) {
.window-title,
.window-icon {
display: none;
opacity: 0;
transition: opacity 0.3s ease;
}
}
.application.sheet.dh-style.minimized {
.window-content {
display: none;
opacity: 0;
transition: opacity 0.1s ease;
}
}
.application.sheet.dh-style:not(.minimized) {
.window-content {
opacity: 1;
transition: opacity 0.3s ease;
}
}
.application.sheet.dh-style .window-content {
overflow: initial;
backdrop-filter: none;
padding: 0;
}
.theme-dark {
.application.sheet.dh-style {
backdrop-filter: blur(4px);
}
}
.theme-light {
.application.sheet.dh-style {
background-image: url('../assets/parchments/dh-parchment-light.png');
background-repeat: no-repeat;
background-position: center;
}
}
.application.sheet.daggerheart.dh-style {
.window-content {
position: relative;
top: -36px;
.tab {
padding: 0 10px;
}
}
}

View file

@ -1,46 +1,46 @@
@import '../utils/colors.less';
@import '../utils/fonts.less';
.sheet.daggerheart.dh-style {
.tab.actions {
.actions-list {
display: flex;
flex-direction: column;
list-style: none;
padding: 0;
margin: 0;
width: 100%;
gap: 5px;
.action-item {
display: grid;
align-items: center;
grid-template-columns: 1fr 4fr 1fr;
cursor: pointer;
h4 {
font-family: @font-body;
font-weight: lighter;
color: @beige;
}
.image {
height: 40px;
width: 40px;
object-fit: cover;
border-radius: 6px;
border: none;
}
.controls {
display: flex;
justify-content: center;
gap: 10px;
a {
text-shadow: none;
}
}
}
}
}
}
@import '../utils/colors.less';
@import '../utils/fonts.less';
.sheet.daggerheart.dh-style {
.tab.actions {
.actions-list {
display: flex;
flex-direction: column;
list-style: none;
padding: 0;
margin: 0;
width: 100%;
gap: 5px;
.action-item {
display: grid;
align-items: center;
grid-template-columns: 1fr 4fr 1fr;
cursor: pointer;
h4 {
font-family: @font-body;
font-weight: lighter;
color: @beige;
}
.image {
height: 40px;
width: 40px;
object-fit: cover;
border-radius: 6px;
border: none;
}
.controls {
display: flex;
justify-content: center;
gap: 10px;
a {
text-shadow: none;
}
}
}
}
}
}

View file

@ -1,131 +1,132 @@
@font-face {
font-family: 'Cinzel';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cinzel/v23/8vIU7ww63mVu7gtR-kwKxNvkNOjw-tbnTYo.ttf) format('truetype');
font-family: 'Cinzel';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cinzel/v23/8vIU7ww63mVu7gtR-kwKxNvkNOjw-tbnTYo.ttf) format('truetype');
}
@font-face {
font-family: 'Cinzel';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cinzel/v23/8vIU7ww63mVu7gtR-kwKxNvkNOjw-jHgTYo.ttf) format('truetype');
font-family: 'Cinzel';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cinzel/v23/8vIU7ww63mVu7gtR-kwKxNvkNOjw-jHgTYo.ttf) format('truetype');
}
@font-face {
font-family: 'Cinzel Decorative';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cinzeldecorative/v17/daaHSScvJGqLYhG8nNt8KPPswUAPniZoaelD.ttf) format('truetype');
font-family: 'Cinzel Decorative';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cinzeldecorative/v17/daaHSScvJGqLYhG8nNt8KPPswUAPniZoaelD.ttf)
format('truetype');
}
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v30/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Ew-.ttf) format('truetype');
font-family: 'Montserrat';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v30/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Ew-.ttf) format('truetype');
}
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v30/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCu170w-.ttf) format('truetype');
font-family: 'Montserrat';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v30/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCu170w-.ttf) format('truetype');
}
.application.sheet.daggerheart.dh-style h1 {
font-family: 'Cinzel Decorative', serif;
margin: 0;
border: none;
font-weight: normal;
font-family: 'Cinzel Decorative', serif;
margin: 0;
border: none;
font-weight: normal;
}
.application.sheet.daggerheart.dh-style h2,
.application.sheet.daggerheart.dh-style h3 {
font-family: 'Cinzel', serif;
margin: 0;
border: none;
font-weight: normal;
font-family: 'Cinzel', serif;
margin: 0;
border: none;
font-weight: normal;
}
.application.sheet.daggerheart.dh-style h4 {
font-family: 'Montserrat', sans-serif;
font-size: 14px;
border: none;
font-weight: 700;
margin: 0;
text-shadow: none;
color: #f3c267;
font-weight: normal;
font-family: 'Montserrat', sans-serif;
font-size: 14px;
border: none;
font-weight: 700;
margin: 0;
text-shadow: none;
color: #f3c267;
font-weight: normal;
}
.application.sheet.daggerheart.dh-style h5 {
font-size: 14px;
color: #f3c267;
margin: 0;
font-weight: normal;
font-size: 14px;
color: #f3c267;
margin: 0;
font-weight: normal;
}
.application.sheet.daggerheart.dh-style p,
.application.sheet.daggerheart.dh-style span {
font-family: 'Montserrat', sans-serif;
font-family: 'Montserrat', sans-serif;
}
.application.sheet.daggerheart.dh-style small {
font-family: 'Montserrat', sans-serif;
opacity: 0.8;
font-family: 'Montserrat', sans-serif;
opacity: 0.8;
}
.application.sheet.daggerheart.dh-style.class .tagify {
background: light-dark(transparent, transparent);
border: 1px solid light-dark(#222, #efe6d8);
height: 34px;
border-radius: 3px;
margin-right: 1px;
background: light-dark(transparent, transparent);
border: 1px solid light-dark(#222, #efe6d8);
height: 34px;
border-radius: 3px;
margin-right: 1px;
}
.application.sheet.daggerheart.dh-style.class .tagify tag div {
display: flex;
justify-content: space-between;
align-items: center;
height: 22px;
display: flex;
justify-content: space-between;
align-items: center;
height: 22px;
}
.application.sheet.daggerheart.dh-style.class .tagify tag div span {
font-weight: 400;
font-weight: 400;
}
.application.sheet.daggerheart.dh-style.class .tagify tag div img {
margin-left: 8px;
height: 20px;
width: 20px;
margin-left: 8px;
height: 20px;
width: 20px;
}
.application.sheet.daggerheart.dh-style.class .tab.settings .fieldsets-section {
display: grid;
gap: 10px;
grid-template-columns: 1fr 1.5fr 1.5fr;
display: grid;
gap: 10px;
grid-template-columns: 1fr 1.5fr 1.5fr;
}
.application.sheet.daggerheart.dh-style.class .tab.settings .list-items {
margin-bottom: 10px;
width: 100%;
margin-bottom: 10px;
width: 100%;
}
.application.sheet.daggerheart.dh-style.class .tab.settings .list-items:last-child {
margin-bottom: 0px;
margin-bottom: 0px;
}
.application.sheet.daggerheart.dh-style.class .tab.settings .list-items .item-line {
display: grid;
align-items: center;
gap: 10px;
grid-template-columns: 1fr 3fr 1fr;
display: grid;
align-items: center;
gap: 10px;
grid-template-columns: 1fr 3fr 1fr;
}
.application.sheet.daggerheart.dh-style.class .tab.settings .list-items .item-line h4 {
font-family: 'Montserrat', sans-serif;
font-weight: lighter;
color: light-dark(#222, #efe6d8);
font-family: 'Montserrat', sans-serif;
font-weight: lighter;
color: light-dark(#222, #efe6d8);
}
.application.sheet.daggerheart.dh-style.class .tab.settings .list-items .item-line .image {
height: 40px;
width: 40px;
object-fit: cover;
border-radius: 6px;
border: none;
height: 40px;
width: 40px;
object-fit: cover;
border-radius: 6px;
border: none;
}
.application.sheet.daggerheart.dh-style.class .tab.settings .list-items .item-line .controls {
display: flex;
justify-content: center;
gap: 10px;
display: flex;
justify-content: center;
gap: 10px;
}
.application.sheet.daggerheart.dh-style.class .tab.settings .list-items .item-line .controls a {
text-shadow: none;
text-shadow: none;
}

View file

@ -1,12 +1,11 @@
@import '../utils/colors.less';
@import '../utils/fonts.less';
.application.sheet.daggerheart.dh-style.domain-card {
section.tab {
height: 400px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: light-dark(@dark-blue, @golden) transparent;
}
}
@import '../utils/colors.less';
@import '../utils/fonts.less';
.application.sheet.daggerheart.dh-style.domain-card {
section.tab {
height: 400px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: light-dark(@dark-blue, @golden) transparent;
}
}

View file

@ -1,20 +1,20 @@
@import '../utils/colors.less';
@import '../utils/fonts.less';
.application.sheet.daggerheart.dh-style.feature {
.item-sheet-header {
display: flex;
.profile {
height: 130px;
width: 130px;
}
}
section.tab {
height: 400px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: light-dark(@dark-blue, @golden) transparent;
}
}
@import '../utils/colors.less';
@import '../utils/fonts.less';
.application.sheet.daggerheart.dh-style.feature {
.item-sheet-header {
display: flex;
.profile {
height: 130px;
width: 130px;
}
}
section.tab {
height: 400px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: light-dark(@dark-blue, @golden) transparent;
}
}

261
styles/levelup.less Normal file
View 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;
}
}

View file

@ -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{
&.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 {
@ -79,7 +79,7 @@
left: 0;
width: var(--fear-percent);
max-width: 100%;
background: linear-gradient(90deg,rgba(2, 0, 38, 1) 0%, rgba(199, 1, 252, 1) 100%);
background: linear-gradient(90deg, rgba(2, 0, 38, 1) 0%, rgba(199, 1, 252, 1) 100%);
z-index: 0;
border-radius: 4px;
}
@ -88,7 +88,6 @@
z-index: 1;
}
&.fear {
}
}
&.isGM {
@ -101,18 +100,20 @@
}
}
}
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;
}
}
&:has(.fear-bar) {
min-width: 200px;
}
}
}

View file

@ -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);

View 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>

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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">

View file

@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View file

@ -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>

View file

@ -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 rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const askQuestion = question => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) =>
rl.question(question, (answer) => {
rl.close();
resolve(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"]) {
try {
await fs.promises.symlink(path.join(fileRoot, p), path.join("foundry", p));
} catch (e) {
if (e.code !== "EEXIST") throw e;
}
for (const p of ['client', 'common', 'tsconfig.json']) {
try {
await fs.promises.symlink(path.join(fileRoot, p), path.join('foundry', p));
} catch (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;
}