Initial commit

This commit is contained in:
WBHarry 2025-05-22 16:53:39 +02:00
commit aa4021d1a2
163 changed files with 26530 additions and 0 deletions

51
.github/workflows/deploy.yml vendored Normal file
View file

@ -0,0 +1,51 @@
name: Release Creation
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Dependencies
run: npm ci
- name: Build Packs
run: npm run pullYMLtoLDB
# get part of the tag after the `v`
- name: Extract tag version number
id: get_version
uses: battila7/get-version-action@v2
# Substitute the Manifest and Download URLs in the module.json
- name: Substitute Manifest and Download Links For Versioned Ones
id: sub_manifest_link_version
uses: microsoft/variable-substitution@v1
with:
files: 'system.json'
env:
version: ${{steps.get_version.outputs.version-without-v}}
url: https://github.com/${{github.repository}}
manifest: https://github.com/${{github.repository}}/releases/latest/download/system.json
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
- run: zip -r ./system.zip system.json README.md LICENSE daggerheart.mjs templates/ styles/daggerheart.css packs/ lang/
# Create a release for this specific version
- name: Update Release with Files
id: create_version_release
uses: ncipollo/release-action@v1
with:
allowUpdates: true # Set this to false if you want to prevent updating existing releases
name: ${{ github.event.release.name }}
draft: ${{ github.event.release.unpublished }}
prerelease: ${{ github.event.release.prerelease }}
token: ${{ secrets.GITHUB_TOKEN }}
artifacts: './module.json, ./module.zip'
tag: ${{ github.event.release.tag_name }}
body: ${{ github.event.release.body }}

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.vscode
node_modules
/packs
Build

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 WBHarry
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 476 476" xml:space="preserve">
<g>
<path d="M442.138,11.508c-1.107-4.625-4.342-8.45-8.718-10.312s-9.375-1.537-13.476,0.875c-53.767,31.626-120.569,31.628-174.34,0
c-4.692-2.762-10.516-2.762-15.21,0c-53.768,31.627-120.571,31.627-174.339,0c-4.099-2.412-9.099-2.734-13.475-0.875
c-4.376,1.861-7.611,5.687-8.718,10.312C-4.815,173.094,55.859,342.213,188.436,442.359l40.522,30.61
c2.675,2.021,5.857,3.031,9.041,3.031c3.183,0,6.367-1.011,9.042-3.031l40.522-30.61
C420.141,342.213,480.815,173.094,442.138,11.508z M269.482,418.421L238,442.201l-31.481-23.78
C88.624,329.366,31.987,181.539,59.004,37.407C115.969,63.545,182.278,61.805,238,32.185c55.724,29.62,122.03,31.361,178.996,5.222
C444.013,181.539,387.376,329.366,269.482,418.421z"/>
<path d="M397.194,71.145c-2.409-1.708-5.451-2.265-8.304-1.519c-18.303,4.766-37.182,7.183-56.114,7.183
c-30.958,0-62.325-6.672-90.712-19.296c-2.588-1.15-5.54-1.15-8.127,0c-28.386,12.624-59.753,19.296-90.711,19.296
c-18.933,0-37.813-2.417-56.115-7.183c-2.857-0.745-5.896-0.188-8.303,1.519s-3.938,4.391-4.183,7.332
c-4.871,58.737,4.658,118.513,27.557,172.864c25.044,59.444,65.5,111.425,116.995,150.323l12.797,9.666
c1.783,1.347,3.905,2.021,6.027,2.021s4.244-0.674,6.027-2.021l12.796-9.666c51.496-38.899,91.952-90.881,116.996-150.323
c22.898-54.352,32.428-114.127,27.557-172.864C401.132,75.535,399.602,72.852,397.194,71.145z M355.388,243.575
c-23.678,56.202-61.93,105.351-110.619,142.13L238,390.819l-6.77-5.113c-48.688-36.778-86.94-85.926-110.619-142.13
c-20.146-47.818-29.348-100.077-26.819-151.852c16.242,3.377,32.819,5.085,49.433,5.085c32.266,0,64.934-6.648,94.775-19.259
c29.842,12.61,62.51,19.259,94.775,19.259c16.613,0,33.19-1.708,49.433-5.085C384.737,143.498,375.535,195.757,355.388,243.575z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
assets/DaggerheartLogo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

140
daggerheart.mjs Normal file
View file

@ -0,0 +1,140 @@
import { SYSTEM } from './module/config/system.mjs';
import * as applications from './module/applications/_module.mjs';
import * as models from './module/data/_module.mjs';
import * as documents from './module/documents/_module.mjs';
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
import DhpCombatTracker from './module/ui/combatTracker.mjs';
import { GMUpdateEvent, handleSocketEvent, socketEvent } from './module/helpers/socket.mjs';
import { registerDHPSettings } from './module/applications/settings.mjs';
import DhpChatLog from './module/ui/chatLog.mjs';
import DhpPlayers from './module/ui/players.mjs';
import DhpRuler from './module/ui/ruler.mjs';
globalThis.SYSTEM = SYSTEM;
Hooks.once('init', () => {
CONFIG.daggerheart = SYSTEM;
game.system.api = {
applications,
models,
documents,
}
CONFIG.statusEffects = Object.values(SYSTEM.GENERAL.conditions).map(x => ({ ...x, name: game.i18n.localize(x.name) }));
CONFIG.Item.documentClass = documents.DhpItem;
CONFIG.Item.dataModels = {
ancestry: models.DhpAncestry,
community: models.DhpCommunity,
class: models.DhpClass,
subclass: models.DhpSubclass,
feature: models.DhpFeature,
domainCard: models.DhpDomainCard,
miscellaneous: models.DhpMiscellaneous,
consumable: models.DhpConsumable,
weapon: models.DhpWeapon,
armor: models.DhpArmor,
};
const { Items, Actors } = foundry.documents.collections;
Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet);
Items.registerSheet(SYSTEM.id, applications.DhpAncestry, {types: ["ancestry"], makeDefault: true});
Items.registerSheet(SYSTEM.id, applications.DhpCommunity, {types: ["community"], makeDefault: true});
Items.registerSheet(SYSTEM.id, applications.DhpClassSheet, {types: ["class"], makeDefault: true});
Items.registerSheet(SYSTEM.id, applications.DhpSubclass, {types: ["subclass"], makeDefault: true});
Items.registerSheet(SYSTEM.id, applications.DhpFeatureSheet, {types: ["feature"], makeDefault: true});
Items.registerSheet(SYSTEM.id, applications.DhpDomainCardSheet, {types: ["domainCard"], makeDefault: true});
Items.registerSheet(SYSTEM.id, applications.DhpMiscellaneous, {types: ["miscellaneous"], makeDefault: true});
Items.registerSheet(SYSTEM.id, applications.DhpConsumable, {types: ["consumable"], makeDefault: true});
Items.registerSheet(SYSTEM.id, applications.DhpWeapon, {types: ["weapon"], makeDefault: true});
Items.registerSheet(SYSTEM.id, applications.DhpArmor, {types: ["armor"], makeDefault: true});
CONFIG.Actor.documentClass = documents.DhpActor;
CONFIG.Actor.dataModels = {
pc: models.DhpPC,
adversary: models.DhpAdversary,
environment: models.DhpEnvironment,
};
Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet);
Actors.registerSheet(SYSTEM.id, applications.DhpPCSheet, {types: ["pc"], makeDefault: true});
Actors.registerSheet(SYSTEM.id, applications.DhpAdversarySheet, {types: ["adversary"], makeDefault: true});
Actors.registerSheet(SYSTEM.id, applications.DhpEnvironment, {types: ["environment"], makeDefault: true});
CONFIG.Combat.dataModels = {
base: models.DhpCombat,
};
CONFIG.Combatant.dataModels = {
base: models.DhpCombatant,
};
CONFIG.ChatMessage.dataModels ={
dualityRoll: models.DhpDualityRoll,
adversaryRoll: models.DhpAdversaryRoll,
abilityUse: models.DhpAbilityUse,
};
CONFIG.ChatMessage.documentClass = applications.DhpChatMessage;
CONFIG.Canvas.rulerClass = DhpRuler;
CONFIG.Combat.documentClass = documents.DhpCombat;
CONFIG.ui.combat = DhpCombatTracker;
CONFIG.ui.chat = DhpChatLog;
CONFIG.ui.players = DhpPlayers;
game.socket.on(`system.${SYSTEM.id}`, handleSocketEvent);
registerDHPSettings();
RegisterHandlebarsHelpers.registerHelpers();
return preloadHandlebarsTemplates();
});
Hooks.once('dicesoniceready', () => {
});
Hooks.on(socketEvent.GMUpdate, async (action, uuid, update) => {
if(game.user.isGM){
const document = uuid ? await fromUuid(uuid) : null;
switch(action){
case GMUpdateEvent.UpdateDocument:
if(document && update){
await document.update(update);
}
break;
case GMUpdateEvent.UpdateFear:
if(game.user.isGM){
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, Math.max(Math.min(update, 6), 0));
Hooks.callAll(socketEvent.DhpFearUpdate);
await game.socket.emit(`system.${SYSTEM.id}`, { action: socketEvent.DhpFearUpdate });
}
break;
}
}
});
const preloadHandlebarsTemplates = async function() {
return foundry.applications.handlebars.loadTemplates([
"systems/daggerheart/templates/sheets/parts/attributes.hbs",
"systems/daggerheart/templates/sheets/parts/defense.hbs",
"systems/daggerheart/templates/sheets/parts/armor.hbs",
"systems/daggerheart/templates/sheets/parts/experience.hbs",
"systems/daggerheart/templates/sheets/parts/features.hbs",
"systems/daggerheart/templates/sheets/parts/gold.hbs",
"systems/daggerheart/templates/sheets/parts/health.hbs",
"systems/daggerheart/templates/sheets/parts/hope.hbs",
"systems/daggerheart/templates/sheets/parts/inventory.hbs",
"systems/daggerheart/templates/sheets/parts/weapons.hbs",
"systems/daggerheart/templates/sheets/parts/domainCard.hbs",
"systems/daggerheart/templates/sheets/parts/heritage.hbs",
"systems/daggerheart/templates/sheets/parts/subclassFeature.hbs",
"systems/daggerheart/templates/sheets/parts/effects.hbs",
"systems/daggerheart/templates/sheets/pc/sections/inventory.hbs",
"systems/daggerheart/templates/sheets/pc/sections/loadout.hbs",
"systems/daggerheart/templates/sheets/pc/parts/heritageCard.hbs",
"systems/daggerheart/templates/sheets/pc/parts/advancementCard.hbs",
"systems/daggerheart/templates/views/parts/level.hbs",
"systems/daggerheart/templates/components/slider.hbs",
]);
};

21
gulpfile.js Normal file
View file

@ -0,0 +1,21 @@
// Less configuration
var gulp = require('gulp');
var less = require('gulp-less');
gulp.task('less', function(cb) {
gulp
.src('styles/daggerheart.less')
.pipe(less())
.pipe(
gulp.dest("styles")
);
cb();
});
gulp.task(
'default',
gulp.series('less', function(cb) {
gulp.watch('styles/**/*.less', gulp.series('less'));
cb();
})
);

1062
lang/en.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,14 @@
export { default as DhpPCSheet } from './sheets/pc.mjs';
export { default as DhpAdversarySheet } from './sheets/adversary.mjs';
export { default as DhpClassSheet } from './sheets/class.mjs';
export { default as DhpSubclass } from './sheets/subclass.mjs';
export { default as DhpFeatureSheet } from './sheets/feature.mjs';
export { default as DhpDomainCardSheet } from './sheets/domainCard.mjs';
export { default as DhpAncestry } from './sheets/ancestry.mjs';
export { default as DhpCommunity } from './sheets/community.mjs';
export { default as DhpMiscellaneous } from './sheets/miscellaneous.mjs';
export { default as DhpConsumable } from './sheets/consumable.mjs';
export { default as DhpWeapon } from './sheets/weapon.mjs';
export { default as DhpArmor } from './sheets/armor.mjs';
export { default as DhpChatMessage } from './chatMessage.mjs';
export { default as DhpEnvironment } from './sheets/environment.mjs';

View file

@ -0,0 +1,229 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class AncestrySelectionDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(resolve){
super({});
this.resolve = resolve;
this.data = {
ancestries: [],
features: [],
ancestryInfo: {
name: '',
img: null,
customImg: 'icons/svg/mystery-man.svg',
description: '',
},
};
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ["daggerheart", "views", "ancestry-selection"],
position: {
width: 800,
height: "auto"
},
actions: {
selectAncestry: this.selectAncestry,
selectFeature: this.selectFeature,
viewItem: this.viewItem,
selectImage: this.selectImage,
editImage: this._onEditImage,
saveAncestry: this.saveAncestry,
},
form: {
submitOnChange: true,
closeOnSubmit: false,
}
};
/** @override */
static PARTS = {
damageSelection: {
id: "ancestrySelection",
template: "systems/daggerheart/templates/views/ancestrySelection.hbs"
}
}
/* -------------------------------------------- */
/** @inheritDoc */
get title() {
return `Ancestry Selection`;
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
const ancestryNameInput = $(htmlElement).find(".ancestry-name");
if(ancestryNameInput.length > 0){
ancestryNameInput.on("change", this.setName.bind(this));
$(htmlElement).find(".ancestry-description").on("change", this.setDescription.bind(this));
}
// $(htmlElement).find(".ancestry-image").on("change", this.selectImage.bind(this));
}
async _prepareContext(_options) {
const systemAncestries = Array.from((await game.packs.get('daggerheart.playtest-ancestries')).index).map(x => ({
...x,
selected: this.data.ancestries.some(selected => selected.uuid === x.uuid),
}));
const customAncestries = game.items.reduce((acc, x) => {
if(x.type === 'ancestry'){
acc.push({ ...x, uuid: x.uuid, selected: this.data.ancestries.some(selected => selected.uuid === x.uuid) });
}
return acc;
}, []);
const ancestryFeatures = this.data.ancestries.flatMap(x =>
x.system.abilities.map(x => ({ ...x, selected: this.data.features.some(selected => selected.uuid === x.uuid) }))
);
return {
systemAncestries,
customAncestries,
ancestryFeatures,
selectedAncestries: this.data.ancestries,
selectedFeatures: this.data.features,
ancestryInfo: this.data.ancestryInfo,
};
}
static async selectAncestry(_, button) {
const newAncestries = [...this.data.ancestries];
if(!newAncestries.findSplice(x => x.uuid === button.dataset.uuid) && this.data.ancestries.length < 2){
const ancestry = await fromUuid(button.dataset.uuid);
newAncestries.push(ancestry);
}
this.data.ancestries = newAncestries;
this.data.features = newAncestries.length === 1 ? newAncestries[0].system.abilities : [];
this.render(true);
}
static async selectFeature(_, button) {
const newFeatures = [...this.data.features];
if(!newFeatures.findSplice(x => x.uuid === button.dataset.uuid) && this.data.features.length < 2){
const feature = await fromUuid(button.dataset.uuid);
newFeatures.push(feature);
}
this.data.features = newFeatures;
this.render(true);
}
static async viewItem(_, button) {
(await fromUuid(button.dataset.uuid)).sheet.render(true);
}
setName(event) {
this.data.ancestryInfo.name = event.currentTarget.value;
this.render(true);
}
setDescription(event) {
this.data.ancestryInfo.description = event.currentTarget.value;
this.render(true);
}
static selectImage(_, button) {
this.data.ancestryInfo.img = button.dataset.img;
this.render(true);
}
static _onEditImage() {
const fp = new FilePicker({
current: this.data.ancestryInfo.img,
type: "image",
redirectToRoot: ['icons/svg/mystery-man.svg'],
callback: async path => this._updateImage.bind(this)(path),
top: this.position.top + 40,
left: this.position.left + 10
});
return fp.browse();
}
_updateImage(path){
this.data.ancestryInfo.customImg = path;
this.data.ancestryInfo.img = path;
this.render(true);
}
static async saveAncestry(_, button) {
if(this.data.ancestries.length === 2){
const { name, img, description } = this.data.ancestryInfo;
this.resolve({ data: { name: name, img: img, type: "ancestry", system: { description: description, abilities: this.data.features.map(x => ({ name: x.name, img: x.img, uuid: x.uuid, subclassLevel: '' })) }}});
} else {
this.resolve({ data: this.data.ancestries[0].toObject() });
}
this.close();
}
}
// export default class DamageSelectionDialog extends FormApplication {
// constructor(rollString, bonusDamage, resolve){
// super({}, {});
// this.data = {
// rollString,
// bonusDamage: bonusDamage.map(x => ({
// ...x,
// hopeUses: 0
// })),
// }
// this.resolve = resolve;
// }
// get title (){
// return 'Damage Options';
// }
// static get defaultOptions() {
// const defaults = super.defaultOptions;
// const overrides = {
// height: 'auto',
// width: 400,
// id: 'damage-selection',
// template: 'systems/daggerheart/templates/views/damageSelection.hbs',
// closeOnSubmit: false,
// classes: ["daggerheart", "views", "damage-selection"],
// };
// const mergedOptions = foundry.utils.mergeObject(defaults, overrides);
// return mergedOptions;
// }
// async getData(){
// const context = super.getData();
// context.rollString = this.data.rollString;
// context.bonusDamage = this.data.bonusDamage;
// return context;
// }
// activateListeners(html) {
// super.activateListeners(html);
// html.find('.roll-button').click(this.finish.bind(this));
// html.find('.').change();
// }
// // async _updateObject(_, formData) {
// // const data = foundry.utils.expandObject(formData);
// // this.data = foundry.utils.mergeObject(this.data, data);
// // this.render(true);
// // }
// finish(){
// this.resolve(this.data);
// this.close();
// }
// }

View file

@ -0,0 +1,9 @@
export default class DhpChatMesssage extends ChatMessage {
async renderHTML() {
if(this.type === 'dualityRoll' || this.type === 'adversaryRoll' || this.type === 'abilityUse'){
this.content = await renderTemplate(this.content, this.system);
}
return super.renderHTML();
}
}

View file

@ -0,0 +1,74 @@
import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs';
const {ApplicationV2} = foundry.applications.api;
export default class DaggerheartActionConfig extends DaggerheartSheet(ApplicationV2) {
constructor(action){
super({});
this.action = action;
this.openSection = null;
}
// get title(){
// return `Action - ${this.action.name}`;
// }
static DEFAULT_OPTIONS = {
tag: 'form',
id: "daggerheart-action",
classes: ["daggerheart", "views", "action"],
position: { width: 600, height: 'auto' },
actions: {
toggleSection: this.toggleSection,
},
form: {
handler: this.updateForm,
closeOnSubmit: true,
},
};
static PARTS = {
form: {
id: "action",
template: "systems/daggerheart/templates/views/action.hbs"
}
}
_getTabs() {
const tabs = {
effects: { active: true, cssClass: '', group: 'primary', id: 'effects', icon: null, label: 'Effects' },
useage: { active: false, cssClass: '', group: 'primary', id: 'useage', icon: null, label: 'Useage' },
conditions: { active: false, cssClass: '', group: 'primary', id: 'conditions', icon: null, label: 'Conditions' },
}
for ( const v of Object.values(tabs) ) {
v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active;
v.cssClass = v.active ? "active" : "";
}
return tabs;
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options, 'action');
context.openSection = this.openSection;
context.tabs = this._getTabs();
return context;
}
static toggleSection(_, button) {
this.openSection = button.dataset.section === this.openSection ? null : button.dataset.section;
this.render(true);
}
static async updateForm(event, _, formData) {
const data = foundry.utils.expandObject(foundry.utils.mergeObject(this.action.toObject(), formData.object));
const newActions = this.action.parent.actions.map(x => x.toObject());
if(!newActions.findSplice(x => x.id === data.id, data)){
newActions.push(data);
}
await this.action.parent.parent.update({ "system.actions": newActions });
}
}

View file

@ -0,0 +1,48 @@
export default function DhpApplicationMixin(Base) {
return class DhpSheet extends Base {
static applicationType = "sheets";
static documentType = "";
static get defaultOptions() {
return Object.assign(super.defaultOptions, {
classes: ["daggerheart", "sheet", this.documentType],
template: `systems/${SYSTEM.id}/templates/${this.applicationType}/${this.documentType}.hbs`,
height: "auto",
submitOnChange: true,
submitOnClose: false,
width: 450
});
}
/** @override */
get title() {
const {documentName, type, name} = this.object;
// const typeLabel = game.i18n.localize(CONFIG[documentName].typeLabels[type]);
const typeLabel = documentName;
return `[${typeLabel}] ${name}`;
}
// async _renderOuter() {
// const html = await super._renderOuter();
// // const overlaySrc = "systems/amia/assets/ThePrimordial.png";
// const overlay = `<div class="outer-render"></div>`
// $(html).find('.window-header').prepend(overlay);
// return html;
// }
activateListeners(html) {
super.activateListeners(html);
html.on("click", "[data-action]", this.#onClickAction.bind(this));
}
async #onClickAction(event) {
event.preventDefault();
const button = event.currentTarget;
const action = button.dataset.action;
return this._handleAction(action, event, button);
}
async _handleAction(action, event, button) {}
}
}

View file

@ -0,0 +1,180 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class DamageSelectionDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(rollString, bonusDamage, hope, resolve){
super({});
this.data = {
rollString,
bonusDamage: bonusDamage.reduce((acc, x) => {
if(x.appliesOn === SYSTEM.EFFECTS.applyLocations.damageRoll.id){
acc.push(({
...x,
hopeUses: 0
}));
}
return acc;
}, []),
hope,
}
this.resolve = resolve;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ["daggerheart", "views", "damage-selection"],
position: {
width: 400,
height: "auto"
},
actions: {
decreaseHopeUse: this.decreaseHopeUse,
increaseHopeUse: this.increaseHopeUse,
rollDamage: this.rollDamage,
},
form: {
handler: this.updateSelection,
submitOnChange: true,
closeOnSubmit: false,
}
};
/** @override */
static PARTS = {
damageSelection: {
id: "damageSelection",
template: "systems/daggerheart/templates/views/damageSelection.hbs"
}
}
/* -------------------------------------------- */
/** @inheritDoc */
get title() {
return `Damage Options`;
}
async _prepareContext(_options) {
return {
rollString: this.getRollString(),
bonusDamage: this.data.bonusDamage,
hope: this.data.hope+1,
hopeUsed: this.getHopeUsed(),
}
}
static updateSelection(event, _, formData){
const { bonusDamage, ...rest } = foundry.utils.expandObject(formData.object);
for(var index in bonusDamage){
this.data.bonusDamage[index].initiallySelected = bonusDamage[index].initiallySelected;
if(bonusDamage[index].hopeUses){
const value = Number.parseInt(bonusDamage[index].hopeUses);
if(!Number.isNaN(value)) this.data.bonusDamage[index].hopeUses = value;
}
}
this.data = foundry.utils.mergeObject(this.data, rest);
this.render(true);
}
getRollString(){
return this.data.rollString.concat(this.data.bonusDamage.reduce((acc, x) => {
if(x.initiallySelected){
const nr = 1+x.hopeUses;
const baseDamage = x.value;
return acc.concat(` + ${nr}${baseDamage}`);
}
return acc;
}, ""));
}
getHopeUsed(){
return this.data.bonusDamage.reduce((acc, x) => acc+x.hopeUses, 0);
}
static decreaseHopeUse(_, button){
const index = Number.parseInt(button.dataset.index);
if(this.data.bonusDamage[index].hopeUses - 1 >= 0) {
this.data.bonusDamage[index].hopeUses -= 1;
this.render(true);
}
}
static increaseHopeUse(_, button){
const index = Number.parseInt(button.dataset.index);
if(this.data.bonusDamage[index].hopeUses <= this.data.hope+1) {
this.data.bonusDamage[index].hopeUses += 1;
this.render(true);
}
}
static rollDamage(){
this.resolve({ rollString: this.getRollString(), bonusDamage: this.data.bonusDamage, hopeUsed: this.getHopeUsed() });
this.close();
}
}
// export default class DamageSelectionDialog extends FormApplication {
// constructor(rollString, bonusDamage, resolve){
// super({}, {});
// this.data = {
// rollString,
// bonusDamage: bonusDamage.map(x => ({
// ...x,
// hopeUses: 0
// })),
// }
// this.resolve = resolve;
// }
// get title (){
// return 'Damage Options';
// }
// static get defaultOptions() {
// const defaults = super.defaultOptions;
// const overrides = {
// height: 'auto',
// width: 400,
// id: 'damage-selection',
// template: 'systems/daggerheart/templates/views/damageSelection.hbs',
// closeOnSubmit: false,
// classes: ["daggerheart", "views", "damage-selection"],
// };
// const mergedOptions = foundry.utils.mergeObject(defaults, overrides);
// return mergedOptions;
// }
// async getData(){
// const context = super.getData();
// context.rollString = this.data.rollString;
// context.bonusDamage = this.data.bonusDamage;
// return context;
// }
// activateListeners(html) {
// super.activateListeners(html);
// html.find('.roll-button').click(this.finish.bind(this));
// html.find('.').change();
// }
// // async _updateObject(_, formData) {
// // const data = foundry.utils.expandObject(formData);
// // this.data = foundry.utils.mergeObject(this.data, data);
// // this.render(true);
// // }
// finish(){
// this.resolve(this.data);
// this.close();
// }
// }

View file

@ -0,0 +1,63 @@
const {HandlebarsApplicationMixin, ApplicationV2} = foundry.applications.api;
export default class DhpDeathMove extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actor){
super({});
this.actor = actor;
this.selectedMove = null;
}
get title(){
return game.i18n.format("DAGGERHEART.Application.DeathMove.Title", { actor: this.actor.name });
}
static DEFAULT_OPTIONS = {
classes: ["daggerheart", "views", "death-move"],
position: { width: 800, height: 'auto' },
actions: {
selectMove: this.selectMove,
takeMove: this.takeMove,
},
};
static PARTS = {
application: {
id: "death-move",
template: "systems/daggerheart/templates/views/deathMove.hbs"
}
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.selectedMove = this.selectedMove;
context.options = SYSTEM.GENERAL.deathMoves;
return context;
}
static selectMove(_, button){
const move = button.dataset.move;
this.selectedMove = SYSTEM.GENERAL.deathMoves[move];
this.render();
}
static async takeMove(){
const cls = getDocumentClass("ChatMessage");
const msg = new cls({
user: game.user.id,
content: await renderTemplate("systems/daggerheart/templates/chat/deathMove.hbs", {
player: game.user.character.name,
title: game.i18n.localize(this.selectedMove.name),
img: this.selectedMove.img,
description: game.i18n.localize(this.selectedMove.description),
}),
});
cls.create(msg.toObject());
this.close();
}
}

View file

@ -0,0 +1,82 @@
const {HandlebarsApplicationMixin, ApplicationV2} = foundry.applications.api;
export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actor, shortrest){
super({});
this.actor = actor;
this.selectedActivity = null;
this.shortrest = shortrest;
this.customActivity = SYSTEM.GENERAL.downtime.custom;
}
get title(){
return `${this.actor.name} - ${this.shortrest ? 'Short Rest': 'Long Rest'}`;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ["daggerheart", "views", "downtime"],
position: { width: 800, height: 'auto' },
actions: {
selectActivity: this.selectActivity,
takeDowntime: this.takeDowntime,
},
form: { handler: this.updateData, submitOnChange: true }
};
static PARTS = {
application: {
id: "downtime",
template: "systems/daggerheart/templates/views/downtime.hbs"
}
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.selectedActivity = this.selectedActivity;
context.options = this.shortrest ? SYSTEM.GENERAL.downtime.shortRest : SYSTEM.GENERAL.downtime.longRest;
context.customActivity = this.customActivity;
context.disabledDowntime = !this.selectedActivity || (this.selectedActivity.id === this.customActivity.id && (!this.customActivity.name || !this.customActivity.description));
return context;
}
static selectActivity(_, button){
const activity = button.dataset.activity;
this.selectedActivity = activity === this.customActivity.id ? this.customActivity : this.shortrest ? SYSTEM.GENERAL.downtime.shortRest[activity] : SYSTEM.GENERAL.downtime.longRest[activity];
this.render();
}
static async takeDowntime(){
const refreshedFeatures = this.shortrest ? this.actor.system.refreshableFeatures.shortRest : [...this.actor.system.refreshableFeatures.shortRest, ...this.actor.system.refreshableFeatures.longRest];
for(var feature of refreshedFeatures){
await feature.system.refresh();
}
const cls = getDocumentClass("ChatMessage");
const msg = new cls({
user: game.user.id,
content: await renderTemplate("systems/daggerheart/templates/chat/downtime.hbs", {
player: game.user.character.name,
title: game.i18n.localize(this.selectedActivity.name),
img: this.selectedActivity.img,
description: game.i18n.localize(this.selectedActivity.description),
refreshedFeatures: refreshedFeatures,
}),
});
cls.create(msg.toObject());
this.close();
}
static async updateData(event, element, formData){
this.customActivity = foundry.utils.mergeObject(this.customActivity, formData.object);
this.render();
}
}

View file

@ -0,0 +1,277 @@
import SelectDialog from "../dialogs/selectDialog.mjs";
import { getTier } from "../helpers/utils.mjs";
import DhpMulticlassDialog from "./multiclassDialog.mjs";
const {HandlebarsApplicationMixin, ApplicationV2} = foundry.applications.api;
export default class DhpLevelup extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actor){
super({});
this.actor = actor;
this.data = foundry.utils.deepClone(actor.system.levelData);
this.activeLevel = actor.system.levelData.currentLevel+1;
}
get title(){
return `${this.actor.name} - Level Up`;
}
static DEFAULT_OPTIONS = {
id: "daggerheart-levelup",
classes: ["daggerheart", "views", "levelup"],
position: { width: 1200, height: 'auto' },
actions: {
toggleBox: this.toggleBox,
advanceLevel: this.advanceLevel,
finishLevelup: this.finishLevelup,
},
};
static PARTS = {
form: {
id: "levelup",
template: "systems/daggerheart/templates/views/levelup.hbs"
}
}
async _prepareContext(_options) {
let selectedChoices = 0, multiclassing = {}, subclassing = {};
const leveledTiers = Object.keys(this.data.levelups).reduce((acc, levelKey) => {
const levelData = this.data.levelups[levelKey];
['tier1','tier2','tier3'].forEach(tierKey => {
let tierUpdate = {};
const tierData = levelData[tierKey];
if(tierData){
tierUpdate = Object.keys(tierData).reduce((acc, propertyKey) => {
const values = tierData[propertyKey];
const level = Number.parseInt(levelKey);
acc[propertyKey] = Object.values(values).map(value => {
if(value && level === this.activeLevel) selectedChoices++;
if(propertyKey === 'multiclass') multiclassing[levelKey] = true;
if(propertyKey === 'subclass') subclassing[tierKey] = true;
return { level: level, value: value };
});
return acc;
}, {});
}
Object.keys(tierUpdate).forEach(propertyKey => {
const property = tierUpdate[propertyKey];
const propertyValues = foundry.utils.getProperty(acc, `${tierKey}.${propertyKey}`)??[];
foundry.utils.setProperty(acc, `${tierKey}.${propertyKey}`, [...propertyValues, ...property]);
});
});
return acc;
}, { tier1: {}, tier2: {}, tier3: {} });
const activeTier = getTier(this.activeLevel);
const data = Object.keys(SYSTEM.ACTOR.levelupData).reduce((acc, tierKey) => {
const tier = SYSTEM.ACTOR.levelupData[tierKey];
acc[tierKey] = {
label: game.i18n.localize(tier.label),
info: game.i18n.localize(tier.info),
pretext: game.i18n.localize(tier.pretext),
postext: game.i18n.localize(tier.posttext),
active: tierKey <= activeTier,
choices: Object.keys(tier.choices).reduce((acc, propertyKey) => {
const property = tier.choices[propertyKey];
acc[propertyKey] = { description: property.description, cost: property.cost ?? 1, values: [] };
for(var i = 0; i < property.maxChoices; i++){
const leveledValue = leveledTiers[tierKey][propertyKey]?.[i];
const subclassLock = propertyKey === 'subclass' && Object.keys(multiclassing).find(x => getTier(Number.parseInt(x)) === tierKey);
const subclassMulticlassLock = propertyKey === 'multiclass' && subclassing[tierKey];
const multiclassLock = propertyKey === 'multiclass' && Object.keys(multiclassing).length > 0 && !(leveledValue && Object.keys(multiclassing).find(x => Number.parseInt(x) === leveledValue.level));
const locked = leveledValue && leveledValue.level !== this.activeLevel || subclassLock || subclassMulticlassLock || multiclassLock;
const disabled = tierKey > activeTier || (selectedChoices === 2 && !(leveledValue && leveledValue.level === this.activeLevel)) || locked;
acc[propertyKey].values.push({
selected: leveledValue?.value !== undefined,
path: `levelups.${this.activeLevel}.${tierKey}.${propertyKey}.${i}`,
description: game.i18n.localize(property.description),
disabled: disabled,
locked: locked,
});
}
return acc;
}, {})
};
return acc;
}, {});
return {
data: data,
activeLevel: this.activeLevel,
changedLevel: this.actor.system.levelData.changedLevel,
completedSelection: selectedChoices === 2,
}
}
static async toggleBox(_, button){
const path = button.dataset.path;
if(foundry.utils.getProperty(this.data, path)){
const pathParts = path.split('.');
const arrayPart = pathParts.slice(0, pathParts.length-1).join('.');
let array = foundry.utils.getProperty(this.data, arrayPart);
if(button.dataset.levelAttribute === 'multiclass'){
array = [];
}
else {
delete array[Number.parseInt(pathParts[pathParts.length-1])];
}
foundry.utils.setProperty(this.data, arrayPart, array);
} else {
const updates = [{ path: path, value: { level: this.activeLevel } }];
const levelChoices = SYSTEM.ACTOR.levelChoices[button.dataset.levelAttribute];
if(button.dataset.levelAttribute === 'subclass'){
if(!this.actor.system.multiclassSubclass){
updates[0].value.value = { multiclass: false, feature: this.actor.system.subclass.system.specializationFeature.unlocked ? 'mastery' : 'specialization' };
}
else {
const choices = [{name: this.actor.system.subclass.name, value: this.actor.system.subclass.uuid}, {name: this.actor.system.multiclassSubclass.name, value: this.actor.system.multiclassSubclass.uuid}];
const indexes = await SelectDialog.selectItem({ actor: this.actor, choices: choices, title: levelChoices.title, nrChoices: 1 });
if(indexes.length === 0) {
this.render();
return;
}
const multiclassSubclass = choices[indexes[0]].name === this.actor.system.multiclassSubclass.name;
updates[0].value.value = { multiclass: multiclassSubclass, feature: this.actor.system.multiclassSubclass.system.specializationFeature.unlocked ? 'mastery' : 'specialization' };
}
}
else if (button.dataset.levelAttribute === 'multiclass'){
const multiclassAwait = new Promise((resolve) => {
new DhpMulticlassDialog(this.actor.name, this.actor.system.class, resolve).render(true);
});
const multiclassData = await multiclassAwait;
if(!multiclassData) {
this.render();
return;
}
const pathParts = path.split('.');
const arrayPart = pathParts.slice(0, pathParts.length-1).join('.');
updates[0] = { path: [arrayPart, '0'].join('.'), value: { level: this.activeLevel, value: { class: multiclassData.class, subclass: multiclassData.subclass, domain: multiclassData.domain, level: this.activeLevel } } };
updates[1] = { path: [arrayPart, '1'].join('.'), value: { level: this.activeLevel, value: { class: multiclassData.class, subclass: multiclassData.subclass, domain: multiclassData.domain, level: this.activeLevel } } };
}
else {
if(levelChoices.choices.length > 0){
if(typeof levelChoices.choices === 'string'){
const choices = foundry.utils.getProperty(this.actor, levelChoices.choices).map(x => ({ name: x.description, value: x.id }));
const indexes = await SelectDialog.selectItem({ actor: this.actor, choices: choices, title: levelChoices.title, nrChoices: levelChoices.nrChoices });
if(indexes.length === 0) {
this.render();
return;
}
updates[0].value.value = choices.filter((_, index) => indexes.includes(index)).map(x => x.value);
}
else {
const indexes = await SelectDialog.selectItem({ actor: this.actor, choices: levelChoices.choices, title: levelChoices.title, nrChoices: levelChoices.nrChoices });
if(indexes.length === 0) {
this.render();
return;
}
updates[0].value.value = levelChoices.choices[indexes[0]].path;
}
}
}
const update = updates.reduce((acc, x) => {
acc[x.path] = x.value;
return acc;
}, {});
this.data = foundry.utils.mergeObject(this.data, update);
}
this.render();
}
static advanceLevel(){
this.activeLevel += 1;
this.render();
}
static async finishLevelup(){
this.data.currentLevel = this.data.changedLevel;
let multiclass = null;
for(var level in this.data.levelups){
for(var tier in this.data.levelups[level]){
for(var category in this.data.levelups[level][tier]) {
for (var value in this.data.levelups[level][tier][category]){
if(category === 'multiclass'){
multiclass = this.data.levelups[level][tier][category][value].value;
this.data.levelups[level][tier][category][value] = true;
} else {
this.data.levelups[level][tier][category][value] = this.data.levelups[level][tier][category][value].value ?? true;
}
}
}
}
}
const tiersMoved = getTier(this.actor.system.levelData.changedLevel, true) - getTier(this.actor.system.levelData.currentLevel, true);
const experiences = Array.from(Array(tiersMoved), (_,index) => ({ id: foundry.utils.randomID(), level: this.actor.system.experiences.length+index*3, description: '', value: 1 }));
await this.actor.update({ system: {
levelData: this.data,
experiences: [...this.actor.system.experiences, ...experiences],
}}, { diff: false });
if(!this.actor.multiclass && multiclass){
const multiclassClass = (await fromUuid(multiclass.class.uuid)).toObject();
multiclassClass.system.domains = [multiclass.domain.id];
multiclassClass.system.multiclass = multiclass.level;
const multiclassFeatures = [];
for(var i = 0; i < multiclassClass.system.features.length; i++){
const feature = (await fromUuid(multiclassClass.system.features[i].uuid)).toObject();
feature.system.multiclass = multiclass.level;
multiclassFeatures.push(feature);
}
const multiclassSubclass = (await fromUuid(multiclass.subclass.uuid)).toObject();
multiclassSubclass.system.multiclass = multiclass.level;
const multiclassSubclassFeatures = {};
const features = [multiclassSubclass.system.foundationFeature, multiclassSubclass.system.specializationFeature, multiclassSubclass.system.masteryFeature];
for(var i = 0; i < features.length; i++){
const path = i === 0 ? 'foundationFeature' : i === 1 ? 'specializationFeature' : 'masteryFeature';
const feature = features[i];
for(var ability of feature.abilities){
const data = (await fromUuid(ability.uuid)).toObject();
if(i > 0 ) data.system.disabled = true;
data.system.multiclass = multiclass.level;
if(!multiclassSubclassFeatures[path]) multiclassSubclassFeatures[path] = [data];
else multiclassSubclassFeatures[path].push(data);
// data.uuid = feature.uuid;
// const abilityData = await this._onDropItemCreate(data);
// ability.uuid = abilityData[0].uuid;
// createdItems.push(abilityData);
}
}
for(let subclassFeaturesKey in multiclassSubclassFeatures){
const values = multiclassSubclassFeatures[subclassFeaturesKey];
const abilityResults = await this.actor.createEmbeddedDocuments('Item', values);
for(var i = 0; i < abilityResults.length; i++){
multiclassSubclass.system[subclassFeaturesKey].abilities[i].uuid = abilityResults[i].uuid;
}
}
await this.actor.createEmbeddedDocuments('Item', [multiclassClass, ...multiclassFeatures, multiclassSubclass]);
}
this.close();
}
}

View file

@ -0,0 +1,98 @@
const {HandlebarsApplicationMixin, ApplicationV2} = foundry.applications.api;
export default class DhpMulticlassDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actorName, actorClass, resolve){
super({});
this.actorName = actorName;
this.actorClass = actorClass;
this.resolve = resolve;
this.classChoices = Array.from(game.items.reduce((acc, x) => {
if(x.type === 'class' && x.name !== actorClass.name){
acc.add(x);
}
return acc;
}, new Set()));
this.subclassChoices = [];
this.domainChoices = [];
this.data = {
class: null,
subclass: null,
domain: null,
};
}
get title(){
return `${this.actorName} - Multiclass`;
}
static DEFAULT_OPTIONS = {
classes: ["daggerheart", "views", "multiclass"],
position: { width: 600, height: 'auto' },
actions: {
selectClass: this.selectClass,
selectSubclass: this.selectSubclass,
selectDomain: this.selectDomain,
finish: this.finish,
},
};
static PARTS = {
form: {
id: "levelup",
template: "systems/daggerheart/templates/views/multiclass.hbs"
}
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.classChoices = this.classChoices;
context.subclassChoices = this.subclassChoices;
context.domainChoices = this.domainChoices;
context.disabledFinish = !this.data.class || !this.data.subclass || !this.data.domain;
context.data = this.data;
return context;
}
static async selectClass(_, button) {
const oldClass = this.data.class;
this.data.class = this.data.class?.uuid === button.dataset.class ? null : await fromUuid(button.dataset.class);
if(oldClass !== button.dataset.class){
this.data.subclass = null;
this.data.domain = null;
this.subclassChoices = this.data.class ? this.data.class.system.subclasses : [];
this.domainChoices = this.data.class ? this.data.class.system.domains.map(x => {
const config = SYSTEM.DOMAIN.domains[x];
return { name: game.i18n.localize(config.name), id: config.id, img: config.src, disabled: this.actorClass.system.domains.includes(config.id) };
}) : [];
}
this.render(true);
}
static async selectSubclass(_, button) {
this.data.subclass = this.data.subclass?.uuid === button.dataset.subclass ? null : this.subclassChoices.find(x => x.uuid === button.dataset.subclass);
this.render(true);
}
static async selectDomain(_, button) {
const domain = this.data.domain?.id === button.dataset.domain ? null : this.domainChoices.find(x => x.id === button.dataset.domain);;
if(domain?.disabled) return;
this.data.domain = domain;
this.render(true);
}
static finish(){
this.close({}, this.data);
}
async close(options={}, data=null) {
this.resolve(data);
super.close(options);
}
}

View file

@ -0,0 +1,76 @@
export default class NpcRollSelectionDialog extends FormApplication {
constructor(experiences, resolve, isNpc){
super({}, {});
this.experiences = experiences;
this.resolve = resolve;
this.selectedExperiences = [];
this.data = {
nrDice: 1,
advantage: null,
};
}
get title (){
return 'Roll Options';
}
static get defaultOptions() {
const defaults = super.defaultOptions;
const overrides = {
height: 'auto',
width: 400,
id: 'roll-selection',
template: 'systems/daggerheart/templates/views/npcRollSelection.hbs',
closeOnSubmit: false,
submitOnChange: true,
classes: ["daggerheart", "views", "npc-roll-selection"],
};
const mergedOptions = foundry.utils.mergeObject(defaults, overrides);
return mergedOptions;
}
async getData(){
const context = super.getData();
context.nrDice = this.data.nrDice;
context.advantage = this.data.advantage;
context.experiences = this.experiences.map(x => ({ ...x, selected: this.selectedExperiences.find(selected => selected.id === x.id) }));
return context;
}
activateListeners(html) {
super.activateListeners(html);
html.find('.increase').click(_ => this.updateNrDice(1));
html.find('.decrease').click(_ => this.updateNrDice(-1));
html.find('.advantage').click(_ => this.updateIsAdvantage(true));
html.find('.disadvantage').click(_ => this.updateIsAdvantage(false));
html.find('.roll-button').click(this.finish.bind(this));
html.find('.roll-dialog-chip').click(this.selectExperience.bind(this));
}
updateNrDice(value){
this.data.nrDice += value;
this.render();
}
updateIsAdvantage(advantage) {
this.data.advantage = this.data.advantage === advantage ? null : advantage;
this.render();
}
selectExperience(event){
const experience = this.experiences[event.currentTarget.dataset.key];
this.selectedExperiences = this.selectedExperiences.find(x => x.name === experience.name) ? this.selectedExperiences.filter(x => x.name !== experience.name) : [...this.selectedExperiences, experience];
this.render();
}
finish(){
this.resolve({ ...this.data, experiences: this.selectedExperiences });
this.close();
}
}

View file

@ -0,0 +1,276 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class RollSelectionDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(experiences, bonusDamage, hopeResource, resolve, isNpc){
super({}, {});
this.experiences = experiences;
this.resolve = resolve;
this.isNpc;
this.selectedExperiences = [];
this.data = {
diceOptions: [{ name: 'd12', value: 'd12' }, { name: 'd20', value: 'd20' }],
hope: ['d12'],
fear: ['d12'],
advantage: null,
disadvantage: null,
bonusDamage: bonusDamage.reduce((acc, x) => {
if(x.appliesOn === SYSTEM.EFFECTS.applyLocations.attackRoll.id){
acc.push(({
...x,
hopeUses: 0
}));
}
return acc;
}, []),
hopeResource: hopeResource,
};
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ["daggerheart", "views", "roll-selection"],
position: {
width: 400,
height: "auto"
},
actions: {
selectExperience: this.selectExperience,
decreaseHopeUse: this.decreaseHopeUse,
increaseHopeUse: this.increaseHopeUse,
setAdvantage: this.setAdvantage,
setDisadvantage: this.setDisadvantage,
finish: this.finish,
},
form: {
handler: this.updateSelection,
submitOnChange: true,
submitOnClose: false,
}
};
/** @override */
static PARTS = {
damageSelection: {
id: "damageSelection",
template: "systems/daggerheart/templates/views/rollSelection.hbs"
}
}
get title() {
return `Roll Options`;
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.isNpc = this.isNpc;
context.diceOptions = this.data.diceOptions;
context.hope = this.data.hope;
context.fear = this.data.fear;
context.advantage = this.data.advantage;
context.disadvantage = this.data.disadvantage;
context.experiences = this.experiences.map(x => ({ ...x, selected: this.selectedExperiences.find(selected => selected.id === x.id) }));
context.bonusDamage = this.data.bonusDamage;
context.hopeResource = this.data.hopeResource+1;
context.hopeUsed = this.getHopeUsed();
return context;
}
static updateSelection(event, _, formData){
const { bonusDamage, ...rest } = foundry.utils.expandObject(formData.object);
for(var index in bonusDamage){
this.data.bonusDamage[index].initiallySelected = bonusDamage[index].initiallySelected;
if(bonusDamage[index].hopeUses){
const value = Number.parseInt(bonusDamage[index].hopeUses);
if(!Number.isNaN(value)) this.data.bonusDamage[index].hopeUses = value;
}
}
this.data = foundry.utils.mergeObject(this.data, rest);
this.render();
}
static selectExperience(_, button){
if(this.selectedExperiences.find(x => x.id === button.dataset.key)){
this.selectedExperiences = this.selectedExperiences.filter(x => x.id !== button.dataset.key);
} else {
this.selectedExperiences = [...this.selectedExperiences, this.experiences.find(x => x.id === button.dataset.key)];
}
this.render();
}
getHopeUsed(){
return this.data.bonusDamage.reduce((acc, x) => acc+x.hopeUses, 0);
}
static decreaseHopeUse(_, button){
const index = Number.parseInt(button.dataset.index);
if(this.data.bonusDamage[index].hopeUses - 1 >= 0) {
this.data.bonusDamage[index].hopeUses -= 1;
this.render(true);
}
}
static increaseHopeUse(_, button){
const index = Number.parseInt(button.dataset.index);
if(this.data.bonusDamage[index].hopeUses <= this.data.hopeResource+1) {
this.data.bonusDamage[index].hopeUses += 1;
this.render(true);
}
}
static setAdvantage(){
this.data.advantage = this.data.advantage ? null : 'd6';
this.data.disadvantage = null;
this.render(true);
}
static setDisadvantage(){
this.data.advantage = null;
this.data.disadvantage = this.data.disadvantage ? null : 'd6';
this.render(true);
}
static async finish(){
const { diceOptions, ...rest } = this.data;
this.resolve({ ...rest, experiences: this.selectedExperiences, hopeUsed: this.getHopeUsed(), bonusDamage: this.data.bonusDamage.reduce((acc, x) => acc.concat(` + ${1+x.hopeUses}${x.value}`), "") });
this.close();
}
}
// V1.3
// const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
// export default class RollSelectionDialog extends HandlebarsApplicationMixin(ApplicationV2) {
// constructor(experiences, bonusDamage, hopeResource, resolve, isNpc){
// super({}, {});
// this.experiences = experiences;
// this.resolve = resolve;
// this.isNpc;
// this.selectedExperiences = [];
// this.data = {
// diceOptions: [{ name: 'd12', value: 'd12' }, { name: 'd20', value: 'd20' }],
// hope: ['d12'],
// fear: ['d12'],
// advantage: null,
// disadvantage: null,
// bonusDamage: bonusDamage.reduce((acc, x) => {
// if(x.appliesOn === SYSTEM.EFFECTS.applyLocations.attackRoll.id){
// acc.push(({
// ...x,
// hopeUses: 0
// }));
// }
// return acc;
// }, []),
// hopeResource: hopeResource,
// };
// }
// static DEFAULT_OPTIONS = {
// tag: 'form',
// classes: ["daggerheart", "views", "roll-selection"],
// position: {
// width: 400,
// height: "auto"
// },
// actions: {
// selectExperience: this.selectExperience,
// decreaseHopeUse: this.decreaseHopeUse,
// increaseHopeUse: this.increaseHopeUse,
// finish: this.finish,
// },
// form: {
// handler: this.updateSelection,
// submitOnChange: true,
// closeOnSubmit: false,
// }
// };
// /** @override */
// static PARTS = {
// damageSelection: {
// id: "damageSelection",
// template: "systems/daggerheart/templates/views/rollSelection.hbs"
// }
// }
// get title() {
// return `Roll Options`;
// }
// async _prepareContext(_options) {
// const context = await super._prepareContext(_options);
// context.isNpc = this.isNpc;
// context.diceOptions = this.data.diceOptions;
// context.hope = this.data.hope;
// context.fear = this.data.fear;
// context.advantage = this.data.advantage;
// context.disadvantage = this.data.disadvantage;
// context.experiences = this.experiences.map(x => ({ ...x, selected: this.selectedExperiences.find(selected => selected.id === x.id) }));
// context.bonusDamage = this.data.bonusDamage;
// context.hopeResource = this.data.hopeResource+1;
// context.hopeUsed = this.getHopeUsed();
// return context;
// }
// static updateSelection(event, _, formData){
// const { bonusDamage, ...rest } = foundry.utils.expandObject(formData.object);
// for(var index in bonusDamage){
// this.data.bonusDamage[index].initiallySelected = bonusDamage[index].initiallySelected;
// if(bonusDamage[index].hopeUses){
// const value = Number.parseInt(bonusDamage[index].hopeUses);
// if(!Number.isNaN(value)) this.data.bonusDamage[index].hopeUses = value;
// }
// }
// this.data = foundry.utils.mergeObject(this.data, rest);
// this.render(true);
// }
// static selectExperience(_, button){
// if(this.selectedExperiences.find(x => x.id === button.dataset.key)){
// this.selectedExperiences = this.selectedExperiences.filter(x => x.id !== button.dataset.key);
// } else {
// this.selectedExperiences = [...this.selectedExperiences, this.experiences.find(x => x.id === button.dataset.key)];
// }
// this.render();
// }
// getHopeUsed(){
// return this.data.bonusDamage.reduce((acc, x) => acc+x.hopeUses, 0);
// }
// static decreaseHopeUse(_, button){
// const index = Number.parseInt(button.dataset.index);
// if(this.data.bonusDamage[index].hopeUses - 1 >= 0) {
// this.data.bonusDamage[index].hopeUses -= 1;
// this.render(true);
// }
// }
// static increaseHopeUse(_, button){
// const index = Number.parseInt(button.dataset.index);
// if(this.data.bonusDamage[index].hopeUses <= this.data.hopeResource+1) {
// this.data.bonusDamage[index].hopeUses += 1;
// this.render(true);
// }
// }
// static finish(){
// const { diceOptions, ...rest } = this.data;
// this.resolve({ ...rest, experiences: this.selectedExperiences, hopeUsed: this.getHopeUsed(), bonusDamage: this.data.bonusDamage.reduce((acc, x) => acc.concat(` + ${1+x.hopeUses}${x.value}`), "") });
// this.close();
// }
// }

View file

@ -0,0 +1,282 @@
class DhpAutomationSettings extends FormApplication {
constructor(object={}, options={}){
super(object, options);
}
static get defaultOptions() {
const defaults = super.defaultOptions;
const overrides = {
height: 'auto',
width: 400,
id: 'daggerheart-automation-settings',
template: 'systems/daggerheart/templates/views/automation-settings.hbs',
closeOnSubmit: true,
submitOnChange: false,
classes: ["daggerheart", "views", "settings"],
};
const mergedOptions = foundry.utils.mergeObject(defaults, overrides);
return mergedOptions;
}
async getData(){
const context = super.getData();
context.settings = SYSTEM.SETTINGS.gameSettings.Automation;
context.hope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope);
context.actionPoints = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.ActionPoints);
return context;
}
activateListeners(html) {
super.activateListeners(html);
}
async _updateObject(_, formData) {
const data = foundry.utils.expandObject(formData);
const updateSettingsKeys = Object.keys(data);
for(var i = 0; i < updateSettingsKeys.length; i++){
await game.settings.set(SYSTEM.id, updateSettingsKeys[i], data[updateSettingsKeys[i]]);
}
}
}
class DhpHomebrewSettings extends FormApplication {
constructor(object={}, options={}){
super(object, options);
}
static get defaultOptions() {
const defaults = super.defaultOptions;
const overrides = {
height: 'auto',
width: 400,
id: 'daggerheart-homebrew-settings',
template: 'systems/daggerheart/templates/views/homebrew-settings.hbs',
closeOnSubmit: true,
submitOnChange: false,
classes: ["daggerheart", "views", "settings"],
};
const mergedOptions = foundry.utils.mergeObject(defaults, overrides);
return mergedOptions;
}
async getData(){
const context = super.getData();
context.settings = SYSTEM.SETTINGS.gameSettings.General;
context.abilityArray = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.AbilityArray);
return context;
}
activateListeners(html) {
super.activateListeners(html);
}
async _updateObject(_, formData) {
const data = foundry.utils.expandObject(formData);
const updateSettingsKeys = Object.keys(data);
for(var i = 0; i < updateSettingsKeys.length; i++){
await game.settings.set(SYSTEM.id, updateSettingsKeys[i], data[updateSettingsKeys[i]]);
}
}
}
class DhpRangeSettings extends FormApplication {
constructor(object={}, options={}){
super(object, options);
this.range = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.RangeMeasurement);
}
static get defaultOptions() {
const defaults = super.defaultOptions;
const overrides = {
height: 'auto',
width: 400,
id: 'daggerheart-range-settings',
template: 'systems/daggerheart/templates/views/range-settings.hbs',
closeOnSubmit: false,
submitOnChange: true,
classes: ["daggerheart", "views", "settings"],
};
const mergedOptions = foundry.utils.mergeObject(defaults, overrides);
return mergedOptions;
}
async getData(){
const context = super.getData();
context.settings = SYSTEM.SETTINGS.gameSettings.General;
context.range = this.range;
context.disabled = context.range.enabled && [context.range.melee, context.range.veryClose, context.range.close, context.range.far, context.range.veryFar].some(x => x === null || x === false);
return context;
}
activateListeners(html) {
super.activateListeners(html);
html.find(".range-reset").click(this.reset.bind(this));
html.find(".save").click(this.save.bind(this));
html.find(".close").click(this.close.bind(this));
}
async _updateObject(_, formData) {
const data = foundry.utils.expandObject(formData, { disabled: true });
this.range = foundry.utils.mergeObject(this.range, data);
this.render(true);
}
reset(){
this.range = {
enabled: false,
melee: 5,
veryClose: 15,
close: 30,
far: 60,
veryFar: 120
};
this.render(true);
}
async save(){
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.RangeMeasurement, this.range);
this.close();
}
}
export const registerDHPSettings = () => {
// const debouncedReload = foundry.utils.debounce(() => window.location.reload(), 100);
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.AbilityArray, {
name: game.i18n.localize("DAGGERHEART.Settings.General.AbilityArray.Name"),
hint: game.i18n.localize("DAGGERHEART.Settings.General.AbilityArray.Hint"),
scope: 'world',
config: false,
type: String,
default: '[2,1,1,0,0,-1]',
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, {
name: game.i18n.localize("DAGGERHEART.Settings.Resources.Fear.Name"),
hint: game.i18n.localize("DAGGERHEART.Settings.Resources.Fear.Hint"),
scope: 'world',
config: false,
type: Number,
default: 0,
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope, {
name: game.i18n.localize("DAGGERHEART.Settings.Automation.Hope.Name"),
hint: game.i18n.localize("DAGGERHEART.Settings.Automation.Hope.Hint"),
scope: 'world',
config: false,
type: Boolean,
default: false,
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.ActionPoints, {
name: game.i18n.localize("DAGGERHEART.Settings.Automation.ActionPoints.Name"),
hint: game.i18n.localize("DAGGERHEART.Settings.Automation.ActionPoints.Hint"),
scope: 'world',
config: false,
type: Boolean,
default: true,
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.RangeMeasurement, {
name: game.i18n.localize("DAGGERHEART.Settings.General.RangeMeasurement.Name"),
hint: game.i18n.localize("DAGGERHEART.Settings.General.RangeMeasurement.Hint"),
scope: 'world',
config: false,
type: Object,
default: {
enabled: false,
melee: 5,
veryClose: 15,
close: 30,
far: 60,
veryFar: 120
},
});
game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.Automation.Name, {
name: game.i18n.localize("DAGGERHEART.Settings.Menu.Automation.Name"),
label: game.i18n.localize("DAGGERHEART.Settings.Menu.Automation.Label"),
hint: game.i18n.localize("DAGGERHEART.Settings.Menu.Automation.Hint"),
icon: SYSTEM.SETTINGS.menu.Automation.Icon,
type: DhpAutomationSettings,
restricted: true
});
game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.Homebrew.Name, {
name: game.i18n.localize("DAGGERHEART.Settings.Menu.Homebrew.Name"),
label: game.i18n.localize("DAGGERHEART.Settings.Menu.Homebrew.Label"),
hint: game.i18n.localize("DAGGERHEART.Settings.Menu.Homebrew.Hint"),
icon: SYSTEM.SETTINGS.menu.Homebrew.Icon,
type: DhpHomebrewSettings,
restricted: true
});
game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.Range.Name, {
name: game.i18n.localize("DAGGERHEART.Settings.Menu.Range.Name"),
label: game.i18n.localize("DAGGERHEART.Settings.Menu.Range.Label"),
hint: game.i18n.localize("DAGGERHEART.Settings.Menu.Range.Hint"),
icon: SYSTEM.SETTINGS.menu.Range.Icon,
type: DhpRangeSettings,
restricted: true
});
}
// const {HandlebarsApplicationMixin, ApplicationV2} = foundry.applications.api;
// export default class DhpSettings extends HandlebarsApplicationMixin(ApplicationV2) {
// constructor(actor, shortrest){
// super({});
// this.actor = actor;
// this.selectedActivity = null;
// this.shortrest = shortrest;
// this.customActivity = SYSTEM.GENERAL.downtime.custom;
// }
// get title(){
// return game.i18n.localize("DAGGERHEART.Application.Settings.Title");
// }
// static DEFAULT_OPTIONS = {
// tag: 'form',
// classes: ["daggerheart", "application", "settings"],
// position: { width: 800, height: 'auto' },
// actions: {
// selectActivity: this.selectActivity,
// },
// form: { handler: this.updateData }
// };
// static PARTS = {
// application: {
// id: "settings",
// template: "systems/daggerheart/templates/application/settings.hbs"
// }
// }
// async _prepareContext(_options) {
// const context = await super._prepareContext(_options);
// return context;
// }
// static async updateData(event, element, formData){
// this.render();
// }
// static close(){
// super.close();
// }
// }

View file

@ -0,0 +1,377 @@
// import DhpApplicationMixin from '../daggerheart-sheet.mjs';
// export class Teest extends DhpApplicationMixin(ActorSheet) {
// static documentType = "adversary";
// constructor(options){
// super(options);
// this.editMode = false;
// }
// /** @override */
// static get defaultOptions() {
// return foundry.utils.mergeObject(super.defaultOptions, {
// classes: ["daggerheart", "sheet", "adversary"],
// width: 600,
// height: 'auto',
// resizable: false,
// });
// }
// async getData() {
// const context = super.getData();
// context.config = SYSTEM;
// context.editMode = this.editMode;
// context.title = `${this.actor.name} - ${game.i18n.localize(SYSTEM.ACTOR.adversaryTypes[this.actor.system.type].name)}`;
// context.data = {
// description: this.object.system.description,
// motivesAndTactics: this.object.system.motivesAndTactics.join(', '),
// tier: this.object.system.tier,
// type: game.i18n.localize(SYSTEM.ACTOR.adversaryTypes[this.object.system.type].name),
// attack: {
// name: this.object.system.attack.name,
// attackModifier: this.object.system.attackModifier,
// range: this.object.system.attack.range ? game.i18n.localize(SYSTEM.GENERAL.range[this.object.system.attack.range].name) : null,
// damage: {
// value: this.object.system.attack.damage.value,
// type: this.object.system.attack.damage.type,
// typeName: this.object.system.attack.damage.type ? game.i18n.localize(SYSTEM.GENERAL.damageTypes[this.object.system.attack.damage.type].abbreviation).toLowerCase() : null,
// },
// },
// damageThresholds: this.object.system.damageThresholds,
// difficulty: this.object.system.difficulty,
// hp: { ...this.object.system.resources.health, lastRowIndex: Math.floor(this.object.system.resources.health.max/5)*5 },
// stress: { ...this.object.system.resources.stress, lastRowIndex: Math.floor(this.object.system.resources.stress.max/5)*5 },
// moves: this.object.system.moves,
// };
// return context;
// }
// async _handleAction(action, event, button) {
// switch(action){
// case 'viewMove':
// await this.viewMove(button);
// break;
// case 'addMove':
// this.addMove();
// break;
// case 'removeMove':
// await this.removeMove(button);
// break;
// case 'toggleSlider':
// this.toggleEditMode();
// break;
// case 'addMotive':
// await this.addMotive();
// break;
// case 'removeMotive':
// await this.removeMotive(button);
// break;
// case 'reactionRoll':
// await this.reactionRoll(event);
// break;
// case 'attackRoll':
// await this.attackRoll(event);
// break;
// case 'addExperience':
// await this.addExperience();
// break;
// case 'removeExperience':
// await this.removeExperience(button);
// break;
// case 'toggleHP':
// await this.toggleHP(button);
// break;
// case 'toggleStress':
// await this.toggleStress(button);
// break;
// }
// }
// async viewMove(button){
// const move = await fromUuid(button.dataset.move);
// move.sheet.render(true);
// }
// async addMove(){
// const result = await this.object.createEmbeddedDocuments("Item", [{
// name: game.i18n.localize('DAGGERHEART.Sheets.Adversary.NewMove'),
// type: 'feature',
// }]);
// await result[0].sheet.render(true);
// }
// async removeMove(button){
// await this.object.items.find(x => x.uuid === button.dataset.move).delete();
// }
// toggleEditMode(){
// this.editMode = !this.editMode;
// this.render();
// }
// async addMotive(){
// await this.object.update({ "system.motivesAndTactics": [...this.object.system.motivesAndTactics, ''] });
// }
// async removeMotive(button){
// await this.object.update({ "system.motivesAndTactics": this.object.system.motivesAndTactics.filter((_, index) => index !== Number.parseInt(button.dataset.motive) )});
// }
// async reactionRoll(event){
// const { roll, diceResults, modifiers } = await this.actor.diceRoll({ title: `${this.actor.name} - Reaction Roll`, value: 0 }, event.shiftKey);
// const cls = getDocumentClass("ChatMessage");
// const msg = new cls({
// type: 'adversaryRoll',
// system: {
// roll: roll._formula,
// total: roll._total,
// modifiers: modifiers,
// diceResults: diceResults,
// },
// content: "systems/daggerheart/templates/chat/adversary-roll.hbs",
// rolls: [roll]
// });
// cls.create(msg.toObject());
// }
// async attackRoll(event){
// const modifier = Number.parseInt(event.currentTarget.dataset.value);
// const { roll, diceResults, modifiers } = await this.actor.diceRoll({ title: `${this.actor.name} - Attack Roll`, value: modifier }, event.shiftKey);
// const targets = Array.from(game.user.targets).map(x => ({
// id: x.id,
// name: x.actor.name,
// img: x.actor.img,
// difficulty: x.actor.system.difficulty,
// evasion: x.actor.system.evasion,
// }));
// const cls = getDocumentClass("ChatMessage");
// const msg = new cls({
// type: 'adversaryRoll',
// system: {
// roll: roll._formula,
// total: roll._total,
// modifiers: modifiers,
// diceResults: diceResults,
// targets: targets,
// damage: { value: event.currentTarget.dataset.damage, type: event.currentTarget.dataset.damageType },
// },
// content: "systems/daggerheart/templates/chat/adversary-attack-roll.hbs",
// rolls: [roll]
// });
// cls.create(msg.toObject());
// }
// async addExperience(){
// await this.object.update({ "system.experiences": [...this.object.system.experiences, { name: 'Experience', value: 1 }] });
// }
// async removeExperience(button){
// await this.object.update({ "system.experiences": this.object.system.experiences.filter((_, index) => index !== Number.parseInt(button.dataset.experience) )});
// }
// async toggleHP(button){
// const index = Number.parseInt(button.dataset.index);
// const newHP = index < this.object.system.resources.health.value ? index : index+1;
// await this.object.update({ "system.resources.health.value": newHP });
// }
// async toggleStress(button){
// const index = Number.parseInt(button.dataset.index);
// const newStress = index < this.object.system.resources.stress.value ? index : index+1;
// await this.object.update({ "system.resources.stress.value": newStress });
// }
// }
import DaggerheartSheet from './daggerheart-sheet.mjs';
const { ActorSheetV2 } = foundry.applications.sheets;
export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) {
constructor(options={}){
super(options);
this.editMode = false;
}
static DEFAULT_OPTIONS = {
tag: 'form',
id: "daggerheart-adversary",
classes: ["daggerheart", "sheet", "adversary"],
position: { width: 600 },
actions: {
viewMove: this.viewMove,
addMove: this.addMove,
removeMove: this.removeMove,
toggleSlider: this.toggleEditMode,
addMotive: this.addMotive,
removeMotive: this.removeMotive,
reactionRoll: this.reactionRoll,
attackRoll: this.attackRoll,
addExperience: this.addExperience,
removeExperience: this.removeExperience,
toggleHP: this.toggleHP,
toggleStress: this.toggleStress,
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false,
},
};
static PARTS = {
form: {
id: "feature",
template: "systems/daggerheart/templates/sheets/adversary.hbs"
}
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.config = SYSTEM;
context.editMode = this.editMode;
context.title = `${this.actor.name} - ${game.i18n.localize(SYSTEM.ACTOR.adversaryTypes[this.actor.system.type].name)}`;
context.data = {
description: this.document.system.description,
motivesAndTactics: this.document.system.motivesAndTactics.join(', '),
tier: this.document.system.tier,
type: game.i18n.localize(SYSTEM.ACTOR.adversaryTypes[this.document.system.type].name),
attack: {
name: this.document.system.attack.name,
attackModifier: this.document.system.attackModifier,
range: this.document.system.attack.range ? game.i18n.localize(SYSTEM.GENERAL.range[this.document.system.attack.range].name) : null,
damage: {
value: this.document.system.attack.damage.value,
type: this.document.system.attack.damage.type,
typeName: this.document.system.attack.damage.type ? game.i18n.localize(SYSTEM.GENERAL.damageTypes[this.document.system.attack.damage.type].abbreviation).toLowerCase() : null,
},
},
damageThresholds: this.document.system.damageThresholds,
difficulty: this.document.system.difficulty,
hp: { ...this.document.system.resources.health, lastRowIndex: Math.floor(this.document.system.resources.health.max/5)*5 },
stress: { ...this.document.system.resources.stress, lastRowIndex: Math.floor(this.document.system.resources.stress.max/5)*5 },
moves: this.document.system.moves,
};
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object)
this.render();
}
static async viewMove(_, button){
const move = await fromUuid(button.dataset.move);
move.sheet.render(true);
}
static async addMove(){
const result = await this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize('DAGGERHEART.Sheets.Adversary.NewMove'),
type: 'feature',
}]);
await result[0].sheet.render(true);
}
static async removeMove(_, button){
await this.document.items.find(x => x.uuid === button.dataset.move).delete();
}
static toggleEditMode(){
this.editMode = !this.editMode;
this.render();
}
static async addMotive(){
await this.document.update({ "system.motivesAndTactics": [...this.document.system.motivesAndTactics, ''] });
}
static async removeMotive(button){
await this.document.update({ "system.motivesAndTactics": this.document.system.motivesAndTactics.filter((_, index) => index !== Number.parseInt(button.dataset.motive) )});
}
static async reactionRoll(event){
const { roll, diceResults, modifiers } = await this.actor.diceRoll({ title: `${this.actor.name} - Reaction Roll`, value: 0 }, event.shiftKey);
const cls = getDocumentClass("ChatMessage");
const msg = new cls({
type: 'adversaryRoll',
system: {
roll: roll._formula,
total: roll._total,
modifiers: modifiers,
diceResults: diceResults,
},
content: "systems/daggerheart/templates/chat/adversary-roll.hbs",
rolls: [roll]
});
cls.create(msg.toObject());
}
static async attackRoll(event, button){
const modifier = Number.parseInt(button.dataset.value);
const { roll, diceResults, modifiers } = await this.actor.diceRoll({ title: `${this.actor.name} - Attack Roll`, value: modifier }, event.shiftKey);
const targets = Array.from(game.user.targets).map(x => ({
id: x.id,
name: x.actor.name,
img: x.actor.img,
difficulty: x.actor.system.difficulty,
evasion: x.actor.system.evasion,
}));
const cls = getDocumentClass("ChatMessage");
const msg = new cls({
type: 'adversaryRoll',
system: {
roll: roll._formula,
total: roll._total,
modifiers: modifiers,
diceResults: diceResults,
targets: targets,
damage: { value: button.dataset.damage, type: button.dataset.damageType },
},
content: "systems/daggerheart/templates/chat/adversary-attack-roll.hbs",
rolls: [roll]
});
cls.create(msg.toObject());
}
static async addExperience(){
await this.document.update({ "system.experiences": [...this.document.system.experiences, { name: 'Experience', value: 1 }] });
}
static async removeExperience(_, button){
await this.document.update({ "system.experiences": this.document.system.experiences.filter((_, index) => index !== Number.parseInt(button.dataset.experience) )});
}
static async toggleHP(_, button){
const index = Number.parseInt(button.dataset.index);
const newHP = index < this.document.system.resources.health.value ? index : index+1;
await this.document.update({ "system.resources.health.value": newHP });
}
static async toggleStress(_, button){
const index = Number.parseInt(button.dataset.index);
const newStress = index < this.document.system.resources.stress.value ? index : index+1;
await this.document.update({ "system.resources.stress.value": newStress });
}
}

View file

@ -0,0 +1,117 @@
// import DhpApplicationMixin from '../daggerheart-sheet.mjs';
// export default class AncestrySheet extends DhpApplicationMixin(ItemSheet) {
// static documentType = "ancestry";
// constructor(options){
// super(options);
// }
// /** @override */
// static get defaultOptions() {
// return foundry.utils.mergeObject(super.defaultOptions, {
// classes: ["daggerheart", "sheet", "heritage"],
// width: 600,
// height: 'auto',
// dragDrop: [{ dragSelector: null, dropSelector: null }],
// });
// }
// /** @override */
// getData() {
// const context = super.getData();
// return context;
// }
// async _handleAction(action, event, button) {
// switch(action){
// case 'editAbility':
// this.editAbility(button);
// break;
// case 'deleteAbility':
// this.deleteAbility(event);
// break;
// }
// }
// async editAbility(button){
// const feature = await fromUuid(button.dataset.ability);
// feature.sheet.render(true);
// }
// async deleteAbility(event){
// event.preventDefault();
// event.stopPropagation();
// await this.item.update({ "system.abilities": this.item.system.abilities.filter(x => x.uuid !== event.currentTarget.dataset.ability) })
// }
// async _onDrop(event) {
// const data = TextEditor.getDragEventData(event);
// const item = await fromUuid(data.uuid);
// if(item.type === 'feature' && item.system.type === SYSTEM.ITEM.featureTypes.ancestry.id) {
// await this.object.update({ "system.abilities": [...this.item.system.abilities, { img: item.img, name: item.name, uuid: item.uuid }] });
// }
// }
// }
import DaggerheartSheet from './daggerheart-sheet.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class AncestrySheet extends DaggerheartSheet(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
id: "daggerheart-ancestry",
classes: ["daggerheart", "sheet", "heritage"],
position: { width: 600 },
actions: {
editAbility: this.editAbility,
deleteAbility: this.deleteAbility,
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false,
},
dragDrop: [{ dragSelector: null, dropSelector: null }],
};
static PARTS = {
form: {
id: "feature",
template: "systems/daggerheart/templates/sheets/ancestry.hbs"
}
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object)
this.render();
}
static async editAbility(_, button){
const feature = await fromUuid(button.dataset.ability);
feature.sheet.render(true);
}
static async deleteAbility(event, button){
event.preventDefault();
event.stopPropagation();
await this.item.update({ "system.abilities": this.item.system.abilities.filter(x => x.uuid !== button.dataset.ability) })
}
async _onDrop(event) {
const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
if(item.type === 'feature' && item.system.type === SYSTEM.ITEM.featureTypes.ancestry.id) {
await this.document.update({ "system.abilities": [...this.document.system.abilities, { img: item.img, name: item.name, uuid: item.uuid }] });
}
}
}

View file

@ -0,0 +1,65 @@
// import DhpApplicationMixin from '../daggerheart-sheet.mjs';
// export default class ArmorSheet extends DhpApplicationMixin(ItemSheet) {
// static documentType = "armor";
// /** @override */
// static get defaultOptions() {
// return foundry.utils.mergeObject(super.defaultOptions, {
// classes: ["daggerheart", "sheet", "armor"],
// width: 400,
// height: 'auto',
// });
// }
// /** @override */
// getData() {
// const context = super.getData();
// context.config = CONFIG.daggerheart;
// return context;
// }
// async _handleAction(action, event, button) {
// switch(action){
// }
// }
// }
import DaggerheartSheet from './daggerheart-sheet.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class ArmorSheet extends DaggerheartSheet(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
id: "daggerheart-armor",
classes: ["daggerheart", "sheet", "armor"],
position: { width: 400 },
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false,
},
dragDrop: [{ dragSelector: null, dropSelector: null }],
};
static PARTS = {
form: {
id: "feature",
template: "systems/daggerheart/templates/sheets/armor.hbs"
}
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.config = CONFIG.daggerheart;
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object)
this.render();
}
}

View file

@ -0,0 +1,419 @@
// import DhpApplicationMixin from '../daggerheart-sheet.mjs';
// import Tagify from '@yaireo/tagify';
// export default class ClassSheet extends DhpApplicationMixin(ItemSheet) {
// static documentType = "class";
// /** @override */
// static get defaultOptions() {
// return foundry.utils.mergeObject(super.defaultOptions, {
// classes: ["daggerheart", "sheet", "class"],
// width: 600,
// height: 'auto',
// resizable: false,
// tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "features" }],
// dragDrop: [
// { dragSelector: '.suggested-item', dropSelector: null },
// { dragSelector: null, dropSelector: '.take-section' },
// { dragSelector: null, dropSelector: '.choice-a-section' },
// { dragSelector: null, dropSelector: '.choice-b-section' },
// { dragSelector: null, dropSelector: '.primary-weapon-section' },
// { dragSelector: null, dropSelector: '.secondary-weapon-section' },
// { dragSelector: null, dropSelector: '.armor-section' },
// { dragSelector: null, dropSelector: null },
// ]
// });
// }
// /** @override */
// async getData() {
// const context = super.getData();
// context.domains = this.object.system.domains.map(x => SYSTEM.DOMAIN.domains[x].name)
// return context;
// }
// activateListeners(html){
// super.activateListeners(html);
// const domainInput = $(html).find('.domain-input')[0];
// const domainTagify = new Tagify(domainInput, {
// tagTextProp: "name",
// enforceWhitelist: true,
// whitelist : Object.keys(SYSTEM.DOMAIN.domains).map(key => {
// const domain = SYSTEM.DOMAIN.domains[key];
// return { value: key, name: game.i18n.localize(domain.name), src: domain.src, background: domain.background };
// }),
// maxTags: 2,
// callbacks : { invalid: this.onAddTag },
// dropdown : {
// mapValueTo: 'name',
// searchKeys: ['name'],
// enabled: 0,
// maxItems: 20,
// closeOnSelect : true,
// highlightFirst: false,
// },
// templates: {
// tag(tagData){ //z-index: unset; background-image: ${tagData.background}; Maybe a domain specific background for the chips?
// return `<tag title="${(tagData.title || tagData.value)}"
// contenteditable='false'
// spellcheck='false'
// tabIndex="${this.settings.a11y.focusableTags ? 0 : -1}"
// class="${this.settings.classNames.tag} ${tagData.class ? tagData.class : ""}"
// ${this.getAttributes(tagData)}>
// <x class="${this.settings.classNames.tagX}" role='button' aria-label='remove tag'></x>
// <div>
// <span class="${this.settings.classNames.tagText}">${tagData[this.settings.tagTextProp] || tagData.value}</span>
// <img src="${tagData.src}"></i>
// </div>
// </tag>`;
// }}
// });
// domainTagify.on('change', this.onDomainSelect.bind(this));
// }
// onAddTag(e){
// if( e.detail.index ===2 ){
// ui.notifications.info(game.i18n.localize("DAGGERHEART.Notification.Info.ClassCanOnlyHaveTwoDomains"));
// }
// }
// async onDomainSelect(event) {
// const domains = event.detail?.value ? JSON.parse(event.detail.value) : [];
// await this.object.update({ "system.domains": domains.map(x => x.value) });
// this.render(true);
// }
// async _handleAction(action, event, button) {
// switch(action){
// case 'removeSubclass':
// await this.removeSubclass(button);
// break;
// case 'viewSubclass':
// await this.viewSubclass(button);
// break;
// case 'removeFeature':
// await this.removeFeature(button);
// break;
// case 'viewFeature':
// await this.viewFeature(button);
// break;
// case 'removeItem':
// await this.removeItem(event);
// break;
// case 'viewItem':
// await this.viewItem(button);
// break;
// case 'removePrimaryWeapon':
// await this.removePrimaryWeapon(event);
// break;
// case 'removeSecondaryWeapon':
// await this.removeSecondaryWeapon(event);
// break;
// case 'removeArmor':
// await this.removeArmor(event);
// break;
// }
// }
// async removeSubclass(button){
// await this.object.update({ "system.subclasses": this.object.system.subclasses.filter(x => x.uuid !== button.dataset.subclass)});
// }
// async viewSubclass(button){
// const subclass = await fromUuid(button.dataset.subclass);
// subclass.sheet.render(true);
// }
// async removeFeature(button){
// await this.object.update({ "system.features": this.object.system.features.filter(x => x.uuid !== button.dataset.feature)});
// }
// async viewFeature(button){
// const feature = await fromUuid(button.dataset.feature);
// feature.sheet.render(true);
// }
// async removeItem(event){
// event.stopPropagation();
// const type = event.currentTarget.dataset.type;
// const path = `system.inventory.${type}`;
// await this.object.update({ [path]: this.object.system.inventory[type].filter(x => x.uuid !== event.currentTarget.dataset.item)});
// }
// async viewItem(button){
// const item = await fromUuid(button.dataset.item);
// item.sheet.render(true);
// }
// async removePrimaryWeapon(event){
// event.stopPropagation();
// await this.object.update({ "system.characterGuide.suggestedPrimaryWeapon": null }, { diff: false });
// }
// async removeSecondaryWeapon(event){
// event.stopPropagation();
// await this.object.update({ "system.characterGuide.suggestedSecondaryWeapon": null }, { diff: false });
// }
// async removeArmor(event){
// event.stopPropagation();
// await this.object.update({ "system.characterGuide.suggestedArmor": null }, { diff: false });
// }
// async _onDragStart(event){
// if(event.currentTarget.classList.contains('suggested-item')){
// event.dataTransfer.setData("text/plain", JSON.stringify({ type: 'Item', uuid: event.currentTarget.dataset.item }));
// }
// super._onDragStart(event);
// }
// async _onDrop(event) {
// const data = TextEditor.getDragEventData(event);
// const item = await fromUuid(data.uuid);
// if(item.type === 'subclass') {
// await this.object.update({ "system.subclasses": [...this.object.system.subclasses, { img: item.img, name: item.name, uuid: item.uuid }] });
// }
// else if(item.type === 'feature') {
// await this.object.update({ "system.features": [...this.object.system.features, { img: item.img, name: item.name, uuid: item.uuid }] });
// }
// else if(item.type === 'weapon'){
// if(event.currentTarget.classList.contains('primary-weapon-section')){
// if(!this.object.system.characterGuide.suggestedPrimaryWeapon && !item.system.secondary) await this.object.update({ "system.characterGuide.suggestedPrimaryWeapon": { img: item.img, name: item.name, uuid: item.uuid } });
// } else if(event.currentTarget.classList.contains('secondary-weapon-section')){
// if(!this.object.system.characterGuide.suggestedSecondaryWeapon && item.system.secondary) await this.object.update({ "system.characterGuide.suggestedSecondaryWeapon": { img: item.img, name: item.name, uuid: item.uuid } });
// }
// }
// else if(item.type === 'armor'){
// if(event.currentTarget.classList.contains('armor-section')){
// if(!this.object.system.characterGuide.suggestedArmor) await this.object.update({ "system.characterGuide.suggestedArmor": { img: item.img, name: item.name, uuid: item.uuid } });
// }
// }
// else if(event.currentTarget.classList.contains('choice-a-section')){
// if(item.type === 'miscellaneous' || item.type === 'consumable'){
// if(this.object.system.inventory.choiceA.length < 2) await this.object.update({ "system.inventory.choiceA": [...this.object.system.inventory.choiceA, { img: item.img, name: item.name, uuid: item.uuid }] });
// }
// }
// else if(item.type === 'miscellaneous'){
// if(event.currentTarget.classList.contains('take-section')){
// if(this.object.system.inventory.take.length < 3) await this.object.update({ "system.inventory.take": [...this.object.system.inventory.take, { img: item.img, name: item.name, uuid: item.uuid }] });
// }
// else if(event.currentTarget.classList.contains('choice-b-section')){
// if(this.object.system.inventory.choiceB.length < 2) await this.object.update({ "system.inventory.choiceB": [...this.object.system.inventory.choiceB, { img: item.img, name: item.name, uuid: item.uuid }] });
// }
// }
// }
// }
import DaggerheartSheet from './daggerheart-sheet.mjs';
import Tagify from "@yaireo/tagify";
const { ItemSheetV2 } = foundry.applications.sheets;
export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
id: "daggerheart-class",
classes: ["daggerheart", "sheet", "class"],
position: { width: 600 },
actions: {
removeSubclass: this.removeSubclass,
viewSubclass: this.viewSubclass,
removeFeature: this.removeFeature,
viewFeature: this.viewFeature,
removeItem: this.removeItem,
viewItem: this.viewItem,
removePrimaryWeapon: this.removePrimaryWeapon,
removeSecondaryWeapon: this.removeSecondaryWeapon,
removeArmor: this.removeArmor,
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false,
},
dragDrop: [
{ dragSelector: '.suggested-item', dropSelector: null },
{ dragSelector: null, dropSelector: '.take-section' },
{ dragSelector: null, dropSelector: '.choice-a-section' },
{ dragSelector: null, dropSelector: '.choice-b-section' },
{ dragSelector: null, dropSelector: '.primary-weapon-section' },
{ dragSelector: null, dropSelector: '.secondary-weapon-section' },
{ dragSelector: null, dropSelector: '.armor-section' },
{ dragSelector: null, dropSelector: null },
]
};
static PARTS = {
form: {
id: "feature",
template: "systems/daggerheart/templates/sheets/class.hbs"
}
}
_getTabs() {
const tabs = {
features: { active: true, cssClass: '', group: 'primary', id: 'features', icon: null, label: game.i18n.localize('DAGGERHEART.Sheets.Class.Tabs.Features') },
guide: { active: false, cssClass: '', group: 'primary', id: 'guide', icon: null, label: game.i18n.localize('DAGGERHEART.Sheets.Class.Tabs.Guide') },
}
for ( const v of Object.values(tabs) ) {
v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active;
v.cssClass = v.active ? "active" : "";
}
return tabs;
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
const domainInput = htmlElement.querySelector('.domain-input');
const domainTagify = new Tagify(domainInput, {
tagTextProp: "name",
enforceWhitelist: true,
whitelist : Object.keys(SYSTEM.DOMAIN.domains).map(key => {
const domain = SYSTEM.DOMAIN.domains[key];
return { value: key, name: game.i18n.localize(domain.label), src: domain.src, background: domain.background };
}),
maxTags: 2,
callbacks : { invalid: this.onAddTag },
dropdown : {
mapValueTo: 'name',
searchKeys: ['name'],
enabled: 0,
maxItems: 20,
closeOnSelect : true,
highlightFirst: false,
},
templates: {
tag(tagData){ //z-index: unset; background-image: ${tagData.background}; Maybe a domain specific background for the chips?
return `<tag title="${(tagData.title || tagData.value)}"
contenteditable='false'
spellcheck='false'
tabIndex="${this.settings.a11y.focusableTags ? 0 : -1}"
class="${this.settings.classNames.tag} ${tagData.class ? tagData.class : ""}"
${this.getAttributes(tagData)}>
<x class="${this.settings.classNames.tagX}" role='button' aria-label='remove tag'></x>
<div>
<span class="${this.settings.classNames.tagText}">${tagData[this.settings.tagTextProp] || tagData.value}</span>
<img src="${tagData.src}"></i>
</div>
</tag>`;
}}
});
domainTagify.on('change', this.onDomainSelect.bind(this));
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.tabs = this._getTabs();
context.domains = this.document.system.domains.map(x => SYSTEM.DOMAIN.domains[x].label);
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object)
this.render();
}
onAddTag(e){
if( e.detail.index ===2 ){
ui.notifications.info(game.i18n.localize("DAGGERHEART.Notification.Info.ClassCanOnlyHaveTwoDomains"));
}
}
async onDomainSelect(event) {
const domains = event.detail?.value ? JSON.parse(event.detail.value) : [];
await this.document.update({ "system.domains": domains.map(x => x.value) });
this.render(true);
}
static async removeSubclass(_, button){
await this.document.update({ "system.subclasses": this.document.system.subclasses.filter(x => x.uuid !== button.dataset.subclass)});
}
static async viewSubclass(_, button){
const subclass = await fromUuid(button.dataset.subclass);
subclass.sheet.render(true);
}
static async removeFeature(_, button){
await this.document.update({ "system.features": this.document.system.features.filter(x => x.uuid !== button.dataset.feature)});
}
static async viewFeature(_, button){
const feature = await fromUuid(button.dataset.feature);
feature.sheet.render(true);
}
static async removeItem(event, button){
event.stopPropagation();
const type = button.dataset.type;
const path = `system.inventory.${type}`;
await this.document.update({ [path]: this.document.system.inventory[type].filter(x => x.uuid !== button.dataset.item)});
}
static async viewItem(_, button){
const item = await fromUuid(button.dataset.item);
item.sheet.render(true);
}
static async removePrimaryWeapon(event){
event.stopPropagation();
await this.document.update({ "system.characterGuide.suggestedPrimaryWeapon": null }, { diff: false });
}
static async removeSecondaryWeapon(event){
event.stopPropagation();
await this.document.update({ "system.characterGuide.suggestedSecondaryWeapon": null }, { diff: false });
}
static async removeArmor(event){
event.stopPropagation();
await this.document.update({ "system.characterGuide.suggestedArmor": null }, { diff: false });
}
async _onDrop(event) {
const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
if(item.type === 'subclass') {
await this.document.update({ "system.subclasses": [...this.document.system.subclasses, { img: item.img, name: item.name, uuid: item.uuid }] });
}
else if(item.type === 'feature') {
await this.document.update({ "system.features": [...this.document.system.features, { img: item.img, name: item.name, uuid: item.uuid }] });
}
else if(item.type === 'weapon'){
if(event.currentTarget.classList.contains('primary-weapon-section')){
if(!this.document.system.characterGuide.suggestedPrimaryWeapon && !item.system.secondary) await this.document.update({ "system.characterGuide.suggestedPrimaryWeapon": { img: item.img, name: item.name, uuid: item.uuid } });
} else if(event.currentTarget.classList.contains('secondary-weapon-section')){
if(!this.document.system.characterGuide.suggestedSecondaryWeapon && item.system.secondary) await this.document.update({ "system.characterGuide.suggestedSecondaryWeapon": { img: item.img, name: item.name, uuid: item.uuid } });
}
}
else if(item.type === 'armor'){
if(event.currentTarget.classList.contains('armor-section')){
if(!this.document.system.characterGuide.suggestedArmor) await this.document.update({ "system.characterGuide.suggestedArmor": { img: item.img, name: item.name, uuid: item.uuid } });
}
}
else if(event.currentTarget.classList.contains('choice-a-section')){
if(item.type === 'miscellaneous' || item.type === 'consumable'){
if(this.document.system.inventory.choiceA.length < 2) await this.document.update({ "system.inventory.choiceA": [...this.document.system.inventory.choiceA, { img: item.img, name: item.name, uuid: item.uuid }] });
}
}
else if(item.type === 'miscellaneous'){
if(event.currentTarget.classList.contains('take-section')){
if(this.document.system.inventory.take.length < 3) await this.document.update({ "system.inventory.take": [...this.document.system.inventory.take, { img: item.img, name: item.name, uuid: item.uuid }] });
}
else if(event.currentTarget.classList.contains('choice-b-section')){
if(this.document.system.inventory.choiceB.length < 2) await this.document.update({ "system.inventory.choiceB": [...this.document.system.inventory.choiceB, { img: item.img, name: item.name, uuid: item.uuid }] });
}
}
}
}

View file

@ -0,0 +1,117 @@
// import DhpApplicationMixin from '../daggerheart-sheet.mjs';
// export default class CommunitySheet extends DhpApplicationMixin(ItemSheet) {
// static documentType = "community";
// constructor(options){
// super(options);
// }
// /** @override */
// static get defaultOptions() {
// return foundry.utils.mergeObject(super.defaultOptions, {
// classes: ["daggerheart", "sheet", "heritage"],
// width: 600,
// height: 'auto',
// dragDrop: [{ dragSelector: null, dropSelector: null }],
// });
// }
// /** @override */
// getData() {
// const context = super.getData();
// return context;
// }
// async _handleAction(action, event, button) {
// switch(action){
// case 'editAbility':
// this.editAbility(button);
// break;
// case 'deleteAbility':
// this.deleteAbility(event);
// break;
// }
// }
// async editAbility(button){
// const feature = await fromUuid(button.dataset.ability);
// feature.sheet.render(true);
// }
// async deleteAbility(event){
// event.preventDefault();
// event.stopPropagation();
// await this.item.update({ "system.abilities": this.item.system.abilities.filter(x => x.uuid !== event.currentTarget.dataset.ability) })
// }
// async _onDrop(event) {
// const data = TextEditor.getDragEventData(event);
// const item = await fromUuid(data.uuid);
// if(item.type === 'feature' && item.system.type === SYSTEM.ITEM.featureTypes.community.id) {
// await this.object.update({ "system.abilities": [...this.item.system.abilities, { img: item.img, name: item.name, uuid: item.uuid }] });
// }
// }
// }
import DaggerheartSheet from './daggerheart-sheet.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class CommunitySheet extends DaggerheartSheet(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
id: "daggerheart-community",
classes: ["daggerheart", "sheet", "heritage"],
position: { width: 600 },
actions: {
editAbility: this.editAbility,
deleteAbility: this.deleteAbility,
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false,
},
dragDrop: [{ dragSelector: null, dropSelector: null }],
};
static PARTS = {
form: {
id: "feature",
template: "systems/daggerheart/templates/sheets/community.hbs"
}
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object)
this.render();
}
static async editAbility(_, button){
const feature = await fromUuid(button.dataset.ability);
feature.sheet.render(true);
}
static async deleteAbility(event, button){
event.preventDefault();
event.stopPropagation();
await this.item.update({ "system.abilities": this.item.system.abilities.filter(x => x.uuid !== button.dataset.ability) })
}
async _onDrop(event) {
const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
if(item.type === 'feature' && item.system.type === SYSTEM.ITEM.featureTypes.community.id) {
await this.document.update({ "system.abilities": [...this.document.system.abilities, { img: item.img, name: item.name, uuid: item.uuid }] });
}
}
}

View file

@ -0,0 +1,57 @@
// import DhpApplicationMixin from '../daggerheart-sheet.mjs';
// export default class ConsumableSheet extends DhpApplicationMixin(ItemSheet) {
// static documentType = "consumable";
// /** @override */
// static get defaultOptions() {
// return foundry.utils.mergeObject(super.defaultOptions, {
// classes: ["daggerheart", "sheet", "consumable"],
// width: 480,
// height: 'auto',
// });
// }
// /** @override */
// getData() {
// const context = super.getData();
// return context;
// }
// }
import DaggerheartSheet from './daggerheart-sheet.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class ConsumableSheet extends DaggerheartSheet(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
id: "daggerheart-consumable",
classes: ["daggerheart", "sheet", "consumable"],
position: { width: 480 },
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false,
},
};
static PARTS = {
form: {
id: "feature",
template: "systems/daggerheart/templates/sheets/consumable.hbs"
}
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object)
this.render();
}
}

View file

@ -0,0 +1,73 @@
const { HandlebarsApplicationMixin } = foundry.applications.api;
export default function DhpApplicationMixin(Base) {
return class DhpSheetV2 extends HandlebarsApplicationMixin(Base) {
constructor(options={}){
super(options);
this._dragDrop = this._createDragDropHandlers();
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
this._dragDrop.forEach(d => d.bind(htmlElement));
}
static DEFAULT_OPTIONS = {
position: {
width: 480,
height: "auto"
},
actions: {
onEditImage: this._onEditImage
},
dragDrop: [],
};
async _prepareContext(_options, objectPath='document') {
const context = await super._prepareContext(_options);
context.source = this[objectPath].toObject();
context.fields = this[objectPath].schema.fields;
context.systemFields = this[objectPath].system ? this[objectPath].system.schema.fields : {};
return context;
}
static _onEditImage(event, target) {
const attr = target.dataset.edit;
const current = foundry.utils.getProperty(this.document, attr);
const { img } = this.document.constructor.getDefaultArtwork?.(this.document.toObject()) ?? {};
const fp = new FilePicker({
current,
type: "image",
redirectToRoot: img ? [img] : [],
callback: async path => this._updateImage.bind(this)(path),
top: this.position.top + 40,
left: this.position.left + 10
});
return fp.browse();
}
async _updateImage(path){
await this.document.update({ "img": path });
}
_createDragDropHandlers() {
return this.options.dragDrop.map(d => {
// d.permissions = {
// dragstart: this._canDragStart.bind(this),
// drop: this._canDragDrop.bind(this)
// };
d.callbacks = {
// dragstart: this._onDragStart.bind(this),
// dragover: this._onDragOver.bind(this),
drop: this._onDrop.bind(this)
};
return new foundry.applications.ux.DragDrop.implementation(d);
});
}
_onDrop(event) {}
}
}

View file

@ -0,0 +1,110 @@
// import DhpApplicationMixin from '../daggerheart-sheet.mjs';
// export default class DomainCardSheet extends DhpApplicationMixin(ItemSheet) {
// static documentType = "domainCard";
// /** @override */
// static get defaultOptions() {
// return foundry.utils.mergeObject(super.defaultOptions, {
// classes: ["daggerheart", "sheet", "domain-card"],
// width: 600,
// height: 600,
// tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "features" }]
// });
// }
// /** @override */
// getData() {
// const context = super.getData();
// context.config = CONFIG.daggerheart;
// return context;
// }
// async _handleAction(action, event, button) {
// switch(action){
// case 'attributeRoll':
// break;
// }
// }
// }
import DaggerheartAction from '../../data/action.mjs';
import DaggerheartActionConfig from '../config/Action.mjs';
import DaggerheartSheet from './daggerheart-sheet.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class DomainCardSheet extends DaggerheartSheet(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
id: "daggerheart-domainCard",
classes: ["daggerheart", "sheet", "domain-card"],
position: { width: 600, height: 600 },
actions: {
addAction: this.addAction,
editAction: this.editAction,
removeAction: this.removeAction,
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false,
},
};
static PARTS = {
form: {
id: "feature",
template: "systems/daggerheart/templates/sheets/domainCard.hbs"
}
}
_getTabs() {
const tabs = {
general: { active: true, cssClass: '', group: 'primary', id: 'general', icon: null, label: 'General' },
actions: { active: false, cssClass: '', group: 'primary', id: 'actions', icon: null, label: 'Actions' },
}
for ( const v of Object.values(tabs) ) {
v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active;
v.cssClass = v.active ? "active" : "";
}
return tabs;
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.config = CONFIG.daggerheart;
context.tabs = this._getTabs();
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object)
this.render();
}
static async addAction(){
const actionIndexes = this.document.system.actions.map(x => x.id.split('-')[2]).sort((a, b) => a-b);
const action = await new DaggerheartAction({
id: `${this.document.id}-Action-${actionIndexes.length > 0 ? actionIndexes[0]+1 : 1}`,
}, {
parent: this.document,
});
await this.document.update({ "system.actions": [...this.document.system.actions, action] });
await (new DaggerheartActionConfig(this.document.system.actions[this.document.system.actions.length-1])).render(true);
}
static async editAction(_, button){
const action = this.document.system.actions[button.dataset.index];
await (new DaggerheartActionConfig(action)).render(true);
}
static async removeAction(event, button){
event.stopPropagation();
await this.document.update({ "system.actions": this.document.system.actions.filter((_, index) => index !== Number.parseInt(button.dataset.index)) });
}
}

View file

@ -0,0 +1,129 @@
import DaggerheartSheet from "./daggerheart-sheet.mjs";
const { DocumentSheetV2 } = foundry.applications.api;
export default class DhpEnvironment extends DaggerheartSheet(DocumentSheetV2) {
constructor(options){
super(options);
this.editMode = false;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ["daggerheart", "sheet", "adversary", "environment"],
position: {
width: 600,
height: "auto"
},
actions: {
toggleSlider: this.toggleSlider,
viewFeature: this.viewFeature,
addFeature: this.addFeature,
removeFeature: this.removeFeature,
addTone: this.addTone,
removeTone: this.removeTone,
useFeature: this.useFeature,
},
form: {
handler: this._updateForm,
closeOnSubmit: false,
submitOnChange: true,
}
};
/** @override */
static PARTS = {
form: {
id: "form",
template: "systems/daggerheart/templates/sheets/environment.hbs"
}
}
/* -------------------------------------------- */
/** @inheritDoc */
get title() {
return `${game.i18n.localize('Environment')} - ${this.document.name}`;
}
async _prepareContext(_options) {
return {
title: `${this.document.name} - ${game.i18n.localize(SYSTEM.ACTOR.adversaryTypes[this.document.system.type].name)}`,
user: this.document,
source: this.document.toObject(),
fields: this.document.schema.fields,
data: {
type: game.i18n.localize(SYSTEM.ACTOR.adversaryTypes[this.document.system.type].name),
features: this.document.items.reduce((acc, x) => {
if(x.type === 'feature'){
const feature = x.toObject();
acc.push({
...feature,
system: {
...feature.system,
actionType: game.i18n.localize(SYSTEM.ITEM.actionTypes[feature.system.actionType].name)
},
uuid: x.uuid
});
}
return acc;
}, []),
},
editMode: this.editMode,
config: SYSTEM,
}
}
static async _updateForm(event, _, formData) {
await this.document.update(formData.object)
this.render();
}
static toggleSlider(){
this.editMode = !this.editMode;
this.render();
}
static async viewFeature(_, button){
const move = await fromUuid(button.dataset.feature);
move.sheet.render(true);
}
static async addFeature(){
const result = await this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize('DAGGERHEART.Sheets.Environment.NewFeature'),
type: 'feature',
}]);
await result[0].sheet.render(true);
}
static async removeFeature(_, button){
await this.document.items.find(x => x.uuid === button.dataset.feature).delete();
}
static async addTone(){
await this.document.update({ "system.toneAndFeel": [...this.document.system.toneAndFeel, ''] });
}
static async removeTone(button){
await this.document.update({ "system.toneAndFeel": this.document.system.toneAndFeel.filter((_, index) => index !== Number.parseInt(button.dataset.tone) )});
}
static async useFeature(_, button){
const item = this.document.items.find(x => x.uuid === button.dataset.feature);
const cls = getDocumentClass("ChatMessage");
const msg = new cls({
user: game.user.id,
content: await renderTemplate("systems/daggerheart/templates/chat/ability-use.hbs", {
title: game.i18n.format("DAGGERHEART.Chat.EnvironmentTitle", { actionType: button.dataset.actionType }),
card: { name: item.name, img: item.img, description: item.system.description },
}),
});
cls.create(msg.toObject());
}
}

View file

@ -0,0 +1,116 @@
import DaggerheartAction from '../../data/action.mjs';
import DaggerheartActionConfig from '../config/Action.mjs';
import DaggerheartSheet from './daggerheart-sheet.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class FeatureSheet extends DaggerheartSheet(ItemSheetV2) {
constructor(options={}){
super(options);
this.selectedEffectType = null;
}
static DEFAULT_OPTIONS = {
tag: 'form',
id: "daggerheart-feature",
classes: ["daggerheart", "sheet", "feature"],
position: { width: 600, height: 600 },
actions: {
addEffect: this.addEffect,
removeEffect: this.removeEffect,
addAction: this.addAction,
editAction: this.editAction,
removeAction: this.removeAction,
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false,
},
};
static PARTS = {
form: {
id: "feature",
template: "systems/daggerheart/templates/sheets/feature.hbs"
}
}
_getTabs() {
const tabs = {
features: { active: true, cssClass: '', group: 'primary', id: 'features', icon: null, label: 'Features' },
effects: { active: false, cssClass: '', group: 'primary', id: 'effects', icon: null, label: 'Effects' },
actions: { active: false, cssClass: '', group: 'primary', id: 'actions', icon: null, label: 'Actions' },
}
for ( const v of Object.values(tabs) ) {
v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active;
v.cssClass = v.active ? "active" : "";
}
return tabs;
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
$(htmlElement).find(".effect-select").on("change", this.effectSelect.bind(this));
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.tabs = this._getTabs(),
context.generalConfig = SYSTEM.GENERAL;
context.itemConfig = SYSTEM.ITEM;
context.properties = SYSTEM.ACTOR.featureProperties;
context.dice = SYSTEM.GENERAL.diceTypes;
context.selectedEffectType = this.selectedEffectType;
context.effectConfig = SYSTEM.EFFECTS;
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object)
this.render();
}
effectSelect(event){
this.selectedEffectType = event.currentTarget.value;
this.render(true);
}
static async addEffect(){
if(!this.selectedEffectType) return;
const { id, name, ...rest } = SYSTEM.EFFECTS.effectTypes[this.selectedEffectType];
const update = {
[foundry.utils.randomID()]: {
type: this.selectedEffectType,
value: '',
...rest
}
};
await this.item.update({ "system.effects": update });
}
static async removeEffect(_, button){
const path = `system.effects.-=${button.dataset.effect}`;
await this.item.update({ [path]: null });
}
static async addAction(){
const action = await new DaggerheartAction({}, {parent: this.document});
await this.document.update({ "system.actions": [...this.document.system.actions, action] });
await (new DaggerheartActionConfig(this.document.system.actions[this.document.system.actions.length-1])).render(true);
}
static async editAction(_, button){
const action = this.document.system.actions[button.dataset.index];
await (new DaggerheartActionConfig(action)).render(true);
}
static async removeAction(event, button){
event.stopPropagation();
await this.document.update({ "system.actions": this.document.system.actions.filter((_, index) => index !== Number.parseInt(button.dataset.index)) });
}
}

View file

@ -0,0 +1,57 @@
// import DhpApplicationMixin from '../daggerheart-sheet.mjs';
// export default class MiscellaneousSheet extends DhpApplicationMixin(ItemSheet) {
// static documentType = "miscellaneous";
// /** @override */
// static get defaultOptions() {
// return foundry.utils.mergeObject(super.defaultOptions, {
// classes: ["daggerheart", "sheet", "miscellaneous"],
// width: 400,
// height: 'auto',
// });
// }
// /** @override */
// getData() {
// const context = super.getData();
// return context;
// }
// }
import DaggerheartSheet from './daggerheart-sheet.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class MiscellaneousSheet extends DaggerheartSheet(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
id: "daggerheart-miscellaneous",
classes: ["daggerheart", "sheet", "miscellaneous"],
position: { width: 400 },
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false,
},
};
static PARTS = {
form: {
id: "feature",
template: "systems/daggerheart/templates/sheets/miscellaneous.hbs"
}
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object)
this.render();
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,168 @@
// import DhpApplicationMixin from '../daggerheart-sheet.mjs';
// export default class SubclassSheet extends DhpApplicationMixin(ItemSheet) {
// static documentType = "subclass";
// constructor(options){
// super(options);
// }
// static get defaultOptions() {
// return foundry.utils.mergeObject(super.defaultOptions, {
// classes: ["daggerheart", "sheet", "subclass"],
// width: 600,
// height: 720,
// tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "general" }],
// dragDrop: [
// { dragSelector: null, dropSelector: '.foundation-tab' },
// { dragSelector: null, dropSelector: '.specialization-tab' },
// { dragSelector: null, dropSelector: '.mastery-tab' }
// ],
// });
// }
// getData() {
// const context = super.getData();
// context.config = CONFIG.daggerheart;
// return context;
// }
// async _handleAction(action, event, button) {
// switch(action){
// case "editAbility":
// this.editAbility(button);
// break;
// case "deleteFeatureAbility":
// this.deleteFeatureAbility(event);
// break;
// }
// }
// async editAbility(button){
// const feature = await fromUuid(button.dataset.ability);
// feature.sheet.render(true);
// }
// async deleteFeatureAbility(event){
// event.preventDefault();
// event.stopPropagation();
// const feature = event.currentTarget.dataset.feature;
// const newAbilities = this.item.system[`${feature}Feature`].abilities.filter(x => x.uuid !== event.currentTarget.dataset.ability);
// const path = `system.${feature}Feature.abilities`;
// await this.item.update({ [path]: newAbilities });
// }
// async _onDrop(event) {
// const data = TextEditor.getDragEventData(event);
// const item = await fromUuid(data.uuid);
// if(item.type === 'feature' && item.system.type === SYSTEM.ITEM.featureTypes.subclass.id) {
// if(event.currentTarget.classList.contains('foundation-tab')){
// await this.object.update({ "system.foundationFeature.abilities": [...this.item.system.foundationFeature.abilities, { img: item.img, name: item.name, uuid: item.uuid }] });
// }
// else if(event.currentTarget.classList.contains('specialization-tab')){
// await this.object.update({ "system.specializationFeature.abilities": [...this.item.system.specializationFeature.abilities, { img: item.img, name: item.name, uuid: item.uuid }] });
// }
// else if(event.currentTarget.classList.contains('mastery-tab')){
// await this.object.update({ "system.masteryFeature.abilities": [...this.item.system.masteryFeature.abilities, { img: item.img, name: item.name, uuid: item.uuid }] });
// }
// }
// }
// }
import DaggerheartSheet from './daggerheart-sheet.mjs';
import DaggerheartFeature from '../../data/feature.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class SubclassSheet extends DaggerheartSheet(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
id: "daggerheart-subclass",
classes: ["daggerheart", "sheet", "subclass"],
position: { width: 600 },
actions: {
editAbility: this.editAbility,
deleteFeatureAbility: this.deleteFeatureAbility,
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false,
},
dragDrop: [
{ dragSelector: null, dropSelector: '.foundation-tab' },
{ dragSelector: null, dropSelector: '.specialization-tab' },
{ dragSelector: null, dropSelector: '.mastery-tab' }
],
};
_getTabs() {
const tabs = {
general: { active: true, cssClass: '', group: 'primary', id: 'general', icon: null, label: game.i18n.localize('DAGGERHEART.Sheets.Subclass.Tabs.General') },
foundation: { active: false, cssClass: '', group: 'primary', id: 'foundation', icon: null, label: game.i18n.localize('DAGGERHEART.Sheets.Subclass.Tabs.Foundation') },
specialization: { active: false, cssClass: '', group: 'primary', id: 'specialization', icon: null, label: game.i18n.localize('DAGGERHEART.Sheets.Subclass.Tabs.Specialization') },
mastery: { active: false, cssClass: '', group: 'primary', id: 'mastery', icon: null, label: game.i18n.localize('DAGGERHEART.Sheets.Subclass.Tabs.Mastery') },
}
for ( const v of Object.values(tabs) ) {
v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active;
v.cssClass = v.active ? "active" : "";
}
return tabs;
}
static PARTS = {
form: {
id: "feature",
template: "systems/daggerheart/templates/sheets/subclass.hbs"
}
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.config = CONFIG.daggerheart;
context.tabs = this._getTabs();
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object)
this.render();
}
static async editAbility(_, button){
const feature = await fromUuid(button.dataset.ability);
feature.sheet.render(true);
}
static async deleteFeatureAbility(event, button){
event.preventDefault();
event.stopPropagation();
const feature = button.dataset.feature;
const newAbilities = this.document.system[`${feature}Feature`].abilities.filter(x => x.uuid !== button.dataset.ability);
const path = `system.${feature}Feature.abilities`;
await this.document.update({ [path]: newAbilities });
}
async _onDrop(event) {
const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
if(item.type === 'feature' && item.system.type === SYSTEM.ITEM.featureTypes.subclass.id) {
if(event.currentTarget.classList.contains('foundation-tab')){
await this.document.update({ "system.foundationFeature.abilities": [...this.document.system.foundationFeature.abilities, item.system] });
}
else if(event.currentTarget.classList.contains('specialization-tab')){
await this.document.update({ "system.specializationFeature.abilities": [...this.document.system.specializationFeature.abilities, data.system] });
}
else if(event.currentTarget.classList.contains('mastery-tab')){
await this.document.update({ "system.masteryFeature.abilities": [...this.document.system.masteryFeature.abilities, data.system] });
}
}
}
}

View file

@ -0,0 +1,65 @@
// import DhpApplicationMixin from '../daggerheart-sheet.mjs';
// export default class WeaponSheet extends DhpApplicationMixin(ItemSheet) {
// static documentType = "weapon";
// /** @override */
// static get defaultOptions() {
// return foundry.utils.mergeObject(super.defaultOptions, {
// classes: ["daggerheart", "sheet", "weapon"],
// width: 400,
// height: 'auto',
// });
// }
// /** @override */
// getData() {
// const context = super.getData();
// context.config = CONFIG.daggerheart;
// return context;
// }
// async _handleAction(action, event, button) {
// switch(action){
// }
// }
// }
import DaggerheartSheet from './daggerheart-sheet.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class WeaponSheet extends DaggerheartSheet(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
id: "daggerheart-weapon",
classes: ["daggerheart", "sheet", "weapon"],
position: { width: 400 },
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false,
},
};
static PARTS = {
form: {
id: "feature",
template: "systems/daggerheart/templates/sheets/weapon.hbs"
}
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.config = CONFIG.daggerheart;
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object)
this.render();
}
}

View file

@ -0,0 +1,17 @@
export const actionTypes = {
damage: {
id: "damage",
name: "DAGGERHEART.Effects.Types.Health.Name"
},
}
export const targetTypes = {
self: {
id: 'self',
label: 'Self',
},
other: {
id: 'other',
label: 'Other',
},
}

View file

@ -0,0 +1,355 @@
export const abilities = {
agility: {
label: "DAGGERHEART.Abilities.Agility.Name",
verbs: ["DAGGERHEART.Abilities.Agility.Verb.Sprint", "DAGGERHEART.Abilities.Agility.Verb.Leap", "DAGGERHEART.Abilities.Agility.Verb.Maneuver"],
},
strength: {
label: "DAGGERHEART.Abilities.Strength.Name",
verbs: ["DAGGERHEART.Abilities.Strength.Verb.Lift", "DAGGERHEART.Abilities.Strength.Verb.Smash", "DAGGERHEART.Abilities.Strength.Verb.Grapple"],
},
finesse: {
label: "DAGGERHEART.Abilities.Finesse.Name",
verbs: ["DAGGERHEART.Abilities.Finesse.Verb.Control", "DAGGERHEART.Abilities.Finesse.Verb.Hide", "DAGGERHEART.Abilities.Finesse.Verb.Tinker"],
},
instinct: {
label: "DAGGERHEART.Abilities.Instinct.Name",
verbs: ["DAGGERHEART.Abilities.Instinct.Verb.Perceive", "DAGGERHEART.Abilities.Instinct.Verb.Sense", "DAGGERHEART.Abilities.Instinct.Verb.Navigate"],
},
presence: {
label: "DAGGERHEART.Abilities.Presence.Name",
verbs: ["DAGGERHEART.Abilities.Presence.Verb.Charm", "DAGGERHEART.Abilities.Presence.Verb.Perform", "DAGGERHEART.Abilities.Presence.Verb.Deceive"],
},
knowledge: {
label: "DAGGERHEART.Abilities.Knowledge.Name",
verbs: ["DAGGERHEART.Abilities.Knowledge.Verb.Recall", "DAGGERHEART.Abilities.Knowledge.Verb.Analyze", "DAGGERHEART.Abilities.Knowledge.Verb.Comprehend"],
},
};
export const featureProperties = {
agility: {
name: "DAGGERHEART.Abilities.Agility.Name",
path: actor => actor.system.attributes.agility.data.value,
},
strength: {
name: "DAGGERHEART.Abilities.Strength.Name",
path: actor => actor.system.attributes.strength.data.value,
},
finesse: {
name: "DAGGERHEART.Abilities.Finesse.Name",
path: actor => actor.system.attributes.finesse.data.value,
},
instinct: {
name: "DAGGERHEART.Abilities.Instinct.Name",
path: actor => actor.system.attributes.instinct.data.value,
},
presence: {
name: "DAGGERHEART.Abilities.Presence.Name",
path: actor => actor.system.attributes.presence.data.value,
},
knowledge: {
name: "DAGGERHEART.Abilities.Knowledge.Name",
path: actor => actor.system.attributes.knowledge.data.value,
},
spellcastingTrait: {
name: "DAGGERHEART.FeatureProperty.SpellcastingTrait",
path: actor => actor.system.attributes[actor.system.subclass.system.spellcastingTrait].data.value,
},
}
export const adversaryTypes = {
bruiser: {
name: "DAGGERHEART.Adversary.Bruiser.Name",
description: "DAGGERHEART.Adversary.Bruiser.Description"
},
horde: {
name: "DAGGERHEART.Adversary.Horde.Name",
description: "DAGGERHEART.Adversary.Horde.Description"
},
leader: {
name: "DAGGERHEART.Adversary.Leader.Name",
description: "DAGGERHEART.Adversary.Leader.Description"
},
minion: {
name: "DAGGERHEART.Adversary.Minion.Name",
description: "DAGGERHEART.Adversary.Minion.Description"
},
ranged: {
name: "DAGGERHEART.Adversary.Ranged.Name",
description: "DAGGERHEART.Adversary.Ranged.Description"
},
skulker: {
name: "DAGGERHEART.Adversary.Skulker.Name",
description: "DAGGERHEART.Adversary.Skulker.Description"
},
social: {
name: "DAGGERHEART.Adversary.Social.Name",
description: "DAGGERHEART.Adversary.Social.Description"
},
solo: {
name: "DAGGERHEART.Adversary.Solo.Name",
description: "DAGGERHEART.Adversary.Solo.Description"
},
standard: {
name: "DAGGERHEART.Adversary.Standard.Name",
description: "DAGGERHEART.Adversary.Standard.Description"
},
support: {
name: "DAGGERHEART.Adversary.Support.Name",
description: "DAGGERHEART.Adversary.Support.Description"
},
};
export const adversaryTraits = {
relentless: {
name: "DAGGERHEART.Adversary.Trait..Name",
description: "DAGGERHEART.Adversary.Trait..Description",
tip: "DAGGERHEART.Adversary.Trait..Tip",
},
slow: {
name: "DAGGERHEART.Adversary.Trait..Name",
description: "DAGGERHEART.Adversary.Trait..Description",
tip: "DAGGERHEART.Adversary.Trait..Tip",
},
minion: {
name: "DAGGERHEART.Adversary.Trait..Name",
description: "DAGGERHEART.Adversary.Trait..Description",
tip: "DAGGERHEART.Adversary.Trait..Tip",
},
};
export const levelChoices = {
attributes: {
name: 'attributes',
title: '',
choices: [],
},
hitPointSlots: {
name: 'hitPointSlots',
title: '',
choices: [],
},
stressSlots: {
name: 'stressSlots',
title: '',
choices: [],
},
experiences: {
name: 'experiences',
title: '',
choices: 'system.experiences',
nrChoices: 2,
},
proficiency: {
name: 'proficiency',
title: '',
choices: [],
},
armorOrEvasionSlot: {
name: 'armorOrEvasionSlot',
title: 'Permanently add one Armor Slot or take +1 to your Evasion',
choices: [{ name: 'Armor Marks +1', path: 'armor' }, { name: 'Evasion +1', path: 'evasion' }],
nrChoices: 1,
},
majorDamageThreshold2: {
name: 'majorDamageThreshold2',
title: '',
choices: [],
},
severeDamageThreshold2: {
name: 'severeDamageThreshold2',
title: '',
choices: [],
},
// minorDamageThreshold2: {
// name: 'minorDamageThreshold2',
// title: '',
// choices: [],
// },
severeDamageThreshold3: {
name: 'severeDamageThreshold3',
title: '',
choices: [],
},
// major2OrSevere4DamageThreshold: {
// name: 'major2OrSevere4DamageThreshold',
// title: 'Increase your Major Damage Threshold by +2 or Severe Damage Threshold by +4',
// choices: [{ name: 'Major Damage Threshold +2', path: 'major' }, { name: 'Severe Damage Threshold +4', path: 'severe' }],
// nrChoices: 1,
// },
// minor1OrMajor1DamageThreshold: {
// name: 'minor1OrMajor1DamageThreshold',
// title: 'Increase your Minor or Major Damage Threshold by +1',
// choices: [{ name: 'Minor Damage Threshold +1', path: 'minor' }, { name: 'Major Damage Threshold +1', path: 'major' }],
// nrChoices: 1,
// },
severeDamageThreshold4: {
name: 'severeDamageThreshold4',
title: '',
choices: [],
},
// majorDamageThreshold1: {
// name: 'majorDamageThreshold2',
// title: '',
// choices: [],
// },
subclass: {
name: 'subclass',
title: 'Select subclass to upgrade',
choices: [],
},
multiclass: {
name: 'multiclass',
title: '',
choices: [{}],
}
};
export const levelupData = {
tier1: {
id: "2_4",
tier: 1,
levels: [2,3,4],
label: 'DAGGERHEART.LevelUp.Tier1.Label',
info: "DAGGERHEART.LevelUp.Tier1.InfoLabel",
pretext: "DAGGERHEART.LevelUp.Tier1.Pretext",
posttext: "DAGGERHEART.LevelUp.Tier1.Posttext",
choices: {
[levelChoices.attributes.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.Attributes",
maxChoices: 3,
},
[levelChoices.hitPointSlots.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.HitPointSlots",
maxChoices: 1,
},
[levelChoices.stressSlots.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.StressSlots",
maxChoices: 1,
},
[levelChoices.experiences.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.Experiences",
maxChoices: 1,
},
[levelChoices.proficiency.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.Proficiency",
maxChoices: 1,
},
[levelChoices.armorOrEvasionSlot.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.ArmorOrEvasionSlot",
maxChoices: 1,
},
[levelChoices.majorDamageThreshold2.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.MajorDamageThreshold2",
maxChoices: 1,
},
[levelChoices.severeDamageThreshold2.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.SevereDamageThreshold2",
maxChoices: 1,
}
}
},
tier2: {
id: "5_7",
tier: 2,
levels: [5,6,7],
label: 'DAGGERHEART.LevelUp.Tier2.Label',
info: "DAGGERHEART.LevelUp.Tier2.InfoLabel",
pretext: "DAGGERHEART.LevelUp.Tier2.Pretext",
posttext: "DAGGERHEART.LevelUp.Tier2.Posttext",
choices: {
[levelChoices.attributes.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.Attributes",
maxChoices: 3,
},
[levelChoices.hitPointSlots.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.HitPointSlots",
maxChoices: 2,
},
[levelChoices.stressSlots.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.StressSlots",
maxChoices: 2,
},
[levelChoices.experiences.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.Experiences",
maxChoices: 1,
},
[levelChoices.proficiency.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.Proficiency",
maxChoices: 2,
},
[levelChoices.armorOrEvasionSlot.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.ArmorOrEvasionSlot",
maxChoices: 2,
},
[levelChoices.majorDamageThreshold2.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.MajorDamageThreshold2",
maxChoices: 1,
},
[levelChoices.severeDamageThreshold3.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.SevereDamageThreshold3",
maxChoices: 1,
},
[levelChoices.subclass.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.Subclass",
maxChoices: 1,
},
[levelChoices.multiclass.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.Multiclass",
maxChoices: 1,
cost: 2,
},
},
},
tier3: {
id: "8_10",
tier: 3,
levels: [8,9,10],
label: 'DAGGERHEART.LevelUp.Tier3.Label',
info: "DAGGERHEART.LevelUp.Tier3.InfoLabel",
pretext: "DAGGERHEART.LevelUp.Tier3.Pretext",
posttext: "DAGGERHEART.LevelUp.Tier3.Posttext",
choices: {
[levelChoices.attributes.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.Attributes",
maxChoices: 3,
},
[levelChoices.hitPointSlots.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.HitPointSlots",
maxChoices: 2,
},
[levelChoices.stressSlots.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.StressSlots",
maxChoices: 2,
},
[levelChoices.experiences.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.Experiences",
maxChoices: 1,
},
[levelChoices.proficiency.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.Proficiency",
maxChoices: 2,
},
[levelChoices.armorOrEvasionSlot.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.ArmorOrEvasionSlot",
maxChoices: 2,
},
[levelChoices.majorDamageThreshold2.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.MajorDamageThreshold2",
maxChoices: 1,
},
[levelChoices.severeDamageThreshold4.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.SevereDamageThreshold4",
maxChoices: 1,
},
[levelChoices.subclass.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.Subclass",
maxChoices: 1,
},
[levelChoices.multiclass.name]: {
description: "DAGGERHEART.LevelUp.ChoiceDescriptions.Multiclass",
maxChoices: 1,
cost: 2,
},
},
}
};

View file

@ -0,0 +1,101 @@
export const domains = {
arcana: {
id: 'arcana',
label: 'Arcana',
src: 'icons/magic/symbols/circled-gem-pink.webp',
description: 'DAGGERHEART.Domains.Arcana',
},
blade: {
id: 'blade',
label: 'Blade',
src: 'icons/weapons/swords/sword-broad-crystal-paired.webp',
description: 'DAGGERHEART.Domains.Blade',
},
bone: {
id: 'bone',
label: 'Bone',
src: 'icons/skills/wounds/bone-broken-marrow-red.webp',
description: 'DAGGERHEART.Domains.Bone',
},
codex: {
id: 'codex',
label: 'Codex',
src: 'icons/sundries/books/book-embossed-jewel-gold-purple.webp',
description: 'DAGGERHEART.Domains.Codex',
},
grace: {
id: 'grace',
label: 'Grace',
src: 'icons/skills/movement/feet-winged-boots-glowing-yellow.webp',
description: 'DAGGERHEART.Domains.Grace',
},
midnight: {
id: 'midnight',
label: 'Midnight',
src: 'icons/environment/settlement/watchtower-castle-night.webp',
background: 'systems/daggerheart/assets/backgrounds/MidnightBackground.webp',
description: 'DAGGERHEART.Domains.Midnight',
},
sage: {
id: 'sage',
label: 'Sage',
src: 'icons/sundries/misc/pipe-wooden-straight-brown.webp',
description: 'DAGGERHEART.Domains.Sage',
},
splendor: {
id: 'splendor',
label: 'Splendor',
src: 'icons/magic/control/control-influence-crown-gold.webp',
description: 'DAGGERHEART.Domains.Splendor',
},
valor: {
id: 'valor',
label: 'Valor',
src: 'icons/magic/control/control-influence-rally-purple.webp',
description: 'DAGGERHEART.Domains.Valor',
},
};
export const classDomainMap = {
rogue: [domains.midnight, domains.grace],
};
export const subclassMap = {
syndicate: {
id: 'syndicate',
label: 'Syndicate',
},
nightwalker: {
id: 'nightwalker',
label: 'Nightwalker',
},
};
export const classMap = {
rogue: {
label: "Rogue",
subclasses: [subclassMap.syndicate.id, subclassMap.nightwalker.id],
},
seraph: {
label: "Seraph",
subclasses: []
},
};
export const cardTypes = {
ability: {
id: 'ability',
label: "DAGGERHEART.Domain.CardTypes.Ability",
img: "",
},
spell: {
id: 'spell',
label: "DAGGERHEART.Domain.CardTypes.Spell",
img: ""
},
grimoire: {
id: 'grimoire',
label: "DAGGERHEART.Domain.CardTypes.Grimoire",
img: ""
}
};

View file

@ -0,0 +1,64 @@
import { range } from "./generalConfig.mjs";
export const valueTypes = {
numberString: {
id: 'numberString',
},
select: {
id: 'select',
}
}
export const parseTypes = {
string: {
id: 'string',
},
number: {
id: 'number',
},
}
export const applyLocations = {
attackRoll: {
id: 'attackRoll',
name: "DAGGERHEART.Effects.ApplyLocations.AttackRoll.Name",
},
damageRoll: {
id: 'damageRoll',
name: "DAGGERHEART.Effects.ApplyLocations.DamageRoll.Name",
}
};
export const effectTypes = {
health: {
id: "health",
name: "DAGGERHEART.Effects.Types.Health.Name",
values: [],
valueType: valueTypes.numberString.id,
parseType: parseTypes.number.id,
},
stress: {
id: "stress",
name: "DAGGERHEART.Effects.Types.Stress.Name",
valueType: valueTypes.numberString.id,
parseType: parseTypes.number.id,
},
reach: {
id: "reach",
name: "DAGGERHEART.Effects.Types.Reach.Name",
valueType: valueTypes.select.id,
parseType: parseTypes.string.id,
options: Object.keys(range).map(x => ({ name: range[x].name, value: x }))
},
damage: {
id: "damage",
name: "DAGGERHEART.Effects.Types.Damage.Name",
valueType: valueTypes.numberString.id,
parseType: parseTypes.string.id,
appliesOn: applyLocations.damageRoll.id,
applyLocationChoices: {
[applyLocations.damageRoll.id]: applyLocations.damageRoll.name,
[applyLocations.attackRoll.id]: applyLocations.attackRoll.name,
},
}
};

View file

@ -0,0 +1,268 @@
export const range = {
melee: {
label: "DAGGERHEART.Range.Melee.Name",
description: "DAGGERHEART.Range.Melee.Description",
distance: 1
},
veryClose: {
label: "DAGGERHEART.Range.VeryClose.Name",
description: "DAGGERHEART.Range.VeryClose.Description",
distance: 3
},
close: {
label: "DAGGERHEART.Range.Close.Name",
description: "DAGGERHEART.Range.Close.Description",
distance: 10
},
far: {
label: "DAGGERHEART.Range.Far.Name",
description: "DAGGERHEART.Range.Far.Description",
distance: 20
},
veryFar: {
label: "DAGGERHEART.Range.VeryFar.Name",
description: "DAGGERHEART.Range.VeryFar.Description",
distance: 30
}
}
export const burden = {
oneHanded: "DAGGERHEART.Burden.OneHanded",
twoHanded: "DAGGERHEART.Burden.TwoHanded"
}
export const damageTypes = {
physical: {
id: 'physical',
label: "DAGGERHEART.DamageType.Physical.Name",
abbreviation: "DAGGERHEART.DamageType.Physical.Abbreviation",
},
magical: {
id: 'magical',
label: "DAGGERHEART.DamageType.Magical.Name",
abbreviation: "DAGGERHEART.DamageType.Magical.Abbreviation",
},
}
export const healingTypes = {
health: {
id: 'health',
label: "DAGGERHEART.HealingType.HitPoints.Name",
abbreviation: "DAGGERHEART.HealingType.HitPoints.Abbreviation"
},
stress: {
id: 'stress',
label: "DAGGERHEART.HealingType.Stress.Name",
abbreviation: "DAGGERHEART.HealingType.Stress.Abbreviation"
},
};
export const conditions = {
vulnerable: {
id: 'vulnerable',
name: "DAGGERHEART.Condition.Vulnerable.Name",
icon: "icons/magic/control/silhouette-fall-slip-prone.webp",
description: "DAGGERHEART.Condition.Vulnerable.Description"
},
hidden: {
id: 'hidden',
name: "DAGGERHEART.Condition.Hidden.Name",
icon: "icons/magic/perception/silhouette-stealth-shadow.webp",
description: "DAGGERHEART.Condition.Hidden.Description"
},
restrained: {
id: 'restrained',
name: "DAGGERHEART.Condition.Restrained.Name",
icon: "icons/magic/control/debuff-chains-shackle-movement-red.webp",
description: "DAGGERHEART.Condition.Restrained.Description"
},
}
export const downtime = {
shortRest: {
tendToWounds: {
id: "tendToWounds",
name: "DAGGERHEART.Downtime.TendToWounds.Name",
img: "icons/magic/life/cross-worn-green.webp",
description: "DAGGERHEART.Downtime.TendToWounds.Description",
},
clearStress: {
id: "clearStress",
name: "DAGGERHEART.Downtime.ClearStress.Name",
img: "icons/magic/perception/eye-ringed-green.webp",
description: "DAGGERHEART.Downtime.ClearStress.Description",
},
repairArmor: {
id: "repairArmor",
name: "DAGGERHEART.Downtime.RepairArmor.Name",
img: "icons/skills/trades/smithing-anvil-silver-red.webp",
description: "DAGGERHEART.Downtime.RepairArmor.Description",
},
prepare: {
id: "prepare",
name: "DAGGERHEART.Downtime.Prepare.Name",
img: "icons/skills/trades/academics-merchant-scribe.webp",
description: "DAGGERHEART.Downtime.Prepare.Description",
},
},
longRest: {
tendToWounds: {
id: "tendToWounds",
name: "DAGGERHEART.Downtime.TendToWounds.Name",
img: "icons/magic/life/cross-worn-green.webp",
description: "DAGGERHEART.Downtime.TendToWounds.Description",
},
clearStress: {
id: "clearStress",
name: "DAGGERHEART.Downtime.ClearStress.Name",
img: "icons/magic/perception/eye-ringed-green.webp",
description: "DAGGERHEART.Downtime.ClearStress.Description",
},
repairArmor: {
id: "repairArmor",
name: "DAGGERHEART.Downtime.RepairArmor.Name",
img: "icons/skills/trades/smithing-anvil-silver-red.webp",
description: "DAGGERHEART.Downtime.RepairArmor.Description",
},
prepare: {
id: "prepare",
name: "DAGGERHEART.Downtime.Prepare.Name",
img: "icons/skills/trades/academics-merchant-scribe.webp",
description: "DAGGERHEART.Downtime.Prepare.Description",
},
workOnAProject: {
id: "workOnAProject",
name: "DAGGERHEART.Downtime.WorkOnAProject.Name",
img: "icons/skills/social/thumbsup-approval-like.webp",
description: "DAGGERHEART.Downtime.WorkOnAProject.Description",
}
},
custom: {
id: 'customActivity',
name: "",
img: "icons/skills/trades/academics-investigation-puzzles.webp",
description: "",
namePlaceholder: "DAGGERHEART.Downtime.Custom.NamePlaceholder",
placeholder: "DAGGERHEART.Downtime.Custom.Placeholder",
}
}
export const deathMoves = {
avoidDeath: {
id: "avoidDeath",
name: "DAGGERHEART.DeathMoves.AvoidDeath.Name",
img: "icons/magic/time/hourglass-yellow-green.webp",
description: "DAGGERHEART.DeathMoves.AvoidDeath.Description",
},
riskItAll: {
id: 'riskItAll',
name: "DAGGERHEART.DeathMoves.RiskItAll.Name",
img: "icons/sundries/gaming/dice-pair-white-green.webp",
description: "DAGGERHEART.DeathMoves.RiskItAll.Description",
},
blazeOfGlory: {
id: "blazeOfGlory",
name: "DAGGERHEART.DeathMoves.BlazeOfGlory.Name",
img: "icons/magic/life/heart-cross-strong-flame-purple-orange.webp",
description: "DAGGERHEART.DeathMoves.BlazeOfGlory.Description",
}
};
export const tiers = {
0: {
key: 0,
id: 'tier0',
name: 'DAGGERHEART.General.Tier.0',
},
1: {
key: 1,
id: 'tier1',
name: 'DAGGERHEART.General.Tier.1',
},
2: {
key: 2,
id: 'tier2',
name: 'DAGGERHEART.General.Tier.2',
},
3: {
key: 3,
id: 'tier3',
name: 'DAGGERHEART.General.Tier.3',
}
};
export const objectTypes = {
pc: {
name: "TYPES.Actor.pc",
},
npc: {
name: "TYPES.Actor.npc",
},
adversary: {
name: "TYPES.Actor.adversary",
},
ancestry: {
name: "TYPES.Item.ancestry",
},
community: {
name: "TYPES.Item.community",
},
class: {
name: "TYPES.Item.class",
},
subclass: {
name: "TYPES.Item.subclass",
},
feature: {
name: "TYPES.Item.feature",
},
domainCard: {
name: "TYPES.Item.domainCard",
},
consumable: {
name: "TYPES.Item.consumable",
},
miscellaneous: {
name: "TYPES.Item.miscellaneous",
},
weapon: {
name: "TYPES.Item.weapon",
},
armor: {
name: "TYPES.Item.armor",
}
};
export const diceTypes = {
d4: "d4",
d6: "d6",
d8: "d8",
d12: "d12",
d20: "d20"
};
export const refreshTypes = {
session: {
id: 'session',
label: "DAGGERHEART.General.RefreshType.Session"
},
shortRest: {
id: 'shortRest',
label: "DAGGERHEART.General.RefreshType.Shortrest",
},
longRest: {
id: 'longRest',
label: "DAGGERHEART.General.RefreshType.Longrest"
}
}
export const abilityCosts = {
hope: {
id: 'hope',
label: 'Hope',
},
stress: {
id: 'stress',
label: 'Stress',
}
}

View file

@ -0,0 +1,351 @@
export const armorFeatures = {
light: {
label: "DAGGERHEART.ArmorFeature.Light.Name",
description: "DAGGERHEART.ArmorFeature.Light.Description",
},
heavy: {
label: "DAGGERHEART.ArmorFeature.Heavy.Name",
description: "DAGGERHEART.ArmorFeature.Heavy.Description",
},
veryHeavy: {
label: "DAGGERHEART.ArmorFeature.VeryHeavy.Name",
description: "DAGGERHEART.ArmorFeature.VeryHeavy.Description",
},
reinforced: {
label: "DAGGERHEART.ArmorFeature.Reinforced.Name",
description: "DAGGERHEART.ArmorFeature.Reinforced.Description",
},
sturdy: {
label: "DAGGERHEART.ArmorFeature.Sturdy.Name",
description: "DAGGERHEART.ArmorFeature.Sturdy.Description",
},
warded: {
label: "DAGGERHEART.ArmorFeature.Warded.Name",
description: "DAGGERHEART.ArmorFeature.Warded.Description",
},
resistant: {
label: "DAGGERHEART.ArmorFeature.Resistant.Name",
description: "DAGGERHEART.ArmorFeature.Resistant.Description",
},
quiet: {
label: "DAGGERHEART.ArmorFeature.Quiet.Name",
description: "DAGGERHEART.ArmorFeature.Quiet.Description",
},
hopeful: {
label: "DAGGERHEART.ArmorFeature.Hopeful.Name",
description: "DAGGERHEART.ArmorFeature.Hopeful.Description",
},
impenetrable: {
label: "DAGGERHEART.ArmorFeature.Impenetrable.Name",
description: "DAGGERHEART.ArmorFeature.Impenetrable.Description",
},
painful: {
label: "DAGGERHEART.ArmorFeature.Painful.Name",
description: "DAGGERHEART.ArmorFeature.Painful.Description",
},
gilded: {
label: "DAGGERHEART.ArmorFeature.Gilded.Name",
description: "DAGGERHEART.ArmorFeature.Gilded.Description",
},
physical: {
label: "DAGGERHEART.ArmorFeature.Physical.Name",
description: "DAGGERHEART.ArmorFeature.Physical.Description",
},
magic: {
label: "DAGGERHEART.ArmorFeature.Magic.Name",
description: "DAGGERHEART.ArmorFeature.Magic.Description",
},
sharp: {
label: "DAGGERHEART.ArmorFeature.Sharp.Name",
description: "DAGGERHEART.ArmorFeature.Sharp.Description",
},
burning: {
label: "DAGGERHEART.ArmorFeature.Burning.Name",
description: "DAGGERHEART.ArmorFeature.Burning.Description",
},
timeslowing: {
label: "DAGGERHEART.ArmorFeature.Timeslowing.Name",
description: "DAGGERHEART.ArmorFeature.Timeslowing.Description",
},
truthseeking: {
label: "DAGGERHEART.ArmorFeature.Truthseeking.Name",
description: "DAGGERHEART.ArmorFeature.Truthseeking.Description",
},
channeling: {
label: "DAGGERHEART.ArmorFeature.Channeling.Name",
description: "DAGGERHEART.ArmorFeature.Channeling.Description",
},
difficult: {
label: "DAGGERHEART.ArmorFeature.Difficult.Name",
description: "DAGGERHEART.ArmorFeature.Difficult.Description",
},
variable: {
label: "DAGGERHEART.ArmorFeature.Variable.Name",
description: "DAGGERHEART.ArmorFeature.Variable.Description",
},
};
export const weaponFeatures = {
light: {
label: "DAGGERHEART.WeaponFeature.Light.Name",
description: "DAGGERHEART.WeaponFeature.Light.Description",
},
heavy: {
label: "DAGGERHEART.WeaponFeature.Heavy.Name",
description: "DAGGERHEART.WeaponFeature.Heavy.Description",
},
massive: {
label: "DAGGERHEART.WeaponFeature.Massive.Name",
description: "DAGGERHEART.WeaponFeature.Massive.Description",
},
reliable: {
label: "DAGGERHEART.WeaponFeature.Reliable.Name",
description: "DAGGERHEART.WeaponFeature.Reliable.Description",
},
quick: {
label: "DAGGERHEART.WeaponFeature.Quick.Name",
description: "DAGGERHEART.WeaponFeature.Quick.Description",
},
cumbersome: {
label: "DAGGERHEART.WeaponFeature.Cumbersome.Name",
description: "DAGGERHEART.WeaponFeature.Cumbersome.Description",
},
versatile: {
label: "DAGGERHEART.WeaponFeature.Versatile.Name",
description: "DAGGERHEART.WeaponFeature.Versatile.Description",
override: {
damage: "",
}
},
powerful: {
label: "DAGGERHEART.WeaponFeature.Powerful.Name",
description: "DAGGERHEART.WeaponFeature.Powerful.Description",
},
scary: {
label: "DAGGERHEART.WeaponFeature.Scary.Name",
description: "DAGGERHEART.WeaponFeature.Scary.Description",
},
brutal: {
label: "DAGGERHEART.WeaponFeature.Brutal.Name",
description: "DAGGERHEART.WeaponFeature.Brutal.Description",
},
reloading: {
label: "DAGGERHEART.WeaponFeature.Reloading.Name",
description: "DAGGERHEART.WeaponFeature.Reloading.Description",
},
eruptive: {
label: "DAGGERHEART.WeaponFeature.Eruptive.Name",
description: "DAGGERHEART.WeaponFeature.Eruptive.Description",
},
persuasive: {
label: "DAGGERHEART.WeaponFeature.Persuasive.Name",
description: "DAGGERHEART.WeaponFeature.Persuasive.Description",
},
pompous: {
label: "DAGGERHEART.WeaponFeature.Pompous.Name",
description: "DAGGERHEART.WeaponFeature.Pompous.Description",
},
invigorating: {
label: "DAGGERHEART.WeaponFeature.Invigorating.Name",
description: "DAGGERHEART.WeaponFeature.Invigorating.Description",
},
dense: {
label: "DAGGERHEART.WeaponFeature.Dense.Name",
description: "DAGGERHEART.WeaponFeature.Dense.Description",
},
soulswift: {
label: "DAGGERHEART.WeaponFeature.Soulswift.Name",
description: "DAGGERHEART.WeaponFeature.Soulswift.Description",
},
protective: {
label: "DAGGERHEART.WeaponFeature.Protective.Name",
description: "DAGGERHEART.WeaponFeature.Protective.Description",
},
devastating: {
label: "DAGGERHEART.WeaponFeature.Devastating.Name",
description: "DAGGERHEART.WeaponFeature.Devastating.Description",
},
retractable: {
label: "DAGGERHEART.WeaponFeature.Retractable.Name",
description: "DAGGERHEART.WeaponFeature.Retractable.Description",
},
burn: {
label: "DAGGERHEART.WeaponFeature.Burn.Name",
description: "DAGGERHEART.WeaponFeature.Burn.Description",
},
painful: {
label: "DAGGERHEART.WeaponFeature.Painful.Name",
description: "DAGGERHEART.WeaponFeature.Painful.Description",
},
otherwordly: {
label: "DAGGERHEART.WeaponFeature.Otherwordly.Name",
description: "DAGGERHEART.WeaponFeature.Otherwordly.Description",
},
lucky: {
label: "DAGGERHEART.WeaponFeature.Lucky.Name",
description: "DAGGERHEART.WeaponFeature.Lucky.Description",
},
selfCorrecting: {
label: "DAGGERHEART.WeaponFeature.SelfCorrecting.Name",
description: "DAGGERHEART.WeaponFeature.SelfCorrecting.Description",
},
healing: {
label: "DAGGERHEART.WeaponFeature.Healing.Name",
description: "DAGGERHEART.WeaponFeature.Healing.Description",
},
timebender: {
label: "DAGGERHEART.WeaponFeature.Timebender.Name",
description: "DAGGERHEART.WeaponFeature.Timebender.Description",
},
enchanted: {
label: "DAGGERHEART.WeaponFeature.Enchanted.Name",
description: "DAGGERHEART.WeaponFeature.Enchanted.Description",
},
serrated: {
label: "DAGGERHEART.WeaponFeature.Serrated.Name",
description: "DAGGERHEART.WeaponFeature.Serrated.Description",
},
grappling: {
label: "DAGGERHEART.WeaponFeature.Grappling.Name",
description: "DAGGERHEART.WeaponFeature.Grappling.Description",
},
long: {
label: "DAGGERHEART.WeaponFeature.Long.Name",
description: "DAGGERHEART.WeaponFeature.Long.Description",
},
destructive: {
label: "DAGGERHEART.WeaponFeature.Destructive.Name",
description: "DAGGERHEART.WeaponFeature.Destructive.Description",
},
concussive: {
label: "DAGGERHEART.WeaponFeature.Concussive.Name",
description: "DAGGERHEART.WeaponFeature.Concussive.Description",
},
bouncing: {
label: "DAGGERHEART.WeaponFeature.Bouncing.Name",
description: "DAGGERHEART.WeaponFeature.Bouncing.Description",
},
penetrating: {
label: "DAGGERHEART.WeaponFeature.Penetrating.Name",
description: "DAGGERHEART.WeaponFeature.Penetrating.Description",
},
lifestealing: {
label: "DAGGERHEART.WeaponFeature.Lifestealing.Name",
description: "DAGGERHEART.WeaponFeature.Lifestealing.Description",
},
greedy: {
label: "DAGGERHEART.WeaponFeature.Greedy.Name",
description: "DAGGERHEART.WeaponFeature.Greedy.Description",
},
bonded: {
label: "DAGGERHEART.WeaponFeature.Bonded.Name",
description: "DAGGERHEART.WeaponFeature.Bonded.Description",
},
barrier: {
label: "DAGGERHEART.WeaponFeature.Barrier.Name",
description: "DAGGERHEART.WeaponFeature.Barrier.Description",
},
paired: {
label: "DAGGERHEART.WeaponFeature.Paired.Name",
description: "DAGGERHEART.WeaponFeature.Paired.Description",
},
whipcrack: {
label: "DAGGERHEART.WeaponFeature.Whipcrack.Name",
description: "DAGGERHEART.WeaponFeature.Whipcrack.Description",
},
hook: {
label: "DAGGERHEART.WeaponFeature.Hook.Name",
description: "DAGGERHEART.WeaponFeature.Hook.Description",
},
doubleDuty: {
label: "DAGGERHEART.WeaponFeature.DoubleDuty.Name",
description: "DAGGERHEART.WeaponFeature.DoubleDuty.Description",
},
parry: {
label: "DAGGERHEART.WeaponFeature.Parry.Name",
description: "DAGGERHEART.WeaponFeature.Parry.Description",
},
retrieve: {
label: "DAGGERHEART.WeaponFeature.Retrieve.Name",
description: "DAGGERHEART.WeaponFeature.Retrieve.Description",
},
deflecting: {
label: "DAGGERHEART.WeaponFeature.Deflecting.Name",
description: "DAGGERHEART.WeaponFeature.Deflecting.Description",
},
chargedAttack: {
label: "DAGGERHEART.WeaponFeature.ChargedAttack.Name",
description: "DAGGERHEART.WeaponFeature.ChargedAttack.Description",
},
sheltering: {
label: "DAGGERHEART.WeaponFeature.Sheltering.Name",
description: "DAGGERHEART.WeaponFeature.Sheltering.Description",
},
doubledUp: {
label: "DAGGERHEART.WeaponFeature.DoubledUp.Name",
description: "DAGGERHEART.WeaponFeature.DoubledUp.Description",
},
lockedOn: {
label: "DAGGERHEART.WeaponFeature.LockedOn.Name",
description: "DAGGERHEART.WeaponFeature.LockedOn.Description",
},
};
export const featureTypes = {
ancestry: {
id: "ancestry",
label: "DAGGERHEART.Feature.Type.Ancestry"
},
community: {
id: "community",
label: "DAGGERHEART.Feature.Type.Community"
},
class: {
id: "class",
label: "DAGGERHEART.Feature.Type.Class"
},
subclass: {
id: "subclass",
label: "DAGGERHEART.Feature.Type.Subclass"
},
}
export const valueTypes = {
normal: {
id: 'normal',
name: "DAGGERHEART.Feature.ValueType.Normal",
data: {
value: 0,
max: 0,
}
},
input: {
id: 'input',
name: "DAGGERHEART.Feature.ValueType.Input",
data: {
value: null,
}
},
dice: {
id: 'dice',
name: "DAGGERHEART.Feature.ValueType.Dice",
data: {
value: null,
}
}
}
export const actionTypes = {
passive: {
id: "passive",
label: "DAGGERHEART.ActionType.Passive"
},
action: {
id: "action",
label: "DAGGERHEART.ActionType.Action"
},
reaction: {
id: "reaction",
label: "DAGGERHEART.ActionType.Reaction"
}
};

View file

@ -0,0 +1,28 @@
export const menu = {
Automation: {
Name: "GameSettingsAutomation",
Icon: "fa-solid fa-robot",
},
Homebrew: {
Name: "GameSettingsHomebrew",
Icon: "fa-solid fa-flask-vial",
},
Range: {
Name: "GameSettingsRange",
Icon: "fa-solid fa-ruler",
},
};
export const gameSettings = {
Automation: {
Hope: "AutomationHope",
ActionPoints: "AutomationActionPoints",
},
Resources: {
Fear: "ResourcesFear"
},
General: {
AbilityArray: "AbilityArray",
RangeMeasurement: "RangeMeasurement",
}
}

20
module/config/system.mjs Normal file
View file

@ -0,0 +1,20 @@
import * as GENERAL from './generalConfig.mjs';
import * as DOMAIN from "./domainConfig.mjs";
import * as ACTOR from './actorConfig.mjs';
import * as ITEM from './itemConfig.mjs';
import * as SETTINGS from './settingsConfig.mjs';
import * as EFFECTS from './effectConfig.mjs';
import * as ACTIONS from './actionConfig.mjs';
export const SYSTEM_ID = "daggerheart";
export const SYSTEM = {
id: SYSTEM_ID,
GENERAL,
DOMAIN,
ACTOR,
ITEM,
SETTINGS,
EFFECTS,
ACTIONS,
};

18
module/data/_module.mjs Normal file
View file

@ -0,0 +1,18 @@
export { default as DhpPC } from './pc.mjs';
export { default as DhpClass } from './class.mjs';
export { default as DhpSubclass } from './subclass.mjs';
export { default as DhpCombat } from './combat.mjs';
export { default as DhpCombatant } from './combatant.mjs';
export { default as DhpAdversary } from './adversary.mjs';
export { default as DhpFeature } from './feature.mjs';
export { default as DhpDomainCard } from './domainCard.mjs';
export { default as DhpAncestry } from './ancestry.mjs';
export { default as DhpCommunity } from './community.mjs';
export { default as DhpMiscellaneous } from './miscellaneous.mjs';
export { default as DhpConsumable } from './consumable.mjs';
export { default as DhpWeapon } from './weapon.mjs';
export { default as DhpArmor } from './armor.mjs';
export { default as DhpDualityRoll } from './dualityRoll.mjs';
export { default as DhpAdversaryRoll } from './adversaryRoll.mjs';
export { default as DhpAbilityUse } from './abilityUse.mjs';
export { default as DhpEnvironment } from './environment.mjs';

View file

@ -0,0 +1,30 @@
export default class DhpAbilityUse extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
title: new fields.StringField({}),
img: new fields.StringField({}),
name: new fields.StringField({}),
description: new fields.StringField({}),
actions: new fields.ArrayField(new fields.SchemaField({
name: new fields.StringField({}),
damage: new fields.SchemaField({
type: new fields.StringField({}),
value: new fields.StringField({}),
}),
healing: new fields.SchemaField({
type: new fields.StringField({}),
value: new fields.StringField({}),
}),
cost: new fields.SchemaField({
type: new fields.StringField({ nullable: true }),
value: new fields.NumberField({ nullable: true }),
}),
target: new fields.SchemaField({
type: new fields.StringField({}),
}),
})),
}
}
}

38
module/data/action.mjs Normal file
View file

@ -0,0 +1,38 @@
export default class DaggerheartAction extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
id: new fields.StringField({}),
name: new fields.StringField({ initial: 'New Action' }),
damage: new fields.SchemaField({
type: new fields.StringField({ choices: SYSTEM.GENERAL.damageTypes, nullable: true, initial: null }),
value: new fields.StringField({}),
}),
healing: new fields.SchemaField({
type: new fields.StringField({ choices: SYSTEM.GENERAL.healingTypes, nullable: true, initial: null }),
value: new fields.StringField(),
}),
conditions: new fields.ArrayField(new fields.SchemaField({
name: new fields.StringField(),
icon: new fields.StringField(),
description: new fields.StringField(),
})),
cost: new fields.SchemaField({
type: new fields.StringField({ choices: SYSTEM.GENERAL.abilityCosts, nullable: true, initial: null }),
value: new fields.NumberField({ nullable: true, initial: null }),
}),
target: new fields.SchemaField({
type: new fields.StringField({ choices: SYSTEM.ACTIONS.targetTypes, initial: SYSTEM.ACTIONS.targetTypes.other.id })
}),
// uses: new fields.SchemaField({
// nr: new fields.StringField({}),
// refreshType: new fields.StringField({ choices: SYSTEM.GENERAL.refreshTypes, initial: SYSTEM.GENERAL.refreshTypes.session.id }),
// refreshed: new fields.BooleanField({ initial: true }),
// }),
}
}
use = async () => {
console.log('Test Use');
};
}

48
module/data/adversary.mjs Normal file
View file

@ -0,0 +1,48 @@
import { MappingField } from "./fields.mjs";
export default class DhpAdversary extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
resources: new fields.SchemaField({
health: new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true }),
min: new fields.NumberField({ initial: 0, integer: true }),
max: new fields.NumberField({ initial: 0, integer: true }),
}),
stress: new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true }),
min: new fields.NumberField({ initial: 0, integer: true }),
max: new fields.NumberField({ initial: 0, integer: true }),
}),
}),
tier: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.tiers), integer: false }),
type: new fields.StringField({ choices: Object.keys(SYSTEM.ACTOR.adversaryTypes), integer: false, initial: Object.keys(SYSTEM.ACTOR.adversaryTypes).find(x => x === 'standard') }),
description: new fields.StringField({}),
motivesAndTactics: new fields.ArrayField(new fields.StringField({})),
attackModifier: new fields.NumberField({ integer: true, nullabe: true, initial: null }),
attack: new fields.SchemaField({
name: new fields.StringField({}),
range: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.range), integer: false }),
damage: new fields.SchemaField({
value: new fields.StringField({}),
type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false }),
})
}),
difficulty: new fields.NumberField({ initial: 1, integer: true }),
damageThresholds: new fields.SchemaField({
minor: new fields.NumberField({ initial: 0, integer: true }),
major: new fields.NumberField({ initial: 0, integer: true }),
severe: new fields.NumberField({ initial: 0, integer: true }),
}),
experiences: new fields.ArrayField(new fields.SchemaField({
name: new fields.StringField({}),
value: new fields.NumberField({ integer: true, nullable: true, initial: null }),
})),
}
}
get moves(){
return this.parent.items.filter(x => x.type === 'feature');
}
}

View file

@ -0,0 +1,50 @@
export default class DhpAdversaryRoll extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
roll: new fields.StringField({}),
total: new fields.NumberField({ integer: true }),
modifiers: new fields.ArrayField(new fields.SchemaField({
value: new fields.NumberField({ integer: true }),
label: new fields.StringField({}),
title: new fields.StringField({}),
})),
diceResults: new fields.ArrayField(new fields.SchemaField({
value: new fields.NumberField({ integer: true }),
discarded: new fields.BooleanField({ initial: false }),
})),
targets: new fields.ArrayField(new fields.SchemaField({
id: new fields.StringField({}),
name: new fields.StringField({}),
img: new fields.StringField({}),
difficulty: new fields.NumberField({ integer: true, nullable: true }),
evasion: new fields.NumberField({ integer: true }),
hit: new fields.BooleanField({ initial: false }),
})),
damage: new fields.SchemaField({
value: new fields.StringField({}),
type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false }),
}, { nullable: true, initial: null })
}
}
prepareDerivedData(){
const diceKeys = Object.keys(this.diceResults);
const highestIndex = 0;
for(var index in diceKeys){
const resultIndex = Number.parseInt(index);
if(highestIndex === resultIndex) continue;
const current = this.diceResults[resultIndex];
const highest = this.diceResults[highestIndex];
if(current.value > highest.value) this.diceResults[highestIndex].discarded = true;
else this.diceResults[resultIndex].discarded = true;
}
this.targets.forEach(target => {
target.hit = target.difficulty ? this.total >= target.difficulty : this.total >= target.evasion;
});
}
}

11
module/data/ancestry.mjs Normal file
View file

@ -0,0 +1,11 @@
import featuresSchema from "./interface/featuresSchema.mjs";
export default class DhpAncestry extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({}),
abilities: featuresSchema(),
}
}
}

40
module/data/armor.mjs Normal file
View file

@ -0,0 +1,40 @@
export default class DhpArmor extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
baseScore: new fields.NumberField({ initial: 1, integer: true }),
feature: new fields.StringField({ choices: SYSTEM.ITEM.armorFeatures, integer: false }),
marks: new fields.SchemaField({
max: new fields.NumberField({ initial: 6, integer: true }),
value: new fields.NumberField({ initial: 0, integer: true }),
}),
description: new fields.HTMLField({}),
}
}
get featureInfo() {
return this.feature ? CONFIG.daggerheart.ITEM.armorFeatures[this.feature] : null;
}
prepareDerivedData(){
if(this.parent.parent){
this.applyLevels();
}
}
// Currently bugged as it double triggers. Should get fixed in an updated foundry version.
applyLevels(){
// let armorBonus = 0;
// for(var level in this.parent.parent.system.levelData.levelups){
// var levelData = this.parent.parent.system.levelData.levelups[level];
// for(var tier in levelData){
// var tierData = levelData[tier];
// if(tierData){
// armorBonus += Object.keys(tierData.armorOrEvasionSlot).filter(x => tierData.armorOrEvasionSlot[x] === 'armor').length;
// }
// }
// }
// this.marks.max += armorBonus;
}
}

93
module/data/class.mjs Normal file
View file

@ -0,0 +1,93 @@
import { getTier } from "../helpers/utils.mjs";
import DhpFeature from "./feature.mjs";
export default class DhpClass extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
domains: new fields.ArrayField(new fields.StringField({})),
classItems: new fields.ArrayField(new fields.SchemaField({
name: new fields.StringField({}),
img: new fields.StringField({}),
uuid: new fields.StringField({}),
})),
damageThresholds: new fields.SchemaField({
minor: new fields.NumberField({ initial: 0, integer: true }),
major: new fields.NumberField({ initial: 0, integer: true }),
severe: new fields.NumberField({ initial: 0, integer: true }),
}),
evasion: new fields.NumberField({ initial: 0, integer: true}),
features: new fields.ArrayField(new fields.SchemaField({
name: new fields.StringField({}),
img: new fields.StringField({}),
uuid: new fields.StringField({}),
})),
subclasses: new fields.ArrayField(new fields.SchemaField({
name: new fields.StringField({}),
img: new fields.StringField({}),
uuid: new fields.StringField({}),
})),
inventory: new fields.SchemaField({
take: new fields.ArrayField(new fields.SchemaField({
name: new fields.StringField({}),
img: new fields.StringField({}),
uuid: new fields.StringField({}),
})),
choiceA: new fields.ArrayField(new fields.SchemaField({
name: new fields.StringField({}),
img: new fields.StringField({}),
uuid: new fields.StringField({}),
})),
choiceB: new fields.ArrayField(new fields.SchemaField({
name: new fields.StringField({}),
img: new fields.StringField({}),
uuid: new fields.StringField({}),
})),
extra: new fields.SchemaField({
title: new fields.StringField({}),
description: new fields.StringField({})
}, { initial: null, nullable: true }),
}),
characterGuide: new fields.SchemaField({
suggestedTraits: new fields.SchemaField({
agility: new fields.NumberField({ initial: 0, integer: true }),
strength: new fields.NumberField({ initial: 0, integer: true }),
finesse: new fields.NumberField({ initial: 0, integer: true }),
instinct: new fields.NumberField({ initial: 0, integer: true }),
presence: new fields.NumberField({ initial: 0, integer: true }),
knowledge: new fields.NumberField({ initial: 0, integer: true }),
}),
suggestedPrimaryWeapon: new fields.SchemaField({
name: new fields.StringField({}),
img: new fields.StringField({}),
uuid: new fields.StringField({}),
}, { initial: null, nullable: true }),
suggestedSecondaryWeapon: new fields.SchemaField({
name: new fields.StringField({}),
img: new fields.StringField({}),
uuid: new fields.StringField({}),
}, { initial: null, nullable: true }),
suggestedArmor: new fields.SchemaField({
name: new fields.StringField({}),
img: new fields.StringField({}),
uuid: new fields.StringField({}),
}, { initial: null, nullable: true }),
characterDescription: new fields.SchemaField({
clothes: new fields.StringField({}),
eyes: new fields.StringField({}),
body: new fields.StringField({}),
color: new fields.StringField({}),
attitude: new fields.StringField({}),
}),
backgroundQuestions: new fields.ArrayField(new fields.StringField({}), { initial: ['', '', ''] }),
connections: new fields.ArrayField(new fields.StringField({}), { initial: ['', '' ,''] }),
}),
multiclass: new fields.NumberField({ initial: null, nullable: true, integer: true }),
description: new fields.HTMLField({}),
}
}
get multiclassTier(){
return getTier(this.multiclass, true);
}
}

9
module/data/combat.mjs Normal file
View file

@ -0,0 +1,9 @@
export default class DhpCombat extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
actions: new fields.NumberField({ initial: 0, integer: true }),
activeCombatant: new fields.StringField({}),
}
}
}

View file

@ -0,0 +1,8 @@
export default class DhpCombatant extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
active: new fields.BooleanField({ initial: false })
}
}
}

11
module/data/community.mjs Normal file
View file

@ -0,0 +1,11 @@
import featuresSchema from "./interface/featuresSchema.mjs";
export default class DhpCommunity extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({}),
abilities: featuresSchema(),
}
}
}

View file

@ -0,0 +1,10 @@
export default class DhpConsumable extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({}),
quantity: new fields.NumberField({ initial: 1, integer: true }),
consumeOnUse: new fields.BooleanField({ initial: false }),
}
}
}

View file

@ -0,0 +1,17 @@
import DaggerheartAction from "./action.mjs";
export default class DhpDomainCard extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
domain: new fields.StringField({ choices: SYSTEM.DOMAIN.domains, integer: false }, { required: true, initial: [] }),
level: new fields.NumberField({ initial: 1, integer: true }),
recallCost: new fields.NumberField({ initial: 0, integer: true }),
type: new fields.StringField({ choices: SYSTEM.DOMAIN.cardTypes, integer: false }, { required: true, initial: [] }),
foundation: new fields.BooleanField({ initial: false }),
effect: new fields.HTMLField({}),
inVault: new fields.BooleanField({ initial: false }),
actions: new fields.ArrayField(new fields.EmbeddedDataField(DaggerheartAction)),
}
}
}

150
module/data/dualityRoll.mjs Normal file
View file

@ -0,0 +1,150 @@
const fields = foundry.data.fields;
const diceField = () => new fields.SchemaField({
dice: new fields.StringField({}),
value: new fields.NumberField({ integer: true}),
});
export default class DhpDualityRoll extends foundry.abstract.TypeDataModel {
static defineSchema() {
return {
roll: new fields.StringField({}),
modifiers: new fields.ArrayField(new fields.SchemaField({
value: new fields.NumberField({ integer: true }),
label: new fields.StringField({}),
title: new fields.StringField({}),
})),
hope: diceField(),
fear: diceField(),
advantage: diceField(),
disadvantage: diceField(),
advantageSelected: new fields.NumberField({ initial: 0 }),
targets: new fields.ArrayField(new fields.SchemaField({
id: new fields.StringField({}),
name: new fields.StringField({}),
img: new fields.StringField({}),
difficulty: new fields.NumberField({ integer: true, nullable: true }),
evasion: new fields.NumberField({ integer: true }),
hit: new fields.BooleanField({ initial: false }),
})),
damage: new fields.SchemaField({
value: new fields.StringField({}),
type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false }),
bonusDamage: new fields.ArrayField(new fields.SchemaField({
value: new fields.StringField({}),
type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false }),
initiallySelected: new fields.BooleanField(),
appliesOn: new fields.StringField({ choices: Object.keys(SYSTEM.EFFECTS.applyLocations) }, { nullable: true, initial: null }),
description: new fields.StringField({}),
hopeIncrease: new fields.StringField({ nullable: true })
}), { nullable: true, initial: null })
})
}
}
get total() {
const modifiers = this.modifiers.reduce((acc, x) => acc+x.value, 0);
const advantage = this.advantage.value ?? this.disadvantage.value ? -this.disadvantage.value : 0;
return this.hope.value + this.fear.value + advantage + modifiers;
}
get totalLabel() {
const label = this.hope.value > this.fear.value ? "DAGGERHEART.General.Hope" : this.fear.value > this.hope.value ? "DAGGERHEART.General.Fear" : "DAGGERHEART.General.CriticalSuccess";
return game.i18n.localize(label);
}
prepareDerivedData(){
const total = this.total;
this.targets.forEach(target => {
target.hit = target.difficulty ? total >= target.difficulty : total >= target.evasion;
});
}
}
//V1.3
// const fields = foundry.data.fields;
// const diceField = () => new fields.SchemaField({
// dice: new fields.StringField({}),
// value: new fields.NumberField({ integer: true}),
// });
// export default class DhpDualityRoll extends foundry.abstract.TypeDataModel {
// static defineSchema() {
// return {
// roll: new fields.StringField({}),
// modifiers: new fields.ArrayField(new fields.SchemaField({
// value: new fields.NumberField({ integer: true }),
// label: new fields.StringField({}),
// title: new fields.StringField({}),
// })),
// hope: diceField(),
// fear: diceField(),
// advantage: diceField(),
// disadvantage: diceField(),
// advantageSelected: new fields.NumberField({ initial: 0 }),
// targets: new fields.ArrayField(new fields.SchemaField({
// id: new fields.StringField({}),
// name: new fields.StringField({}),
// img: new fields.StringField({}),
// difficulty: new fields.NumberField({ integer: true, nullable: true }),
// evasion: new fields.NumberField({ integer: true }),
// hit: new fields.BooleanField({ initial: false }),
// })),
// damage: new fields.SchemaField({
// value: new fields.StringField({}),
// type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false }),
// bonusDamage: new fields.ArrayField(new fields.SchemaField({
// value: new fields.StringField({}),
// type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false }),
// initiallySelected: new fields.BooleanField(),
// appliesOn: new fields.StringField({ choices: Object.keys(SYSTEM.EFFECTS.applyLocations) }, { nullable: true, initial: null }),
// description: new fields.StringField({}),
// hopeIncrease: new fields.StringField({ nullable: true })
// }), { nullable: true, initial: null })
// })
// }
// }
// get total() {
// const modifiers = this.modifiers.reduce((acc, x) => acc+x.value, 0);
// const regular = {
// normal: this.disadvantage.value ? Math.min(this.disadvantage.value, this.hope.value) + this.fear.value + modifiers : this.hope.value + this.fear.value + modifiers,
// alternate: this.advantage.value ? this.advantage.value + this.fear.value + modifiers : null,
// };
// const advantageSolve = this.advantageSelected === 0 ? null : {
// normal: this.advantageSelected === 1 ? this.hope.value + this.fear.value + modifiers : this.advantage.value + this.fear.value + modifiers,
// alternate: null,
// };
// return advantageSolve ?? regular;
// }
// get totalLabel() {
// if(this.advantage.value && this.advantageSelected === 0) return game.i18n.localize("DAGGERHEART.Chat.DualityRoll.AdvantageChooseTitle");
// const hope = !this.advantage.value || this.advantageSelected === 1 ? this.hope.value : this.advantage.value;
// const label = hope > this.fear.value ? "DAGGERHEART.General.Hope" : this.fear.value > hope ? "DAGGERHEART.General.Fear" : "DAGGERHEART.General.CriticalSuccess";
// return game.i18n.localize(label);
// }
// get dualityDiceStates() {
// return {
// hope: this.hope.value > this.fear.value ? 'hope' : this.fear.value > this.hope.value ? 'fear' : 'critical',
// alternate: this.advantage.value > this.fear.value ? 'hope' : this.fear.value > this.advantage.value ? 'fear' : 'critical',
// }
// }
// prepareDerivedData(){
// const total = this.total;
// if(total.alternate) return false;
// this.targets.forEach(target => {
// target.hit = target.difficulty ? total.normal >= target.difficulty : total.normal >= target.evasion;
// });
// }
// }

View file

@ -0,0 +1,20 @@
export default class DhpEnvironment extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
resources: new fields.SchemaField({
}),
tier: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.tiers), integer: false }),
type: new fields.StringField({ choices: Object.keys(SYSTEM.ACTOR.adversaryTypes), integer: false, initial: Object.keys(SYSTEM.ACTOR.adversaryTypes).find(x => x === 'standard') }),
description: new fields.StringField({}),
toneAndFeel: new fields.StringField({}),
difficulty: new fields.NumberField({ initial: 1, integer: true }),
potentialAdversaries: new fields.StringField({}),
}
}
get features(){
return this.parent.items.filter(x => x.type === 'feature');
}
}

77
module/data/feature.mjs Normal file
View file

@ -0,0 +1,77 @@
import { getTier } from "../helpers/utils.mjs";
import DaggerheartAction from "./action.mjs";
import { MappingField } from "./fields.mjs";
import DhpEffect from "./interface/effects.mjs";
export default class DhpFeature extends DhpEffect {
static defineSchema() {
const fields = foundry.data.fields;
return foundry.utils.mergeObject({}, {
type: new fields.StringField({ choices: SYSTEM.ITEM.featureTypes }),
actionType: new fields.StringField({ choices: SYSTEM.ITEM.actionTypes, initial: SYSTEM.ITEM.actionTypes.passive.id }),
featureType: new fields.SchemaField({
type: new fields.StringField({ choices: SYSTEM.ITEM.valueTypes, initial: Object.keys(SYSTEM.ITEM.valueTypes).find(x => x === 'normal') }),
data: new fields.SchemaField({
value: new fields.StringField({}),
property: new fields.StringField({ choices: SYSTEM.ACTOR.featureProperties, initial: Object.keys(SYSTEM.ACTOR.featureProperties).find(x => x === 'spellcastingTrait') }),
max: new fields.NumberField({ initial: 1, integer: true }),
numbers: new MappingField(new fields.SchemaField({
value: new fields.NumberField({ integer: true }),
used: new fields.BooleanField({ initial: false }),
})),
}),
}),
refreshData: new fields.SchemaField({
type: new fields.StringField({ choices: SYSTEM.GENERAL.refreshTypes }),
uses: new fields.NumberField({ initial: 1, integer: true }),
refreshed: new fields.BooleanField({ initial: true })
}, { nullable: true, initial: null }),
multiclass: new fields.NumberField({ initial: null, nullable: true, integer: true }),
disabled: new fields.BooleanField({ initial: false }),
description: new fields.HTMLField({}),
effects: new MappingField(new fields.SchemaField({
type: new fields.StringField({ choices: SYSTEM.EFFECTS.effectTypes }),
valueType: new fields.StringField({ choices: SYSTEM.EFFECTS.valueTypes }),
parseType: new fields.StringField({ choices: SYSTEM.EFFECTS.parseTypes }),
initiallySelected: new fields.BooleanField({ initial: true }),
options: new fields.ArrayField(new fields.SchemaField({
name: new fields.StringField({}),
value: new fields.StringField({}),
}), { nullable: true, initial: null }),
dataField: new fields.StringField({}),
appliesOn: new fields.StringField({ choices: SYSTEM.EFFECTS.applyLocations }, { nullable: true, initial: null }),
applyLocationChoices: new MappingField(new fields.StringField({}), { nullable: true, initial: null }),
valueData: new fields.SchemaField({
value: new fields.StringField({}),
fromValue: new fields.StringField({ initial: null, nullable: true }),
type: new fields.StringField({ initial: null, nullable: true }),
hopeIncrease: new fields.StringField({ initial: null, nullable: true })
}),
})),
actions: new fields.ArrayField(new fields.EmbeddedDataField(DaggerheartAction)),
});
}
get multiclassTier(){
return getTier(this.multiclass);
}
async refresh(){
if(this.refreshData){
if(this.featureType.type === SYSTEM.ITEM.valueTypes.dice.id) {
const update = { "system.refreshData.refreshed": true };
Object.keys(this.featureType.data.numbers).forEach(x => update[`system.featureType.data.numbers.-=${x}`] = null);
await this.parent.update(update);
}
else {
await this.parent.update({ "system.refreshData.refreshed": true});
}
}
}
// prepareDerivedData(){
// if(this.featureType.type === SYSTEM.ITEM.valueTypes.dice.id){
// this.featureType.numbers = ;
// }
// }
}

109
module/data/fields.mjs Normal file
View file

@ -0,0 +1,109 @@
export class MappingField extends foundry.data.fields.ObjectField {
constructor(model, options) {
if ( !(model instanceof foundry.data.fields.DataField) ) {
throw new Error("MappingField must have a DataField as its contained element");
}
super(options);
/**
* The embedded DataField definition which is contained in this field.
* @type {DataField}
*/
this.model = model;
}
/* -------------------------------------------- */
/** @inheritdoc */
static get _defaults() {
return foundry.utils.mergeObject(super._defaults, {
initialKeys: null,
initialValue: null,
initialKeysOnly: false
});
}
/* -------------------------------------------- */
/** @inheritdoc */
_cleanType(value, options) {
Object.entries(value).forEach(([k, v]) => value[k] = this.model.clean(v, options));
return value;
}
/* -------------------------------------------- */
/** @inheritdoc */
getInitialValue(data) {
let keys = this.initialKeys;
const initial = super.getInitialValue(data);
if ( !keys || !foundry.utils.isEmpty(initial) ) return initial;
if ( !(keys instanceof Array) ) keys = Object.keys(keys);
for ( const key of keys ) initial[key] = this._getInitialValueForKey(key);
return initial;
}
/* -------------------------------------------- */
/**
* Get the initial value for the provided key.
* @param {string} key Key within the object being built.
* @param {object} [object] Any existing mapping data.
* @returns {*} Initial value based on provided field type.
*/
_getInitialValueForKey(key, object) {
const initial = this.model.getInitialValue();
return this.initialValue?.(key, initial, object) ?? initial;
}
/* -------------------------------------------- */
/** @override */
_validateType(value, options={}) {
if ( foundry.utils.getType(value) !== "Object" ) throw new Error("must be an Object");
const errors = this._validateValues(value, options);
if ( !foundry.utils.isEmpty(errors) ) throw new foundry.data.fields.ModelValidationError(errors);
}
/* -------------------------------------------- */
/**
* Validate each value of the object.
* @param {object} value The object to validate.
* @param {object} options Validation options.
* @returns {Object<Error>} An object of value-specific errors by key.
*/
_validateValues(value, options) {
const errors = {};
for ( const [k, v] of Object.entries(value) ) {
const error = this.model.validate(v, options);
if ( error ) errors[k] = error;
}
return errors;
}
/* -------------------------------------------- */
/** @override */
initialize(value, model, options={}) {
if ( !value ) return value;
const obj = {};
const initialKeys = (this.initialKeys instanceof Array) ? this.initialKeys : Object.keys(this.initialKeys ?? {});
const keys = this.initialKeysOnly ? initialKeys : Object.keys(value);
for ( const key of keys ) {
const data = value[key] ?? this._getInitialValueForKey(key, value);
obj[key] = this.model.initialize(data, model, options);
}
return obj;
}
/* -------------------------------------------- */
/** @inheritdoc */
_getField(path) {
if ( path.length === 0 ) return this;
else if ( path.length === 1 ) return this.model;
path.shift();
return this.model._getField(path);
}
}

View file

@ -0,0 +1,75 @@
import DaggerheartAction from "../action.mjs";
import { MappingField } from "../fields.mjs";
export default class DhpEffects extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
effects: new MappingField(new fields.SchemaField({
type: new fields.StringField({ choices: Object.keys(SYSTEM.EFFECTS.effectTypes) }),
valueType: new fields.StringField({ choices: Object.keys(SYSTEM.EFFECTS.valueTypes) }),
parseType: new fields.StringField({ choices: Object.keys(SYSTEM.EFFECTS.parseTypes) }),
initiallySelected: new fields.BooleanField({ initial: true }),
options: new fields.ArrayField(new fields.SchemaField({
name: new fields.StringField({}),
value: new fields.StringField({}),
}), { nullable: true, initial: null }),
dataField: new fields.StringField({}),
appliesOn: new fields.StringField({ choices: Object.keys(SYSTEM.EFFECTS.applyLocations) }, { nullable: true, initial: null }),
applyLocationChoices: new MappingField(new fields.StringField({}), { nullable: true, initial: null }),
valueData: new fields.SchemaField({
value: new fields.StringField({}),
fromValue: new fields.StringField({ initial: null, nullable: true }),
type: new fields.StringField({ initial: null, nullable: true }),
hopeIncrease: new fields.StringField({ initial: null, nullable: true })
}),
})),
actions: new fields.ArrayField(new fields.EmbeddedDataField(DaggerheartAction)),
// actions: new fields.SchemaField({
// damage: new fields.ArrayField(new fields.SchemaField({
// type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.extendedDamageTypes), initial: SYSTEM.GENERAL.extendedDamageTypes.physical.id }),
// value: new fields.StringField({}),
// })),
// uses: new fields.SchemaField({
// nr: new fields.StringField({}),
// refreshType: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.refreshTypes), initial: SYSTEM.GENERAL.refreshTypes.session.id }),
// refreshed: new fields.BooleanField({ initial: true }),
// }),
// }),
}
}
get effectData(){
const effectValues = Object.values(this.effects);
const effectCategories = Object.keys(SYSTEM.EFFECTS.effectTypes).reduce((acc, effectType) => {
acc[effectType] = effectValues.reduce((acc, effect) => {
if(effect.type === effectType){
acc.push({ ...effect, valueData: this.#parseValues(effect.parseType, effect.valueData) });
}
return acc;
}, []);
return acc;
}, {});
return effectCategories;
}
#parseValues(parseType, values){
return Object.keys(values).reduce((acc, prop) => {
acc[prop] = this.#parseValue(parseType, values[prop]);
return acc;
}, {});
}
#parseValue(parseType, value) {
switch(parseType){
case SYSTEM.EFFECTS.parseTypes.number.id:
return Number.parseInt(value);
default:
return value;
}
}
}

View file

@ -0,0 +1,10 @@
const fields = foundry.data.fields;
const featuresSchema = () => new fields.ArrayField(new fields.SchemaField({
name: new fields.StringField({}),
img: new fields.StringField({}),
uuid: new fields.StringField({}),
subclassLevel: new fields.StringField({}),
}))
export default featuresSchema;

View file

@ -0,0 +1,9 @@
export default class DhpMiscellaneous extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({}),
quantity: new fields.NumberField({ initial: 1, integer: true })
}
}
}

497
module/data/pc.mjs Normal file
View file

@ -0,0 +1,497 @@
import { getPathValue, getTier } from "../helpers/utils.mjs";
import { MappingField } from "./fields.mjs";
const fields = foundry.data.fields;
const attributeField = () => new fields.SchemaField({
data: new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true }),
base: new fields.NumberField({ initial: 0, integer: true }),
bonus: new fields.NumberField({ initial: 0, integer: true }),
actualValue: new fields.NumberField({ initial: 0, integer: true }),
overrideValue: new fields.NumberField({ initial: 0, integer: true }),
}),
levelMarks: new fields.ArrayField(new fields.NumberField({ nullable: true, initial: null, integer: true })),
levelMark: new fields.NumberField({ nullable: true, initial: null, integer: true }),
});
const levelUpTier = () => ({
attributes: new MappingField(new fields.BooleanField()),
hitPointSlots: new MappingField(new fields.BooleanField()),
stressSlots: new MappingField(new fields.BooleanField()),
experiences: new MappingField(new fields.ArrayField(new fields.StringField({}))),
proficiency: new MappingField(new fields.BooleanField()),
armorOrEvasionSlot: new MappingField(new fields.StringField({})),
majorDamageThreshold2: new MappingField(new fields.BooleanField()),
severeDamageThreshold2: new MappingField(new fields.BooleanField()),
severeDamageThreshold3: new MappingField(new fields.BooleanField()),
severeDamageThreshold4: new MappingField(new fields.BooleanField()),
subclass: new MappingField(new fields.SchemaField({
multiclass: new fields.BooleanField(),
feature: new fields.StringField({}),
})),
multiclass: new MappingField(new fields.BooleanField()),
});
// const weapon = () => new fields.SchemaField({
// name: new fields.StringField({}),
// trait: new fields.StringField({}),
// range: new fields.StringField({}),
// damage: new fields.SchemaField({
// value: new fields.StringField({}),
// type: new fields.StringField({}),
// }),
// feature: new fields.StringField({}),
// img: new fields.StringField({}),
// uuid: new fields.StringField({}),
// }, { initial: null, nullable: true });
export default class DhpPC extends foundry.abstract.TypeDataModel {
static defineSchema() {
return {
resources: new fields.SchemaField({
health: new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true }),
min: new fields.NumberField({ initial: 0, integer: true }),
max: new fields.NumberField({ initial: 6, integer: true }),
}),
stress: new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true }),
min: new fields.NumberField({ initial: 0, integer: true }),
max: new fields.NumberField({ initial: 6, integer: true }),
}),
hope: new fields.SchemaField({
value: new fields.NumberField({ initial: -1, integer: true }), // FIXME. Logic is gte and needs -1 in PC/Hope. Change to 0
min: new fields.NumberField({ initial: 0, integer: true }),
}),
}),
bonuses: new fields.SchemaField({
damage: new fields.ArrayField(new fields.SchemaField({
value: new fields.NumberField({ integer: true, initial: 0 }),
type: new fields.StringField({ nullable: true }),
initiallySelected: new fields.BooleanField(),
hopeIncrease: new fields.StringField({ initial: null, nullable: true }),
description: new fields.StringField({}),
})),
}),
attributes: new fields.SchemaField({
agility: attributeField(),
strength: attributeField(),
finesse: attributeField(),
instinct: attributeField(),
presence: attributeField(),
knowledge: attributeField(),
}),
proficiency: new fields.SchemaField({
value: new fields.NumberField({ initial: 1, integer: true}),
min: new fields.NumberField({ initial: 1, integer: true}),
max: new fields.NumberField({ initial: 6, integer: true}),
}),
damageThresholds: new fields.SchemaField({
minor: new fields.NumberField({ initial: 0, integer: true }),
major: new fields.NumberField({ initial: 0, integer: true }),
severe: new fields.NumberField({ initial: 0, integer: true }),
}),
evasion: new fields.NumberField({ initial: 0, integer: true }),
// armor: new fields.SchemaField({
// value: new fields.NumberField({ initial: 0, integer: true }),
// customValue: new fields.NumberField({ initial: null, nullable: true }),
// }),
experiences: new fields.ArrayField(new fields.SchemaField({
id: new fields.StringField({ required: true }),
level: new fields.NumberField({ required: true, integer: true }),
description: new fields.StringField({}),
value: new fields.NumberField({ integer: true, nullable: true, initial: null }),
}), {
initial: [
{ id: foundry.utils.randomID(), level: 1, description: '', value: 2 },
{ id: foundry.utils.randomID(), level: 1, description: '', value: 2 },
]
}),
gold: new fields.SchemaField({
coins: new fields.NumberField({ initial: 0, integer: true }),
handfulls: new fields.NumberField({ initial: 0, integer: true }),
bags: new fields.NumberField({ initial: 0, integer: true }),
chests: new fields.NumberField({ initial: 0, integer: true }),
}),
pronouns: new fields.StringField({}),
domainData: new fields.SchemaField({
maxLoadout: new fields.NumberField({ initial: 2, integer: true }),
maxCards: new fields.NumberField({ initial: 2, integer: true }),
}),
levelData: new fields.SchemaField({
currentLevel: new fields.NumberField({ initial: 1, integer: true }),
changedLevel: new fields.NumberField({ initial: 1, integer: true }),
levelups: new MappingField(new fields.SchemaField({
level: new fields.NumberField({ required: true, integer: true }),
tier1: new fields.SchemaField({
...levelUpTier()
}),
tier2: new fields.SchemaField({
...levelUpTier()
}, { nullable: true, initial: null }),
tier3: new fields.SchemaField({
...levelUpTier()
}, { nullable: true, initial: null }),
})),
}),
story: new fields.SchemaField({
background: new fields.HTMLField(),
appearance: new fields.HTMLField(),
connections: new fields.HTMLField(),
scars: new fields.ArrayField(new fields.SchemaField({
name: new fields.StringField({}),
description: new fields.HTMLField(),
})),
}),
description: new fields.StringField({}),
//Temporary until new FoundryVersion fix --> See Armor.Mjs DataPreparation
armorMarks: new fields.SchemaField({
max: new fields.NumberField({ initial: 6, integer: true }),
value: new fields.NumberField({ initial: 0, integer: true }),
}),
}
}
get canLevelUp(){
// return Object.values(this.levels.data).some(x => !x.completed);
return this.levelData.currentLevel !== this.levelData.changedLevel;
}
get tier(){
return this.#getTier(this.levelData.currentLevel);
}
get ancestry(){
return this.parent.items.find(x => x.type === 'ancestry') ?? null;
}
get class(){
return this.parent.items.find(x => x.type === 'class' && !x.system.multiclass) ?? null;
}
get multiclass(){
return this.parent.items.find(x => x.type === 'class' && x.system.multiclass) ?? null;
}
get multiclassSubclass(){
return this.parent.items.find(x => x.type === 'subclass' && x.system.multiclass) ?? null;
}
get subclass(){
return this.parent.items.find(x => x.type === 'subclass' && !x.system.multiclass) ?? null;
}
get subclassFeatures(){
const subclass = this.subclass;
const multiclass = this.multiclassSubclass;
const subclassItems = this.parent.items.filter(x => x.type === 'feature' && x.system.type === 'subclass');
return {
subclass: !subclass ? {} : {
foundation: subclassItems.filter(x => subclass.system.foundationFeature.abilities.some(ability => ability.uuid === x.uuid)),
specialization: subclassItems.filter(x => subclass.system.specializationFeature.abilities.some(ability => ability.uuid === x.uuid)),
mastery: subclassItems.filter(x => subclass.system.masteryFeature.abilities.some(ability => ability.uuid === x.uuid)),
},
multiclassSubclass: !multiclass ? {} : {
foundation: subclassItems.filter(x => multiclass.system.foundationFeature.abilities.some(ability => ability.uuid === x.uuid)),
specialization: subclassItems.filter(x => multiclass.system.specializationFeature.abilities.some(ability => ability.uuid === x.uuid)),
mastery: subclassItems.filter(x => multiclass.system.masteryFeature.abilities.some(ability => ability.uuid === x.uuid)),
}
}
}
get community(){
return this.parent.items.find(x => x.type === 'community') ?? null;
}
get classFeatures(){
return this.parent.items.filter(x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.class.id && !x.system.multiclass);
}
get multiclassFeatures(){
return this.parent.items.filter(x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.class.id && x.system.multiclass);
}
get domains(){
const classDomains = this.class ? this.class.system.domains : [];
const multiclassDomains = this.multiclass ? this.multiclass.system.domains : [];
return [...classDomains, ...multiclassDomains]
}
get domainCards(){
const domainCards = this.parent.items.filter(x => x.type === 'domainCard');
const loadout = domainCards.filter(x => !x.system.inVault);
const vault = domainCards.filter(x => x.system.inVault);
return {
loadout: loadout,
vault: vault,
total: [...loadout, ...vault]
};
}
get armor(){
return this.parent.items.find(x => x.type === 'armor');
}
get activeWeapons(){
const primaryWeapon = this.parent.items.find(x => x.type === 'weapon' && x.system.active && !x.system.secondary);
const secondaryWeapon = this.parent.items.find(x => x.type === 'weapon' && x.system.active && x.system.secondary);
return {
primary: this.#weaponData(primaryWeapon),
secondary: this.#weaponData(secondaryWeapon),
burden: this.getBurden(primaryWeapon, secondaryWeapon)
};
}
get inventoryWeapons(){
const inventoryWeaponFirst = this.parent.items.find(x => x.type === 'weapon' && x.system.inventoryWeapon === 1);
const inventoryWeaponSecond = this.parent.items.find(x => x.type === 'weapon' && x.system.inventoryWeapon === 2);
return {
first: this.#weaponData(inventoryWeaponFirst),
second: this.#weaponData(inventoryWeaponSecond),
};
}
get totalAttributeMarks(){
return Object.keys(this.levelData.levelups).reduce((nr, level) => {
const nrAttributeMarks = Object.keys(this.levelData.levelups[level]).reduce((nr, tier) => {
nr += Object.keys(this.levelData.levelups[level][tier]?.attributes ?? {}).length * 2;
return nr;
}, 0);
nr.push(...Array(nrAttributeMarks).fill(Number.parseInt(level)));
return nr;
}, []);
}
get availableAttributeMarks(){
const attributeMarks = Object.keys(this.attributes).flatMap(y => this.attributes[y].levelMarks);
return this.totalAttributeMarks.reduce((acc, attribute) => {
if(!attributeMarks.findSplice(x => x === attribute)){
acc.push(attribute);
}
return acc;
}, []);
}
get effects(){
return this.parent.items.reduce((acc, item) => {
const effects = item.system.effectData;
if(effects && !item.system.disabled){
for(var key in effects){
const effect = effects[key];
for(var effectEntry of effect){
if(!acc[key]) acc[key] = [];
acc[key].push({ name: item.name, value: effectEntry });
}
}
}
return acc;
}, {});
}
get refreshableFeatures(){
return this.parent.items.reduce((acc, x) => {
if(x.type === 'feature' && x.system.refreshData.type){
acc[x.system.refreshData.type].push(x);
}
return acc;
}, { shortRest: [], longRest: [] });
}
#weaponData(weapon){
return weapon ? {
name: weapon.name,
trait: CONFIG.daggerheart.ACTOR.abilities[weapon.system.trait].name, //Should not be done in data?
range: CONFIG.daggerheart.GENERAL.range[weapon.system.range],
damage: {
value: weapon.system.damage.value,
type: CONFIG.daggerheart.GENERAL.damageTypes[weapon.system.damage.type],
},
feature: CONFIG.daggerheart.ITEM.weaponFeatures[weapon.system.feature],
img: weapon.img,
uuid: weapon.uuid
} : null
}
prepareDerivedData(){
this.resources.hope.max = 6 - this.story.scars.length;
if(this.resources.hope.value >= this.resources.hope.max){
this.resources.hope.value = Math.max(this.resources.hope.max-1, 0);
}
for(var attributeKey in this.attributes){
const attribute = this.attributes[attributeKey];
attribute.levelMark = attribute.levelMarks.find(x => this.isSameTier(x)) ?? null;
const actualValue = attribute.data.base + attribute.levelMarks.length + attribute.data.bonus;
attribute.data.actualValue = actualValue;
attribute.data.value = attribute.data.overrideValue ? attribute.data.overrideValue : attribute.data.actualValue;
}
this.evasion = this.class?.system?.evasion ?? 0;
// this.armor.value = this.activeArmor?.baseScore ?? 0;
this.damageThresholds = this.class?.system?.damageThresholds ?? { minor: 0, major: 0, severe: 0 };
this.applyLevels();
this.applyEffects();
}
applyLevels(){
let healthBonus = 0, stressBonus = 0, proficiencyBonus = 0, evasionBonus = 0, armorBonus = 0, minorThresholdBonus = 0, majorThresholdBonus = 0, severeThresholdBonus = 0;
let experienceBonuses = {};
let advancementFirst = null, advancementSecond = null;
for(var level in this.levelData.levelups){
var levelData = this.levelData.levelups[level];
for(var tier in levelData){
var tierData = levelData[tier];
if(tierData){
healthBonus += Object.keys(tierData.hitPointSlots).length;
stressBonus += Object.keys(tierData.stressSlots).length;
proficiencyBonus += Object.keys(tierData.proficiency).length;
advancementFirst = Object.keys(tierData.subclass).length > 0 && (level >= 5 && level <= 7) ? { ...tierData.subclass[0], tier: getTier(Number.parseInt(level), true) } : advancementFirst;
advancementSecond = Object.keys(tierData.subclass).length > 0 && (level >= 8 && level <= 10) ? { ...tierData.subclass[0], tier: getTier(Number.parseInt(level), true) } : advancementSecond;
for(var index in Object.keys(tierData.experiences)){
for(var experienceKey in tierData.experiences[index]){
var experience = tierData.experiences[index][experienceKey];
experienceBonuses[experience] = experienceBonuses[experience] ? experienceBonuses[experience]+1 : 1;
}
}
evasionBonus += Object.keys(tierData.armorOrEvasionSlot).filter(x => tierData.armorOrEvasionSlot[x] === 'evasion').length;
armorBonus += Object.keys(tierData.armorOrEvasionSlot).filter(x => tierData.armorOrEvasionSlot[x] === 'armor').length;
majorThresholdBonus += Object.keys(tierData.majorDamageThreshold2).length * 2;
severeThresholdBonus += Object.keys(tierData.severeDamageThreshold2).length * 2;
severeThresholdBonus += Object.keys(tierData.severeDamageThreshold3).length * 3;
severeThresholdBonus += Object.keys(tierData.severeDamageThreshold4).length * 4;
}
}
}
this.resources.health.max += healthBonus;
this.resources.stress.max += stressBonus;
this.proficiency.value += proficiencyBonus;
this.evasion += evasionBonus;
this.armorMarks = {
max: this.armor ? this.armor.system.marks.max + armorBonus : 0,
value: this.armor ? this.armor.system.marks.value : 0,
};
this.damageThresholds.minor += minorThresholdBonus;
this.damageThresholds.major += majorThresholdBonus;
this.damageThresholds.severe += severeThresholdBonus;
this.experiences = this.experiences.map(x => ({ ...x, value: x.value + (experienceBonuses[x.id] ?? 0) }));
const subclassFeatures = this.subclassFeatures;
if(advancementFirst){
if(advancementFirst.multiclass){
this.multiclassSubclass.system[`${advancementFirst.feature}Feature`].unlocked = true;
this.multiclassSubclass.system[`${advancementFirst.feature}Feature`].tier = advancementFirst.tier;
subclassFeatures.multiclassSubclass[advancementFirst.feature].forEach(x => x.system.disabled = false);
} else {
this.subclass.system[`${advancementFirst.feature}Feature`].unlocked = true;
this.subclass.system[`${advancementFirst.feature}Feature`].tier = advancementFirst.tier;
subclassFeatures.subclass[advancementFirst.feature].forEach(x => x.system.disabled = false);
}
}
if(advancementSecond){
if(advancementSecond.multiclass){
this.multiclassSubclass.system[`${advancementSecond.feature}Feature`].unlocked = true;
this.multiclassSubclass.system[`${advancementSecond.feature}Feature`].tier = advancementSecond.tier;
subclassFeatures.multiclassSubclass[advancementSecond.feature].forEach(x => x.system.disabled = false);
} else {
this.subclass.system[`${advancementSecond.feature}Feature`].unlocked = true;
this.subclass.system[`${advancementSecond.feature}Feature`].tier = advancementSecond.tier;
subclassFeatures.subclass[advancementSecond.feature].forEach(x => x.system.disabled = false);
}
}
//General progression
for(var i = 0; i < this.levelData.currentLevel; i++){
const tier = getTier(i+1);
if(tier !== 'tier0'){
this.domainData.maxLoadout = Math.min(this.domainData.maxLoadout+1, 5);
this.domainData.maxCards += 1;
}
switch(tier){
case 'tier1':
this.damageThresholds.severe += 2;
break;
case 'tier2':
this.damageThresholds.major += 1;
this.damageThresholds.severe += 3;
break;
case 'tier3':
this.damageThresholds.major += 2;
this.damageThresholds.severe += 4;
break;
}
}
}
applyEffects(){
const effects = this.effects;
for(var key in effects){
const effectType = effects[key];
for(var effect of effectType) {
switch(key) {
case SYSTEM.EFFECTS.effectTypes.health.id:
this.resources.health.max += effect.value.valueData.value;
break;
case SYSTEM.EFFECTS.effectTypes.stress.id:
this.resources.stress.max += effect.value.valueData.value;
break;
case SYSTEM.EFFECTS.effectTypes.damage.id:
this.bonuses.damage.push({
value: getPathValue(effect.value.valueData.value, this),
type: 'physical',
description: effect.name,
hopeIncrease: effect.value.valueData.hopeIncrease,
initiallySelected: effect.value.initiallySelected,
appliesOn: effect.value.appliesOn,
});
}
}
}
}
getBurden(primary, secondary){
const twoHanded =
primary?.system?.burden === 'twoHanded' ||
secondary?.system?.burden === 'twoHanded' ||
(
primary?.system?.burden === 'oneHanded' &&
secondary?.system?.burden === 'oneHanded'
);
const oneHanded =
!twoHanded &&
(
primary?.system?.burden === 'oneHanded' ||
secondary?.system?.burden === 'oneHanded'
);
return twoHanded ? 'twoHanded' : oneHanded ? 'oneHanded' : null;
}
isSameTier(level){
return this.#getTier(this.levelData.currentLevel) === this.#getTier(level);
}
#getTier(level){
if(level >= 8) return 3;
else if(level >= 5) return 2;
else if(level >= 2) return 1;
else return 0;
}
}

34
module/data/subclass.mjs Normal file
View file

@ -0,0 +1,34 @@
import { getTier } from "../helpers/utils.mjs";
import featuresSchema from "./interface/featuresSchema.mjs";
import DaggerheartFeature from './feature.mjs';
export default class DhpSubclass extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({}),
spellcastingTrait: new fields.StringField({ choices: SYSTEM.ACTOR.abilities, integer: false, nullable: true, initial: null }),
foundationFeature: new fields.SchemaField({
description: new fields.HTMLField({}),
abilities: new fields.ArrayField(new fields.EmbeddedDataField(DaggerheartFeature)),
}),
specializationFeature: new fields.SchemaField({
unlocked: new fields.BooleanField({ initial: false }),
tier: new fields.NumberField({ initial: null, nullable: true, integer: true }),
description: new fields.HTMLField({}),
abilities: new fields.ArrayField(new fields.EmbeddedDataField(DaggerheartFeature)),
}),
masteryFeature: new fields.SchemaField({
unlocked: new fields.BooleanField({ initial: false }),
tier: new fields.NumberField({ initial: null, nullable: true, integer: true }),
description: new fields.HTMLField({}),
abilities: new fields.ArrayField(new fields.EmbeddedDataField(DaggerheartFeature)),
}),
multiclass: new fields.NumberField({ initial: null, nullable: true, integer: true }),
}
}
get multiclassTier(){
return getTier(this.multiclass);
}
}

47
module/data/weapon.mjs Normal file
View file

@ -0,0 +1,47 @@
export default class DhpWeapon extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
active: new fields.BooleanField({ initial: false }),
inventoryWeapon: new fields.NumberField({ initial: null, nullable: true, integer: true }),
secondary: new fields.BooleanField({ initial: false }),
trait: new fields.StringField({ choices: SYSTEM.ACTOR.abilities, integer: false }),
range: new fields.StringField({ choices: SYSTEM.GENERAL.range, integer: false }),
damage: new fields.SchemaField({
value: new fields.StringField({}),
type: new fields.StringField({ choices: SYSTEM.GENERAL.damageTypes, integer: false }),
}),
burden: new fields.StringField({ choices: SYSTEM.GENERAL.burden, integer: false }),
feature: new fields.StringField({ choices: SYSTEM.ITEM.weaponFeatures, integer: false }),
quantity: new fields.NumberField({ initial: 1, integer: true }),
description: new fields.HTMLField({}),
}
}
prepareDerivedData(){
if(this.parent.parent){
this.applyEffects();
}
}
applyEffects(){
const effects = this.parent.parent.system.effects;
for(var key in effects){
const effectType = effects[key];
for(var effect of effectType) {
switch(key) {
case SYSTEM.EFFECTS.effectTypes.reach.id:
if(SYSTEM.GENERAL.range[this.range].distance < SYSTEM.GENERAL.range[effect.valueData.value].distance){
this.range = effect.valueData.value;
}
break;
// case SYSTEM.EFFECTS.effectTypes.damage.id:
// if(this.damage.type === 'physical') this.damage.value = (`${this.damage.value} + ${this.parent.parent.system.levelData.currentLevel}`);
// break;
}
}
}
}
}

View file

@ -0,0 +1,94 @@
export default class SelectDialog extends Dialog {
constructor(data, options) {
super(options);
this.data = {
title: data.title,
buttons: data.buttons,
content: renderTemplate("systems/daggerheart/templates/dialog/item-select.hbs", {
items: data.choices
}),
};
this.actor = data.actor;
this.actionCostMax = data.actionCostMax;
this.nrChoices = data.nrChoices;
this.validate= data.validate;
}
async getData(options={}) {
let buttons = Object.keys(this.data.buttons).reduce((obj, key) => {
let b = this.data.buttons[key];
b.cssClass = (this.data.default === key ? [key, "default", "bright"] : [key]).join(" ");
if ( b.condition !== false ) obj[key] = b;
return obj;
}, {});
const content = await this.data.content;
return {
content: content,
buttons: buttons
};
}
activateListeners(html) {
super.activateListeners(html);
$(html).find('.item-button').click(this.selectChoice);
}
selectChoice = async (event) => {
if(this.validate){
if(!this.validate(event.currentTarget.dataset.validateProp)){
return;
}
}
event.currentTarget.classList.toggle('checked');
$(event.currentTarget).find('i')[0].classList.toggle('checked');
const buttons = $(this.element[0]).find('button.checked');
if(buttons.length === this.nrChoices){
$(event.currentTarget).closest('.window-content').find('.confirm')[0].disabled = false;
} else {
$(event.currentTarget).closest('.window-content').find('.confirm')[0].disabled = true;
}
}
/**
*
* @param {*} data
* choices, actor, title, cancelMessage, nrChoices, validate
* @returns
*/
static async selectItem(data) {
return this.wait({
title: data.title ?? "Selection",
buttons: {
no: {
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize("DAGGERHEART.General.Cancel"),
callback: _ => {
if(data.cancelMessage){
ChatMessage.create({content: data.cancelMessage });
}
return [];
}
},
confirm: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize("DAGGERHEART.General.OK"),
callback: html => {
const buttons = $(html).find('button.checked');
return buttons.map(key => Number.parseInt(buttons[key].dataset.index)).toArray();
},
disabled: true
},
},
choices: data.choices,
actor: data.actor,
nrChoices: data.nrChoices ?? 1,
validate: data.validate
});
}
}

View file

@ -0,0 +1,3 @@
export { default as DhpActor } from './actor.mjs';
export { default as DhpItem } from './item.mjs';
export { default as DhpCombat } from './combat.mjs';

349
module/documents/actor.mjs Normal file
View file

@ -0,0 +1,349 @@
import DamageSelectionDialog from "../applications/damageSelectionDialog.mjs";
import NpcRollSelectionDialog from "../applications/npcRollSelectionDialog.mjs";
import RollSelectionDialog from "../applications/rollSelectionDialog.mjs";
import { GMUpdateEvent, socketEvent } from "../helpers/socket.mjs";
export default class DhpActor extends Actor {
_preCreate(data, changes, user){
if(data.type === 'pc'){
data.prototypeToken = { actorLink: true, disposition: 1, sight: { enabled: true } };
}
super._preCreate(data, changes, user);
}
prepareData(){
super.prepareData();
}
async _preUpdate(changed, options, user) {
//Level Down
if(changed.system?.levelData?.changedLevel && this.system.levelData.currentLevel > changed.system.levelData.changedLevel){
changed.system.levelData.currentLevel = changed.system.levelData.changedLevel;
changed.system.levelData.levelups = Object.keys(this.system.levelData.levelups).reduce((acc, x) => {
if(x > changed.system.levelData.currentLevel){
acc[`-=${x}`] = null;
}
return acc;
}, {});
changed.system.attributes = Object.keys(this.system.attributes).reduce((acc, key) => {
acc[key] = { levelMarks: this.system.attributes[key].levelMarks.filter(x => x <= changed.system.levelData.currentLevel) };
return acc;
}, {});
changed.system.experiences = this.system.experiences.filter(x => x.level <= changed.system.levelData.currentLevel);
if(this.system.multiclass && this.system.multiclass.system.multiclass > changed.system.levelData.changedLevel){
const multiclassFeatures = this.items.filter(x => x.system.multiclass);
for(var feature of multiclassFeatures){
await feature.delete();
}
}
}
super._preUpdate(changed, options, user);
}
async diceRoll(modifier, shiftKey) {
if(this.type === 'pc'){
return await this.dualityRoll(modifier, shiftKey);
}
else {
return await this.npcRoll(modifier, shiftKey);
}
}
async npcRoll(modifier, shiftKey) {
let nrDice = 1;
let advantage = null;
const modifiers = [
{
value: Number.parseInt(modifier.value),
label: modifier.value >= 0 ? `+${modifier.value}` : `-${modifier.value}`,
title: modifier.title,
}
];
if(!shiftKey) {
const dialogClosed = new Promise((resolve, _) => {
new NpcRollSelectionDialog(this.system.experiences, resolve).render(true);
});
const result = await dialogClosed;
nrDice = result.nrDice;
advantage = result.advantage;
result.experiences.forEach(x => modifiers.push({ value: x.value, label: x.value >= 0 ? `+${x.value}` : `-${x.value}`, title: x.description }))
}
const roll = new Roll(`${nrDice}d20${advantage === true ? 'kh' : advantage === false ? 'kl': ''} ${modifiers.map(x => `+ ${x.value}`).join(' ')}`);
let rollResult = await roll.evaluate();
const diceResults = rollResult.dice.flatMap(x => x.results.flatMap(result => ({ value: result.result })));
return { roll, diceResults: diceResults, modifiers: modifiers };
}
async dualityRoll(modifier, shiftKey, bonusDamage=[]){
let hopeDice = 'd12', fearDice = 'd12', advantageDice = null, disadvantageDice = null, bonusDamageString = "";
const modifiers = [
{
value: Number.parseInt(modifier.value),
label: modifier.value >= 0 ? `+${modifier.value}` : `-${modifier.value}`,
title: modifier.title,
}
];
if(!shiftKey) {
const dialogClosed = new Promise((resolve, _) => {
new RollSelectionDialog(this.system.experiences, bonusDamage, this.system.resources.hope.value, resolve).render(true);
});
const result = await dialogClosed;
hopeDice = result.hope, fearDice = result.fear, advantageDice = result.advantage, disadvantageDice = result.disadvantage;
result.experiences.forEach(x => modifiers.push({ value: x.value, label: x.value >= 0 ? `+${x.value}` : `-${x.value}`, title: x.description }))
bonusDamageString = result.bonusDamage;
const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope);
if(automateHope && result.hopeUsed){
await this.update({ "system.resources.hope.value": this.system.resources.hope.value - result.hopeUsed });
}
}
const roll = new Roll(`1${hopeDice} + 1${fearDice}${advantageDice ? ` + 1${advantageDice}` : disadvantageDice ? ` - 1${disadvantageDice}` : ''} ${modifiers.map(x => `+ ${x.value}`).join(' ')}`);
let rollResult = await roll.evaluate();
rollResult.dice[0].options.appearance = {
colorset:"inspired",
foreground: "#FFFFFF",
background: "#008080",
outline: "#000000",
edge: "#806400",
texture: "bloodmoon",
material: "metal",
font: "Arial Black",
system: "standard"
};
if(advantageDice || disadvantageDice){
rollResult.dice[1].options.appearance = {
colorset:"inspired",
foreground: disadvantageDice ? "#b30000" : "#FFFFFF",
background: "#008080",
outline: disadvantageDice ? "#000000" : "#000000",
edge: "#806400",
texture: "bloodmoon",
material: "metal",
font: "Arial Black",
system: "standard"
};
rollResult.dice[2].options.appearance = {
colorset:"bloodmoon",
foreground: "#000000",
background: "#430070",
outline: "#b30000",
edge: "#000000",
texture: "bloodmoon",
material: "metal",
font: "Arial Black",
system: "standard"
};
}
else {
rollResult.dice[1].options.appearance = {
colorset:"bloodmoon",
foreground: "#000000",
background: "#430070",
outline: "#b30000",
edge: "#000000",
texture: "bloodmoon",
material: "metal",
font: "Arial Black",
system: "standard"
};
}
const hope = rollResult.dice[0].results[0].result;
const advantage = advantageDice ? rollResult.dice[1].results[0].result : null;
const disadvantage = disadvantageDice ? rollResult.dice[1].results[0].result : null;
const fear = advantage || disadvantage ? rollResult.dice[2].results[0].result : rollResult.dice[1].results[0].result;
if(disadvantage){
rollResult = {...rollResult, total: rollResult.total - Math.max(hope, disadvantage) };
}
if(advantage){
rollResult = {...rollResult, total: 'Select Hope Die' };
}
const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope);
if (automateHope && hope > fear){
await this.update({ "system.resources.hope.value": Math.min(this.system.resources.hope.value+1, this.system.resources.hope.max) });
}
if(automateHope && hope === fear){
await this.update({ "system.resources": {
"hope.value": Math.min(this.system.resources.hope.value+1, this.system.resources.hope.max),
"stress.value": Math.max(this.system.resources.stress.value-1, 0),
}});
}
return { roll, rollResult, hope: { dice: hopeDice, value: hope }, fear: { dice: fearDice, value: fear }, advantage: { dice: advantageDice, value: advantage }, disadvantage: { dice: disadvantageDice, value: disadvantage }, modifiers: modifiers, bonusDamageString };
}
async damageRoll(damage, shiftKey){
let rollString = damage.value;
let bonusDamage = damage.bonusDamage?.filter(x => x.initiallySelected) ?? [];
if(!shiftKey) {
const dialogClosed = new Promise((resolve, _) => {
new DamageSelectionDialog(rollString, bonusDamage, this.system.resources.hope.value, resolve).render(true);
});
const result = await dialogClosed;
bonusDamage = result.bonusDamage;
rollString = result.rollString;
const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope);
if(automateHope && result.hopeUsed){
await this.update({ "system.resources.hope.value": this.system.resources.hope.value - result.hopeUsed });
}
}
const roll = new Roll(rollString);
let rollResult = await roll.evaluate();
const dice = [];
const modifiers = [];
for(var i = 0; i < rollResult.terms.length; i++){
const term = rollResult.terms[i];
if(term.faces){
dice.push({type: `d${term.faces}`, value: term.total});
}
else if (term.operator){
}
else if(term.number){
const operator = i === 0 ? '' : rollResult.terms[i-1].operator;
modifiers.push(`${operator}${term.number}`);
}
}
const cls = getDocumentClass("ChatMessage");
const msg = new cls({
user: game.user.id,
content: await renderTemplate("systems/daggerheart/templates/chat/damage-roll.hbs", {
roll: rollString,
total: rollResult.total,
dice: dice,
modifiers: modifiers
}),
rolls: [roll]
});
cls.create(msg.toObject());
}
async takeDamage(damage, type){
const hpDamage =
damage >= this.system.damageThresholds.severe ? 3 :
damage >= this.system.damageThresholds.major ? 2 :
damage >= this.system.damageThresholds.minor ? 1 :
0;
const update = { "system.resources.health.value": Math.min(this.system.resources.health.value+hpDamage, this.system.resources.health.max) };
if(game.user.isGM){
await this.update(update);
} else {
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateDocument,
uuid: this.uuid,
update: update,
}
});
}
}
async takeHealing(healing, type) {
let update = { };
switch(type){
case SYSTEM.GENERAL.healingTypes.health.id:
update = { "system.resources.health.value": Math.max(this.system.resources.health.value - healing, 0) };
break;
case SYSTEM.GENERAL.healingTypes.stress.id:
update = { "system.resources.stress.value": Math.max(this.system.resources.stress.value - healing, 0) };
break;
}
if(game.user.isGM){
await this.update(update);
} else {
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateDocument,
uuid: this.uuid,
update: update,
}
});
}
}
async emulateItemDrop(data) {
const event = new DragEvent("drop", { altKey: game.keyboard.isModifierActive("Alt") });
return this.sheet._onDropItem(event, { data: data });
}
//Move to action-scope?
async useAction(action) {
const userTargets = Array.from(game.user.targets);
const otherTarget = action.target.type ===SYSTEM.ACTIONS.targetTypes.other.id;
if(otherTarget && userTargets.length === 0) {
ui.notifications.error(game.i18n.localize("DAGGERHEART.Notification.Error.ActionRequiresTarget"));
return;
}
if(action.cost.type != null && action.cost.value != null){
if (this.system.resources[action.cost.type].value < action.cost.value-1) {
ui.notifications.error(game.i18n.localize(`Insufficient ${action.cost.type} to use this ability`));
return;
}
}
// const targets = otherTarget ? userTargets : [game.user.character];
if(action.damage.type){
let roll = { formula: action.damage.value, result: action.damage.value };
if(Number.isNaN(Number.parseInt(action.damage.value))){
roll = await new Roll(`1${action.damage.value}`).evaluate();
}
const cls = getDocumentClass("ChatMessage");
const msg = new cls({
user: game.user.id,
content: await renderTemplate("systems/daggerheart/templates/chat/damage-roll.hbs", {
roll: roll.formula,
total: roll.result,
type: action.damage.type,
}),
});
cls.create(msg.toObject());
}
if(action.healing.type){
let roll = { formula: action.healing.value, result: action.healing.value };
if(Number.isNaN(Number.parseInt(action.healing.value))){
roll = await new Roll(`1${action.healing.value}`).evaluate();
}
const cls = getDocumentClass("ChatMessage");
const msg = new cls({
user: game.user.id,
content: await renderTemplate("systems/daggerheart/templates/chat/healing-roll.hbs", {
roll: roll.formula,
total: roll.result,
type: action.healing.type,
}),
});
cls.create(msg.toObject());
}
}
}

View file

@ -0,0 +1,41 @@
import { GMUpdateEvent, socketEvent } from "../helpers/socket.mjs";
export default class DhpCombat extends Combat {
_sortCombatants(a, b) {
if(a.isNPC !== b.isNPC){
const aVal = a.isNPC ? 0 : 1;
const bVal = b.isNPC ? 0 : 1;
return aVal - bVal;
}
return a.name.localeCompare(b.name);
}
async useActionToken(combatantId) {
const automateActionPoints = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.ActionPoints);
if(game.user.isGM){
if(this.system.actions < 1) return;
const update = automateActionPoints ?
{ "system.activeCombatant": combatantId, "system.actions": Math.max(this.system.actions-1, 0) } :
{ "system.activeCombatant": combatantId };
await this.update(update);
} else {
const update = automateActionPoints ?
{ "system.activeCombatant": combatantId, "system.actions": this.system.actions+1} :
{ "system.activeCombatant": combatantId };
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateDocument,
uuid: this.uuid,
update: update
}
});
}
}
}

72
module/documents/item.mjs Normal file
View file

@ -0,0 +1,72 @@
export default class DhpItem extends Item {
_preCreate(data, changes, user){
super._preCreate(data, changes, user);
}
prepareData(){
super.prepareData();
if(this.type === 'class'){
// Bad. Make this better.
// this.system.domains = CONFIG.daggerheart.DOMAIN.classDomainMap[Object.keys(CONFIG.daggerheart.DOMAIN.classDomainMap).find(x => x === this.name.toLowerCase())];
}
}
isInventoryItem(){
return ['weapon', 'armor', 'miscellaneous', 'consumable'].includes(this.type);
}
_onUpdate(data, options, userId) {
super._onUpdate(data, options, userId);
}
static async createDialog(data = {}, { parent = null, pack = null, ...options } = {}) {
const documentName = this.metadata.name;
const types = game.documentTypes[documentName].filter(t => t !== CONST.BASE_DOCUMENT_TYPE);
let collection;
if ( !parent ) {
if ( pack ) collection = game.packs.get(pack);
else collection = game.collections.get(documentName);
}
const folders = collection?._formatFolderSelectOptions() ?? [];
const label = game.i18n.localize(this.metadata.label);
const title = game.i18n.format("DOCUMENT.Create", {type: label});
const typeObjects = types.reduce((obj, t) => {
const label = CONFIG[documentName]?.typeLabels?.[t] ?? t;
obj[t] = { value: t, label: game.i18n.has(label) ? game.i18n.localize(label) : t };
return obj;
}, {});
// Render the document creation form
const html = await renderTemplate("systems/daggerheart/templates/sidebar/documentCreate.hbs", {
folders,
name: data.name || game.i18n.format("DOCUMENT.New", {type: label}),
folder: data.folder,
hasFolders: folders.length >= 1,
type: data.type || CONFIG[documentName]?.defaultType || typeObjects.armor,
types: {
Items: [typeObjects.armor, typeObjects.weapon, typeObjects.consumable, typeObjects.miscellaneous],
Character: [typeObjects.class, typeObjects.subclass, typeObjects.ancestry, typeObjects.community, typeObjects.feature, typeObjects.domainCard],
},
hasTypes: types.length > 1
});
// Render the confirmation dialog window
return Dialog.prompt({
title: title,
content: html,
label: title,
callback: html => {
const form = html[0].querySelector("form");
const fd = new FormDataExtended(form);
foundry.utils.mergeObject(data, fd.object, {inplace: true});
if ( !data.folder ) delete data.folder;
if ( types.length === 1 ) data.type = types[0];
if ( !data.name?.trim() ) data.name = this.defaultName();
return this.create(data, {parent, pack, renderSheet: true});
},
rejectClose: false,
options
});
}
}

View file

@ -0,0 +1,105 @@
import { getWidthOfText } from "./utils.mjs";
export default class RegisterHandlebarsHelpers {
static registerHelpers(){
Handlebars.registerHelper({
looseEq: this.looseEq,
times: this.times,
join: this.join,
add: this.add,
subtract: this.subtract,
objectSelector: this.objectSelector,
includes: this.includes,
simpleEditor: this.simpleEditor,
debug: this.debug,
});
};
static looseEq(a, b){
return a == b;
}
static times(nr, block){
var accum = '';
for(var i = 0; i < nr; ++i)
accum += block.fn(i);
return accum;
}
static join(...options){
return options.slice(0, options.length-1);
}
static add(a, b){
const aNum = Number.parseInt(a);
const bNum = Number.parseInt(b);
return (Number.isNaN(aNum) ? 0 : aNum) + (Number.isNaN(bNum) ? 0 : bNum);
}
static subtract(a, b){
const aNum = Number.parseInt(a);
const bNum = Number.parseInt(b);
return (Number.isNaN(aNum) ? 0 : aNum) - (Number.isNaN(bNum) ? 0 : bNum);
}
static objectSelector(options){
let { title, values, titleFontSize, ids, style } = options.hash;
const titleLength = getWidthOfText(title, titleFontSize, true, true);
const margins = 12;
const buttons = options.fn();
const nrButtons = Math.max($(buttons).length-1, 1);
const iconWidth = 26;
const texts = values.reduce((acc, x, index) => {
if(x){
acc.push(`<span class="object-select-item" data-action="viewObject" data-value="${ids[index]}">${x}</span>`);
}
return acc;
}, []).join(' ');
const html =
`<div ${style ? 'style="'+style+'"' : ''}">
<div class="object-select-display iconbar">
<span class="object-select-title">${title}</span>
<div class="object-select-text" style="padding-left: ${titleLength+margins}px; padding-right: ${nrButtons*iconWidth}px;">
${texts}
</div>
${buttons}
</div>
</div>
`;
return new Handlebars.SafeString(html);
}
static rangePicker(options) {
let {name, value, min, max, step} = options.hash;
name = name || "range";
value = value ?? "";
if ( Number.isNaN(value) ) value = "";
const html =
`<input type="range" name="${name}" value="${value}" min="${min}" max="${max}" step="${step}"/>
<span class="range-value">${value}</span>`;
return new Handlebars.SafeString(html);
}
static includes(list, item){
return list.includes(item);
}
static simpleEditor(content, options) {
const { target, editable=true, button, engine="tinymce", collaborate=false, class: cssClass } = options.hash;
const config = {name: target, value: content, button, collaborate, editable, engine};
const element = foundry.applications.fields.createEditorInput(config);
if ( cssClass ) element.querySelector(".editor-content").classList.add(cssClass);
return new Handlebars.SafeString(element.outerHTML);
}
static debug(a) {
console.log(JSON.stringify(a));
return a;
}
}

20
module/helpers/socket.mjs Normal file
View file

@ -0,0 +1,20 @@
export function handleSocketEvent({action=null, data={}}={}) {
switch (action) {
case socketEvent.GMUpdate:
Hooks.callAll(socketEvent.GMUpdate, data.action, data.uuid, data.update);
break;
case socketEvent.DhpFearUpdate:
Hooks.callAll(socketEvent.DhpFearUpdate);
break;
}
}
export const socketEvent = {
GMUpdate: "DhpGMUpdate",
DhpFearUpdate: "DhpFearUpdate",
};
export const GMUpdateEvent = {
UpdateDocument: "DhpGMUpdateDocument",
UpdateFear: "DhpUpdateFear"
};

82
module/helpers/utils.mjs Normal file
View file

@ -0,0 +1,82 @@
export const loadCompendiumOptions = async (compendiums) => {
const compendiumValues = [];
for(var compendium of compendiums){
const values = await getCompendiumOptions(compendium);
compendiumValues.push(values);
}
return compendiumValues;
};
const getCompendiumOptions = async (compendium) => {
const compendiumPack = await game.packs.get(compendium);
const values = [];
for(var value of compendiumPack.index){
const document = await compendiumPack.getDocument(value._id);
values.push(document);
}
return values;
};
export const getWidthOfText = (txt, fontsize, allCaps, bold) => {
// if(getWidthOfText.e === undefined){
// getWidthOfText.e = document.createElement('span');
// getWidthOfText.e.style.display = "none";
// document.body.appendChild(getWidthOfText.e);
// }
// if(getWidthOfText.e.style.fontSize !== fontsize)
// getWidthOfText.e.style.fontSize = fontsize;
// if(getWidthOfText.e.style.fontFamily !== 'Signika, sans-serif')
// getWidthOfText.e.style.fontFamily = 'Signika, sans-serif';
// getWidthOfText.e.innerText = txt;
// return getWidthOfText.e.offsetWidth;
const text = allCaps ? txt.toUpperCase() : txt;
if(getWidthOfText.c === undefined){
getWidthOfText.c=document.createElement('canvas');
getWidthOfText.ctx=getWidthOfText.c.getContext('2d');
}
var fontspec = `${bold ? 'bold': ''} ${fontsize}px` + ' ' + 'Signika, sans-serif';
if(getWidthOfText.ctx.font !== fontspec)
getWidthOfText.ctx.font = fontspec;
return getWidthOfText.ctx.measureText(text).width;
}
export const padArray = (arr, len, fill) => {
return arr.concat(Array(len).fill(fill)).slice(0,len);
}
export const getTier = (level, asNr) => {
switch(Math.floor((level+1)/3)){
case 1:
return asNr ? 1 : 'tier1';
case 2:
return asNr ? 2 : 'tier2';
case 3:
return asNr ? 3 : 'tier3';
default:
return asNr ? 0 : 'tier0';
}
}
export const capitalize = (string) => {
return string.charAt(0).toUpperCase() + string.slice(1);
}
export const getPathValue = (path, entity, numeric) => {
const pathValue = foundry.utils.getProperty(entity, path);
if(pathValue) return numeric ? Number.parseInt(pathValue) : pathValue;
return numeric ? Number.parseInt(path) : path;
};
export const generateId = (title, length) => {
const id = title.split(" ").map((w, i) => {
const p = w.slugify({replacement: "", strict: true});
return i ? p.titleCase() : p;
}).join("");
return Number.isNumeric(length) ? id.slice(0, length).padEnd(length, "0") : id;
}

103
module/ui/chatLog.mjs Normal file
View file

@ -0,0 +1,103 @@
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
constructor(){
super();
this.targetTemplate = {
activeLayer: undefined,
document: undefined,
object: undefined,
minimizedSheets: [],
config: undefined,
targets: undefined
}
this.setupHooks();
}
addChatListeners = async (app, html, data) => {
html.querySelectorAll('.roll-damage-button').forEach(element => element.addEventListener('click', event => this.onRollDamage(event, data.message)));
html.querySelectorAll('.target-container').forEach(element => element.addEventListener('hover', hover(this.hoverTarget, this.unhoverTarget))); // ????
// html.find('.target-container').mouseout(this.unhoverTarget);
html.querySelectorAll('.damage-button').forEach(element => element.addEventListener('click', this.onDamage));
html.querySelectorAll('.healing-button').forEach(element => element.addEventListener('click', this.onHealing));
html.querySelectorAll('.target-indicator').forEach(element => element.addEventListener('click', this.onToggleTargets));
html.querySelectorAll('.advantage').forEach(element => element.hover(this.hoverAdvantage)); // ??
html.querySelectorAll('.advantage').forEach(element => element.addEventListener('click', event => this.selectAdvantage.bind(this)(event, data.message)));
html.querySelectorAll('.ability-use-button').forEach(element => element.addEventListener('click', this.abilityUseButton.bind(this)(event, data.message)));
}
setupHooks(){
Hooks.on('renderChatMessageHTML', this.addChatListeners.bind());
}
close(options){
Hooks.off('renderChatMessageHTML', this.addChatListeners);
super.close(options);
}
onRollDamage = async (event, message) => {
event.stopPropagation();
await game.user.character.damageRoll(message.system.damage, event.shiftKey);
};
hoverTarget = (event) => {
event.stopPropagation();
const token = canvas.tokens.get(event.currentTarget.dataset.token);
if ( !token.controlled ) token._onHoverIn(event, {hoverOutOthers: true});
}
unhoverTarget = (event) => {
const token = canvas.tokens.get(event.currentTarget.dataset.token);
if ( !token.controlled ) token._onHoverOut(event);
};
onDamage = async (event) => {
event.stopPropagation();
const damage = Number.parseInt(event.currentTarget.dataset.value);
const targets = Array.from(game.user.targets);
if(targets.length === 0) ui.notifications.info(game.i18n.localize("DAGGERHEART.Notification.Info.NoTargetsSelected"));
for(var target of targets){
await target.actor.takeDamage(damage, event.currentTarget.dataset.type);
}
};
onHealing = async (event) => {
event.stopPropagation();
const healing = Number.parseInt(event.currentTarget.dataset.value);
const targets = Array.from(game.user.targets);
if(targets.length === 0) ui.notifications.info(game.i18n.localize("DAGGERHEART.Notification.Info.NoTargetsSelected"));
for(var target of targets){
await target.actor.takeHealing(healing, event.currentTarget.dataset.type);
}
}
onToggleTargets = async (event) => {
event.stopPropagation();
$($(event.currentTarget).parent()).find('.target-container').toggleClass('hidden');
};
hoverAdvantage = (event) => {
$(event.currentTarget).siblings('.advantage').toggleClass('unused');
};
selectAdvantage = async (event, message) => {
event.stopPropagation();
const updateMessage = game.messages.get(message._id);
await updateMessage.update({ system: { advantageSelected: event.currentTarget.id === 'hope' ? 1 : 2 }});
$(event.currentTarget).siblings('.advantage').off('click');
$(event.currentTarget).off('click');
}
abilityUseButton = async (event, message) => {
event.stopPropagation();
const action = message.system.actions[Number.parseInt(event.currentTarget.dataset.index)];
await game.user.character.useAction(action);
}
}

200
module/ui/combatTracker.mjs Normal file
View file

@ -0,0 +1,200 @@
import { GMUpdateEvent, socketEvent } from "../helpers/socket.mjs";
export default class DhpCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
constructor(data, context) {
super(data, context);
Hooks.on(socketEvent.DhpFearUpdate, this.onFearUpdate);
}
get template(){
return 'systems/daggerheart/templates/ui/combatTracker.hbs';
}
activateListeners(html) {
super.activateListeners(html);
html.on("click", ".token-action-tokens .use-action-token", this.useActionToken.bind(this));
html.on("click", ".encounter-gm-resources .trade-actions", this.tradeActions.bind(this));
html.on("click", ".encounter-gm-resources .trade-fear", this.tradeFear.bind(this));
html.on("click", ".encounter-gm-resources .icon-button.up", this.increaseResource.bind(this));
html.on("click", ".encounter-gm-resources .icon-button.down", this.decreaseResource.bind(this));
}
async useActionToken(event){
event.stopPropagation();
const combatant = event.currentTarget.dataset.combatant;
await game.combat.useActionToken(combatant);
}
async tradeActions(event){
if(event.currentTarget.classList.contains('disabled')) return;
const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear);
const value = currentFear+1;
if(value <= 6){
Hooks.callAll(socketEvent.GMUpdate,GMUpdateEvent.UpdateFear, null, value);
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: { action: GMUpdateEvent.UpdateFear, update: value },
});
await game.combat.update({ "system.actions": game.combat.system.actions-2 });
}
}
async tradeFear(){
if(event.currentTarget.classList.contains('disabled')) return;
const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear);
const value = currentFear-1;
if(value >= 0){
Hooks.callAll(socketEvent.GMUpdate,GMUpdateEvent.UpdateFear, null, value);
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: { action: GMUpdateEvent.UpdateFear, update: value },
});
await game.combat.update({ "system.actions": game.combat.system.actions+2 });
}
}
async increaseResource(event) {
if(event.currentTarget.dataset.type === 'action'){
await game.combat.update({ "system.actions": game.combat.system.actions+1 });
}
const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear);
const value = currentFear+1;
if(event.currentTarget.dataset.type === 'fear' && value <= 6){
Hooks.callAll(socketEvent.GMUpdate,GMUpdateEvent.UpdateFear, null, value);
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: { action: GMUpdateEvent.UpdateFear, update: value },
});
}
this.render();
}
async decreaseResource(event) {
if(event.currentTarget.dataset.type === 'action' && game.combat.system.actions-1 >= 0){
await game.combat.update({ "system.actions": game.combat.system.actions-1 });
}
const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear);
const value = currentFear-1;
if(event.currentTarget.dataset.type === 'fear' && value >= 0){
Hooks.callAll(socketEvent.GMUpdate,GMUpdateEvent.UpdateFear, null, value);
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: { action: GMUpdateEvent.UpdateFear, update: value },
});
}
this.render();
}
async getData(options={}) {
let context = await super.getData(options);
// Get the combat encounters possible for the viewed Scene
const combat = this.viewed;
const hasCombat = combat !== null;
const combats = this.combats;
const currentIdx = combats.findIndex(c => c === combat);
const previousId = currentIdx > 0 ? combats[currentIdx-1].id : null;
const nextId = currentIdx < combats.length - 1 ? combats[currentIdx+1].id : null;
const settings = game.settings.get("core", Combat.CONFIG_SETTING);
// Prepare rendering data
context = foundry.utils.mergeObject(context, {
combats: combats,
currentIndex: currentIdx + 1,
combatCount: combats.length,
hasCombat: hasCombat,
combat,
turns: [],
previousId,
nextId,
started: this.started,
control: false,
settings,
linked: combat?.scene !== null,
labels: {}
});
context.labels.scope = game.i18n.localize(`COMBAT.${context.linked ? "Linked" : "Unlinked"}`);
if ( !hasCombat ) return context;
// Format information about each combatant in the encounter
let hasDecimals = false;
const turns = [];
for ( let [i, combatant] of combat.turns.entries() ) {
if ( !combatant.visible ) continue;
// Prepare turn data
const resource = combatant.permission >= CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER ? combatant.resource : null;
const turn = {
id: combatant.id,
name: combatant.name,
img: await this._getCombatantThumbnail(combatant),
active: combatant.id === combat.system.activeCombatant,
owner: combatant.isOwner,
defeated: combatant.isDefeated,
hidden: combatant.hidden,
initiative: combatant.initiative,
hasRolled: combatant.initiative !== null,
hasResource: resource !== null,
resource: resource,
canPing: (combatant.sceneId === canvas.scene?.id) && game.user.hasPermission("PING_CANVAS"),
playerCharacter: game.user?.character?.id === combatant.actor.id,
ownedByPlayer: combatant.hasPlayerOwner,
};
if ( (turn.initiative !== null) && !Number.isInteger(turn.initiative) ) hasDecimals = true;
turn.css = [
turn.active ? "active" : "",
turn.hidden ? "hidden" : "",
turn.defeated ? "defeated" : ""
].join(" ").trim();
// Actor and Token status effects
turn.effects = new Set();
if ( combatant.token ) {
combatant.token.effects.forEach(e => turn.effects.add(e));
if ( combatant.token.overlayEffect ) turn.effects.add(combatant.token.overlayEffect);
}
if ( combatant.actor ) {
for ( const effect of combatant.actor.temporaryEffects ) {
if ( effect.statuses.has(CONFIG.specialStatusEffects.DEFEATED) ) turn.defeated = true;
else if ( effect.icon ) turn.effects.add(effect.icon);
}
}
turns.push(turn);
}
// Format initiative numeric precision
const precision = CONFIG.Combat.initiative.decimals;
turns.forEach(t => {
if ( t.initiative !== null ) t.initiative = t.initiative.toFixed(hasDecimals ? precision : 0);
});
const fear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear);
// Merge update data for rendering
return foundry.utils.mergeObject(context, {
round: combat.round,
turn: combat.turn,
turns: turns,
control: combat.combatant?.players?.includes(game.user),
fear: fear,
});
}
onFearUpdate = async () => {
this.render(true);
}
async close(options){
Hooks.off(socketEvent.DhpFearUpdate, this.onFearUpdate);
return super.close(options);
}
}

53
module/ui/players.mjs Normal file
View file

@ -0,0 +1,53 @@
import { GMUpdateEvent, socketEvent } from "../helpers/socket.mjs";
export default class DhpPlayers extends foundry.applications.ui.Players {
constructor(data, context) {
super(data, context);
Hooks.on(socketEvent.DhpFearUpdate, this.onFearUpdate);
}
get template(){
return 'systems/daggerheart/templates/ui/players.hbs';
}
async getData(options={}) {
const context = super.getData(options);
context.fear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear);
context.user = game.user;
return context;
}
activateListeners(html) {
// Toggle online/offline
html.find(".players-mode").click(this._onToggleOfflinePlayers.bind(this));
html.find(".fear-control.up").click(async event => await this.updateFear(event, 1));
html.find(".fear-control.down").click(async event => await this.updateFear(event, -1));
// Context menu
const contextOptions = this._getUserContextOptions();
Hooks.call("getUserContextOptions", html, contextOptions);
new ContextMenu(html, ".player", contextOptions);
}
async updateFear(_, change){
const fear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear);
const value = Math.max(Math.min(fear+change, 6), 0);
Hooks.callAll(socketEvent.GMUpdate,GMUpdateEvent.UpdateFear, null, value);
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: { action: GMUpdateEvent.UpdateFear, update: value },
});
}
onFearUpdate = async () => {
this.render(true);
}
async close(options){
Hooks.off(socketEvent.DhpFearUpdate, this.onFearUpdate);
return super.close(options);
}
}

29
module/ui/ruler.mjs Normal file
View file

@ -0,0 +1,29 @@
export default class DhpRuler extends foundry.canvas.interaction.Ruler {
_getSegmentLabel(segment, totalDistance) {
const range = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.RangeMeasurement);
if(!range.enabled) return super._getSegmentLabel(segment, totalDistance);
const segmentDistance = Math.round(segment.distance * 100) / 100;
const totalDistanceValue = Math.round(totalDistance * 100) / 100;
return `${this.#getRangeLabel(segmentDistance, range)} [${this.#getRangeLabel(totalDistanceValue, range)}]`;
}
#getRangeLabel(distance, settings){
if(distance <= settings.melee){
return game.i18n.localize("DAGGERHEART.Range.Melee.Name");
}
if(distance <= settings.veryClose){
return game.i18n.localize("DAGGERHEART.Range.VeryClose.Name");
}
if(distance <= settings.close){
return game.i18n.localize("DAGGERHEART.Range.Close.Name");
}
if(distance <= settings.far){
return game.i18n.localize("DAGGERHEART.Range.Far.Name");
}
if(distance <= settings.veryFar){
return game.i18n.localize("DAGGERHEART.Range.VeryFar.Name");
}
}
}

6291
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

22
package.json Normal file
View file

@ -0,0 +1,22 @@
{
"dependencies": {
"@yaireo/tagify": "^4.17.9",
"gulp": "^4.0.2",
"gulp-less": "^5.0.0",
"rollup": "^4.40.0"
},
"scripts": {
"start": "concurrently \"rollup -c --watch\" \"node C:/FoundryDev/resources/app/main.js --dataPath=C:/FoundryDevFiles --noupnp\" \"gulp\"",
"start-test": "node C:/FoundryDev/resources/app/main.js --dataPath=C:/FoundryDevFiles && rollup -c --watch && gulp",
"pushLDBtoYML": "node ./tools/pushLDBtoYML.mjs",
"pullYMLtoLDB": "node ./tools/pullYMLtoLDB.mjs"
},
"devDependencies": {
"@foundryvtt/foundryvtt-cli": "^1.0.2",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"postcss": "^8.4.32",
"rollup-plugin-postcss": "^4.0.2",
"concurrently": "^8.2.2"
}
}

6
postcss.config.js Normal file
View file

@ -0,0 +1,6 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: []
}
module.exports = config

29
rollup.config.mjs Normal file
View file

@ -0,0 +1,29 @@
import postcss from 'rollup-plugin-postcss';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
export default {
input: 'daggerheart.mjs',
output: {
file: 'build/daggerheart.js',
format: 'cjs',
sourcemap: true,
},
plugins: [
postcss({
config: {
path: './postcss.config.js'
},
use: {
less: { javascriptEnabled: true }
},
extensions: ['.less'],
extract: false
}),
commonjs({
include: /node_modules/,
requireReturnsDefault: 'auto',
}),
resolve()
],
}

View file

@ -0,0 +1,70 @@
{
"name": "Test Class",
"type": "class",
"img": "icons/svg/item-bag.svg",
"system": {
"domains": [],
"classItems": [],
"damageThresholds": {
"minor": 0,
"major": 0,
"severe": 0
},
"evasion": 0,
"features": [],
"subclasses": [],
"inventory": {
"take": [],
"choiceA": [],
"choiceB": [],
"extra": null
},
"characterGuide": {
"suggestedTraits": {
"agility": 0,
"strength": 0,
"finesse": 0,
"instinct": 0,
"presence": 0,
"knowledge": 0
},
"suggestedPrimaryWeapon": null,
"suggestedSecondaryWeapon": null,
"suggestedArmor": null,
"characterDescription": {},
"backgroundQuestions": [
"",
"",
""
],
"connections": [
"",
"",
""
]
},
"multiclass": null,
"description": ""
},
"effects": [],
"folder": null,
"ownership": {
"default": 0,
"HeB5VwikUgL2Hxck": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.342",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1747924765406,
"modifiedTime": 1747924765406,
"lastModifiedBy": "HeB5VwikUgL2Hxck"
},
"_id": "h9wTtM4iczXHqvf8",
"sort": 0,
"_key": "!items!h9wTtM4iczXHqvf8"
}

562
styles/application.less Normal file
View file

@ -0,0 +1,562 @@
form.daggerheart.views.downtime { // Shouldn't be needed, but DEFAULT_OPTIONS doesn't accept Height: 'auto'
height: auto !important;
}
div.daggerheart.views.death-move { // Shouldn't be needed, but DEFAULT_OPTIONS doesn't accept Height: 'auto'
height: auto !important;
}
div.daggerheart.views.multiclass { // Shouldn't be needed, but DEFAULT_OPTIONS doesn't accept Height: 'auto'
height: auto !important;
}
.daggerheart.views {
&.levelup {
.levelup-title-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 32px;
margin-bottom: 4px;
.level-title {
text-decoration: underline;
}
.level-display {
display: flex;
align-items: center;
i {
margin: 0 @halfMargin;
}
}
}
.levelup-section {
display: flex;
align-items: flex-start;
margin-bottom: 8px;
font-size: 11px;
.levelup-container {
flex: 1;
&:nth-of-type(2) {
padding: 0 4px;
}
&.disabled {
opacity: 0.2;
}
.levelup-inner-container {
height: 700px;
padding: 24px 58px 0;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.levelup-legend {
margin-left: auto;
margin-right: auto;
font-weight: bold;
z-index: 1;
}
.levelup-info {
background: @primaryAccent;
width: 100%;
text-align: center;
position: absolute;
top: -6px;
padding: 8px 0;
}
.levelup-pretext {
padding: 8px 0;
}
.levelup-body {
display: flex;
flex-direction: column;
.levelup-choice-row {
display: flex;
align-items: center;
padding: 4px;
.levelup-choice-row-inner {
display: flex;
align-items: center;
}
.levelup-choice-input-container {
position: relative;
display: flex;
align-items: center;
input {
&:disabled:checked::before {
opacity: 0.4;
color: var(--color-warm-1);
}
}
i.fa-link {
transform: rotate(45deg);
position: relative;
top: 2px;
margin: 0 -3px;
}
i.fa-lock {
position: absolute;
top: 0;
left: 0;
font-size: 8px;
}
}
}
}
.levelup-posttext {
padding: 8px 0;
}
}
}
}
}
.downtime-container {
.activity-container {
display: flex;
align-items: center;
padding: 8px;
.activity-title {
flex: 1;
display: flex;
align-items: center;
.activity-title-text {
font-size: 24px;
font-weight: bold;
}
.activity-image {
width: 120px;
border: 2px solid black;
border-radius: 50%;
margin-right: 8px;
cursor: pointer;
&:hover, &.selected {
filter: drop-shadow(0 0 6px gold);
}
}
.custom-name-input {
font-size: 24px;
font-weight: bold;
padding: 0;
background: transparent;
color: rgb(239, 230, 216);
}
}
.activity-body {
flex: 1;
font-style: italic;
}
}
}
&.downtime {
.activity-text-area {
resize: none;
}
}
.range-reset {
flex: 0;
width: 21px;
height: 21px;
margin: 3px 4px;
border: 1px solid black;
display: flex;
align-items: center;
justify-content: center;
}
&.roll-selection {
.roll-selection-container {
i {
filter: invert(0%) sepia(100%) saturate(0%) hue-rotate(21deg) brightness(17%) contrast(103%);
}
}
.roll-dialog-container {
.disadvantage, .advantage {
border: 2px solid @secondaryAccent;
border-radius: 6px;
display: flex;
align-items: center;
padding: 4px;
margin-bottom: 6px;
&.selected {
filter: drop-shadow(0px 0px 3px @mainShadow);
}
input {
border: 0;
}
button {
flex: 0;
border-radius: 50%;
height: 20px;
width: 20px;
display: flex;
align-items: center;
justify-content: center;
margin: 2px 0 2px 4px;
padding: 12px;
i {
margin: 0;
}
}
}
.roll-dialog-experience-container {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: @halfMargin;
.roll-dialog-chip {
border: @thinBorder solid black;
border-radius: 6px;
min-width: calc(33% - 2px);
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: @halfMargin;
cursor: pointer;
padding: @fullPadding;
background: grey;
overflow: hidden;
font-weight: bold;
&.hover {
filter: drop-shadow(0 0 3px @mainShadow);
}
span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&.selected {
background: green;
span {
filter: drop-shadow(0 0 3px @secondaryShadow);
}
}
}
}
.hope-container {
display: flex;
gap: @fullMargin;
align-items: center;
font-size: 18px;
}
}
}
&.npc-roll-selection {
.npc-roll-dialog-container {
display: flex;
flex-direction: column;
.selection-container {
display: flex;
align-items: center;
margin-bottom: @fullMargin;
.dice-container {
display: flex;
align-items: center;
flex: 1;
.dice-inner-container{
position: relative;
display: flex;
align-items: center;
i {
font-size: 18px;
}
img {
border: 0;
position: relative;
left: 1px;
}
.dice-number {
position: absolute;
top: calc(50% - 14px);
left: calc(50% - 7px);
font-size: 24px;
font-weight: bold;
}
}
}
}
.roll-dialog-experience-container {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
gap: @halfMargin;
flex: 2;
height: 100%;
.roll-dialog-chip {
border: @thinBorder solid black;
border-radius: 6px;
flex-basis: calc(50% - 2px);
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
padding: @fullPadding;
background: grey;
overflow: hidden;
&.hover {
filter: drop-shadow(0 0 3px @mainShadow);
}
span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&.selected i {
color: green;
}
}
}
}
}
&.multiclass {
.multiclass-container {
margin-bottom: @largeMargin;
.multiclass-category-title {
margin-top: @largeMargin;
}
.multiclass-class-choices {
display: flex;
width: 100%;
height: 100%;
flex-wrap: wrap;
}
.multiclass-spaced-choices {
display: flex;
justify-content: space-around;
width: 100%;
height: 100%;
}
.multiclass-class-choice {
display: flex;
align-items: center;
flex-basis: 33.33%;
font-weight: bold;
font-size: 24px;
cursor: pointer;
&.selected:not(.disabled), &:hover:not(.disabled) {
filter: drop-shadow(0 0 3px gold);
}
&.inactive, &.disabled {
cursor: initial;
opacity: 0.4;
}
img {
width: 80px;
height: 80px;
margin-right: @largeMargin;
}
}
}
}
&.damage-selection {
.hope-container {
display: flex;
gap: @fullMargin;
align-items: center;
font-size: 18px;
}
}
&.action {
.action-category {
display: flex;
flex-direction: column;
.action-category-label {
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 6px;
cursor: pointer;
padding: 0 @fullPadding;
margin: 0 auto @halfMargin;
&:hover {
background-color: darkgray;
}
}
.action-category-data {
max-height: 0;
transition: max-height 0.2s ease-in-out;
overflow: hidden;
&.open {
max-height: initial;
}
}
}
}
&.ancestry-selection {
.ancestry-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: @fullMargin;
.ancestry-container {
width: 100%;
display: flex;
flex-wrap: wrap;
.ancestry-inner-container {
flex-basis: 25%;
display: flex;
flex-direction: column;
align-items: center;
.image-container {
img {
width: 120px;
border: 4px solid black;
border-radius: 50%;
&.selected {
border-color: @secondaryShadow;
}
&:hover:not(.selected) {
filter: drop-shadow(0 0 3px @secondaryShadow);
}
&.disabled {
opacity: 0.3;
}
}
}
.name-container {
div {
font-size: 18px;
font-weight: bold;
cursor: help;
}
}
}
}
.mixed-ancestry-container {
width: 100%;
display: flex;
gap: @fullMargin;
> div {
flex: 1;
}
.mixed-ancestry-name {
text-align: center;
div {
font-size: 24px;
}
}
.mixed-ancestry-images {
display: flex;
align-items: center;
gap: @halfMargin;
.mixed-ancestry-image {
position: relative;
max-width: 33%;
&:hover i {
opacity: 1;
}
i {
position: absolute;
font-size: 32px;
top: calc(50% - 20px);
left: calc(50% - 20px);
padding: @fullPadding;
background-color: grey;
opacity: 0;
cursor: pointer;
&:hover {
filter: drop-shadow(0 0 3px @secondaryShadow);
}
}
img {
max-width: 100%;
}
}
img {
max-width: 33%;
border: 4px solid black;
border-radius: 50%;
&.selected {
border-color: @secondaryShadow;
}
}
}
}
}
}
}

212
styles/chat.less Normal file
View file

@ -0,0 +1,212 @@
.daggerheart.chat {
&.downtime {
display: flex;
flex-direction: column;
align-items: center;
.downtime-title-container {
display: flex;
flex-direction: column;
align-items: center;
.downtime-subtitle {
font-size: 17px;
}
}
.downtime-image {
width: 80px;
}
.downtime-refresh-container {
margin-top: @fullMargin;
width: 100%;
.refresh-title {
font-weight: bold;
}
}
}
&.roll {
.dice-tooltip {
.dice-rolls {
display: flex;
align-items: center;
justify-content: space-around;
.dice-hope-container {
display: flex;
.roll.die:not(:last-of-type) {
margin-right: @fullMargin;
}
}
.modifiers-container {
display: flex;
.modifier-value:not(:last-of-type) {
margin-right: @fullMargin;
}
}
.roll.die {
&.hope {
color: white;
-webkit-text-stroke-color: @hope;
-webkit-text-stroke-width: 1.5px;
font-weight: 400;
}
&.fear {
color: white;
-webkit-text-stroke-color: @fear;
-webkit-text-stroke-width: 1.5px;
font-weight: 400;
}
&.disadvantage {
color: white;
-webkit-text-stroke-color: @disadvantage;
-webkit-text-stroke-width: 1.5px;
font-weight: 400;
}
&.advantage {
color: white;
-webkit-text-stroke-color: @advantage;
-webkit-text-stroke-width: 1.5px;
font-weight: 400;
}
//V1.3
// &.advantage {
// filter: drop-shadow(0 -4px 4px gold);
// cursor: pointer;
// }
&.unused {
opacity: 0.3;
}
}
.modifier-value {
text-align: center;
font-weight: bold;
font-size: 16px;
}
}
}
.dice-total {
.dice-total-value {
.hope {
color: @hope;
}
.fear {
color: @fear;
}
.critical {
color: @critical;
}
}
}
.dice-total-label {
font-size: 12px;
font-weight: bold;
font-variant: all-small-caps;
margin: -@fullMargin 0;
}
.target-section {
margin-top: 5px;
.target-container {
display: flex;
transition: all 0.2s ease-in-out;
&:hover {
filter: drop-shadow(0 0 3px @secondaryShadow);
border-color: gold;
}
&.hidden {
display: none;
border: 0;
}
&.hit {
background: @hit;
}
&.miss {
background: @miss;
}
img {
flex: 0;
width: 22px;
height: 22px;
margin-left: 8px;
align-self: center;
border-color: transparent;
}
.target-inner-container {
flex: 1;
display: flex;
justify-content: center;
margin-right: @hugeMargin;
}
}
}
.roll-damage-button {
margin-top: 5px;
}
}
&.domain-card {
display: flex;
flex-direction: column;
align-items: center;
.domain-card-title {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
div {
font-size: 20px;
font-variant: small-caps;
font-weight: bold;
}
h2 {
width: 100%;
text-align: center;
}
}
.ability-card-footer {
display: flex;
width: 100%;
margin-top: @fullMargin;
flex-wrap: wrap;
button {
border-radius: 6px;
background: @positive;
border-color: black;
flex-basis: calc(50% - 2px);
&:nth-of-type(n+3){
margin-top: @tinyMargin;
}
}
}
img {
width: 80px;
}
}
}

5
styles/class.less Normal file
View file

@ -0,0 +1,5 @@
.daggerheart.sheet.class {
.editor {
height: 500px;
}
}

45
styles/components.less Normal file
View file

@ -0,0 +1,45 @@
.slider-container {
position: relative;
background: lightslategray;
.slider-inner-container {
position: absolute;
top: 1px;
left: -60px;
background-color: inherit;
color: inherit;
border-radius: 30px;
cursor: pointer;
display: flex;
align-items: center;
height: 20px;
width: 40px;
padding: 0 4px;
border: @thinBorder solid black;
&:hover {
filter: drop-shadow(0 0 3px red);
}
input:checked {
opacity: 0;
width: 0;
height: 0;
& + .slider-icon {
transform: translateX(17px);
transition: 1s;
}
}
.slider-icon {
position: absolute;
left: 4px;
height: 15px;
width: 15px;
border-radius: 50%;
transition: 1s;
transform: translateX(0);
}
}
}

2613
styles/daggerheart.css Normal file

File diff suppressed because it is too large Load diff

132
styles/daggerheart.less Normal file
View file

@ -0,0 +1,132 @@
@import "./variables/variables.less";
@import "./class.less";
@import "./pc.less";
@import "./ui.less";
@import "./chat.less";
@import "./item.less";
@import "./application.less";
@import "./sheets//sheets.less";
@import "./components.less";
@import "./dialog.less";
@import "../node_modules/@yaireo/tagify/dist/tagify.css";
#logo {
content: url(../assets/DaggerheartLogo.webp);
height: 50px;
width: 50px;
position: relative;
left: 25px;
}
.daggerheart {
.vertical-separator {
border-left: 2px solid black;
height: 56px;
flex: 0;
align-self: center;
}
/* Flex */
.flex-centered {
display: flex;
align-items: center;
justify-content: center;
}
.flex-col-centered {
display: flex;
flex-direction: column;
align-items: center;
}
.flex-spaced {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.flex-min {
display: flex;
flex: 0;
}
/****/
img[data-edit="img"] {
min-width: 64px;
min-height: 64px;
}
.editor {
height: 200px;
}
button {
i {
margin: 0;
}
}
.icon-button {
&.spaced {
margin-left: @halfMargin;
}
&.active {
filter: drop-shadow(0 0 3px @mainShadow);
&.secondary {
filter: drop-shadow(0 0 3px @secondaryShadow);
}
}
&.disabled {
opacity: 0.6;
}
&:hover:not(.disabled){
cursor: pointer;
filter: drop-shadow(0 0 3px @mainShadow);
&.secondary {
filter: drop-shadow(0 0 3px @secondaryShadow);
}
}
}
// .window-content {
// background: crimson;
// > form, >div {
// background: url(../ui/parchment.jpg) repeat;
// }
// }
}
#players {
h3 {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: nowrap;
.players-container {
display: flex;
align-items: center;
}
.fear-control {
font-size: 10px;
&.disabled {
opacity: 0.4;
}
&:hover:not(.disabled) {
cursor: pointer;
filter: drop-shadow(0 0 3px @mainShadow);
}
}
}
}

12
styles/dialog.less Normal file
View file

@ -0,0 +1,12 @@
.item-button {
&.checked {
background: green;
}
.item-icon {
opacity: 0;
transition: opacity 0.2s;
&.checked {
opacity: 1;
}
}
}

60
styles/item.less Normal file
View file

@ -0,0 +1,60 @@
.daggerheart.sheet {
&.feature {
background-color: red;
.editable {
display: flex;
flex-direction: column;
}
.sheet-body {
flex: 1;
display: flex;
flex-direction: column;
}
.feature-description {
flex: 1;
display: flex;
flex-direction: column;
}
}
&.class {
.class-feature {
display: flex;
img {
width: 40px;
}
button {
width: 40px;
}
}
}
.domain-card-description {
.editor {
height: 300px;
}
}
.item-container {
margin-top: @halfMargin;
gap: @halfMargin;
align-items: baseline;
}
.item-sidebar {
border-right: @thinBorder groove darkgray;
min-width: 160px;
flex: 0;
padding: @fullPadding;
label {
margin-right: @fullMargin;
font-weight: bold;
}
input[type="checkbox"] {
margin: 0;
}
}
}

1506
styles/pc.less Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,281 @@
.daggerheart.sheet.adversary {
.adversary-header-container {
position: relative;
background-color: grey;
display: flex;
.adversary-header {
flex: 1;
img {
height: 60px;
width: 60px;
}
.adversary-title {
display: flex;
align-items: center;
text-align: center;
font-size: 28px;
.title-text {
width: 100%;
}
input {
font-size: 28px;
border: 0;
height: 100%;
}
}
}
.adversary-toggle {
position: absolute;
top: 0;
right: 0;
background-color: white;
color: black;
flex: 0;
}
}
.motive-container {
background: lightgrey;
margin-bottom: @fullMargin;
padding-bottom: @fullPadding;
.motive-title {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
.motive-title-base {
font-size: 21px;
}
.motive-title-value {
font-style: italic;
position: relative;
top: 2px;
}
i {
margin-left: 4px;
cursor: pointer;
&:hover {
filter: drop-shadow(0 0 3px red),
}
}
}
}
.adversary-content-container {
display: flex;
align-items: baseline;
}
.adversary-statistics-container {
flex: 1;
margin-right: 24px;
display: flex;
flex-direction: column;
gap: @mediumMargin;
.statistic-title {
flex: 0;
white-space: nowrap;
font-weight: bold;
}
.statistic-row {
display: flex;
align-items: center;
.statistic-value {
flex: 0;
white-space: nowrap;
margin-left: 4px;
}
.adversary-roll {
border: 0;
width: 16px;
margin-left: 4px;
align-self: baseline;
transition: transform 0.2s;
&:hover {
transform: rotate(30deg);
filter: drop-shadow(0px 0px 3px red);
cursor: pointer;
}
}
}
.statistic-resource-container {
display: flex;
align-items: center;
label {
min-width: 44px;
}
.statistic-resource-inner-container {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: @halfMargin;
}
.resource-title {
align-self: center;
font-weight: bold;
}
.statistic-resource-input {
margin: 0;
flex: 0;
min-width: 16px;
}
}
.attack-container {
border: 1px solid black dotted;
}
.experience-row {
display: flex;
* {
flex: 0;
white-space: nowrap;
}
}
.experience-container {
i {
margin-left: 4px;
cursor: pointer;
&:hover {
filter: drop-shadow(0 0 3px red),
}
}
}
.experience-chip {
border: 2px solid @secondaryAccent;
border-radius: 6px;
display: flex;
align-items: center;
padding: 4px;
margin-bottom: 6px;
.experience-text {
flex: 1;
}
.experience-value {
flex: 0;
min-width: @inputSingleMinWidth;
margin: 0 4px;
}
.experience-button {
flex: 0;
border-radius: 50%;
height: 20px;
width: 20px;
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
}
}
}
.adversary-damage-threshold-container {
input {
min-width: @inputSingleMinWidth;
}
}
.adversary-moves-container {
flex: 2.5;
.moves-title {
text-decoration: underline;
font-weight: bold;
}
.move-container {
cursor: pointer;
&:hover {
background: @hoverBackground;
}
.moves-name {
font-weight: bold;
text-decoration: none;;
}
.move-description {
p {
margin-top: 0;
}
}
}
.moves-edit-container {
i {
margin-left: 4px;
cursor: pointer;
&:hover {
filter: drop-shadow(0 0 3px red),
}
}
}
}
.chip-container {
display: flex;
align-items: center;
justify-content: space-between;
background: @primaryAccent;
padding: 8px;
border: 2px solid black;
border-radius: 6px;
&:not(:last-child) {
margin-bottom: 8px;
}
.chip-inner-container {
display: flex;
align-items: center;
img {
height: 40px;
width: 40px;
margin-right: 8px;
}
.chip-title {
font-size: 22px;
font-weight: bold;
font-style: italic;
}
}
button {
height: 40px;
width: 40px;
background: white;
}
}
}

119
styles/sheets/class.less Normal file
View file

@ -0,0 +1,119 @@
.daggerheart.sheet.class {
.guide {
.guide-section {
gap: @fullMargin;
}
.drop-section {
width: 100%;
legend {
margin-left: auto;
margin-right: auto;
font-size: 12px;
}
.drop-section-body {
min-height: 40px;
display: flex;
flex-direction: column;
align-items: center;
}
}
.trait-input {
text-align: center;
min-width: 24px;
}
.suggested-item {
padding: @smallPadding @fullPadding;
border-radius: 6px;
border: @thinBorder solid black;
background: @primaryAccent;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
&:not(:last-child) {
margin: @halfMargin;
}
img {
width: 30px;
}
div {
text-align: center;
}
i {
border-radius: 50%;
margin-right: 4px;
font-size: 11px;
}
}
.extra-section {
display: flex;
flex-direction: column;
align-items: center;
.extra-title {
font-size: 14px;
font-weight: bold;
}
.extra-input {
margin-bottom: @halfMargin;
}
}
}
.guide-section-title-centered {
font-weight: bold;
font-size: 18px;
}
.inventory-section {
width: 100%;
border: 2px solid black;
border-style: dotted;
min-height: 80px;
.inventory-title {
font-weight: bold;
font-size: 14px;
text-align: center;
}
}
.tagify {
background: var(--color-light-1);
border: 1px solid var(--color-border);
height: 34px;
width: 100%;
border-radius: 3px;
margin-right: 1px;
tag {
div {
display: flex;
justify-content: space-between;
align-items: center;
height: 22px;
span {
font-weight: 400;
}
img {
margin-left: 8px;
height: 20px;
width: 20px;
}
}
}
}
}

View file

@ -0,0 +1,6 @@
.daggerheart.sheet.heritage {
.editor {
height: 200px;
}
}

186
styles/sheets/sheets.less Normal file
View file

@ -0,0 +1,186 @@
@import "./heritage.less";
@import "./class.less";
@import "./adversary.less";
.daggerheart.sheet {
.title-container {
display: flex;
gap: @fullMargin;
div {
flex: 1;
align-items: baseline;
}
}
.editor-form-group {
display: flex;
flex-direction: column;
label {
font-weight: bold;
text-align: center;
}
}
.option-select {
position: absolute;
top: calc(50% - 10px);
right: 8px;
height: 20px;
width: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
&.deeper {
right: 32px;
}
&:hover:not(:disabled) {
filter: drop-shadow(0px 0px 3px @mainShadow);
cursor: pointer;
}
i {
margin: 0;
font-size: 11px;
}
}
.ability-title {
width: 100%;
display: flex;
h2 {
flex: 1;
}
i {
cursor: pointer;
&:hover {
filter: drop-shadow(0px 0px 3px @mainShadow);
}
}
}
.ability-choices {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.ability-chip {
border: 2px solid @secondaryAccent;
border-radius: 6px;
display: flex;
align-items: center;
padding: 4px;
margin-bottom: 6px;
flex: calc(33% - 4px);
max-width: calc(33% - 4px);
&.selected {
filter: drop-shadow(0px 0px 3px @mainShadow);
}
&:nth-of-type(3n-1) {
margin-left: 6px;
margin-right: 6px;
}
input {
border: 0;
}
button {
flex: 0;
border-radius: 50%;
height: 20px;
width: 20px;
display: flex;
align-items: center;
justify-content: center;
margin: 2px 0 2px 4px;
padding: 12px;
i {
margin: 0;
}
}
}
.object-select-display {
position: relative;
width: calc(100% - 2px);
background: rgba(0, 0, 0, 0.05);
height: var(--form-field-height);;
display: flex;
border: 1px solid rgb(122, 121, 113);
border-radius: 3px;
.object-select-title {
position: absolute;
left: 4px;
text-align: center;
font-weight: bold;
text-transform: uppercase;
}
.object-select-text {
align-self: center;
}
.object-select-item {
cursor: pointer;
&:hover {
filter: drop-shadow(0px 0px 3px red);
}
}
}
.feature-container {
display: flex;
align-items: center;
justify-content: space-between;
background: @primaryAccent;
padding: 8px;
border: 2px solid black;
border-radius: 6px;
&:not(:last-child) {
margin-bottom: 8px;
}
.feature-inner-container {
display: flex;
align-items: center;
img {
height: 40px;
width: 40px;
margin-right: 8px;
}
.feature-title {
font-size: 22px;
font-weight: bold;
font-style: italic;
}
}
button {
height: 40px;
width: 40px;
background: inherit;
border: 0;
i {
}
}
}
}

72
styles/ui.less Normal file
View file

@ -0,0 +1,72 @@
.combat-sidebar {
.encounter-gm-resources {
flex: 0;
display: flex;
justify-content: center;
padding: @largePadding 0;
.gm-resource-controls {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 4px;
justify-content: center;
}
.gm-resource-tools {
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 5px 0 @fullPadding;
i {
margin: 0 @tinyMargin;
font-size: 16px;
&.disabled {
opacity: 0.6;
}
&:hover:not(.disabled) {
cursor: pointer;
filter: drop-shadow(0 0 3px @mainShadow);
}
}
}
.gm-resource {
background: rgba(255, 255, 255, 0.1);
padding: @fullPadding;
border-radius: 8px;
border: @normalBorder solid black;
font-size: 20px;
}
}
.token-action-tokens {
flex: 0 0 48px;
text-align: center;
.use-action-token {
&.disabled {
opacity: 0.6;
}
}
}
.icon-button {
&.spaced {
margin-left: @halfMargin;
}
&.disabled {
opacity: 0.6;
}
&:hover:not(.disabled){
cursor: pointer;
filter: drop-shadow(0 0 3px @mainShadow);
}
}
}

View file

@ -0,0 +1,24 @@
/* General */
@hope: #008080;
@fear: #430070;
@critical: #ffd700;
@advantage: green;
@disadvantage: #b30000;
@miss: rgb(255, 0, 0);
@hit: rgb(0, 128, 0);
@positive: #699969;
@negative: #ff7f7f;
@borderPrimary: #b5b3a4;
@borderTertiary: #7a7971;
/* Drop Shadows */
@mainShadow: red;
@secondaryShadow: gold;
/* Background */
@secondaryBackground: #7a7971;
@primaryAccent: #778899;
@secondaryAccent: #708090;
@formBackground: #782e22;
@hoverBackground: #2f4f4f40;

View file

@ -0,0 +1,26 @@
/* Base Value */
@distance: 8;
/* Margins */
@tinyMargin: (@distance / 4) * 1px;
@halfMargin: (@distance / 2) * 1px;
@fullMargin: @distance * 1px;
@mediumMargin: @distance * 1.5px; // Specific, but might be good?
@largeMargin: @distance * 2px;
@threeQuarterMargin: @distance * 3px; // Too specific? If not used a lot, possibly remove and just use @distance * 3px
@hugeMargin: @distance * 4px;
@massiveMargin: @distance * 8px;
/* Borders */
@thinBorder: 1px;
@normalBorder: 2px;
@thickBorder: 4px;
@normalRadius: 6px;
/* Padding */
@smallPadding: 2px;
@fullPadding: 4px;
@largePadding: 8px;
/* Inputs */
@inputSingleMinWidth: 26px;

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