Compare commits

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

26 commits

Author SHA1 Message Date
Eldritch-Oliver
f3aecc1ce8 Remove handlebars log 2025-10-15 22:35:52 -06:00
Eldritch-Oliver
1d13f38f81 Tweak the way armour shows up on the sheet so that it's a bit easier to deal with 2025-10-15 22:35:44 -06:00
Eldritch-Oliver
8ed6f49c8d Finish the initial requirements for the header partial 2025-10-15 22:34:46 -06:00
Eldritch-Oliver
d70c5113b1 Remove log that's a bit spammy 2025-10-15 22:33:39 -06:00
Eldritch-Oliver
0d1c2ddbb3 Fix deprecation warning 2025-10-15 21:27:40 -06:00
Eldritch-Oliver
cd69228e68 Add Craft as it's own section item. Display the attack range 2025-10-13 21:29:56 -06:00
Eldritch-Oliver
28345bdef0 Begin making the BookGeistSheet have the final design 2025-10-12 21:29:59 -06:00
Eldritch-Oliver
798e7441b4 Add a helper to output a value if it's truthy or nothing otherwise 2025-10-12 21:28:52 -06:00
Eldritch-Oliver
d81d86ef47 Auto-equip weapons when embedded into Geist 2025-10-12 20:37:16 -06:00
Eldritch-Oliver
e7ac049ae3 Update the eslint indentation rule to be better when using a buncha mixins 2025-10-12 20:18:18 -06:00
Eldritch-Oliver
4a8ce9b099 Update the GenericAppMixin so that it persists focus better 2025-10-12 18:01:47 -06:00
Eldritch-Oliver
28989c2d35 Update fate path preparation to use localized labels 2025-10-12 14:08:01 -06:00
Eldritch-Oliver
f237bce4d9 Update the GenericApp to be using _prepareContext instead of _preparePartContext 2025-10-12 01:42:23 -06:00
Eldritch-Oliver
7935a85188 Initialize the BookGeistSheet 2025-10-12 01:42:05 -06:00
Eldritch-Oliver
163423ea5b Add dev button for getting an easy actor reference in the console 2025-10-12 00:56:07 -06:00
Eldritch-Oliver
0626279fbe Move the GenericApp mixin into the mixins folder 2025-10-11 22:52:51 -06:00
Eldritch-Oliver
ca185ba42a Add a custom mixin that allows doing complex structural position while maintaining partial rerenders 2025-10-11 22:52:12 -06:00
Eldritch-Oliver
e8baec0bc4 Implement the script that handles preparing the manifest for release 2025-10-11 17:19:03 -06:00
Eldritch-Oliver
165c24f32c Remove unneeded helper method 2025-10-11 17:02:58 -06:00
Eldritch-Oliver
d6beb4ba63 Localize the button label 2025-10-11 17:02:01 -06:00
Eldritch-Oliver
7de3f1ca87 Add an item sheet for editing Traits 2025-10-10 21:50:01 -06:00
Eldritch-Oliver
4d0f29d7f0 Prevent creating a Trait when it's on a non-geist Actor 2025-10-10 21:49:32 -06:00
Eldritch-Oliver
0e0f9d3831 Clean up some non-functional CSS 2025-10-10 21:09:44 -06:00
Eldritch-Oliver
803c1673e2 Move dev-only hooks into their own folder that isn't included in the build 2025-10-10 19:25:07 -06:00
Eldritch-Oliver
8de63e91c7 Get the foundation Trait type defined 2025-10-10 18:07:02 -06:00
Eldritch-Oliver
507c9b0341 Remove parts of the item schemas that aren't necessary 2025-10-10 18:05:56 -06:00
44 changed files with 931 additions and 123 deletions

View file

@ -0,0 +1,23 @@
/*
This hook exists to be able to change all of the actor-sheets to allow
them to have dev-mode controls in their header that are useful for
testing purposes. (This is particularly useful for testing embedded
items that are only allowed to exist on specific actor types)
*/
Hooks.on(`getHeaderControlsActorSheetV2`, (app, controls) => {
if (!game.settings.get(`ripcrypt`, `devMode`)) { return }
controls.push(
{
icon: `fa-solid fa-terminal`,
label: `Embed New Item (DEV)`,
action: `createItem`,
},
{
label: `Make Global Reference`,
onClick: () => {
globalThis._doc = app.actor;
},
},
);
});

View file

@ -1,4 +1,4 @@
import { Logger } from "../utils/Logger.mjs"; import { Logger } from "../../module/utils/Logger.mjs";
const loaders = { const loaders = {
svg(data) { svg(data) {

3
dev/main.mjs Normal file
View file

@ -0,0 +1,3 @@
// Hooks
import "./hooks/hotReload.mjs";
import "./hooks/getHeaderControlsActorSheetV2.mjs";

View file

@ -74,7 +74,16 @@ export default [
"@stylistic/space-infix-ops": `warn`, "@stylistic/space-infix-ops": `warn`,
"@stylistic/eol-last": `warn`, "@stylistic/eol-last": `warn`,
"@stylistic/operator-linebreak": [`warn`, `before`], "@stylistic/operator-linebreak": [`warn`, `before`],
"@stylistic/indent": [`warn`, `tab`], "@stylistic/indent": [
`warn`,
`tab`,
{
SwitchCase: 1,
ignoredNodes: [
`> .superClass`,
],
},
],
"@stylistic/brace-style": [`warn`, `stroustrup`, { "allowSingleLine": true }], "@stylistic/brace-style": [`warn`, `stroustrup`, { "allowSingleLine": true }],
"@stylistic/quotes": [`warn`, `backtick`, { "avoidEscape": true }], "@stylistic/quotes": [`warn`, `backtick`, { "avoidEscape": true }],
"@stylistic/comma-dangle": [`warn`, { arrays: `always-multiline`, objects: `always-multiline`, imports: `always-multiline`, exports: `always-multiline`, functions: `always-multiline` }], "@stylistic/comma-dangle": [`warn`, { arrays: `always-multiline`, objects: `always-multiline`, imports: `always-multiline`, exports: `always-multiline`, functions: `always-multiline` }],

View file

@ -12,6 +12,7 @@
}, },
"include": [ "include": [
"module/**/*", "module/**/*",
"dev/**/*",
"foundry/client/client.mjs", "foundry/client/client.mjs",
"foundry/client/global.d.mts", "foundry/client/global.d.mts",
"foundry/common/primitives/global.d.mts" "foundry/common/primitives/global.d.mts"

View file

@ -11,6 +11,7 @@
"good": "Good", "good": "Good",
"shield": "Shield", "shield": "Shield",
"skill": "Skill", "skill": "Skill",
"trait": "Trait",
"weapon": "Weapon" "weapon": "Weapon"
} }
}, },
@ -173,6 +174,7 @@
"guts-value-readonly": "The current amount of guts the character has", "guts-value-readonly": "The current amount of guts the character has",
"guts-max-readonly": "The maximum amount of guts the character can have" "guts-max-readonly": "The maximum amount of guts the character can have"
}, },
"edit-description": "Edit Description",
"traits-placeholder": "New Trait...", "traits-placeholder": "New Trait...",
"short-range": "Short @RipCrypt.common.range", "short-range": "Short @RipCrypt.common.range",
"long-range": "Long @RipCrypt.common.range", "long-range": "Long @RipCrypt.common.range",
@ -201,7 +203,8 @@
"invalid-socket": "Invalid socket data received, this means a module or system bug is present.", "invalid-socket": "Invalid socket data received, this means a module or system bug is present.",
"unknown-socket-event": "An unknown socket event was received: {event}", "unknown-socket-event": "An unknown socket event was received: {event}",
"no-active-gm": "No active @USER.GM is logged in, you must wait for a @USER.GM to be active before you can do that.", "no-active-gm": "No active @USER.GM is logged in, you must wait for a @USER.GM to be active before you can do that.",
"malformed-socket-payload": "Socket event \"{event}\" received with malformed payload. Details: {details}" "malformed-socket-payload": "Socket event \"{event}\" received with malformed payload. Details: {details}",
"invalid-parent-document": "Cannot create an item with type \"{itemType}\" on a parent document of type \"{parentType}\""
}, },
"warn": { "warn": {
"cannot-go-negative": "\"{name}\" is unable to be a negative number." "cannot-go-negative": "\"{name}\" is unable to be a negative number."

View file

@ -0,0 +1,217 @@
import { filePath } from "../../consts.mjs";
import { GenericAppMixin } from "../mixins/GenericApp.mjs";
import { LaidOutAppMixin } from "../mixins/LaidOutAppMixin.mjs";
import { localizer } from "../../utils/Localizer.mjs";
import { editItemFromElement, deleteItemFromElement } from "../utils.mjs";
import { gameTerms } from "../../gameTerms.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
const { ActorSheetV2 } = foundry.applications.sheets;
const { ContextMenu, TextEditor } = foundry.applications.ux;
export class BookGeistSheet extends
LaidOutAppMixin(
GenericAppMixin(
HandlebarsApplicationMixin(
ActorSheetV2,
))) {
// #region Options
static DEFAULT_OPTIONS = {
classes: [
`ripcrypt--actor`,
`BookGeistSheet`,
],
position: {
width: `auto`,
height: `auto`,
},
window: {
resizable: true,
},
form: {
submitOnChange: true,
closeOnSubmite: false,
},
};
static PARTS = {
layout: {
root: true,
template: filePath(`templates/Apps/BookGeistSheet/layout.hbs`),
},
image: { template: filePath(`templates/Apps/BookGeistSheet/image.hbs`) },
header: { template: filePath(`templates/Apps/BookGeistSheet/header.hbs`) },
stats: { template: filePath(`templates/Apps/BookGeistSheet/stats.hbs`) },
items: { template: filePath(`templates/Apps/BookGeistSheet/items.hbs`) },
};
// #endregion Options
// #region Lifecycle
async _onRender() {
await super._onRender();
new ContextMenu.implementation(
this.element,
`[data-ctx-menu="item"]`,
[
{
name: localizer(`RipCrypt.common.edit`),
condition: (el) => {
const itemId = el.dataset.itemId;
return itemId !== ``;
},
callback: editItemFromElement,
},
{
name: localizer(`RipCrypt.common.delete`),
condition: (el) => {
const itemId = el.dataset.itemId;
return itemId !== ``;
},
callback: deleteItemFromElement,
},
],
{ jQuery: false, fixed: true },
);
};
// #endregion Lifecycle
// #region Data Prep
async _preparePartContext(partID, ctx) {
switch (partID) {
case `layout`: await this._prepareLayoutContext(ctx); break;
case `image`: await this._prepareImageContext(ctx); break;
case `header`: await this._prepareHeaderContext(ctx); break;
case `stats`: await this._prepareStatsContext(ctx); break;
case `items`: await this._prepareItemsContext(ctx); break;
};
return ctx;
};
async _prepareLayoutContext(ctx) {
ctx.imageVisible = true;
};
async _prepareImageContext(ctx) {
ctx.url = this.actor.img;
};
async _prepareHeaderContext(ctx) {
ctx.name = this.actor.name;
ctx.rank = this.actor.system.level.rank;
ctx.ranks = Object.values(gameTerms.Rank)
.map((value, index) => ({
value,
label: index
}));
ctx.description = await TextEditor.implementation.enrichHTML(this.actor.system.description);
};
async _prepareStatsContext(ctx) {
const system = this.actor.system;
const fate = system.fate;
if (fate) {
ctx.path = {
full: localizer(`RipCrypt.common.ordinals.${fate}.full`),
abbv: localizer(`RipCrypt.common.ordinals.${fate}.abbv`),
};
}
else {
ctx.path = {
full: null,
abbv: localizer(`RipCrypt.common.empty`),
};
};
Object.assign(ctx, system.ability);
ctx.guts = system.guts;
ctx.speed = system.speed;
};
async _prepareItemsContext(ctx) {
const armours = this.actor.system.equippedArmour;
const shield = this.actor.system.equippedShield;
let defenses = [];
for (const [location, armour] of Object.entries(armours)) {
if (!armour) { continue }
const defense = {
name: localizer(`RipCrypt.common.anatomy.${location}`),
tooltip: null,
protection: 0,
shielded: false,
};
if (armour) {
defense.armourUUID = armour.uuid;
defense.tooltip = armour.name,
defense.protection = armour.system.protection;
}
if (shield?.system.location.has(location)) {
defense.shieldUUID = shield.uuid;
defense.shielded = true;
defense.protection += shield.system.protection;
};
if (defense.protection > 0) {
defenses.push(defense);
};
};
ctx.defenses = defenses
ctx.traits = []; // Array<{name: string}>
for (const item of this.actor.items) {
switch (item.type) {
case `weapon`: {
if (!item.system.equipped) { continue };
ctx.attacks ??= [];
ctx.attacks.push(this._prepareWeapon(item));
break;
};
case `craft`: {
ctx.crafts ??= [];
ctx.crafts.push(this._prepareCraft(item));
break;
};
case `trait`: {
ctx.traits.push(this._prepareTrait(item));
break;
};
};
};
};
_prepareWeapon(weapon) {
const hasShortRange = weapon.system.range.short != null;
const hasLongRange = weapon.system.range.long != null;
const isRanged = hasShortRange || hasLongRange;
return {
uuid: weapon.uuid,
name: weapon.name,
damage: weapon.system.damage,
isRanged,
range: weapon.system.range,
};
};
_prepareCraft(craft) {
return {
uuid: craft.uuid,
name: craft.name,
};
};
_prepareTrait(trait) {
return {
uuid: trait.uuid,
name: trait.name,
description: trait.system.description,
};
};
// #endregion Data Prep
// #region Actions
// #endregion Actions
};

View file

@ -1,6 +1,6 @@
import { CraftCardV1 } from "./CraftCardV1.mjs"; import { CraftCardV1 } from "./CraftCardV1.mjs";
import { filePath } from "../../consts.mjs"; import { filePath } from "../../consts.mjs";
import { GenericAppMixin } from "../GenericApp.mjs"; import { GenericAppMixin } from "../mixins/GenericApp.mjs";
import { SkillsCardV1 } from "./SkillsCardV1.mjs"; import { SkillsCardV1 } from "./SkillsCardV1.mjs";
import { StatsCardV1 } from "./StatsCardV1.mjs"; import { StatsCardV1 } from "./StatsCardV1.mjs";

View file

@ -1,7 +1,7 @@
import { deleteItemFromElement, editItemFromElement } from "../utils.mjs"; import { deleteItemFromElement, editItemFromElement } from "../utils.mjs";
import { documentSorter, filePath } from "../../consts.mjs"; import { documentSorter, filePath } from "../../consts.mjs";
import { gameTerms } from "../../gameTerms.mjs"; import { gameTerms } from "../../gameTerms.mjs";
import { GenericAppMixin } from "../GenericApp.mjs"; import { GenericAppMixin } from "../mixins/GenericApp.mjs";
import { localizer } from "../../utils/Localizer.mjs"; import { localizer } from "../../utils/Localizer.mjs";
import { Logger } from "../../utils/Logger.mjs"; import { Logger } from "../../utils/Logger.mjs";

View file

@ -2,7 +2,7 @@ import { deleteItemFromElement, editItemFromElement } from "../utils.mjs";
import { documentSorter, filePath } from "../../consts.mjs"; import { documentSorter, filePath } from "../../consts.mjs";
import { AmmoTracker } from "../popovers/AmmoTracker.mjs"; import { AmmoTracker } from "../popovers/AmmoTracker.mjs";
import { gameTerms } from "../../gameTerms.mjs"; import { gameTerms } from "../../gameTerms.mjs";
import { GenericAppMixin } from "../GenericApp.mjs"; import { GenericAppMixin } from "../mixins/GenericApp.mjs";
import { ItemFlags } from "../../flags/item.mjs"; import { ItemFlags } from "../../flags/item.mjs";
import { localizer } from "../../utils/Localizer.mjs"; import { localizer } from "../../utils/Localizer.mjs";
import { Logger } from "../../utils/Logger.mjs"; import { Logger } from "../../utils/Logger.mjs";

View file

@ -2,7 +2,7 @@ import { deleteItemFromElement, editItemFromElement } from "../utils.mjs";
import { DelveDiceHUD } from "../DelveDiceHUD.mjs"; import { DelveDiceHUD } from "../DelveDiceHUD.mjs";
import { filePath } from "../../consts.mjs"; import { filePath } from "../../consts.mjs";
import { gameTerms } from "../../gameTerms.mjs"; import { gameTerms } from "../../gameTerms.mjs";
import { GenericAppMixin } from "../GenericApp.mjs"; import { GenericAppMixin } from "../mixins/GenericApp.mjs";
import { localizer } from "../../utils/Localizer.mjs"; import { localizer } from "../../utils/Localizer.mjs";
import { Logger } from "../../utils/Logger.mjs"; import { Logger } from "../../utils/Logger.mjs";

View file

@ -143,7 +143,6 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) {
}; };
}; };
Logger.log(`${partId} Context`, ctx);
return ctx; return ctx;
}; };

View file

@ -1,5 +1,5 @@
import { filePath } from "../consts.mjs"; import { filePath } from "../consts.mjs";
import { GenericAppMixin } from "./GenericApp.mjs"; import { GenericAppMixin } from "./mixins/GenericApp.mjs";
import { localizer } from "../utils/Localizer.mjs"; import { localizer } from "../utils/Localizer.mjs";
import { Logger } from "../utils/Logger.mjs"; import { Logger } from "../utils/Logger.mjs";

View file

@ -1,5 +1,5 @@
import { filePath } from "../../consts.mjs"; import { filePath } from "../../consts.mjs";
import { GenericAppMixin } from "../GenericApp.mjs"; import { GenericAppMixin } from "../mixins/GenericApp.mjs";
import { Logger } from "../../utils/Logger.mjs"; import { Logger } from "../../utils/Logger.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api; const { HandlebarsApplicationMixin } = foundry.applications.api;

View file

@ -1,6 +1,6 @@
import { filePath } from "../../consts.mjs"; import { filePath } from "../../consts.mjs";
import { gameTerms } from "../../gameTerms.mjs"; import { gameTerms } from "../../gameTerms.mjs";
import { GenericAppMixin } from "../GenericApp.mjs"; import { GenericAppMixin } from "../mixins/GenericApp.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api; const { HandlebarsApplicationMixin } = foundry.applications.api;
const { ItemSheetV2 } = foundry.applications.sheets; const { ItemSheetV2 } = foundry.applications.sheets;

View file

@ -0,0 +1,53 @@
import { filePath } from "../../consts.mjs";
import { GenericAppMixin } from "../mixins/GenericApp.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
const { ItemSheetV2 } = foundry.applications.sheets;
export class TraitSheet extends GenericAppMixin(HandlebarsApplicationMixin(ItemSheetV2)) {
// #region Options
static DEFAULT_OPTIONS = {
classes: [
`ripcrypt--item`,
`TraitSheet`,
],
position: {
width: `auto`,
height: `auto`,
},
window: {
resizable: true,
},
form: {
submitOnChange: true,
closeOnSubmit: false,
},
};
static PARTS = {
content: {
template: filePath(`templates/Apps/TraitSheet/content.hbs`),
root: true,
},
};
// #endregion Options
// #region Data Prep
async _prepareContext() {
const TextEditor = foundry.applications.ux.TextEditor.implementation;
const ctx = {
meta: {
idp: this.id,
},
item: this.document,
enriched: {
system: {
description: await TextEditor.enrichHTML(this.document.system.description),
},
},
};
return ctx;
};
// #endregion Data Prep
};

View file

@ -1,7 +1,7 @@
import { createItemFromElement, deleteItemFromElement, editItemFromElement, updateForeignDocumentFromEvent } from "./utils.mjs"; import { createItemFromElement, deleteItemFromElement, editItemFromElement, updateForeignDocumentFromEvent } from "../utils.mjs";
import { DicePool } from "./DicePool.mjs"; import { DicePool } from "../DicePool.mjs";
import { RichEditor } from "./RichEditor.mjs"; import { RichEditor } from "../RichEditor.mjs";
import { toBoolean } from "../consts.mjs"; import { toBoolean } from "../../consts.mjs";
/** /**
* A mixin that takes the class from HandlebarsApplicationMixin and combines it * A mixin that takes the class from HandlebarsApplicationMixin and combines it
@ -37,6 +37,8 @@ export function GenericAppMixin(HandlebarsApp) {
_popoverManagers = new Map(); _popoverManagers = new Map();
/** @type {Map<number, string>} */ /** @type {Map<number, string>} */
_hookIDs = new Map(); _hookIDs = new Map();
/** @type {string | null} */
#focus = null;
// #endregion // #endregion
// #region Lifecycle // #region Lifecycle
@ -53,6 +55,26 @@ export function GenericAppMixin(HandlebarsApp) {
}; };
}; };
/**
* @override
* This method overrides Foundry's default behaviour for caching the focused
* element so that it actually works when the application has a root partial
*/
async _preRender(...args) {
if (this.rendered) {
const target = this.element.querySelector(`:focus`);
if (target) {
if (target.id) {
this.#focus = `#${CSS.escape(target.id)}`;
}
else if (target.name) {
this.#focus = `${target.tagName}[name="${target.name}"]`;
};
};
};
return super._preRender(...args);
};
/** @override */ /** @override */
async _onRender(...args) { async _onRender(...args) {
await super._onRender(...args); await super._onRender(...args);
@ -83,19 +105,30 @@ export function GenericAppMixin(HandlebarsApp) {
}); });
}; };
async _preparePartContext(partId, ctx, opts) { /**
ctx = await super._preparePartContext(partId, ctx, opts); * @override
delete ctx.document; * This method overrides Foundry's default behaviour for caching the focused
delete ctx.fields; * element so that it actually works when the application has a root partial
*/
async _postRender(...args) {
if (this.rendered) {
const target = this.element.querySelector(this.#focus);
target?.focus();
};
this.#focus = null;
return super._postRender(...args);
};
async _prepareContext(_options) {
const ctx = {};
ctx.meta ??= {}; ctx.meta ??= {};
ctx.meta.idp = this.document?.uuid ?? this.id; ctx.meta.idp = this.id;
if (this.document) { if (this.document) {
ctx.meta.limited = this.document.limited; ctx.meta.limited = this.document.limited;
ctx.meta.editable = this.isEditable || game.user.isGM; ctx.meta.editable = this.isEditable || game.user.isGM;
ctx.meta.embedded = this.document.isEmbedded; ctx.meta.embedded = this.document.isEmbedded;
}; };
delete ctx.editable;
return ctx; return ctx;
}; };

View file

@ -0,0 +1,56 @@
/**
* This mixin makes it so that we can provide a specific layout template without
* needing to reference each of the inner areas via a partial embedded in the root,
* enabling partial re-renders for parts of the sheet without losing advanced
* layout capabilities.
*
* @param {ReturnType<HandlebarsApp>} HandlebarsApp The mixin'd class from HAM to further mix
*/
export function LaidOutAppMixin(HandlebarsApp) {
class LaidOutApp extends HandlebarsApp {
#partDescriptors;
/**
* This caches the part descriptors into this class of the heirarchy, because
* Foundry doesn't expose the partDescriptors from the HAM directly, so we
* inject a heirarchy call so that we can nab the pointer that Foundry has
* in the HAM so that we can also read/write it from this class.
*/
_configureRenderParts(options) {
const parts = super._configureRenderParts(options);
this.#partDescriptors = parts;
return parts;
};
/**
* @override
* This is essentially Foundry's HandlebarsApplicationMixin implementation,
* however if an existing part for non-root elements don't get concatenated
* into the DOM.
*/
_replaceHTML(result, content, options) {
const partInfo = this.#partDescriptors;
for ( const [partId, htmlElement] of Object.entries(result) ) {
const part = partInfo[partId];
const priorElement = part.root ? content : content.querySelector(`[data-application-part="${partId}"]`);
const state = {};
if ( priorElement ) {
this._preSyncPartState(partId, htmlElement, priorElement, state);
if ( part.root ) {
priorElement.replaceChildren(...htmlElement.children);
}
else {
priorElement.replaceWith(htmlElement);
}
this._syncPartState(partId, htmlElement, priorElement, state);
}
else {
continue;
};
this._attachPartListeners(partId, htmlElement, options);
this.parts[partId] = htmlElement;
}
};
};
return LaidOutApp;
};

View file

@ -1,3 +1,18 @@
import { EntityData } from "./Entity.mjs"; import { EntityData } from "./Entity.mjs";
export class GeistData extends EntityData {}; const { fields } = foundry.data;
export class GeistData extends EntityData {
static defineSchema() {
const schema = super.defineSchema();
schema.description = new fields.HTMLField({
blank: true,
nullable: true,
trim: true,
initial: null,
});
return schema;
};
};

View file

@ -2,19 +2,6 @@ import { CommonItemData } from "./Common.mjs";
import { gameTerms } from "../../gameTerms.mjs"; import { gameTerms } from "../../gameTerms.mjs";
export class AmmoData extends CommonItemData { export class AmmoData extends CommonItemData {
// MARK: Base Data
prepareBaseData() {
super.prepareBaseData();
};
// MARK: Derived Data
prepareDerivedData() {
super.prepareDerivedData();
};
// #region Getters
// #endregion
// #region Sheet Data // #region Sheet Data
getFormFields(_ctx) { getFormFields(_ctx) {
const fields = [ const fields = [

View file

@ -4,7 +4,7 @@ import { gameTerms } from "../../gameTerms.mjs";
const { fields } = foundry.data; const { fields } = foundry.data;
export class CommonItemData extends foundry.abstract.TypeDataModel { export class CommonItemData extends foundry.abstract.TypeDataModel {
// MARK: Schema // #region Schema
static defineSchema() { static defineSchema() {
return { return {
quantity: requiredInteger({ min: 0, initial: 1 }), quantity: requiredInteger({ min: 0, initial: 1 }),
@ -21,14 +21,5 @@ export class CommonItemData extends foundry.abstract.TypeDataModel {
}), }),
}; };
}; };
// #endregion Schema
// MARK: Base Data
prepareBaseData() {
super.prepareBaseData();
};
// MARK: Derived Data
prepareDerivedData() {
super.prepareDerivedData();
};
}; };

View file

@ -21,19 +21,6 @@ export class CraftData extends SkillData {
return schema; return schema;
}; };
// MARK: Base Data
prepareBaseData() {
super.prepareBaseData();
};
// MARK: Derived Data
prepareDerivedData() {
super.prepareDerivedData();
};
// #region Getters
// #endregion
// #region Sheet Data // #region Sheet Data
async getFormFields(_ctx) { async getFormFields(_ctx) {
const fields = [ const fields = [

View file

@ -17,19 +17,6 @@ export class GoodData extends CommonItemData {
return schema; return schema;
}; };
// MARK: Base Data
prepareBaseData() {
super.prepareBaseData();
};
// MARK: Derived Data
prepareDerivedData() {
super.prepareDerivedData();
};
// #region Getters
// #endregion
// #region Sheet Data // #region Sheet Data
async getFormFields(_ctx) { async getFormFields(_ctx) {
const fields = [ const fields = [

View file

@ -34,19 +34,6 @@ export class SkillData extends foundry.abstract.TypeDataModel {
return schema; return schema;
}; };
// MARK: Base Data
prepareBaseData() {
super.prepareBaseData();
};
// MARK: Derived Data
prepareDerivedData() {
super.prepareDerivedData();
};
// #region Getters
// #endregion
// #region Sheet Data // #region Sheet Data
async getFormFields(_ctx) { async getFormFields(_ctx) {
const fields = [ const fields = [

View file

@ -0,0 +1,25 @@
import { localizer } from "../../utils/Localizer.mjs";
const { fields } = foundry.data;
export class TraitData extends foundry.abstract.TypeDataModel {
// #region Schema
static defineSchema() {
return {
description: new fields.HTMLField({ blank: true, nullable: false, trim: true }),
};
};
// #endregion Schema
// #region Lifecycle
async _preCreate() {
if (this.parent.isEmbedded && this.parent.parent.type !== `geist`) {
ui.notifications.error(localizer(
`RipCrypt.notifs.error.invalid-parent-document`,
{ itemType: `trait`, parentType: this.parent.parent.type },
));
return false;
};
};
// #endregion
};

View file

@ -49,10 +49,13 @@ export class WeaponData extends CommonItemData {
async _preCreate(item, options) { async _preCreate(item, options) {
const showEquipPrompt = options.showEquipPrompt ?? true; const showEquipPrompt = options.showEquipPrompt ?? true;
if (showEquipPrompt && this.parent.isEmbedded && this._canEquip()) { if (showEquipPrompt && this.parent.isEmbedded && this._canEquip()) {
const shouldEquip = await DialogV2.confirm({ let shouldEquip = this.parent.parent.type === `geist`;
shouldEquip ||= await DialogV2.confirm({
window: { title: `Equip Item?` }, window: { title: `Equip Item?` },
content: `Do you want to equip ${item.name}?`, content: `Do you want to equip ${item.name}?`,
}); });
if (shouldEquip) { if (shouldEquip) {
this.updateSource({ "equipped": true }); this.updateSource({ "equipped": true });
}; };

View file

@ -9,5 +9,6 @@ export default {
"rc-options": options, "rc-options": options,
// #region Simple // #region Simple
"rc-ifOut": (v) => (v || ``),
"rc-empty-state": (v) => v ?? localizer(`RipCrypt.common.empty`), "rc-empty-state": (v) => v ?? localizer(`RipCrypt.common.empty`),
}; };

View file

@ -1,6 +1,7 @@
// Applications // Applications
import { AllItemSheetV1 } from "../Apps/ItemSheets/AllItemSheetV1.mjs"; import { AllItemSheetV1 } from "../Apps/ItemSheets/AllItemSheetV1.mjs";
import { ArmourSheet } from "../Apps/ItemSheets/ArmourSheet.mjs"; import { ArmourSheet } from "../Apps/ItemSheets/ArmourSheet.mjs";
import { BookGeistSheet } from "../Apps/ActorSheets/BookGeistSheet.mjs";
import { CombinedHeroSheet } from "../Apps/ActorSheets/CombinedHeroSheet.mjs"; import { CombinedHeroSheet } from "../Apps/ActorSheets/CombinedHeroSheet.mjs";
import { CraftCardV1 } from "../Apps/ActorSheets/CraftCardV1.mjs"; import { CraftCardV1 } from "../Apps/ActorSheets/CraftCardV1.mjs";
import { DelveDiceHUD } from "../Apps/DelveDiceHUD.mjs"; import { DelveDiceHUD } from "../Apps/DelveDiceHUD.mjs";
@ -17,6 +18,7 @@ import { GoodData } from "../data/Item/Good.mjs";
import { HeroData } from "../data/Actor/Hero.mjs"; import { HeroData } from "../data/Actor/Hero.mjs";
import { ShieldData } from "../data/Item/Shield.mjs"; import { ShieldData } from "../data/Item/Shield.mjs";
import { SkillData } from "../data/Item/Skill.mjs"; import { SkillData } from "../data/Item/Skill.mjs";
import { TraitData } from "../data/Item/Trait.mjs";
import { WeaponData } from "../data/Item/Weapon.mjs"; import { WeaponData } from "../data/Item/Weapon.mjs";
// Class Overrides // Class Overrides
@ -37,6 +39,7 @@ import { registerMetaSettings } from "../settings/metaSettings.mjs";
import { registerSockets } from "../sockets/_index.mjs"; import { registerSockets } from "../sockets/_index.mjs";
import { registerUserSettings } from "../settings/userSettings.mjs"; import { registerUserSettings } from "../settings/userSettings.mjs";
import { registerWorldSettings } from "../settings/worldSettings.mjs"; import { registerWorldSettings } from "../settings/worldSettings.mjs";
import { TraitSheet } from "../Apps/ItemSheets/TraitSheet.mjs";
const { Items, Actors } = foundry.documents.collections; const { Items, Actors } = foundry.documents.collections;
@ -62,6 +65,7 @@ Hooks.once(`init`, () => {
CONFIG.Item.dataModels.good = GoodData; CONFIG.Item.dataModels.good = GoodData;
CONFIG.Item.dataModels.shield = ShieldData; CONFIG.Item.dataModels.shield = ShieldData;
CONFIG.Item.dataModels.skill = SkillData; CONFIG.Item.dataModels.skill = SkillData;
CONFIG.Item.dataModels.trait = TraitData;
CONFIG.Item.dataModels.weapon = WeaponData; CONFIG.Item.dataModels.weapon = WeaponData;
// #endregion // #endregion
@ -87,22 +91,21 @@ Hooks.once(`init`, () => {
label: `RipCrypt.sheet-names.StatsCardV1`, label: `RipCrypt.sheet-names.StatsCardV1`,
themes: StatsCardV1.themes, themes: StatsCardV1.themes,
}); });
Actors.registerSheet(game.system.id, StatsCardV1, {
makeDefault: true,
types: [`geist`],
label: `RipCrypt.sheet-names.StatsCardV1`,
themes: StatsCardV1.themes,
});
Actors.registerSheet(game.system.id, SkillsCardV1, { Actors.registerSheet(game.system.id, SkillsCardV1, {
types: [`hero`, `geist`], types: [`hero`],
label: `RipCrypt.sheet-names.SkillsCardV1`, label: `RipCrypt.sheet-names.SkillsCardV1`,
themes: SkillsCardV1.themes, themes: SkillsCardV1.themes,
}); });
Actors.registerSheet(game.system.id, CraftCardV1, { Actors.registerSheet(game.system.id, CraftCardV1, {
types: [`hero`, `geist`], types: [`hero`],
label: `RipCrypt.sheet-names.CraftCardV1`, label: `RipCrypt.sheet-names.CraftCardV1`,
themes: CraftCardV1.themes, themes: CraftCardV1.themes,
}); });
Actors.registerSheet(game.system.id, BookGeistSheet, {
types: [`geist`],
label: `RipCrypt.sheet-names.BookGeistSheet`,
themes: BookGeistSheet.themes,
});
// #endregion // #endregion
// #region Items // #region Items
@ -118,8 +121,15 @@ Hooks.once(`init`, () => {
label: `RipCrypt.sheet-names.ArmourSheet`, label: `RipCrypt.sheet-names.ArmourSheet`,
themes: ArmourSheet.themes, themes: ArmourSheet.themes,
}); });
Items.registerSheet(game.system.id, TraitSheet, {
makeDefault: true,
types: [`trait`],
label: `RipCrypt.sheet-names.TraitSheet`,
themes: TraitSheet.themes,
});
Items.unregisterSheet(game.system.id, AllItemSheetV1, { Items.unregisterSheet(game.system.id, AllItemSheetV1, {
types: [`armour`, `shield`], types: [`armour`, `shield`, `trait`],
}); });
// #endregion // #endregion
// #endregion // #endregion

View file

@ -1,7 +1,6 @@
// Hooks // Hooks
import "./hooks/init.mjs"; import "./hooks/init.mjs";
import "./hooks/ready.mjs"; import "./hooks/ready.mjs";
import "./hooks/hotReload.mjs";
// Global API // Global API
import "./api.mjs"; import "./api.mjs";

View file

@ -0,0 +1,34 @@
/*
The intent of this script is to do all of the modifications of the
manifest file that we need to do in order to release the system. This
can include removing dev-only fields/attributes that end users will
never, and should never, care about nor need.
*/
import { readFile, writeFile } from "fs/promises";
const MANIFEST_PATH = `system.json`;
let manifest;
try {
manifest = JSON.parse(await readFile(MANIFEST_PATH, `utf-8`));
} catch {
console.error(`Failed to parse manifest file.`);
process.exit(1);
};
// Filter out dev-only resources
if (manifest.esmodules) {
manifest.esmodules = manifest.esmodules.filter(
filepath => !filepath.startsWith(`dev/`)
);
};
// Remove dev flags
delete manifest.flags?.hotReload;
if (Object.keys(manifest.flags).length === 0) {
delete manifest.flags;
};
await writeFile(MANIFEST_PATH, JSON.stringify(manifest, undefined, `\t`));

View file

@ -12,7 +12,8 @@
{ "name": "Oliver" } { "name": "Oliver" }
], ],
"esmodules": [ "esmodules": [
"module/main.mjs" "module/main.mjs",
"dev/main.mjs"
], ],
"styles": [ "styles": [
{ {
@ -36,7 +37,7 @@
"flags": { "flags": {
"hotReload": { "hotReload": {
"extensions": ["css", "hbs", "json", "mjs", "svg"], "extensions": ["css", "hbs", "json", "mjs", "svg"],
"paths": ["assets", "templates", "langs", "module"] "paths": ["assets", "templates", "langs", "module", "dev"]
} }
}, },
"documentTypes": { "documentTypes": {
@ -48,9 +49,22 @@
"ammo": {}, "ammo": {},
"armour": {}, "armour": {},
"craft": {}, "craft": {},
"good": {}, "good": {
"htmlFields": [
"description"
]
},
"shield": {}, "shield": {},
"skill": {}, "skill": {
"htmlFields": [
"description"
]
},
"trait": {
"htmlFields": [
"description"
]
},
"weapon": {} "weapon": {}
} }
} }

View file

@ -0,0 +1,24 @@
<div>
<div class="overview">
<input
type="text"
name="name"
value="{{name}}"
>
<div class="grow"></div>
<label for="{{meta-idp}}-rank">
Rank
</label>
<select
id="{{meta.idp}}-rank"
name="system.level.rank"
>
{{rc-options rank ranks}}
</select>
</div>
{{#if description}}
<div class="description">
{{{description}}}
</div>
{{/if}}
</div>

View file

@ -0,0 +1,8 @@
<div class="img-wrapper">
<img
src="{{url}}"
alt=""
data-action="editImage"
data-edit="img"
>
</div>

View file

@ -0,0 +1,78 @@
<div class="items">
{{#if attacks}}
<div>Attacks</div>
<div>
{{#each attacks as |attack|}}
<div
class="attack"
data-ctx-menu="item"
data-item-id="{{attack.uuid}}"
>
{{attack.name}}
{{attack.damage}}
{{#if attack.isRanged}}
<span class="range">
({{attack.range.short}} / {{attack.range.long}})
</span>
{{/if}}
</div>
{{/each}}
</div>
{{/if}}
{{#if crafts}}
<div>Craft</div>
<div>
{{#each crafts as |craft|}}
<div
class="craft"
data-ctx-menu="item"
data-item-id="{{craft.uuid}}"
>
{{craft.name}}
</div>
{{/each}}
</div>
{{/if}}
<div>Defense</div>
<ul>
{{#each defenses as |defense|}}
<li
class="defense"
data-tooltip="{{defense.tooltip}}"
data-ctx-menu="item"
data-item-id="{{defense.armourUUID}}"
>
{{defense.name}} ({{defense.protection}}{{#if defense.shielded}},
<rc-icon
name="icons/shield/solid.v1"
var:size="14px"
var:fill="currentColor"
data-ctx-menu="item"
data-item-id="{{defense.shieldUUID}}"
/>
{{/if}})
</li>
{{/each}}
</ul>
<div>Traits</div>
<ul class="traits">
{{#each traits as |trait|}}
<li
class="trait"
data-ctx-menu="item"
data-item-id="{{trait.uuid}}"
>
{{trait.name}}
{{#if trait.description}}
<rc-icon
var:fill="currentColor"
name="icons/info-circle"
data-tooltip="{{trait.description}}"
/>
{{/if}}
</li>
{{else}}
None
{{/each}}
</ul>
</div>

View file

@ -0,0 +1,10 @@
<div>
{{#if imageVisible}}
<div data-application-part="image"></div>
{{/if}}
<div class="info">
<div data-application-part="header"></div>
<div data-application-part="stats"></div>
<div data-application-part="items"></div>
</div>
</div>

View file

@ -0,0 +1,97 @@
<div>
<table>
<thead>
<tr>
<td class="alt">Path</td>
<td>Grit</td>
<td>Gait</td>
<td>Grip</td>
<td>Glim</td>
<td class="alt">Guts</td>
<td class="alt">Move</td>
</tr>
</thead>
<tbody>
<tr>
<td
class="alt"
data-tooltip="{{path.full}}"
>
{{path.abbv}}
</td>
<td>
{{#if meta.editable}}
<input
type="number"
id="{{meta.idp}}-grit"
value="{{grit}}"
name="system.ability.grit"
>
{{else if meta.limited}}
???
{{else}}
{{grit}}
{{/if}}
</td>
<td>
{{#if meta.editable}}
<input
type="number"
id="{{meta.idp}}-gait"
value="{{gait}}"
name="system.ability.gait"
>
{{else if meta.limited}}
???
{{else}}
{{gait}}
{{/if}}
</td>
<td>
{{#if meta.editable}}
<input
type="number"
id="{{meta.idp}}-grip"
value="{{grip}}"
name="system.ability.grip"
>
{{else if meta.limited}}
???
{{else}}
{{grip}}
{{/if}}
</td>
<td>
{{#if meta.editable}}
<input
type="number"
id="{{meta.idp}}-glim"
value="{{glim}}"
name="system.ability.glim"
>
{{else if meta.limited}}
???
{{else}}
{{glim}}
{{/if}}
</td>
<td class="alt">
{{#if meta.editable}}
<input
type="number"
id="{{meta.idp}}-guts-value"
value="{{guts.value}}"
name="system.guts.value"
>
/ {{guts.max}}
{{else if meta.limited}}
??/??
{{else}}
{{guts.value}}/{{guts.max}}
{{/if}}
</td>
<td class="alt">{{speed.move}} / {{speed.run}}</td>
</tr>
</tbody>
</table>
</div>

View file

@ -0,0 +1,101 @@
.BookGeistSheet {
> .window-content {
display: flex;
flex-direction: row;
gap: 4px;
padding: 8px;
color: var(--base-text);
background: var(--base-background);
}
.info {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 4px;
}
.img-wrapper {
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
img {
width: 150px;
height: 150px;
}
}
.overview {
display: flex;
flex-direction: row;
gap: 4px;
input {
width: 50%;
}
}
table {
td {
border: 1px solid var(--accent-1);
text-align: center;
input {
width: 30px;
background: unset;
text-align: center;
}
}
thead td {
font-weight: bold;
border-top-width: 0;
&:first-of-type, &:last-of-type {
border-left-width: 0;
border-right-width: 0;
}
}
tbody tr {
td:first-of-type, td:last-of-type {
border-left-width: 0;
border-right-width: 0;
}
&:last-of-type td {
border-bottom-width: 0;
}
}
.alt {
background-color: var(--alt-row-background);
}
}
.items {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 4fr);
grid-template-rows: repeat(3, auto);
gap: 2px;
ul {
display: flex;
flex-direction: row;
gap: 4px;
list-style-type: none;
}
li {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
background-color: var(--accent-2);
border-radius: 4px;
padding: 2px 4px;
}
}
}

View file

@ -0,0 +1,21 @@
<div>
<input
type="text"
class="name"
aria-label="{{ rc-i18n "Name" }}"
name="name"
value="{{item.name}}"
{{disabled meta.limited}}
autocomplete="off"
>
<button
type="button"
data-action="openRichEditor"
data-path="system.description"
data-uuid="{{item.uuid}}"
data-collaborative="true"
>
{{ rc-i18n "RipCrypt.Apps.edit-description" }}
</button>
<div class="value">{{{ enriched.system.description }}}</div>
</div>

View file

@ -0,0 +1,35 @@
.ripcrypt.TraitSheet {
--input-underline: none;
max-width: 300px;
> .window-content {
display: flex;
flex-direction: column;
gap: 8px;
padding: 4px;
color: var(--base-text);
background: var(--base-background);
}
input {
border-radius: 4px;
padding: 2px 4px;
}
.value {
background: var(--input-background);
color: var(--input-text);
padding: 4px;
> :first-child {
margin-top: 0;
}
> :last-child {
margin-bottom: 0;
}
&:empty {
display: none;
}
}
}

View file

@ -1,3 +1,4 @@
@import url("./common.css");
@import url("./AllItemSheetV1/style.css"); @import url("./AllItemSheetV1/style.css");
@import url("./CombinedHeroSheet/style.css"); @import url("./CombinedHeroSheet/style.css");
@import url("./DelveDiceHUD/style.css"); @import url("./DelveDiceHUD/style.css");
@ -7,24 +8,8 @@
@import url("./SkillsCardV1/style.css"); @import url("./SkillsCardV1/style.css");
@import url("./RichEditor/style.css"); @import url("./RichEditor/style.css");
@import url("./ArmourSheet/style.css"); @import url("./ArmourSheet/style.css");
@import url("./TraitSheet/style.css");
@import url("./BookGeistSheet/style.css");
@import url("./popover.css"); @import url("./popover.css");
@import url("./popovers/AmmoTracker/style.css"); @import url("./popovers/AmmoTracker/style.css");
.ripcrypt {
.window-content {
flex: initial;
padding: 0;
margin: 0;
}
.StatsCardV1,
.SkillsCardV1,
.CraftCardV1 {
padding: 8px;
/* height: 270px; */
width: 680px;
--col-gap: 2px;
--row-gap: 4px;
}
}

17
templates/Apps/common.css Normal file
View file

@ -0,0 +1,17 @@
.ripcrypt {
.window-content {
flex: initial;
padding: 0;
margin: 0;
}
.StatsCardV1,
.SkillsCardV1,
.CraftCardV1 {
padding: 8px;
/* height: 270px; */
width: 680px;
--col-gap: 2px;
--row-gap: 4px;
}
}

View file

@ -2,7 +2,6 @@
Required parameters: Required parameters:
"name" : the name of the item "name" : the name of the item
"system.quantity" : the quantity of the item "system.quantity" : the quantity of the item
"meta.idp" : the ID Prefix for the application
--}} --}}
<header class="item-header"> <header class="item-header">
<div class="name-row"> <div class="name-row">

View file

@ -1,3 +0,0 @@
.ripcrypt prose-mirror * {
all: revert-layer;
}

View file

@ -26,4 +26,3 @@
@import url("../Apps/apps.css") layer(apps); @import url("../Apps/apps.css") layer(apps);
/* Exceptions */ /* Exceptions */
@import url("./elements/prose-mirror.css") layer(exceptions);