This commit is contained in:
Oliver 2025-10-16 04:36:00 +00:00 committed by GitHub
commit 5b12534126
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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 = {
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/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"
}
},
@ -173,6 +174,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 +203,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,
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 { 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

@ -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

@ -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 });
};

View file

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

View file

@ -1,6 +1,7 @@
// 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";
@ -17,6 +18,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
@ -37,6 +39,7 @@ import { registerMetaSettings } from "../settings/metaSettings.mjs";
import { registerSockets } from "../sockets/_index.mjs";
import { registerUserSettings } from "../settings/userSettings.mjs";
import { registerWorldSettings } from "../settings/worldSettings.mjs";
import { TraitSheet } from "../Apps/ItemSheets/TraitSheet.mjs";
const { Items, Actors } = foundry.documents.collections;
@ -62,6 +65,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 +91,21 @@ 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, {
types: [`geist`],
label: `RipCrypt.sheet-names.BookGeistSheet`,
themes: BookGeistSheet.themes,
});
// #endregion
// #region Items
@ -118,8 +121,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

@ -1,7 +1,6 @@
// Hooks
import "./hooks/init.mjs";
import "./hooks/ready.mjs";
import "./hooks/hotReload.mjs";
// Global API
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" }
],
"esmodules": [
"module/main.mjs"
"module/main.mjs",
"dev/main.mjs"
],
"styles": [
{
@ -36,7 +37,7 @@
"flags": {
"hotReload": {
"extensions": ["css", "hbs", "json", "mjs", "svg"],
"paths": ["assets", "templates", "langs", "module"]
"paths": ["assets", "templates", "langs", "module", "dev"]
}
},
"documentTypes": {
@ -48,9 +49,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">
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("./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);