Compare commits

...
Sign in to create a new pull request.

24 commits

Author SHA1 Message Date
Oliver-Akins
5d41b087b5 Remove the function definition where it wasn't needed 2024-05-05 16:41:17 -06:00
Oliver-Akins
c3e7aee501 Add a dd-range component for repeated checkboxes that represent something like "1/3" 2024-05-05 16:40:59 -06:00
Oliver-Akins
0972a0491f Cleanup and improve JSDoc typing 2024-05-05 16:39:56 -06:00
Oliver-Akins
718bd8398d Move the data that should not be stored in the DB into the Actor's prepareBaseData method 2024-05-05 16:39:25 -06:00
Oliver-Akins
91c95da639 Begin playing around with the weapon sheet 2024-04-30 21:31:49 -06:00
Oliver-Akins
d1cb412e5a Add event handling for the custom input elements 2024-04-30 21:31:11 -06:00
Oliver-Akins
7b3ea59185 Genericise some of the item sheet styling so that I'm not duplicating styles 2024-04-30 21:30:27 -06:00
Oliver-Akins
5b5e74e085 Make it so that the custom input delegates focus 2024-04-30 21:23:15 -06:00
Oliver-Akins
d433e6a51d Prevent specific data fields from being modifiable via AEs 2024-04-30 20:00:05 -06:00
Oliver-Akins
d92d6b9d3c Remove unneeded diceChoice function now that DiceField is a class I can use 2024-04-30 19:56:02 -06:00
Oliver-Akins
851992161f Make the Material datamodel be the DescribedItemData 2024-04-30 19:55:16 -06:00
Oliver-Akins
e4ac6f4ca1 Add markers for VSC minimap 2024-04-28 19:11:55 -06:00
Oliver-Akins
90a9badddc Remove file from git 2024-04-28 17:39:14 -06:00
Oliver-Akins
194068c22c Update the materialize mixins to accept a base colour (allows preventing transparency for things like nav-bars; closes #160) 2024-04-28 17:19:31 -06:00
Oliver-Akins
11d2a2a10f Make minimum Foundry version 12 2024-04-28 14:05:23 -06:00
Oliver-Akins
14ab0aa5dc Update the verified version to v12 2024-04-28 00:23:57 -06:00
Oliver-Akins
d84c5e2a20 Add the setting translations to the new localization file 2024-04-28 00:20:47 -06:00
Oliver-Akins
327f921b9c Implement the DiceField and required changes to get it working (closes #179) 2024-04-27 02:07:22 -06:00
Oliver-Akins
0064e10635 Make the ActiveEffects able to pull values from properties on the target object for it's value 2024-04-27 02:05:59 -06:00
Oliver-Akins
76cca3f672 Merge remote-tracking branch 'origin/main' into foundry/v12 2024-04-27 00:38:22 -06:00
Oliver-Akins
901eea4ff3 Remove reference to the DiceField data field (for now) 2024-04-19 18:52:11 -06:00
Oliver-Akins
ee77f10957 Remove methods that don't need to be overridden for StringField 2024-04-19 18:51:56 -06:00
Oliver-Akins
5f1dc53a91 Disable AE legacy transferral 2024-04-18 23:34:05 -06:00
Oliver-Akins
761961a7fb Begin playing around with a custom DataField for dice selection that will allow using the default AE modes on it 2024-04-18 23:33:55 -06:00
32 changed files with 759 additions and 204 deletions

View file

@ -101,6 +101,32 @@
"title": "Delete Effect", "title": "Delete Effect",
"content": "<p>Are you sure you would like to delete the active effect: {name}</p>" "content": "<p>Are you sure you would like to delete the active effect: {name}</p>"
} }
},
"settings": {
"showAvatarOnSheet": {
"name": "Show Avatar On Player Sheet",
"description": "Determines whether or not to show the avatar to you on the Player Character sheets, turning this off will replace the image with a file picker so that you can still change the image from the character sheet."
},
"playersCanChangeGroup": {
"name": "Allow Players to Change Group",
"description": "Setting this to true allows non-GM players to modify the group that the Actor belongs to. While this is disabled the GM will still be able to modify each player's group by editing the character sheet."
},
"resourcesOrSupplies": {
"name": "Use Resources or Supplies",
"description": "Determines which term to use for the objects that allow travelling into the next hex tile. This is because of the",
"option": {
"supplies": "Supplies",
"resources": "Resources"
}
},
"openEmbeddedOnCreate": {
"name": "Edit Custom Items Immediately When Created",
"description": "Tells the character sheets that have \"Add\" buttons to open the Item's sheet when you create the new item so that you can immediately edit it without needing to click more buttons."
},
"aspectLimit": {
"name": "Character Aspect Limit",
"description": "Limit how many Aspects a single character can have."
}
} }
}, },
"TYPES": { "TYPES": {

View file

@ -4,6 +4,8 @@ import { StyledShadowElement } from "./mixins/Styles.mjs";
Attributes: Attributes:
@property {string} name - The name of the icon, takes precedence over the path @property {string} name - The name of the icon, takes precedence over the path
@property {string} path - The path of the icon file @property {string} path - The path of the icon file
@extends {HTMLElement}
*/ */
export class DotDungeonIcon extends StyledShadowElement(HTMLElement) { export class DotDungeonIcon extends StyledShadowElement(HTMLElement) {
static elementName = `dd-icon`; static elementName = `dd-icon`;

View file

@ -13,8 +13,14 @@ Attributes:
Styling: Styling:
- `--height`: Controls the height of the element + the width of the buttons (default: 1.25rem) - `--height`: Controls the height of the element + the width of the buttons (default: 1.25rem)
- `--width`: Controls the width of the number input (default 50px) - `--width`: Controls the width of the number input (default 50px)
@extends {HTMLElement}
*/ */
export class DotDungeonIncrementer extends StyledShadowElement(HTMLElement) { export class DotDungeonIncrementer
extends StyledShadowElement(
HTMLElement,
{ mode: `open`, delegatesFocus: true }
) {
static elementName = `dd-incrementer`; static elementName = `dd-incrementer`;
static formAssociated = true; static formAssociated = true;
@ -38,14 +44,14 @@ export class DotDungeonIncrementer extends StyledShadowElement(HTMLElement) {
get form() { get form() {
return this._internals.form; return this._internals.form;
} };
get name() { get name() {
return this.getAttribute(`name`); return this.getAttribute(`name`);
} };
set name(value) { set name(value) {
this.setAttribute(`name`, value); this.setAttribute(`name`, value);
} };
get value() { get value() {
return this.getAttribute(`value`); return this.getAttribute(`value`);
@ -56,7 +62,7 @@ export class DotDungeonIncrementer extends StyledShadowElement(HTMLElement) {
get type() { get type() {
return `number`; return `number`;
} };
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();

View file

@ -1,9 +1,11 @@
import { DotDungeonIncrementer } from "./incrementer.mjs"; import { DotDungeonIncrementer } from "./incrementer.mjs";
import { DotDungeonIcon } from "./icon.mjs"; import { DotDungeonIcon } from "./icon.mjs";
import { DotDungeonRange } from "./range.mjs";
const components = [ const components = [
DotDungeonIcon, DotDungeonIcon,
DotDungeonIncrementer, DotDungeonIncrementer,
DotDungeonRange,
]; ];
export function registerCustomComponents() { export function registerCustomComponents() {

View file

@ -1,7 +1,7 @@
/** /**
* @param {HTMLElement} Base * @param {HTMLElement} Base
*/ */
export function StyledShadowElement(Base) { export function StyledShadowElement(Base, shadowOptions = { mode: `open` }) {
return class extends Base { return class extends Base {
/** /**
* The path to the CSS that is loaded * The path to the CSS that is loaded
@ -33,7 +33,7 @@ export function StyledShadowElement(Base) {
constructor() { constructor() {
super(); super();
this._shadow = this.attachShadow({ mode: `open` }); this._shadow = this.attachShadow(shadowOptions);
this._style = document.createElement(`style`); this._style = document.createElement(`style`);
this._shadow.appendChild(this._style); this._shadow.appendChild(this._style);
}; };

139
module/components/range.mjs Normal file
View file

@ -0,0 +1,139 @@
import { DotDungeonIcon } from "./icon.mjs";
import { StyledShadowElement } from "./mixins/Styles.mjs";
/**
Attributes:
@property {string} name - The path to the value to update in the datamodel
@property {number} value - The actual value of the input
@property {number} max - The maximum value that this range has
@extends {HTMLElement}
*/
export class DotDungeonRange
extends StyledShadowElement(
HTMLElement,
{ mode: `open`, delegatesFocus: true }
) {
static elementName = `dd-range`;
static formAssociated = true;
static observedAttributes = [`max`];
static _stylePath = `v3/components/range.css`;
_internals;
#input;
constructor() {
super();
// Form internals
this._internals = this.attachInternals();
this._internals.role = `spinbutton`;
};
get form() {
return this._internals.form;
};
get name() {
return this.getAttribute(`name`);
};
set name(value) {
this.setAttribute(`name`, value);
};
get value() {
try {
return parseInt(this.getAttribute(`value`));
} catch {
throw new Error(`Failed to parse attribute: "value" - Make sure it's an integer`);
};
};
set value(value) {
this.setAttribute(`value`, value);
};
get max() {
try {
return parseInt(this.getAttribute(`max`));
} catch {
throw new Error(`Failed to parse attribute: "max" - Make sure it's an integer`);
};
};
set max(value) {
this.setAttribute(`max`, value);
};
get type() {
return `number`;
};
connectedCallback() {
super.connectedCallback();
// Attribute validation
if (!this.hasAttribute(`max`)) {
throw new Error(`dotdungeon | Cannot have a range without a maximum value`);
};
// Keyboard accessible input for the thing
this.#input = document.createElement(`input`);
this.#input.type = `number`;
this.#input.min = 0;
this.#input.max = this.max;
this.#input.value = this.value;
this.#input.addEventListener(`change`, () => {
const inputValue = parseInt(this.#input.value);
if (inputValue === this.value) return;
this._updateValue.bind(this)(Math.sign(this.value - inputValue));
this._updateValue(Math.sign(this.value - inputValue));
});
this._shadow.appendChild(this.#input);
// Shadow-DOM construction
this._elements = new Array(this.max);
const container = document.createElement(`div`);
container.classList.add(`container`);
// Creating the node for filled content
const filledContainer = document.createElement(`div`);
filledContainer.classList.add(`range-increment`, `filled`);
const filledNode = this.querySelector(`[slot="filled"]`);
if (filledNode) filledContainer.appendChild(filledNode);
const emptyContainer = document.createElement(`div`);
emptyContainer.classList.add(`range-increment`, `empty`);
const emptyNode = this.querySelector(`[slot="empty"]`);
if (emptyNode) emptyContainer.appendChild(emptyNode);
this._elements.fill(filledContainer, 0, this.value);
this._elements.fill(emptyContainer, this.value);
container.append(...this._elements.map((slot, i) => {
const node = slot.cloneNode(true);
node.setAttribute(`data-index`, i + 1);
node.addEventListener(`click`, () => {
const filled = node.classList.contains(`filled`);
this._updateValue(filled ? -1 : 1);
});
return node;
}));
this._shadow.appendChild(container);
/*
This converts all of the namespace prefixed properties on the element to
CSS variables so that they don't all need to be provided by doing style=""
*/
for (const attrVar of this.attributes) {
if (attrVar.name?.startsWith(`var:`)) {
const prop = attrVar.name.replace(`var:`, ``);
this.style.setProperty(`--` + prop, attrVar.value);
};
};
};
_updateValue(delta) {
this.value += delta;
this.dispatchEvent(new Event(`change`, { bubbles: true }));
};
};

View file

@ -68,6 +68,11 @@ export const itemFilters = [
`service`, `service`,
]; ];
export const invalidActiveEffectTargets = new Set([
`system.uses_inventory_slot`,
`system.quantity_affects_used_capacity`,
]);
export default { export default {
stats, stats,
statDice, statDice,
@ -86,4 +91,5 @@ export default {
syncDice, syncDice,
localizerConfig, localizerConfig,
itemFilters, itemFilters,
invalidActiveEffectTargets,
}; };

View file

@ -1,7 +1,25 @@
import { invalidActiveEffectTargets } from "../../config.mjs";
export class DotDungeonActiveEffect extends ActiveEffect { export class DotDungeonActiveEffect extends ActiveEffect {
// Invert the logic of the disabled property so it's easier to modify via // Invert the logic of the disabled property so it's easier to modify via
// embedded controls // embedded controls
get enabled() { return !this.disabled }; get enabled() { return !this.disabled };
set enabled(newValue) { this.disabled = !newValue }; set enabled(newValue) { this.disabled = !newValue };
apply(object, change) {
if (invalidActiveEffectTargets.has(change.key)) return;
change.value = change.value.replace(
/@(?<key>[\w\.]+)/gi,
(...args) => {
const key = args[1];
if (foundry.utils.hasProperty(object, key)) {
return foundry.utils.getProperty(object, key)
};
return args[0];
}
)
return super.apply(object, change);
}
}; };

View file

@ -6,7 +6,7 @@ export class DotDungeonActor extends Actor {
provide all that data to AE's without needing to disable any inputs. provide all that data to AE's without needing to disable any inputs.
*/ */
prepareEmbeddedDocuments() { prepareEmbeddedDocuments() {
this.preAE = foundry.utils.deepClone(this.system); this.preAE = foundry.utils.duplicate(this.system);
super.prepareEmbeddedDocuments(); super.prepareEmbeddedDocuments();
}; };

View file

@ -2,6 +2,17 @@ import { DotDungeonActor } from "./GenericActor.mjs";
export class Player extends DotDungeonActor { export class Player extends DotDungeonActor {
/*
These are special data properties that will be used by ActiveEffects to modify
certain limits within the actor, allowing for neat hacks that change these and
possible configuration of them.
*/
prepareBaseData() {
this.system.weapon_slots = 2;
this.system.inventory_slots = 0;
this.system.respawn_limit = 3;
};
applyActiveEffects() { applyActiveEffects() {
super.applyActiveEffects(); super.applyActiveEffects();

View file

@ -1,6 +1,5 @@
// Data Models // Data Models
import { DescribedItemData } from "./models/Item/DescribedItemData.mjs"; import { DescribedItemData } from "./models/Item/DescribedItemData.mjs";
import { CommonItemData } from "./models/Item/CommonItemData.mjs";
import { WeaponItemData } from "./models/Item/Weapon.mjs"; import { WeaponItemData } from "./models/Item/Weapon.mjs";
import { AspectItemData } from "./models/Item/Aspect.mjs"; import { AspectItemData } from "./models/Item/Aspect.mjs";
import { SpellItemData } from "./models/Item/Spell.mjs"; import { SpellItemData } from "./models/Item/Spell.mjs";
@ -16,6 +15,7 @@ import { ItemProxy } from "./documents/Item/_proxy.mjs";
// Item Sheets // Item Sheets
import { UntypedItemSheet } from "./sheets/Items/UntypedItemSheet.mjs"; import { UntypedItemSheet } from "./sheets/Items/UntypedItemSheet.mjs";
import { WeaponSheet } from "./sheets/Items/WeaponSheet.mjs";
import { AspectSheet } from "./sheets/Items/AspectSheet.mjs"; import { AspectSheet } from "./sheets/Items/AspectSheet.mjs";
import { SpellSheet } from "./sheets/Items/SpellSheet.mjs"; import { SpellSheet } from "./sheets/Items/SpellSheet.mjs";
import { PetSheet } from "./sheets/Items/PetSheet.mjs"; import { PetSheet } from "./sheets/Items/PetSheet.mjs";
@ -39,6 +39,7 @@ import { devInit } from "./hooks/devInit.mjs";
import DOTDUNGEON from "./config.mjs"; import DOTDUNGEON from "./config.mjs";
// MARK: init hook
Hooks.once(`init`, async () => { Hooks.once(`init`, async () => {
console.debug(`.dungeon | Initializing`); console.debug(`.dungeon | Initializing`);
CONFIG.ActiveEffect.legacyTransferral = false; CONFIG.ActiveEffect.legacyTransferral = false;
@ -49,7 +50,7 @@ Hooks.once(`init`, async () => {
CONFIG.Actor.dataModels.sync = SyncData; CONFIG.Actor.dataModels.sync = SyncData;
CONFIG.Actor.dataModels.mob = MobData; CONFIG.Actor.dataModels.mob = MobData;
CONFIG.Item.dataModels.untyped = DescribedItemData; CONFIG.Item.dataModels.untyped = DescribedItemData;
CONFIG.Item.dataModels.material = CommonItemData; CONFIG.Item.dataModels.material = DescribedItemData;
CONFIG.Item.dataModels.foil = DescribedItemData; CONFIG.Item.dataModels.foil = DescribedItemData;
CONFIG.Item.dataModels.weapon = WeaponItemData; CONFIG.Item.dataModels.weapon = WeaponItemData;
CONFIG.Item.dataModels.aspect = AspectItemData; CONFIG.Item.dataModels.aspect = AspectItemData;
@ -95,6 +96,11 @@ Hooks.once(`init`, async () => {
types: ["aspect"], types: ["aspect"],
label: "dotdungeon.sheet-names.AspectSheet" label: "dotdungeon.sheet-names.AspectSheet"
}); });
Items.registerSheet("dotdungeon", WeaponSheet, {
makeDefault: true,
types: ["weapon"],
label: "dotdungeon.sheet-names.WeaponSheet"
});
Items.registerSheet("dotdungeon", SpellSheet, { Items.registerSheet("dotdungeon", SpellSheet, {
makeDefault: true, makeDefault: true,
types: ["spell"], types: ["spell"],
@ -116,6 +122,7 @@ Hooks.once(`init`, async () => {
}); });
// MARK: ready hook
Hooks.once(`ready`, () => { Hooks.once(`ready`, () => {
console.debug(".dungeon | Ready"); console.debug(".dungeon | Ready");

View file

@ -32,11 +32,15 @@ export const partials = [
`actors/char-sheet/v2/partials/inventory/items/weapon.v2.pc.hbs`, `actors/char-sheet/v2/partials/inventory/items/weapon.v2.pc.hbs`,
`actors/char-sheet/v2/partials/inventory/items/pet.v2.pc.hbs`, `actors/char-sheet/v2/partials/inventory/items/pet.v2.pc.hbs`,
// The v2 Untyped sheet partials // The partials used for Untyped v2 and other item sheets that don't have a
// unique design for the other tabs
`items/untyped/v2/tabs/general.v2.untyped.hbs`, `items/untyped/v2/tabs/general.v2.untyped.hbs`,
`items/untyped/v2/tabs/details.v2.untyped.hbs`, `items/untyped/v2/tabs/details.v2.untyped.hbs`,
`items/untyped/v2/tabs/effects.v2.untyped.hbs`, `items/untyped/v2/tabs/effects.v2.untyped.hbs`,
`items/untyped/v2/tabs/settings.v2.untyped.hbs`, `items/untyped/v2/tabs/settings.v2.untyped.hbs`,
// The weapon sheet partials
`items/weapon/v1/tabs/details.v1.weapon.hbs`,
]; ];
export const preAliasedPartials = { export const preAliasedPartials = {

View file

@ -7,7 +7,7 @@ import { options } from "./options.mjs";
export default { export default {
// Complex helpers // MARK: Complex helpers
"dd-schemaOptions": schemaOptions, "dd-schemaOptions": schemaOptions,
"dd-array": createArray, "dd-array": createArray,
"dd-objectValue": objectValue, "dd-objectValue": objectValue,
@ -15,13 +15,13 @@ export default {
"dd-i18n": handlebarsLocalizer, "dd-i18n": handlebarsLocalizer,
"dd-options": options, "dd-options": options,
// Simple helpers // MARK: Simple helpers
"dd-stringify": v => JSON.stringify(v, null, ` `), "dd-stringify": v => JSON.stringify(v, null, ` `),
"dd-empty": v => v.length == 0, "dd-empty": v => v.length == 0,
"dd-set-has": (s, k) => s.has(k), "dd-set-has": (s, k) => s.has(k),
"dd-empty-state": (v) => v ?? localizer(`dotdungeon.common.empty`), "dd-empty-state": (v) => v ?? localizer(`dotdungeon.common.empty`),
// Logic helpers // MARK: Logic helpers
"eq": (a, b) => a == b, "eq": (a, b) => a == b,
"neq": (a, b) => a != b, "neq": (a, b) => a != b,
"not": v => !v, "not": v => !v,

View file

@ -1,15 +1,5 @@
import DOTDUNGEON from "../../config.mjs"; import DOTDUNGEON from "../../config.mjs";
import { DiceField } from "../fields/DiceField.mjs";
function diceChoiceField() {
return new foundry.data.fields.StringField({
initial: ``,
blank: true,
trim: true,
options() {
return DOTDUNGEON.statDice;
},
});
};
function trainingLevelField() { function trainingLevelField() {
return new foundry.data.fields.NumberField({ return new foundry.data.fields.NumberField({
@ -24,26 +14,18 @@ export class PlayerData extends foundry.abstract.TypeDataModel {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
/*
These are special data properties that will be used by ActiveEffects
to modify certain limits within the actor, allowing for neat hacks
that change these
*/
weapon_slots: new fields.NumberField({ initial: 2 }),
inventory_slots: new fields.NumberField({ initial: 0 }),
bytes: new fields.NumberField({ bytes: new fields.NumberField({
initial: 0, initial: 0,
min: 0, min: 0,
integer: true, integer: true,
}), }),
stats: new fields.SchemaField({ stats: new fields.SchemaField({
build: diceChoiceField(), build: new DiceField(),
meta: diceChoiceField(), meta: new DiceField(),
presence: diceChoiceField(), presence: new DiceField(),
hands: diceChoiceField(), hands: new DiceField(),
tilt: diceChoiceField(), tilt: new DiceField(),
rng: diceChoiceField(), rng: new DiceField(),
}), }),
skills: new fields.SchemaField({ skills: new fields.SchemaField({
build: new fields.SchemaField({ build: new fields.SchemaField({
@ -87,11 +69,7 @@ export class PlayerData extends foundry.abstract.TypeDataModel {
min: 0, min: 0,
integer: true integer: true
}), }),
respawns: new fields.SchemaField({ respawns: new fields.NumberField({ initial: 0, }),
r1: new fields.BooleanField(),
r2: new fields.BooleanField(),
r3: new fields.BooleanField(),
}),
syncDelta: new fields.NumberField({ syncDelta: new fields.NumberField({
required: true, required: true,
integer: true, integer: true,

View file

@ -0,0 +1,88 @@
import { statDice } from "../../config.mjs";
/**
* A subclass of DataField that allows ActiveEffects to integrate with dice
* values and increase/decrease the value step-wise according to the dice ladder.
*/
export class DiceField extends foundry.data.fields.DataField {
static get _defaults() {
return foundry.utils.mergeObject(super._defaults, {
trim: true,
blank: true,
initial: ``,
choices: [``, ...statDice],
});
};
constructor(options = {}, context = {}) {
super(options, context);
// v- because for some reason Foundry doesn't respect the _defaults getter
this.blank = true;
};
_cast(value) { return value };
_castChangeDelta(delta) { return delta };
/**
* @param {string} value The current value
* @param {string} delta The AE value
* @param {unknown} model
* @param {unknown} changes
*/
_applyChangeAdd(value, delta, model, changes) {
if (value === "") return value;
delta = parseInt(delta);
const dieIndex = statDice.findIndex(die => die === value);
const newIndex = Math.min(Math.max(0, dieIndex + delta), statDice.length - 1);
value = statDice[newIndex];
return value;
};
/**
* @param {string} value The current value
* @param {string} delta The AE value
* @param {unknown} model
* @param {unknown} changes
*/
_applyChangeMultiply(value, delta, model, changes) {
console.warn(`.dungeon | Cannot apply Multiply ActiveEffects to DiceFields. Not changing value.`);
return value;
};
/**
* @param {string} value The current value
* @param {string} delta The AE value
* @param {unknown} model
* @param {unknown} changes
*/
_applyChangeOverride(value, delta, model, changes) {
return delta;
};
/**
* @param {string} value The current value
* @param {string} delta The AE value
* @param {unknown} model
* @param {unknown} changes
*/
_applyChangeUpgrade(value, delta, model, changes) {
if (value === "") return value;
const currentIndex = statDice.findIndex(die => die === value);
const upgradedIndex = statDice.findIndex(die => die === delta);
return statDice[Math.max(currentIndex, upgradedIndex)];
};
/**
* @param {string} value The current value
* @param {string} delta The AE value
* @param {unknown} model
* @param {unknown} changes
*/
_applyChangeDowngrade(value, delta, model, changes) {
if (value === "") return value;
const currentIndex = statDice.findIndex(die => die === value);
const upgradedIndex = statDice.findIndex(die => die === delta);
return statDice[Math.min(currentIndex, upgradedIndex)];
};
};

View file

@ -92,7 +92,7 @@ export class PlayerSheetv2 extends GenericActorSheet {
get #statData() { get #statData() {
const stats = []; const stats = [];
const usedDice = new Set(Object.values(this.actor.system.stats)); const usedDice = new Set(Object.values(this.actor.preAE.stats));
for (const statName in this.actor.system.stats) { for (const statName in this.actor.system.stats) {
const stat = { const stat = {
key: statName, key: statName,

View file

@ -58,7 +58,7 @@ export class GenericActorSheet extends ActorSheet {
*/ */
html.find( html.find(
CONFIG.CACHE.componentListeners.map(n => `${n}[name]`).join(`,`) CONFIG.CACHE.componentListeners.map(n => `${n}[name]`).join(`,`)
).on(`change`, () => this._onChangeInput.bind(this)); ).on(`change`, this._onChangeInput.bind(this));
/* /*
Utility event listeners that apply Utility event listeners that apply

View file

@ -43,6 +43,15 @@ export class GenericItemSheet extends ItemSheet {
if (!this.isEditable) return; if (!this.isEditable) return;
console.debug(`.dungeon | Adding event listeners for Generic Item: ${this.id}`); console.debug(`.dungeon | Adding event listeners for Generic Item: ${this.id}`);
/*
Custom element event listeners because Foundry doesn't listen to them by
default.
*/
html.find(
CONFIG.CACHE.componentListeners.map(n => `${n}[name]`).join(`,`)
).on(`change`, this._onChangeInput.bind(this));
html.find(`button[data-increment]`) html.find(`button[data-increment]`)
.on(`click`, this._incrementValue.bind(this)); .on(`click`, this._incrementValue.bind(this));
html.find(`button[data-decrement]`) html.find(`button[data-decrement]`)

View file

@ -0,0 +1,116 @@
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 WeaponSheet extends GenericItemSheet {
static get defaultOptions() {
let opts = foundry.utils.mergeObject(
super.defaultOptions,
{
template: `systems/dotdungeon/templates/items/weapon/v1/index.hbs`,
width: 300,
height: 340,
tabs: [
{
group: `page`,
navSelector: `nav.page`,
contentSelector: `.page-content`,
initial: `general`,
},
],
}
);
opts.classes.push(`dotdungeon`, `style-v3`);
return opts;
};
activateListeners(html) {
super.activateListeners(html);
new GenericContextMenu(html, `.photo.panel`, [
{
name: localizer(`dotdungeon.common.view-larger`),
callback: () => {
(new ImagePopout(this.item.img)).render(true);
},
},
{
name: localizer(`dotdungeon.common.edit`),
condition: () => this.isEditable,
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 Weapon 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() {
const ctx = await super.getData();
ctx.meta.showSettingsTab = ctx.isGM || this.item.isOwned;
ctx.meta.isEmbedded = this.item.isOwned;
ctx.meta.isEditable = this.isEditable;
return ctx;
};
};

View file

@ -1,80 +0,0 @@
## Tabs:
- Main
- Stats
- Skills
- Inventory
- Player
- Containers
- Inventory (divided into category of items)
- This is the items that the player will have "on them"
- Storage (divided into category of items)
- This is all of the items that the players owns and has put into storage *somewhere*
- Transportation
- Combat
- Easy skill buttons:
- Melee
- Accuracy
- Weapons
- Sync / Respawns
- Info
- Account name
- PFP
- Group name
- Aspects
- Roles
- Spells
======
## Requirements:
Stats:
- Needs to list all 6 of the primary stats
- Needs to have a dropdown for to select a die
- Nice to have: disables dice that have been selected in other stats
- Needs to have a button to roll the stat (when a die is selected)
- Foundry v12: ActiveEffect - Die needs to be able to be affected by ActiveEffects
Skills:
- Each of the 25 skills needs to be grouped under a header of what stat it's
associated with
- Each skill must have a dropdown to indicate training level (null, trained,
expert, locked)
- Every skill must have a button to roll the dice that is labelled with the
correct formula for that skill (or "Locked" if the skill is locked)
- ActiveEffect - Increase Modifier
- Foundry v12: ActiveEffect - Increase Training Level
Combat:
- Two weapon slots for the equipped weapon(s)
- A single armor slot
- Quick-access to the Melee / Accuracy skills
Inventory:
- Needs three sub-tabs:
- Player
- Storage
- Transportation
- Player Subtab:
- Needs to have a section for container items, and indicating how many slots
each one has.
- List all of the items that the player has with the "inventory" location
- Show the total number of items the player on their character and how many
total slots are available
- Needs some way to move items to a different storage area (embedded-only
item sheet field maybe)
- Storage Subtab:
- List all of the items that the player has marked as in-storage
- Transportation:
- This is currently just a placeholder tab, no functionality needed other
than existing
Spells:
- Lists all spells on a page (sortable by: alphabetical, cost, etc.)
Info:
- Needs a place to edit the actor's name
- Needs a place to edit the actor's image
- Needs a place to edit the group name (if enabled by the GM, or is the GM)
- Needs a place to see and manage all equipped aspects
- Needs a place to see and manage all equipped roles

View file

@ -0,0 +1,68 @@
/*
Disclaimer: This CSS is used by a custom web component and is scoped to JUST
the corresponding web component. Importing this into other files is forbidden
*/
@use "../mixins/material";
@use "./common.scss";
.container {
@include material.elevate(4);
display: grid;
flex-direction: row;
gap: 4px;
border-radius: 4px;
grid-template-columns: minmax(0, 1fr);
grid-auto-columns: minmax(0, 1fr);
grid-template-rows: 1fr;
padding: 4px;
box-sizing: border-box;
}
input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
&:focus ~ .container {
@include material.elevate(8)
}
}
.range-increment {
grid-row: 1;
&:empty {
@include material.elevate(4);
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border-radius: 2px;
margin: 0;
cursor: pointer;
width: 1.25rem;
height: 1.25rem;
position: relative;
&:hover {
@include material.elevate(8);
}
&.filled::before {
content: "";
background: var(--checkbox-checked);
border-radius: 3px;
$margin: 4px;
top: $margin;
bottom: $margin;
left: $margin;
right: $margin;
position: absolute;
}
}
}

View file

@ -6,16 +6,24 @@
border-radius: 4px; border-radius: 4px;
padding: var(--gap); padding: var(--gap);
&.space-between {
justify-content: space-between;
}
&--row { &--row {
@extend .panel; @extend .panel;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: var(--gap); gap: var(--gap);
align-items: center; align-items: center;
}
&.space-between { &--column {
justify-content: space-between; @extend .panel;
} display: flex;
flex-direction: column;
gap: var(--gap);
justify-content: center;
} }
} }
} }

View file

@ -19,7 +19,9 @@
/* Sheet Layouts */ /* Sheet Layouts */
@use "./layouts/datasheet.scss"; @use "./layouts/datasheet.scss";
@use "./layouts/items/common.scss";
@use "./layouts/items/untyped/v2.scss"; @use "./layouts/items/untyped/v2.scss";
@use "./layouts/items/weapon/v1.scss";
/* Sheet Options */ /* Sheet Options */
.dotdungeon.style-v3 { .dotdungeon.style-v3 {

View file

@ -0,0 +1,71 @@
@use "../../mixins/material";
@use "../../mixins/utils";
.dotdungeon.style-v3 .item {
--gap: 8px;
.nav-bar {
@include material.elevate(8, $base: var(--surface));
position: absolute;
bottom: 0;
left: 0;
right: 6px;
nav {
display: flex;
flex-direction: row;
gap: var(--gap);
padding: var(--gap);
}
}
.page-content {
padding: calc(var(--gap) * 1);
padding-bottom: 60px;
height: 100%;
> .tab {
height: 100%;
gap: var(--gap);
}
}
@include utils.tab("general") {
display: grid;
--height: 50px;
grid-template-columns: var(--height) 1fr;
grid-template-rows: var(--height) 1fr;
.description {
grid-column: 1 / -1;
}
}
%flex-col {
display: flex;
flex-direction: column;
gap: 8px;
}
@include utils.tab("effects") {
@extend %flex-col;
}
@include utils.tab("settings") {
@extend %flex-col;
.capacity-usage, .quantity-capacity, .combat-relevant {
display: flex;
align-items: center;
}
.capacity {
&--calculated {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
}
}
}

View file

@ -2,53 +2,10 @@
@use "../../../mixins/utils"; @use "../../../mixins/utils";
.dotdungeon.style-v3 .item--untyped { .dotdungeon.style-v3 .item--untyped {
--gap: 8px; @include utils.tab("details") {
.nav-bar {
@include material.elevate(8);
position: absolute;
bottom: 0;
left: 0;
right: 6px;
nav {
display: flex;
flex-direction: row;
gap: var(--gap);
padding: var(--gap);
}
}
.page-content {
padding: calc(var(--gap) * 1);
padding-bottom: 60px;
height: 100%;
> .tab {
height: 100%;
gap: var(--gap);
}
}
@include utils.tab("general") {
display: grid;
--height: 50px;
grid-template-columns: var(--height) 1fr;
grid-template-rows: var(--height) 1fr;
.description {
grid-column: 1 / -1;
}
}
%flex-col {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: var(--gap);
}
@include utils.tab("details") {
@extend %flex-col;
.number { .number {
display: grid; display: grid;
@ -56,26 +13,4 @@
align-items: center; align-items: center;
} }
} }
@include utils.tab("effects") {
@extend %flex-col;
}
@include utils.tab("settings") {
@extend %flex-col;
.capacity-usage, .quantity-capacity, .combat-relevant {
display: flex;
align-items: center;
}
.capacity {
&--calculated {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
}
}
} }

View file

@ -0,0 +1,13 @@
@use "../../../mixins/material";
@use "../../../mixins/utils";
.dotdungeon.style-v3 .item--weapon-v1 {
@include utils.tab("details") {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
.full-width {
grid-column: 1 / -1;
}
}
}

View file

@ -1,12 +1,31 @@
@mixin elevate($height) { @use "sass:map";
background-color: var(--elevation-#{$height}dp-bg);
$elevations: (
0: 0%,
1: 5%,
2: 7%,
3: 8%,
4: 9%,
6: 11%,
8: 12%,
12: 14%,
16: 15%,
24: 16%,
);
@mixin elevate($height, $base: transparent) {
background-color: color-mix(
in lab,
#{$base},
white map.get($elevations, $height)
);
-webkit-box-shadow: 0px 0px #{$height * 1.75}px 0px rgba(0,0,0,0.75); -webkit-box-shadow: 0px 0px #{$height * 1.75}px 0px rgba(0,0,0,0.75);
-moz-box-shadow: 0px 0px #{$height * 1.75}px 0px rgba(0,0,0,0.75); -moz-box-shadow: 0px 0px #{$height * 1.75}px 0px rgba(0,0,0,0.75);
box-shadow: 0px 0px #{$height * 1.75}px 0px rgba(0,0,0,0.75); box-shadow: 0px 0px #{$height * 1.75}px 0px rgba(0,0,0,0.75);
} }
@mixin undo { @mixin undo($base: transparent) {
background-color: transparent; background-color: #{$base};
-webkit-box-shadow: none; -webkit-box-shadow: none;
-moz-box-shadow: none; -moz-box-shadow: none;
box-shadow: none; box-shadow: none;

View file

@ -7,9 +7,9 @@
"manifest": "https://github.com/Oliver-Akins/foundry.dungeon/releases/latest/download/system.json", "manifest": "https://github.com/Oliver-Akins/foundry.dungeon/releases/latest/download/system.json",
"url": "https://github.com/Oliver-Akins/foundry.dungeon", "url": "https://github.com/Oliver-Akins/foundry.dungeon",
"compatibility": { "compatibility": {
"minimum": 11, "minimum": 12,
"verified": 11, "verified": 12,
"maximum": 11 "maximum": 12
}, },
"authors": [ "authors": [
{ {

View file

@ -17,6 +17,9 @@
data-roll-label="Stat Roll : {{stat.name}}" data-roll-label="Stat Roll : {{stat.name}}"
> >
Roll Roll
{{#if stat.value}}
1{{stat.value}}
{{/if}}
</button> </button>
</div> </div>
{{#if stat.skills}} {{#if stat.skills}}

View file

@ -1,4 +1,4 @@
<form class="item--untyped theme-dark"> <form class="item item--untyped theme-dark">
<div class="page-content"> <div class="page-content">
{{> dotdungeon.untyped.v2.general }} {{> dotdungeon.untyped.v2.general }}
{{> dotdungeon.untyped.v2.details }} {{> dotdungeon.untyped.v2.details }}

View file

@ -0,0 +1,21 @@
<form class="item item--weapon-v1 theme-dark">
<div class="page-content">
{{> dotdungeon.untyped.v2.general }}
{{> dotdungeon.weapon.v1.details }}
{{#if meta.showSettingsTab}}
{{> dotdungeon.untyped.v2.settings }}
{{/if}}
{{> dotdungeon.untyped.v2.effects }}
</div>
<div class="nav-bar">
<nav data-group="page" class="page" aria-label="">
<a class="button" data-group="page" data-tab="general">General</a>
<a class="button" data-group="page" data-tab="details">Details</a>
<a class="button" data-group="page" data-tab="effects">Effects</a>
{{#if meta.showSettingsTab}}
<a class="button" data-group="page" data-tab="settings">Settings</a>
{{/if}}
</nav>
</div>
</form>

View file

@ -0,0 +1,83 @@
<div class="tab" data-group="page" data-tab="details">
<div class="full-width panel--row space-between">
{{#if meta.isEditable}}
<label
for="{{meta.idp}}-purchase-cost"
class="justify-start"
>
Purchase Cost
</label>
<dd-incrementer
var:height="1.5rem"
name="system.buy"
value="{{system.buy}}"
id="{{meta.idp}}-purchase-cost"
min="0"
></dd-incrementer>
{{else}}
<span>Purchase Cost</span>
<span class="text-right">{{dd-empty-state system.buy}}</span>
{{/if}}
</div>
<div class="full-width panel--row space-between">
{{#if meta.isEditable}}
<label
for="{{meta.idp}}-usage-cost"
class="justify-start"
>
Usage Cost
</label>
<dd-incrementer
var:height="1.5rem"
name="system.usage_cost"
value="{{system.usage_cost}}"
id="{{meta.idp}}-usage-cost"
min="0"
></dd-incrementer>
{{else}}
<span>Usage Cost</span>
<span class="text-right">{{dd-empty-state system.usage_cost}}</span>
{{/if}}
</div>
<div class="detail panel--column">
<span>Rarity</span>
{{#if meta.isEditable}}
<select
class="text-center"
name="system.tier"
id="{{meta.idp}}-item-tier"
>
{{{ dd-options system.tier config.itemTiers localize=true }}}
</select>
{{else}}
<span>{{dd-i18n (concat "dotdungeon.rarity." system.tier)}}</span>
{{/if}}
</div>
{{#if meta.isEmbedded}}
<div class="detail panel--column">
Item Location
{{#if meta.isEditable}}
<select
class="text-center"
name="system.location"
id="{{meta.idp}}-location"
>
{{{ dd-options system.location item.availableLocations localize=true }}}
</select>
{{else}}
{{dd-i18n (concat "dotdungeon.location." system.location)}}
{{/if}}
</div>
<div class="number full-width panel--row space-between">
{{#if meta.isEditable}}
<label for="{{meta.idp}}-quantity">
Quantity
</label>
<dd-incrementer value="{{system.supplies}}"></dd-incrementer>
{{else}}
Quantity
{{system.quantity}}
{{/if}}
</div>
{{/if}}
</div>