Merge remote-tracking branch 'origin/main' into foundry/v12

This commit is contained in:
Oliver-Akins 2024-04-27 00:38:22 -06:00
commit 76cca3f672
17 changed files with 223 additions and 81 deletions

2
.gitignore vendored
View file

@ -6,7 +6,7 @@ references/
/.*/
!/.vscode/
!/.github/
/foundry.js
/*.ref.*
*.lock
*.zip

View file

@ -81,9 +81,11 @@
"send-to-chat": "Send to Chat",
"edit": "Edit",
"delete": "Delete",
"reset": "Reset",
"empty": "---",
"help": "Help",
"gm": "Server"
"gm": "Server",
"view-larger": "View Larger"
},
"sheet-names": {
"*DataSheet": "Data Sheet"
@ -93,6 +95,12 @@
"title": "What is Calculated Capacity?",
"content": "<p>The calculated capacity is how much space in your inventory that the item will take up, the way it is calculated is determined by the item. Usually the main thing that affects the capacity is the item's quantity, but this can be turned off by the @dotdungeon.common.gm, which means that no matter the quantity it will only use up one capacity. The @dotdungeon.common.gm can also entirely disable capacity usage which will make the used capacity always be zero.</p>"
}
},
"delete": {
"ActiveEffect": {
"title": "Delete Effect",
"content": "<p>Are you sure you would like to delete the active effect: {name}</p>"
}
}
},
"TYPES": {
@ -113,6 +121,9 @@
"legendaryItem": "Legendary Item",
"spell": "Spell",
"untyped": "Custom"
},
"ActiveEffect": {
"base": "Effect"
}
}
}

View file

@ -0,0 +1,7 @@
export class DotDungeonActiveEffect extends ActiveEffect {
// Invert the logic of the disabled property so it's easier to modify via
// embedded controls
get enabled() { return !this.disabled };
set enabled(newValue) { this.disabled = !newValue };
};

View file

@ -0,0 +1,42 @@
import { DotDungeonActiveEffect } from "./GenericActiveEffect.mjs";
const classes = {};
const defaultClass = DotDungeonActiveEffect;
export const ActiveEffectProxy = new Proxy(function () {}, {
construct(target, args) {
const [data] = args;
if (!classes.hasOwnProperty(data.type)) {
return new defaultClass(...args);
}
return new classes[data.type](...args);
},
get(target, prop, receiver) {
if (["create", "createDocuments"].includes(prop)) {
return function (data, options) {
if (data.constructor === Array) {
return data.map(i => ActiveEffectProxy.create(i, options))
}
if (!classes.hasOwnProperty(data.type)) {
return defaultClass.create(data, options);
}
return classes[data.type].create(data, options);
};
};
if (prop == Symbol.hasInstance) {
return function (instance) {
if (instance instanceof defaultClass) return true;
return Object.values(classes).some(i => instance instanceof i);
};
};
return defaultClass[prop];
},
});

View file

@ -1,4 +1,15 @@
export class DotDungeonActor extends Actor {
/*
Using this to take a "snapshot" of the system data prior to applying AE's so
that the inputs can still have the non-modified value in them, while we still
provide all that data to AE's without needing to disable any inputs.
*/
prepareEmbeddedDocuments() {
this.preAE = foundry.utils.deepClone(this.system);
super.prepareEmbeddedDocuments();
};
async createEmbeddedItem(defaults, opts = {}) {
let items = await this.createEmbeddedDocuments(`Item`, defaults);
if (!Array.isArray(items)) items = items ? [items] : [];

View file

@ -1,8 +1,22 @@
import { DotDungeonActor } from "./GenericActor.mjs";
import { DotDungeonItem } from "../Item/GenericItem.mjs";
export class Player extends DotDungeonActor {
applyActiveEffects() {
super.applyActiveEffects();
/*
These are the (groups of) fields that ActiveEffects may modify safely and
remain editable in the sheet. This needs to be done because of default
Foundry behaviour that otherwise prevents these fields from being edited.
The deletes must use optional chaining otherwise they can cause issues
during the document preparation lifecycle as an actor with no AE's affecting
anything in one of these areas will result in these paths being undefined.
*/
delete this.overrides.system?.stats;
delete this.overrides.system?.skills;
};
async createCustomPet() {
const body = new URLSearchParams({
number: 1,

View file

@ -5,4 +5,11 @@ export class Material extends DotDungeonItem {
let affects = game.settings.get(`dotdungeon`, `materialsAffectCapacity`);
return affects ? super.usedCapacity : 0;
};
get availableLocations() {
return [
{ value: null, label: `dotdungeon.location.unknown` },
{ value: `inventory`, label: `dotdungeon.location.inventory` },
];
};
};

View file

@ -10,6 +10,7 @@ import { SyncData } from "./models/Actor/Sync.mjs";
import { MobData } from "./models/Actor/Mob.mjs";
// Main Documents
import { ActiveEffectProxy } from "./documents/ActiveEffect/_proxy.mjs";
import { ActorProxy } from "./documents/Actor/_proxy.mjs";
import { ItemProxy } from "./documents/Item/_proxy.mjs";
@ -56,6 +57,7 @@ Hooks.once(`init`, async () => {
CONFIG.Item.dataModels.pet = PetItemData;
CONFIG.Actor.documentClass = ActorProxy;
CONFIG.Item.documentClass = ItemProxy;
CONFIG.ActiveEffect.documentClass = ActiveEffectProxy;
CONFIG.DOTDUNGEON = DOTDUNGEON;
@ -111,9 +113,6 @@ Hooks.once(`init`, async () => {
hbs.registerHandlebarsHelpers();
hbs.preloadHandlebarsTemplates();
registerCustomComponents();
CONFIG.CACHE ??= {};
CONFIG.CACHE.icons = await hbs.preloadIcons();
});

View file

@ -43,25 +43,6 @@ export const preAliasedPartials = {
"dotdungeon.pc.v2.foil": "actors/char-sheet/v2/partials/inventory/items/untyped.v2.pc.hbs",
};
export const icons = [
`caret-right.svg`,
`caret-down.svg`,
`garbage-bin.svg`,
`chat-bubble.svg`,
`dice/d4.svg`,
`dice/d6.svg`,
`dice/d8.svg`,
`dice/d10.svg`,
`dice/d12.svg`,
`dice/d20.svg`,
`create.svg`,
`close.svg`,
`edit.svg`,
`sheet.svg`,
`minus.svg`,
];
export async function registerHandlebarsHelpers() {
Handlebars.registerHelper(helpers);
};
@ -97,38 +78,3 @@ export async function preloadHandlebarsTemplates() {
console.groupEnd();
return loadTemplates(paths);
};
/**
* Loads all of the icons that are needed in the handlebars templating to make
* the sheet look nicer.
*
* @returns An object containing icon names to the corresponding HTML data for
* displaying the icon
*/
export async function preloadIcons() {
const pathPrefix = `systems/dotdungeon/assets/`
const parsedIcons = {};
for (const icon of icons) {
const iconName = icon.split(`/`).slice(-1)[0].slice(0, -4);
if (icon.endsWith(`.svg`)) {
try {
const response = await fetchWithTimeout(`${pathPrefix}${icon}`);
if (response.status !== 200) { continue };
const svgData = await response.text();
parsedIcons[iconName] = svgData;
} catch {
console.error(`.dungeon | Failed to fetch/parse icon: ${icon}`);
continue;
};
}
else if (icon.endsWith(`.png`)) {
parsedIcons[iconName] = `<img alt="" src="${pathPrefix}${icon}">`;
}
else {
console.warn(`.dungeon | Icon "${icon}" failed to be handled by a loader`)
};
};
return parsedIcons;
};

View file

@ -39,9 +39,8 @@ export class PlayerSheetv2 extends GenericActorSheet {
html.find(`.create-ae`).on(`click`, async ($e) => {
console.debug(`Creating an ActiveEffect?`);
ActiveEffect.implementation.create({
name: "Default AE",
}, { parent: this.actor, renderSheet: true });
const ae = this.actor.createEmbeddedDocuments(`ActiveEffect`, [{name: "Default AE"}]);
ae.sheet.render(true);
});
html.find(`[data-filter-toggle]`).on(`change`, ($e) => {
const target = $e.delegateTarget;
@ -74,6 +73,7 @@ export class PlayerSheetv2 extends GenericActorSheet {
/** @type {ActorHandler} */
const actor = this.actor;
ctx.preAE = actor.preAE;
ctx.system = actor.system;
ctx.flags = actor.flags;
ctx.items = this.actor.itemTypes;
@ -97,6 +97,7 @@ export class PlayerSheetv2 extends GenericActorSheet {
const stat = {
key: statName,
name: localizer(`dotdungeon.stat.${statName}`),
original: this.actor.preAE.stats[statName],
value: this.actor.system.stats[statName],
};
@ -111,7 +112,7 @@ export class PlayerSheetv2 extends GenericActorSheet {
return {
value: die,
label: localizer(`dotdungeon.die.${die}`, { stat: statName }),
disabled: usedDice.has(die) && this.actor.system.stats[statName] !== die,
disabled: usedDice.has(die) && this.actor.preAE.stats[statName] !== die,
};
})
];
@ -127,8 +128,9 @@ export class PlayerSheetv2 extends GenericActorSheet {
key: skill,
name: game.i18n.format(`dotdungeon.skills.${skill}`),
value,
original: this.actor.preAE.skills[statName][skill],
formula: `1` + stat.value + modifierToString(value, { spaces: true }),
rollDisabled: value === -1,
rollDisabled: this.actor.preAE.skills[statName][skill] === -1,
});
};

View file

@ -40,7 +40,7 @@ export class GenericActorSheet extends ActorSheet {
ctx.actor = this.actor;
ctx.config = DOTDUNGEON;
ctx.icons = CONFIG.CACHE.icons;
ctx.icons = {};
return ctx;
};

View file

@ -30,9 +30,10 @@ export class GenericItemSheet extends ItemSheet {
ctx.item = this.item;
ctx.system = this.item.system;
ctx.flags = this.item.flags;
ctx.effects = this.item.effects;
ctx.config = DOTDUNGEON;
ctx.icons = CONFIG.CACHE.icons;
ctx.icons = {};
return ctx;
};

View file

@ -1,5 +1,7 @@
import { GenericContextMenu } from "../../utils/GenericContextMenu.mjs";
import { DialogManager } from "../../utils/DialogManager.mjs";
import { GenericItemSheet } from "./GenericItemSheet.mjs";
import { localizer } from "../../utils/localizer.mjs";
export class UntypedItemSheet extends GenericItemSheet {
static get defaultOptions() {
@ -28,22 +30,78 @@ export class UntypedItemSheet extends GenericItemSheet {
new GenericContextMenu(html, `.photo.panel`, [
{
name: `View Larger`,
callback: (html) => {
console.log(`.dungeon | View Larger`);
name: localizer(`dotdungeon.common.view-larger`),
callback: () => {
(new ImagePopout(this.item.img)).render(true);
},
},
{
name: `Change Photo`,
name: localizer(`dotdungeon.common.edit`),
condition: () => this.isEditable,
callback: (html) => {
console.log(`.dungeon | Change Photo`);
callback: () => {
const fp = new FilePicker({
callback: (path) => {
this.item.update({"img": path});
},
});
fp.render(true);
},
},
{
name: localizer(`dotdungeon.common.reset`),
condition: () => this.isEditable,
callback: () => {
console.log(`.dungeon | Reset Item Image`)
},
}
]);
if (!this.isEditable) return;
console.debug(`.dungeon | Adding event listeners for Untyped Item: ${this.item.id}`);
html.find(`.create-ae`).on(`click`, async () => {
await this.item.createEmbeddedDocuments(
`ActiveEffect`,
[{name: localizer(`dotdungeon.default.name`, { document: `ActiveEffect`, type: `base` })}],
{ renderSheet: true }
);
});
new GenericContextMenu(html, `.effect.panel`, [
{
name: localizer(`dotdungeon.common.edit`),
callback: async (html) => {
(await fromUuid(html.closest(`.effect`)[0].dataset.embeddedId))?.sheet.render(true);
},
},
{
name: localizer(`dotdungeon.common.delete`),
callback: async (html) => {
const target = html.closest(`.effect`)[0];
const data = target.dataset;
const id = data.embeddedId;
const doc = await fromUuid(id);
DialogManager.createOrFocus(
`${doc.uuid}-delete`,
{
title: localizer(`dotdungeon.delete.ActiveEffect.title`, doc),
content: localizer(`dotdungeon.delete.ActiveEffect.content`, doc),
buttons: {
yes: {
label: localizer(`Yes`),
callback() {
doc.delete();
},
},
no: {
label: localizer(`No`),
}
}
}
);
},
}
]);
};
async getData() {

View file

@ -18,12 +18,20 @@ export function localizer(key, args = {}, depth = 0) {
return localized;
};
/*
Helps prevent recursion on the same key so that we aren't doing excess work.
*/
const localizedSubkeys = new Map();
for (const match of subkeys) {
const subkey = match.groups.key;
localized =
localized.slice(0, match.index)
+ localizer(subkey, args, depth + 1)
+ localized.slice(match.index + subkey.length + 1)
if (localizedSubkeys.has(subkey)) continue;
localizedSubkeys.set(subkey, localizer(subkey, args, depth + 1));
};
return localized;
return localized.replace(
localizerConfig.subKeyPattern,
(_fullMatch, subkey) => {
return localizedSubkeys.get(subkey);
}
);
};

View file

@ -44,6 +44,7 @@
%flex-col {
display: flex;
flex-direction: column;
gap: 8px;
}
@include utils.tab("details") {
@ -56,6 +57,10 @@
}
}
@include utils.tab("effects") {
@extend %flex-col;
}
@include utils.tab("settings") {
@extend %flex-col;

View file

@ -7,7 +7,7 @@
name="system.stats.{{stat.key}}"
class="e-2dp dice-select"
>
{{{dd-options stat.value stat.dieOptions}}}
{{{dd-options stat.original stat.dieOptions}}}
</select>
<button
type="button"
@ -35,7 +35,7 @@
class="e-2dp skill__training"
>
{{{dd-options
skill.value
skill.original
@root.config.trainingLevels
localize=true
}}}

View file

@ -1,3 +1,34 @@
<div class="tab" data-group="page" data-tab="effects">
Effects Tab
{{#each effects as | effect |}}
<div class="effect panel" data-embedded-id="{{effect.uuid}}">
<div class="effect__name">
{{effect.name}}
</div>
<div>
{{ifThen effect.disabled "Disabled" "Enabled"}}
</div>
{{!-- TODO: For some reason this embedded update logic was failing
<label
class="effect__name"
for="{{effect.uuid}}-toggle"
>
{{ effect.name }}
</label>
<div class="effect__active">
<input
type="checkbox"
{{checked effect.enabled}}
id="{{effect.uuid}}-toggle"
data-embedded-id="{{effect.uuid}}"
data-embedded-update="disabled"
data-embedded-update-on="change"
>
</div>
--}}
</div>
{{/each}}
<button type="button" class="create-ae">
<dd-icon name="ui/plus" var:fill="currentColor"></dd-icon>
Create Effect
</button>
</div>