Compare commits

..

41 commits

Author SHA1 Message Date
0d42db1457 Add the Forgejo URL for the main project repo 2025-12-25 16:57:39 -07:00
180a050811 Update the verified version 2025-12-25 16:56:42 -07:00
316c0645d1 Merge branch 'main' into feature/trait-items 2025-12-25 23:46:35 +00:00
b5b022247e Use semantic lists for the other item lists 2025-12-25 16:46:07 -07:00
07cba3d409 Add a hook listener to prevent invasive tweaks from OFT during development 2025-12-25 16:45:46 -07:00
ef719d4892 Make the fate path editable from the geist sheet 2025-12-25 16:37:29 -07:00
17f2dc95a5 Localize Geist sheet text that wasn't previously localized 2025-12-25 16:29:14 -07:00
086f4d1f01 Set the default Geist sheet 2025-12-25 16:16:05 -07:00
577f01a37f Move import into the correct area 2025-12-25 16:15:37 -07:00
d8806be534 Remove redundant check in loop 2025-12-25 16:15:04 -07:00
e48c4a8e10 Correct typo 2025-12-25 16:13:51 -07:00
9f3d5636cb Remove unused helper 2025-12-25 16:13:32 -07:00
5bee99a523 Remove the menu option I have in the OFT module 2025-12-24 20:22:07 -07:00
8ea22d7eaf Remove the devMode settings and use the inDev flag instead 2025-12-24 20:21:32 -07:00
f1b23c54d8 Update the manifest to include an inDev property 2025-12-24 20:19:32 -07:00
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
48 changed files with 953 additions and 166 deletions

View file

@ -0,0 +1,16 @@
/*
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) => {
controls.push(
{
icon: `fa-solid fa-terminal`,
label: `Embed New Item (DEV)`,
action: `createItem`,
},
);
});

View file

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

View file

@ -0,0 +1,7 @@
/*
This is to prevent my tweaks module from adding invasive tweaks during development
of the system. But allowing all of the non-invasive tweaks to be used.
*/
Hooks.on(`oft.preRegisterTweak`, (tweak, invasive) => {
return !invasive;
});

4
dev/main.mjs Normal file
View file

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

View file

@ -74,7 +74,16 @@ export default [
"@stylistic/space-infix-ops": `warn`,
"@stylistic/eol-last": `warn`,
"@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/quotes": [`warn`, `backtick`, { "avoidEscape": true }],
"@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": [
"module/**/*",
"dev/**/*",
"foundry/client/client.mjs",
"foundry/client/global.d.mts",
"foundry/common/primitives/global.d.mts"

View file

@ -11,6 +11,7 @@
"good": "Good",
"shield": "Shield",
"skill": "Skill",
"trait": "Trait",
"weapon": "Weapon"
}
},
@ -57,6 +58,7 @@
"fract": "Fract",
"focus": "Focus"
},
"attacks": "Attacks",
"aura": "Aura",
"cost": "Cost",
"currency": {
@ -65,6 +67,7 @@
"copper": "Copper"
},
"damage": "Damage",
"defense": "Defense",
"delete": "Delete",
"description": "Description",
"details": "Details",
@ -173,6 +176,7 @@
"guts-value-readonly": "The current amount of guts the character has",
"guts-max-readonly": "The maximum amount of guts the character can have"
},
"edit-description": "Edit Description",
"traits-placeholder": "New Trait...",
"short-range": "Short @RipCrypt.common.range",
"long-range": "Long @RipCrypt.common.range",
@ -201,7 +205,8 @@
"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}",
"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": {
"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,
closeOnSubmit: 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;
ctx.fateOptions = Object.values(gameTerms.FatePath)
.map(value => ({ value, label: `RipCrypt.common.ordinals.${value}.abbv` }));
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}`),
armourUUID: armour.uuid,
tooltip: armour.name,
protection: armour.system.protection,
shielded: false,
};
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 { filePath } from "../../consts.mjs";
import { GenericAppMixin } from "../GenericApp.mjs";
import { GenericAppMixin } from "../mixins/GenericApp.mjs";
import { SkillsCardV1 } from "./SkillsCardV1.mjs";
import { StatsCardV1 } from "./StatsCardV1.mjs";

View file

@ -1,7 +1,7 @@
import { deleteItemFromElement, editItemFromElement } from "../utils.mjs";
import { documentSorter, filePath } from "../../consts.mjs";
import { gameTerms } from "../../gameTerms.mjs";
import { GenericAppMixin } from "../GenericApp.mjs";
import { GenericAppMixin } from "../mixins/GenericApp.mjs";
import { localizer } from "../../utils/Localizer.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 { AmmoTracker } from "../popovers/AmmoTracker.mjs";
import { gameTerms } from "../../gameTerms.mjs";
import { GenericAppMixin } from "../GenericApp.mjs";
import { GenericAppMixin } from "../mixins/GenericApp.mjs";
import { ItemFlags } from "../../flags/item.mjs";
import { localizer } from "../../utils/Localizer.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 { filePath } from "../../consts.mjs";
import { gameTerms } from "../../gameTerms.mjs";
import { GenericAppMixin } from "../GenericApp.mjs";
import { GenericAppMixin } from "../mixins/GenericApp.mjs";
import { localizer } from "../../utils/Localizer.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;
};

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import { filePath } from "../../consts.mjs";
import { gameTerms } from "../../gameTerms.mjs";
import { GenericAppMixin } from "../GenericApp.mjs";
import { GenericAppMixin } from "../mixins/GenericApp.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
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

@ -71,7 +71,7 @@ export class RipCryptIcon extends StyledShadowElement(HTMLElement) {
This is so that when we get an HMR event from Foundry we can appropriately
handle it using our logic to update the component and the icon cache.
*/
if (game.settings.get(`ripcrypt`, `devMode`)) {
if (game.system.flags.inDev) {
this.#svgHmr = Hooks.on(`${game.system.id}-hmr:svg`, (iconName, data) => {
if (this._name === iconName || this._path?.endsWith(data.path)) {
const svg = this.#parseSVG(data.content);

View file

@ -1,7 +1,7 @@
import { createItemFromElement, deleteItemFromElement, editItemFromElement, updateForeignDocumentFromEvent } from "./utils.mjs";
import { DicePool } from "./DicePool.mjs";
import { RichEditor } from "./RichEditor.mjs";
import { toBoolean } from "../consts.mjs";
import { createItemFromElement, deleteItemFromElement, editItemFromElement, updateForeignDocumentFromEvent } from "../utils.mjs";
import { DicePool } from "../DicePool.mjs";
import { RichEditor } from "../RichEditor.mjs";
import { toBoolean } from "../../consts.mjs";
/**
* A mixin that takes the class from HandlebarsApplicationMixin and combines it
@ -37,6 +37,8 @@ export function GenericAppMixin(HandlebarsApp) {
_popoverManagers = new Map();
/** @type {Map<number, string>} */
_hookIDs = new Map();
/** @type {string | null} */
#focus = null;
// #endregion
// #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 */
async _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);
delete ctx.document;
delete ctx.fields;
/**
* @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 _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.idp = this.document?.uuid ?? this.id;
ctx.meta.idp = this.id;
if (this.document) {
ctx.meta.limited = this.document.limited;
ctx.meta.editable = this.isEditable || game.user.isGM;
ctx.meta.embedded = this.document.isEmbedded;
};
delete ctx.editable;
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";
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";
export class AmmoData extends CommonItemData {
// MARK: Base Data
prepareBaseData() {
super.prepareBaseData();
};
// MARK: Derived Data
prepareDerivedData() {
super.prepareDerivedData();
};
// #region Getters
// #endregion
// #region Sheet Data
getFormFields(_ctx) {
const fields = [

View file

@ -57,7 +57,7 @@ export class ArmourData extends CommonItemData {
};
async _preUpdate(changes, options, user) {
if (options.force && game.settings.get(`ripcrypt`, `devMode`)) { return };
if (options.force && game.system.flags.inDev) { return };
// Ensure changes is a diffed object
const diff = diffObject(this.parent._source, changes);

View file

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

View file

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

View file

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

View file

@ -34,19 +34,6 @@ export class SkillData extends foundry.abstract.TypeDataModel {
return schema;
};
// MARK: Base Data
prepareBaseData() {
super.prepareBaseData();
};
// MARK: Derived Data
prepareDerivedData() {
super.prepareDerivedData();
};
// #region Getters
// #endregion
// #region Sheet Data
async getFormFields(_ctx) {
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) {
const showEquipPrompt = options.showEquipPrompt ?? true;
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?` },
content: `Do you want to equip ${item.name}?`,
});
if (shouldEquip) {
this.updateSource({ "equipped": true });
};
@ -67,7 +70,7 @@ export class WeaponData extends CommonItemData {
* @returns
*/
async _preUpdate(changes, options, user) {
if (options.force && game.settings.get(`ripcrypt`, `devMode`)) { return };
if (options.force && game.system.flags.inDev) { return };
const diff = diffObject(this.parent._source, changes);
let valid = super._preUpdate(changes, options, user);

View file

@ -1,12 +1,14 @@
// Applications
import { AllItemSheetV1 } from "../Apps/ItemSheets/AllItemSheetV1.mjs";
import { ArmourSheet } from "../Apps/ItemSheets/ArmourSheet.mjs";
import { BookGeistSheet } from "../Apps/ActorSheets/BookGeistSheet.mjs";
import { CombinedHeroSheet } from "../Apps/ActorSheets/CombinedHeroSheet.mjs";
import { CraftCardV1 } from "../Apps/ActorSheets/CraftCardV1.mjs";
import { DelveDiceHUD } from "../Apps/DelveDiceHUD.mjs";
import { RipCryptCombatTracker } from "../Apps/sidebar/CombatTracker.mjs";
import { SkillsCardV1 } from "../Apps/ActorSheets/SkillsCardV1.mjs";
import { StatsCardV1 } from "../Apps/ActorSheets/StatsCardV1.mjs";
import { TraitSheet } from "../Apps/ItemSheets/TraitSheet.mjs";
// Data Models
import { AmmoData } from "../data/Item/Ammo.mjs";
@ -17,6 +19,7 @@ import { GoodData } from "../data/Item/Good.mjs";
import { HeroData } from "../data/Actor/Hero.mjs";
import { ShieldData } from "../data/Item/Shield.mjs";
import { SkillData } from "../data/Item/Skill.mjs";
import { TraitData } from "../data/Item/Trait.mjs";
import { WeaponData } from "../data/Item/Weapon.mjs";
// Class Overrides
@ -32,7 +35,6 @@ import { RipCryptToken } from "../documents/token.mjs";
import helpers from "../handlebarHelpers/_index.mjs";
import { Logger } from "../utils/Logger.mjs";
import { registerCustomComponents } from "../Apps/components/_index.mjs";
import { registerDevSettings } from "../settings/devSettings.mjs";
import { registerMetaSettings } from "../settings/metaSettings.mjs";
import { registerSockets } from "../sockets/_index.mjs";
import { registerUserSettings } from "../settings/userSettings.mjs";
@ -48,7 +50,6 @@ Hooks.once(`init`, () => {
// #region Settings
registerMetaSettings();
registerDevSettings();
registerUserSettings();
registerWorldSettings();
// #endregion
@ -62,6 +63,7 @@ Hooks.once(`init`, () => {
CONFIG.Item.dataModels.good = GoodData;
CONFIG.Item.dataModels.shield = ShieldData;
CONFIG.Item.dataModels.skill = SkillData;
CONFIG.Item.dataModels.trait = TraitData;
CONFIG.Item.dataModels.weapon = WeaponData;
// #endregion
@ -87,22 +89,22 @@ Hooks.once(`init`, () => {
label: `RipCrypt.sheet-names.StatsCardV1`,
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, {
types: [`hero`, `geist`],
types: [`hero`],
label: `RipCrypt.sheet-names.SkillsCardV1`,
themes: SkillsCardV1.themes,
});
Actors.registerSheet(game.system.id, CraftCardV1, {
types: [`hero`, `geist`],
types: [`hero`],
label: `RipCrypt.sheet-names.CraftCardV1`,
themes: CraftCardV1.themes,
});
Actors.registerSheet(game.system.id, BookGeistSheet, {
makeDefault: true,
types: [`geist`],
label: `RipCrypt.sheet-names.BookGeistSheet`,
themes: BookGeistSheet.themes,
});
// #endregion
// #region Items
@ -118,8 +120,15 @@ Hooks.once(`init`, () => {
label: `RipCrypt.sheet-names.ArmourSheet`,
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, {
types: [`armour`, `shield`],
types: [`armour`, `shield`, `trait`],
});
// #endregion
// #endregion

View file

@ -3,23 +3,6 @@ import { Logger } from "../utils/Logger.mjs";
Hooks.once(`ready`, () => {
Logger.log(`Ready`);
let defaultTab = game.settings.get(`ripcrypt`, `defaultTab`);
if (defaultTab) {
try {
Logger.debug(`Switching sidebar tab to:`, defaultTab);
ui.sidebar.changeTab(defaultTab, `primary`);
}
catch {
Logger.error(`Failed to change to sidebar tab:`, defaultTab);
};
};
if (game.settings.get(`ripcrypt`, `devMode`)) {
ui.sidebar.expand();
if (game.paused) { game.togglePause(false, { broadcast: true }) };
};
ui.delveDice.render({ force: true });
// MARK: 1-time updates

View file

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

View file

@ -1,17 +0,0 @@
export function registerDevSettings() {
game.settings.register(`ripcrypt`, `devMode`, {
scope: `client`,
type: Boolean,
config: false,
default: false,
requiresReload: false,
});
game.settings.register(`ripcrypt`, `defaultTab`, {
name: `Default Tab`,
scope: `client`,
type: String,
config: game.settings.get(`ripcrypt`, `devMode`),
requiresReload: false,
});
};

View file

@ -0,0 +1,35 @@
/*
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?.inDev;
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

@ -5,14 +5,15 @@
"version": "0.2.0",
"compatibility": {
"minimum": 13,
"verified": "13.350",
"verified": "13.351",
"maximum": 13
},
"authors": [
{ "name": "Oliver" }
],
"esmodules": [
"module/main.mjs"
"module/main.mjs",
"dev/main.mjs"
],
"styles": [
{
@ -27,16 +28,17 @@
"path": "langs/en-ca.json"
}
],
"url": "https://github.com/Eldritch-Oliver/Foundry-RipCrypt",
"manifest": "https://github.com/Eldritch-Oliver/Foundry-RipCrypt/releases/latest/download/module.json",
"download": "#{DOWNLOAD}#",
"url": "https://git.varify.ca/Foundry/ripcrypt",
"manifest": "",
"download": "",
"readme": "README.md",
"bugs": "",
"socket": true,
"flags": {
"inDev": true,
"hotReload": {
"extensions": ["css", "hbs", "json", "mjs", "svg"],
"paths": ["assets", "templates", "langs", "module"]
"paths": ["assets", "templates", "langs", "module", "dev"]
}
},
"documentTypes": {
@ -48,9 +50,22 @@
"ammo": {},
"armour": {},
"craft": {},
"good": {},
"good": {
"htmlFields": [
"description"
]
},
"shield": {},
"skill": {},
"skill": {
"htmlFields": [
"description"
]
},
"trait": {
"htmlFields": [
"description"
]
},
"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">
{{ rc-i18n "RipCrypt.common.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>{{ rc-i18n "RipCrypt.common.attacks" }}</div>
<ul>
{{#each attacks as |attack|}}
<li
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}}
</li>
{{/each}}
</ul>
{{/if}}
{{#if crafts}}
<div>{{ rc-i18n "RipCrypt.common.glimcraft" }}</div>
<ul>
{{#each crafts as |craft|}}
<li
class="craft"
data-ctx-menu="item"
data-item-id="{{craft.uuid}}"
>
{{craft.name}}
</li>
{{/each}}
</ul>
{{/if}}
<div>{{ rc-i18n "RipCrypt.common.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>{{ rc-i18n "RipCrypt.common.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}}
{{ rc-i18n "RipCrypt.common.empty" }}
{{/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,106 @@
<div>
<table>
<thead>
<tr>
<td class="alt">{{ rc-i18n "RipCrypt.common.fate" }}</td>
<td>{{ rc-i18n "RipCrypt.common.abilities.grit" }}</td>
<td>{{ rc-i18n "RipCrypt.common.abilities.gait" }}</td>
<td>{{ rc-i18n "RipCrypt.common.abilities.grip" }}</td>
<td>{{ rc-i18n "RipCrypt.common.abilities.glim" }}</td>
<td class="alt">{{ rc-i18n "RipCrypt.common.guts" }}</td>
<td class="alt">{{ rc-i18n "RipCrypt.common.move" }}</td>
</tr>
</thead>
<tbody>
<tr>
<td
class="alt"
data-tooltip="{{path.full}}"
>
{{#if meta.editable}}
<select name="system.fate">
<option value="">{{rc-i18n "RipCrypt.common.empty"}}</option>
{{ rc-options path.full fateOptions localize=true }}
</select>
{{else if meta.limited}}
???
{{else}}
{{path.abbv}}
{{/if}}
</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,102 @@
.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);
align-items: center;
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("./CombinedHeroSheet/style.css");
@import url("./DelveDiceHUD/style.css");
@ -7,24 +8,8 @@
@import url("./SkillsCardV1/style.css");
@import url("./RichEditor/style.css");
@import url("./ArmourSheet/style.css");
@import url("./TraitSheet/style.css");
@import url("./BookGeistSheet/style.css");
@import url("./popover.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:
"name" : the name of the item
"system.quantity" : the quantity of the item
"meta.idp" : the ID Prefix for the application
--}}
<header class="item-header">
<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);
/* Exceptions */
@import url("./elements/prose-mirror.css") layer(exceptions);