Merged with main

This commit is contained in:
WBHarry 2025-07-22 14:36:35 +02:00
commit 480d04fee5
784 changed files with 13985 additions and 27621 deletions

View file

@ -37,7 +37,7 @@ jobs:
download: https://github.com/${{github.repository}}/releases/download/${{github.event.release.tag_name}}/system.zip download: https://github.com/${{github.repository}}/releases/download/${{github.event.release.tag_name}}/system.zip
# Create a zip file with all files required by the module to add to the release # Create a zip file with all files required by the module to add to the release
- run: zip -r ./system.zip system.json README.md LICENSE build/daggerheart.js assets/ templates/ styles/daggerheart.css packs/ lang/ - run: zip -r ./system.zip system.json README.md LICENSE build/daggerheart.js build/tagify.css styles/daggerheart.css assets/ templates/ packs/ lang/
# Create a release for this specific version # Create a release for this specific version
- name: Update Release with Files - name: Update Release with Files

View file

@ -4,7 +4,7 @@
- [Overview](#overview) - [Overview](#overview)
- [User Install Guide](#user-install) - [User Install Guide](#user-install)
- [Developer Setup](#developer-setup) - [Developer Setup](#development-setup)
- [Contribution Info](#contributing) - [Contribution Info](#contributing)
## Overview ## Overview

View file

@ -3,12 +3,12 @@ import * as applications from './module/applications/_module.mjs';
import * as models from './module/data/_module.mjs'; import * as models from './module/data/_module.mjs';
import * as documents from './module/documents/_module.mjs'; import * as documents from './module/documents/_module.mjs';
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
import { DhDualityRollEnricher, DhTemplateEnricher } from './module/enrichers/_module.mjs'; import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs';
import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs'; import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs';
import { NarrativeCountdowns, registerCountdownApplicationHooks } from './module/applications/ui/countdowns.mjs'; import { NarrativeCountdowns } from './module/applications/ui/countdowns.mjs';
import { DualityRollColor } from './module/data/settings/Appearance.mjs'; import { DualityRollColor } from './module/data/settings/Appearance.mjs';
import { DHRoll, DualityRoll, D20Roll, DamageRoll, DualityDie } from './module/dice/_module.mjs'; import { DHRoll, DualityRoll, D20Roll, DamageRoll, DualityDie } from './module/dice/_module.mjs';
import { renderDualityButton } from './module/enrichers/DualityRollEnricher.mjs'; import { enrichedDualityRoll, renderDualityButton } from './module/enrichers/DualityRollEnricher.mjs';
import { renderMeasuredTemplate } from './module/enrichers/TemplateEnricher.mjs'; import { renderMeasuredTemplate } from './module/enrichers/TemplateEnricher.mjs';
import { registerCountdownHooks } from './module/data/countdowns.mjs'; import { registerCountdownHooks } from './module/data/countdowns.mjs';
import { import {
@ -18,6 +18,9 @@ import {
} from './module/systemRegistration/_module.mjs'; } from './module/systemRegistration/_module.mjs';
import { placeables } from './module/canvas/_module.mjs'; import { placeables } from './module/canvas/_module.mjs';
import { registerRollDiceHooks } from './module/dice/dhRoll.mjs'; import { registerRollDiceHooks } from './module/dice/dhRoll.mjs';
import { registerDHActorHooks } from './module/documents/actor.mjs';
import './node_modules/@yaireo/tagify/dist/tagify.css';
import { renderDamageButton } from './module/enrichers/DamageEnricher.mjs';
Hooks.once('init', () => { Hooks.once('init', () => {
CONFIG.DH = SYSTEM; CONFIG.DH = SYSTEM;
@ -27,23 +30,16 @@ Hooks.once('init', () => {
documents documents
}; };
CONFIG.TextEditor.enrichers.push( CONFIG.TextEditor.enrichers.push(...enricherConfig);
...[
{
pattern: /\[\[\/dr\s?(.*?)\]\]/g,
enricher: DhDualityRollEnricher
},
{
pattern: /^@Template\[(.*)\]$/g,
enricher: DhTemplateEnricher
}
]
);
CONFIG.statusEffects = Object.values(SYSTEM.GENERAL.conditions).map(x => ({ CONFIG.statusEffects = [
...x, ...CONFIG.statusEffects.filter(x => !['dead', 'unconscious'].includes(x.id)),
name: game.i18n.localize(x.name) ...Object.values(SYSTEM.GENERAL.conditions).map(x => ({
})); ...x,
name: game.i18n.localize(x.name),
systemEffect: true
}))
];
CONFIG.Dice.daggerheart = { CONFIG.Dice.daggerheart = {
DualityDie: DualityDie, DualityDie: DualityDie,
@ -56,6 +52,14 @@ Hooks.once('init', () => {
CONFIG.Dice.rolls = [...CONFIG.Dice.rolls, ...[DHRoll, DualityRoll, D20Roll, DamageRoll]]; CONFIG.Dice.rolls = [...CONFIG.Dice.rolls, ...[DHRoll, DualityRoll, D20Roll, DamageRoll]];
CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate; CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate;
const { DocumentSheetConfig } = foundry.applications.apps;
CONFIG.Token.documentClass = documents.DhToken;
CONFIG.Token.prototypeSheetClass = applications.sheetConfigs.DhPrototypeTokenConfig;
DocumentSheetConfig.unregisterSheet(TokenDocument, 'core', foundry.applications.sheets.TokenConfig);
DocumentSheetConfig.registerSheet(TokenDocument, SYSTEM.id, applications.sheetConfigs.DhTokenConfig, {
makeDefault: true
});
CONFIG.Item.documentClass = documents.DHItem; CONFIG.Item.documentClass = documents.DHItem;
//Registering the Item DataModel //Registering the Item DataModel
@ -93,12 +97,12 @@ Hooks.once('init', () => {
CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect; CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect;
CONFIG.ActiveEffect.dataModels = models.activeEffects.config; CONFIG.ActiveEffect.dataModels = models.activeEffects.config;
foundry.applications.apps.DocumentSheetConfig.unregisterSheet( DocumentSheetConfig.unregisterSheet(
CONFIG.ActiveEffect.documentClass, CONFIG.ActiveEffect.documentClass,
'core', 'core',
foundry.applications.sheets.ActiveEffectConfig foundry.applications.sheets.ActiveEffectConfig
); );
foundry.applications.apps.DocumentSheetConfig.registerSheet( DocumentSheetConfig.registerSheet(
CONFIG.ActiveEffect.documentClass, CONFIG.ActiveEffect.documentClass,
SYSTEM.id, SYSTEM.id,
applications.sheetConfigs.ActiveEffectConfig, applications.sheetConfigs.ActiveEffectConfig,
@ -107,6 +111,8 @@ Hooks.once('init', () => {
} }
); );
CONFIG.Token.hudClass = applications.hud.DHTokenHUD;
CONFIG.Combat.dataModels = { CONFIG.Combat.dataModels = {
base: models.DhCombat base: models.DhCombat
}; };
@ -119,9 +125,12 @@ Hooks.once('init', () => {
CONFIG.ChatMessage.documentClass = documents.DhChatMessage; CONFIG.ChatMessage.documentClass = documents.DhChatMessage;
CONFIG.Canvas.rulerClass = placeables.DhRuler; CONFIG.Canvas.rulerClass = placeables.DhRuler;
CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer;
CONFIG.Token.objectClass = placeables.DhTokenPlaceable;
CONFIG.Combat.documentClass = documents.DhpCombat; CONFIG.Combat.documentClass = documents.DhpCombat;
CONFIG.ui.combat = applications.ui.DhCombatTracker; CONFIG.ui.combat = applications.ui.DhCombatTracker;
CONFIG.ui.chat = applications.ui.DhChatLog; CONFIG.ui.chat = applications.ui.DhChatLog;
CONFIG.ui.hotbar = applications.ui.DhHotbar;
CONFIG.Token.rulerClass = placeables.DhTokenRuler; CONFIG.Token.rulerClass = placeables.DhTokenRuler;
CONFIG.ui.resources = applications.ui.DhFearTracker; CONFIG.ui.resources = applications.ui.DhFearTracker;
@ -152,40 +161,22 @@ Hooks.on('ready', () => {
registerCountdownHooks(); registerCountdownHooks();
socketRegistration.registerSocketHooks(); socketRegistration.registerSocketHooks();
registerCountdownApplicationHooks();
registerRollDiceHooks(); registerRollDiceHooks();
registerDHActorHooks();
}); });
Hooks.once('dicesoniceready', () => {}); Hooks.once('dicesoniceready', () => {});
Hooks.on('renderChatMessageHTML', (_, element) => { Hooks.on('renderChatMessageHTML', (_, element) => {
element enricherRenderSetup(element);
.querySelectorAll('.duality-roll-button')
.forEach(element => element.addEventListener('click', renderDualityButton));
element
.querySelectorAll('.measured-template-button')
.forEach(element => element.addEventListener('click', renderMeasuredTemplate));
}); });
Hooks.on('renderJournalEntryPageProseMirrorSheet', (_, element) => { Hooks.on('renderJournalEntryPageProseMirrorSheet', (_, element) => {
element enricherRenderSetup(element);
.querySelectorAll('.duality-roll-button')
.forEach(element => element.addEventListener('click', renderDualityButton));
element
.querySelectorAll('.measured-template-button')
.forEach(element => element.addEventListener('click', renderMeasuredTemplate));
}); });
Hooks.on('renderHandlebarsApplication', (_, element) => { Hooks.on('renderHandlebarsApplication', (_, element) => {
element enricherRenderSetup(element);
.querySelectorAll('.duality-roll-button')
.forEach(element => element.addEventListener('click', renderDualityButton));
element
.querySelectorAll('.measured-template-button')
.forEach(element => element.addEventListener('click', renderMeasuredTemplate));
}); });
Hooks.on('chatMessage', (_, message) => { Hooks.on('chatMessage', (_, message) => {
@ -197,49 +188,21 @@ Hooks.on('chatMessage', (_, message) => {
} }
const traitValue = rollCommand.trait?.toLowerCase(); const traitValue = rollCommand.trait?.toLowerCase();
const advantageState = rollCommand.advantage ? true : rollCommand.disadvantage ? false : null; const advantage = rollCommand.advantage
? CONFIG.DH.ACTIONS.advandtageState.advantage.value
: rollCommand.disadvantage
? CONFIG.DH.ACTIONS.advandtageState.disadvantage.value
: undefined;
const difficulty = rollCommand.difficulty;
// Target not required if an attribute is not used. const target = getCommandTarget();
const target = traitValue ? getCommandTarget() : undefined; const title = traitValue
if (target || !traitValue) { ? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
new Promise(async (resolve, reject) => { ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label)
const trait = target ? target.system.traits[traitValue] : undefined; })
if (traitValue && !trait) { : game.i18n.localize('DAGGERHEART.GENERAL.duality');
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.attributeFaulty'));
reject();
return;
}
const title = traitValue
? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label)
})
: game.i18n.localize('DAGGERHEART.GENERAL.duality');
const config = {
title: title,
roll: {
trait: traitValue
},
data: {
traits: {
[traitValue]: trait
}
},
source: target,
hasSave: false,
dialog: { configure: false },
evaluate: true,
advantage: rollCommand.advantage == true,
disadvantage: rollCommand.disadvantage == true
};
await CONFIG.Dice.daggerheart['DualityRoll'].build(config);
resolve();
});
}
enrichedDualityRoll({ traitValue, target, difficulty, title, label: 'test', actionType: null, advantage });
return false; return false;
} }
}); });

View file

@ -13,6 +13,9 @@
"armor": "Armor", "armor": "Armor",
"beastform": "Beastform" "beastform": "Beastform"
}, },
"ActiveEffect": {
"beastform": "Beastform"
},
"Actor": { "Actor": {
"character": "Character", "character": "Character",
"companion": "Companion", "companion": "Companion",
@ -20,6 +23,9 @@
"environment": "Environment" "environment": "Environment"
} }
}, },
"CONTROLS": {
"inFront": "In Front"
},
"DAGGERHEART": { "DAGGERHEART": {
"ACTIONS": { "ACTIONS": {
"Config": { "Config": {
@ -30,8 +36,15 @@
} }
}, },
"Settings": { "Settings": {
"attackBonus": "Attack Bonus",
"attackName": "Attack Name",
"includeBase": { "label": "Include Item Damage" },
"multiplier": "Multiplier",
"resultBased": { "resultBased": {
"label": "Formula based on Hope/Fear result." "label": "Formula based on Hope/Fear result."
},
"applyTo": {
"label": "Targeted Resource"
} }
}, },
"TYPES": { "TYPES": {
@ -96,9 +109,14 @@
"tier": { "label": "Tier" }, "tier": { "label": "Tier" },
"type": { "label": "Type" } "type": { "label": "Type" }
}, },
"hordeDamage": "Horde Damage",
"horderHp": "Horde/HP" "horderHp": "Horde/HP"
}, },
"Character": { "Character": {
"advantageSources": {
"label": "Advantage Sources",
"hint": "Add single words or short text as reminders and hints of what a character has advantage on."
},
"age": "Age", "age": "Age",
"companionFeatures": "Companion Features", "companionFeatures": "Companion Features",
"contextMenu": { "contextMenu": {
@ -107,7 +125,12 @@
"sendToChat": "Send To Chat", "sendToChat": "Send To Chat",
"toLoadout": "Send to Loadout", "toLoadout": "Send to Loadout",
"toVault": "Send to Vault", "toVault": "Send to Vault",
"unequip": "Unequip" "unequip": "Unequip",
"useItem": "Use Item"
},
"disadvantageSources": {
"label": "Disadvantage Sources",
"hint": "Add single words or short text as reminders and hints of what a character has disadvantage on."
}, },
"faith": "Faith", "faith": "Faith",
"levelUp": "You can level up", "levelUp": "You can level up",
@ -129,7 +152,9 @@
"partner": { "label": "Partner" }, "partner": { "label": "Partner" },
"resources": { "resources": {
"stress": { "stress": {
"value": { "label": "Stress" } "value": { "label": "Stress" },
"currentStress": { "label": "Current Stress" },
"maxStress": { "label": "Max Stress" }
} }
} }
}, },
@ -158,12 +183,25 @@
}, },
"APPLICATIONS": { "APPLICATIONS": {
"CharacterCreation": { "CharacterCreation": {
"setupTabs": {
"ancestry": "Ancestry",
"community": "Community",
"class": "Class",
"experience": "Experience",
"traits": "Traits",
"domainCards": "Domain Cards"
},
"ancestryNamePlaceholder": "Your ancestry's name",
"buttonTitle": "Character Setup",
"choice": "Choice", "choice": "Choice",
"finishCreation": "Finish Character Setup", "finishCreation": "Finish Character Setup",
"heritage": "Heritage", "heritage": "Heritage",
"initialExperiences": "Initial Experiences", "initialExperiences": "Initial Experiences",
"mixedAncestry": "Mixed Ancestry",
"newExperience": "New Experience..", "newExperience": "New Experience..",
"selectAncestry": "Select Ancestry", "selectAncestry": "Select Ancestry",
"selectPrimaryAncestry": "Select Primary Ancestry",
"selectSecondaryAncestry": "Select Secondary Ancestry",
"selectArmor": "Select Armor", "selectArmor": "Select Armor",
"selectClass": "Select Class", "selectClass": "Select Class",
"selectCommunity": "Select Community", "selectCommunity": "Select Community",
@ -171,6 +209,7 @@
"selectSecondaryWeapon": "Select Secondary Weapon", "selectSecondaryWeapon": "Select Secondary Weapon",
"selectSubclass": "Select Subclass", "selectSubclass": "Select Subclass",
"startingItems": "Starting Items", "startingItems": "Starting Items",
"story": "Story",
"suggestedArmor": "Suggested Armor", "suggestedArmor": "Suggested Armor",
"suggestedPrimaryWeapon": "Suggested Primary Weapon", "suggestedPrimaryWeapon": "Suggested Primary Weapon",
"suggestedSecondaryWeapon": "Suggested Secondary Weapon", "suggestedSecondaryWeapon": "Suggested Secondary Weapon",
@ -185,6 +224,16 @@
"requestingSpotlight": "Requesting The Spotlight", "requestingSpotlight": "Requesting The Spotlight",
"requestSpotlight": "Request The Spotlight" "requestSpotlight": "Request The Spotlight"
}, },
"ContextMenu": {
"disableEffect": "Disable Effect",
"enableEffect": "Enable Effect",
"equip": "Equip",
"sendToChat": "Send To Chat",
"toLoadout": "Send to Loadout",
"toVault": "Send to Vault",
"unequip": "Unequip",
"useItem": "Use Item"
},
"Countdown": { "Countdown": {
"addCountdown": "Add Countdown", "addCountdown": "Add Countdown",
"FIELDS": { "FIELDS": {
@ -213,6 +262,10 @@
"encounter": "Encounter" "encounter": "Encounter"
} }
}, },
"DeleteConfirmation": {
"title": "Delete {type} - {name}",
"text": "Are you sure you want to delete {name}?"
},
"DamageReduction": { "DamageReduction": {
"armorMarks": "Armor Marks", "armorMarks": "Armor Marks",
"armorWithStress": "Spend 1 stress to use an extra mark", "armorWithStress": "Spend 1 stress to use an extra mark",
@ -227,8 +280,9 @@
"title": "{actor} - Death Move" "title": "{actor} - Death Move"
}, },
"Downtime": { "Downtime": {
"downtimeHeader": "Downtime Moves ({current}/{max})",
"longRest": { "longRest": {
"title": "Long Rest",
"moves": "Long Rest Moves ({current}/{max})",
"clearStress": { "clearStress": {
"description": "Describe how you blow off steam or pull yourself together, and clear all marked Stress.", "description": "Describe how you blow off steam or pull yourself together, and clear all marked Stress.",
"name": "Clear Stress" "name": "Clear Stress"
@ -245,7 +299,6 @@
"description": "Describe how you patch yourself up and remove all marked Hit Points. You may also do this on an ally instead.", "description": "Describe how you patch yourself up and remove all marked Hit Points. You may also do this on an ally instead.",
"name": "Tend to Wounds" "name": "Tend to Wounds"
}, },
"title": "Long Rest",
"workOnAProject": { "workOnAProject": {
"description": "Establish or continue work on a project.", "description": "Establish or continue work on a project.",
"name": "Work on a Project" "name": "Work on a Project"
@ -253,6 +306,7 @@
}, },
"shortRest": { "shortRest": {
"title": "Short Rest", "title": "Short Rest",
"moves": "Short Rest Moves ({current}/{max})",
"tendToWounds": { "tendToWounds": {
"name": "Tend to Wounds", "name": "Tend to Wounds",
"description": "Describe how you hastily patch yourself up, then clear a number of Hit Points equal to 1d4 + your tier. You can do this to an ally instead." "description": "Describe how you hastily patch yourself up, then clear a number of Hit Points equal to 1d4 + your tier. You can do this to an ally instead."
@ -269,6 +323,12 @@
"name": "Prepare", "name": "Prepare",
"description": "Describe how you prepare yourself for the path ahead, then gain a Hope. If you choose to Prepare with one or more members of your party, you each gain 2 Hope." "description": "Describe how you prepare yourself for the path ahead, then gain a Hope. If you choose to Prepare with one or more members of your party, you each gain 2 Hope."
} }
},
"takeDowntime": "Take Downtime"
},
"HUD": {
"tokenHUD": {
"genericEffects": "Foundry Effects"
} }
}, },
"Levelup": { "Levelup": {
@ -374,6 +434,15 @@
"OwnershipSelection": { "OwnershipSelection": {
"title": "Ownership Selection - {name}", "title": "Ownership Selection - {name}",
"default": "Default Ownership" "default": "Default Ownership"
},
"ResourceDice": {
"title": "{name} Resource",
"rerollDice": "Reroll Dice"
}
},
"CLASS": {
"Feature": {
"rallyDice": "Bardic Rally Dice"
} }
}, },
"CONFIG": { "CONFIG": {
@ -478,14 +547,10 @@
"name": "Impenetrable", "name": "Impenetrable",
"description": "Once per short rest, when you would mark your last Hit Point, you can instead mark a Stress." "description": "Once per short rest, when you would mark your last Hit Point, you can instead mark a Stress."
}, },
"magic": { "magical": {
"name": "Magic", "name": "Magical",
"description": "You can't mark an Armor Slot to reduce physical damage." "description": "You can't mark an Armor Slot to reduce physical damage."
}, },
"painful": {
"name": "Painful",
"description": "Each time you mark an Armor Slot, you must mark a Stress."
},
"physical": { "physical": {
"name": "Physical", "name": "Physical",
"description": "You can't mark an Armor Slot to reduce magic damage." "description": "You can't mark an Armor Slot to reduce magic damage."
@ -527,14 +592,19 @@
"description": "You reduce incoming magic damage by your Armor Score before applying it to your damage thresholds." "description": "You reduce incoming magic damage by your Armor Score before applying it to your damage thresholds."
} }
}, },
"BeastformType": {
"normal": "Normal",
"evolved": "Evolved",
"hybrid": "Hybrid"
},
"Burden": { "Burden": {
"oneHanded": "One-Handed", "oneHanded": "One-Handed",
"twoHanded": "Two-Handed" "twoHanded": "Two-Handed"
}, },
"Condition": { "Condition": {
"vulnerable": { "dead": {
"name": "Vulnerable", "name": "Dead",
"description": "While a creature is Vulnerable, all rolls targeting them have advantage.\nA creature who is already Vulnerable cant be made to take the condition again." "description": "The character is dead"
}, },
"hidden": { "hidden": {
"name": "Hidden", "name": "Hidden",
@ -543,6 +613,14 @@
"restrained": { "restrained": {
"name": "Restrained", "name": "Restrained",
"description": "When an effect makes a creature Restrained, it means they cannot move until this condition is cleared.\nThey can still take actions from their current position." "description": "When an effect makes a creature Restrained, it means they cannot move until this condition is cleared.\nThey can still take actions from their current position."
},
"unconcious": {
"name": "Unconcious",
"description": "Your character cant move or act while unconscious, they cant be targeted by an attack."
},
"vulnerable": {
"name": "Vulnerable",
"description": "While a creature is Vulnerable, all rolls targeting them have advantage.\nA creature who is already Vulnerable cant be made to take the condition again."
} }
}, },
"CountdownType": { "CountdownType": {
@ -620,8 +698,16 @@
"armorStack": { "armorStack": {
"name": "Armor Stack", "name": "Armor Stack",
"abbreviation": "AS" "abbreviation": "AS"
},
"fear": {
"name": "Fear",
"abbreviation": "FR"
} }
}, },
"ItemResourceType": {
"simple": "Simple",
"diceValue": "Dice Value"
},
"Range": { "Range": {
"self": { "self": {
"name": "Self", "name": "Self",
@ -655,11 +741,11 @@
} }
}, },
"RollTypes": { "RollTypes": {
"ability": { "trait": {
"name": "Ability" "name": "Trait"
}, },
"weapon": { "attack": {
"name": "Weapon" "name": "Attack"
}, },
"spellcast": { "spellcast": {
"name": "SpellCast" "name": "SpellCast"
@ -668,6 +754,10 @@
"name": "Dice Set" "name": "Dice Set"
} }
}, },
"SelectAction": {
"selectType": "Select Action Type",
"selectAction": "Action Selection"
},
"Traits": { "Traits": {
"agility": { "agility": {
"name": "Agility", "name": "Agility",
@ -727,7 +817,7 @@
"WeaponFeature": { "WeaponFeature": {
"barrier": { "barrier": {
"name": "Barrier", "name": "Barrier",
"description": "+{armorScore} to Armor Score; -1 to Evasion" "description": "Gain your character's Tier + 1 to Armor Score; -1 to Evasion"
}, },
"bonded": { "bonded": {
"name": "Bonded", "name": "Bonded",
@ -843,7 +933,7 @@
}, },
"paired": { "paired": {
"name": "Paired", "name": "Paired",
"description": "+{bonusDamage} to primary weapon damage to targets within Melee range" "description": "Add your character's Tier + 1 to primary weapon damage against targets within Melee range"
}, },
"parry": { "parry": {
"name": "Parry", "name": "Parry",
@ -863,7 +953,7 @@
}, },
"protective": { "protective": {
"name": "Protective", "name": "Protective",
"description": "+{tier} to Armor Score" "description": "Add your character's Tier to your Armor Score"
}, },
"quick": { "quick": {
"name": "Quick", "name": "Quick",
@ -889,6 +979,10 @@
"name": "Scary", "name": "Scary",
"description": "On a successful attack, the target must mark a Stress." "description": "On a successful attack, the target must mark a Stress."
}, },
"selfCorrecting": {
"name": "Self Correcting",
"description": "When you roll a 1 on a damage die, it deals 6 damage instead."
},
"serrated": { "serrated": {
"name": "Serrated", "name": "Serrated",
"description": "When you roll a 1 on a damage die, it deals 8 damage instead." "description": "When you roll a 1 on a damage die, it deals 8 damage instead."
@ -908,10 +1002,6 @@
"timebending": { "timebending": {
"name": "Timebending", "name": "Timebending",
"description": "You can choose the target of your attack after making your attack roll." "description": "You can choose the target of your attack after making your attack roll."
},
"versatile": {
"name": "Versatile",
"description": "This weapon can also be used with these statistics—{characterTrait}, {range}, {damage}."
} }
} }
}, },
@ -941,6 +1031,10 @@
"stress": { "stress": {
"name": "Stress" "name": "Stress"
} }
},
"Attachments": {
"attachHint": "Drop items here to attach them",
"transferHint": "If checked, this effect will be applied to any actor that owns this Effect's parent Item. The effect is always applied if this Item is attached to another one."
} }
}, },
"GENERAL": { "GENERAL": {
@ -950,32 +1044,79 @@
}, },
"Advantage": { "Advantage": {
"full": "Advantage", "full": "Advantage",
"plural": "Advantages",
"short": "Adv" "short": "Adv"
}, },
"Adversary": { "Adversary": {
"singular": "Adversary", "singular": "Adversary",
"plural": "Adversaries" "plural": "Adversaries"
}, },
"Bonuses": {
"rest": {
"shortRest": {
"shortRestMoves": {
"label": "Short Rest: Bonus Short Rest Moves",
"hint": "The number of extra Short Rest Moves the character can take during a Short Rest."
},
"longRestMoves": {
"label": "Short Rest: Bonus Long Rest Moves",
"hint": "The number of extra Long Rest Moves the character can take during a Short Rest."
}
},
"longRest": {
"shortRestMoves": {
"label": "Long Rest: Bonus Short Rest Moves",
"hint": "The number of extra Short Rest Moves the character can take during a Long Rest."
},
"longRestMoves": {
"label": "Long Rest: Bonus Long Rest Moves",
"hint": "The number of extra Long Rest Moves the character can take during a Long Rest."
}
}
}
},
"Character": { "Character": {
"singular": "Character", "singular": "Character",
"plural": "Characters" "plural": "Characters"
}, },
"Cost": {
"single": "Cost",
"plural": "Costs"
},
"Damage": { "Damage": {
"severe": "Severe", "severe": "Severe",
"major": "Major", "major": "Major",
"minor": "Minor", "minor": "Minor",
"none": "None" "none": "None",
"allDamage": "All Damage",
"physicalDamage": "Physical Damage",
"magicalDamage": "Magical Damage",
"primaryWeapon": "Primary Weapon Damage",
"secondaryWeapon": "Secondary Weapon Damage"
},
"DamageResistance": {
"none": "None",
"resistance": "Resistance",
"immunity": "Immunity",
"physicalReduction": "Physical Damage Reduction",
"magicalReduction": "Magical Damage Reduction"
}, },
"DamageThresholds": { "DamageThresholds": {
"title": "Damage Thresholds", "title": "Damage Thresholds",
"minor": "Minor", "minor": "Minor",
"major": "Major", "major": "Major",
"severe": "Severe" "severe": "Severe",
"majorThreshold": "Major Damage Threshold",
"severeThreshold": "Severe Damage Threshold"
}, },
"Dice": { "Dice": {
"single": "Die", "single": "Die",
"plural": "Dice" "plural": "Dice"
}, },
"Difficulty": {
"all": "Difficulty: all",
"reaction": "Difficulty: reaction"
},
"Disadvantage": { "Disadvantage": {
"full": "Disadvantage", "full": "Disadvantage",
"short": "Dis" "short": "Dis"
@ -985,39 +1126,39 @@
"plural": "Domains", "plural": "Domains",
"arcana": { "arcana": {
"label": "Arcana", "label": "Arcana",
"Description": "This is the domain of the innate or instinctual use of magic. Those who walk this path tap into the raw, enigmatic forces of the realms to manipulate both the elements and their own energy. Arcana offers wielders a volatile power, but it is incredibly potent when correctly channeled." "description": "This is the domain of the innate or instinctual use of magic. Those who walk this path tap into the raw, enigmatic forces of the realms to manipulate both the elements and their own energy. Arcana offers wielders a volatile power, but it is incredibly potent when correctly channeled."
}, },
"blade": { "blade": {
"label": "Blade", "label": "Blade",
"Description": "This is the domain of those who dedicate their lives to the mastery of weapons. Whether by blade, bow, or perhaps a more specialized arm, those who follow this path have the skill to cut short the lives of others. Blade requires study and dedication from its followers, in exchange for inexorable power over death." "description": "This is the domain of those who dedicate their lives to the mastery of weapons. Whether by blade, bow, or perhaps a more specialized arm, those who follow this path have the skill to cut short the lives of others. Blade requires study and dedication from its followers, in exchange for inexorable power over death."
}, },
"bone": { "bone": {
"label": "Bone", "label": "Bone",
"Description": "This is the domain of mastery of swiftness and tactical mastery. Practitioners of this domain have an uncanny control over their own physical abilities, and an eye for predicting the behaviors of others in combat. Bone grants its adherents unparalleled understanding of bodies and their movements in exchange for diligent training." "description": "This is the domain of mastery of swiftness and tactical mastery. Practitioners of this domain have an uncanny control over their own physical abilities, and an eye for predicting the behaviors of others in combat. Bone grants its adherents unparalleled understanding of bodies and their movements in exchange for diligent training."
}, },
"codex": { "codex": {
"label": "Codex", "label": "Codex",
"Description": "This is the domain of intensive magical study. Those who seek magical knowledge turn to the recipes of power recorded in books, on scrolls, etched into walls, or tattooed on bodies. Codex offers a commanding and versatile understanding of magic to those devotees who are willing to seek beyond the common knowledge." "description": "This is the domain of intensive magical study. Those who seek magical knowledge turn to the recipes of power recorded in books, on scrolls, etched into walls, or tattooed on bodies. Codex offers a commanding and versatile understanding of magic to those devotees who are willing to seek beyond the common knowledge."
}, },
"grace": { "grace": {
"label": "Grace", "label": "Grace",
"Description": "This is the domain of charisma. Through rapturous storytelling, clever charm, or a shroud of lies, those who channel this power define the realities of their adversaries, bending perception to their will. Grace offers its wielders raw magnetism and mastery over language." "description": "This is the domain of charisma. Through rapturous storytelling, clever charm, or a shroud of lies, those who channel this power define the realities of their adversaries, bending perception to their will. Grace offers its wielders raw magnetism and mastery over language."
}, },
"midnight": { "midnight": {
"label": "Midnight", "label": "Midnight",
"Description": "This is the domain of shadows and secrecy. Whether by clever tricks, or cloak of night those who channel these forces are practiced in that art of obscurity and there is nothing hidden they cannot reach. Midnight offers practitioners the incredible power to control and create enigmas." "description": "This is the domain of shadows and secrecy. Whether by clever tricks, or cloak of night those who channel these forces are practiced in that art of obscurity and there is nothing hidden they cannot reach. Midnight offers practitioners the incredible power to control and create enigmas."
}, },
"sage": { "sage": {
"label": "Sage", "label": "Sage",
"Description": "This is the domain of the natural world. Those who walk this path tap into the unfettered power of the earth and its creatures to unleash raw magic. Sage grants its adherents the vitality of a blooming flower and ferocity of a hungry predator." "description": "This is the domain of the natural world. Those who walk this path tap into the unfettered power of the earth and its creatures to unleash raw magic. Sage grants its adherents the vitality of a blooming flower and ferocity of a hungry predator."
}, },
"splendor": { "splendor": {
"label": "Splendor", "label": "Splendor",
"Description": "This is the domain of life. Through this magic, followers gain the ability to heal, though such power also grants the wielder some control over death. Splendor offers its disciples the magnificent ability to both give and end life." "description": "This is the domain of life. Through this magic, followers gain the ability to heal, though such power also grants the wielder some control over death. Splendor offers its disciples the magnificent ability to both give and end life."
}, },
"valor": { "valor": {
"label": "Valor", "label": "Valor",
"Description": "This is the domain of protection. Whether through attack or defense, those who choose this discipline channel formidable strength to protect their allies in battle. Valor offers great power to those who raise their shield in defense of others." "description": "This is the domain of protection. Whether through attack or defense, those who choose this discipline channel formidable strength to protect their allies in battle. Valor offers great power to those who raise their shield in defense of others."
} }
}, },
"Effect": { "Effect": {
@ -1028,15 +1169,73 @@
"single": "Experience", "single": "Experience",
"plural": "Experiences" "plural": "Experiences"
}, },
"Healing": {
"healingAmount": "Healing Amount"
},
"Modifier": {
"single": "Modifier",
"plural": "Modifiers"
},
"Neutral": { "Neutral": {
"full": "None", "full": "None",
"short": "no" "short": "no"
}, },
"Range": {
"other": "Range Increase: Other",
"spell": "Range Increase: Spell",
"weapon": "Range Increase: Weapon"
},
"RefreshType": { "RefreshType": {
"session": "Session", "session": "Session",
"shortrest": "Short Rest", "shortrest": "Short Rest",
"longrest": "Long Rest" "longrest": "Long Rest"
}, },
"Resource": {
"single": "Resource",
"plural": "Resources"
},
"Roll": {
"attack": "Attack Roll",
"difficulty": "Roll (Difficulty {difficulty})",
"primaryWeaponAttack": "Primary Weapon Attack Roll",
"secondaryWeaponAttack": "Secondary Weapon Attack Roll",
"spellcast": "Spellcast Roll",
"trait": "Trait Roll",
"action": "Action Roll",
"reaction": "Reaction Roll"
},
"Rules": {
"damageReduction": {
"increasePerArmorMark": {
"label": "Damage Reduction per Armor Slot",
"hint": "A used armor slot normally reduces damage by one step. This value increases the number of steps damage is reduced by."
},
"maxArmorMarkedBonus": "Max Armor Used",
"maxArmorMarkedStress": {
"label": "Max Armor Used With Stress",
"hint": "If this value is set you can use up to that much stress to spend additional Armor Marks beyond your normal maximum."
},
"stress": {
"severe": {
"label": "Stress Damage Reduction: Severe",
"hint": "The cost in stress you can pay to reduce severe damage down to major."
},
"major": {
"label": "Stress Damage Reduction: Major",
"hint": "The cost in stress you can pay to reduce major damage down to minor."
},
"minor": {
"label": "Stress Damage Reduction: Minor",
"hint": "The cost in stress you can pay to reduce minor damage to none."
}
}
},
"attack": {
"damage": {
"value": { "label": "Base Attack: Damage" }
}
}
},
"Tabs": { "Tabs": {
"details": "Details", "details": "Details",
"attack": "Attack", "attack": "Attack",
@ -1065,52 +1264,124 @@
"specialization": "Specialization", "specialization": "Specialization",
"mastery": "Mastery", "mastery": "Mastery",
"optional": "Optional", "optional": "Optional",
"recovery": "Recovery",
"setup": "Setup", "setup": "Setup",
"equipment": "Equipment" "equipment": "Equipment",
}, "attachments": "Attachments",
"Tiers": { "advanced": "Advanced",
"singular": "Tier",
"tier1": "Tier 1", "tier1": "Tier 1",
"tier2": "Tier 2", "tier2": "Tier 2",
"tier3": "Tier 3", "tier3": "Tier 3",
"tier4": "Tier 4" "tier4": "tier 4"
},
"Tiers": {
"singular": "Tier",
"1": "Tier 1",
"2": "Tier 2",
"3": "Tier 3",
"4": "Tier 4"
}, },
"Trait": { "Trait": {
"single": "Trait", "single": "Trait",
"plural": "Traits" "plural": "Traits"
}, },
"actorName": "Actor Name",
"amount": "Amount",
"armorScore": "Armor Score",
"activeEffects": "Active Effects", "activeEffects": "Active Effects",
"armorSlots": "Armor Slots",
"attack": "Attack", "attack": "Attack",
"basics": "Basics", "basics": "Basics",
"bonus": "Bonus", "bonus": "Bonus",
"burden": "Burden", "burden": "Burden",
"check": "{check} Check", "check": "{check} Check",
"continue": "Continue",
"criticalSuccess": "Critical Success", "criticalSuccess": "Critical Success",
"damage": "Damage",
"damageType": "Damage Type",
"description": "Description", "description": "Description",
"difficulty": "Difficulty",
"dropActorsHere": "Drop Actors here",
"duality": "Duality", "duality": "Duality",
"dualityRoll": "Duality Roll", "dualityRoll": "Duality Roll",
"enabled": "Enabled", "enabled": "Enabled",
"evasion": "Evasion", "evasion": "Evasion",
"equipment": "Equipment",
"experience": {
"single": "Experience",
"plural": "Experiences"
},
"failure": "Failure",
"fear": "Fear", "fear": "Fear",
"features": "Features", "features": "Features",
"hitPoints": "Hit Points", "formula": "Formula",
"healing": "Healing",
"HitPoints": {
"single": "Hit Point",
"plural": "Hit Points",
"short": "HP"
},
"hope": "Hope", "hope": "Hope",
"hordeHp": "Horde HP",
"identify": "Identity",
"imagePath": "Image Path",
"inactiveEffects": "Inactive Effects", "inactiveEffects": "Inactive Effects",
"inventory": "Inventory", "inventory": "Inventory",
"level": "Level", "level": "Level",
"modifier": "Modifier", "levelUp": "Level Up",
"loadout": "Loadout",
"max": "Max",
"multiclass": "Multiclass", "multiclass": "Multiclass",
"newCategory": "New Category",
"none": "None",
"partner": "Partner",
"proficiency": "Proficiency",
"quantity": "Quantity", "quantity": "Quantity",
"range": "Range", "range": "Range",
"recovery": "Recovery",
"resource": "Resource",
"roll": "Roll",
"rollAll": "Roll All",
"rollDamage": "Roll Damage",
"save": "Save",
"scalable": "Scalable",
"situationalBonus": "Situational Bonus",
"stress": "Stress", "stress": "Stress",
"success": "Success",
"take": "Take", "take": "Take",
"target": "Target", "Target": {
"single": "Target",
"plural": "Targets"
},
"title": "Title", "title": "Title",
"total": "Total",
"true": "True",
"type": "Type", "type": "Type",
"unarmed": "Unarmed",
"unarmedStrike": "Unarmed Strike",
"unarmored": "Unarmored", "unarmored": "Unarmored",
"use": "Use" "use": "Use",
"used": "Used",
"uses": "Uses",
"value": "Value",
"withThing": "With {thing}"
}, },
"ITEMS": { "ITEMS": {
"FIELDS": {
"resource": {
"amount": { "label": "Amount" },
"dieFaces": { "label": "Die Faces" },
"icon": { "label": "Icon" },
"max": { "label": "Max" },
"recovery": { "label": "Recovery" },
"type": { "label": "Type" },
"value": { "label": "Value" }
}
},
"Ancestry": {
"primaryFeature": "Primary Feature",
"secondaryFeature": "Secondary Feature"
},
"Armor": { "Armor": {
"baseScore": "Base Score", "baseScore": "Base Score",
"baseThresholds": { "baseThresholds": {
@ -1121,20 +1392,39 @@
}, },
"Beastform": { "Beastform": {
"FIELDS": { "FIELDS": {
"beastformType": { "label": "Beastform Type" },
"tier": { "label": "Tier" }, "tier": { "label": "Tier" },
"mainTrait": { "label": "Main Trait" },
"examples": { "label": "Examples" }, "examples": { "label": "Examples" },
"advantageOn": { "label": "Gain Advantage On" }, "advantageOn": { "label": "Gain Advantage On" },
"tokenImg": { "label": "Token Image" }, "tokenImg": { "label": "Token Image" },
"tokenRingImg": { "label": "Subject Texture" },
"tokenSize": { "tokenSize": {
"placeholder": "Using character dimensions", "placeholder": "Using character dimensions",
"height": { "label": "Height" }, "height": { "label": "Height" },
"width": { "label": "Width" } "width": { "label": "Width" }
},
"evolved": {
"maximumTier": { "label": "Maximum Tier" },
"mainTraitBonus": { "label": "Main Trait Bonus" }
},
"hybrid": {
"beastformOptions": { "label": "Nr Beastforms" },
"advantages": { "label": "Nr Advantages" },
"features": { "label": "Nr Features" }
} }
}, },
"attackName": "Beast Attack",
"beastformEffect": "Beastform Transformation",
"dialogTitle": "Beastform Selection", "dialogTitle": "Beastform Selection",
"tokenTitle": "Beastform Token", "tokenTitle": "Beastform Token",
"transform": "Transform", "transform": "Transform",
"beastformEffect": "Beastform Transformation" "evolve": "Evolve",
"evolvedFeatureTitle": "Evolved",
"evolvedDrag": "Drag a form here to evolve it.",
"hybridize": "Hybridize",
"hybridizeFeatureTitle": "Hybrid Features",
"hybridizeDrag": "Drag a form here to hybridize it."
}, },
"Class": { "Class": {
"hopeFeatures": "Hope Features", "hopeFeatures": "Hope Features",
@ -1155,8 +1445,8 @@
}, },
"DomainCard": { "DomainCard": {
"type": "Type", "type": "Type",
"foundation": "Foundation",
"recallCost": "Recall Cost", "recallCost": "Recall Cost",
"foundationTitle": "Foundation",
"specializationTitle": "Specialization", "specializationTitle": "Specialization",
"masteryTitle": "Mastery" "masteryTitle": "Mastery"
}, },
@ -1164,6 +1454,7 @@
"spellcastingTrait": "Spellcasting Trait" "spellcastingTrait": "Spellcasting Trait"
}, },
"Weapon": { "Weapon": {
"weaponType": "Weapon Type",
"primaryWeapon": "Primary Weapon", "primaryWeapon": "Primary Weapon",
"secondaryWeapon": "Secondary Weapon" "secondaryWeapon": "Secondary Weapon"
} }
@ -1171,7 +1462,8 @@
"SETTINGS": { "SETTINGS": {
"Appearance": { "Appearance": {
"FIELDS": { "FIELDS": {
"displayFear": { "label": "Fear Display" } "displayFear": { "label": "Fear Display" },
"showGenericStatusEffects": { "label": "Show Foundry Status Effects" }
}, },
"fearDisplay": { "fearDisplay": {
"token": "Tokens", "token": "Tokens",
@ -1185,17 +1477,18 @@
"hint": "Automatically increase the GM's fear pool on a fear duality roll result." "hint": "Automatically increase the GM's fear pool on a fear duality roll result."
}, },
"FIELDS": { "FIELDS": {
"hope": { "hopeFear": {
"label": "Hope", "label": "Hope & Fear",
"hint": "Automatically increase a character's hope on a hope duality roll result." "gm": { "label": "GM" },
"players": { "label": "Players" }
}, },
"actionPoints": { "actionPoints": {
"label": "Action Points", "label": "Action Points",
"hint": "Automatically give and take Action Points as combatants take their turns." "hint": "Automatically give and take Action Points as combatants take their turns."
}, },
"countdowns": { "hordeDamage": {
"label": "Countdowns", "label": "Automatic Horde Damage",
"hint": "Automatically progress non-custom countdowns" "hint": "Automatically active horde effect to lower damage when reaching half or lower HP."
} }
} }
}, },
@ -1226,6 +1519,7 @@
} }
}, },
"Menu": { "Menu": {
"title": "Daggerheart Game Settings",
"automation": { "automation": {
"name": "Automation Settings", "name": "Automation Settings",
"label": "Configure Automation", "label": "Configure Automation",
@ -1285,8 +1579,8 @@
}, },
"UI": { "UI": {
"Chat": { "Chat": {
"dualityRoll": { "applyEffect": {
"abilityCheckTitle": "{ability} Check" "title": "Apply Effects - {name}"
}, },
"attackRoll": { "attackRoll": {
"title": "Attack - {attack}", "title": "Attack - {attack}",
@ -1302,25 +1596,28 @@
"hitTarget": "Hit Targets", "hitTarget": "Hit Targets",
"selectedTarget": "Selected" "selectedTarget": "Selected"
}, },
"applyEffect": {
"title": "Apply Effects - {name}"
},
"healingRoll": {
"title": "Heal - {healing}",
"heal": "Heal"
},
"deathMove": { "deathMove": {
"title": "Death Move" "title": "Death Move"
}, },
"domainCard": { "domainCard": {
"title": "Domain Card" "title": "Domain Card"
}, },
"dualityRoll": {
"abilityCheckTitle": "{ability} Check"
},
"featureTitle": "Class Feature",
"foundationCard": { "foundationCard": {
"ancestryTitle": "Ancestry Card", "ancestryTitle": "Ancestry Card",
"communityTitle": "Community Card", "communityTitle": "Community Card",
"subclassFeatureTitle": "Subclass Feature" "subclassFeatureTitle": "Subclass Feature"
}, },
"featureTitle": "Class Feature" "healingRoll": {
"title": "Heal - {healing}",
"heal": "Heal"
},
"resourceRoll": {
"playerMessage": "{user} rerolled their {name}"
}
}, },
"Notifications": { "Notifications": {
"adversaryMissing": "The linked adversary doesn't exist in the world.", "adversaryMissing": "The linked adversary doesn't exist in the world.",
@ -1360,9 +1657,27 @@
"damageAlreadyNone": "The damage has already been reduced to none", "damageAlreadyNone": "The damage has already been reduced to none",
"noAvailableArmorMarks": "You have no more available armor marks", "noAvailableArmorMarks": "You have no more available armor marks",
"notEnoughStress": "You don't have enough stress", "notEnoughStress": "You don't have enough stress",
"damageIgnore": "{character} did not take damage" "damageIgnore": "{character} did not take damage",
"featureIsMissing": "Feature is missing",
"actionIsMissing": "Action is missing",
"attackIsMissing": "Attack is missing",
"unownedActionMacro": "Cannot make a Use macro for an Action not on your character",
"unownedAttackMacro": "Cannot make a Use macro for an Attack that doesn't belong to one of your characters",
"featureNotHope": "This feature is used as something else than a Hope feature and cannot be used here.",
"featureNotClass": "This feature is used as something else than a Class feature and cannot be used here.",
"featureNotPrimary": "This feature is used as something else than a Primary feature and cannot be used here.",
"featureNotSecondary": "This feature is used as something else than a Secondary feature and cannot be used here.",
"featureNotFoundation": "This feature is used as something else than a Foundation feature and cannot be used here.",
"featureNotSpecialization": "This feature is used as something else than a Specialization feature and cannot be used here.",
"featureNotMastery": "This feature is used as something else than a Mastery feature and cannot be used here.",
"beastformMissingEffect": "The Beastform is missing a Beastform Effect. Cannot be used.",
"beastformToManyAdvantages": "You cannot select any more advantages.",
"beastformToManyFeatures": "You cannot select any more features.",
"beastformEquipWeapon": "You cannot use weapons while in a Beastform."
}, },
"Tooltip": { "Tooltip": {
"disableEffect": "Disable Effect",
"enableEffect": "Enable Effect",
"openItemWorld": "Open Item World", "openItemWorld": "Open Item World",
"openActorWorld": "Open Actor World", "openActorWorld": "Open Actor World",
"sendToChat": "Send to Chat", "sendToChat": "Send to Chat",
@ -1371,7 +1686,10 @@
"unequip": "Unequip", "unequip": "Unequip",
"sendToVault": "Send to Vault", "sendToVault": "Send to Vault",
"sendToLoadout": "Send to Loadout", "sendToLoadout": "Send to Loadout",
"makeDeathMove": "Make a Death Move" "makeDeathMove": "Make a Death Move",
"rangeAndTarget": "Range & Target",
"dragApplyEffect": "Drag effect to apply it to an actor",
"appliedEvenIfSuccessful": "Applied even if save succeeded"
} }
} }
} }

View file

@ -1,5 +1,6 @@
export * as characterCreation from './characterCreation/_module.mjs'; export * as characterCreation from './characterCreation/_module.mjs';
export * as dialogs from './dialogs/_module.mjs'; export * as dialogs from './dialogs/_module.mjs';
export * as hud from './hud/_module.mjs';
export * as levelup from './levelup/_module.mjs'; export * as levelup from './levelup/_module.mjs';
export * as settings from './settings/_module.mjs'; export * as settings from './settings/_module.mjs';
export * as sheets from './sheets/_module.mjs'; export * as sheets from './sheets/_module.mjs';

View file

@ -11,13 +11,16 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
this.setup = { this.setup = {
traits: this.character.system.traits, traits: this.character.system.traits,
ancestry: this.character.system.ancestry ?? {}, ancestryName: '',
mixedAncestry: false,
primaryAncestry: this.character.system.ancestry ?? {},
secondaryAncestry: {},
community: this.character.system.community ?? {}, community: this.character.system.community ?? {},
class: this.character.system.class?.value ?? {}, class: this.character.system.class?.value ?? {},
subclass: this.character.system.class?.subclass ?? {}, subclass: this.character.system.class?.subclass ?? {},
experiences: { experiences: {
[foundry.utils.randomID()]: { description: '', value: 2 }, [foundry.utils.randomID()]: { name: '', value: 2 },
[foundry.utils.randomID()]: { description: '', value: 2 } [foundry.utils.randomID()]: { name: '', value: 2 }
}, },
domainCards: { domainCards: {
[foundry.utils.randomID()]: {}, [foundry.utils.randomID()]: {},
@ -47,12 +50,13 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
classes: ['daggerheart', 'dialog', 'dh-style', 'character-creation'], classes: ['daggerheart', 'dialog', 'dh-style', 'character-creation'],
position: { width: 800, height: 'auto' }, position: { width: 700, height: 'auto' },
actions: { actions: {
viewCompendium: this.viewCompendium, viewCompendium: this.viewCompendium,
viewItem: this.viewItem, viewItem: this.viewItem,
useSuggestedTraits: this.useSuggestedTraits, useSuggestedTraits: this.useSuggestedTraits,
equipmentChoice: this.equipmentChoice, equipmentChoice: this.equipmentChoice,
setupGoNext: this.setupGoNext,
finish: this.finish finish: this.finish
}, },
form: { form: {
@ -76,6 +80,12 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
static PARTS = { static PARTS = {
tabs: { template: 'systems/daggerheart/templates/characterCreation/tabs.hbs' }, tabs: { template: 'systems/daggerheart/templates/characterCreation/tabs.hbs' },
setup: { template: 'systems/daggerheart/templates/characterCreation/tabs/setup.hbs' }, setup: { template: 'systems/daggerheart/templates/characterCreation/tabs/setup.hbs' },
ancestry: { template: 'systems/daggerheart/templates/characterCreation/setupTabs/ancestry.hbs' },
community: { template: 'systems/daggerheart/templates/characterCreation/setupTabs/community.hbs' },
class: { template: 'systems/daggerheart/templates/characterCreation/setupTabs/class.hbs' },
traits: { template: 'systems/daggerheart/templates/characterCreation/setupTabs/traits.hbs' },
experience: { template: 'systems/daggerheart/templates/characterCreation/setupTabs/experience.hbs' },
domainCards: { template: 'systems/daggerheart/templates/characterCreation/setupTabs/domainCards.hbs' },
equipment: { template: 'systems/daggerheart/templates/characterCreation/tabs/equipment.hbs' }, equipment: { template: 'systems/daggerheart/templates/characterCreation/tabs/equipment.hbs' },
// story: { template: 'systems/daggerheart/templates/characterCreation/tabs/story.hbs' }, // story: { template: 'systems/daggerheart/templates/characterCreation/tabs/story.hbs' },
footer: { template: 'systems/daggerheart/templates/characterCreation/footer.hbs' } footer: { template: 'systems/daggerheart/templates/characterCreation/footer.hbs' }
@ -107,6 +117,51 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
// } // }
}; };
static SETUPTABS = {
ancestry: {
active: true,
cssClass: '',
group: 'setup',
id: 'ancestry',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.setupTabs.ancestry'
},
community: {
active: false,
cssClass: '',
group: 'setup',
id: 'community',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.setupTabs.community'
},
class: {
active: false,
cssClass: '',
group: 'setup',
id: 'class',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.setupTabs.class'
},
traits: {
active: false,
cssClass: '',
group: 'setup',
id: 'traits',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.setupTabs.traits'
},
experience: {
active: false,
cssClass: '',
group: 'setup',
id: 'experience',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.setupTabs.experience'
},
domainCards: {
active: false,
cssClass: '',
group: 'setup',
id: 'domainCards',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.setupTabs.domainCards'
}
};
_getTabs(tabs) { _getTabs(tabs) {
for (const v of Object.values(tabs)) { for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active; v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active;
@ -114,14 +169,16 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
switch (v.id) { switch (v.id) {
case 'setup': case 'setup':
const ancestryFinished = this.setup.primaryAncestry.uuid;
const communityFinished = this.setup.community.uuid;
const classFinished = this.setup.class.uuid && this.setup.subclass.uuid; const classFinished = this.setup.class.uuid && this.setup.subclass.uuid;
const heritageFinished = this.setup.ancestry.uuid && this.setup.community.uuid;
const traitsFinished = Object.values(this.setup.traits).every(x => x.value !== null); const traitsFinished = Object.values(this.setup.traits).every(x => x.value !== null);
const experiencesFinished = Object.values(this.setup.experiences).every(x => x.description); const experiencesFinished = Object.values(this.setup.experiences).every(x => x.name);
const domainCardsFinished = Object.values(this.setup.domainCards).every(x => x.uuid); const domainCardsFinished = Object.values(this.setup.domainCards).every(x => x.uuid);
v.finished = v.finished =
ancestryFinished &&
communityFinished &&
classFinished && classFinished &&
heritageFinished &&
traitsFinished && traitsFinished &&
experiencesFinished && experiencesFinished &&
domainCardsFinished; domainCardsFinished;
@ -146,15 +203,44 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
return tabs; return tabs;
} }
_getSetupTabs(tabs) {
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active;
v.cssClass = v.active ? 'active' : '';
switch (v.id) {
case 'community':
v.disabled = this.setup.visibility < 2;
break;
case 'class':
v.disabled = this.setup.visibility < 3;
break;
case 'traits':
v.disabled = this.setup.visibility < 4;
break;
case 'experience':
v.disabled = this.setup.visibility < 5;
break;
case 'domainCards':
v.disabled = this.setup.visibility < 6;
break;
}
}
return tabs;
}
changeTab(tab, group, options) { changeTab(tab, group, options) {
super.changeTab(tab, group, options); super.changeTab(tab, group, options);
for (var listTab of Object.keys(this.constructor.TABS)) { if (group === 'primary') {
const marker = options.navElement.querySelector(`a[data-action="tab"].${listTab} .finish-marker`); for (var listTab of Object.keys(this.constructor.TABS)) {
if (listTab === tab) { const marker = options.navElement.querySelector(`a[data-action="tab"].${listTab} .finish-marker`);
marker.classList.add('active'); if (listTab === tab) {
} else { marker.classList.add('active');
marker.classList.remove('active'); } else {
marker.classList.remove('active');
}
} }
} }
} }
@ -163,6 +249,11 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
super._attachPartListeners(partId, htmlElement, options); super._attachPartListeners(partId, htmlElement, options);
this._dragDrop.forEach(d => d.bind(htmlElement)); this._dragDrop.forEach(d => d.bind(htmlElement));
htmlElement.querySelectorAll('.mixed-ancestry-slider').forEach(element => {
element.addEventListener('input', this.mixedAncestryToggle.bind(this));
element.addEventListener('click', this.mixedAncestryToggle.bind(this));
});
} }
async _prepareContext(_options) { async _prepareContext(_options) {
@ -174,7 +265,30 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
async _preparePartContext(partId, context) { async _preparePartContext(partId, context) {
switch (partId) { switch (partId) {
case 'footer':
context.isLastTab = this.tabGroups.setup === 'domainCards';
switch (this.tabGroups.setup) {
case null:
case 'ancestry':
context.nextDisabled = this.setup.visibility === 1;
break;
case 'community':
context.nextDisabled = this.setup.visibility === 2;
break;
case 'class':
context.nextDisabled = this.setup.visibility === 3;
break;
case 'traits':
context.nextDisabled = this.setup.visibility === 4;
break;
case 'experience':
context.nextDisabled = this.setup.visibility === 5;
break;
}
break;
case 'setup': case 'setup':
context.setupTabs = this._getSetupTabs(this.constructor.SETUPTABS);
const availableTraitModifiers = game.settings const availableTraitModifiers = game.settings
.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew) .get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew)
.traitArray.map(trait => ({ key: trait, name: trait })); .traitArray.map(trait => ({ key: trait, name: trait }));
@ -215,13 +329,13 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
context.experience = { context.experience = {
values: this.setup.experiences, values: this.setup.experiences,
nrTotal: Object.keys(this.setup.experiences).length, nrTotal: Object.keys(this.setup.experiences).length,
nrSelected: Object.values(this.setup.experiences).reduce( nrSelected: Object.values(this.setup.experiences).reduce((acc, exp) => acc + (exp.name ? 1 : 0), 0)
(acc, exp) => acc + (exp.description ? 1 : 0),
0
)
}; };
context.ancestry = { ...this.setup.ancestry, compendium: 'ancestries' }; context.mixedAncestry = Number(this.setup.mixedAncestry);
context.ancestryName = this.setup.ancestryName;
context.primaryAncestry = { ...this.setup.primaryAncestry, compendium: 'ancestries' };
context.secondaryAncestry = { ...this.setup.secondaryAncestry, compendium: 'ancestries' };
context.community = { ...this.setup.community, compendium: 'communities' }; context.community = { ...this.setup.community, compendium: 'communities' };
context.class = { ...this.setup.class, compendium: 'classes' }; context.class = { ...this.setup.class, compendium: 'classes' };
context.subclass = { ...this.setup.subclass, compendium: 'subclasses' }; context.subclass = { ...this.setup.subclass, compendium: 'subclasses' };
@ -278,18 +392,29 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
this.render(); this.render();
} }
mixedAncestryToggle(event) {
event.preventDefault();
event.stopPropagation();
this.setup.mixedAncestry = !this.setup.mixedAncestry;
if (!this.setup.mixedAncestry) this.setup.secondaryAncestry = {};
this.render();
}
getUpdateVisibility() { getUpdateVisibility() {
switch (this.setup.visibility) { switch (this.setup.visibility) {
case 6:
return 6;
case 5: case 5:
return 5; return Object.values(this.setup.experiences).every(x => x.name) ? 6 : 5;
case 4: case 4:
return Object.values(this.setup.experiences).every(x => x.description) ? 5 : 4; return Object.values(this.setup.traits).every(x => x.value !== null) ? 5 : 4;
case 3: case 3:
return Object.values(this.setup.traits).every(x => x.value !== null) ? 4 : 3; return this.setup.class.uuid && this.setup.subclass.uuid ? 4 : 3;
case 2: case 2:
return this.setup.ancestry.uuid && this.setup.community.uuid ? 3 : 2; return this.setup.community.uuid ? 3 : 2;
case 1: case 1:
return this.setup.class.uuid && this.setup.subclass.uuid ? 2 : 1; return this.setup.primaryAncestry.uuid ? 2 : 1;
} }
} }
@ -348,8 +473,44 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
this.render(); this.render();
} }
static setupGoNext() {
switch (this.setup.visibility) {
case 2:
this.tabGroups.setup = 'community';
break;
case 3:
this.tabGroups.setup = 'class';
break;
case 4:
this.tabGroups.setup = 'traits';
break;
case 5:
this.tabGroups.setup = 'experience';
break;
case 6:
this.tabGroups.setup = 'domainCards';
break;
}
this.render();
}
static async finish() { static async finish() {
await this.character.createEmbeddedDocuments('Item', [this.setup.ancestry]); const primaryAncestryFeature = this.setup.primaryAncestry.system.primaryFeature;
const secondaryAncestryFeature = this.setup.secondaryAncestry?.uuid
? this.setup.secondaryAncestry.system.secondaryFeature
: this.setup.primaryAncestry.system.secondaryFeature;
const ancestry = {
...this.setup.primaryAncestry,
name: this.setup.ancestryName ?? this.setup.primaryAncestry.name,
system: {
...this.setup.primaryAncestry.system,
features: [primaryAncestryFeature.uuid, secondaryAncestryFeature.uuid]
}
};
await this.character.createEmbeddedDocuments('Item', [ancestry]);
await this.character.createEmbeddedDocuments('Item', [this.setup.community]); await this.character.createEmbeddedDocuments('Item', [this.setup.community]);
await this.character.createEmbeddedDocuments('Item', [this.setup.class]); await this.character.createEmbeddedDocuments('Item', [this.setup.class]);
await this.character.createEmbeddedDocuments('Item', [this.setup.subclass]); await this.character.createEmbeddedDocuments('Item', [this.setup.subclass]);
@ -396,8 +557,15 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
async _onDrop(event) { async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await foundry.utils.fromUuid(data.uuid); const item = await foundry.utils.fromUuid(data.uuid);
if (item.type === 'ancestry' && event.target.closest('.ancestry-card')) { if (item.type === 'ancestry' && event.target.closest('.primary-ancestry-card')) {
this.setup.ancestry = { this.setup.ancestryName = item.name;
this.setup.primaryAncestry = {
...item,
effects: Array.from(item.effects).map(x => x.toObject()),
uuid: item.uuid
};
} else if (item.type === 'ancestry' && event.target.closest('.secondary-ancestry-card')) {
this.setup.secondaryAncestry = {
...item, ...item,
effects: Array.from(item.effects).map(x => x.toObject()), effects: Array.from(item.effects).map(x => x.toObject()),
uuid: item.uuid uuid: item.uuid

View file

@ -1,5 +1,4 @@
export { default as BeastformDialog } from './beastformDialog.mjs'; export { default as BeastformDialog } from './beastformDialog.mjs';
export { default as costSelectionDialog } from './costSelectionDialog.mjs';
export { default as d20RollDialog } from './d20RollDialog.mjs'; export { default as d20RollDialog } from './d20RollDialog.mjs';
export { default as DamageDialog } from './damageDialog.mjs'; export { default as DamageDialog } from './damageDialog.mjs';
export { default as DamageReductionDialog } from './damageReductionDialog.mjs'; export { default as DamageReductionDialog } from './damageReductionDialog.mjs';
@ -7,3 +6,4 @@ export { default as DamageSelectionDialog } from './damageSelectionDialog.mjs';
export { default as DeathMove } from './deathMove.mjs'; export { default as DeathMove } from './deathMove.mjs';
export { default as Downtime } from './downtime.mjs'; export { default as Downtime } from './downtime.mjs';
export { default as OwnershipSelection } from './ownershipSelection.mjs'; export { default as OwnershipSelection } from './ownershipSelection.mjs';
export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs';

View file

@ -1,67 +1,261 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class BeastformDialog extends HandlebarsApplicationMixin(ApplicationV2) { export default class BeastformDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(configData) { constructor(configData, item) {
super(); super();
this.item = item;
this.configData = configData; this.configData = configData;
this.selected = null; this.selected = null;
this.evolved = { form: null };
this.hybrid = { forms: {}, advantages: {}, features: {} };
this._dragDrop = this._createDragDropHandlers();
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
classes: ['daggerheart', 'views', 'dh-style', 'beastform-selection'], classes: ['daggerheart', 'views', 'dialog', 'dh-style', 'beastform-selection'],
position: { position: {
width: 600, width: 600,
height: 'auto' height: 'auto'
}, },
window: {
icon: 'fa-solid fa-paw'
},
actions: { actions: {
selectBeastform: this.selectBeastform, selectBeastform: this.selectBeastform,
toggleHybridFeature: this.toggleHybridFeature,
toggleHybridAdvantage: this.toggleHybridAdvantage,
submitBeastform: this.submitBeastform submitBeastform: this.submitBeastform
}, },
form: { form: {
handler: this.updateBeastform, handler: this.updateBeastform,
submitOnChange: true, submitOnChange: true,
submitOnClose: false submitOnClose: false
} },
dragDrop: [{ dragSelector: '.beastform-container', dropSelector: '.advanced-form-container' }]
}; };
get title() { get title() {
return game.i18n.localize('DAGGERHEART.ITEMS.Beastform.dialogTitle'); return this.item.name;
} }
/** @override */ /** @override */
static PARTS = { static PARTS = {
beastform: { header: { template: 'systems/daggerheart/templates/dialogs/beastform/header.hbs' },
template: 'systems/daggerheart/templates/dialogs/beastformDialog.hbs' tabs: { template: 'systems/daggerheart/templates/dialogs/beastform/tabs.hbs' },
beastformTier: { template: 'systems/daggerheart/templates/dialogs/beastform/beastformTier.hbs' },
advanced: { template: 'systems/daggerheart/templates/dialogs/beastform/advanced.hbs' },
footer: { template: 'systems/daggerheart/templates/dialogs/beastform/footer.hbs' }
};
/** @inheritdoc */
static TABS = {
primary: {
tabs: [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }],
initial: '1',
labelPrefix: 'DAGGERHEART.GENERAL.Tiers'
} }
}; };
changeTab(tab, group, options) {
super.changeTab(tab, group, options);
this.render();
}
_createDragDropHandlers() {
return this.options.dragDrop.map(d => {
d.callbacks = {
dragstart: this._onDragStart.bind(this),
drop: this._onDrop.bind(this)
};
return new foundry.applications.ux.DragDrop.implementation(d);
});
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
this._dragDrop.forEach(d => d.bind(htmlElement));
}
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.beastformTiers = game.items.reduce((acc, x) => { context.selected = this.selected;
const tier = CONFIG.DH.GENERAL.tiers[x.system.tier]; context.selectedBeastformEffect = this.selected?.effects?.find?.(x => x.type === 'beastform');
if (x.type !== 'beastform' || tier.value > this.configData.tierLimit) return acc;
if (!acc[tier.value]) acc[tier.value] = { label: game.i18n.localize(tier.label), values: {} }; context.evolved = this.evolved;
acc[tier.value].values[x.uuid] = { selected: this.selected == x.uuid, value: x };
context.hybridForms = Object.keys(this.hybrid.forms).reduce((acc, formKey) => {
if (!this.hybrid.forms[formKey]) {
acc[formKey] = null;
} else {
const data = this.hybrid.forms[formKey].toObject();
acc[formKey] = {
...data,
system: {
...data.system,
features: this.hybrid.forms[formKey].system.features.map(feature => ({
...feature.toObject(),
uuid: feature.uuid,
selected: Boolean(this.hybrid.features?.[formKey]?.[feature.uuid])
})),
advantageOn: Object.keys(data.system.advantageOn).reduce((acc, key) => {
acc[key] = {
...data.system.advantageOn[key],
selected: Boolean(this.hybrid.advantages?.[formKey]?.[key])
};
return acc;
}, {})
}
};
}
return acc; return acc;
}, {}); // Also get from compendium when added }, {});
context.canSubmit = this.selected;
const maximumDragTier = Math.max(
this.selected?.system?.evolved?.maximumTier ?? 0,
this.selected?.system?.hybrid?.maximumTier ?? 0
);
const compendiumBeastforms = await game.packs.get(`daggerheart.beastforms`)?.getDocuments();
const beastformTiers = [...(compendiumBeastforms ? compendiumBeastforms : []), ...game.items].reduce(
(acc, x) => {
const tier = CONFIG.DH.GENERAL.tiers[x.system.tier];
if (x.type !== 'beastform' || tier.id > this.configData.tierLimit) return acc;
if (!acc[tier.id]) acc[tier.id] = { label: game.i18n.localize(tier.label), values: {} };
acc[tier.id].values[x.uuid] = {
selected: this.selected?.uuid == x.uuid,
value: x,
draggable:
!['evolved', 'hybrid'].includes(x.system.beastformType) && maximumDragTier
? x.system.tier <= maximumDragTier
: false
};
return acc;
},
{}
);
context.tier = beastformTiers[this.tabGroups.primary];
context.tierKey = this.tabGroups.primary;
context.canSubmit = this.canSubmit();
return context; return context;
} }
canSubmit() {
if (this.selected) {
switch (this.selected.system.beastformType) {
case 'normal':
return true;
case 'evolved':
return this.evolved.form;
case 'hybrid':
const selectedAdvantages = Object.values(this.hybrid.advantages).reduce(
(acc, form) => acc + Object.values(form).length,
0
);
const selectedFeatures = Object.values(this.hybrid.features).reduce(
(acc, form) => acc + Object.values(form).length,
0
);
const advantagesSelected = selectedAdvantages === this.selected.system.hybrid.advantages;
const featuresSelected = selectedFeatures === this.selected.system.hybrid.features;
return advantagesSelected && featuresSelected;
}
}
return false;
}
static updateBeastform(event, _, formData) { static updateBeastform(event, _, formData) {
this.selected = foundry.utils.mergeObject(this.selected, formData.object); this.selected = foundry.utils.mergeObject(this.selected, formData.object);
this.render(); this.render();
} }
static selectBeastform(_, target) { static async selectBeastform(_, target) {
this.selected = this.selected === target.dataset.uuid ? null : target.dataset.uuid; this.element.querySelectorAll('.beastform-container ').forEach(element => {
if (element.dataset.uuid === target.dataset.uuid && this.selected?.uuid !== target.dataset.uuid) {
element.classList.remove('inactive');
} else {
element.classList.add('inactive');
}
});
const uuid = this.selected?.uuid === target.dataset.uuid ? null : target.dataset.uuid;
this.selected = uuid ? await foundry.utils.fromUuid(uuid) : null;
if (this.selected) {
if (this.selected.system.beastformType !== 'evolved') this.evolved.form = null;
if (this.selected.system.beastformType !== 'hybrid') {
this.hybrid.forms = {};
this.hybrid.advantages = {};
this.hybrid.features = {};
} else {
this.hybrid.forms = [...Array(this.selected.system.hybrid.beastformOptions).keys()].reduce((acc, _) => {
acc[foundry.utils.randomID()] = null;
return acc;
}, {});
}
}
this.render();
}
static toggleHybridFeature(_, button) {
const current = this.hybrid.features[button.dataset.form];
if (!current) this.hybrid.features[button.dataset.form] = {};
if (this.hybrid.features[button.dataset.form][button.id])
delete this.hybrid.features[button.dataset.form][button.id];
else {
const currentFeatures = Object.values(this.hybrid.features).reduce(
(acc, form) => acc + Object.values(form).length,
0
);
if (currentFeatures === this.selected.system.hybrid.features) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.beastformToManyFeatures'));
return;
}
const feature = this.hybrid.forms[button.dataset.form].system.features.find(x => x.uuid === button.id);
this.hybrid.features[button.dataset.form][button.id] = feature;
}
this.render();
}
static toggleHybridAdvantage(_, button) {
const current = this.hybrid.advantages[button.dataset.form];
if (!current) this.hybrid.advantages[button.dataset.form] = {};
if (this.hybrid.advantages[button.dataset.form][button.id])
delete this.hybrid.advantages[button.dataset.form][button.id];
else {
const currentAdvantages = Object.values(this.hybrid.advantages).reduce(
(acc, form) => acc + Object.values(form).length,
0
);
if (currentAdvantages === this.selected.system.hybrid.advantages) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.beastformToManyAdvantages'));
return;
}
const advantage = this.hybrid.forms[button.dataset.form].system.advantageOn[button.id];
this.hybrid.advantages[button.dataset.form][button.id] = advantage;
}
this.render(); this.render();
} }
@ -71,14 +265,61 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
/** @override */ /** @override */
_onClose(options = {}) { _onClose(options = {}) {
if (!options.submitted) this.config = false; if (!options.submitted) this.selected = null;
} }
static async configure(configData) { static async configure(configData, item) {
return new Promise(resolve => { return new Promise(resolve => {
const app = new this(configData); const app = new this(configData, item);
app.addEventListener('close', () => resolve(app.selected), { once: true }); const featureItem = item;
app.addEventListener(
'close',
() => resolve({ selected: app.selected, evolved: app.evolved, hybrid: app.hybrid, item: featureItem }),
{ once: true }
);
app.render({ force: true }); app.render({ force: true });
}); });
} }
async _onDragStart(event) {
const target = event.currentTarget;
const abort = () => event.preventDefault();
if (!this.selected) abort();
const draggedForm = await foundry.utils.fromUuid(target.dataset.uuid);
if (['evolved', 'hybrid'].includes(draggedForm.system.beastformType)) abort();
if (this.selected.system.beastformType === 'evolved') {
if (draggedForm.system.tier > this.selected.system.evolved.maximumTier) abort();
}
if (this.selected.system.beastformType === 'hybrid') {
if (draggedForm.system.tier > this.selected.system.hybrid.maximumTier) abort();
}
event.dataTransfer.setData('text/plain', JSON.stringify(target.dataset));
event.dataTransfer.setDragImage(target, 60, 0);
}
async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
if (!item) return;
if (event.target.closest('.advanced-form-container.evolved')) {
this.evolved.form = item;
} else {
const hybridContainer = event.target.closest('.advanced-form-container.hybridized');
if (hybridContainer) {
const existingId = Object.keys(this.hybrid.forms).find(
key => this.hybrid.forms[key]?.uuid === item.uuid
);
if (existingId) this.hybrid.forms[existingId] = null;
this.hybrid.forms[hybridContainer.id] = item;
}
}
this.render();
}
} }

View file

@ -1,66 +0,0 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class CostSelectionDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(costs, uses, action, resolve) {
super({});
this.costs = costs;
this.uses = uses;
this.action = action;
this.resolve = resolve;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'views', 'damage-selection'],
position: {
width: 400,
height: 'auto'
},
actions: {
sendCost: this.sendCost
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
}
};
/** @override */
static PARTS = {
costSelection: {
id: 'costSelection',
template: 'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs'
}
};
/* -------------------------------------------- */
/** @inheritDoc */
get title() {
return `Cost Options`;
}
async _prepareContext(_options) {
const updatedCosts = this.action.calcCosts(this.costs),
updatedUses = this.action.calcUses(this.uses);
return {
costs: updatedCosts,
uses: updatedUses,
canUse: this.action.hasCost(updatedCosts) && this.action.hasUses(updatedUses)
};
}
static async updateForm(event, _, formData) {
const data = foundry.utils.expandObject(formData.object);
this.costs = foundry.utils.mergeObject(this.costs, data.costs);
this.uses = foundry.utils.mergeObject(this.uses, data.uses);
this.render(true);
}
static sendCost(event) {
event.preventDefault();
this.resolve({ costs: this.action.getRealCosts(this.costs), uses: this.uses });
this.close();
}
}

View file

@ -22,7 +22,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
id: 'roll-selection', id: 'roll-selection',
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'roll-selection'], classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'roll-selection'],
position: { position: {
width: 550 width: 'auto'
}, },
window: { window: {
icon: 'fa-solid fa-dice' icon: 'fa-solid fa-dice'
@ -52,10 +52,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
rollSelection: { rollSelection: {
id: 'rollSelection', id: 'rollSelection',
template: 'systems/daggerheart/templates/dialogs/dice-roll/rollSelection.hbs' template: 'systems/daggerheart/templates/dialogs/dice-roll/rollSelection.hbs'
},
costSelection: {
id: 'costSelection',
template: 'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs'
} }
}; };
@ -63,9 +59,22 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.rollConfig = this.config; context.rollConfig = this.config;
context.hasRoll = !!this.config.roll; context.hasRoll = !!this.config.roll;
context.canRoll = true;
context.selectedRollMode = this.config.selectedRollMode;
context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({
action,
label,
icon
}));
if (this.config.costs?.length) { if (this.config.costs?.length) {
const updatedCosts = this.action.calcCosts(this.config.costs); const updatedCosts = this.action.calcCosts(this.config.costs);
context.costs = updatedCosts; context.costs = updatedCosts.map(x => ({
...x,
label: x.keyIsID
? this.action.parent.parent.name
: game.i18n.localize(CONFIG.DH.GENERAL.abilityCosts[x.key].label)
}));
context.canRoll = this.action.hasCost(updatedCosts); context.canRoll = this.action.hasCost(updatedCosts);
this.config.data.scale = this.config.costs[0].total; this.config.data.scale = this.config.costs[0].total;
} }
@ -73,9 +82,10 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.uses = this.action.calcUses(this.config.uses); context.uses = this.action.calcUses(this.config.uses);
context.canRoll = context.canRoll && this.action.hasUses(context.uses); context.canRoll = context.canRoll && this.action.hasUses(context.uses);
} }
if(this.roll) { if (this.roll) {
context.roll = this.roll; context.roll = this.roll;
context.rollType = this.roll?.constructor.name; context.rollType = this.roll?.constructor.name;
context.rallyDie = this.roll.rallyChoices;
context.experiences = Object.keys(this.config.data.experiences).map(id => ({ context.experiences = Object.keys(this.config.data.experiences).map(id => ({
id, id,
...this.config.data.experiences[id] ...this.config.data.experiences[id]
@ -84,7 +94,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.advantage = this.config.roll?.advantage; context.advantage = this.config.roll?.advantage;
context.disadvantage = this.config.roll?.disadvantage; context.disadvantage = this.config.roll?.disadvantage;
context.diceOptions = CONFIG.DH.GENERAL.diceTypes; context.diceOptions = CONFIG.DH.GENERAL.diceTypes;
context.canRoll = true;
context.isLite = this.config.roll?.lite; context.isLite = this.config.roll?.lite;
context.extraFormula = this.config.extraFormula; context.extraFormula = this.config.extraFormula;
context.formula = this.roll.constructFormula(this.config); context.formula = this.roll.constructFormula(this.config);
@ -94,6 +103,8 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
static updateRollConfiguration(event, _, formData) { static updateRollConfiguration(event, _, formData) {
const { ...rest } = foundry.utils.expandObject(formData.object); const { ...rest } = foundry.utils.expandObject(formData.object);
this.config.selectedRollMode = rest.selectedRollMode;
if (this.config.costs) { if (this.config.costs) {
this.config.costs = foundry.utils.mergeObject(this.config.costs, rest.costs); this.config.costs = foundry.utils.mergeObject(this.config.costs, rest.costs);
} }
@ -117,11 +128,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
} }
static selectExperience(_, button) { static selectExperience(_, button) {
/* if (this.config.experiences.find(x => x === button.dataset.key)) {
this.config.experiences = this.config.experiences.filter(x => x !== button.dataset.key);
} else {
this.config.experiences = [...this.config.experiences, button.dataset.key];
} */
this.config.experiences = this.config.experiences =
this.config.experiences.indexOf(button.dataset.key) > -1 this.config.experiences.indexOf(button.dataset.key) > -1
? this.config.experiences.filter(x => x !== button.dataset.key) ? this.config.experiences.filter(x => x !== button.dataset.key)

View file

@ -11,11 +11,14 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
id: 'roll-selection', id: 'roll-selection',
classes: ['daggerheart', 'views', 'damage-selection'], classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'damage-selection'],
position: { position: {
width: 400, width: 400,
height: 'auto' height: 'auto'
}, },
window: {
icon: 'fa-solid fa-dice'
},
actions: { actions: {
submitRoll: this.submitRoll submitRoll: this.submitRoll
}, },
@ -34,17 +37,33 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
} }
}; };
get title() {
return game.i18n.localize('DAGGERHEART.EFFECTS.ApplyLocations.damageRoll.name');
}
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.title = this.config.title; context.config = CONFIG.DH;
context.extraFormula = this.config.extraFormula; context.title = this.config.title
? this.config.title
: game.i18n.localize('DAGGERHEART.EFFECTS.ApplyLocations.damageRoll.name');
// context.extraFormula = this.config.extraFormula;
context.formula = this.roll.constructFormula(this.config); context.formula = this.roll.constructFormula(this.config);
context.directDamage = this.config.directDamage;
context.selectedRollMode = this.config.selectedRollMode;
context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({
action,
label,
icon
}));
return context; return context;
} }
static updateRollConfiguration(event, _, formData) { static updateRollConfiguration(_event, _, formData) {
const { ...rest } = foundry.utils.expandObject(formData.object); const { ...rest } = foundry.utils.expandObject(formData.object);
this.config.extraFormula = rest.extraFormula; foundry.utils.mergeObject(this.config.roll, rest.roll);
this.config.selectedRollMode = rest.selectedRollMode;
this.render(); this.render();
} }

View file

@ -1,9 +1,9 @@
import { damageKeyToNumber, getDamageLabel } from '../../helpers/utils.mjs'; import { damageKeyToNumber, getDamageLabel } from '../../helpers/utils.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; const { DialogV2, ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class DamageReductionDialog extends HandlebarsApplicationMixin(ApplicationV2) { export default class DamageReductionDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(resolve, reject, actor, damage) { constructor(resolve, reject, actor, damage, damageType) {
super({}); super({});
this.resolve = resolve; this.resolve = resolve;
@ -11,37 +11,46 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.actor = actor; this.actor = actor;
this.damage = damage; this.damage = damage;
const maxArmorMarks = Math.min( const canApplyArmor = damageType.every(t => actor.system.armorApplicableDamageTypes[t] === true);
actor.system.armorScore - actor.system.armor.system.marks.value, const maxArmorMarks = canApplyArmor
actor.system.rules.maxArmorMarked.total ? Math.min(
); actor.system.armorScore - actor.system.armor.system.marks.value,
actor.system.rules.damageReduction.maxArmorMarked.value
)
: 0;
const armor = [...Array(maxArmorMarks).keys()].reduce((acc, _) => { const armor = [...Array(maxArmorMarks).keys()].reduce((acc, _) => {
acc[foundry.utils.randomID()] = { selected: false }; acc[foundry.utils.randomID()] = { selected: false };
return acc; return acc;
}, {}); }, {});
const stress = [...Array(actor.system.rules.maxArmorMarked.stressExtra ?? 0).keys()].reduce((acc, _) => { const stress = [...Array(actor.system.rules.damageReduction.maxArmorMarked.stressExtra ?? 0).keys()].reduce(
acc[foundry.utils.randomID()] = { selected: false }; (acc, _) => {
return acc; acc[foundry.utils.randomID()] = { selected: false };
}, {}); return acc;
},
{}
);
this.marks = { armor, stress }; this.marks = { armor, stress };
this.availableStressReductions = Object.keys(actor.system.rules.stressDamageReduction).reduce((acc, key) => { this.availableStressReductions = Object.keys(actor.system.rules.damageReduction.stressDamageReduction).reduce(
const dr = actor.system.rules.stressDamageReduction[key]; (acc, key) => {
if (dr.enabled) { const dr = actor.system.rules.damageReduction.stressDamageReduction[key];
if (acc === null) acc = {}; if (dr.enabled) {
if (acc === null) acc = {};
const damage = damageKeyToNumber(key); const damage = damageKeyToNumber(key);
acc[damage] = { acc[damage] = {
cost: dr.cost, cost: dr.cost,
selected: false, selected: false,
from: getDamageLabel(damage), from: getDamageLabel(damage),
to: getDamageLabel(damage - 1) to: getDamageLabel(damage - 1)
}; };
} }
return acc; return acc;
}, null); },
null
);
} }
get title() { get title() {
@ -90,7 +99,8 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
context.armorScore = this.actor.system.armorScore; context.armorScore = this.actor.system.armorScore;
context.armorMarks = currentMarks; context.armorMarks = currentMarks;
context.basicMarksUsed = selectedArmorMarks.length === this.actor.system.rules.maxArmorMarked.total; context.basicMarksUsed =
selectedArmorMarks.length === this.actor.system.rules.damageReduction.maxArmorMarked.value;
const stressReductionStress = this.availableStressReductions const stressReductionStress = this.availableStressReductions
? stressReductions.reduce((acc, red) => acc + red.cost, 0) ? stressReductions.reduce((acc, red) => acc + red.cost, 0)
@ -100,7 +110,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
? { ? {
value: value:
this.actor.system.resources.stress.value + selectedStressMarks.length + stressReductionStress, this.actor.system.resources.stress.value + selectedStressMarks.length + stressReductionStress,
maxTotal: this.actor.system.resources.stress.maxTotal max: this.actor.system.resources.stress.max
} }
: null; : null;
@ -122,12 +132,15 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
getDamageInfo = () => { getDamageInfo = () => {
const selectedArmorMarks = Object.values(this.marks.armor).filter(x => x.selected); const selectedArmorMarks = Object.values(this.marks.armor).filter(x => x.selected);
const selectedStressMarks = Object.values(this.marks.stress).filter(x => x.selected); const selectedStressMarks = Object.values(this.marks.stress).filter(x => x.selected);
const stressReductions = Object.values(this.availableStressReductions).filter(red => red.selected); const stressReductions = this.availableStressReductions
? Object.values(this.availableStressReductions).filter(red => red.selected)
: [];
const currentMarks = const currentMarks =
this.actor.system.armor.system.marks.value + selectedArmorMarks.length + selectedStressMarks.length; this.actor.system.armor.system.marks.value + selectedArmorMarks.length + selectedStressMarks.length;
const currentDamage = const armorMarkReduction =
this.damage - selectedArmorMarks.length - selectedStressMarks.length - stressReductions.length; selectedArmorMarks.length * this.actor.system.rules.damageReduction.increasePerArmorMark;
const currentDamage = this.damage - armorMarkReduction - selectedStressMarks.length - stressReductions.length;
return { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage }; return { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage };
}; };
@ -184,7 +197,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
: 0; : 0;
const currentStress = const currentStress =
this.actor.system.resources.stress.value + selectedStressMarks.length + stressReductionStress; this.actor.system.resources.stress.value + selectedStressMarks.length + stressReductionStress;
if (currentStress + stressReduction.cost > this.actor.system.resources.stress.maxTotal) { if (currentStress + stressReduction.cost > this.actor.system.resources.stress.max) {
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.notEnoughStress')); ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.notEnoughStress'));
return; return;
} }
@ -210,9 +223,17 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
async close(fromSave) { async close(fromSave) {
if (!fromSave) { if (!fromSave) {
this.reject(); this.resolve();
} }
await super.close({}); await super.close({});
} }
static async armorStackQuery({ actorId, damage, type }) {
return new Promise(async (resolve, reject) => {
const actor = await fromUuid(actorId);
if (!actor || !actor?.isOwner) reject();
new DamageReductionDialog(resolve, reject, actor, damage, type).render({ force: true });
});
}
} }

View file

@ -23,7 +23,7 @@ export default class DamageSelectionDialog extends HandlebarsApplicationMixin(Ap
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
classes: ['daggerheart', 'views', 'damage-selection'], classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'damage-selection'],
position: { position: {
width: 400, width: 400,
height: 'auto' height: 'auto'

View file

@ -7,8 +7,23 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
this.actor = actor; this.actor = actor;
this.shortrest = shortrest; this.shortrest = shortrest;
const options = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves; this.moveData = foundry.utils.deepClone(
this.moveData = shortrest ? options.shortRest : options.longRest; game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves
);
this.nrChoices = {
shortRest: {
taken: 0,
max:
(shortrest ? this.moveData.shortRest.nrChoices : 0) +
actor.system.bonuses.rest[`${shortrest ? 'short' : 'long'}Rest`].shortMoves
},
longRest: {
taken: 0,
max:
(!shortrest ? this.moveData.longRest.nrChoices : 0) +
actor.system.bonuses.rest[`${shortrest ? 'short' : 'long'}Rest`].longMoves
}
};
} }
get title() { get title() {
@ -17,8 +32,8 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
classes: ['daggerheart', 'views', 'downtime'], classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'downtime'],
position: { width: 680, height: 'auto' }, position: { width: 'auto', height: 'auto' },
actions: { actions: {
selectMove: this.selectMove, selectMove: this.selectMove,
takeDowntime: this.takeDowntime takeDowntime: this.takeDowntime
@ -29,7 +44,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
static PARTS = { static PARTS = {
application: { application: {
id: 'downtime', id: 'downtime',
template: 'systems/daggerheart/templates/dialogs/downtime.hbs' template: 'systems/daggerheart/templates/dialogs/downtime/downtime.hbs'
} }
}; };
@ -37,46 +52,85 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
super._attachPartListeners(partId, htmlElement, options); super._attachPartListeners(partId, htmlElement, options);
htmlElement htmlElement
.querySelectorAll('.activity-image') .querySelectorAll('.activity-container')
.forEach(element => element.addEventListener('contextmenu', this.deselectMove.bind(this))); .forEach(element => element.addEventListener('contextmenu', this.deselectMove.bind(this)));
} }
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.title = game.i18n.localize(
`DAGGERHEART.APPLICATIONS.Downtime.${this.shortrest ? 'shortRest' : 'longRest'}.title`
);
context.selectedActivity = this.selectedActivity; context.selectedActivity = this.selectedActivity;
context.moveData = this.moveData; context.moveData = this.moveData;
context.nrCurrentChoices = Object.values(this.moveData.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0);
context.disabledDowntime = context.nrCurrentChoices < context.moveData.nrChoices; const shortRestMovesSelected = this.#nrSelectedMoves('shortRest');
const longRestMovesSelected = this.#nrSelectedMoves('longRest');
context.nrChoices = {
...this.nrChoices,
shortRest: {
...this.nrChoices.shortRest,
current: this.nrChoices.shortRest.taken + shortRestMovesSelected
},
longRest: {
...this.nrChoices.longRest,
current: this.nrChoices.longRest.taken + longRestMovesSelected
}
};
context.shortRestMoves = this.nrChoices.shortRest.max > 0 ? this.moveData.shortRest : null;
context.longRestMoves = this.nrChoices.longRest.max > 0 ? this.moveData.longRest : null;
context.disabledDowntime = shortRestMovesSelected === 0 && longRestMovesSelected === 0;
return context; return context;
} }
static selectMove(_, button) { static selectMove(_, target) {
const nrSelected = Object.values(this.moveData.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0); const { category, move } = target.dataset;
if (nrSelected === this.moveData.nrChoices) {
const nrSelected = this.#nrSelectedMoves(category);
if (nrSelected + this.nrChoices[category].taken >= this.nrChoices[category].max) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noMoreMoves')); ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noMoreMoves'));
return; return;
} }
const move = button.dataset.move; this.moveData[category].moves[move].selected = this.moveData[category].moves[move].selected
this.moveData.moves[move].selected = this.moveData.moves[move].selected ? this.moveData[category].moves[move].selected + 1
? this.moveData.moves[move].selected + 1
: 1; : 1;
this.render(); this.render();
} }
deselectMove(event) { deselectMove(event) {
const move = event.currentTarget.dataset.move; const button = event.target.closest('.activity-container');
this.moveData.moves[move].selected = this.moveData.moves[move].selected const { move, category } = button.dataset;
? this.moveData.moves[move].selected - 1 this.moveData[category].moves[move].selected = this.moveData[category].moves[move].selected
? this.moveData[category].moves[move].selected - 1
: 0; : 0;
this.render(); this.render();
// On macOS with a single-button mouse (e.g. a laptop trackpad),
// right-click is triggered with ctrl+click, which triggers both a
// `contextmenu` event and a regular click event. We need to stop
// event propagation to prevent the click event from triggering the
// `selectMove` function and undoing the change we just made.
event.stopPropagation();
// Having stopped propagation, we're no longer subject to Foundry's
// default `contextmenu` handler, so we also have to prevent the
// default behaviour to prevent a context menu from appearing.
event.preventDefault();
} }
static async takeDowntime() { static async takeDowntime() {
const moves = Object.values(this.moveData.moves).filter(x => x.selected); const moves = Object.values(this.moveData).flatMap(category => {
return Object.values(category.moves)
.filter(x => x.selected)
.flatMap(move => [...Array(move.selected).keys()].map(_ => move));
});
const cls = getDocumentClass('ChatMessage'); const cls = getDocumentClass('ChatMessage');
const msg = new cls({ const msg = new cls({
@ -88,7 +142,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
content: await foundry.applications.handlebars.renderTemplate( content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/downtime.hbs', 'systems/daggerheart/templates/ui/chat/downtime.hbs',
{ {
title: `${this.actor.name} - ${game.i18n.localize(`DAGGERHEART.APPLICATIONS.Downtime.${this.shortRest ? 'shortRest' : 'longRest'}.title`)}`, title: `${this.actor.name} - ${game.i18n.localize(`DAGGERHEART.APPLICATIONS.Downtime.${this.shortrest ? 'shortRest' : 'longRest'}.title`)}`,
moves: moves moves: moves
} }
) )
@ -96,11 +150,33 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
cls.create(msg.toObject()); cls.create(msg.toObject());
this.close(); // Reset selection and update number of taken moves
for (const [catName, category] of Object.entries(this.moveData)) {
for (const move of Object.values(category.moves)) {
if (move.selected > 0) {
this.nrChoices[catName].taken += move.selected;
move.selected = 0;
}
}
}
// We can close the window when all moves are taken
if (
this.nrChoices.shortRest.taken >= this.nrChoices.shortRest.max &&
this.nrChoices.longRest.taken >= this.nrChoices.longRest.max
) {
this.close();
} else {
this.render();
}
} }
static async updateData(event, element, formData) { static async updateData(event, element, formData) {
this.customActivity = foundry.utils.mergeObject(this.customActivity, formData.object); this.customActivity = foundry.utils.mergeObject(this.customActivity, formData.object);
this.render(); this.render();
} }
#nrSelectedMoves(category) {
return Object.values(this.moveData[category].moves).reduce((acc, x) => acc + (x.selected ?? 0), 0);
}
} }

View file

@ -0,0 +1,99 @@
import { itemAbleRollParse } from '../../helpers/utils.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class ResourceDiceDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(item, actor, options = {}) {
super(options);
this.item = item;
this.actor = actor;
this.diceStates = foundry.utils.deepClone(item.system.resource.diceStates);
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'resource-dice'],
window: {
icon: 'fa-solid fa-dice'
},
actions: {
rerollDice: this.rerollDice,
save: this.save
},
form: {
handler: this.updateResourceDice,
submitOnChange: true,
submitOnClose: false
}
};
/** @override */
static PARTS = {
resourceDice: {
id: 'resourceDice',
template: 'systems/daggerheart/templates/dialogs/dice-roll/resourceDice.hbs'
}
};
get title() {
return game.i18n.format('DAGGERHEART.APPLICATIONS.ResourceDice.title', { name: this.item.name });
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.item = this.item;
context.actor = this.actor;
context.diceStates = this.diceStates;
return context;
}
static async updateResourceDice(event, _, formData) {
const { diceStates } = foundry.utils.expandObject(formData.object);
this.diceStates = Object.keys(diceStates).reduce((acc, key) => {
const resourceState = this.item.system.resource.diceStates[key];
acc[key] = { ...diceStates[key], used: Boolean(resourceState?.used) };
return acc;
}, {});
this.render();
}
static async save() {
this.rollValues = Object.values(this.diceStates);
this.close();
}
static async rerollDice() {
const max = itemAbleRollParse(this.item.system.resource.max, this.actor, this.item);
const diceFormula = `${max}${this.item.system.resource.dieFaces}`;
const roll = await new Roll(diceFormula).evaluate();
if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true);
this.rollValues = roll.terms[0].results.map(x => ({ value: x.result, used: false }));
this.resetUsed = true;
const cls = getDocumentClass('ChatMessage');
const msg = new cls({
user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/resource-roll.hbs',
{
user: this.actor.name,
name: this.item.name
}
)
});
cls.create(msg.toObject());
this.close();
}
static async create(item, actor, options = {}) {
return new Promise(resolve => {
const app = new this(item, actor, options);
app.addEventListener('close', () => resolve(app.rollValues), { once: true });
app.render({ force: true });
});
}
}

View file

@ -0,0 +1 @@
export { default as DHTokenHUD } from './tokenHUD.mjs';

View file

@ -0,0 +1,84 @@
export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
static DEFAULT_OPTIONS = {
classes: ['daggerheart']
};
/** @override */
static PARTS = {
hud: {
root: true,
template: 'systems/daggerheart/templates/hud/tokenHUD.hbs'
}
};
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.systemStatusEffects = Object.keys(context.statusEffects).reduce((acc, key) => {
const effect = context.statusEffects[key];
if (effect.systemEffect) acc[key] = effect;
return acc;
}, {});
const useGeneric = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance
).showGenericStatusEffects;
context.genericStatusEffects = useGeneric
? Object.keys(context.statusEffects).reduce((acc, key) => {
const effect = context.statusEffects[key];
if (!effect.systemEffect) acc[key] = effect;
return acc;
}, {})
: null;
return context;
}
_getStatusEffectChoices() {
// Include all HUD-enabled status effects
const choices = {};
for (const status of CONFIG.statusEffects) {
if (
status.hud === false ||
(foundry.utils.getType(status.hud) === 'Object' &&
status.hud.actorTypes?.includes(this.document.actor.type) === false)
) {
continue;
}
choices[status.id] = {
_id: status._id,
id: status.id,
systemEffect: status.systemEffect,
title: game.i18n.localize(status.name ?? /** @deprecated since v12 */ status.label),
src: status.img ?? /** @deprecated since v12 */ status.icon,
isActive: false,
isOverlay: false
};
}
// Update the status of effects which are active for the token actor
const activeEffects = this.actor?.effects || [];
for (const effect of activeEffects) {
for (const statusId of effect.statuses) {
const status = choices[statusId];
if (!status) continue;
if (status._id) {
if (status._id !== effect.id) continue;
} else {
if (effect.statuses.size !== 1) continue;
}
status.isActive = true;
if (effect.getFlag('core', 'overlay')) status.isOverlay = true;
break;
}
}
// Flag status CSS class
for (const status of Object.values(choices)) {
status.cssClass = [status.isActive ? 'active' : null, status.isOverlay ? 'overlay' : null].filterJoin(' ');
}
return choices;
}
}

View file

@ -223,8 +223,8 @@ export default class DhCharacterLevelUp extends LevelUpBase {
context.achievements = { context.achievements = {
proficiency: { proficiency: {
old: this.actor.system.proficiency.total, old: this.actor.system.proficiency,
new: this.actor.system.proficiency.total + achivementProficiency, new: this.actor.system.proficiency + achivementProficiency,
shown: achivementProficiency > 0 shown: achivementProficiency > 0
}, },
damageThresholds: { damageThresholds: {
@ -332,16 +332,16 @@ export default class DhCharacterLevelUp extends LevelUpBase {
new: context.achievements.proficiency.new + (advancement.proficiency ?? 0) new: context.achievements.proficiency.new + (advancement.proficiency ?? 0)
}, },
hitPoints: { hitPoints: {
old: this.actor.system.resources.hitPoints.maxTotal, old: this.actor.system.resources.hitPoints.max,
new: this.actor.system.resources.hitPoints.maxTotal + (advancement.hitPoint ?? 0) new: this.actor.system.resources.hitPoints.max + (advancement.hitPoint ?? 0)
}, },
stress: { stress: {
old: this.actor.system.resources.stress.maxTotal, old: this.actor.system.resources.stress.max,
new: this.actor.system.resources.stress.maxTotal + (advancement.stress ?? 0) new: this.actor.system.resources.stress.max + (advancement.stress ?? 0)
}, },
evasion: { evasion: {
old: this.actor.system.evasion.total, old: this.actor.system.evasion,
new: this.actor.system.evasion.total + (advancement.evasion ?? 0) new: this.actor.system.evasion + (advancement.evasion ?? 0)
} }
}, },
traits: Object.keys(this.actor.system.traits).reduce((acc, traitKey) => { traits: Object.keys(this.actor.system.traits).reduce((acc, traitKey) => {
@ -349,8 +349,8 @@ export default class DhCharacterLevelUp extends LevelUpBase {
if (!acc) acc = {}; if (!acc) acc = {};
acc[traitKey] = { acc[traitKey] = {
label: game.i18n.localize(abilities[traitKey].label), label: game.i18n.localize(abilities[traitKey].label),
old: this.actor.system.traits[traitKey].total, old: this.actor.system.traits[traitKey].max,
new: this.actor.system.traits[traitKey].total + advancement.trait[traitKey] new: this.actor.system.traits[traitKey].max + advancement.trait[traitKey]
}; };
} }
return acc; return acc;

View file

@ -122,12 +122,12 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
context.advancements = { context.advancements = {
statistics: { statistics: {
stress: { stress: {
old: this.actor.system.resources.stress.maxTotal, old: this.actor.system.resources.stress.max,
new: this.actor.system.resources.stress.maxTotal + (advancement.stress ?? 0) new: this.actor.system.resources.stress.max + (advancement.stress ?? 0)
}, },
evasion: { evasion: {
old: this.actor.system.evasion.total, old: this.actor.system.evasion,
new: this.actor.system.evasion.total + (advancement.evasion ?? 0) new: this.actor.system.evasion + (advancement.evasion ?? 0)
} }
}, },
experiences: experiences:

View file

@ -128,7 +128,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
context.tabs.advancements.progress = { selected: selections, max: currentLevel.maxSelections }; context.tabs.advancements.progress = { selected: selections, max: currentLevel.maxSelections };
context.showTabs = this.tabGroups.primary !== 'summary'; context.showTabs = this.tabGroups.primary !== 'summary';
break; break;
const { current: currentActorLevel, changed: changedActorLevel } = this.actor.system.levelData.level;
const actorArmor = this.actor.system.armor; const actorArmor = this.actor.system.armor;
const levelKeys = Object.keys(this.levelup.levels); const levelKeys = Object.keys(this.levelup.levels);
let achivementProficiency = 0; let achivementProficiency = 0;
@ -157,8 +157,8 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
context.achievements = { context.achievements = {
proficiency: { proficiency: {
old: this.actor.system.proficiency.total, old: this.actor.system.proficiency,
new: this.actor.system.proficiency.total + achivementProficiency, new: this.actor.system.proficiency + achivementProficiency,
shown: achivementProficiency > 0 shown: achivementProficiency > 0
}, },
damageThresholds: { damageThresholds: {
@ -265,16 +265,16 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
new: context.achievements.proficiency.new + (advancement.proficiency ?? 0) new: context.achievements.proficiency.new + (advancement.proficiency ?? 0)
}, },
hitPoints: { hitPoints: {
old: this.actor.system.resources.hitPoints.maxTotal, old: this.actor.system.resources.hitPoints.max,
new: this.actor.system.resources.hitPoints.maxTotal + (advancement.hitPoint ?? 0) new: this.actor.system.resources.hitPoints.max + (advancement.hitPoint ?? 0)
}, },
stress: { stress: {
old: this.actor.system.resources.stress.maxTotal, old: this.actor.system.resources.stress.max,
new: this.actor.system.resources.stress.maxTotal + (advancement.stress ?? 0) new: this.actor.system.resources.stress.max + (advancement.stress ?? 0)
}, },
evasion: { evasion: {
old: this.actor.system.evasion.total, old: this.actor.system.evasion,
new: this.actor.system.evasion.total + (advancement.evasion ?? 0) new: this.actor.system.evasion + (advancement.evasion ?? 0)
} }
}, },
traits: Object.keys(this.actor.system.traits).reduce((acc, traitKey) => { traits: Object.keys(this.actor.system.traits).reduce((acc, traitKey) => {
@ -282,8 +282,8 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
if (!acc) acc = {}; if (!acc) acc = {};
acc[traitKey] = { acc[traitKey] = {
label: game.i18n.localize(abilities[traitKey].label), label: game.i18n.localize(abilities[traitKey].label),
old: this.actor.system.traits[traitKey].total, old: this.actor.system.traits[traitKey].value,
new: this.actor.system.traits[traitKey].total + advancement.trait[traitKey] new: this.actor.system.traits[traitKey].value + advancement.trait[traitKey]
}; };
} }
return acc; return acc;

View file

@ -12,14 +12,17 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
} }
get title() { get title() {
return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.appearance.name'); return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title');
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
id: 'daggerheart-appearance-settings', id: 'daggerheart-appearance-settings',
classes: ['daggerheart', 'setting', 'dh-style'], classes: ['daggerheart', 'dialog', 'dh-style', 'setting'],
position: { width: '600', height: 'auto' }, position: { width: '600', height: 'auto' },
window: {
icon: 'fa-solid fa-gears'
},
actions: { actions: {
reset: this.reset, reset: this.reset,
save: this.save save: this.save

View file

@ -12,14 +12,17 @@ export default class DhAutomationSettings extends HandlebarsApplicationMixin(App
} }
get title() { get title() {
return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.automation.name'); return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title');
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
id: 'daggerheart-automation-settings', id: 'daggerheart-automation-settings',
classes: ['daggerheart', 'setting', 'dh-style'], classes: ['daggerheart', 'dh-style', 'dialog', 'setting'],
position: { width: '600', height: 'auto' }, position: { width: '600', height: 'auto' },
window: {
icon: 'fa-solid fa-gears'
},
actions: { actions: {
reset: this.reset, reset: this.reset,
save: this.save save: this.save

View file

@ -4,13 +4,14 @@ import DHActionConfig from '../../sheets-configs/action-config.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhSettingsActionView extends HandlebarsApplicationMixin(ApplicationV2) { export default class DhSettingsActionView extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(resolve, reject, title, name, img, description, actions) { constructor(resolve, reject, title, name, icon, img, description, actions) {
super({}); super({});
this.resolve = resolve; this.resolve = resolve;
this.reject = reject; this.reject = reject;
this.viewTitle = title; this.viewTitle = title;
this.name = name; this.name = name;
this.icon = icon;
this.img = img; this.img = img;
this.description = description; this.description = description;
this.actions = actions; this.actions = actions;
@ -23,7 +24,7 @@ export default class DhSettingsActionView extends HandlebarsApplicationMixin(App
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
classes: ['daggerheart', 'setting', 'dh-style'], classes: ['daggerheart', 'setting', 'dh-style'],
position: { width: '400', height: 'auto' }, position: { width: 440, height: 'auto' },
actions: { actions: {
editImage: this.onEditImage, editImage: this.onEditImage,
addItem: this.addItem, addItem: this.addItem,
@ -46,6 +47,7 @@ export default class DhSettingsActionView extends HandlebarsApplicationMixin(App
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.name = this.name; context.name = this.name;
context.icon = this.icon;
context.img = this.img; context.img = this.img;
context.description = this.description; context.description = this.description;
context.enrichedDescription = await foundry.applications.ux.TextEditor.enrichHTML(context.description); context.enrichedDescription = await foundry.applications.ux.TextEditor.enrichHTML(context.description);
@ -55,8 +57,9 @@ export default class DhSettingsActionView extends HandlebarsApplicationMixin(App
} }
static async updateData(event, element, formData) { static async updateData(event, element, formData) {
const { name, img, description } = foundry.utils.expandObject(formData.object); const { name, icon, description } = foundry.utils.expandObject(formData.object);
this.name = name; this.name = name;
this.icon = icon;
this.description = description; this.description = description;
this.render(); this.render();
@ -65,6 +68,7 @@ export default class DhSettingsActionView extends HandlebarsApplicationMixin(App
static async saveForm(event) { static async saveForm(event) {
this.resolve({ this.resolve({
name: this.name, name: this.name,
icon: this.icon,
img: this.img, img: this.img,
description: this.description, description: this.description,
actions: this.actions actions: this.actions

View file

@ -13,14 +13,17 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
} }
get title() { get title() {
return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.homebrew.name'); return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title');
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
id: 'daggerheart-homebrew-settings', id: 'daggerheart-homebrew-settings',
classes: ['daggerheart', 'setting', 'dh-style'], classes: ['daggerheart', 'dh-style', 'dialog', 'setting'],
position: { width: '600', height: 'auto' }, position: { width: '600', height: 'auto' },
window: {
icon: 'fa-solid fa-gears'
},
actions: { actions: {
addItem: this.addItem, addItem: this.addItem,
editItem: this.editItem, editItem: this.editItem,
@ -76,6 +79,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
reject, reject,
game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.downtimeMoves'), game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.downtimeMoves'),
move.name, move.name,
move.icon,
move.img, move.img,
move.description, move.description,
move.actions move.actions
@ -87,6 +91,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
await this.settings.updateSource({ await this.settings.updateSource({
[`restMoves.${type}.moves.${id}`]: { [`restMoves.${type}.moves.${id}`]: {
name: data.name, name: data.name,
icon: data.icon,
img: data.img, img: data.img,
description: data.description description: data.description
} }

View file

@ -12,14 +12,17 @@ export default class DhRangeMeasurementSettings extends HandlebarsApplicationMix
} }
get title() { get title() {
return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.automation.name'); return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title');
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
id: 'daggerheart-automation-settings', id: 'daggerheart-automation-settings',
classes: ['daggerheart', 'setting', 'dh-style'], classes: ['daggerheart', 'dialog', 'dh-style', 'setting'],
position: { width: '600', height: 'auto' }, position: { width: '600', height: 'auto' },
window: {
icon: 'fa-solid fa-gears'
},
actions: { actions: {
reset: this.reset, reset: this.reset,
save: this.save save: this.save

View file

@ -12,14 +12,17 @@ export default class DHVariantRuleSettings extends HandlebarsApplicationMixin(Ap
} }
get title() { get title() {
return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.variantRules.name'); return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title');
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
id: 'daggerheart-appearance-settings', id: 'daggerheart-appearance-settings',
classes: ['daggerheart', 'setting', 'dh-style'], classes: ['daggerheart', 'dialog', 'dh-style', 'setting'],
position: { width: '600', height: 'auto' }, position: { width: '600', height: 'auto' },
window: {
icon: 'fa-solid fa-gears'
},
actions: { actions: {
reset: this.reset, reset: this.reset,
save: this.save save: this.save

View file

@ -3,3 +3,5 @@ export { default as AdversarySettings } from './adversary-settings.mjs';
export { default as CompanionSettings } from './companion-settings.mjs'; export { default as CompanionSettings } from './companion-settings.mjs';
export { default as EnvironmentSettings } from './environment-settings.mjs'; export { default as EnvironmentSettings } from './environment-settings.mjs';
export { default as ActiveEffectConfig } from './activeEffectConfig.mjs'; export { default as ActiveEffectConfig } from './activeEffectConfig.mjs';
export { default as DhTokenConfig } from './token-config.mjs';
export { default as DhPrototypeTokenConfig } from './prototype-token-config.mjs';

View file

@ -56,10 +56,6 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
id: 'effect', id: 'effect',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/effect.hbs' template: 'systems/daggerheart/templates/sheets-settings/action-settings/effect.hbs'
} }
/* form: {
id: 'action',
template: 'systems/daggerheart/templates/config/action.hbs'
} */
}; };
static TABS = { static TABS = {
@ -111,13 +107,14 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
context.hasBaseDamage = !!this.action.parent.attack; context.hasBaseDamage = !!this.action.parent.attack;
context.getRealIndex = this.getRealIndex.bind(this); context.getRealIndex = this.getRealIndex.bind(this);
context.getEffectDetails = this.getEffectDetails.bind(this); context.getEffectDetails = this.getEffectDetails.bind(this);
context.costOptions = this.getCostOptions();
context.disableOption = this.disableOption.bind(this); context.disableOption = this.disableOption.bind(this);
context.isNPC = this.action.actor && this.action.actor.type !== 'character'; context.isNPC = this.action.actor?.isNPC;
context.hasRoll = this.action.hasRoll; context.hasRoll = this.action.hasRoll;
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers; const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
context.tierOptions = [ context.tierOptions = [
{ key: 1, label: game.i18n.localize('DAGGERHEART.GENERAL.Tiers.tier1') }, { key: 1, label: game.i18n.localize('DAGGERHEART.GENERAL.Tiers.1') },
...Object.values(settingsTiers).map(x => ({ key: x.tier, label: x.name })) ...Object.values(settingsTiers).map(x => ({ key: x.tier, label: x.name }))
]; ];
@ -129,8 +126,21 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
this.render(true); this.render(true);
} }
disableOption(index, options, choices) { getCostOptions() {
const filtered = foundry.utils.deepClone(options); const options = foundry.utils.deepClone(CONFIG.DH.GENERAL.abilityCosts);
const resource = this.action.parent.resource;
if (resource) {
options[this.action.parent.parent.id] = {
label: this.action.parent.parent.name,
group: 'TYPES.Actor.character'
};
}
return options;
}
disableOption(index, costOptions, choices) {
const filtered = foundry.utils.deepClone(costOptions);
Object.keys(filtered).forEach(o => { Object.keys(filtered).forEach(o => {
if (choices.find((c, idx) => c.type === o && index !== idx)) filtered[o].disabled = true; if (choices.find((c, idx) => c.type === o && index !== idx)) filtered[o].disabled = true;
}); });
@ -146,11 +156,19 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
return this.action.item.effects.get(id); return this.action.item.effects.get(id);
} }
_prepareSubmitData(event, formData) { _prepareSubmitData(_event, formData) {
const submitData = foundry.utils.expandObject(formData.object); const submitData = foundry.utils.expandObject(formData.object);
for (const keyPath of this.constructor.CLEAN_ARRAYS) { for (const keyPath of this.constructor.CLEAN_ARRAYS) {
const data = foundry.utils.getProperty(submitData, keyPath); const data = foundry.utils.getProperty(submitData, keyPath);
if (data) foundry.utils.setProperty(submitData, keyPath, Object.values(data)); const dataValues = data ? Object.values(data) : [];
if (keyPath === 'cost') {
for (var value of dataValues) {
const item = this.action.parent.parent.id === value.key;
value.keyIsID = Boolean(item);
}
}
if (data) foundry.utils.setProperty(submitData, keyPath, dataValues);
} }
return submitData; return submitData;
} }
@ -161,7 +179,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
container = foundry.utils.getProperty(this.action.parent, this.action.systemPath); container = foundry.utils.getProperty(this.action.parent, this.action.systemPath);
let newActions; let newActions;
if (Array.isArray(container)) { if (Array.isArray(container)) {
newActions = foundry.utils.getProperty(this.action.parent, this.action.systemPath).map(x => x.toObject()); // Find better way newActions = foundry.utils.getProperty(this.action.parent, this.action.systemPath).map(x => x.toObject());
if (!newActions.findSplice(x => x._id === data._id, data)) newActions.push(data); if (!newActions.findSplice(x => x._id === data._id, data)) newActions.push(data);
} else newActions = data; } else newActions = data;

View file

@ -1,4 +1,24 @@
import autocomplete from 'autocompleter';
export default class DhActiveEffectConfig extends foundry.applications.sheets.ActiveEffectConfig { export default class DhActiveEffectConfig extends foundry.applications.sheets.ActiveEffectConfig {
constructor(options) {
super(options);
const ignoredActorKeys = ['config', 'DhEnvironment'];
this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => {
if (!ignoredActorKeys.includes(key)) {
const model = game.system.api.models.actors[key];
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model);
const group = game.i18n.localize(model.metadata.label);
const choices = CONFIG.Token.documentClass
.getTrackedAttributeChoices(attributes, model)
.map(x => ({ ...x, group: group }));
acc.push(...choices);
}
return acc;
}, []);
}
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'sheet', 'dh-style'] classes: ['daggerheart', 'sheet', 'dh-style']
}; };
@ -27,36 +47,62 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
} }
}; };
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
const changeChoices = this.changeChoices;
htmlElement.querySelectorAll('.effect-change-input').forEach(element => {
autocomplete({
input: element,
fetch: function (text, update) {
if (!text) {
update(changeChoices);
} else {
text = text.toLowerCase();
var suggestions = changeChoices.filter(n => n.label.toLowerCase().includes(text));
update(suggestions);
}
},
render: function (item, search) {
const label = game.i18n.localize(item.label);
const matchIndex = label.toLowerCase().indexOf(search);
const beforeText = label.slice(0, matchIndex);
const matchText = label.slice(matchIndex, matchIndex + search.length);
const after = label.slice(matchIndex + search.length, label.length);
const element = document.createElement('li');
element.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
if (item.hint) {
element.dataset.tooltip = game.i18n.localize(item.hint);
}
return element;
},
renderGroup: function (label) {
const itemElement = document.createElement('div');
itemElement.textContent = game.i18n.localize(label);
return itemElement;
},
onSelect: function (item) {
element.value = `system.${item.value}`;
},
click: e => e.fetch(),
customize: function (_input, _inputRect, container) {
container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ;
},
minLength: 0
});
});
}
async _preparePartContext(partId, context) { async _preparePartContext(partId, context) {
const partContext = await super._preparePartContext(partId, context); const partContext = await super._preparePartContext(partId, context);
switch (partId) { switch (partId) {
case 'changes': case 'changes':
const fieldPaths = [];
const validFieldPath = fieldPath => this.validFieldPath(fieldPath, this.#unapplicablePaths);
context.document.parent.system.schema.apply(function () {
if (!(this instanceof foundry.data.fields.SchemaField)) {
if (validFieldPath(this.fieldPath)) {
fieldPaths.push(this.fieldPath);
}
}
});
context.fieldPaths = fieldPaths;
break; break;
} }
return partContext; return partContext;
} }
#unapplicablePaths = ['story', 'pronouns', 'description'];
validFieldPath(fieldPath, unapplicablePaths) {
const splitPath = fieldPath.split('.');
if (splitPath.length > 1 && unapplicablePaths.includes(splitPath[1])) return false;
/* The current value of a resource should not be modified */
if (new RegExp(/resources.*\.value/).exec(fieldPath)) return false;
return true;
}
} }

View file

@ -70,6 +70,18 @@ export default class DHAdversarySettings extends DHBaseActorSettings {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #removeExperience(_, target) { static async #removeExperience(_, target) {
const experience = this.actor.system.experiences[target.dataset.experience];
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize(`DAGGERHEART.GENERAL.Experience.single`),
name: experience.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: experience.name })
});
if (!confirmed) return;
await this.actor.update({ [`system.experiences.-=${target.dataset.experience}`]: null }); await this.actor.update({ [`system.experiences.-=${target.dataset.experience}`]: null });
} }

View file

@ -1,3 +1,4 @@
import { getDocFromElement } from '../../helpers/utils.mjs';
import DHBaseActorSettings from '../sheets/api/actor-setting.mjs'; import DHBaseActorSettings from '../sheets/api/actor-setting.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
@ -9,8 +10,7 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
actions: { actions: {
addCategory: DHEnvironmentSettings.#addCategory, addCategory: DHEnvironmentSettings.#addCategory,
removeCategory: DHEnvironmentSettings.#removeCategory, removeCategory: DHEnvironmentSettings.#removeCategory,
viewAdversary: this.#viewAdversary, deleteAdversary: DHEnvironmentSettings.#deleteAdversary
deleteAdversary: this.#deleteAdversary
}, },
dragDrop: [ dragDrop: [
{ dragSelector: null, dropSelector: '.category-container' }, { dragSelector: null, dropSelector: '.category-container' },
@ -69,23 +69,30 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
await this.actor.update({ [`system.potentialAdversaries.-=${target.dataset.categoryId}`]: null }); await this.actor.update({ [`system.potentialAdversaries.-=${target.dataset.categoryId}`]: null });
} }
static async #viewAdversary(_, button) { /**
const adversary = await foundry.utils.fromUuid(button.dataset.adversary); *
if (!adversary) { * @type {ApplicationClickAction}
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.adversaryMissing')); * @returns
return; */
} static async #deleteAdversary(_event, target) {
const doc = getDocFromElement(target);
const { category } = target.dataset;
const path = `system.potentialAdversaries.${category}.adversaries`;
adversary.sheet.render({ force: true }); const confirmed = await foundry.applications.api.DialogV2.confirm({
} window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize('TYPES.Actor.adversary'),
name: doc.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: doc.name })
});
static async #deleteAdversary(event, target) { if (!confirmed) return;
const adversaryKey = target.dataset.adversary;
const path = `system.potentialAdversaries.${target.dataset.potentialAdversary}.adversaries`; const adversaries = foundry.utils.getProperty(this.actor, path);
console.log(target.dataset.potentialAdversar); const newAdversaries = adversaries.filter(a => a.uuid !== doc.uuid);
const newAdversaries = foundry.utils
.getProperty(this.actor, path)
.filter(x => x && (x?.uuid ?? x) !== adversaryKey);
await this.actor.update({ [path]: newAdversaries }); await this.actor.update({ [path]: newAdversaries });
} }

View file

@ -0,0 +1,20 @@
export default class DhPrototypeTokenConfig extends foundry.applications.sheets.PrototypeTokenConfig {
/** @inheritDoc */
async _prepareResourcesTab() {
const token = this.token;
const usesTrackableAttributes = !foundry.utils.isEmpty(CONFIG.Actor.trackableAttributes);
const attributeSource =
this.actor?.system instanceof foundry.abstract.DataModel && usesTrackableAttributes
? this.actor?.type
: this.actor?.system;
const TokenDocument = foundry.utils.getDocumentClass('Token');
const attributes = TokenDocument.getTrackedAttributes(attributeSource);
return {
barAttributes: TokenDocument.getTrackedAttributeChoices(attributes, attributeSource),
bar1: token.getBarAttribute?.('bar1'),
bar2: token.getBarAttribute?.('bar2'),
turnMarkerModes: DhPrototypeTokenConfig.TURN_MARKER_MODES,
turnMarkerAnimations: CONFIG.Combat.settings.turnMarkerAnimations
};
}
}

View file

@ -0,0 +1,20 @@
export default class DhTokenConfig extends foundry.applications.sheets.TokenConfig {
/** @inheritDoc */
async _prepareResourcesTab() {
const token = this.token;
const usesTrackableAttributes = !foundry.utils.isEmpty(CONFIG.Actor.trackableAttributes);
const attributeSource =
this.actor?.system instanceof foundry.abstract.DataModel && usesTrackableAttributes
? this.actor?.type
: this.actor?.system;
const TokenDocument = foundry.utils.getDocumentClass('Token');
const attributes = TokenDocument.getTrackedAttributes(attributeSource);
return {
barAttributes: TokenDocument.getTrackedAttributeChoices(attributes, attributeSource),
bar1: token.getBarAttribute?.('bar1'),
bar2: token.getBarAttribute?.('bar2'),
turnMarkerModes: DhTokenConfig.TURN_MARKER_MODES,
turnMarkerAnimations: CONFIG.Combat.settings.turnMarkerAnimations
};
}
}

View file

@ -8,9 +8,7 @@ export default class AdversarySheet extends DHBaseActorSheet {
position: { width: 660, height: 766 }, position: { width: 660, height: 766 },
window: { resizable: true }, window: { resizable: true },
actions: { actions: {
reactionRoll: AdversarySheet.#reactionRoll, reactionRoll: AdversarySheet.#reactionRoll
useItem: this.useItem,
toChat: this.toChat
}, },
window: { window: {
resizable: true resizable: true
@ -28,7 +26,7 @@ export default class AdversarySheet extends DHBaseActorSheet {
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
primary: { primary: {
tabs: [{ id: 'features' }, { id: 'notes' }, { id: 'effects' }], tabs: [{ id: 'features' }, { id: 'effects' }, { id: 'notes' }],
initial: 'features', initial: 'features',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }
@ -41,10 +39,63 @@ export default class AdversarySheet extends DHBaseActorSheet {
return context; return context;
} }
getItem(element) { /**@inheritdoc */
const itemId = (element.target ?? element).closest('[data-item-id]').dataset.itemId, async _preparePartContext(partId, context, options) {
item = this.document.items.get(itemId); context = await super._preparePartContext(partId, context, options);
return item; switch (partId) {
case 'header':
await this._prepareHeaderContext(context, options);
break;
case 'notes':
await this._prepareNotesContext(context, options);
break;
}
return context;
}
/**
* Prepare render context for the Biography part.
* @param {ApplicationRenderContext} context
* @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/
async _prepareNotesContext(context, _options) {
const { system } = this.document;
const { TextEditor } = foundry.applications.ux;
const paths = {
notes: 'notes'
};
for (const [key, path] of Object.entries(paths)) {
const value = foundry.utils.getProperty(system, path);
context[key] = {
field: system.schema.getField(path),
value,
enriched: await TextEditor.implementation.enrichHTML(value, {
secrets: this.document.isOwner,
relativeTo: this.document
})
};
}
}
/**
* Prepare render context for the Header part.
* @param {ApplicationRenderContext} context
* @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/
async _prepareHeaderContext(context, _options) {
const { system } = this.document;
const { TextEditor } = foundry.applications.ux;
context.description = await TextEditor.implementation.enrichHTML(system.description, {
secrets: this.document.isOwner,
relativeTo: this.document
});
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -72,42 +123,4 @@ export default class AdversarySheet extends DHBaseActorSheet {
this.actor.diceRoll(config); this.actor.diceRoll(config);
} }
/**
*
* @type {ApplicationClickAction}
*/
static async useItem(event) {
const action = this.getItem(event) ?? this.actor.system.attack;
action.use(event);
}
/**
*
* @type {ApplicationClickAction}
*/
static async toChat(event, button) {
if (button?.dataset?.type === 'experience') {
const experience = this.document.system.experiences[button.dataset.uuid];
const cls = getDocumentClass('ChatMessage');
const systemData = {
name: game.i18n.localize('DAGGERHEART.GENERAL.Experience.single'),
description: `${experience.name} ${experience.total.signedString()}`
};
const msg = new cls({
type: 'abilityUse',
user: game.user.id,
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/ability-use.hbs',
systemData
)
});
cls.create(msg.toObject());
} else {
const item = this.getItem(event) ?? this.document.system.attack;
item.toChat(this.document.id);
}
}
} }

View file

@ -4,6 +4,7 @@ import { abilities } from '../../../config/actorConfig.mjs';
import DhCharacterlevelUp from '../../levelup/characterLevelup.mjs'; import DhCharacterlevelUp from '../../levelup/characterLevelup.mjs';
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
import FilterMenu from '../../ux/filter-menu.mjs'; import FilterMenu from '../../ux/filter-menu.mjs';
import { getDocFromElement, itemAbleRollParse } from '../../../helpers/utils.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
@ -14,7 +15,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
classes: ['character'], classes: ['character'],
position: { width: 850, height: 800 }, position: { width: 850, height: 800 },
actions: { actions: {
triggerContextMenu: CharacterSheet.#triggerContextMenu,
toggleVault: CharacterSheet.#toggleVault, toggleVault: CharacterSheet.#toggleVault,
rollAttribute: CharacterSheet.#rollAttribute, rollAttribute: CharacterSheet.#rollAttribute,
toggleHope: CharacterSheet.#toggleHope, toggleHope: CharacterSheet.#toggleHope,
@ -23,17 +23,39 @@ export default class CharacterSheet extends DHBaseActorSheet {
makeDeathMove: CharacterSheet.#makeDeathMove, makeDeathMove: CharacterSheet.#makeDeathMove,
levelManagement: CharacterSheet.#levelManagement, levelManagement: CharacterSheet.#levelManagement,
toggleEquipItem: CharacterSheet.#toggleEquipItem, toggleEquipItem: CharacterSheet.#toggleEquipItem,
useItem: this.useItem, //TODO Fix this toggleResourceDice: CharacterSheet.#toggleResourceDice,
toChat: this.toChat handleResourceDice: CharacterSheet.#handleResourceDice,
useDowntime: this.useDowntime
}, },
window: { window: {
resizable: true resizable: true
}, },
dragDrop: [], dragDrop: [
{
dragSelector: '[data-item-id][draggable="true"]',
dropSelector: null
}
],
contextMenus: [ contextMenus: [
{ {
handler: CharacterSheet._getContextMenuOptions, handler: CharacterSheet.#getDomainCardContextOptions,
selector: '[data-item-id]', selector: '[data-item-uuid][data-type="domainCard"]',
options: {
parentClassHooks: false,
fixed: true
}
},
{
handler: CharacterSheet.#getEquipamentContextOptions,
selector: '[data-item-uuid][data-type="armor"], [data-item-uuid][data-type="weapon"]',
options: {
parentClassHooks: false,
fixed: true
}
},
{
handler: CharacterSheet.#getItemContextOptions,
selector: '[data-item-uuid][data-type="consumable"], [data-item-uuid][data-type="miscellaneous"]',
options: { options: {
parentClassHooks: false, parentClassHooks: false,
fixed: true fixed: true
@ -85,6 +107,22 @@ export default class CharacterSheet extends DHBaseActorSheet {
} }
}; };
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelectorAll('.inventory-item-resource').forEach(element => {
element.addEventListener('change', this.updateItemResource.bind(this));
});
htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => {
element.addEventListener('change', this.updateItemQuantity.bind(this));
});
// Add listener for armor marks input
htmlElement.querySelectorAll('.armor-marks-input').forEach(element => {
element.addEventListener('change', this.updateArmorMarks.bind(this));
});
}
/** @inheritDoc */ /** @inheritDoc */
async _onRender(context, options) { async _onRender(context, options) {
await super._onRender(context, options); await super._onRender(context, options);
@ -97,20 +135,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
this._createSearchFilter(); this._createSearchFilter();
} }
/* -------------------------------------------- */
getItem(element) {
const listElement = (element.target ?? element).closest('[data-item-id]');
const itemId = listElement.dataset.itemId;
switch (listElement.dataset.type) {
case 'effect':
return this.document.effects.get(itemId);
default:
return this.document.items.get(itemId);
}
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Prepare Context */ /* Prepare Context */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -160,98 +184,135 @@ export default class CharacterSheet extends DHBaseActorSheet {
case 'sidebar': case 'sidebar':
await this._prepareSidebarContext(context, options); await this._prepareSidebarContext(context, options);
break; break;
case 'biography':
await this._prepareBiographyContext(context, options);
break;
} }
return context; return context;
} }
/**
* Prepare render context for the Loadout part.
* @param {ApplicationRenderContext} context
* @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/
async _prepareLoadoutContext(context, _options) { async _prepareLoadoutContext(context, _options) {
context.listView = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsList); context.cardView = !game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsList);
} }
/**
* Prepare render context for the Sidebar part.
* @param {ApplicationRenderContext} context
* @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/
async _prepareSidebarContext(context, _options) { async _prepareSidebarContext(context, _options) {
context.isDeath = this.document.system.deathMoveViable; context.isDeath = this.document.system.deathMoveViable;
} }
/**
* Prepare render context for the Biography part.
* @param {ApplicationRenderContext} context
* @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/
async _prepareBiographyContext(context, _options) {
const { system } = this.document;
const { TextEditor } = foundry.applications.ux;
const paths = {
background: 'biography.background',
connections: 'biography.connections'
};
for (const [key, path] of Object.entries(paths)) {
const value = foundry.utils.getProperty(system, path);
context[key] = {
field: system.schema.getField(path),
value,
enriched: await TextEditor.implementation.enrichHTML(value, {
secrets: this.document.isOwner,
relativeTo: this.document
})
};
}
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Context Menu */ /* Context Menu */
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Get the set of ContextMenu options. * Get the set of ContextMenu options for DomainCards.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
* @this {CharacterSheet} * @this {CharacterSheet}
* @protected * @protected
*/ */
static _getContextMenuOptions() { static #getDomainCardContextOptions() {
/** /**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */
* Get the item from the element. const options = [
* @param {HTMLElement} el
* @returns {foundry.documents.Item?}
*/
const getItem = el => this.actor.items.get(el.closest('[data-item-id]')?.dataset.itemId);
return [
{ {
name: 'DAGGERHEART.Sheets.PC.ContextMenu.UseItem', name: 'toLoadout',
icon: '<i class="fa-solid fa-burst"></i>', icon: 'fa-solid fa-arrow-up',
condition: el => { condition: target => getDocFromElement(target).system.inVault,
const item = getItem(el); callback: target => getDocFromElement(target).update({ 'system.inVault': false })
return !['class', 'subclass'].includes(item.type);
},
callback: (button, event) => CharacterSheet.useItem.call(this, event, button)
}, },
{ {
name: 'DAGGERHEART.Sheets.PC.ContextMenu.Equip', name: 'toVault',
icon: '<i class="fa-solid fa-hands"></i>', icon: 'fa-solid fa-arrow-down',
condition: el => { condition: target => !getDocFromElement(target).system.inVault,
const item = getItem(el); callback: target => getDocFromElement(target).update({ 'system.inVault': true })
return ['weapon', 'armor'].includes(item.type) && !item.system.equipped;
},
callback: CharacterSheet.#toggleEquipItem.bind(this)
},
{
name: 'DAGGERHEART.Sheets.PC.ContextMenu.Unequip',
icon: '<i class="fa-solid fa-hands"></i>',
condition: el => {
const item = getItem(el);
return ['weapon', 'armor'].includes(item.type) && item.system.equipped;
},
callback: CharacterSheet.#toggleEquipItem.bind(this)
},
{
name: 'DAGGERHEART.Sheets.PC.ContextMenu.ToLoadout',
icon: '<i class="fa-solid fa-arrow-up"></i>',
condition: el => {
const item = getItem(el);
return ['domainCard'].includes(item.type) && item.system.inVault;
},
callback: target => getItem(target).update({ 'system.inVault': false })
},
{
name: 'DAGGERHEART.Sheets.PC.ContextMenu.ToVault',
icon: '<i class="fa-solid fa-arrow-down"></i>',
condition: el => {
const item = getItem(el);
return ['domainCard'].includes(item.type) && !item.system.inVault;
},
callback: target => getItem(target).update({ 'system.inVault': true })
},
{
name: 'DAGGERHEART.Sheets.PC.ContextMenu.SendToChat',
icon: '<i class="fa-regular fa-message"></i>',
callback: CharacterSheet.toChat.bind(this)
},
{
name: 'DAGGERHEART.Sheets.PC.ContextMenu.Edit',
icon: '<i class="fa-solid fa-pen-to-square"></i>',
callback: target => getItem(target).sheet.render({ force: true })
},
{
name: 'DAGGERHEART.Sheets.PC.ContextMenu.Delete',
icon: '<i class="fa-solid fa-trash"></i>',
callback: el => getItem(el).delete()
} }
]; ].map(option => ({
...option,
name: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.name}`,
icon: `<i class="${option.icon}"></i>`
}));
return [...options, ...this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true })];
}
/**
* Get the set of ContextMenu options for Armors and Weapons.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
* @this {CharacterSheet}
* @protected
*/
static #getEquipamentContextOptions() {
const options = [
{
name: 'equip',
icon: 'fa-solid fa-hands',
condition: target => !getDocFromElement(target).system.equipped,
callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target)
},
{
name: 'unequip',
icon: 'fa-solid fa-hands',
condition: target => getDocFromElement(target).system.equipped,
callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target)
}
].map(option => ({
...option,
name: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.name}`,
icon: `<i class="${option.icon}"></i>`
}));
return [...options, ...this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true })];
}
/**
* Get the set of ContextMenu options for Consumable and Miscellaneous.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
* @this {CharacterSheet}
* @protected
*/
static #getItemContextOptions() {
return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true });
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Filter Tracking */ /* Filter Tracking */
@ -345,7 +406,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
this.#filteredItems.inventory.search.clear(); this.#filteredItems.inventory.search.clear();
for (const li of html.querySelectorAll('.inventory-item')) { for (const li of html.querySelectorAll('.inventory-item')) {
const item = this.document.items.get(li.dataset.itemId); const item = getDocFromElement(li);
const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name);
if (matchesSearch) this.#filteredItems.inventory.search.add(item.id); if (matchesSearch) this.#filteredItems.inventory.search.add(item.id);
const { menu } = this.#filteredItems.inventory; const { menu } = this.#filteredItems.inventory;
@ -365,14 +426,14 @@ export default class CharacterSheet extends DHBaseActorSheet {
this.#filteredItems.loadout.search.clear(); this.#filteredItems.loadout.search.clear();
for (const li of html.querySelectorAll('.items-list .inventory-item, .card-list .card-item')) { for (const li of html.querySelectorAll('.items-list .inventory-item, .card-list .card-item')) {
const item = this.document.items.get(li.dataset.itemId); const item = getDocFromElement(li);
const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name);
if (matchesSearch) this.#filteredItems.loadout.search.add(item.id); if (matchesSearch) this.#filteredItems.loadout.search.add(item.id);
const { menu } = this.#filteredItems.loadout; const { menu } = this.#filteredItems.loadout;
li.hidden = !(menu.has(item.id) && matchesSearch); li.hidden = !(menu.has(item.id) && matchesSearch);
} }
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Filter Menus */ /* Filter Menus */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -416,7 +477,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
this.#filteredItems.inventory.menu.clear(); this.#filteredItems.inventory.menu.clear();
for (const li of html.querySelectorAll('.inventory-item')) { for (const li of html.querySelectorAll('.inventory-item')) {
const item = this.document.items.get(li.dataset.itemId); const item = getDocFromElement(li);
const matchesMenu = const matchesMenu =
filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f)); filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f));
@ -437,7 +498,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
this.#filteredItems.loadout.menu.clear(); this.#filteredItems.loadout.menu.clear();
for (const li of html.querySelectorAll('.items-list .inventory-item, .card-list .card-item')) { for (const li of html.querySelectorAll('.items-list .inventory-item, .card-list .card-item')) {
const item = this.document.items.get(li.dataset.itemId); const item = getDocFromElement(li);
const matchesMenu = const matchesMenu =
filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f)); filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f));
@ -448,6 +509,35 @@ export default class CharacterSheet extends DHBaseActorSheet {
} }
} }
/* -------------------------------------------- */
/* Application Listener Actions */
/* -------------------------------------------- */
async updateItemResource(event) {
const item = getDocFromElement(event.currentTarget);
if (!item) return;
const max = item.system.resource.max ? itemAbleRollParse(item.system.resource.max, this.document, item) : null;
const value = max ? Math.min(Number(event.currentTarget.value), max) : event.currentTarget.value;
await item.update({ 'system.resource.value': value });
}
async updateItemQuantity(event) {
const item = getDocFromElement(event.currentTarget);
if (!item) return;
await item.update({ 'system.quantity': event.currentTarget.value });
}
async updateArmorMarks(event) {
const armor = this.document.system.armor;
if (!armor) return;
const maxMarks = this.document.system.armorScore;
const value = Math.min(Math.max(Number(event.currentTarget.value), 0), maxMarks);
await armor.update({ 'system.marks.value': value });
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Application Clicks Actions */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -495,7 +585,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
const config = { const config = {
event: event, event: event,
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`, title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`,
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilitychecktitle', { headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: abilityLabel ability: abilityLabel
}), }),
roll: { roll: {
@ -505,13 +595,14 @@ export default class CharacterSheet extends DHBaseActorSheet {
this.document.diceRoll(config); this.document.diceRoll(config);
} }
//TODO: redo toggleEquipItem method
/** /**
* Toggles the equipped state of an item (armor or weapon). * Toggles the equipped state of an item (armor or weapon).
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #toggleEquipItem(_event, button) { static async #toggleEquipItem(_event, button) {
//TODO: redo this const item = getDocFromElement(button);
const item = this.actor.items.get(button.closest('[data-item-id]')?.dataset.itemId);
if (!item) return; if (!item) return;
if (item.system.equipped) { if (item.system.equipped) {
await item.update({ 'system.equipped': false }); await item.update({ 'system.equipped': false });
@ -528,6 +619,12 @@ export default class CharacterSheet extends DHBaseActorSheet {
await item.update({ 'system.equipped': true }); await item.update({ 'system.equipped': true });
break; break;
case 'weapon': case 'weapon':
if (this.document.effects.find(x => x.type === 'beastform')) {
return ui.notifications.warn(
game.i18n.localize('DAGGERHEART.UI.Notifications.beastformEquipWeapon')
);
}
await this.document.system.constructor.unequipBeforeEquip.bind(this.document.system)(item); await this.document.system.constructor.unequipBeforeEquip.bind(this.document.system)(item);
await item.update({ 'system.equipped': true }); await item.update({ 'system.equipped': true });
@ -559,76 +656,69 @@ export default class CharacterSheet extends DHBaseActorSheet {
* Toggles whether an item is stored in the vault. * Toggles whether an item is stored in the vault.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #toggleVault(event, button) { static async #toggleVault(_event, button) {
const docId = button.closest('[data-item-id]')?.dataset.itemId; const doc = getDocFromElement(button);
const doc = this.document.items.get(docId);
await doc?.update({ 'system.inVault': !doc.system.inVault }); await doc?.update({ 'system.inVault': !doc.system.inVault });
} }
/** /**
* Trigger the context menu. * Toggle the used state of a resource dice.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static #triggerContextMenu(event, _) { static async #toggleResourceDice(event, target) {
return CONFIG.ux.ContextMenu.triggerContextMenu(event); const item = getDocFromElement(target);
const { dice } = event.target.closest('.item-resource').dataset;
const diceState = item.system.resource.diceStates[dice];
await item.update({
[`system.resource.diceStates.${dice}.used`]: diceState ? !diceState.used : true
});
} }
/** /**
* Use a item * Handle the roll values of resource dice.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async useItem(event, button) { static async #handleResourceDice(_, target) {
const item = this.getItem(button); const item = getDocFromElement(target);
if (!item) return; if (!item) return;
// Should dandle its actions. Or maybe they'll be separate buttons as per an Issue on the board const rollValues = await game.system.api.applications.dialogs.ResourceDiceDialog.create(item, this.document);
if (item.type === 'feature') { if (!rollValues) return;
item.use(event);
} else if (item instanceof ActiveEffect) { await item.update({
item.toChat(this); 'system.resource.diceStates': rollValues.reduce((acc, state, index) => {
} else { acc[index] = { value: state.value, used: state.used };
const wasUsed = await item.use(event); return acc;
if (wasUsed && item.type === 'weapon') { }, {})
Hooks.callAll(CONFIG.DH.HOOKS.characterAttack, {}); });
}
}
} }
/** static useDowntime(_, button) {
* Send item to Chat new game.system.api.applications.dialogs.Downtime(this.document, button.dataset.type === 'shortRest').render(
* @type {ApplicationClickAction} true
*/ );
static async toChat(event, button) {
if (button?.dataset?.type === 'experience') {
const experience = this.document.system.experiences[button.dataset.uuid];
const cls = getDocumentClass('ChatMessage');
const systemData = {
name: game.i18n.localize('DAGGERHEART.GENERAL.Experience.single'),
description: `${experience.name} ${experience.total < 0 ? experience.total : `+${experience.total}`}`
};
const msg = new cls({
type: 'abilityUse',
user: game.user.id,
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/ability-use.hbs',
systemData
)
});
cls.create(msg.toObject());
} else {
const item = this.getItem(event);
if (!item) return;
item.toChat(this.document.id);
}
} }
async _onDragStart(_, event) { async _onDragStart(event) {
const item = getDocFromElement(event.target);
const dragData = {
type: item.documentName,
uuid: item.uuid
};
event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
super._onDragStart(event); super._onDragStart(event);
} }
async _onDrop(event) { async _onDrop(event) {
// Prevent event bubbling to avoid duplicate handling
event.preventDefault();
event.stopPropagation();
super._onDrop(event); super._onDrop(event);
this._onDropItem(event, TextEditor.getDragEventData(event)); this._onDropItem(event, TextEditor.getDragEventData(event));
} }

View file

@ -6,11 +6,7 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['actor', 'companion'], classes: ['actor', 'companion'],
position: { width: 300 }, position: { width: 300 },
actions: { actions: {}
viewActor: this.viewActor,
useItem: this.useItem,
toChat: this.toChat
}
}; };
static PARTS = { static PARTS = {
@ -29,52 +25,4 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }
}; };
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
static async viewActor(_, button) {
const target = button.closest('[data-item-uuid]');
const actor = await foundry.utils.fromUuid(target.dataset.itemUuid);
if (!actor) return;
actor.sheet.render(true);
}
getAction(element) {
const itemId = (element.target ?? element).closest('[data-item-id]').dataset.itemId,
item = this.document.system.actions.find(x => x.id === itemId);
return item;
}
static async useItem(event) {
const action = this.getAction(event) ?? this.actor.system.attack;
action.use(event);
}
static async toChat(event, button) {
if (button?.dataset?.type === 'experience') {
const experience = this.document.system.experiences[button.dataset.uuid];
const cls = getDocumentClass('ChatMessage');
const systemData = {
name: game.i18n.localize('DAGGERHEART.GENERAL.Experience.single'),
description: `${experience.name} ${experience.total < 0 ? experience.total : `+${experience.total}`}`
};
const msg = new cls({
type: 'abilityUse',
user: game.user.id,
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/ability-use.hbs',
systemData
)
});
cls.create(msg.toObject());
} else {
const item = this.getAction(event) ?? this.document.system.attack;
item.toChat(this.document.id);
}
}
} }

View file

@ -9,10 +9,7 @@ export default class DhpEnvironment extends DHBaseActorSheet {
position: { position: {
width: 500 width: 500
}, },
actions: { actions: {},
useItem: this.useItem,
toChat: this.toChat
},
dragDrop: [{ dragSelector: '.action-section .inventory-item', dropSelector: null }] dragDrop: [{ dragSelector: '.action-section .inventory-item', dropSelector: null }]
}; };
@ -35,47 +32,67 @@ export default class DhpEnvironment extends DHBaseActorSheet {
} }
}; };
/* -------------------------------------------- */ /**@inheritdoc */
async _preparePartContext(partId, context, options) {
getItem(element) { context = await super._preparePartContext(partId, context, options);
const itemId = (element.target ?? element).closest('[data-item-id]').dataset.itemId, switch (partId) {
item = this.document.items.get(itemId); case 'header':
return item; await this._prepareHeaderContext(context, options);
break;
case 'notes':
await this._prepareNotesContext(context, options);
break;
}
return context;
} }
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
/** /**
* * Prepare render context for the Biography part.
* @type {ApplicationClickAction} * @param {ApplicationRenderContext} context
* @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/ */
async viewAdversary(_, button) { async _prepareNotesContext(context, _options) {
const target = button.closest('[data-item-uuid]'); const { system } = this.document;
const adversary = await foundry.utils.fromUuid(target.dataset.itemUuid); const { TextEditor } = foundry.applications.ux;
if (!adversary) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.adversaryMissing'));
return;
}
adversary.sheet.render({ force: true }); const paths = {
} notes: 'notes'
};
static async useItem(event, button) { for (const [key, path] of Object.entries(paths)) {
const action = this.getItem(event); const value = foundry.utils.getProperty(system, path);
if (!action) { context[key] = {
await this.viewAdversary(event, button); field: system.schema.getField(path),
} else { value,
action.use(event); enriched: await TextEditor.implementation.enrichHTML(value, {
secrets: this.document.isOwner,
relativeTo: this.document
})
};
} }
} }
static async toChat(event) { /**
const item = this.getItem(event); * Prepare render context for the Header part.
item.toChat(this.document.id); * @param {ApplicationRenderContext} context
* @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/
async _prepareHeaderContext(context, _options) {
const { system } = this.document;
const { TextEditor } = foundry.applications.ux;
context.description = await TextEditor.implementation.enrichHTML(system.description, {
secrets: this.document.isOwner,
relativeTo: this.document
});
} }
/* -------------------------------------------- */
async _onDragStart(event) { async _onDragStart(event) {
const item = event.currentTarget.closest('.inventory-item'); const item = event.currentTarget.closest('.inventory-item');

View file

@ -1,5 +1,6 @@
export { default as DHApplicationMixin } from './application-mixin.mjs'; export { default as DHApplicationMixin } from './application-mixin.mjs';
export { default as DHBaseItemSheet } from './base-item.mjs'; export { default as DHBaseItemSheet } from './base-item.mjs';
export { default as DHHeritageSheet } from './heritage-sheet.mjs'; export { default as DHHeritageSheet } from './heritage-sheet.mjs';
export { default as DHItemAttachmentSheet } from './item-attachment-sheet.mjs';
export { default as DHBaseActorSheet } from './base-actor.mjs'; export { default as DHBaseActorSheet } from './base-actor.mjs';
export { default as DHBaseActorSettings } from './actor-setting.mjs'; export { default as DHBaseActorSettings } from './actor-setting.mjs';

View file

@ -42,9 +42,12 @@ export default class DHBaseActorSettings extends DHApplicationMixin(DocumentShee
/**@inheritdoc */ /**@inheritdoc */
async _prepareContext(options) { async _prepareContext(options) {
const context = await super._prepareContext(options); const context = await super._prepareContext(options);
context.systemFields.attack.fields = this.actor.system.attack.schema.fields;
context.isNPC = this.actor.isNPC; context.isNPC = this.actor.isNPC;
if (context.systemFields.attack) {
context.systemFields.attack.fields = this.actor.system.attack.schema.fields;
}
return context; return context;
} }
} }

View file

@ -1,5 +1,10 @@
const { HandlebarsApplicationMixin } = foundry.applications.api; const { HandlebarsApplicationMixin } = foundry.applications.api;
import { tagifyElement } from '../../../helpers/utils.mjs'; import { getDocFromElement, tagifyElement } from '../../../helpers/utils.mjs';
import DHActionConfig from '../../sheets-configs/action-config.mjs';
/**
* @typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction
*/
/** /**
* @typedef {object} DragDropConfig * @typedef {object} DragDropConfig
@ -71,11 +76,34 @@ export default function DHApplicationMixin(Base) {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'sheet', 'dh-style'], classes: ['daggerheart', 'sheet', 'dh-style'],
actions: { actions: {
triggerContextMenu: DHSheetV2.#triggerContextMenu,
createDoc: DHSheetV2.#createDoc, createDoc: DHSheetV2.#createDoc,
editDoc: DHSheetV2.#editDoc, editDoc: DHSheetV2.#editDoc,
deleteDoc: DHSheetV2.#deleteDoc deleteDoc: DHSheetV2.#deleteDoc,
toChat: DHSheetV2.#toChat,
useItem: DHSheetV2.#useItem,
useAction: DHSheetV2.#useAction,
toggleEffect: DHSheetV2.#toggleEffect,
toggleExtended: DHSheetV2.#toggleExtended
}, },
contextMenus: [], contextMenus: [
{
handler: DHSheetV2.#getEffectContextOptions,
selector: '[data-item-uuid][data-type="effect"]',
options: {
parentClassHooks: false,
fixed: true
}
},
{
handler: DHSheetV2.#getActionContextOptions,
selector: '[data-item-uuid][data-type="action"]',
options: {
parentClassHooks: false,
fixed: true
}
}
],
dragDrop: [], dragDrop: [],
tagifyConfigs: [] tagifyConfigs: []
}; };
@ -99,6 +127,27 @@ export default function DHApplicationMixin(Base) {
this._createTagifyElements(this.options.tagifyConfigs); this._createTagifyElements(this.options.tagifyConfigs);
} }
/* -------------------------------------------- */
/* Sync Parts */
/* -------------------------------------------- */
/**@inheritdoc */
_syncPartState(partId, newElement, priorElement, state) {
super._syncPartState(partId, newElement, priorElement, state);
for (const el of priorElement.querySelectorAll('.extensible.extended')) {
const { actionId, itemUuid } = el.parentElement.dataset;
const selector = `${actionId ? `[data-action-id="${actionId}"]` : `[data-item-uuid="${itemUuid}"]`} .extensible`;
const newExtensible = newElement.querySelector(selector);
if (!newExtensible) continue;
newExtensible.classList.add('extended');
const descriptionElement = newExtensible.querySelector('.invetory-description');
if (descriptionElement) {
this.#prepareInventoryDescription(newExtensible, descriptionElement);
}
}
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Tags */ /* Tags */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -185,12 +234,146 @@ export default function DHApplicationMixin(Base) {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Get the set of ContextMenu options which should be used for journal entry pages in the sidebar. * Get the set of ContextMenu options for DomainCards.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
* @this {CharacterSheet}
* @protected * @protected
*/ */
_getEntryContextOptions() { static #getEffectContextOptions() {
return []; /**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */
const options = [
{
name: 'disableEffect',
icon: 'fa-solid fa-lightbulb',
condition: target => !getDocFromElement(target).disabled,
callback: target => getDocFromElement(target).update({ disabled: true })
},
{
name: 'enableEffect',
icon: 'fa-regular fa-lightbulb',
condition: target => getDocFromElement(target).disabled,
callback: target => getDocFromElement(target).update({ disabled: false })
}
].map(option => ({
...option,
name: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.name}`,
icon: `<i class="${option.icon}"></i>`
}));
return [...options, ...this._getContextMenuCommonOptions.call(this, { toChat: true })];
}
/**
* Get the set of ContextMenu options for Actions.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
* @this {DHSheetV2}
* @protected
*/
static #getActionContextOptions() {
/**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */
const getAction = target => {
const { actionId } = target.closest('[data-action-id]').dataset;
const { actions, attack } = this.document.system;
return attack?.id === actionId ? attack : actions?.find(a => a.id === actionId);
};
const options = [
{
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem',
icon: 'fa-solid fa-burst',
condition:
this.document instanceof foundry.documents.Actor ||
(this.document instanceof foundry.documents.Item && this.document.parent),
callback: (target, event) => getAction(target).use(event)
},
{
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
icon: 'fa-solid fa-message',
callback: target => getAction(target).toChat(this.document.id)
},
{
name: 'CONTROLS.CommonEdit',
icon: 'fa-solid fa-pen-to-square',
callback: target => new DHActionConfig(getAction(target)).render({ force: true })
},
{
name: 'CONTROLS.CommonDelete',
icon: 'fa-solid fa-trash',
condition: target => {
const { actionId } = target.closest('[data-action-id]').dataset;
const { attack } = this.document.system;
return attack?.id !== actionId;
},
callback: async target => {
const action = getAction(target);
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize(`DAGGERHEART.GENERAL.Action.single`),
name: action.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', {
name: action.name
})
});
if (!confirmed) return;
return this.document.update({
'system.actions': this.document.system.actions.filter(a => a.id !== action.id)
});
}
}
].map(option => ({
...option,
icon: `<i class="${option.icon}"></i>`
}));
return options;
}
/**
* Get the set of ContextMenu options.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
*/
_getContextMenuCommonOptions({ usable = false, toChat = false, deletable = true }) {
const options = [
{
name: 'CONTROLS.CommonEdit',
icon: 'fa-solid fa-pen-to-square',
callback: target => getDocFromElement(target).sheet.render({ force: true })
}
];
if (usable)
options.unshift({
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem',
icon: 'fa-solid fa-burst',
callback: (target, event) => getDocFromElement(target).use(event)
});
if (toChat)
options.unshift({
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
icon: 'fa-solid fa-message',
callback: target => getDocFromElement(target).toChat(this.document.id)
});
if (deletable)
options.push({
name: 'CONTROLS.CommonDelete',
icon: 'fa-solid fa-trash',
callback: (target, event) => {
const doc = getDocFromElement(target);
if (event.shiftKey) return doc.delete();
else return doc.deleteDialog();
}
});
return options.map(option => ({
...option,
icon: `<i class="${option.icon}"></i>`
}));
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -207,50 +390,233 @@ export default function DHApplicationMixin(Base) {
return context; return context;
} }
/* -------------------------------------------- */
/* Prepare Descriptions */
/* -------------------------------------------- */
/**
* Prepares and enriches an inventory item or action description for display.
* @param {HTMLElement} extensibleElement - The parent element containing the description.
* @param {HTMLElement} descriptionElement - The element where the enriched description will be rendered.
* @returns {Promise<void>}
*/
async #prepareInventoryDescription(extensibleElement, descriptionElement) {
const parent = extensibleElement.closest('[data-item-uuid], [data-action-id]');
const { actionId, itemUuid } = parent?.dataset || {};
if (!actionId && !itemUuid) return;
const doc = itemUuid
? getDocFromElement(extensibleElement)
: this.document.system.attack?.id === actionId
? this.document.system.attack
: this.document.system.actions?.find(a => a.id === actionId);
if (!doc) return;
const description = doc.system?.description ?? doc.description;
const isAction = !!actionId;
descriptionElement.innerHTML = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
description,
{
relativeTo: isAction ? doc.parent : doc,
rollData: doc.getRollData?.(),
secrets: isAction ? doc.parent.isOwner : doc.isOwner
}
);
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Application Clicks Actions */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Create an embedded document. * Create an embedded document.
* @param {PointerEvent} event - The originating click event * @type {ApplicationClickAction}
* @param {HTMLElement} button - The capturing HTML element which defines the [data-action="removeAction"]
*/ */
static async #createDoc(event, button) { static async #createDoc(event, target) {
const { documentClass, type } = button.dataset; const { documentClass, type, inVault, disabled } = target.dataset;
console.log(documentClass, type); const parentIsItem = this.document.documentName === 'Item';
const parent = this.document; const parent = parentIsItem && documentClass === 'Item' ? null : this.document;
const cls = getDocumentClass(documentClass); if (type === 'action') {
return await cls.createDocuments( const { type: actionType } =
[ (await foundry.applications.api.DialogV2.input({
window: { title: 'Select Action Type' },
classes: ['daggerheart', 'dh-style'],
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/actionTypes/actionType.hbs',
{
types: CONFIG.DH.ACTIONS.actionTypes,
itemName: game.i18n.localize('DAGGERHEART.CONFIG.SelectAction.selectAction')
}
),
ok: {
label: game.i18n.format('DOCUMENT.Create', {
type: game.i18n.localize('DAGGERHEART.GENERAL.Action.single')
})
}
})) ?? {};
if (!actionType) return;
const cls = game.system.api.models.actions.actionsTypes[actionType];
const action = new cls(
{ {
name: cls.defaultName({ type, parent }), _id: foundry.utils.randomID(),
type type: actionType,
name: game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[actionType].name),
...cls.getSourceConfig(this.document)
},
{
parent: this.document
} }
], );
{ parent, renderSheet: !event.shiftKey } await this.document.update({ 'system.actions': [...this.document.system.actions, action] });
); await new DHActionConfig(this.document.system.actions[this.document.system.actions.length - 1]).render({
force: true
});
return action;
} else {
const cls = getDocumentClass(documentClass);
const data = {
name: cls.defaultName({ type, parent }),
type
};
if (inVault) data['system.inVault'] = true;
if (disabled) data.disabled = true;
const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey });
if (parentIsItem && type === 'feature') {
await this.document.update({
'system.features': this.document.system.toObject().features.concat(doc.uuid)
});
}
return doc;
}
} }
/** /**
* Renders an embedded document. * Renders an embedded document.
* @param {PointerEvent} event - The originating click event * @type {ApplicationClickAction}
* @param {HTMLElement} button - The capturing HTML element which defines the [data-action="removeAction"]
*/ */
static #editDoc(_event, button) { static #editDoc(_event, target) {
const { type, docId } = button.dataset; const doc = getDocFromElement(target);
this.document.getEmbeddedDocument(type, docId, { strict: true }).sheet.render({ force: true }); if (doc) return doc.sheet.render({ force: true });
// TODO: REDO this
const { actionId } = target.closest('[data-action-id]').dataset;
const { actions, attack } = this.document.system;
const action = attack?.id === actionId ? attack : actions?.find(a => a.id === actionId);
new DHActionConfig(action).render({ force: true });
} }
/** /**
* Delete an embedded document. * Delete an embedded document.
* @param {PointerEvent} _event - The originating click event * @type {ApplicationClickAction}
* @param {HTMLElement} button - The capturing HTML element which defines the [data-action="removeAction"]
*/ */
static async #deleteDoc(_event, button) { static async #deleteDoc(event, target) {
const { type, docId } = button.dataset; const doc = getDocFromElement(target);
await this.document.getEmbeddedDocument(type, docId, { strict: true }).delete();
if (doc) {
if (event.shiftKey) return doc.delete();
else return await doc.deleteDialog();
}
// TODO: REDO this
const { actionId } = target.closest('[data-action-id]').dataset;
const { actions, attack } = this.document.system;
if (attack?.id === actionId) return;
const action = actions.find(a => a.id === actionId);
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize(`DAGGERHEART.GENERAL.Action.single`),
name: action.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: action.name })
});
if (!confirmed) return;
}
return await this.document.update({
'system.actions': actions.filter(a => a.id !== action.id)
});
}
/**
* Send item to Chat
* @type {ApplicationClickAction}
*/
static async #toChat(_event, target) {
let doc = getDocFromElement(target);
// TODO: REDO this
if (!doc) {
const { actionId } = target.closest('[data-action-id]').dataset;
const { actions, attack } = this.document.system;
doc = attack?.id === actionId ? attack : actions?.find(a => a.id === actionId);
}
return doc.toChat(this.document.id);
}
/**
* Use a item
* @type {ApplicationClickAction}
*/
static async #useItem(event, target) {
let doc = getDocFromElement(target);
// TODO: REDO this
if (!doc) {
const { actionId } = target.closest('[data-action-id]').dataset;
const { actions, attack } = this.document.system;
doc = attack?.id === actionId ? attack : actions?.find(a => a.id === actionId);
if (this.document instanceof foundry.documents.Item && !this.document.parent) return;
}
await doc.use(event);
}
/**
* Use a item
* @type {ApplicationClickAction}
*/
static async #useAction(event, target) {
const doc = getDocFromElement(target);
const { actionId } = target.closest('[data-action-id]').dataset;
const { actions, attack } = doc.system;
const action = attack?.id === actionId ? attack : actions?.find(a => a.id === actionId);
await action.use(event, doc);
}
/**
* Toggle a ActiveEffect
* @type {ApplicationClickAction}
*/
static async #toggleEffect(_, target) {
const doc = getDocFromElement(target);
await doc.update({ disabled: !doc.disabled });
}
/**
* Trigger the context menu.
* @type {ApplicationClickAction}
*/
static #triggerContextMenu(event, _) {
return CONFIG.ux.ContextMenu.triggerContextMenu(event);
}
/**
* Toggle the 'extended' class on the .extensible element inside inventory-item-content
* @type {ApplicationClickAction}
* @this {DHSheetV2}
*/
static async #toggleExtended(_, target) {
const container = target.closest('.inventory-item');
const extensible = container?.querySelector('.extensible');
const t = extensible?.classList.toggle('extended');
const descriptionElement = extensible?.querySelector('.invetory-description');
if (t && !!descriptionElement) this.#prepareInventoryDescription(extensible, descriptionElement);
} }
} }

View file

@ -21,11 +21,24 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
submitOnChange: true submitOnChange: true
}, },
actions: { actions: {
openSettings: DHBaseActorSheet.#openSettings openSettings: DHBaseActorSheet.#openSettings,
sendExpToChat: DHBaseActorSheet.#sendExpToChat
}, },
dragDrop: [] contextMenus: [
{
handler: DHBaseActorSheet.#getFeatureContextOptions,
selector: '[data-item-uuid][data-type="feature"]',
options: {
parentClassHooks: false,
fixed: true
}
}
],
dragDrop: [{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null }]
}; };
/* -------------------------------------------- */
/**@type {typeof DHBaseActorSettings}*/ /**@type {typeof DHBaseActorSettings}*/
#settingSheet; #settingSheet;
@ -35,6 +48,10 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
return (this.#settingSheet ??= SheetClass ? new SheetClass({ document: this.document }) : null); return (this.#settingSheet ??= SheetClass ? new SheetClass({ document: this.document }) : null);
} }
/* -------------------------------------------- */
/* Prepare Context */
/* -------------------------------------------- */
/**@inheritdoc */ /**@inheritdoc */
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
@ -42,6 +59,54 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
return context; return context;
} }
/**@inheritdoc */
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'effects':
await this._prepareEffectsContext(context, options);
break;
}
return context;
}
/**
* Prepare render context for the Effect part.
* @param {ApplicationRenderContext} context
* @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/
async _prepareEffectsContext(context, _options) {
context.effects = {
actives: [],
inactives: []
};
for (const effect of this.actor.allApplicableEffects()) {
const list = effect.active ? context.effects.actives : context.effects.inactives;
list.push(effect);
}
}
/* -------------------------------------------- */
/* Context Menu */
/* -------------------------------------------- */
/**
* Get the set of ContextMenu options for Features.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
* @this {DHSheetV2}
* @protected
*/
static #getFeatureContextOptions() {
return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true });
}
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
/** /**
* Open the Actor Setting Sheet * Open the Actor Setting Sheet
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
@ -49,4 +114,50 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
static async #openSettings() { static async #openSettings() {
await this.settingSheet.render({ force: true }); await this.settingSheet.render({ force: true });
} }
/**
* Send Experience to Chat
* @type {ApplicationClickAction}
*/
static async #sendExpToChat(_, button) {
const experience = this.document.system.experiences[button.dataset.id];
const systemData = {
name: game.i18n.localize('DAGGERHEART.GENERAL.Experience.single'),
description: `${experience.name} ${experience.value.signedString()}`
};
foundry.documents.ChatMessage.implementation.create({
type: 'abilityUse',
user: game.user.id,
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/ability-use.hbs',
systemData
)
});
}
/* -------------------------------------------- */
/* Application Drag/Drop */
/* -------------------------------------------- */
/**
* On dragStart on the item.
* @param {DragEvent} event - The drag event
*/
async _onDragStart(event) {
const attackItem = event.currentTarget.closest('.inventory-item[data-type="attack"]');
if (attackItem) {
const attackData = {
type: 'Attack',
actorUuid: this.document.uuid,
img: this.document.system.attack.img,
fromInternal: true
};
event.dataTransfer.setData('text/plain', JSON.stringify(attackData));
event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0);
}
}
} }

View file

@ -1,4 +1,4 @@
import DHActionConfig from '../../sheets-configs/action-config.mjs'; import { getDocFromElement } from '../../../helpers/utils.mjs';
import DHApplicationMixin from './application-mixin.mjs'; import DHApplicationMixin from './application-mixin.mjs';
const { ItemSheetV2 } = foundry.applications.sheets; const { ItemSheetV2 } = foundry.applications.sheets;
@ -15,20 +15,31 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['item'], classes: ['item'],
position: { width: 600 }, position: { width: 600 },
window: { resizable: true },
form: { form: {
submitOnChange: true submitOnChange: true
}, },
actions: { actions: {
addAction: DHBaseItemSheet.#addAction,
editAction: DHBaseItemSheet.#editAction,
removeAction: DHBaseItemSheet.#removeAction, removeAction: DHBaseItemSheet.#removeAction,
addFeature: DHBaseItemSheet.#addFeature, addFeature: DHBaseItemSheet.#addFeature,
editFeature: DHBaseItemSheet.#editFeature, deleteFeature: DHBaseItemSheet.#deleteFeature,
removeFeature: DHBaseItemSheet.#removeFeature addResource: DHBaseItemSheet.#addResource,
removeResource: DHBaseItemSheet.#removeResource
}, },
dragDrop: [ dragDrop: [
{ dragSelector: null, dropSelector: '.tab.features .drop-section' }, { dragSelector: null, dropSelector: '.tab.features .drop-section' },
{ dragSelector: '.feature-item', dropSelector: null } { dragSelector: '.feature-item', dropSelector: null },
{ dragSelector: '.action-item', dropSelector: null }
],
contextMenus: [
{
handler: DHBaseItemSheet.#getFeatureContextOptions,
selector: '[data-item-uuid][data-type="feature"]',
options: {
parentClassHooks: false,
fixed: true
}
}
] ]
}; };
@ -37,7 +48,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
primary: { primary: {
tabs: [{ id: 'description' }, { id: 'settings' }, { id: 'actions' }], tabs: [{ id: 'description' }, { id: 'settings' }, { id: 'actions' }, { id: 'effects' }],
initial: 'description', initial: 'description',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }
@ -48,8 +59,8 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
/* -------------------------------------------- */ /* -------------------------------------------- */
/**@inheritdoc */ /**@inheritdoc */
async _preparePartContext(partId, context) { async _preparePartContext(partId, context, options) {
await super._preparePartContext(partId, context); await super._preparePartContext(partId, context, options);
const { TextEditor } = foundry.applications.ux; const { TextEditor } = foundry.applications.ux;
switch (partId) { switch (partId) {
@ -61,77 +72,78 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
secrets: this.item.isOwner secrets: this.item.isOwner
}); });
break; break;
case 'effects':
await this._prepareEffectsContext(context, options);
break;
case 'features':
context.isGM = game.user.isGM;
break;
} }
return context; return context;
} }
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
/** /**
* Render a dialog prompting the user to select an action type. * Prepare render context for the Effect part.
* * @param {ApplicationRenderContext} context
* @returns {Promise<object>} An object containing the selected action type. * @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/ */
static async selectActionType() { async _prepareEffectsContext(context, _options) {
const content = await foundry.applications.handlebars.renderTemplate( context.effects = {
'systems/daggerheart/templates/actionTypes/actionType.hbs', actives: [],
{ types: CONFIG.DH.ACTIONS.actionTypes } inactives: []
), };
title = 'Select Action Type';
return foundry.applications.api.DialogV2.prompt({ for (const effect of this.item.effects) {
window: { title }, const list = effect.active ? context.effects.actives : context.effects.inactives;
content, list.push(effect);
ok: {
label: title,
callback: (event, button, dialog) => button.form.elements.type.value
}
});
}
/**
* Add a new action to the item, prompting the user for its type.
* @type {ApplicationClickAction}
*/
static async #addAction(_event, _button) {
const actionType = await DHBaseItemSheet.selectActionType();
if (!actionType) return;
try {
const cls =
game.system.api.models.actions.actionsTypes[actionType] ??
game.system.api.models.actions.actionsTypes.attack,
action = new cls(
{
_id: foundry.utils.randomID(),
type: actionType,
name: game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[actionType].name),
...cls.getSourceConfig(this.document)
},
{
parent: this.document
}
);
await this.document.update({ 'system.actions': [...this.document.system.actions, action] });
await new DHActionConfig(this.document.system.actions[this.document.system.actions.length - 1]).render({
force: true
});
} catch (error) {
console.log(error);
} }
} }
/* -------------------------------------------- */
/* Context Menu */
/* -------------------------------------------- */
/** /**
* Edit an existing action on the item * Get the set of ContextMenu options for Features.
* @type {ApplicationClickAction} * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
* @this {DHSheetV2}
* @protected
*/ */
static async #editAction(_event, button) { static #getFeatureContextOptions() {
const action = this.document.system.actions[button.dataset.index]; const options = this._getContextMenuCommonOptions({ usable: true, toChat: true, deletable: false });
await new DHActionConfig(action).render({ force: true }); options.push({
name: 'CONTROLS.CommonDelete',
icon: '<i class="fa-solid fa-trash"></i>',
callback: async target => {
const feature = getDocFromElement(target);
if (!feature) return;
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize(`TYPES.Item.feature`),
name: feature.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', {
name: feature.name
})
});
if (!confirmed) return;
await this.document.update({
'system.features': this.document.system.toObject().features.filter(uuid => uuid !== feature.uuid)
});
}
});
return options;
} }
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
/** /**
* Remove an action from the item. * Remove an action from the item.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
@ -139,6 +151,21 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
static async #removeAction(event, button) { static async #removeAction(event, button) {
event.stopPropagation(); event.stopPropagation();
const actionIndex = button.closest('[data-index]').dataset.index; const actionIndex = button.closest('[data-index]').dataset.index;
const action = this.document.system.actions[actionIndex];
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize(`DAGGERHEART.GENERAL.Action.single`),
name: action.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: action.name })
});
if (!confirmed) return;
}
await this.document.update({ await this.document.update({
'system.actions': this.document.system.actions.filter((_, index) => index !== Number.parseInt(actionIndex)) 'system.actions': this.document.system.actions.filter((_, index) => index !== Number.parseInt(actionIndex))
}); });
@ -148,43 +175,49 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
* Add a new feature to the item, prompting the user for its type. * Add a new feature to the item, prompting the user for its type.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #addFeature(_event, _button) { static async #addFeature(_, target) {
const feature = await game.items.documentClass.create({ const { type } = target.dataset;
type: 'feature', const cls = foundry.documents.Item.implementation;
name: game.i18n.format('DOCUMENT.New', { type: game.i18n.localize('TYPES.Item.feature') }) const feature = await cls.create({
'type': 'feature',
'name': cls.defaultName({ type: 'feature' }),
'system.subType': CONFIG.DH.ITEM.featureSubTypes[type]
}); });
await this.document.update({ await this.document.update({
'system.features': [...this.document.system.features.filter(x => x).map(x => x.uuid), feature.uuid] 'system.features': [...this.document.system.features, feature].map(f => f.uuid)
}); });
} }
/**
* Edit an existing feature on the item
* @type {ApplicationClickAction}
*/
static async #editFeature(_event, button) {
const target = button.closest('.feature-item');
const feature = this.document.system.features.find(x => x?.id === target.id);
if (!feature) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.notifications.featureIsMissing'));
return;
}
feature.sheet.render(true);
}
/** /**
* Remove a feature from the item. * Remove a feature from the item.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #removeFeature(event, button) { static async #deleteFeature(_, target) {
event.stopPropagation(); const feature = getDocFromElement(target);
const target = button.closest('.feature-item'); if (!feature) return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing'));
await feature.update({ 'system.subType': null });
await this.document.update({ await this.document.update({
'system.features': this.document.system.features 'system.features': this.document.system.features.map(x => x.uuid).filter(uuid => uuid !== feature.uuid)
.filter(feature => feature && feature.id !== target.id) });
.map(x => x.uuid) }
/**
* Add a resource to the item.
* @type {ApplicationClickAction}
*/
static async #addResource() {
await this.document.update({
'system.resource': { type: 'simple', value: 0 }
});
}
/**
* Remove the resource from the item.
* @type {ApplicationClickAction}
*/
static async #removeResource() {
await this.document.update({
'system.resource': null
}); });
} }
@ -202,13 +235,30 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
if (featureItem) { if (featureItem) {
const feature = this.document.system.features.find(x => x?.id === featureItem.id); const feature = this.document.system.features.find(x => x?.id === featureItem.id);
if (!feature) { if (!feature) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.notifications.featureIsMissing')); ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing'));
return; return;
} }
const featureData = { type: 'Item', data: { ...feature.toObject(), _id: null }, fromInternal: true }; const featureData = { type: 'Item', data: { ...feature.toObject(), _id: null }, fromInternal: true };
event.dataTransfer.setData('text/plain', JSON.stringify(featureData)); event.dataTransfer.setData('text/plain', JSON.stringify(featureData));
event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0); event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0);
} else {
const actionItem = event.currentTarget.closest('.action-item');
if (actionItem) {
const action = this.document.system.actions[actionItem.dataset.index];
if (!action) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.actionIsMissing'));
return;
}
const actionData = {
type: 'Action',
data: { ...action.toObject(), id: action.id, itemUuid: this.document.uuid },
fromInternal: true
};
event.dataTransfer.setData('text/plain', JSON.stringify(actionData));
event.dataTransfer.setDragImage(actionItem.querySelector('img'), 60, 0);
}
} }
} }

View file

@ -10,10 +10,6 @@ export default class DHHeritageSheet extends DHBaseItemSheet {
static PARTS = { static PARTS = {
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' }, description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' },
feature: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-features.hbs',
scrollable: ['.feature']
},
effects: { effects: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs', template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs',
scrollable: ['.effects'] scrollable: ['.effects']

View file

@ -0,0 +1,85 @@
export default function ItemAttachmentSheet(Base) {
return class extends Base {
static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
dragDrop: [
...(super.DEFAULT_OPTIONS.dragDrop || []),
{ dragSelector: null, dropSelector: '.attachments-section' }
],
actions: {
...super.DEFAULT_OPTIONS.actions,
removeAttachment: this.#removeAttachment
}
};
static PARTS = {
...super.PARTS,
attachments: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-attachments.hbs',
scrollable: ['.attachments']
}
};
static TABS = {
...super.TABS,
primary: {
...super.TABS?.primary,
tabs: [...(super.TABS?.primary?.tabs || []), { id: 'attachments' }],
initial: super.TABS?.primary?.initial || 'description',
labelPrefix: super.TABS?.primary?.labelPrefix || 'DAGGERHEART.GENERAL.Tabs'
}
};
async _preparePartContext(partId, context) {
await super._preparePartContext(partId, context);
if (partId === 'attachments') {
context.attachedItems = await prepareAttachmentContext(this.document);
}
return context;
}
async _onDrop(event) {
const data = TextEditor.getDragEventData(event);
const attachmentsSection = event.target.closest('.attachments-section');
if (!attachmentsSection) return super._onDrop(event);
event.preventDefault();
event.stopPropagation();
const item = await Item.implementation.fromDropData(data);
if (!item) return;
// Call the data model's public method
await this.document.system.addAttachment(item);
}
static async #removeAttachment(event, target) {
// Call the data model's public method
await this.document.system.removeAttachment(target.dataset.uuid);
}
async _preparePartContext(partId, context) {
await super._preparePartContext(partId, context);
if (partId === 'attachments') {
// Keep this simple UI preparation in the mixin
const attachedUUIDs = this.document.system.attached;
context.attachedItems = await Promise.all(
attachedUUIDs.map(async uuid => {
const item = await fromUuid(uuid);
return {
uuid: uuid,
name: item?.name || 'Unknown Item',
img: item?.img || 'icons/svg/item-bag.svg'
};
})
);
}
return context;
}
};
}

View file

@ -3,12 +3,80 @@ import DHHeritageSheet from '../api/heritage-sheet.mjs';
export default class AncestrySheet extends DHHeritageSheet { export default class AncestrySheet extends DHHeritageSheet {
/**@inheritdoc */ /**@inheritdoc */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['ancestry'] classes: ['ancestry'],
actions: {
editFeature: AncestrySheet.#editFeature,
removeFeature: AncestrySheet.#removeFeature
},
dragDrop: [{ dragSelector: null, dropSelector: '.tab.features .drop-section' }]
}; };
/**@inheritdoc */ /**@inheritdoc */
static PARTS = { static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/items/ancestry/header.hbs' }, header: { template: 'systems/daggerheart/templates/sheets/items/ancestry/header.hbs' },
...super.PARTS ...super.PARTS,
features: { template: 'systems/daggerheart/templates/sheets/items/ancestry/features.hbs' }
}; };
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
/**
* Edit an existing feature on the item
* @type {ApplicationClickAction}
*/
static async #editFeature(_event, button) {
const target = button.closest('.feature-item');
const feature = this.document.system[`${target.dataset.type}Feature`];
if (!feature || Object.keys(feature).length === 0) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing'));
return;
}
feature.sheet.render(true);
}
/**
* Remove a feature from the item.
* @type {ApplicationClickAction}
*/
static async #removeFeature(event, button) {
event.stopPropagation();
const target = button.closest('.feature-item');
const feature = this.document.system[`${target.dataset.type}Feature`];
if (feature) await feature.update({ 'system.subType': null });
await this.document.update({
'system.features': this.document.system.features.filter(x => x && x.uuid !== feature.uuid).map(x => x.uuid)
});
}
/* -------------------------------------------- */
/* Application Drag/Drop */
/* -------------------------------------------- */
/**
* On drop on the item.
* @param {DragEvent} event - The drag event
*/
async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await fromUuid(data.uuid);
if (item?.type === 'feature') {
const subType = event.target.closest('.primary-feature') ? 'primary' : 'secondary';
if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes[subType]) {
const error = subType === 'primary' ? 'featureNotPrimary' : 'featureNotSecondary';
ui.notifications.warn(game.i18n.localize(`DAGGERHEART.UI.Notifications.${error}`));
return;
}
await item.update({ 'system.subType': subType });
await this.document.update({
'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
});
}
}
} }

View file

@ -1,10 +1,10 @@
import DHBaseItemSheet from '../api/base-item.mjs'; import DHBaseItemSheet from '../api/base-item.mjs';
import ItemAttachmentSheet from '../api/item-attachment-sheet.mjs';
export default class ArmorSheet extends DHBaseItemSheet { export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
/**@inheritdoc */ /**@inheritdoc */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['armor'], classes: ['armor'],
dragDrop: [{ dragSelector: null, dropSelector: null }],
tagifyConfigs: [ tagifyConfigs: [
{ {
selector: '.features-input', selector: '.features-input',
@ -26,7 +26,12 @@ export default class ArmorSheet extends DHBaseItemSheet {
settings: { settings: {
template: 'systems/daggerheart/templates/sheets/items/armor/settings.hbs', template: 'systems/daggerheart/templates/sheets/items/armor/settings.hbs',
scrollable: ['.settings'] scrollable: ['.settings']
} },
effects: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs',
scrollable: ['.effects']
},
...super.PARTS
}; };
/**@inheritdoc */ /**@inheritdoc */
@ -35,7 +40,7 @@ export default class ArmorSheet extends DHBaseItemSheet {
switch (partId) { switch (partId) {
case 'settings': case 'settings':
context.features = this.document.system.features.map(x => x.value); context.features = this.document.system.armorFeatures.map(x => x.value);
break; break;
} }
@ -47,6 +52,6 @@ export default class ArmorSheet extends DHBaseItemSheet {
* @param {Array<Object>} selectedOptions - The currently selected tag objects. * @param {Array<Object>} selectedOptions - The currently selected tag objects.
*/ */
static async #onFeatureSelect(selectedOptions) { static async #onFeatureSelect(selectedOptions) {
await this.document.update({ 'system.features': selectedOptions.map(x => ({ value: x.value })) }); await this.document.update({ 'system.armorFeatures': selectedOptions.map(x => ({ value: x.value })) });
} }
} }

View file

@ -1,4 +1,5 @@
import DHBaseItemSheet from '../api/base-item.mjs'; import DHBaseItemSheet from '../api/base-item.mjs';
import Tagify from '@yaireo/tagify';
export default class BeastformSheet extends DHBaseItemSheet { export default class BeastformSheet extends DHBaseItemSheet {
/**@inheritdoc */ /**@inheritdoc */
@ -15,6 +16,7 @@ export default class BeastformSheet extends DHBaseItemSheet {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-features.hbs', template: 'systems/daggerheart/templates/sheets/global/tabs/tab-features.hbs',
scrollable: ['.features'] scrollable: ['.features']
}, },
advanced: { template: 'systems/daggerheart/templates/sheets/items/beastform/advanced.hbs' },
effects: { effects: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs', template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs',
scrollable: ['.effects'] scrollable: ['.effects']
@ -23,16 +25,77 @@ export default class BeastformSheet extends DHBaseItemSheet {
static TABS = { static TABS = {
primary: { primary: {
tabs: [{ id: 'settings' }, { id: 'features' }, { id: 'effects' }], tabs: [{ id: 'settings' }, { id: 'features' }, { id: 'advanced' }, { id: 'effects' }],
initial: 'settings', initial: 'settings',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }
}; };
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
const advantageOnInput = htmlElement.querySelector('.advantageon-input');
if (advantageOnInput) {
const tagifyElement = new Tagify(advantageOnInput, {
tagTextProp: 'name',
templates: {
tag(tagData) {
return `<tag
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>`;
}
}
});
tagifyElement.on('add', this.advantageOnAdd.bind(this));
tagifyElement.on('remove', this.advantageOnRemove.bind(this));
}
}
/**@inheritdoc */ /**@inheritdoc */
async _preparePartContext(partId, context) { async _preparePartContext(partId, context, options) {
await super._preparePartContext(partId, context); await super._preparePartContext(partId, context, options);
switch (partId) {
case 'settings':
context.advantageOn = JSON.stringify(
Object.keys(context.document.system.advantageOn).map(key => ({
value: key,
name: context.document.system.advantageOn[key].value
}))
);
break;
case 'effects':
context.effects.actives = context.effects.actives.map(effect => {
const data = effect.toObject();
data.id = effect.id;
if (effect.type === 'beastform') data.mandatory = true;
return data;
});
break;
}
return context; return context;
} }
async advantageOnAdd(event) {
await this.document.update({
[`system.advantageOn.${foundry.utils.randomID()}`]: { value: event.detail.data.value }
});
}
async advantageOnRemove(event) {
await this.document.update({
[`system.advantageOn.-=${event.detail.data.value}`]: null
});
}
} }

View file

@ -9,11 +9,7 @@ export default class ClassSheet extends DHBaseItemSheet {
position: { width: 700 }, position: { width: 700 },
actions: { actions: {
removeItemFromCollection: ClassSheet.#removeItemFromCollection, removeItemFromCollection: ClassSheet.#removeItemFromCollection,
removeSuggestedItem: ClassSheet.#removeSuggestedItem, removeSuggestedItem: ClassSheet.#removeSuggestedItem
viewDoc: ClassSheet.#viewDoc,
addFeature: this.addFeature,
editFeature: this.editFeature,
deleteFeature: this.deleteFeature
}, },
tagifyConfigs: [ tagifyConfigs: [
{ {
@ -46,13 +42,17 @@ export default class ClassSheet extends DHBaseItemSheet {
settings: { settings: {
template: 'systems/daggerheart/templates/sheets/items/class/settings.hbs', template: 'systems/daggerheart/templates/sheets/items/class/settings.hbs',
scrollable: ['.settings'] scrollable: ['.settings']
},
effects: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs',
scrollable: ['.effects']
} }
}; };
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
primary: { primary: {
tabs: [{ id: 'description' }, { id: 'features' }, { id: 'settings' }], tabs: [{ id: 'description' }, { id: 'features' }, { id: 'settings' }, { id: 'effects' }],
initial: 'description', initial: 'description',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }
@ -78,6 +78,7 @@ export default class ClassSheet extends DHBaseItemSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
async _onDrop(event) { async _onDrop(event) {
event.stopPropagation();
const data = TextEditor.getDragEventData(event); const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid); const item = await fromUuid(data.uuid);
const target = event.target.closest('fieldset.drop-section'); const target = event.target.closest('fieldset.drop-section');
@ -85,6 +86,28 @@ export default class ClassSheet extends DHBaseItemSheet {
await this.document.update({ await this.document.update({
'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid] 'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid]
}); });
} else if (item.type === 'feature') {
if (target.classList.contains('hope-feature')) {
if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.hope) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotHope'));
return;
}
await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.hope });
await this.document.update({
'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
});
} else if (target.classList.contains('class-feature')) {
if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.class) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotClass'));
return;
}
await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.class });
await this.document.update({
'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
});
}
} else if (item.type === 'weapon') { } else if (item.type === 'weapon') {
if (target.classList.contains('primary-weapon-section')) { if (target.classList.contains('primary-weapon-section')) {
if (!this.document.system.characterGuide.suggestedPrimaryWeapon && !item.system.secondary) if (!this.document.system.characterGuide.suggestedPrimaryWeapon && !item.system.secondary)
@ -144,7 +167,7 @@ export default class ClassSheet extends DHBaseItemSheet {
static async #removeItemFromCollection(_event, element) { static async #removeItemFromCollection(_event, element) {
const { uuid, target } = element.dataset; const { uuid, target } = element.dataset;
const prop = foundry.utils.getProperty(this.document.system, target); const prop = foundry.utils.getProperty(this.document.system, target);
await this.document.update({ [target]: prop.filter(i => i.uuid !== uuid) }); await this.document.update({ [`system.${target}`]: prop.filter(i => i.uuid !== uuid) });
} }
/** /**
@ -156,56 +179,4 @@ export default class ClassSheet extends DHBaseItemSheet {
const { target } = element.dataset; const { target } = element.dataset;
await this.document.update({ [`system.characterGuide.${target}`]: null }); await this.document.update({ [`system.characterGuide.${target}`]: null });
} }
/**
* Open the sheet of a item by UUID.
* @param {PointerEvent} _event -
* @param {HTMLElement} button
*/
static async #viewDoc(_event, button) {
const doc = await fromUuid(button.dataset.uuid);
doc.sheet.render({ force: true });
}
getActionPath(type) {
return type === 'hope' ? 'hopeFeatures' : 'classFeatures';
}
static async addFeature(_, target) {
const actionPath = this.getActionPath(target.dataset.type);
const feature = await game.items.documentClass.create({
type: 'feature',
name: game.i18n.format('DOCUMENT.New', { type: game.i18n.localize('TYPES.Item.feature') })
});
await this.document.update({
[`system.${actionPath}`]: [
...this.document.system[actionPath].filter(x => x).map(x => x.uuid),
feature.uuid
]
});
}
static async editFeature(_, button) {
const target = button.closest('.feature-item');
const actionPath = this.getActionPath(button.dataset.type);
const feature = this.document.system[actionPath].find(x => x?.id === target.dataset.featureId);
if (!feature) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.notifications.featureIsMissing'));
return;
}
feature.sheet.render(true);
}
static async deleteFeature(event, button) {
event.stopPropagation();
const target = button.closest('.feature-item');
const actionPath = this.getActionPath(button.dataset.type);
await this.document.update({
[`system.${actionPath}`]: this.document.system[actionPath]
.filter(feature => feature && feature.id !== target.dataset.featureId)
.map(x => x.uuid)
});
}
} }

View file

@ -9,6 +9,10 @@ export default class CommunitySheet extends DHHeritageSheet {
/**@inheritdoc */ /**@inheritdoc */
static PARTS = { static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/items/community/header.hbs' }, header: { template: 'systems/daggerheart/templates/sheets/items/community/header.hbs' },
...super.PARTS ...super.PARTS,
features: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-features.hbs',
scrollable: ['.feature']
}
}; };
} }

View file

@ -19,6 +19,10 @@ export default class ConsumableSheet extends DHBaseItemSheet {
settings: { settings: {
template: 'systems/daggerheart/templates/sheets/items/consumable/settings.hbs', template: 'systems/daggerheart/templates/sheets/items/consumable/settings.hbs',
scrollable: ['.settings'] scrollable: ['.settings']
},
effects: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs',
scrollable: ['.effects']
} }
}; };
} }

View file

@ -1,5 +1,3 @@
import { actionsTypes } from '../../../data/action/_module.mjs';
import DHActionConfig from '../../sheets-configs/action-config.mjs';
import DHBaseItemSheet from '../api/base-item.mjs'; import DHBaseItemSheet from '../api/base-item.mjs';
export default class FeatureSheet extends DHBaseItemSheet { export default class FeatureSheet extends DHBaseItemSheet {
@ -7,13 +5,7 @@ export default class FeatureSheet extends DHBaseItemSheet {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
id: 'daggerheart-feature', id: 'daggerheart-feature',
classes: ['feature'], classes: ['feature'],
position: { height: 600 }, actions: {}
window: { resizable: true },
actions: {
addAction: FeatureSheet.#addAction,
editAction: FeatureSheet.#editAction,
removeAction: FeatureSheet.#removeAction
}
}; };
/**@override */ /**@override */
@ -21,6 +13,7 @@ export default class FeatureSheet extends DHBaseItemSheet {
header: { template: 'systems/daggerheart/templates/sheets/items/feature/header.hbs' }, header: { template: 'systems/daggerheart/templates/sheets/items/feature/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' }, description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' },
settings: { template: 'systems/daggerheart/templates/sheets/items/feature/settings.hbs' },
actions: { actions: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs', template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs',
scrollable: ['.actions'] scrollable: ['.actions']
@ -34,97 +27,9 @@ export default class FeatureSheet extends DHBaseItemSheet {
/**@override */ /**@override */
static TABS = { static TABS = {
primary: { primary: {
tabs: [{ id: 'description' }, { id: 'actions' }, { id: 'effects' }], tabs: [{ id: 'description' }, { id: 'settings' }, { id: 'actions' }, { id: 'effects' }],
initial: 'description', initial: 'description',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }
}; };
/* -------------------------------------------- */
/**@inheritdoc */
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
return context;
}
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
/**
* Render a dialog prompting the user to select an action type.
*
* @returns {Promise<object>} An object containing the selected action type.
*/
static async selectActionType() {
const content = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/actionTypes/actionType.hbs',
{ types: CONFIG.DH.ACTIONS.actionTypes }
),
title = 'Select Action Type';
return foundry.applications.api.DialogV2.prompt({
window: { title },
content,
ok: {
label: title,
callback: (event, button, dialog) => button.form.elements.type.value
}
});
}
/**
* Add a new action to the item, prompting the user for its type.
* @param {PointerEvent} _event - The originating click event
* @param {HTMLElement} _button - The capturing HTML element which defines the [data-action="addAction"]
*/
static async #addAction(_event, _button) {
const actionType = await FeatureSheet.selectActionType();
if (!actionType) return;
try {
const cls = actionsTypes[actionType] ?? actionsTypes.attack,
action = new cls(
{
_id: foundry.utils.randomID(),
type: actionType,
name: game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[actionType].name),
...cls.getSourceConfig(this.document)
},
{
parent: this.document
}
);
await this.document.update({ 'system.actions': [...this.document.system.actions, action] });
await new DHActionConfig(this.document.system.actions[this.document.system.actions.length - 1]).render({
force: true
});
} catch (error) {
console.log(error);
}
}
/**
* Edit an existing action on the item
* @param {PointerEvent} _event - The originating click event
* @param {HTMLElement} button - The capturing HTML element which defines the [data-action="editAction"]
*/
static async #editAction(_event, button) {
const action = this.document.system.actions[button.dataset.index];
await new DHActionConfig(action).render({ force: true });
}
/**
* Remove an action from the item.
* @param {PointerEvent} event - The originating click event
* @param {HTMLElement} button - The capturing HTML element which defines the [data-action="removeAction"]
*/
static async #removeAction(event, button) {
event.stopPropagation();
const actionIndex = button.closest('[data-index]').dataset.index;
await this.document.update({
'system.actions': this.document.system.actions.filter((_, index) => index !== Number.parseInt(actionIndex))
});
}
} }

View file

@ -19,6 +19,10 @@ export default class MiscellaneousSheet extends DHBaseItemSheet {
settings: { settings: {
template: 'systems/daggerheart/templates/sheets/items/miscellaneous/settings.hbs', template: 'systems/daggerheart/templates/sheets/items/miscellaneous/settings.hbs',
scrollable: ['.settings'] scrollable: ['.settings']
},
effects: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs',
scrollable: ['.effects']
} }
}; };
} }

View file

@ -6,11 +6,7 @@ export default class SubclassSheet extends DHBaseItemSheet {
classes: ['subclass'], classes: ['subclass'],
position: { width: 600 }, position: { width: 600 },
window: { resizable: false }, window: { resizable: false },
actions: { actions: {}
addFeature: this.addFeature,
editFeature: this.editFeature,
deleteFeature: this.deleteFeature
}
}; };
/**@override */ /**@override */
@ -25,53 +21,29 @@ export default class SubclassSheet extends DHBaseItemSheet {
settings: { settings: {
template: 'systems/daggerheart/templates/sheets/items/subclass/settings.hbs', template: 'systems/daggerheart/templates/sheets/items/subclass/settings.hbs',
scrollable: ['.settings'] scrollable: ['.settings']
},
effects: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs',
scrollable: ['.effects']
} }
}; };
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
primary: { primary: {
tabs: [{ id: 'description' }, { id: 'features' }, { id: 'settings' }], tabs: [{ id: 'description' }, { id: 'features' }, { id: 'settings' }, { id: 'effects' }],
initial: 'description', initial: 'description',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }
}; };
static async addFeature(_, target) {
const feature = await game.items.documentClass.create({
type: 'feature',
name: game.i18n.format('DOCUMENT.New', { type: game.i18n.localize('TYPES.Item.feature') })
});
await this.document.update({
[`system.${target.dataset.type}`]: feature.uuid
});
}
static async editFeature(_, button) {
const feature = this.document.system[button.dataset.type];
if (!feature) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.notifications.featureIsMissing'));
return;
}
feature.sheet.render(true);
}
static async deleteFeature(event, button) {
event.stopPropagation();
await this.document.update({
[`system.${button.dataset.type}`]: null
});
}
async _onDragStart(event) { async _onDragStart(event) {
const featureItem = event.currentTarget.closest('.drop-section'); const featureItem = event.currentTarget.closest('.drop-section');
if (featureItem) { if (featureItem) {
const feature = this.document.system[featureItem.dataset.type]; const feature = this.document.system[featureItem.dataset.type];
if (!feature) { if (!feature) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.notifications.featureIsMissing')); ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing'));
return; return;
} }
@ -82,18 +54,45 @@ export default class SubclassSheet extends DHBaseItemSheet {
} }
async _onDrop(event) { async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.fromInternal) return; if (data.fromInternal) return;
const item = await fromUuid(data.uuid); const item = await fromUuid(data.uuid);
if (item?.type === 'feature') { const target = event.target.closest('fieldset.drop-section');
const dropSection = event.target.closest('.drop-section'); if (item.type === 'feature') {
if (this.document.system[dropSection.dataset.type]) { if (target.dataset.type === 'foundation') {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.notifications.featureIsFull')); if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.foundation) {
return; ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotFoundation'));
} return;
}
await this.document.update({ [`system.${dropSection.dataset.type}`]: item.uuid }); await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.foundation });
await this.document.update({
'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
});
} else if (target.dataset.type === 'specialization') {
if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.specialization) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotSpecialization'));
return;
}
await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.specialization });
await this.document.update({
'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
});
} else if (target.dataset.type === 'mastery') {
if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.mastery) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotMastery'));
return;
}
await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.mastery });
await this.document.update({
'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
});
}
} }
} }
} }

View file

@ -1,6 +1,7 @@
import DHBaseItemSheet from '../api/base-item.mjs'; import DHBaseItemSheet from '../api/base-item.mjs';
import ItemAttachmentSheet from '../api/item-attachment-sheet.mjs';
export default class WeaponSheet extends DHBaseItemSheet { export default class WeaponSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
/**@inheritdoc */ /**@inheritdoc */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['weapon'], classes: ['weapon'],
@ -25,15 +26,20 @@ export default class WeaponSheet extends DHBaseItemSheet {
settings: { settings: {
template: 'systems/daggerheart/templates/sheets/items/weapon/settings.hbs', template: 'systems/daggerheart/templates/sheets/items/weapon/settings.hbs',
scrollable: ['.settings'] scrollable: ['.settings']
} },
effects: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs',
scrollable: ['.effects']
},
...super.PARTS
}; };
/**@inheritdoc */ /**@inheritdoc */
async _preparePartContext(partId, context) { async _preparePartContext(partId, context) {
super._preparePartContext(partId, context); await super._preparePartContext(partId, context);
switch (partId) { switch (partId) {
case 'settings': case 'settings':
context.features = this.document.system.features.map(x => x.value); context.features = this.document.system.weaponFeatures.map(x => x.value);
context.systemFields.attack.fields = this.document.system.attack.schema.fields; context.systemFields.attack.fields = this.document.system.attack.schema.fields;
break; break;
} }
@ -45,6 +51,6 @@ export default class WeaponSheet extends DHBaseItemSheet {
* @param {Array<Object>} selectedOptions - The currently selected tag objects. * @param {Array<Object>} selectedOptions - The currently selected tag objects.
*/ */
static async #onFeatureSelect(selectedOptions) { static async #onFeatureSelect(selectedOptions) {
await this.document.update({ 'system.features': selectedOptions.map(x => ({ value: x.value })) }); await this.document.update({ 'system.weaponFeatures': selectedOptions.map(x => ({ value: x.value })) });
} }
} }

View file

@ -2,3 +2,4 @@ export { default as DhChatLog } from './chatLog.mjs';
export { default as DhCombatTracker } from './combatTracker.mjs'; export { default as DhCombatTracker } from './combatTracker.mjs';
export * as DhCountdowns from './countdowns.mjs'; export * as DhCountdowns from './countdowns.mjs';
export { default as DhFearTracker } from './fearTracker.mjs'; export { default as DhFearTracker } from './fearTracker.mjs';
export { default as DhHotbar } from './hotbar.mjs';

View file

@ -1,6 +1,6 @@
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog { export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
constructor() { constructor(options) {
super(); super(options);
this.targetTemplate = { this.targetTemplate = {
activeLayer: undefined, activeLayer: undefined,
@ -83,15 +83,15 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
actor.system.attack?._id === actionId actor.system.attack?._id === actionId
? actor.system.attack ? actor.system.attack
: item.system.attack?._id === actionId : item.system.attack?._id === actionId
? item.system.attack ? item.system.attack
: item?.system?.actions?.find(a => a._id === actionId); : item?.system?.actions?.find(a => a._id === actionId);
return action; return action;
} }
onRollDamage = async (event, message) => { onRollDamage = async (event, message) => {
event.stopPropagation(); event.stopPropagation();
const actor = await this.getActor(message.system.source.actor); const actor = await this.getActor(message.system.source.actor);
if (!actor || !game.user.isGM) return true; if (game.user.character?.id !== actor.id && !game.user.isGM) return true;
if (message.system.source.item && message.system.source.action) { if (message.system.source.item && message.system.source.action) {
const action = this.getAction(actor, message.system.source.item, message.system.source.action); const action = this.getAction(actor, message.system.source.item, message.system.source.action);
if (!action || !action?.rollDamage) return; if (!action || !action?.rollDamage) return;
@ -190,7 +190,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.attackTargetDoesNotExist')); ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.attackTargetDoesNotExist'));
return; return;
} }
game.canvas.pan(token); game.canvas.pan(token);
}; };
@ -212,13 +211,25 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
} }
if (targets.length === 0) if (targets.length === 0)
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected')); return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected'));
for (let target of targets) {
let damage = message.system.roll.total;
if (message.system.onSave && message.system.targets.find(t => t.id === target.id)?.saved?.success === true)
damage = Math.ceil(damage * (CONFIG.DH.ACTIONS.damageOnSave[message.system.onSave]?.mod ?? 1));
await target.actor.takeDamage(damage, message.system.roll.type); for (let target of targets) {
let damages = foundry.utils.deepClone(message.system.damage?.roll ?? message.system.roll);
if (
message.system.onSave &&
message.system.targets.find(t => t.id === target.id)?.saved?.success === true
) {
const mod = CONFIG.DH.ACTIONS.damageOnSave[message.system.onSave]?.mod ?? 1;
Object.entries(damages).forEach(([k, v]) => {
v.total = 0;
v.parts.forEach(part => {
part.total = Math.ceil(part.total * mod);
v.total += part.total;
});
});
}
target.actor.takeDamage(damages);
} }
}; };
@ -227,10 +238,10 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
const targets = Array.from(game.user.targets); const targets = Array.from(game.user.targets);
if (targets.length === 0) if (targets.length === 0)
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected')); return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected'));
for (var target of targets) { for (var target of targets) {
await target.actor.takeHealing([{ value: message.system.roll.total, type: message.system.roll.type }]); target.actor.takeHealing(message.system.roll);
} }
}; };
@ -289,53 +300,54 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
await actor.useAction(action); await actor.useAction(action);
}; };
actionUseButton = async (_, message) => { actionUseButton = async (event, message) => {
const { moveIndex, actionIndex } = event.currentTarget.dataset;
const parent = await foundry.utils.fromUuid(message.system.actor); const parent = await foundry.utils.fromUuid(message.system.actor);
const actionType = Object.values(message.system.moves)[0].actions[0]; const actionType = message.system.moves[moveIndex].actions[actionIndex];
const cls = CONFIG.DH.ACTIONS.actionTypes[actionType.type]; const cls = game.system.api.models.actions.actionsTypes[actionType.type];
const action = new cls( const action = new cls(
{ ...actionType, _id: foundry.utils.randomID(), name: game.i18n.localize(actionType.name) }, { ...actionType, _id: foundry.utils.randomID(), name: game.i18n.localize(actionType.name) },
{ parent: parent } { parent: parent.system }
); );
action.use(); action.use(event);
}; };
//Reroll Functionality //Reroll Functionality
rerollEvent = async(event,message)=> { rerollEvent = async (event, message) => {
let DieTerm=foundry.dice.terms.Die; let DieTerm = foundry.dice.terms.Die;
let dicetype = event.target.value; let dicetype = event.target.value;
let originalRoll_parsed=message.rolls.map(roll => JSON.parse(roll))[0]; let originalRoll_parsed = message.rolls.map(roll => JSON.parse(roll))[0];
console.log("Parsed Map:",originalRoll_parsed); console.log('Parsed Map:', originalRoll_parsed);
let originalRoll=Roll.fromData(originalRoll_parsed); let originalRoll = Roll.fromData(originalRoll_parsed);
let diceIndex; let diceIndex;
console.log("Dice to reroll is:",dicetype,", and the message id is:",message._id,originalRoll_parsed); console.log('Dice to reroll is:', dicetype, ', and the message id is:', message._id, originalRoll_parsed);
console.log("Original Roll Terms:",originalRoll.terms); console.log('Original Roll Terms:', originalRoll.terms);
switch(dicetype){ switch (dicetype) {
case "hope": { case 'hope': {
diceIndex=0; //Hope Die diceIndex = 0; //Hope Die
break; break;
}; }
case "fear" :{ case 'fear': {
diceIndex=2; //Fear Die diceIndex = 2; //Fear Die
break; break;
}; }
default: default:
ui.notifications.warn("Invalid Dice type selected."); ui.notifications.warn('Invalid Dice type selected.');
break; break;
} }
let rollClone=originalRoll.clone(); let rollClone = originalRoll.clone();
let rerolledTerm=originalRoll.terms[diceIndex]; let rerolledTerm = originalRoll.terms[diceIndex];
console.log("originalRoll:",originalRoll,"rerolledTerm",rerolledTerm); console.log('originalRoll:', originalRoll, 'rerolledTerm', rerolledTerm);
if (!(rerolledTerm instanceof DieTerm)) { if (!(rerolledTerm instanceof DieTerm)) {
ui.notifications.error("Selected term is not a die."); ui.notifications.error('Selected term is not a die.');
return; return;
} }
await rollClone.reroll({allowStrings:true})[diceIndex]; await rollClone.reroll({ allowStrings: true })[diceIndex];
console.log(rollClone); console.log(rollClone);
await rollClone.evaluate({allowStrings:true}); await rollClone.evaluate({ allowStrings: true });
console.log(rollClone.result); console.log(rollClone.result);
/* /*
const confirm = await foundry.applications.api.DialogV2.confirm({ const confirm = await foundry.applications.api.DialogV2.confirm({
window: { title: 'Confirm Reroll' }, window: { title: 'Confirm Reroll' },
content: `<p>You have rerolled your <strong>${dicetype}</strong> die to <strong>${rollClone.result}</strong>.</p><p>Apply this new roll?</p>` content: `<p>You have rerolled your <strong>${dicetype}</strong> die to <strong>${rollClone.result}</strong>.</p><p>Apply this new roll?</p>`
@ -343,5 +355,5 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
if (!confirm) return; if (!confirm) return;
rollClone.toMessage({flavor: 'Selective reroll applied for ${dicetype}.'}); rollClone.toMessage({flavor: 'Selective reroll applied for ${dicetype}.'});
console.log("Updated Roll",rollClone);*/ console.log("Updated Roll",rollClone);*/
} };
} }

View file

@ -66,6 +66,11 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
} }
async setCombatantSpotlight(combatantId) { async setCombatantSpotlight(combatantId) {
const update = {
system: {
'spotlight.requesting': false
}
};
const combatant = this.viewed.combatants.get(combatantId); const combatant = this.viewed.combatants.get(combatantId);
const toggleTurn = this.viewed.combatants.contents const toggleTurn = this.viewed.combatants.contents
@ -73,10 +78,18 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
.map(x => x.id) .map(x => x.id)
.indexOf(combatantId); .indexOf(combatantId);
if (this.viewed.turn !== toggleTurn) Hooks.callAll(CONFIG.DH.HOOKS.spotlight, {}); if (this.viewed.turn !== toggleTurn) {
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
await updateCountdowns(CONFIG.DH.GENERAL.countdownTypes.spotlight.id);
const autoPoints = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).actionPoints;
if (autoPoints) {
update.system.actionTokens = Math.max(combatant.system.actionTokens - 1, 0);
}
}
await this.viewed.update({ turn: this.viewed.turn === toggleTurn ? null : toggleTurn }); await this.viewed.update({ turn: this.viewed.turn === toggleTurn ? null : toggleTurn });
await combatant.update({ 'system.spotlight.requesting': false }); await combatant.update(update);
} }
static async requestSpotlight(_, target) { static async requestSpotlight(_, target) {

View file

@ -1,4 +1,3 @@
import { countdownTypes } from '../../config/generalConfig.mjs';
import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import constructHTMLButton from '../../helpers/utils.mjs'; import constructHTMLButton from '../../helpers/utils.mjs';
import OwnershipSelection from '../dialogs/ownershipSelection.mjs'; import OwnershipSelection from '../dialogs/ownershipSelection.mjs';
@ -328,43 +327,29 @@ export class EncounterCountdowns extends Countdowns {
}; };
} }
export const registerCountdownApplicationHooks = () => { export async function updateCountdowns(progressType) {
const updateCountdowns = async shouldProgress => { const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
if (game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).countdowns) { const update = Object.keys(countdownSetting).reduce((update, typeKey) => {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); return foundry.utils.mergeObject(
for (let countdownCategoryKey in countdownSetting) { update,
const countdownCategory = countdownSetting[countdownCategoryKey]; Object.keys(countdownSetting[typeKey].countdowns).reduce((acc, countdownKey) => {
for (let countdownKey in countdownCategory.countdowns) { const countdown = countdownSetting[typeKey].countdowns[countdownKey];
const countdown = countdownCategory.countdowns[countdownKey]; if (countdown.progress.current > 0 && countdown.progress.type.value === progressType) {
acc[`${typeKey}.countdowns.${countdownKey}.progress.current`] = countdown.progress.current - 1;
if (shouldProgress(countdown)) {
await countdownSetting.updateSource({
[`${countdownCategoryKey}.countdowns.${countdownKey}.progress.current`]:
countdown.progress.current - 1
});
await game.settings.set(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Countdowns,
countdownSetting
);
foundry.applications.instances.get(`${countdownCategoryKey}-countdowns`)?.render();
}
} }
}
}
};
Hooks.on(CONFIG.DH.HOOKS.characterAttack, async () => { return acc;
updateCountdowns(countdown => { }, {})
return ( );
countdown.progress.type.value === countdownTypes.characterAttack.id && countdown.progress.current > 0 }, {});
);
});
});
Hooks.on(CONFIG.DH.HOOKS.spotlight, async () => { await countdownSetting.updateSource(update);
updateCountdowns(countdown => { await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSetting);
return countdown.progress.type.value === countdownTypes.spotlight.id && countdown.progress.current > 0;
}); const data = { refreshType: RefreshType.Countdown };
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data
}); });
}; Hooks.callAll(socketEvent.Refresh, data);
}

View file

@ -0,0 +1,129 @@
export default class DhHotbar extends foundry.applications.ui.Hotbar {
constructor(options) {
super(options);
this.setupHooks();
}
static async useItem(uuid) {
const item = await fromUuid(uuid);
if (!item) {
return ui.notifications.warn('WARNING.ObjectDoesNotExist', {
format: {
name: game.i18n.localize('Document'),
identifier: uuid
}
});
}
await item.use({});
}
static async useAction(itemUuid, actionId) {
const item = await foundry.utils.fromUuid(itemUuid);
if (!item) {
return ui.notifications.warn('WARNING.ObjectDoesNotExist', {
format: {
name: game.i18n.localize('Document'),
identifier: itemUuid
}
});
}
const action = item.system.actions.find(x => x.id === actionId);
if (!action) {
return ui.notifications.warn('DAGGERHEART.UI.Notifications.actionIsMissing');
}
await action.use({});
}
static async useAttack(actorUuid) {
const actor = await foundry.utils.fromUuid(actorUuid);
if (!actor) {
return ui.notifications.warn('WARNING.ObjectDoesNotExist', {
format: {
name: game.i18n.localize('Document'),
identifier: actorUuid
}
});
}
const attack = actor.system.attack;
if (!attack) {
return ui.notifications.warn('DAGGERHEART.UI.Notifications.attackIsMissing');
}
await attack.use({});
}
setupHooks() {
Hooks.on('hotbarDrop', (bar, data, slot) => {
if (data.type === 'Item') {
const item = foundry.utils.fromUuidSync(data.uuid);
if (item.uuid.startsWith('Compendium') || !item.isOwned || !item.isOwner) return true;
switch (item.type) {
case 'ancestry':
case 'community':
case 'class':
case 'subclass':
return true;
default:
this.createItemMacro(item, slot);
return false;
}
} else if (data.type === 'Action') {
const item = foundry.utils.fromUuidSync(data.data.itemUuid);
if (item.uuid.startsWith('Compendium')) return true;
if (!item.isOwned || !item.isOwner) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.unownedActionMacro'));
return false;
}
this.createActionMacro(data, slot);
return false;
} else if (data.type === 'Attack') {
const actor = foundry.utils.fromUuidSync(data.actorUuid);
if (actor.uuid.startsWith('Compendium')) return true;
if (!actor.isOwner) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.unownedAttackMacro'));
return false;
}
this.createAttackMacro(data, slot);
return false;
}
});
}
async createItemMacro(data, slot) {
const macro = await Macro.implementation.create({
name: data.name,
type: CONST.MACRO_TYPES.SCRIPT,
img: data.img,
command: `await game.system.api.applications.ui.DhHotbar.useItem("${data.uuid}");`
});
await game.user.assignHotbarMacro(macro, slot);
}
async createActionMacro(data, slot) {
const macro = await Macro.implementation.create({
name: data.data.name,
type: CONST.MACRO_TYPES.SCRIPT,
img: data.data.img,
command: `await game.system.api.applications.ui.DhHotbar.useAction("${data.data.itemUuid}", "${data.data.id}");`
});
await game.user.assignHotbarMacro(macro, slot);
}
async createAttackMacro(data, slot) {
const macro = await Macro.implementation.create({
name: data.name,
type: CONST.MACRO_TYPES.SCRIPT,
img: data.img,
command: `await game.system.api.applications.ui.DhHotbar.useAttack("${data.actorUuid}");`
});
await game.user.assignHotbarMacro(macro, slot);
}
}

View file

@ -100,7 +100,7 @@ export default class DHContextMenu extends foundry.applications.ux.ContextMenu {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
const { clientX, clientY } = event; const { clientX, clientY } = event;
const selector = '[data-item-id]'; const selector = '[data-item-uuid]';
const target = event.target.closest(selector) ?? event.currentTarget.closest(selector); const target = event.target.closest(selector) ?? event.currentTarget.closest(selector);
target?.dispatchEvent( target?.dispatchEvent(
new PointerEvent('contextmenu', { new PointerEvent('contextmenu', {

View file

@ -1,3 +1,5 @@
export { default as DhMeasuredTemplate } from './measuredTemplate.mjs'; export { default as DhMeasuredTemplate } from './measuredTemplate.mjs';
export { default as DhRuler } from './ruler.mjs'; export { default as DhRuler } from './ruler.mjs';
export { default as DhTemplateLayer } from './templateLayer.mjs';
export { default as DhTokenPlaceable } from './token.mjs';
export { default as DhTokenRuler } from './tokenRuler.mjs'; export { default as DhTokenRuler } from './tokenRuler.mjs';

View file

@ -0,0 +1,116 @@
export default class DhTemplateLayer extends foundry.canvas.layers.TemplateLayer {
static prepareSceneControls() {
const sc = foundry.applications.ui.SceneControls;
return {
name: 'templates',
order: 2,
title: 'CONTROLS.GroupMeasure',
icon: 'fa-solid fa-ruler-combined',
visible: game.user.can('TEMPLATE_CREATE'),
onChange: (event, active) => {
if (active) canvas.templates.activate();
},
onToolChange: () => canvas.templates.setAllRenderFlags({ refreshState: true }),
tools: {
circle: {
name: 'circle',
order: 1,
title: 'CONTROLS.MeasureCircle',
icon: 'fa-regular fa-circle',
toolclip: {
src: 'toolclips/tools/measure-circle.webm',
heading: 'CONTROLS.MeasureCircle',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete'])
}
},
cone: {
name: 'cone',
order: 2,
title: 'CONTROLS.MeasureCone',
icon: 'fa-solid fa-angle-left',
toolclip: {
src: 'toolclips/tools/measure-cone.webm',
heading: 'CONTROLS.MeasureCone',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
inFront: {
name: 'inFront',
order: 3,
title: 'CONTROLS.inFront',
icon: 'fa-solid fa-eye',
toolclip: {
src: 'toolclips/tools/measure-cone.webm',
heading: 'CONTROLS.inFront',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
rect: {
name: 'rect',
order: 4,
title: 'CONTROLS.MeasureRect',
icon: 'fa-regular fa-square',
toolclip: {
src: 'toolclips/tools/measure-rect.webm',
heading: 'CONTROLS.MeasureRect',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
ray: {
name: 'ray',
order: 5,
title: 'CONTROLS.MeasureRay',
icon: 'fa-solid fa-up-down',
toolclip: {
src: 'toolclips/tools/measure-ray.webm',
heading: 'CONTROLS.MeasureRay',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
clear: {
name: 'clear',
order: 6,
title: 'CONTROLS.MeasureClear',
icon: 'fa-solid fa-trash',
visible: game.user.isGM,
onChange: () => canvas.templates.deleteAll(),
button: true
}
},
activeTool: 'circle'
};
}
_onDragLeftStart(event) {
const interaction = event.interactionData;
// Snap the origin to the grid
if (!event.shiftKey) interaction.origin = this.getSnappedPoint(interaction.origin);
// Create a pending MeasuredTemplateDocument
const tool = game.activeTool === 'inFront' ? 'cone' : game.activeTool;
const previewData = {
user: game.user.id,
t: tool,
x: interaction.origin.x,
y: interaction.origin.y,
sort: Math.max(this.getMaxSort() + 1, 0),
distance: 1,
direction: 0,
fillColor: game.user.color || '#FF0000',
hidden: event.altKey
};
const defaults = CONFIG.MeasuredTemplate.defaults;
if (game.activeTool === 'cone') previewData.angle = defaults.angle;
else if (game.activeTool === 'inFront') previewData.angle = 180;
else if (game.activeTool === 'ray') previewData.width = defaults.width * canvas.dimensions.distance;
const cls = foundry.utils.getDocumentClass('MeasuredTemplate');
const doc = new cls(previewData, { parent: canvas.scene });
// Create a preview MeasuredTemplate object
const template = new this.constructor.placeableClass(doc);
doc._object = template;
interaction.preview = this.preview.addChild(template);
template.draw();
}
}

View file

@ -0,0 +1,36 @@
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
/** @inheritDoc */
async _drawEffects() {
this.effects.renderable = false;
// Clear Effects Container
this.effects.removeChildren().forEach(c => c.destroy());
this.effects.bg = this.effects.addChild(new PIXI.Graphics());
this.effects.bg.zIndex = -1;
this.effects.overlay = null;
// Categorize effects
const activeEffects = this.actor ? Array.from(this.actor.effects).filter(x => !x.disabled) : [];
const overlayEffect = activeEffects.findLast(e => e.img && e.getFlag('core', 'overlay'));
// Draw effects
const promises = [];
for (const [i, effect] of activeEffects.entries()) {
if (!effect.img) continue;
const promise =
effect === overlayEffect
? this._drawOverlay(effect.img, effect.tint)
: this._drawEffect(effect.img, effect.tint);
promises.push(
promise.then(e => {
if (e) e.zIndex = i;
})
);
}
await Promise.allSettled(promises);
this.effects.sortChildren();
this.effects.renderable = true;
this.renderFlags.set({ refreshEffects: true });
}
}

View file

@ -4,7 +4,6 @@ export * as domainConfig from './domainConfig.mjs';
export * as effectConfig from './effectConfig.mjs'; export * as effectConfig from './effectConfig.mjs';
export * as flagsConfig from './flagsConfig.mjs'; export * as flagsConfig from './flagsConfig.mjs';
export * as generalConfig from './generalConfig.mjs'; export * as generalConfig from './generalConfig.mjs';
export * as hooksConfig from './hooksConfig.mjs';
export * as itemConfig from './itemConfig.mjs'; export * as itemConfig from './itemConfig.mjs';
export * as settingsConfig from './settingsConfig.mjs'; export * as settingsConfig from './settingsConfig.mjs';
export * as systemConfig from './system.mjs'; export * as systemConfig from './system.mjs';

View file

@ -1,5 +1,6 @@
export const abilities = { export const abilities = {
agility: { agility: {
id: 'agility',
label: 'DAGGERHEART.CONFIG.Traits.agility.name', label: 'DAGGERHEART.CONFIG.Traits.agility.name',
verbs: [ verbs: [
'DAGGERHEART.CONFIG.Traits.agility.verb.sprint', 'DAGGERHEART.CONFIG.Traits.agility.verb.sprint',
@ -8,6 +9,7 @@ export const abilities = {
] ]
}, },
strength: { strength: {
id: 'strength',
label: 'DAGGERHEART.CONFIG.Traits.strength.name', label: 'DAGGERHEART.CONFIG.Traits.strength.name',
verbs: [ verbs: [
'DAGGERHEART.CONFIG.Traits.strength.verb.lift', 'DAGGERHEART.CONFIG.Traits.strength.verb.lift',
@ -16,6 +18,7 @@ export const abilities = {
] ]
}, },
finesse: { finesse: {
id: 'finesse',
label: 'DAGGERHEART.CONFIG.Traits.finesse.name', label: 'DAGGERHEART.CONFIG.Traits.finesse.name',
verbs: [ verbs: [
'DAGGERHEART.CONFIG.Traits.finesse.verb.control', 'DAGGERHEART.CONFIG.Traits.finesse.verb.control',
@ -24,6 +27,7 @@ export const abilities = {
] ]
}, },
instinct: { instinct: {
id: 'instinct',
label: 'DAGGERHEART.CONFIG.Traits.instinct.name', label: 'DAGGERHEART.CONFIG.Traits.instinct.name',
verbs: [ verbs: [
'DAGGERHEART.CONFIG.Traits.instinct.verb.perceive', 'DAGGERHEART.CONFIG.Traits.instinct.verb.perceive',
@ -32,6 +36,7 @@ export const abilities = {
] ]
}, },
presence: { presence: {
id: 'presence',
label: 'DAGGERHEART.CONFIG.Traits.presence.name', label: 'DAGGERHEART.CONFIG.Traits.presence.name',
verbs: [ verbs: [
'DAGGERHEART.CONFIG.Traits.presence.verb.charm', 'DAGGERHEART.CONFIG.Traits.presence.verb.charm',
@ -40,6 +45,7 @@ export const abilities = {
] ]
}, },
knowledge: { knowledge: {
id: 'knowledge',
label: 'DAGGERHEART.CONFIG.Traits.knowledge.name', label: 'DAGGERHEART.CONFIG.Traits.knowledge.name',
verbs: [ verbs: [
'DAGGERHEART.CONFIG.Traits.knowledge.verb.recall', 'DAGGERHEART.CONFIG.Traits.knowledge.verb.recall',
@ -113,7 +119,7 @@ export const adversaryTypes = {
}, },
social: { social: {
id: 'social', id: 'social',
label: 'DAGGERHEART.CONFIG.AdversaryTypee.social.label', label: 'DAGGERHEART.CONFIG.AdversaryType.social.label',
description: 'DAGGERHEART.ACTORS.Adversary.social.description' description: 'DAGGERHEART.ACTORS.Adversary.social.description'
}, },
solo: { solo: {
@ -411,7 +417,7 @@ export const levelupData = {
}; };
export const subclassFeatureLabels = { export const subclassFeatureLabels = {
1: 'DAGGERHEART.ITEMS.DomainCard.foundation', 1: 'DAGGERHEART.ITEMS.DomainCard.foundationTitle',
2: 'DAGGERHEART.ITEMS.DomainCard.specializationTitle', 2: 'DAGGERHEART.ITEMS.DomainCard.specializationTitle',
3: 'DAGGERHEART.ITEMS.DomainCard.masteryTitle' 3: 'DAGGERHEART.ITEMS.DomainCard.masteryTitle'
}; };

View file

@ -3,55 +3,55 @@ export const domains = {
id: 'arcana', id: 'arcana',
label: 'DAGGERHEART.GENERAL.Domain.arcana.label', label: 'DAGGERHEART.GENERAL.Domain.arcana.label',
src: 'systems/daggerheart/assets/icons/domains/arcana.svg', src: 'systems/daggerheart/assets/icons/domains/arcana.svg',
description: 'DAGGERHEART.GENERAL.Domain.Arcana' description: 'DAGGERHEART.GENERAL.Domain.arcana.description'
}, },
blade: { blade: {
id: 'blade', id: 'blade',
label: 'DAGGERHEART.GENERAL.Domain.blade.label', label: 'DAGGERHEART.GENERAL.Domain.blade.label',
src: 'systems/daggerheart/assets/icons/domains/blade.svg', src: 'systems/daggerheart/assets/icons/domains/blade.svg',
description: 'DAGGERHEART.GENERAL.Domain.Blade' description: 'DAGGERHEART.GENERAL.Domain.blade.description'
}, },
bone: { bone: {
id: 'bone', id: 'bone',
label: 'DAGGERHEART.GENERAL.Domain.bone.label', label: 'DAGGERHEART.GENERAL.Domain.bone.label',
src: 'systems/daggerheart/assets/icons/domains/bone.svg', src: 'systems/daggerheart/assets/icons/domains/bone.svg',
description: 'DAGGERHEART.GENERAL.Domain.Bone' description: 'DAGGERHEART.GENERAL.Domain.bone.description'
}, },
codex: { codex: {
id: 'codex', id: 'codex',
label: 'DAGGERHEART.GENERAL.Domain.codex.label', label: 'DAGGERHEART.GENERAL.Domain.codex.label',
src: 'systems/daggerheart/assets/icons/domains/codex.svg', src: 'systems/daggerheart/assets/icons/domains/codex.svg',
description: 'DAGGERHEART.GENERAL.Domain.Codex' description: 'DAGGERHEART.GENERAL.Domain.codex.description'
}, },
grace: { grace: {
id: 'grace', id: 'grace',
label: 'DAGGERHEART.GENERAL.Domain.grace.label', label: 'DAGGERHEART.GENERAL.Domain.grace.label',
src: 'systems/daggerheart/assets/icons/domains/grace.svg', src: 'systems/daggerheart/assets/icons/domains/grace.svg',
description: 'DAGGERHEART.GENERAL.Domain.Grace' description: 'DAGGERHEART.GENERAL.Domain.grace.description'
}, },
midnight: { midnight: {
id: 'midnight', id: 'midnight',
label: 'DAGGERHEART.GENERAL.Domain.midnight.label', label: 'DAGGERHEART.GENERAL.Domain.midnight.label',
src: 'systems/daggerheart/assets/icons/domains/midnight.svg', src: 'systems/daggerheart/assets/icons/domains/midnight.svg',
description: 'DAGGERHEART.GENERAL.Domain.Midnight' description: 'DAGGERHEART.GENERAL.Domain.midnight.description'
}, },
sage: { sage: {
id: 'sage', id: 'sage',
label: 'DAGGERHEART.GENERAL.Domain.sage.label', label: 'DAGGERHEART.GENERAL.Domain.sage.label',
src: 'systems/daggerheart/assets/icons/domains/sage.svg', src: 'systems/daggerheart/assets/icons/domains/sage.svg',
description: 'DAGGERHEART.GENERAL.Domain.Sage' description: 'DAGGERHEART.GENERAL.Domain.sage.description'
}, },
splendor: { splendor: {
id: 'splendor', id: 'splendor',
label: 'DAGGERHEART.GENERAL.Domain.splendor.label', label: 'DAGGERHEART.GENERAL.Domain.splendor.label',
src: 'systems/daggerheart/assets/icons/domains/splendor.svg', src: 'systems/daggerheart/assets/icons/domains/splendor.svg',
description: 'DAGGERHEART.GENERAL.Domain.Splendor' description: 'DAGGERHEART.GENERAL.Domain.splendor.description'
}, },
valor: { valor: {
id: 'valor', id: 'valor',
label: 'DAGGERHEART.GENERAL.Domain.valor.label', label: 'DAGGERHEART.GENERAL.Domain.valor.label',
src: 'systems/daggerheart/assets/icons/domains/valor.svg', src: 'systems/daggerheart/assets/icons/domains/valor.svg',
description: 'DAGGERHEART.GENERAL.Domain.Valor' description: 'DAGGERHEART.GENERAL.Domain.valor.description'
} }
}; };

View file

@ -7,3 +7,5 @@ export const encounterCountdown = {
simple: 'countdown-encounter-simple', simple: 'countdown-encounter-simple',
position: 'countdown-encounter-position' position: 'countdown-encounter-position'
}; };
export const itemAttachmentSource = 'attachmentSource';

View file

@ -59,13 +59,13 @@ export const damageTypes = {
id: 'physical', id: 'physical',
label: 'DAGGERHEART.CONFIG.DamageType.physical.name', label: 'DAGGERHEART.CONFIG.DamageType.physical.name',
abbreviation: 'DAGGERHEART.CONFIG.DamageType.physical.abbreviation', abbreviation: 'DAGGERHEART.CONFIG.DamageType.physical.abbreviation',
icon: ["fa-hand-fist"] icon: 'fa-hand-fist'
}, },
magical: { magical: {
id: 'magical', id: 'magical',
label: 'DAGGERHEART.CONFIG.DamageType.magical.name', label: 'DAGGERHEART.CONFIG.DamageType.magical.name',
abbreviation: 'DAGGERHEART.CONFIG.DamageType.magical.abbreviation', abbreviation: 'DAGGERHEART.CONFIG.DamageType.magical.abbreviation',
icon: ["fa-wand-sparkles"] icon: 'fa-wand-sparkles'
} }
}; };
@ -89,6 +89,11 @@ export const healingTypes = {
id: 'armorStack', id: 'armorStack',
label: 'DAGGERHEART.CONFIG.HealingType.armorStack.name', label: 'DAGGERHEART.CONFIG.HealingType.armorStack.name',
abbreviation: 'DAGGERHEART.CONFIG.HealingType.armorStack.abbreviation' abbreviation: 'DAGGERHEART.CONFIG.HealingType.armorStack.abbreviation'
},
fear: {
id: 'fear',
label: 'DAGGERHEART.CONFIG.HealingType.fear.name',
abbreviation: 'DAGGERHEART.CONFIG.HealingType.fear.abbreviation'
} }
}; };
@ -110,6 +115,18 @@ export const conditions = {
name: 'DAGGERHEART.CONFIG.Condition.restrained.name', name: 'DAGGERHEART.CONFIG.Condition.restrained.name',
icon: 'icons/magic/control/debuff-chains-shackle-movement-red.webp', icon: 'icons/magic/control/debuff-chains-shackle-movement-red.webp',
description: 'DAGGERHEART.CONFIG.Condition.restrained.description' description: 'DAGGERHEART.CONFIG.Condition.restrained.description'
},
unconcious: {
id: 'unconcious',
name: 'DAGGERHEART.CONFIG.Condition.unconcious.name',
icon: 'icons/magic/control/sleep-bubble-purple.webp',
description: 'DAGGERHEART.CONFIG.Condition.unconcious.description'
},
dead: {
id: 'dead',
name: 'DAGGERHEART.CONFIG.Condition.dead.name',
icon: 'icons/magic/death/grave-tombstone-glow-teal.webp',
description: 'DAGGERHEART.CONFIG.Condition.dead.description'
} }
}; };
@ -118,6 +135,7 @@ export const defaultRestOptions = {
tendToWounds: { tendToWounds: {
id: 'tendToWounds', id: 'tendToWounds',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.tendToWounds.name'), name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.tendToWounds.name'),
icon: 'fa-solid fa-bandage',
img: 'icons/magic/life/cross-worn-green.webp', img: 'icons/magic/life/cross-worn-green.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.tendToWounds.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.tendToWounds.description'),
actions: [ actions: [
@ -127,11 +145,11 @@ export const defaultRestOptions = {
img: 'icons/magic/life/cross-worn-green.webp', img: 'icons/magic/life/cross-worn-green.webp',
actionType: 'action', actionType: 'action',
healing: { healing: {
type: 'health', applyTo: healingTypes.hitPoints.id,
value: { value: {
custom: { custom: {
enabled: true, enabled: true,
formula: '1d4 + 1' // should be 1d4 + {tier}. How to use the roll param? formula: '1d4 + @tier'
} }
} }
} }
@ -141,6 +159,7 @@ export const defaultRestOptions = {
clearStress: { clearStress: {
id: 'clearStress', id: 'clearStress',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.clearStress.name'), name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.clearStress.name'),
icon: 'fa-regular fa-face-surprise',
img: 'icons/magic/perception/eye-ringed-green.webp', img: 'icons/magic/perception/eye-ringed-green.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.clearStress.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.clearStress.description'),
actions: [ actions: [
@ -150,11 +169,11 @@ export const defaultRestOptions = {
img: 'icons/magic/perception/eye-ringed-green.webp', img: 'icons/magic/perception/eye-ringed-green.webp',
actionType: 'action', actionType: 'action',
healing: { healing: {
type: 'stress', applyTo: healingTypes.stress.id,
value: { value: {
custom: { custom: {
enabled: true, enabled: true,
formula: '1d4 + 1' // should be 1d4 + {tier}. How to use the roll param? formula: '1d4 + @tier'
} }
} }
} }
@ -164,13 +183,31 @@ export const defaultRestOptions = {
repairArmor: { repairArmor: {
id: 'repairArmor', id: 'repairArmor',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.repairArmor.name'), name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.repairArmor.name'),
icon: 'fa-solid fa-hammer',
img: 'icons/skills/trades/smithing-anvil-silver-red.webp', img: 'icons/skills/trades/smithing-anvil-silver-red.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.repairArmor.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.repairArmor.description'),
actions: [] actions: [
{
type: 'healing',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.repairArmor.name'),
img: 'icons/skills/trades/smithing-anvil-silver-red.webp',
actionType: 'action',
healing: {
applyTo: healingTypes.armorStack.id,
value: {
custom: {
enabled: true,
formula: '1d4 + @tier'
}
}
}
}
]
}, },
prepare: { prepare: {
id: 'prepare', id: 'prepare',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.name'), name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.name'),
icon: 'fa-solid fa-dumbbell',
img: 'icons/skills/trades/academics-merchant-scribe.webp', img: 'icons/skills/trades/academics-merchant-scribe.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.description'),
actions: [] actions: []
@ -180,6 +217,7 @@ export const defaultRestOptions = {
tendToWounds: { tendToWounds: {
id: 'tendToWounds', id: 'tendToWounds',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.tendToWounds.name'), name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.tendToWounds.name'),
icon: 'fa-solid fa-bandage',
img: 'icons/magic/life/cross-worn-green.webp', img: 'icons/magic/life/cross-worn-green.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.tendToWounds.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.tendToWounds.description'),
actions: [] actions: []
@ -187,6 +225,7 @@ export const defaultRestOptions = {
clearStress: { clearStress: {
id: 'clearStress', id: 'clearStress',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.clearStress.name'), name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.clearStress.name'),
icon: 'fa-regular fa-face-surprise',
img: 'icons/magic/perception/eye-ringed-green.webp', img: 'icons/magic/perception/eye-ringed-green.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.clearStress.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.clearStress.description'),
actions: [] actions: []
@ -194,6 +233,7 @@ export const defaultRestOptions = {
repairArmor: { repairArmor: {
id: 'repairArmor', id: 'repairArmor',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.repairArmor.name'), name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.repairArmor.name'),
icon: 'fa-solid fa-hammer',
img: 'icons/skills/trades/smithing-anvil-silver-red.webp', img: 'icons/skills/trades/smithing-anvil-silver-red.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.repairArmor.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.repairArmor.description'),
actions: [] actions: []
@ -201,6 +241,7 @@ export const defaultRestOptions = {
prepare: { prepare: {
id: 'prepare', id: 'prepare',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.name'), name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.name'),
icon: 'fa-solid fa-dumbbell',
img: 'icons/skills/trades/academics-merchant-scribe.webp', img: 'icons/skills/trades/academics-merchant-scribe.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.description'),
actions: [] actions: []
@ -208,19 +249,12 @@ export const defaultRestOptions = {
workOnAProject: { workOnAProject: {
id: 'workOnAProject', id: 'workOnAProject',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.workOnAProject.name'), name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.workOnAProject.name'),
icon: 'fa-solid fa-diagram-project',
img: 'icons/skills/social/thumbsup-approval-like.webp', img: 'icons/skills/social/thumbsup-approval-like.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.workOnAProject.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.workOnAProject.description'),
actions: [] actions: []
} }
}), })
custom: {
id: 'customActivity',
name: '',
img: 'icons/skills/trades/academics-investigation-puzzles.webp',
description: '',
namePlaceholder: 'DAGGERHEART.APPLICATIONS.Downtime.custom.namePlaceholder',
placeholder: 'DAGGERHEART.APPLICATIONS.Downtime.custom.placeholder'
}
}; };
export const deathMoves = { export const deathMoves = {
@ -245,25 +279,21 @@ export const deathMoves = {
}; };
export const tiers = { export const tiers = {
tier1: { 1: {
id: 'tier1', id: 1,
label: 'DAGGERHEART.GENERAL.Tiers.tier1', label: 'DAGGERHEART.GENERAL.Tiers.1'
value: 1
}, },
tier2: { 2: {
id: 'tier2', id: 2,
label: 'DAGGERHEART.GENERAL.Tiers.tier2', label: 'DAGGERHEART.GENERAL.Tiers.2'
value: 2
}, },
tier3: { 3: {
id: 'tier3', id: 3,
label: 'DAGGERHEART.GENERAL.Tiers.tier3', label: 'DAGGERHEART.GENERAL.Tiers.3'
value: 3
}, },
tier4: { 4: {
id: 'tier4', id: 4,
label: 'DAGGERHEART.GENERAL.Tiers.tier4', label: 'DAGGERHEART.GENERAL.Tiers.4'
value: 4
} }
}; };
@ -366,31 +396,6 @@ export const abilityCosts = {
label: 'Armor Stack', label: 'Armor Stack',
group: 'TYPES.Actor.character' group: 'TYPES.Actor.character'
}, },
prayer: {
id: 'prayer',
label: 'Prayer Dice',
group: 'TYPES.Actor.character'
},
favor: {
id: 'favor',
label: 'Favor Points',
group: 'TYPES.Actor.character'
},
slayer: {
id: 'slayer',
label: 'Slayer Dice',
group: 'TYPES.Actor.character'
},
tide: {
id: 'tide',
label: 'Tide',
group: 'TYPES.Actor.character'
},
chaos: {
id: 'chaos',
label: 'Chaos',
group: 'TYPES.Actor.character'
},
fear: { fear: {
id: 'fear', id: 'fear',
label: 'Fear', label: 'Fear',
@ -401,29 +406,29 @@ export const abilityCosts = {
export const countdownTypes = { export const countdownTypes = {
spotlight: { spotlight: {
id: 'spotlight', id: 'spotlight',
label: 'DAGGERHEART.CONFIG.CountdownTypes.Spotlight' label: 'DAGGERHEART.CONFIG.CountdownType.spotlight'
}, },
characterAttack: { characterAttack: {
id: 'characterAttack', id: 'characterAttack',
label: 'DAGGERHEART.CONFIG.CountdownTypes.CharacterAttack' label: 'DAGGERHEART.CONFIG.CountdownType.characterAttack'
}, },
custom: { custom: {
id: 'custom', id: 'custom',
label: 'DAGGERHEART.CONFIG.CountdownTypes.Custom' label: 'DAGGERHEART.CONFIG.CountdownType.custom'
} }
}; };
export const rollTypes = { export const rollTypes = {
weapon: { attack: {
id: 'weapon', id: 'attack',
label: 'DAGGERHEART.CONFIG.RollTypes.weapon.name' label: 'DAGGERHEART.CONFIG.RollTypes.attack.name'
}, },
spellcast: { spellcast: {
id: 'spellcast', id: 'spellcast',
label: 'DAGGERHEART.CONFIG.RollTypes.spellcast.name' label: 'DAGGERHEART.CONFIG.RollTypes.spellcast.name'
}, },
ability: { trait: {
id: 'ability', id: 'trait',
label: 'DAGGERHEART.CONFIG.RollTypes.ability.name' label: 'DAGGERHEART.CONFIG.RollTypes.trait.name'
}, },
diceSet: { diceSet: {
id: 'diceSet', id: 'diceSet',

View file

@ -1,4 +0,0 @@
export const hooks = {
characterAttack: 'characterAttackHook',
spotlight: 'spotlightHook'
};

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,6 @@ import * as DOMAIN from './domainConfig.mjs';
import * as ACTOR from './actorConfig.mjs'; import * as ACTOR from './actorConfig.mjs';
import * as ITEM from './itemConfig.mjs'; import * as ITEM from './itemConfig.mjs';
import * as SETTINGS from './settingsConfig.mjs'; import * as SETTINGS from './settingsConfig.mjs';
import { hooks as HOOKS } from './hooksConfig.mjs';
import * as EFFECTS from './effectConfig.mjs'; import * as EFFECTS from './effectConfig.mjs';
import * as ACTIONS from './actionConfig.mjs'; import * as ACTIONS from './actionConfig.mjs';
import * as FLAGS from './flagsConfig.mjs'; import * as FLAGS from './flagsConfig.mjs';
@ -17,7 +16,6 @@ export const SYSTEM = {
ACTOR, ACTOR,
ITEM, ITEM,
SETTINGS, SETTINGS,
HOOKS,
EFFECTS, EFFECTS,
ACTIONS, ACTIONS,
FLAGS FLAGS

View file

@ -76,11 +76,7 @@ export class DHActionDiceData extends foundry.abstract.DataModel {
}; };
} }
getFormula(actor) { getFormula() {
/* const multiplier = this.multiplier === 'flat' ? this.flatMultiplier : actor.system[this.multiplier]?.total;
return this.custom.enabled
? this.custom.formula
: `${multiplier ?? 1}${this.dice}${this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : ''}`; */
const multiplier = this.multiplier === 'flat' ? this.flatMultiplier : `@${this.multiplier}`, const multiplier = this.multiplier === 'flat' ? this.flatMultiplier : `@${this.multiplier}`,
bonus = this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : ''; bonus = this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : '';
return this.custom.enabled ? this.custom.formula : `${multiplier ?? 1}${this.dice}${bonus}`; return this.custom.enabled ? this.custom.formula : `${multiplier ?? 1}${this.dice}${bonus}`;
@ -91,25 +87,25 @@ export class DHDamageField extends fields.SchemaField {
constructor(options, context = {}) { constructor(options, context = {}) {
const damageFields = { const damageFields = {
parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)), parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)),
includeBase: new fields.BooleanField({ initial: false }) includeBase: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label'
})
}; };
// if (hasBase) damageFields.includeBase = new fields.BooleanField({ initial: true });
super(damageFields, options, context); super(damageFields, options, context);
} }
} }
export class DHDamageData extends foundry.abstract.DataModel { export class DHResourceData extends foundry.abstract.DataModel {
/** @override */ /** @override */
static defineSchema() { static defineSchema() {
return { return {
// ...super.defineSchema(), applyTo: new fields.StringField({
base: new fields.BooleanField({ initial: false, readonly: true, label: 'Base' }), choices: CONFIG.DH.GENERAL.healingTypes,
type: new fields.StringField({ required: true,
choices: CONFIG.DH.GENERAL.damageTypes, blank: false,
initial: 'physical', initial: CONFIG.DH.GENERAL.healingTypes.hitPoints.id,
label: 'Type', label: 'DAGGERHEART.ACTIONS.Settings.applyTo.label'
nullable: false,
required: true
}), }),
resultBased: new fields.BooleanField({ resultBased: new fields.BooleanField({
initial: false, initial: false,
@ -120,3 +116,24 @@ export class DHDamageData extends foundry.abstract.DataModel {
}; };
} }
} }
export class DHDamageData extends DHResourceData {
/** @override */
static defineSchema() {
return {
...super.defineSchema(),
base: new fields.BooleanField({ initial: false, readonly: true, label: 'Base' }),
type: new fields.SetField(
new fields.StringField({
choices: CONFIG.DH.GENERAL.damageTypes,
initial: 'physical',
nullable: false,
required: true
}),
{
label: 'Type'
}
)
};
}
}

View file

@ -5,7 +5,7 @@ export default class DHAttackAction extends DHDamageAction {
static extraSchemas = [...super.extraSchemas, ...['roll', 'save']]; static extraSchemas = [...super.extraSchemas, ...['roll', 'save']];
static getRollType(parent) { static getRollType(parent) {
return parent.type === 'weapon' ? 'weapon' : 'spellcast'; return parent.type === 'weapon' ? 'attack' : 'spellcast';
} }
get chatTemplate() { get chatTemplate() {
@ -14,14 +14,14 @@ export default class DHAttackAction extends DHDamageAction {
prepareData() { prepareData() {
super.prepareData(); super.prepareData();
if(!!this.item?.system?.attack) { if (!!this.item?.system?.attack) {
if (this.damage.includeBase) { if (this.damage.includeBase) {
const baseDamage = this.getParentDamage(); const baseDamage = this.getParentDamage();
this.damage.parts.unshift(new DHDamageData(baseDamage)); this.damage.parts.unshift(new DHDamageData(baseDamage));
} }
if(this.roll.useDefault) { if (this.roll.useDefault) {
this.roll.trait = this.item.system.attack.roll.trait; this.roll.trait = this.item.system.attack.roll.trait;
this.roll.type = 'weapon'; this.roll.type = 'attack';
} }
} }
} }
@ -37,4 +37,17 @@ export default class DHAttackAction extends DHDamageAction {
base: true base: true
}; };
} }
async use(event, ...args) {
const result = await super.use(event, args);
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
await updateCountdowns(CONFIG.DH.GENERAL.countdownTypes.characterAttack.id);
return result;
}
// get modifiers() {
// return [];
// }
} }

View file

@ -1,4 +1,4 @@
import { DHActionDiceData, DHActionRollData, DHDamageData, DHDamageField } from './actionDice.mjs'; import { DHActionDiceData, DHActionRollData, DHDamageData, DHDamageField, DHResourceData } from './actionDice.mjs';
import DhpActor from '../../documents/actor.mjs'; import DhpActor from '../../documents/actor.mjs';
import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs'; import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs';
@ -35,12 +35,12 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
}), }),
cost: new fields.ArrayField( cost: new fields.ArrayField(
new fields.SchemaField({ new fields.SchemaField({
type: new fields.StringField({ key: new fields.StringField({
choices: CONFIG.DH.GENERAL.abilityCosts,
nullable: false, nullable: false,
required: true, required: true,
initial: 'hope' initial: 'hope'
}), }),
keyIsID: new fields.BooleanField(),
value: new fields.NumberField({ nullable: true, initial: 1 }), value: new fields.NumberField({ nullable: true, initial: 1 }),
scalable: new fields.BooleanField({ initial: false }), scalable: new fields.BooleanField({ initial: false }),
step: new fields.NumberField({ nullable: true, initial: null }) step: new fields.NumberField({ nullable: true, initial: null })
@ -96,21 +96,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
onSave: new fields.BooleanField({ initial: false }) onSave: new fields.BooleanField({ initial: false })
}) })
), ),
healing: new fields.SchemaField({ healing: new fields.EmbeddedDataField(DHResourceData),
type: new fields.StringField({
choices: CONFIG.DH.GENERAL.healingTypes,
required: true,
blank: false,
initial: CONFIG.DH.GENERAL.healingTypes.hitPoints.id,
label: 'Healing'
}),
resultBased: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.ACTIONS.Settings.resultBased.label'
}),
value: new fields.EmbeddedDataField(DHActionDiceData),
valueAlt: new fields.EmbeddedDataField(DHActionDiceData)
}),
beastform: new fields.SchemaField({ beastform: new fields.SchemaField({
tierAccess: new fields.SchemaField({ tierAccess: new fields.SchemaField({
exact: new fields.NumberField({ integer: true, nullable: true, initial: null }) exact: new fields.NumberField({ integer: true, nullable: true, initial: null })
@ -150,18 +136,18 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
} }
static getRollType(parent) { static getRollType(parent) {
return 'ability'; return 'trait';
} }
static getSourceConfig(parent) { static getSourceConfig(parent) {
const updateSource = {}; const updateSource = {};
updateSource.img ??= parent?.img ?? parent?.system?.img; updateSource.img ??= parent?.img ?? parent?.system?.img;
if (parent?.type === 'weapon') { if (parent?.type === 'weapon' && this === game.system.api.models.actions.actionsTypes.attack) {
updateSource['damage'] = { includeBase: true }; updateSource['damage'] = { includeBase: true };
updateSource['range'] = parent?.system?.attack?.range; updateSource['range'] = parent?.system?.attack?.range;
updateSource['roll'] = { updateSource['roll'] = {
useDefault: true useDefault: true
} };
} else { } else {
if (parent?.system?.trait) { if (parent?.system?.trait) {
updateSource['roll'] = { updateSource['roll'] = {
@ -177,18 +163,12 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
} }
getRollData(data = {}) { getRollData(data = {}) {
if (!this.actor) return null;
const actorData = this.actor.getRollData(false); const actorData = this.actor.getRollData(false);
// Remove when included directly in Actor getRollData // Add Roll results to RollDatas
actorData.prof = actorData.proficiency?.value ?? 1;
actorData.cast = actorData.spellcast?.value ?? 1;
actorData.result = data.roll?.total ?? 1; actorData.result = data.roll?.total ?? 1;
/* actorData.scale = data.costs?.length
? data.costs.reduce((a, c) => {
a[c.type] = c.value;
return a;
}, {})
: 1; */
actorData.scale = data.costs?.length // Right now only return the first scalable cost. actorData.scale = data.costs?.length // Right now only return the first scalable cost.
? (data.costs.find(c => c.scalable)?.total ?? 1) ? (data.costs.find(c => c.scalable)?.total ?? 1)
: 1; : 1;
@ -198,6 +178,8 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
} }
async use(event, ...args) { async use(event, ...args) {
if (!this.actor) throw new Error("An Action can't be used outside of an Actor context.");
const isFastForward = event.shiftKey || (!this.hasRoll && !this.hasSave); const isFastForward = event.shiftKey || (!this.hasRoll && !this.hasSave);
// Prepare base Config // Prepare base Config
const initConfig = this.initActionConfig(event); const initConfig = this.initActionConfig(event);
@ -211,7 +193,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
// Prepare Costs // Prepare Costs
const costsConfig = this.prepareCost(); const costsConfig = this.prepareCost();
if (isFastForward && !this.hasCost(costsConfig)) if (isFastForward && !(await this.hasCost(costsConfig)))
return ui.notifications.warn("You don't have the resources to use that action."); return ui.notifications.warn("You don't have the resources to use that action.");
// Prepare Uses // Prepare Uses
@ -234,7 +216,6 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return; if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return;
// Display configuration window if necessary // Display configuration window if necessary
// if (config.dialog?.configure && this.requireConfigurationDialog(config)) {
if (this.requireConfigurationDialog(config)) { if (this.requireConfigurationDialog(config)) {
config = await D20RollDialog.configure(null, config); config = await D20RollDialog.configure(null, config);
if (!config) return; if (!config) return;
@ -275,7 +256,8 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
hasDamage: !!this.damage?.parts?.length, hasDamage: !!this.damage?.parts?.length,
hasHealing: !!this.healing, hasHealing: !!this.healing,
hasEffect: !!this.effects?.length, hasEffect: !!this.effects?.length,
hasSave: this.hasSave hasSave: this.hasSave,
selectedRollMode: game.settings.get('core', 'rollMode')
}; };
} }
@ -285,7 +267,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
prepareCost() { prepareCost() {
const costs = this.cost?.length ? foundry.utils.deepClone(this.cost) : []; const costs = this.cost?.length ? foundry.utils.deepClone(this.cost) : [];
return costs; return this.calcCosts(costs);
} }
prepareUse() { prepareUse() {
@ -295,7 +277,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
} }
prepareTarget() { prepareTarget() {
if(!this.target?.type) return []; if (!this.target?.type) return [];
let targets; let targets;
if (this.target?.type === CONFIG.DH.ACTIONS.targetTypes.self.id) if (this.target?.type === CONFIG.DH.ACTIONS.targetTypes.self.id)
targets = this.constructor.formatTarget(this.actor.token ?? this.actor.prototypeToken); targets = this.constructor.formatTarget(this.actor.token ?? this.actor.prototypeToken);
@ -315,7 +297,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
prepareRoll() { prepareRoll() {
const roll = { const roll = {
modifiers: [], modifiers: this.modifiers,
trait: this.roll?.trait, trait: this.roll?.trait,
label: 'Attack', label: 'Attack',
type: this.actionType, type: this.actionType,
@ -334,10 +316,26 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
} }
async consume(config) { async consume(config) {
const usefulResources = foundry.utils.deepClone(this.actor.system.resources);
for (var cost of config.costs) {
if (cost.keyIsID) {
usefulResources[cost.key] = {
value: cost.value,
target: this.parent.parent,
keyIsID: true
};
}
}
const resources = config.costs const resources = config.costs
.filter(c => c.enabled !== false) .filter(c => c.enabled !== false)
.map(c => { .map(c => {
return { type: c.type, value: (c.total ?? c.value) * -1 }; const resource = usefulResources[c.key];
return {
key: c.key,
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
target: resource.target,
keyIsID: resource.keyIsID
};
}); });
await this.actor.modifyResource(resources); await this.actor.modifyResource(resources);
@ -353,6 +351,13 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
get hasRoll() { get hasRoll() {
return !!this.roll?.type || !!this.roll?.bonus; return !!this.roll?.type || !!this.roll?.bonus;
} }
get modifiers() {
if (!this.actor) return [];
const modifiers = [];
/** Placeholder for specific bonuses **/
return modifiers;
}
/* ROLL */ /* ROLL */
/* SAVE */ /* SAVE */
@ -378,23 +383,46 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
}); });
} }
hasCost(costs) { async getResources(costs) {
const actorResources = this.actor.system.resources;
const itemResources = {};
for (var itemResource of costs) {
if (itemResource.keyIsID) {
itemResources[itemResource.key] = {
value: this.parent.resource.value ?? 0
};
}
}
return {
...actorResources,
...itemResources
};
}
/* COST */
async hasCost(costs) {
const realCosts = this.getRealCosts(costs), const realCosts = this.getRealCosts(costs),
hasFearCost = realCosts.findIndex(c => c.type === 'fear'); hasFearCost = realCosts.findIndex(c => c.key === 'fear');
if (hasFearCost > -1) { if (hasFearCost > -1) {
const fearCost = realCosts.splice(hasFearCost, 1); const fearCost = realCosts.splice(hasFearCost, 1)[0];
if ( if (
!game.user.isGM || !game.user.isGM ||
fearCost[0].total > game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) fearCost.total > game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear)
) )
return false; return false;
} }
/* isReversed is a sign that the resource is inverted, IE it counts upwards instead of down */
const resources = await this.getResources(realCosts);
return realCosts.reduce( return realCosts.reduce(
(a, c) => a && this.actor.system.resources[c.type]?.value >= (c.total ?? c.value), (a, c) =>
a && resources[c.key].isReversed
? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max
: resources[c.key]?.value >= (c.total ?? c.value),
true true
); );
} }
/* COST */
/* USES */ /* USES */
calcUses(uses) { calcUses(uses) {
@ -409,7 +437,6 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
if (!uses) return true; if (!uses) return true;
return (uses.hasOwnProperty('enabled') && !uses.enabled) || uses.value + 1 <= uses.max; return (uses.hasOwnProperty('enabled') && !uses.enabled) || uses.value + 1 <= uses.max;
} }
/* USES */
/* TARGET */ /* TARGET */
isTargetFriendly(target) { isTargetFriendly(target) {
@ -432,7 +459,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
name: actor.actor.name, name: actor.actor.name,
img: actor.actor.img, img: actor.actor.img,
difficulty: actor.actor.system.difficulty, difficulty: actor.actor.system.difficulty,
evasion: actor.actor.system.evasion?.total evasion: actor.actor.system.evasion
}; };
} }
/* TARGET */ /* TARGET */

View file

@ -10,10 +10,12 @@ export default class DhBeastformAction extends DHBaseAction {
const abort = await this.handleActiveTransformations(); const abort = await this.handleActiveTransformations();
if (abort) return; if (abort) return;
const beastformUuid = await BeastformDialog.configure(beastformConfig); const item = args[0];
if (!beastformUuid) return;
await this.transform(beastformUuid); const { selected, evolved, hybrid } = await BeastformDialog.configure(beastformConfig, item);
if (!selected) return;
await this.transform(selected, evolved, hybrid);
} }
prepareBeastformConfig(config) { prepareBeastformConfig(config) {
@ -29,21 +31,48 @@ export default class DhBeastformAction extends DHBaseAction {
}; };
} }
async transform(beastformUuid) { async transform(selectedForm, evolvedData, hybridData) {
const beastform = await foundry.utils.fromUuid(beastformUuid); const formData = evolvedData?.form ? evolvedData.form.toObject() : selectedForm.toObject();
this.actor.createEmbeddedDocuments('Item', [beastform.toObject()]); const beastformEffect = formData.effects.find(x => x.type === 'beastform');
if (!beastformEffect) {
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
return;
}
if (evolvedData?.form) {
const evolvedForm = selectedForm.effects.find(x => x.type === 'beastform');
if (!evolvedForm) {
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
return;
}
beastformEffect.changes = [...beastformEffect.changes, ...evolvedForm.changes];
formData.system.features = [...formData.system.features, ...selectedForm.system.features.map(x => x.uuid)];
}
if (selectedForm.system.beastformType === CONFIG.DH.ITEM.beastformTypes.hybrid.id) {
formData.system.advantageOn = Object.values(hybridData.advantages).reduce((advantages, formCategory) => {
Object.keys(formCategory).forEach(advantageKey => {
advantages[advantageKey] = formCategory[advantageKey];
});
return advantages;
}, {});
formData.system.features = [
...formData.system.features,
...Object.values(hybridData.features).flatMap(x => Object.keys(x))
];
}
this.actor.createEmbeddedDocuments('Item', [formData]);
} }
async handleActiveTransformations() { async handleActiveTransformations() {
const beastformEffects = this.actor.effects.filter(x => x.type === 'beastform'); const beastformEffects = this.actor.effects.filter(x => x.type === 'beastform');
if (beastformEffects.length > 0) { const existingEffects = beastformEffects.length > 0;
for (let effect of beastformEffects) { await this.actor.deleteEmbeddedDocuments(
await effect.delete(); 'ActiveEffect',
} beastformEffects.map(x => x.id)
);
return true; return existingEffects;
}
return false;
} }
} }

View file

@ -1,3 +1,4 @@
import { setsEqual } from '../../helpers/utils.mjs';
import DHBaseAction from './baseAction.mjs'; import DHBaseAction from './baseAction.mjs';
export default class DHDamageAction extends DHBaseAction { export default class DHDamageAction extends DHBaseAction {
@ -6,33 +7,63 @@ export default class DHDamageAction extends DHBaseAction {
getFormulaValue(part, data) { getFormulaValue(part, data) {
let formulaValue = part.value; let formulaValue = part.value;
if (this.hasRoll && part.resultBased && data.system.roll.result.duality === -1) return part.valueAlt; if (this.hasRoll && part.resultBased && data.system.roll.result.duality === -1) return part.valueAlt;
const isAdversary = this.actor.type === 'adversary';
if (isAdversary && this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) {
const hasHordeDamage = this.actor.effects.find(
x => x.name === game.i18n.localize('DAGGERHEART.CONFIG.AdversaryType.horde.label')
);
if (hasHordeDamage) return part.valueAlt;
}
return formulaValue; return formulaValue;
} }
formatFormulas(formulas, systemData) {
const formattedFormulas = [];
formulas.forEach(formula => {
if (isNaN(formula.formula))
formula.formula = Roll.replaceFormulaData(formula.formula, this.getRollData(systemData));
const same = formattedFormulas.find(
f => setsEqual(f.damageTypes, formula.damageTypes) && f.applyTo === formula.applyTo
);
if (same) same.formula += ` + ${formula.formula}`;
else formattedFormulas.push(formula);
});
return formattedFormulas;
}
async rollDamage(event, data) { async rollDamage(event, data) {
let formula = this.damage.parts.map(p => this.getFormulaValue(p, data).getFormula(this.actor)).join(' + '); const systemData = data.system ?? data;
if (!formula || formula == '') return; let formulas = this.damage.parts.map(p => ({
let roll = { formula: formula, total: formula }, formula: this.getFormulaValue(p, data).getFormula(this.actor),
bonusDamage = []; damageTypes: p.applyTo === 'hitPoints' && !p.type.size ? new Set(['physical']) : p.type,
applyTo: p.applyTo
}));
if (isNaN(formula)) formula = Roll.replaceFormulaData(formula, this.getRollData(data.system ?? data)); if (!formulas.length) return;
formulas = this.formatFormulas(formulas, systemData);
const config = { const config = {
title: game.i18n.format('DAGGERHEART.UI.Chat.damageRoll.title', { damage: this.name }), title: game.i18n.format('DAGGERHEART.UI.Chat.damageRoll.title', { damage: game.i18n.localize(this.name) }),
roll: { formula }, roll: formulas,
targets: data.system?.targets.filter(t => t.hit) ?? data.targets, targets: systemData.targets.filter(t => t.hit) ?? data.targets,
hasSave: this.hasSave, hasSave: this.hasSave,
isCritical: data.system?.roll?.isCritical ?? false, isCritical: systemData.roll?.isCritical ?? false,
source: data.system?.source, source: systemData.source,
data: this.getRollData(),
event event
}; };
if (this.hasSave) config.onSave = this.save.damageMod; if (this.hasSave) config.onSave = this.save.damageMod;
if (data.system) { if (data.system) {
config.source.message = data._id; config.source.message = data._id;
config.directDamage = false; config.directDamage = false;
} else {
config.directDamage = true;
} }
roll = CONFIG.Dice.daggerheart.DamageRoll.build(config); return CONFIG.Dice.daggerheart.DamageRoll.build(config);
} }
} }

View file

@ -15,28 +15,34 @@ export default class DHHealingAction extends DHBaseAction {
} }
async rollHealing(event, data) { async rollHealing(event, data) {
let formulaValue = this.getFormulaValue(data), const systemData = data.system ?? data;
formula = formulaValue.getFormula(this.actor); let formulas = [
{
if (!formula || formula == '') return; formula: this.getFormulaValue(data).getFormula(this.actor),
let roll = { formula: formula, total: formula }, applyTo: this.healing.applyTo
bonusDamage = []; }
];
const config = { const config = {
title: game.i18n.format('DAGGERHEART.UI.Chat.healingRoll.title', { title: game.i18n.format('DAGGERHEART.UI.Chat.healingRoll.title', {
healing: game.i18n.localize(CONFIG.DH.GENERAL.healingTypes[this.healing.type].label) healing: game.i18n.localize(CONFIG.DH.GENERAL.healingTypes[this.healing.applyTo].label)
}), }),
roll: { formula }, roll: formulas,
targets: (data.system?.targets ?? data.targets).filter(t => t.hit), targets: (data.system?.targets ?? data.targets).filter(t => t.hit),
messageType: 'healing', messageType: 'healing',
type: this.healing.type, source: systemData.source,
data: this.getRollData(),
event event
}; };
roll = CONFIG.Dice.daggerheart.DamageRoll.build(config); return CONFIG.Dice.daggerheart.DamageRoll.build(config);
} }
get chatTemplate() { get chatTemplate() {
return 'systems/daggerheart/templates/ui/chat/healing-roll.hbs'; return 'systems/daggerheart/templates/ui/chat/healing-roll.hbs';
} }
get modifiers() {
return [];
}
} }

View file

@ -10,6 +10,11 @@ export default class BeastformEffect extends foundry.abstract.TypeDataModel {
base64: false, base64: false,
nullable: true nullable: true
}), }),
tokenRingImg: new fields.FilePathField({
initial: 'icons/svg/mystery-man.svg',
categories: ['IMAGE'],
base64: false
}),
tokenSize: new fields.SchemaField({ tokenSize: new fields.SchemaField({
height: new fields.NumberField({ integer: true, nullable: true }), height: new fields.NumberField({ integer: true, nullable: true }),
width: new fields.NumberField({ integer: true, nullable: true }) width: new fields.NumberField({ integer: true, nullable: true })
@ -21,6 +26,13 @@ export default class BeastformEffect extends foundry.abstract.TypeDataModel {
}; };
} }
async _onCreate() {
if (this.parent.parent?.type === 'character') {
this.parent.parent.system.primaryWeapon?.update?.({ 'system.equipped': false });
this.parent.parent.system.secondayWeapon?.update?.({ 'system.equipped': false });
}
}
async _preDelete() { async _preDelete() {
if (this.parent.parent.type === 'character') { if (this.parent.parent.type === 'character') {
const update = { const update = {
@ -28,6 +40,11 @@ export default class BeastformEffect extends foundry.abstract.TypeDataModel {
width: this.characterTokenData.tokenSize.width, width: this.characterTokenData.tokenSize.width,
texture: { texture: {
src: this.characterTokenData.tokenImg src: this.characterTokenData.tokenImg
},
ring: {
subject: {
texture: this.characterTokenData.tokenRingImg
}
} }
}; };

View file

@ -1,12 +1,7 @@
import DHAdversarySettings from '../../applications/sheets-configs/adversary-settings.mjs'; import DHAdversarySettings from '../../applications/sheets-configs/adversary-settings.mjs';
import ActionField from '../fields/actionField.mjs'; import ActionField from '../fields/actionField.mjs';
import BaseDataActor from './base.mjs'; import BaseDataActor from './base.mjs';
import { resourceField, bonusField } from '../fields/actorField.mjs';
const resourceField = () =>
new foundry.data.fields.SchemaField({
value: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
max: new foundry.data.fields.NumberField({ initial: 0, integer: true })
});
export default class DhpAdversary extends BaseDataActor { export default class DhpAdversary extends BaseDataActor {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary']; static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary'];
@ -22,28 +17,44 @@ export default class DhpAdversary extends BaseDataActor {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
tier: new fields.StringField({ ...super.defineSchema(),
tier: new fields.NumberField({
required: true, required: true,
integer: true,
choices: CONFIG.DH.GENERAL.tiers, choices: CONFIG.DH.GENERAL.tiers,
initial: CONFIG.DH.GENERAL.tiers.tier1.id initial: CONFIG.DH.GENERAL.tiers[1].id
}), }),
type: new fields.StringField({ type: new fields.StringField({
required: true, required: true,
choices: CONFIG.DH.ACTOR.adversaryTypes, choices: CONFIG.DH.ACTOR.adversaryTypes,
initial: CONFIG.DH.ACTOR.adversaryTypes.standard.id initial: CONFIG.DH.ACTOR.adversaryTypes.standard.id
}), }),
description: new fields.StringField(),
motivesAndTactics: new fields.StringField(), motivesAndTactics: new fields.StringField(),
notes: new fields.HTMLField(), notes: new fields.HTMLField(),
difficulty: new fields.NumberField({ required: true, initial: 1, integer: true }), difficulty: new fields.NumberField({ required: true, initial: 1, integer: true }),
hordeHp: new fields.NumberField({ required: true, initial: 1, integer: true }), hordeHp: new fields.NumberField({
required: true,
initial: 1,
integer: true,
label: 'DAGGERHEART.GENERAL.hordeHp'
}),
damageThresholds: new fields.SchemaField({ damageThresholds: new fields.SchemaField({
major: new fields.NumberField({ required: true, initial: 0, integer: true }), major: new fields.NumberField({
severe: new fields.NumberField({ required: true, initial: 0, integer: true }) required: true,
initial: 0,
integer: true,
label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold'
}),
severe: new fields.NumberField({
required: true,
initial: 0,
integer: true,
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
})
}), }),
resources: new fields.SchemaField({ resources: new fields.SchemaField({
hitPoints: resourceField(), hitPoints: resourceField(0, 'DAGGERHEART.GENERAL.HitPoints.plural', true),
stress: resourceField() stress: resourceField(0, 'DAGGERHEART.GENERAL.stress', true)
}), }),
attack: new ActionField({ attack: new ActionField({
initial: { initial: {
@ -58,11 +69,12 @@ export default class DhpAdversary extends BaseDataActor {
amount: 1 amount: 1
}, },
roll: { roll: {
type: 'weapon' type: 'attack'
}, },
damage: { damage: {
parts: [ parts: [
{ {
type: ['physical'],
value: { value: {
multiplier: 'flat' multiplier: 'flat'
} }
@ -74,13 +86,18 @@ export default class DhpAdversary extends BaseDataActor {
experiences: new fields.TypedObjectField( experiences: new fields.TypedObjectField(
new fields.SchemaField({ new fields.SchemaField({
name: new fields.StringField(), name: new fields.StringField(),
total: new fields.NumberField({ required: true, integer: true, initial: 1 }) value: new fields.NumberField({ required: true, integer: true, initial: 1 })
}) })
), ),
bonuses: new fields.SchemaField({ bonuses: new fields.SchemaField({
difficulty: new fields.SchemaField({ roll: new fields.SchemaField({
all: new fields.NumberField({ integer: true, initial: 0 }), attack: bonusField('DAGGERHEART.GENERAL.Roll.attack'),
reaction: new fields.NumberField({ integer: true, initial: 0 }) action: bonusField('DAGGERHEART.GENERAL.Roll.action'),
reaction: bonusField('DAGGERHEART.GENERAL.Roll.reaction')
}),
damage: new fields.SchemaField({
physical: bonusField('DAGGERHEART.GENERAL.Damage.physicalDamage'),
magical: bonusField('DAGGERHEART.GENERAL.Damage.magicalDamage')
}) })
}) })
}; };
@ -93,4 +110,37 @@ export default class DhpAdversary extends BaseDataActor {
get features() { get features() {
return this.parent.items.filter(x => x.type === 'feature'); return this.parent.items.filter(x => x.type === 'feature');
} }
async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false;
if (this.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) {
if (changes.system?.resources?.hitPoints?.value) {
const halfHP = Math.ceil(this.resources.hitPoints.max / 2);
const newHitPoints = changes.system.resources.hitPoints.value;
const previouslyAboveHalf = this.resources.hitPoints.value < halfHP;
const loweredBelowHalf = previouslyAboveHalf && newHitPoints >= halfHP;
const raisedAboveHalf = !previouslyAboveHalf && newHitPoints < halfHP;
if (loweredBelowHalf) {
await this.parent.createEmbeddedDocuments('ActiveEffect', [
{
name: game.i18n.localize('DAGGERHEART.CONFIG.AdversaryType.horde.label'),
img: 'icons/magic/movement/chevrons-down-yellow.webp',
disabled: !game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation)
.hordeDamage
}
]);
} else if (raisedAboveHalf) {
const hordeEffects = this.parent.effects.filter(
x => x.name === game.i18n.localize('DAGGERHEART.CONFIG.AdversaryType.horde.label')
);
await this.parent.deleteEmbeddedDocuments(
'ActiveEffect',
hordeEffects.map(x => x.id)
);
}
}
}
}
} }

View file

@ -1,4 +1,11 @@
import DHBaseActorSettings from "../../applications/sheets/api/actor-setting.mjs"; import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs';
const resistanceField = reductionLabel =>
new foundry.data.fields.SchemaField({
resistance: new foundry.data.fields.BooleanField({ initial: false }),
immunity: new foundry.data.fields.BooleanField({ initial: false }),
reduction: new foundry.data.fields.NumberField({ integer: true, initial: 0, label: reductionLabel })
});
/** /**
* Describes metadata about the actor data model type * Describes metadata about the actor data model type
@ -16,6 +23,7 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
type: 'base', type: 'base',
isNPC: true, isNPC: true,
settingSheet: null, settingSheet: null,
hasResistances: true
}; };
} }
@ -27,10 +35,15 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
/** @inheritDoc */ /** @inheritDoc */
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
const schema = {};
return { if (this.metadata.isNPC) schema.description = new fields.HTMLField({ required: true, nullable: true });
description: new fields.HTMLField({ required: true, nullable: true }) if (this.metadata.hasResistances)
}; schema.resistance = new fields.SchemaField({
physical: resistanceField('DAGGERHEART.GENERAL.DamageResistance.physicalReduction'),
magical: resistanceField('DAGGERHEART.GENERAL.DamageResistance.magicalReduction')
});
return schema;
} }
/** /**

View file

@ -1,35 +1,18 @@
import { burden } from '../../config/generalConfig.mjs'; import { burden } from '../../config/generalConfig.mjs';
import ActionField from '../fields/actionField.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import DhLevelData from '../levelData.mjs'; import DhLevelData from '../levelData.mjs';
import BaseDataActor from './base.mjs'; import BaseDataActor from './base.mjs';
import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs';
const attributeField = () => import ActionField from '../fields/actionField.mjs';
new foundry.data.fields.SchemaField({
value: new foundry.data.fields.NumberField({ initial: null, integer: true }),
bonus: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
tierMarked: new foundry.data.fields.BooleanField({ initial: false })
});
const resourceField = max =>
new foundry.data.fields.SchemaField({
value: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
bonus: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
max: new foundry.data.fields.NumberField({ initial: max, integer: true })
});
const stressDamageReductionRule = () =>
new foundry.data.fields.SchemaField({
enabled: new foundry.data.fields.BooleanField({ required: true, initial: false }),
cost: new foundry.data.fields.NumberField({ integer: true })
});
export default class DhCharacter extends BaseDataActor { export default class DhCharacter extends BaseDataActor {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Character'];
static get metadata() { static get metadata() {
return foundry.utils.mergeObject(super.metadata, { return foundry.utils.mergeObject(super.metadata, {
label: 'TYPES.Actor.character', label: 'TYPES.Actor.character',
type: 'character', type: 'character',
isNPC: false, isNPC: false
}); });
} }
@ -37,36 +20,43 @@ export default class DhCharacter extends BaseDataActor {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
...super.defineSchema(),
resources: new fields.SchemaField({ resources: new fields.SchemaField({
hitPoints: new fields.SchemaField({ hitPoints: resourceField(0, 'DAGGERHEART.GENERAL.HitPoints.plural', true),
value: new foundry.data.fields.NumberField({ initial: 0, integer: true }), stress: resourceField(6, 'DAGGERHEART.GENERAL.stress', true),
bonus: new foundry.data.fields.NumberField({ initial: 0, integer: true }) hope: resourceField(6, 'DAGGERHEART.GENERAL.hope')
}),
stress: resourceField(6),
hope: resourceField(6),
tokens: new fields.ObjectField(),
dice: new fields.ObjectField()
}), }),
traits: new fields.SchemaField({ traits: new fields.SchemaField({
agility: attributeField(), agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'),
strength: attributeField(), strength: attributeField('DAGGERHEART.CONFIG.Traits.strength.name'),
finesse: attributeField(), finesse: attributeField('DAGGERHEART.CONFIG.Traits.finesse.name'),
instinct: attributeField(), instinct: attributeField('DAGGERHEART.CONFIG.Traits.instinct.name'),
presence: attributeField(), presence: attributeField('DAGGERHEART.CONFIG.Traits.presence.name'),
knowledge: attributeField() knowledge: attributeField('DAGGERHEART.CONFIG.Traits.knowledge.name')
}), }),
proficiency: new fields.SchemaField({ proficiency: new fields.NumberField({
value: new fields.NumberField({ initial: 1, integer: true }), initial: 1,
bonus: new fields.NumberField({ initial: 0, integer: true }) integer: true,
label: 'DAGGERHEART.GENERAL.proficiency'
}), }),
evasion: new fields.SchemaField({ evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }),
bonus: new fields.NumberField({ initial: 0, integer: true }) armorScore: new fields.NumberField({ integer: true, initial: 0, label: 'DAGGERHEART.GENERAL.armorScore' }),
damageThresholds: new fields.SchemaField({
severe: new fields.NumberField({
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold'
}),
major: new fields.NumberField({
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
})
}), }),
experiences: new fields.TypedObjectField( experiences: new fields.TypedObjectField(
new fields.SchemaField({ new fields.SchemaField({
name: new fields.StringField(), name: new fields.StringField(),
value: new fields.NumberField({ integer: true, initial: 0 }), value: new fields.NumberField({ integer: true, initial: 0 })
bonus: new fields.NumberField({ integer: true, initial: 0 })
}) })
), ),
gold: new fields.SchemaField({ gold: new fields.SchemaField({
@ -78,7 +68,7 @@ export default class DhCharacter extends BaseDataActor {
scars: new fields.TypedObjectField( scars: new fields.TypedObjectField(
new fields.SchemaField({ new fields.SchemaField({
name: new fields.StringField({}), name: new fields.StringField({}),
description: new fields.HTMLField() description: new fields.StringField()
}) })
), ),
biography: new fields.SchemaField({ biography: new fields.SchemaField({
@ -98,43 +88,174 @@ export default class DhCharacter extends BaseDataActor {
value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }), value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }),
subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }) subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true })
}), }),
attack: new ActionField({
initial: {
name: 'Attack',
img: 'icons/skills/melee/unarmed-punch-fist-yellow-red.webp',
_id: foundry.utils.randomID(),
systemPath: 'attack',
type: 'attack',
range: 'melee',
target: {
type: 'any',
amount: 1
},
roll: {
type: 'attack',
trait: 'strength'
},
damage: {
parts: [
{
type: ['physical'],
value: {
custom: {
enabled: true,
formula: '@system.rules.attack.damage.value'
}
}
}
]
}
}
}),
advantageSources: new fields.ArrayField(new fields.StringField(), {
label: 'DAGGERHEART.ACTORS.Character.advantageSources.label',
hint: 'DAGGERHEART.ACTORS.Character.advantageSources.hint'
}),
disadvantageSources: new fields.ArrayField(new fields.StringField(), {
label: 'DAGGERHEART.ACTORS.Character.disadvantageSources.label',
hint: 'DAGGERHEART.ACTORS.Character.disadvantageSources.hint'
}),
levelData: new fields.EmbeddedDataField(DhLevelData), levelData: new fields.EmbeddedDataField(DhLevelData),
bonuses: new fields.SchemaField({ bonuses: new fields.SchemaField({
armorScore: new fields.NumberField({ integer: true, initial: 0 }),
damageThresholds: new fields.SchemaField({
severe: new fields.NumberField({ integer: true, initial: 0 }),
major: new fields.NumberField({ integer: true, initial: 0 })
}),
roll: new fields.SchemaField({ roll: new fields.SchemaField({
attack: new fields.NumberField({ integer: true, initial: 0 }), attack: bonusField('DAGGERHEART.GENERAL.Roll.attack'),
spellcast: new fields.NumberField({ integer: true, initial: 0 }), spellcast: bonusField('DAGGERHEART.GENERAL.Roll.spellcast'),
action: new fields.NumberField({ integer: true, initial: 0 }), trait: bonusField('DAGGERHEART.GENERAL.Roll.trait'),
hopeOrFear: new fields.NumberField({ integer: true, initial: 0 }) action: bonusField('DAGGERHEART.GENERAL.Roll.action'),
reaction: bonusField('DAGGERHEART.GENERAL.Roll.reaction'),
primaryWeapon: bonusField('DAGGERHEART.GENERAL.Roll.primaryWeaponAttack'),
secondaryWeapon: bonusField('DAGGERHEART.GENERAL.Roll.secondaryWeaponAttack')
}), }),
damage: new fields.SchemaField({ damage: new fields.SchemaField({
all: new fields.NumberField({ integer: true, initial: 0 }), physical: bonusField('DAGGERHEART.GENERAL.Damage.physicalDamage'),
physical: new fields.NumberField({ integer: true, initial: 0 }), magical: bonusField('DAGGERHEART.GENERAL.Damage.magicalDamage'),
magic: new fields.NumberField({ integer: true, initial: 0 }) primaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.primaryWeapon'),
secondaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.secondaryWeapon')
}),
healing: bonusField('DAGGERHEART.GENERAL.Healing.healingAmount'),
range: new fields.SchemaField({
weapon: new fields.NumberField({
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.Range.weapon'
}),
spell: new fields.NumberField({
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.Range.spell'
}),
other: new fields.NumberField({
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.Range.other'
})
}),
rally: new fields.ArrayField(new fields.StringField(), {
label: 'DAGGERHEART.CLASS.Feature.rallyDice'
}),
rest: new fields.SchemaField({
shortRest: new fields.SchemaField({
shortMoves: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 0,
label: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.shortRestMoves.label',
hint: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.shortRestMoves.hint'
}),
longMoves: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 0,
label: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.longRestMoves.label',
hint: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.longRestMoves.hint'
})
}),
longRest: new fields.SchemaField({
shortMoves: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 0,
label: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.shortRestMoves.label',
hint: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.shortRestMoves.hint'
}),
longMoves: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 0,
label: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.longRestMoves.label',
hint: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.longRestMoves.hint'
})
})
}) })
}), }),
companion: new ForeignDocumentUUIDField({ type: 'Actor', nullable: true, initial: null }), companion: new ForeignDocumentUUIDField({ type: 'Actor', nullable: true, initial: null }),
rules: new fields.SchemaField({ rules: new fields.SchemaField({
maxArmorMarked: new fields.SchemaField({ damageReduction: new fields.SchemaField({
value: new fields.NumberField({ required: true, integer: true, initial: 1 }), maxArmorMarked: new fields.SchemaField({
bonus: new fields.NumberField({ required: true, integer: true, initial: 0 }), value: new fields.NumberField({
stressExtra: new fields.NumberField({ required: true, integer: true, initial: 0 }) required: true,
integer: true,
initial: 1,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.maxArmorMarkedBonus'
}),
stressExtra: new fields.NumberField({
required: true,
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.maxArmorMarkedStress.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.maxArmorMarkedStress.hint'
})
}),
stressDamageReduction: new fields.SchemaField({
severe: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.severe'),
major: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.major'),
minor: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.minor')
}),
increasePerArmorMark: new fields.NumberField({
integer: true,
initial: 1,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint'
}),
magical: new fields.BooleanField({ initial: false }),
physical: new fields.BooleanField({ initial: false })
}), }),
stressDamageReduction: new fields.SchemaField({ attack: new fields.SchemaField({
severe: stressDamageReductionRule(), damage: new fields.SchemaField({
major: stressDamageReductionRule(), value: new fields.StringField({
minor: stressDamageReductionRule() required: true,
initial: '@profd4',
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.value.label'
})
})
}), }),
strangePatterns: new fields.NumberField({ weapon: new fields.SchemaField({
integer: true, /* Unimplemented
min: 1, -> Should remove the lowest damage dice from weapon damage
max: 12, -> Reflect this in the chat message somehow so players get feedback that their choice is helping them.
nullable: true, */
initial: null dropLowestDamageDice: new fields.BooleanField({ initial: false }),
/* Unimplemented
-> Should flip any lowest possible dice rolls for weapon damage to highest
-> Reflect this in the chat message somehow so players get feedback that their choice is helping them.
*/
flipMinDiceValue: new fields.BooleanField({ intial: false })
}), }),
runeWard: new fields.BooleanField({ initial: false }) runeWard: new fields.BooleanField({ initial: false })
}) })
@ -169,6 +290,11 @@ export default class DhCharacter extends BaseDataActor {
return !this.class.value || !this.class.subclass; return !this.class.value || !this.class.subclass;
} }
get spellcastModifier() {
const subClasses = this.parent.items.filter(x => x.type === 'subclass') ?? [];
return Math.max(subClasses?.map(sc => this.traits[sc.system.spellcastingTrait]?.value));
}
get spellcastingModifiers() { get spellcastingModifiers() {
return { return {
main: this.class.subclass?.system?.spellcastingTrait, main: this.class.subclass?.system?.spellcastingTrait,
@ -198,6 +324,24 @@ export default class DhCharacter extends BaseDataActor {
return this.parent.items.find(x => x.type === 'armor' && x.system.equipped); return this.parent.items.find(x => x.type === 'armor' && x.system.equipped);
} }
get activeBeastform() {
return this.parent.effects.find(x => x.type === 'beastform');
}
get usedUnarmed() {
const primaryWeaponEquipped = this.primaryWeapon?.system?.equipped;
const secondaryWeaponEquipped = this.secondaryWeapon?.system?.equipped;
return !primaryWeaponEquipped && !secondaryWeaponEquipped
? {
...this.attack,
id: this.attack.id,
name: this.activeBeastform ? 'DAGGERHEART.ITEMS.Beastform.attackName' : this.attack.name,
img: this.activeBeastform ? 'icons/creatures/claws/claw-straight-brown.webp' : this.attack.img,
actor: this.parent
}
: null;
}
get sheetLists() { get sheetLists() {
const ancestryFeatures = [], const ancestryFeatures = [],
communityFeatures = [], communityFeatures = [],
@ -207,23 +351,23 @@ export default class DhCharacter extends BaseDataActor {
features = []; features = [];
for (let item of this.parent.items) { for (let item of this.parent.items) {
if (item.system.type === CONFIG.DH.ITEM.featureTypes.ancestry.id) { if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.ancestry.id) {
ancestryFeatures.push(item); ancestryFeatures.push(item);
} else if (item.system.type === CONFIG.DH.ITEM.featureTypes.community.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.community.id) {
communityFeatures.push(item); communityFeatures.push(item);
} else if (item.system.type === CONFIG.DH.ITEM.featureTypes.class.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.class.id) {
classFeatures.push(item); classFeatures.push(item);
} else if (item.system.type === CONFIG.DH.ITEM.featureTypes.subclass.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) {
const subclassState = this.class.subclass.system.featureState; const subclassState = this.class.subclass.system.featureState;
const identifier = item.system.identifier; const subType = item.system.subType;
if ( if (
identifier === 'foundationFeature' || subType === CONFIG.DH.ITEM.featureSubTypes.foundation ||
(identifier === 'specializationFeature' && subclassState >= 2) || (subType === CONFIG.DH.ITEM.featureSubTypes.specialization && subclassState >= 2) ||
(identifier === 'masterFeature' && subclassState >= 3) (subType === CONFIG.DH.ITEM.featureSubTypes.mastery && subclassState >= 3)
) { ) {
subclassFeatures.push(item); subclassFeatures.push(item);
} }
} else if (item.system.type === CONFIG.DH.ITEM.featureTypes.companion.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.companion.id) {
companionFeatures.push(item); companionFeatures.push(item);
} else if (item.type === 'feature' && !item.system.type) { } else if (item.type === 'feature' && !item.system.type) {
features.push(item); features.push(item);
@ -278,9 +422,14 @@ export default class DhCharacter extends BaseDataActor {
} }
get deathMoveViable() { get deathMoveViable() {
return ( return this.resources.hitPoints.max > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.max;
this.resources.hitPoints.maxTotal > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.maxTotal }
);
get armorApplicableDamageTypes() {
return {
physical: !this.rules.damageReduction.magical,
magical: !this.rules.damageReduction.physical
};
} }
static async unequipBeforeEquip(itemToEquip) { static async unequipBeforeEquip(itemToEquip) {
@ -306,6 +455,8 @@ export default class DhCharacter extends BaseDataActor {
} }
prepareBaseData() { prepareBaseData() {
this.evasion = this.class.value?.system?.evasion ?? 0;
const currentLevel = this.levelData.level.current; const currentLevel = this.levelData.level.current;
const currentTier = const currentTier =
currentLevel === 1 currentLevel === 1
@ -316,32 +467,32 @@ export default class DhCharacter extends BaseDataActor {
for (let levelKey in this.levelData.levelups) { for (let levelKey in this.levelData.levelups) {
const level = this.levelData.levelups[levelKey]; const level = this.levelData.levelups[levelKey];
this.proficiency.bonus += level.achievements.proficiency; this.proficiency += level.achievements.proficiency;
for (let selection of level.selections) { for (let selection of level.selections) {
switch (selection.type) { switch (selection.type) {
case 'trait': case 'trait':
selection.data.forEach(data => { selection.data.forEach(data => {
this.traits[data].bonus += 1; this.traits[data].value += 1;
this.traits[data].tierMarked = selection.tier === currentTier; this.traits[data].tierMarked = selection.tier === currentTier;
}); });
break; break;
case 'hitPoint': case 'hitPoint':
this.resources.hitPoints.bonus += selection.value; this.resources.hitPoints.max += selection.value;
break; break;
case 'stress': case 'stress':
this.resources.stress.bonus += selection.value; this.resources.stress.max += selection.value;
break; break;
case 'evasion': case 'evasion':
this.evasion.bonus += selection.value; this.evasion += selection.value;
break; break;
case 'proficiency': case 'proficiency':
this.proficiency.bonus = selection.value; this.proficiency = selection.value;
break; break;
case 'experience': case 'experience':
Object.keys(this.experiences).forEach(key => { Object.keys(this.experiences).forEach(key => {
const experience = this.experiences[key]; const experience = this.experiences[key];
experience.bonus += selection.value; experience.value += selection.value;
}); });
break; break;
} }
@ -349,6 +500,7 @@ export default class DhCharacter extends BaseDataActor {
} }
const armor = this.armor; const armor = this.armor;
this.armorScore = armor ? armor.system.baseScore : 0;
this.damageThresholds = { this.damageThresholds = {
major: armor major: armor
? armor.system.baseThresholds.major + this.levelData.level.current ? armor.system.baseThresholds.major + this.levelData.level.current
@ -357,38 +509,19 @@ export default class DhCharacter extends BaseDataActor {
? armor.system.baseThresholds.severe + this.levelData.level.current ? armor.system.baseThresholds.severe + this.levelData.level.current
: this.levelData.level.current * 2 : this.levelData.level.current * 2
}; };
this.resources.hope.max -= Object.keys(this.scars).length;
this.resources.hitPoints.max = this.class.value?.system?.hitPoints ?? 0;
} }
prepareDerivedData() { prepareDerivedData() {
this.resources.hope.max -= Object.keys(this.scars).length; const baseHope = this.resources.hope.value + (this.companion?.system?.resources?.hope ?? 0);
this.resources.hope.value = Math.min(this.resources.hope.value, this.resources.hope.max); this.resources.hope.value = Math.min(baseHope, this.resources.hope.max);
for (var traitKey in this.traits) {
var trait = this.traits[traitKey];
trait.total = (trait.value ?? 0) + trait.bonus;
}
for (var experienceKey in this.experiences) {
var experience = this.experiences[experienceKey];
experience.total = experience.value + experience.bonus;
}
this.rules.maxArmorMarked.total = this.rules.maxArmorMarked.value + this.rules.maxArmorMarked.bonus;
this.armorScore = this.armor ? this.armor.system.baseScore + (this.bonuses.armorScore ?? 0) : 0;
this.resources.hitPoints.maxTotal = (this.class.value?.system?.hitPoints ?? 0) + this.resources.hitPoints.bonus;
this.resources.stress.maxTotal = this.resources.stress.max + this.resources.stress.bonus;
this.evasion.total = (this.class?.evasion ?? 0) + this.evasion.bonus;
this.proficiency.total = this.proficiency.value + this.proficiency.bonus;
} }
getRollData() { getRollData() {
const data = super.getRollData(); const data = super.getRollData();
return { return {
...data, ...data,
...this.resources.tokens,
...this.resources.dice,
...this.bonuses,
tier: this.tier, tier: this.tier,
level: this.levelData.level.current level: this.levelData.level.current
}; };

View file

@ -4,6 +4,7 @@ import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import ActionField from '../fields/actionField.mjs'; import ActionField from '../fields/actionField.mjs';
import { adjustDice, adjustRange } from '../../helpers/utils.mjs'; import { adjustDice, adjustRange } from '../../helpers/utils.mjs';
import DHCompanionSettings from '../../applications/sheets-configs/companion-settings.mjs'; import DHCompanionSettings from '../../applications/sheets-configs/companion-settings.mjs';
import { resourceField, bonusField } from '../fields/actorField.mjs';
export default class DhCompanion extends BaseDataActor { export default class DhCompanion extends BaseDataActor {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Companion']; static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Companion'];
@ -12,6 +13,7 @@ export default class DhCompanion extends BaseDataActor {
return foundry.utils.mergeObject(super.metadata, { return foundry.utils.mergeObject(super.metadata, {
label: 'TYPES.Actor.companion', label: 'TYPES.Actor.companion',
type: 'companion', type: 'companion',
isNPC: false,
settingSheet: DHCompanionSettings settingSheet: DHCompanionSettings
}); });
} }
@ -20,24 +22,23 @@ export default class DhCompanion extends BaseDataActor {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
...super.defineSchema(),
partner: new ForeignDocumentUUIDField({ type: 'Actor' }), partner: new ForeignDocumentUUIDField({ type: 'Actor' }),
resources: new fields.SchemaField({ resources: new fields.SchemaField({
stress: new fields.SchemaField({ stress: resourceField(3, 'DAGGERHEART.GENERAL.stress', true),
value: new fields.NumberField({ initial: 0, integer: true }), hope: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.hope' })
bonus: new fields.NumberField({ initial: 0, integer: true }),
max: new fields.NumberField({ initial: 3, integer: true })
}),
hope: new fields.NumberField({ initial: 0, integer: true })
}), }),
evasion: new fields.SchemaField({ evasion: new fields.NumberField({
value: new fields.NumberField({ required: true, min: 1, initial: 10, integer: true }), required: true,
bonus: new fields.NumberField({ initial: 0, integer: true }) min: 1,
initial: 10,
integer: true,
label: 'DAGGERHEART.GENERAL.evasion'
}), }),
experiences: new fields.TypedObjectField( experiences: new fields.TypedObjectField(
new fields.SchemaField({ new fields.SchemaField({
name: new fields.StringField({}), name: new fields.StringField({}),
value: new fields.NumberField({ integer: true, initial: 0 }), value: new fields.NumberField({ integer: true, initial: 0 })
bonus: new fields.NumberField({ integer: true, initial: 0 })
}), }),
{ {
initial: { initial: {
@ -59,17 +60,16 @@ export default class DhCompanion extends BaseDataActor {
amount: 1 amount: 1
}, },
roll: { roll: {
type: 'weapon', type: 'attack',
bonus: 0, bonus: 0
trait: 'instinct'
}, },
damage: { damage: {
parts: [ parts: [
{ {
multiplier: 'flat', type: ['physical'],
value: { value: {
dice: 'd6', dice: 'd6',
multiplier: 'flat' multiplier: 'prof'
} }
} }
] ]
@ -77,20 +77,22 @@ export default class DhCompanion extends BaseDataActor {
} }
}), }),
actions: new fields.ArrayField(new ActionField()), actions: new fields.ArrayField(new ActionField()),
levelData: new fields.EmbeddedDataField(DhLevelData) levelData: new fields.EmbeddedDataField(DhLevelData),
bonuses: new fields.SchemaField({
damage: new fields.SchemaField({
physical: bonusField('DAGGERHEART.GENERAL.Damage.physicalDamage'),
magical: bonusField('DAGGERHEART.GENERAL.Damage.magicalDamage')
})
})
}; };
} }
get traits() { get proficiency() {
return { return this.partner?.system?.proficiency ?? 1;
instinct: { total: this.attack.roll.bonus }
};
} }
prepareBaseData() { prepareBaseData() {
const partnerSpellcastingModifier = this.partner?.system?.spellcastingModifiers?.main; this.attack.roll.bonus = this.partner?.system?.spellcastModifier ?? 0;
const spellcastingModifier = this.partner?.system?.traits?.[partnerSpellcastingModifier]?.total;
this.attack.roll.bonus = spellcastingModifier ?? 0; // Needs to expand on which modifier it is that should be used because of multiclassing;
for (let levelKey in this.levelData.levelups) { for (let levelKey in this.levelData.levelups) {
const level = this.levelData.levelups[levelKey]; const level = this.levelData.levelups[levelKey];
@ -107,15 +109,15 @@ export default class DhCompanion extends BaseDataActor {
} }
break; break;
case 'stress': case 'stress':
this.resources.stress.bonus += selection.value; this.resources.stress.max += selection.value;
break; break;
case 'evasion': case 'evasion':
this.evasion.bonus += selection.value; this.evasion += selection.value;
break; break;
case 'experience': case 'experience':
Object.keys(this.experiences).forEach(key => { Object.keys(this.experiences).forEach(key => {
const experience = this.experiences[key]; const experience = this.experiences[key];
experience.bonus += selection.value; experience.value += selection.value;
}); });
break; break;
} }
@ -123,20 +125,6 @@ export default class DhCompanion extends BaseDataActor {
} }
} }
prepareDerivedData() {
for (var experienceKey in this.experiences) {
var experience = this.experiences[experienceKey];
experience.total = experience.value + experience.bonus;
}
if (this.partner) {
this.partner.system.resources.hope.max += this.resources.hope;
}
this.resources.stress.maxTotal = this.resources.stress.max + this.resources.stress.bonus;
this.evasion.total = this.evasion.value + this.evasion.bonus;
}
async _preDelete() { async _preDelete() {
if (this.partner) { if (this.partner) {
await this.partner.update({ 'system.companion': null }); await this.partner.update({ 'system.companion': null });

View file

@ -9,20 +9,22 @@ export default class DhEnvironment extends BaseDataActor {
return foundry.utils.mergeObject(super.metadata, { return foundry.utils.mergeObject(super.metadata, {
label: 'TYPES.Actor.environment', label: 'TYPES.Actor.environment',
type: 'environment', type: 'environment',
settingSheet: DHEnvironmentSettings settingSheet: DHEnvironmentSettings,
hasResistances: false
}); });
} }
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
tier: new fields.StringField({ ...super.defineSchema(),
tier: new fields.NumberField({
required: true, required: true,
integer: true,
choices: CONFIG.DH.GENERAL.tiers, choices: CONFIG.DH.GENERAL.tiers,
initial: CONFIG.DH.GENERAL.tiers.tier1.id initial: CONFIG.DH.GENERAL.tiers[1].id
}), }),
type: new fields.StringField({ choices: CONFIG.DH.ACTOR.environmentTypes }), type: new fields.StringField({ choices: CONFIG.DH.ACTOR.environmentTypes }),
description: new fields.StringField(),
impulses: new fields.StringField(), impulses: new fields.StringField(),
difficulty: new fields.NumberField({ required: true, initial: 11, integer: true }), difficulty: new fields.NumberField({ required: true, initial: 11, integer: true }),
potentialAdversaries: new fields.TypedObjectField( potentialAdversaries: new fields.TypedObjectField(

View file

@ -102,7 +102,7 @@ class DhCountdown extends foundry.abstract.DataModel {
value: new fields.StringField({ value: new fields.StringField({
required: true, required: true,
choices: CONFIG.DH.GENERAL.countdownTypes, choices: CONFIG.DH.GENERAL.countdownTypes,
initial: CONFIG.DH.GENERAL.countdownTypes.spotlight.id, initial: CONFIG.DH.GENERAL.countdownTypes.custom.id,
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.type.value.label' label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.type.value.label'
}), }),
label: new fields.StringField({ label: new fields.StringField({
@ -132,7 +132,13 @@ class DhCountdown extends foundry.abstract.DataModel {
export const registerCountdownHooks = () => { export const registerCountdownHooks = () => {
Hooks.on(socketEvent.Refresh, ({ refreshType, application }) => { Hooks.on(socketEvent.Refresh, ({ refreshType, application }) => {
if (refreshType === RefreshType.Countdown) { if (refreshType === RefreshType.Countdown) {
foundry.applications.instances.get(application)?.render(); if (application) {
foundry.applications.instances.get(application)?.render();
} else {
foundry.applications.instances.get('narrative-countdowns')?.render();
foundry.applications.instances.get('encounter-countdowns')?.render();
}
return false; return false;
} }
}); });

View file

@ -0,0 +1,32 @@
const fields = foundry.data.fields;
const attributeField = label =>
new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true, label }),
tierMarked: new fields.BooleanField({ initial: false })
});
const resourceField = (max = 0, label, reverse = false) =>
new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true, label }),
max: new fields.NumberField({ initial: max, integer: true }),
isReversed: new fields.BooleanField({ initial: reverse })
});
const stressDamageReductionRule = localizationPath =>
new fields.SchemaField({
enabled: new fields.BooleanField({ required: true, initial: false }),
cost: new fields.NumberField({
integer: true,
label: `${localizationPath}.label`,
hint: `${localizationPath}.hint`
})
});
const bonusField = label =>
new fields.SchemaField({
bonus: new fields.NumberField({ integer: true, initial: 0, label: `${game.i18n.localize(label)} Value` }),
dice: new fields.ArrayField(new fields.StringField(), { label: `${game.i18n.localize(label)} Dice` })
});
export { attributeField, resourceField, stressDamageReductionRule, bonusField };

View file

@ -1,5 +1,6 @@
import DHAncestry from './ancestry.mjs'; import DHAncestry from './ancestry.mjs';
import DHArmor from './armor.mjs'; import DHArmor from './armor.mjs';
import DHAttachableItem from './attachableItem.mjs';
import DHClass from './class.mjs'; import DHClass from './class.mjs';
import DHCommunity from './community.mjs'; import DHCommunity from './community.mjs';
import DHConsumable from './consumable.mjs'; import DHConsumable from './consumable.mjs';
@ -13,6 +14,7 @@ import DHBeastform from './beastform.mjs';
export { export {
DHAncestry, DHAncestry,
DHArmor, DHArmor,
DHAttachableItem,
DHClass, DHClass,
DHCommunity, DHCommunity,
DHConsumable, DHConsumable,
@ -27,6 +29,7 @@ export {
export const config = { export const config = {
ancestry: DHAncestry, ancestry: DHAncestry,
armor: DHArmor, armor: DHArmor,
attachableItem: DHAttachableItem,
class: DHClass, class: DHClass,
community: DHCommunity, community: DHCommunity,
consumable: DHConsumable, consumable: DHConsumable,

View file

@ -18,4 +18,20 @@ export default class DHAncestry extends BaseDataItem {
features: new ForeignDocumentUUIDArrayField({ type: 'Item' }) features: new ForeignDocumentUUIDArrayField({ type: 'Item' })
}; };
} }
get primaryFeature() {
return (
this.features.find(x => x?.system?.subType === CONFIG.DH.ITEM.featureSubTypes.primary) ??
(this.features.filter(x => !x).length > 0 ? {} : null)
);
}
get secondaryFeature() {
return (
this.features.find(x => x?.system?.subType === CONFIG.DH.ITEM.featureSubTypes.secondary) ??
(this.features.filter(x => !x || x.system.subType === CONFIG.DH.ITEM.featureSubTypes.primary).length > 1
? {}
: null)
);
}
} }

View file

@ -1,15 +1,15 @@
import BaseDataItem from './base.mjs'; import AttachableItem from './attachableItem.mjs';
import ActionField from '../fields/actionField.mjs'; import ActionField from '../fields/actionField.mjs';
import { armorFeatures } from '../../config/itemConfig.mjs'; import { armorFeatures } from '../../config/itemConfig.mjs';
import { actionsTypes } from '../action/_module.mjs';
export default class DHArmor extends BaseDataItem { export default class DHArmor extends AttachableItem {
/** @inheritDoc */ /** @inheritDoc */
static get metadata() { static get metadata() {
return foundry.utils.mergeObject(super.metadata, { return foundry.utils.mergeObject(super.metadata, {
label: 'TYPES.Item.armor', label: 'TYPES.Item.armor',
type: 'armor', type: 'armor',
hasDescription: true, hasDescription: true,
isQuantifiable: true,
isInventoryItem: true isInventoryItem: true
}); });
} }
@ -22,7 +22,7 @@ export default class DHArmor extends BaseDataItem {
tier: new fields.NumberField({ required: true, integer: true, initial: 1, min: 1 }), tier: new fields.NumberField({ required: true, integer: true, initial: 1, min: 1 }),
equipped: new fields.BooleanField({ initial: false }), equipped: new fields.BooleanField({ initial: false }),
baseScore: new fields.NumberField({ integer: true, initial: 0 }), baseScore: new fields.NumberField({ integer: true, initial: 0 }),
features: new fields.ArrayField( armorFeatures: new fields.ArrayField(
new fields.SchemaField({ new fields.SchemaField({
value: new fields.StringField({ value: new fields.StringField({
required: true, required: true,
@ -44,25 +44,28 @@ export default class DHArmor extends BaseDataItem {
}; };
} }
get featureInfo() { get customActions() {
return this.feature ? CONFIG.DH.ITEM.armorFeatures[this.feature] : null; return this.actions.filter(
action => !this.armorFeatures.some(feature => feature.actionIds.includes(action.id))
);
} }
async _preUpdate(changes, options, user) { async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user); const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false; if (allowed === false) return false;
if (changes.system.features) { if (changes.system.armorFeatures) {
const removed = this.features.filter(x => !changes.system.features.includes(x)); const removed = this.armorFeatures.filter(x => !changes.system.armorFeatures.includes(x));
const added = changes.system.features.filter(x => !this.features.includes(x)); const added = changes.system.armorFeatures.filter(x => !this.armorFeatures.includes(x));
const effectIds = [];
const actionIds = [];
for (var feature of removed) { for (var feature of removed) {
for (var effectId of feature.effectIds) { effectIds.push(...feature.effectIds);
await this.parent.effects.get(effectId).delete(); actionIds.push(...feature.actionIds);
}
changes.system.actions = this.actions.filter(x => !feature.actionIds.includes(x._id));
} }
await this.parent.deleteEmbeddedDocuments('ActiveEffect', effectIds);
changes.system.actions = this.actions.filter(x => !actionIds.includes(x._id));
for (var feature of added) { for (var feature of added) {
const featureData = armorFeatures[feature.value]; const featureData = armorFeatures[feature.value];

View file

@ -0,0 +1,160 @@
import BaseDataItem from './base.mjs';
export default class AttachableItem extends BaseDataItem {
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
attached: new fields.ArrayField(new fields.DocumentUUIDField({ type: 'Item', nullable: true }))
};
}
async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false;
// Handle equipped status changes for attachment effects
if (changes.system?.equipped !== undefined && changes.system.equipped !== this.equipped) {
await this.#handleAttachmentEffectsOnEquipChange(changes.system.equipped);
}
}
async #handleAttachmentEffectsOnEquipChange(newEquippedStatus) {
const actor = this.parent.parent?.type === 'character' ? this.parent.parent : this.parent.parent?.parent;
const parentType = this.parent.type;
if (!actor || !this.attached?.length) {
return;
}
if (newEquippedStatus) {
// Item is being equipped - add attachment effects
for (const attachedUuid of this.attached) {
const attachedItem = await fromUuid(attachedUuid);
if (attachedItem && attachedItem.effects.size > 0) {
await this.#copyAttachmentEffectsToActor({
attachedItem,
attachedUuid,
parentType
});
}
}
} else {
// Item is being unequipped - remove attachment effects
await this.#removeAllAttachmentEffects(parentType);
}
}
async #copyAttachmentEffectsToActor({ attachedItem, attachedUuid, parentType }) {
const actor = this.parent.parent;
if (!actor || !attachedItem.effects.size > 0 || !this.equipped) {
return [];
}
const effectsToCreate = [];
for (const effect of attachedItem.effects) {
const effectData = effect.toObject();
effectData.origin = `${this.parent.uuid}:${attachedUuid}`;
const attachmentSource = {
itemUuid: attachedUuid,
originalEffectId: effect.id
};
attachmentSource[`${parentType}Uuid`] = this.parent.uuid;
effectData.flags = {
...effectData.flags,
[CONFIG.DH.id]: {
...effectData.flags?.[CONFIG.DH.id],
[CONFIG.DH.FLAGS.itemAttachmentSource]: attachmentSource
}
};
effectsToCreate.push(effectData);
}
if (effectsToCreate.length > 0) {
return await actor.createEmbeddedDocuments('ActiveEffect', effectsToCreate);
}
return [];
}
async #removeAllAttachmentEffects(parentType) {
const actor = this.parent.parent;
if (!actor) return;
const parentUuidProperty = `${parentType}Uuid`;
const effectsToRemove = actor.effects.filter(effect => {
const attachmentSource = effect.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.itemAttachmentSource);
return attachmentSource && attachmentSource[parentUuidProperty] === this.parent.uuid;
});
if (effectsToRemove.length > 0) {
await actor.deleteEmbeddedDocuments(
'ActiveEffect',
effectsToRemove.map(e => e.id)
);
}
}
/**
* Public method for adding an attachment
*/
async addAttachment(droppedItem) {
const newUUID = droppedItem.uuid;
if (this.attached.includes(newUUID)) {
ui.notifications.warn(`${droppedItem.name} is already attached to this ${this.parent.type}.`);
return;
}
const updatedAttached = [...this.attached, newUUID];
await this.parent.update({
'system.attached': updatedAttached
});
// Copy effects if equipped
if (this.equipped && droppedItem.effects.size > 0) {
await this.#copyAttachmentEffectsToActor({
attachedItem: droppedItem,
attachedUuid: newUUID,
parentType: this.parent.type
});
}
}
/**
* Public method for removing an attachment
*/
async removeAttachment(attachedUuid) {
await this.parent.update({
'system.attached': this.attached.filter(uuid => uuid !== attachedUuid)
});
// Remove effects
await this.#removeAttachmentEffects(attachedUuid);
}
async #removeAttachmentEffects(attachedUuid) {
const actor = this.parent.parent;
if (!actor) return;
const parentType = this.parent.type;
const parentUuidProperty = `${parentType}Uuid`;
const effectsToRemove = actor.effects.filter(effect => {
const attachmentSource = effect.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.itemAttachmentSource);
return (
attachmentSource &&
attachmentSource[parentUuidProperty] === this.parent.uuid &&
attachmentSource.itemUuid === attachedUuid
);
});
if (effectsToRemove.length > 0) {
await actor.deleteEmbeddedDocuments(
'ActiveEffect',
effectsToRemove.map(e => e.id)
);
}
}
}

View file

@ -11,12 +11,15 @@
const fields = foundry.data.fields; const fields = foundry.data.fields;
export default class BaseDataItem extends foundry.abstract.TypeDataModel { export default class BaseDataItem extends foundry.abstract.TypeDataModel {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ITEMS'];
/** @returns {ItemDataModelMetadata}*/ /** @returns {ItemDataModelMetadata}*/
static get metadata() { static get metadata() {
return { return {
label: 'Base Item', label: 'Base Item',
type: 'base', type: 'base',
hasDescription: false, hasDescription: false,
hasResource: false,
isQuantifiable: false, isQuantifiable: false,
isInventoryItem: false isInventoryItem: false
}; };
@ -33,6 +36,36 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
if (this.metadata.hasDescription) schema.description = new fields.HTMLField({ required: true, nullable: true }); if (this.metadata.hasDescription) schema.description = new fields.HTMLField({ required: true, nullable: true });
if (this.metadata.hasResource) {
schema.resource = new fields.SchemaField(
{
type: new fields.StringField({
choices: CONFIG.DH.ITEM.itemResourceTypes,
initial: CONFIG.DH.ITEM.itemResourceTypes.simple
}),
value: new fields.NumberField({ integer: true, min: 0, initial: 0 }),
max: new fields.StringField({ nullable: true, initial: null }),
icon: new fields.StringField(),
recovery: new fields.StringField({
choices: CONFIG.DH.GENERAL.refreshTypes,
initial: null,
nullable: true
}),
diceStates: new fields.TypedObjectField(
new fields.SchemaField({
value: new fields.NumberField({ integer: true, initial: 1, min: 1 }),
used: new fields.BooleanField({ initial: false })
})
),
dieFaces: new fields.StringField({
choices: CONFIG.DH.GENERAL.diceTypes,
initial: CONFIG.DH.GENERAL.diceTypes.d4
})
},
{ nullable: true, initial: null }
);
}
if (this.metadata.isQuantifiable) if (this.metadata.isQuantifiable)
schema.quantity = new fields.NumberField({ integer: true, initial: 1, min: 0, required: true }); schema.quantity = new fields.NumberField({ integer: true, initial: 1, min: 0, required: true });
@ -62,28 +95,27 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
return data; return data;
} }
/**@inheritdoc */
async _preCreate(data, options, user) { async _preCreate(data, options, user) {
// Skip if no initial action is required or actions already exist // Skip if no initial action is required or actions already exist
if (!this.metadata.hasInitialAction || !foundry.utils.isEmpty(this.actions)) return; if (this.metadata.hasInitialAction && foundry.utils.isEmpty(this.actions)) {
const metadataType = this.metadata.type;
const actionType = { weapon: 'attack' }[metadataType];
const ActionClass = game.system.api.models.actions.actionsTypes[actionType];
const metadataType = this.metadata.type; const action = new ActionClass(
const actionType = { weapon: 'attack' }[metadataType]; {
const ActionClass = game.system.api.models.actions.actionsTypes[actionType]; _id: foundry.utils.randomID(),
type: actionType,
name: game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[actionType].name),
...ActionClass.getSourceConfig(this.parent)
},
{
parent: this.parent
}
);
const action = new ActionClass( this.updateSource({ actions: [action] });
{ }
_id: foundry.utils.randomID(),
type: actionType,
name: game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[actionType].name),
...ActionClass.getSourceConfig(this.parent)
},
{
parent: this.parent
}
);
this.updateSource({ actions: [action] });
} }
_onCreate(data) { _onCreate(data) {
@ -95,7 +127,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
...feature, ...feature,
system: { system: {
...feature.system, ...feature.system,
type: this.parent.type, originItemType: this.parent.type,
originId: data._id, originId: data._id,
identifier: feature.identifier identifier: feature.identifier
} }

View file

@ -3,7 +3,7 @@ import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayFie
import BaseDataItem from './base.mjs'; import BaseDataItem from './base.mjs';
export default class DHBeastform extends BaseDataItem { export default class DHBeastform extends BaseDataItem {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.Sheets.Beastform']; static LOCALIZATION_PREFIXES = ['DAGGERHEART.ITEMS.Beastform'];
/** @inheritDoc */ /** @inheritDoc */
static get metadata() { static get metadata() {
@ -19,23 +19,65 @@ export default class DHBeastform extends BaseDataItem {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
...super.defineSchema(), ...super.defineSchema(),
tier: new fields.StringField({ beastformType: new fields.StringField({
required: true, required: true,
choices: CONFIG.DH.ITEM.beastformTypes,
initial: CONFIG.DH.ITEM.beastformTypes.normal.id
}),
tier: new fields.NumberField({
required: true,
integer: true,
choices: CONFIG.DH.GENERAL.tiers, choices: CONFIG.DH.GENERAL.tiers,
initial: CONFIG.DH.GENERAL.tiers.tier1.id initial: CONFIG.DH.GENERAL.tiers[1].id
}), }),
tokenImg: new fields.FilePathField({ tokenImg: new fields.FilePathField({
initial: 'icons/svg/mystery-man.svg', initial: 'icons/svg/mystery-man.svg',
categories: ['IMAGE'], categories: ['IMAGE'],
base64: false base64: false
}), }),
tokenRingImg: new fields.FilePathField({
initial: 'icons/svg/mystery-man.svg',
categories: ['IMAGE'],
base64: false
}),
tokenSize: new fields.SchemaField({ tokenSize: new fields.SchemaField({
height: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }), height: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }),
width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }) width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true })
}), }),
mainTrait: new fields.StringField({
required: true,
choices: CONFIG.DH.ACTOR.abilities,
initial: CONFIG.DH.ACTOR.abilities.agility.id
}),
examples: new fields.StringField(), examples: new fields.StringField(),
advantageOn: new fields.ArrayField(new fields.StringField()), advantageOn: new fields.TypedObjectField(
features: new ForeignDocumentUUIDArrayField({ type: 'Item' }) new fields.SchemaField({
value: new fields.StringField()
})
),
features: new ForeignDocumentUUIDArrayField({ type: 'Item' }),
evolved: new fields.SchemaField({
maximumTier: new fields.NumberField({
integer: true,
choices: CONFIG.DH.GENERAL.tiers
}),
mainTraitBonus: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 0
})
}),
hybrid: new fields.SchemaField({
maximumTier: new fields.NumberField({
integer: true,
choices: CONFIG.DH.GENERAL.tiers,
label: 'DAGGERHEART.ITEMS.Beastform.FIELDS.evolved.maximumTier.label'
}),
beastformOptions: new fields.NumberField({ required: true, integer: true, initial: 2, min: 2 }),
advantages: new fields.NumberField({ required: true, integer: true, initial: 2, min: 2 }),
features: new fields.NumberField({ required: true, integer: true, initial: 2, min: 2 })
})
}; };
} }
@ -56,40 +98,64 @@ export default class DHBeastform extends BaseDataItem {
'Item', 'Item',
this.features.map(x => x.toObject()) this.features.map(x => x.toObject())
); );
const effects = await this.parent.parent.createEmbeddedDocuments(
const extraEffects = await this.parent.parent.createEmbeddedDocuments(
'ActiveEffect', 'ActiveEffect',
this.parent.effects.map(x => x.toObject()) this.parent.effects.filter(x => x.type !== 'beastform').map(x => x.toObject())
); );
await this.parent.parent.createEmbeddedDocuments('ActiveEffect', [ const beastformEffect = this.parent.effects.find(x => x.type === 'beastform');
{ await beastformEffect.updateSource({
type: 'beastform', changes: [
name: game.i18n.localize('DAGGERHEART.ITEMS.Beastform.beastformEffect'), ...beastformEffect.changes,
img: 'icons/creatures/abilities/paw-print-pair-purple.webp', {
system: { key: 'system.advantageSources',
isBeastform: true, mode: 2,
characterTokenData: { value: Object.values(this.advantageOn)
tokenImg: this.parent.parent.prototypeToken.texture.src, .map(x => x.value)
tokenSize: { .join(', ')
height: this.parent.parent.prototypeToken.height,
width: this.parent.parent.prototypeToken.width
}
},
advantageOn: this.advantageOn,
featureIds: features.map(x => x.id),
effectIds: effects.map(x => x.id)
} }
],
system: {
characterTokenData: {
tokenImg: this.parent.parent.prototypeToken.texture.src,
tokenRingImg: this.parent.parent.prototypeToken.ring.subject.texture,
tokenSize: {
height: this.parent.parent.prototypeToken.height,
width: this.parent.parent.prototypeToken.width
}
},
advantageOn: this.advantageOn,
featureIds: features.map(x => x.id),
effectIds: extraEffects.map(x => x.id)
} }
]); });
await this.parent.parent.createEmbeddedDocuments('ActiveEffect', [beastformEffect.toObject()]);
await updateActorTokens(this.parent.parent, { await updateActorTokens(this.parent.parent, {
height: this.tokenSize.height, height: this.tokenSize.height,
width: this.tokenSize.width, width: this.tokenSize.width,
texture: { texture: {
src: this.tokenImg src: this.tokenImg
},
ring: {
subject: {
texture: this.tokenRingImg
}
} }
}); });
return false; return false;
} }
_onCreate() {
this.parent.createEmbeddedDocuments('ActiveEffect', [
{
type: 'beastform',
name: game.i18n.localize('DAGGERHEART.ITEMS.Beastform.beastformEffect'),
img: 'icons/creatures/abilities/paw-print-pair-purple.webp'
}
]);
}
} }

View file

@ -24,11 +24,10 @@ export default class DHClass extends BaseDataItem {
integer: true, integer: true,
min: 1, min: 1,
initial: 5, initial: 5,
label: 'DAGGERHEART.GENERAL.hitPoints' label: 'DAGGERHEART.GENERAL.HitPoints.plural'
}), }),
evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }), evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }),
hopeFeatures: new ForeignDocumentUUIDArrayField({ type: 'Item' }), features: new ForeignDocumentUUIDArrayField({ type: 'Item' }),
classFeatures: new ForeignDocumentUUIDArrayField({ type: 'Item' }),
subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
inventory: new fields.SchemaField({ inventory: new fields.SchemaField({
take: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), take: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
@ -52,12 +51,18 @@ export default class DHClass extends BaseDataItem {
}; };
} }
get hopeFeature() { get hopeFeatures() {
return this.hopeFeatures.length > 0 ? this.hopeFeatures[0] : null; return (
this.features.filter(x => x?.system?.subType === CONFIG.DH.ITEM.featureSubTypes.hope) ??
(this.features.filter(x => !x).length > 0 ? {} : null)
);
} }
get features() { get classFeatures() {
return [...this.hopeFeatures.filter(x => x), ...this.classFeatures.filter(x => x)]; return (
this.features.filter(x => x?.system?.subType === CONFIG.DH.ITEM.featureSubTypes.class) ??
(this.features.filter(x => !x).length > 0 ? {} : null)
);
} }
async _preCreate(data, options, user) { async _preCreate(data, options, user) {

View file

@ -7,7 +7,8 @@ export default class DHDomainCard extends BaseDataItem {
return foundry.utils.mergeObject(super.metadata, { return foundry.utils.mergeObject(super.metadata, {
label: 'TYPES.Item.domainCard', label: 'TYPES.Item.domainCard',
type: 'domainCard', type: 'domainCard',
hasDescription: true hasDescription: true,
hasResource: true
}); });
} }
@ -28,7 +29,6 @@ export default class DHDomainCard extends BaseDataItem {
required: true, required: true,
initial: CONFIG.DH.DOMAIN.cardTypes.ability.id initial: CONFIG.DH.DOMAIN.cardTypes.ability.id
}), }),
foundation: new fields.BooleanField({ initial: false }),
inVault: new fields.BooleanField({ initial: false }), inVault: new fields.BooleanField({ initial: false }),
actions: new fields.ArrayField(new ActionField()) actions: new fields.ArrayField(new ActionField())
}; };

View file

@ -7,7 +7,8 @@ export default class DHFeature extends BaseDataItem {
return foundry.utils.mergeObject(super.metadata, { return foundry.utils.mergeObject(super.metadata, {
label: 'TYPES.Item.feature', label: 'TYPES.Item.feature',
type: 'feature', type: 'feature',
hasDescription: true hasDescription: true,
hasResource: true
}); });
} }
@ -16,10 +17,33 @@ export default class DHFeature extends BaseDataItem {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
...super.defineSchema(), ...super.defineSchema(),
type: new fields.StringField({ choices: CONFIG.DH.ITEM.featureTypes, nullable: true, initial: null }), originItemType: new fields.StringField({
choices: CONFIG.DH.ITEM.featureTypes,
nullable: true,
initial: null
}),
subType: new fields.StringField({ choices: CONFIG.DH.ITEM.featureSubTypes, nullable: true, initial: null }),
originId: new fields.StringField({ nullable: true, initial: null }), originId: new fields.StringField({ nullable: true, initial: null }),
identifier: new fields.StringField(), identifier: new fields.StringField(),
actions: new fields.ArrayField(new ActionField()) actions: new fields.ArrayField(new ActionField())
}; };
} }
get spellcastingModifier() {
let traitValue = 0;
if (this.actor && this.originId && ['class', 'subclass'].includes(this.originItemType)) {
if (this.originItemType === 'subclass') {
traitValue =
this.actor.system.traits[this.actor.items.get(this.originId).system.spellcastingTrait]?.value ?? 0;
} else {
const subclass =
this.actor.system.multiclass.value?.id === this.originId
? this.actor.system.multiclass.subclass
: this.actor.system.class.subclass;
traitValue = this.actor.system.traits[subclass.system.spellcastingTrait]?.value ?? 0;
}
}
return traitValue;
}
} }

View file

@ -1,4 +1,4 @@
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
import BaseDataItem from './base.mjs'; import BaseDataItem from './base.mjs';
export default class DHSubclass extends BaseDataItem { export default class DHSubclass extends BaseDataItem {
@ -22,20 +22,22 @@ export default class DHSubclass extends BaseDataItem {
nullable: true, nullable: true,
initial: null initial: null
}), }),
foundationFeature: new ForeignDocumentUUIDField({ type: 'Item' }), features: new ForeignDocumentUUIDArrayField({ type: 'Item' }),
specializationFeature: new ForeignDocumentUUIDField({ type: 'Item' }),
masteryFeature: new ForeignDocumentUUIDField({ type: 'Item' }),
featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }), featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }),
isMulticlass: new fields.BooleanField({ initial: false }) isMulticlass: new fields.BooleanField({ initial: false })
}; };
} }
get features() { get foundationFeatures() {
return [ return this.features.filter(x => x.system.subType === CONFIG.DH.ITEM.featureSubTypes.foundation);
{ ...this.foundationFeature.toObject(), identifier: 'foundationFeature' }, }
{ ...this.specializationFeature.toObject(), identifier: 'specializationFeature' },
{ ...this.masteryFeature.toObject(), identifier: 'masteryFeature' } get specializationFeatures() {
]; return this.features.filter(x => x.system.subType === CONFIG.DH.ITEM.featureSubTypes.specialization);
}
get masteryFeatures() {
return this.features.filter(x => x.system.subType === CONFIG.DH.ITEM.featureSubTypes.mastery);
} }
async _preCreate(data, options, user) { async _preCreate(data, options, user) {

View file

@ -1,16 +1,15 @@
import BaseDataItem from './base.mjs'; import AttachableItem from './attachableItem.mjs';
import { actionsTypes } from '../action/_module.mjs'; import { actionsTypes } from '../action/_module.mjs';
import ActionField from '../fields/actionField.mjs'; import ActionField from '../fields/actionField.mjs';
export default class DHWeapon extends BaseDataItem { export default class DHWeapon extends AttachableItem {
/** @inheritDoc */ /** @inheritDoc */
static get metadata() { static get metadata() {
return foundry.utils.mergeObject(super.metadata, { return foundry.utils.mergeObject(super.metadata, {
label: 'TYPES.Item.weapon', label: 'TYPES.Item.weapon',
type: 'weapon', type: 'weapon',
hasDescription: true, hasDescription: true,
isQuantifiable: true, isInventoryItem: true
isInventoryItem: true,
// hasInitialAction: true // hasInitialAction: true
}); });
} }
@ -26,8 +25,7 @@ export default class DHWeapon extends BaseDataItem {
//SETTINGS //SETTINGS
secondary: new fields.BooleanField({ initial: false }), secondary: new fields.BooleanField({ initial: false }),
burden: new fields.StringField({ required: true, choices: CONFIG.DH.GENERAL.burden, initial: 'oneHanded' }), burden: new fields.StringField({ required: true, choices: CONFIG.DH.GENERAL.burden, initial: 'oneHanded' }),
weaponFeatures: new fields.ArrayField(
features: new fields.ArrayField(
new fields.SchemaField({ new fields.SchemaField({
value: new fields.StringField({ value: new fields.StringField({
required: true, required: true,
@ -52,14 +50,15 @@ export default class DHWeapon extends BaseDataItem {
}, },
roll: { roll: {
trait: 'agility', trait: 'agility',
type: 'weapon' type: 'attack'
}, },
damage: { damage: {
parts: [ parts: [
{ {
type: ['physical'],
value: { value: {
multiplier: 'prof', multiplier: 'prof',
dice: "d8" dice: 'd8'
} }
} }
] ]
@ -74,22 +73,30 @@ export default class DHWeapon extends BaseDataItem {
return [this.attack, ...this.actions]; return [this.attack, ...this.actions];
} }
get customActions() {
return this.actions.filter(
action => !this.weaponFeatures.some(feature => feature.actionIds.includes(action.id))
);
}
async _preUpdate(changes, options, user) { async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user); const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false; if (allowed === false) return false;
if (changes.system?.features) { if (changes.system?.weaponFeatures) {
const removed = this.features.filter(x => !changes.system.features.includes(x)); const removed = this.weaponFeatures.filter(x => !changes.system.weaponFeatures.includes(x));
const added = changes.system.features.filter(x => !this.features.includes(x)); const added = changes.system.weaponFeatures.filter(x => !this.weaponFeatures.includes(x));
const removedEffectsUpdate = [];
const removedActionsUpdate = [];
for (let weaponFeature of removed) { for (let weaponFeature of removed) {
for (var effectId of weaponFeature.effectIds) { removedEffectsUpdate.push(...weaponFeature.effectIds);
await this.parent.effects.get(effectId).delete(); removedActionsUpdate.push(...weaponFeature.actionIds);
}
changes.system.actions = this.actions.filter(x => !weaponFeature.actionIds.includes(x._id));
} }
await this.parent.deleteEmbeddedDocuments('ActiveEffect', removedEffectsUpdate);
changes.system.actions = this.actions.filter(x => !removedActionsUpdate.includes(x._id));
for (let weaponFeature of added) { for (let weaponFeature of added) {
const featureData = CONFIG.DH.ITEM.weaponFeatures[weaponFeature.value]; const featureData = CONFIG.DH.ITEM.weaponFeatures[weaponFeature.value];
if (featureData.effects?.length > 0) { if (featureData.effects?.length > 0) {
@ -102,17 +109,37 @@ export default class DHWeapon extends BaseDataItem {
]); ]);
weaponFeature.effectIds = embeddedItems.map(x => x.id); weaponFeature.effectIds = embeddedItems.map(x => x.id);
} }
const newActions = [];
if (featureData.actions?.length > 0) { if (featureData.actions?.length > 0) {
const newActions = featureData.actions.map(action => { for (let action of featureData.actions) {
const cls = actionsTypes[action.type]; const embeddedEffects = await this.parent.createEmbeddedDocuments(
return new cls( 'ActiveEffect',
{ ...action, _id: foundry.utils.randomID(), name: game.i18n.localize(action.name) }, (action.effects ?? []).map(effect => ({
{ parent: this } ...effect,
transfer: false,
name: game.i18n.localize(effect.name),
description: game.i18n.localize(effect.description)
}))
); );
}); const cls = actionsTypes[action.type];
changes.system.actions = [...this.actions, ...newActions]; newActions.push(
weaponFeature.actionIds = newActions.map(x => x._id); new cls(
{
...action,
_id: foundry.utils.randomID(),
name: game.i18n.localize(action.name),
description: game.i18n.localize(action.description),
effects: embeddedEffects.map(x => ({ _id: x.id }))
},
{ parent: this }
)
);
}
} }
changes.system.actions = [...this.actions, ...newActions];
weaponFeature.actionIds = newActions.map(x => x._id);
} }
} }
} }

View file

@ -43,7 +43,12 @@ export default class DhLevelData extends foundry.abstract.DataModel {
data: new fields.ArrayField(new fields.StringField({ required: true })), data: new fields.ArrayField(new fields.StringField({ required: true })),
secondaryData: new fields.TypedObjectField(new fields.StringField({ required: true })), secondaryData: new fields.TypedObjectField(new fields.StringField({ required: true })),
itemUuid: new fields.DocumentUUIDField({ required: true }), itemUuid: new fields.DocumentUUIDField({ required: true }),
featureIds: new fields.ArrayField(new fields.StringField()) features: new fields.ArrayField(
new fields.SchemaField({
onPartner: new fields.BooleanField(),
id: new fields.StringField()
})
)
}) })
) )
}) })
@ -51,10 +56,6 @@ export default class DhLevelData extends foundry.abstract.DataModel {
}; };
} }
get actions() {
return Object.values(this.levelups).flatMap(level => level.selections.flatMap(s => s.actions));
}
get canLevelUp() { get canLevelUp() {
return this.level.current < this.level.changed; return this.level.current < this.level.changed;
} }

View file

@ -70,7 +70,8 @@ export const CompanionLevelOptionType = {
{ {
name: 'DAGGERHEART.APPLICATIONS.Levelup.actions.creatureComfort.name', name: 'DAGGERHEART.APPLICATIONS.Levelup.actions.creatureComfort.name',
img: 'icons/magic/life/heart-cross-purple-orange.webp', img: 'icons/magic/life/heart-cross-purple-orange.webp',
description: 'DAGGERHEART.APPLICATIONS.Levelup.actions.creatureComfort.description' description: 'DAGGERHEART.APPLICATIONS.Levelup.actions.creatureComfort.description',
toPartner: true
} }
] ]
}, },
@ -81,7 +82,8 @@ export const CompanionLevelOptionType = {
{ {
name: 'DAGGERHEART.APPLICATIONS.Levelup.actions.armored.name', name: 'DAGGERHEART.APPLICATIONS.Levelup.actions.armored.name',
img: 'icons/equipment/shield/kite-wooden-oak-glow.webp', img: 'icons/equipment/shield/kite-wooden-oak-glow.webp',
description: 'DAGGERHEART.APPLICATIONS.Levelup.actions.armored.description' description: 'DAGGERHEART.APPLICATIONS.Levelup.actions.armored.description',
toPartner: true
} }
] ]
}, },
@ -100,7 +102,8 @@ export const CompanionLevelOptionType = {
{ {
name: 'DAGGERHEART.APPLICATIONS.Levelup.actions.bonded.name', name: 'DAGGERHEART.APPLICATIONS.Levelup.actions.bonded.name',
img: 'icons/magic/life/heart-red-blue.webp', img: 'icons/magic/life/heart-red-blue.webp',
description: 'DAGGERHEART.APPLICATIONS.Levelup.actions.bonded.description' description: 'DAGGERHEART.APPLICATIONS.Levelup.actions.bonded.description',
toPartner: true
} }
] ]
}, },

View file

@ -40,6 +40,10 @@ export default class DhAppearance extends foundry.abstract.DataModel {
outline: new fields.ColorField({ required: true, initial: '#ffffff' }), outline: new fields.ColorField({ required: true, initial: '#ffffff' }),
edge: new fields.ColorField({ required: true, initial: '#000000' }) edge: new fields.ColorField({ required: true, initial: '#000000' })
}) })
}),
showGenericStatusEffects: new fields.BooleanField({
initial: true,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.showGenericStatusEffects.label'
}) })
}; };
} }

View file

@ -1,23 +1,28 @@
export default class DhAutomation extends foundry.abstract.DataModel { export default class DhAutomation extends foundry.abstract.DataModel {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.SETTINGS.Automation']; // Doesn't work for some reason
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
hope: new fields.BooleanField({ hopeFear: new fields.SchemaField({
required: true, gm: new fields.BooleanField({
initial: false, required: true,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hope.label' initial: false,
}), // Label need to be updated into something like "Duality Roll Auto Gain" + a hint label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hopeFear.gm.label'
}),
players: new fields.BooleanField({
required: true,
initial: false,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hopeFear.players.label'
})
}),
actionPoints: new fields.BooleanField({ actionPoints: new fields.BooleanField({
required: true, required: true,
initial: false, initial: false,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.actionPoints.label' label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.actionPoints.label'
}), }),
countdowns: new fields.BooleanField({ hordeDamage: new fields.BooleanField({
requireD: true, required: true,
initial: false, initial: true,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.countdowns.label' label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hordeDamage.label'
}) })
}; };
} }

Some files were not shown because too many files have changed in this diff Show more