diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..180dbd6 --- /dev/null +++ b/.env.template @@ -0,0 +1,2 @@ +# The absolute path to the Foundry installation to create symlinks to +FOUNDRY_ROOT="" diff --git a/.github/workflows/draft-release.yaml b/.github/workflows/draft-release.yaml index 7763240..eecafc4 100644 --- a/.github/workflows/draft-release.yaml +++ b/.github/workflows/draft-release.yaml @@ -29,20 +29,22 @@ jobs: if: ${{ steps.check-tag.outputs.exists == 'true' }} run: exit 1 - - name: Ensure there are specific files to release - if: ${{ vars.files_to_release == '' }} - run: exit 1 + - name: "Building compendia" + run: "npm run data:build" - - name: Move system.json to a temp file - id: manifest-move - run: mv system.json module.temp.json + - name: "Removing compendium source" + run: "rm -rf packs/**/_source" - - name: Update the download property in the manifest + - name: Update the manifest with the relevant properties id: manifest-update - run: cat module.temp.json | jq -r --tab '.download = "https://github.com/${{ github.repository }}/releases/download/v${{ steps.version.outputs.version }}/release.zip"' > system.json + uses: microsoft/variable-substitution@v1 + with: + files: "system.json" + env: + download: "https://github.com/${{ github.repository }}/releases/download/v${{ steps.version.outputs.version }}/release.zip" - name: Create the zip - run: zip -r release.zip ${{ vars.files_to_release }} + run: zip -r release.zip system.json packs module langs assets templates README.md - name: Create the draft release uses: ncipollo/release-action@v1 @@ -50,5 +52,6 @@ jobs: tag: "v${{ steps.version.outputs.version }}" commit: ${{ github.ref }} draft: true + body: generateReleaseNotes: true artifacts: "release.zip,system.json" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 23d5f03..e058206 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ dist/ *.link +*.txt +/foundry # Dependency directories node_modules/ @@ -11,3 +13,10 @@ jspm_packages/ .env.test.local .env.production.local .env.local + +# Ignore all of the binaries and stuff that gets built for Foundry from the raw +# JSON data because it's annoying seeing it in my git changes when it isn't actually +# needed. +/packs/**/* +!/packs/**/*/ +!/packs/**/*.json diff --git a/.vscode/ripcrypt.html-data.json b/.vscode/ripcrypt.html-data.json index 628a94a..3efb728 100644 --- a/.vscode/ripcrypt.html-data.json +++ b/.vscode/ripcrypt.html-data.json @@ -26,6 +26,20 @@ { "name": "var:stroke-width", "description": "The stroke width of the icon, must be a valid CSS unit" }, { "name": "var:stroke-linejoin", "description": "The stroke linejoin of the icon, must be a valid CSS value" } ] + }, + { + "name": "rc-border", + "description": "Creates a stylized border in the same sort of design that the published RipCrypt book uses", + "attributes": [ + { "name": "var:vertical-displacement", "description": "How much vertical displacement the title receives, defaults to 12.5px" }, + { "name": "var:padding", "description": "How much padding the border container has" }, + { "name": "var:border-color", "description": "The CSS value that is used as the colour of the border" }, + { "name": "var:padding-top", "description": "How much padding the top of the border element has, if not provided, defaults to the value of vertical displacement plus 4px" }, + { "name": "var:margin-top", "description": "How much margin the top of the border element has, if not provided, defaults to the value of vertical displacement" }, + { "name": "var:border-mask", "description": "The CSS colour used to mask out the border element, if not provided defaults to the --base-background CSS variable"}, + { "name": "var:title-height", "description": "The CSS height for the title, defaults to 20px" }, + { "name": "var:title-background", "description": "The CSS colour to make the title element, defaults to var:border-color" } + ] } ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index b8cf8a9..c60389c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,10 @@ { "files.exclude": { - "**/node_modules": true + "**/node_modules": true, + "foundry": true }, "search.exclude": { - "foundry.*.link": true + "foundry": true }, "html.customData": [ "./.vscode/foundry.html-data.json", @@ -11,5 +12,6 @@ ], "workbench.editorAssociations": { "*.svg": "default", - } + }, + "git.branchProtection": [] } \ No newline at end of file diff --git a/assets/_credit.txt b/assets/_credit.txt index 39124c6..83c76aa 100644 --- a/assets/_credit.txt +++ b/assets/_credit.txt @@ -1,6 +1,11 @@ -Oliver Akins: +Eldritch-Oliver: - geist-silhouette.v2.svg : All rights reserved. - caster-silhouette.v1.svg : All rights reserved. + - icons/star-empty.svg : Modified from https://thenounproject.com/icon/star-7711815/ by Llisole + - icons/star.svg : Modified from https://thenounproject.com/icon/star-7711815/ by Llisole + - icons/shield/checked.v1.svg : Modified from https://thenounproject.com/icon/shield-5565751/ by Corner Pixel + - icons/shield/crossed.v1.svg : Modified from https://thenounproject.com/icon/shield-5565751/ by Corner Pixel + - icons/shield/solid.v1.svg : Modified from https://thenounproject.com/icon/shield-5565751/ by Corner Pixel Kýnan Antos (Gritsilk Games): - hero-silhouette.svg : Licensed to Distribute and Modify within the bounds of the "Foundry-RipCrypt" system. @@ -11,6 +16,14 @@ ARISO: Abdulloh Fauzan: - icons/info-circle.svg (https://thenounproject.com/icon/information-4176576/) : Rights Purchased +hanifmuhammad: + - icons/plus.svg (https://thenounproject.com/icon/plus-7363257/) : Rights Purchased + +QOLBIN SALIIM: + - icons/arrow-left.svg (https://thenounproject.com/icon/arrow-1933583/) : Rights Purchased + - icons/arrow-right.svg (https://thenounproject.com/icon/arrow-1933581/) : Rights Purchased + - icons/arrow-compass.svg (https://thenounproject.com/icon/arrow-2052607/) : Rights Purchased + Soetarman Atmodjo: - icons/roll.svg (https://thenounproject.com/icon/dice-5195278/) : Rights Purchased diff --git a/assets/caster-silhouette.v1.svg b/assets/caster-silhouette.v1.svg index ed60c3e..9b53fcc 100644 --- a/assets/caster-silhouette.v1.svg +++ b/assets/caster-silhouette.v1.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/arrow-compass.svg b/assets/icons/arrow-compass.svg new file mode 100644 index 0000000..b1e8a40 --- /dev/null +++ b/assets/icons/arrow-compass.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/arrow-left.svg b/assets/icons/arrow-left.svg new file mode 100644 index 0000000..e1a347e --- /dev/null +++ b/assets/icons/arrow-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/arrow-right.svg b/assets/icons/arrow-right.svg new file mode 100644 index 0000000..a477835 --- /dev/null +++ b/assets/icons/arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/evil.svg b/assets/icons/evil.svg new file mode 100644 index 0000000..5d9fb11 --- /dev/null +++ b/assets/icons/evil.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/hero.svg b/assets/icons/hero.svg new file mode 100644 index 0000000..55de62b --- /dev/null +++ b/assets/icons/hero.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg new file mode 100644 index 0000000..5e8c131 --- /dev/null +++ b/assets/icons/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/shield/checked.v1.svg b/assets/icons/shield/checked.v1.svg new file mode 100644 index 0000000..c22ff10 --- /dev/null +++ b/assets/icons/shield/checked.v1.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/shield/crossed.v1.svg b/assets/icons/shield/crossed.v1.svg new file mode 100644 index 0000000..bfcae76 --- /dev/null +++ b/assets/icons/shield/crossed.v1.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/shield/solid.v1.svg b/assets/icons/shield/solid.v1.svg new file mode 100644 index 0000000..7f37ece --- /dev/null +++ b/assets/icons/shield/solid.v1.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/star-empty.svg b/assets/icons/star-empty.svg new file mode 100644 index 0000000..8760cc9 --- /dev/null +++ b/assets/icons/star-empty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/star.svg b/assets/icons/star.svg new file mode 100644 index 0000000..829431b --- /dev/null +++ b/assets/icons/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/augments.d.ts b/augments.d.ts new file mode 100644 index 0000000..dc091c2 --- /dev/null +++ b/augments.d.ts @@ -0,0 +1,14 @@ +declare global { + class Hooks extends foundry.helpers.Hooks {}; + const fromUuid = foundry.utils.fromUuid; +} + +interface Actor { + /** The system-specific data */ + system: any; +}; + +interface Item { + /** The system-specific data */ + system: any; +}; \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index aabd56c..4ef4c09 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,7 +4,7 @@ import stylistic from "@stylistic/eslint-plugin"; export default [ // Tell eslint to ignore files that I don't mind being formatted slightly differently - { ignores: [ `scripts/` ] }, + { ignores: [ `scripts/`, `foundry/` ] }, { languageOptions: { globals: globals.browser, @@ -22,9 +22,7 @@ export default [ Hooks: `readonly`, ui: `readonly`, Actor: `readonly`, - Actors: `readonly`, Item: `readonly`, - Items: `readonly`, foundry: `readonly`, ChatMessage: `readonly`, ActiveEffect: `readonly`, @@ -36,6 +34,7 @@ export default [ Combatant: `readonly`, canvas: `readonly`, Token: `readonly`, + Tour: `readonly`, }, }, }, @@ -76,7 +75,7 @@ export default [ "@stylistic/eol-last": `warn`, "@stylistic/operator-linebreak": [`warn`, `before`], "@stylistic/indent": [`warn`, `tab`], - "@stylistic/brace-style": [`warn`, `1tbs`, { "allowSingleLine": true }], + "@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` }], "@stylistic/comma-style": [`warn`, `last`], diff --git a/jsconfig.json b/jsconfig.json index 8b0d1fc..47b5b55 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,11 +1,19 @@ { "compilerOptions": { - "module": "ES2020", - "target": "ES2020" + "module": "es2022", + "target": "es2022", + "types": [ + "./augments.d.ts" + ], + "paths": { + "@client/*": ["./foundry/client/*"], + "@common/*": ["./foundry/common/*"], + } }, - "exclude": ["node_modules", "**/node_modules/*"], - "include": ["module/**/*", "foundry.v13.link/client/**/*.js", "foundry.v13.link/**/*.mjs"], - "typeAcquisition": { - "include": ["jquery"] - } + "include": [ + "module/**/*", + "foundry/client/client.mjs", + "foundry/client/global.d.mts", + "foundry/common/primitives/global.d.mts" + ] } \ No newline at end of file diff --git a/langs/en-ca.json b/langs/en-ca.json index ac5a6bb..a5d28cc 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -1,7 +1,8 @@ { "TYPES": { "Actor": { - "hero": "Hero" + "hero": "Hero", + "geist": "Geist" }, "Item": { "ammo": "Ammo", @@ -16,9 +17,14 @@ "RipCrypt": { "sheet-names": { "AllItemsSheetV1": "RipCrypt Item Sheet", + "ArmourSheet": "Armour Sheet", "CombinedHeroSheet": "Hero Sheet", - "HeroSummaryCardV1": "Hero Stat Card", - "HeroSkillsCardV1": "Hero Skill Card" + "StatsCardV1": "Hero Stat Card", + "CraftCardV1": "Hero Craft Card", + "SkillsCardV1": "Hero Skill Card" + }, + "app-titles": { + "AmmoTracker": "Ammo Tracker" }, "common": { "abilities": { @@ -51,6 +57,8 @@ "fract": "Fract", "focus": "Focus" }, + "aura": "Aura", + "cost": "Cost", "currency": { "gold": "Gold", "silver": "Silver", @@ -59,12 +67,15 @@ "damage": "Damage", "delete": "Delete", "description": "Description", + "details": "Details", "difficulties": { "easy": "Easy", "normal": "Normal", "tough": "Tough", - "hard": "Hard" + "hard": "Hard", + "random": "Random Condition" }, + "difficulty": "Difficulty", "drag": "Drag", "edit": "Edit", "edge": "Edge", @@ -72,15 +83,28 @@ "equipped": "Equipped", "fate": "Fate", "gear": "Gear", + "glimcraft": "Glimcraft", "glory": "Glory", "guts": "Guts", "location": "Location", "move": "Move", - "path": { - "North": "North", - "East": "East", - "South": "South", - "West": "West" + "ordinals": { + "North": { + "full": "North", + "abbv": "N" + }, + "East": { + "full": "East", + "abbv": "E" + }, + "South": { + "full": "South", + "abbv": "S" + }, + "West": { + "full": "West", + "abbv": "W" + } }, "protection": "Protection", "quantity": "Quantity", @@ -117,11 +141,29 @@ "condensedRange": { "name": "Condense Weapon Range Input", "hint": "With this enabled, the weapon range will be displayed as \"X / Y\" when editing a weapon. While disabled it will be as displayed as two different rows, one for Short Range and one for Long Range" + }, + "sandsOfFateInitial": { + "name": "Sands of Fate Initial", + "hint": "What value should The Hourglass reset to when a Cryptic Event occurs" + }, + "onCrypticEvent": { + "name": "Cryptic Event Alert", + "hint": "What happens when a cryptic event occurs by clicking the \"Next Delve Tour\" button in the HUD", + "options": { + "notif": "Notification", + "pause": "Pause Game", + "both": "Notification and Pause Game", + "nothing": "Do Nothing" + } + }, + "allowUpdateSandsSocket": { + "name": "Player Haste Updates the Sands of Fate", + "hint": "This setting determines if when a player makes a haste check that the result will automatically be applied to the global Sands of Fate. Disabling this is good if you want to let players roll without needing to worry about automation messing anything up while they spam rolls." } }, "Apps": { - "move-run": "@RipCrypt.common.move • @RipCrypt.common.run", - "traits-range": "@RipCrypt.common.traits • @RipCrypt.common.range", + "damage-reduction": "@RipCrypt.common.damage reduction", + "traits-range": "@RipCrypt.common.traits & @RipCrypt.common.range", "grit-skills": "@RipCrypt.common.abilities.grit Skills", "gait-skills": "@RipCrypt.common.abilities.gait Skills", "grip-skills": "@RipCrypt.common.abilities.grip Skills", @@ -140,19 +182,48 @@ "numberOfDice": "# of Dice", "rollTarget": "Target", "difficulty": "(DC: {dc})", - "RichEditor-no-collaborative": "Warning: This editor is not collaborative, that means that if you and someone else are editing it at the same time, you won't see that someone else is making changes until they save, and then your changes will be lost." + "RichEditor-no-collaborative": "Warning: This editor is not collaborative, that means that if you and someone else are editing it at the same time, you won't see that someone else is making changes until they save, and then your changes will be lost.", + "starred-ammo-placeholder": "Starred Ammo Slot", + "AmmoTracker": { + "no-ammo": "You don't have any ammo!", + "star-button": "Add {name} as a starred ammo", + "star-button-tooltip": "Add Star", + "unstar-button": "Remove {name} as a starred ammo", + "unstar-button-tooltip": "Remove Star" + }, + "protects-the-location": "Protects your {part}" }, "notifs": { "error": { "cannot-equip": "Cannot equip the {itemType}, see console for more details.", - "invalid-delta": "The delta for \"{name}\" is not a number, cannot finish processing the action." + "invalid-delta": "The delta for \"{name}\" is not a number, cannot finish processing the action.", + "at-favourite-limit": "Cannot favourite more than three items, unfavourite one to make space.", + "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}" }, "warn": { "cannot-go-negative": "\"{name}\" is unable to be a negative number." + }, + "info": { + "cryptic-event-alert": "A Cryptic Event Has Occured!" } }, "tooltips": { - "shield-bonus": "Shield Bonus: {value}" + "shield-bonus": "Shield Bonus: {value}", + "set-fate-to": "Set Fate to {ordinal}", + "current-tour": "Current Delve Tour", + "create-new-item": "Create new item", + "next-tour": "Next Delve Tour", + "prev-tour": "Previous Delve Tour", + "auras": { + "normal": "The distance of your aura normally", + "heavy": "The distance of your aura when using Heavycraft" + } } + }, + "USER": { + "GM": "Keeper" } } diff --git a/module/Apps/ActorSheets/CombinedHeroSheet.mjs b/module/Apps/ActorSheets/CombinedHeroSheet.mjs index 8e874cd..b81923a 100644 --- a/module/Apps/ActorSheets/CombinedHeroSheet.mjs +++ b/module/Apps/ActorSheets/CombinedHeroSheet.mjs @@ -1,9 +1,8 @@ +import { CraftCardV1 } from "./CraftCardV1.mjs"; import { filePath } from "../../consts.mjs"; import { GenericAppMixin } from "../GenericApp.mjs"; -import { HeroCraftCardV1 } from "./HeroCraftCardV1.mjs"; -import { HeroSkillsCardV1 } from "./HeroSkillsCardV1.mjs"; -import { HeroSummaryCardV1 } from "./HeroSummaryCardV1.mjs"; -import { Logger } from "../../utils/Logger.mjs"; +import { SkillsCardV1 } from "./SkillsCardV1.mjs"; +import { StatsCardV1 } from "./StatsCardV1.mjs"; const { HandlebarsApplicationMixin } = foundry.applications.api; const { ActorSheetV2 } = foundry.applications.sheets; @@ -23,7 +22,9 @@ export class CombinedHeroSheet extends GenericAppMixin(HandlebarsApplicationMixi window: { resizable: false, }, - actions: {}, + actions: { + ...StatsCardV1.DEFAULT_OPTIONS.actions, + }, form: { submitOnChange: true, closeOnSubmit: false, @@ -32,10 +33,10 @@ export class CombinedHeroSheet extends GenericAppMixin(HandlebarsApplicationMixi static PARTS = { summary: { - template: filePath(`templates/Apps/HeroSummaryCardV1/content.hbs`), + template: filePath(`templates/Apps/StatsCardV1/content.hbs`), }, skills: { - template: filePath(`templates/Apps/HeroSkillsCardV1/content.hbs`), + template: filePath(`templates/Apps/SkillsCardV1/content.hbs`), }, craft: { template: filePath(`templates/Apps/CombinedHeroSheet/crafts.hbs`), @@ -47,8 +48,8 @@ export class CombinedHeroSheet extends GenericAppMixin(HandlebarsApplicationMixi async _onRender(context, options) { await super._onRender(context, options); - const summaryElement = this.element.querySelector(`.HeroSummaryCardV1`); - HeroSummaryCardV1._onRender( + const summaryElement = this.element.querySelector(`.StatsCardV1`); + StatsCardV1._onRender( context, { ...options, @@ -57,8 +58,9 @@ export class CombinedHeroSheet extends GenericAppMixin(HandlebarsApplicationMixi }, ); - const skillsElement = this.element.querySelector(`.HeroSkillsCardV1`); - HeroSkillsCardV1._onRender.bind(this)( + const skillsElement = this.element.querySelector(`.SkillsCardV1`); + SkillsCardV1._createPopoverListeners.bind(this)(); + SkillsCardV1._onRender.bind(this)( context, { ...options, @@ -68,7 +70,7 @@ export class CombinedHeroSheet extends GenericAppMixin(HandlebarsApplicationMixi ); const craftsElement = this.element.querySelector(`.crafts-summary`); - HeroCraftCardV1._onRender.bind(this)( + CraftCardV1._onRender.bind(this)( context, { ...options, @@ -84,28 +86,27 @@ export class CombinedHeroSheet extends GenericAppMixin(HandlebarsApplicationMixi switch (partId) { case `summary`: { - ctx = await HeroSummaryCardV1.prepareGuts(ctx); - ctx = await HeroSummaryCardV1.prepareWeapons(ctx); - ctx = await HeroSummaryCardV1.prepareArmor(ctx); - ctx = await HeroSummaryCardV1.prepareFatePath(ctx); - ctx = await HeroSummaryCardV1.prepareAbilityRow(ctx); - ctx = await HeroSummaryCardV1.prepareSpeed(ctx); - ctx = await HeroSummaryCardV1.prepareLevelData(ctx); + ctx = await StatsCardV1.prepareGuts(ctx); + ctx = await StatsCardV1.prepareWeapons(ctx); + ctx = await StatsCardV1.prepareArmor(ctx); + ctx = await StatsCardV1.prepareFatePath(ctx); + ctx = await StatsCardV1.prepareAbilityRow(ctx); + ctx = await StatsCardV1.prepareSpeed(ctx); + ctx = await StatsCardV1.prepareLevelData(ctx); break; }; case `skills`: { - ctx = await HeroSkillsCardV1.prepareGear(ctx); - ctx = await HeroSkillsCardV1.prepareAmmo(ctx); - ctx = await HeroSkillsCardV1.prepareSkills(ctx); + ctx = await SkillsCardV1.prepareGear(ctx); + ctx = await SkillsCardV1.prepareAmmo(ctx); + ctx = await SkillsCardV1.prepareSkills(ctx); break; }; case `craft`: { - ctx = await HeroCraftCardV1.prepareCraft(ctx); + ctx = await CraftCardV1.prepareCraft(ctx); break; }; }; - Logger.debug(`Context keys:`, Object.keys(ctx)); return ctx; }; // #endregion diff --git a/module/Apps/ActorSheets/HeroCraftCardV1.mjs b/module/Apps/ActorSheets/CraftCardV1.mjs similarity index 83% rename from module/Apps/ActorSheets/HeroCraftCardV1.mjs rename to module/Apps/ActorSheets/CraftCardV1.mjs index 7d78029..4a650df 100644 --- a/module/Apps/ActorSheets/HeroCraftCardV1.mjs +++ b/module/Apps/ActorSheets/CraftCardV1.mjs @@ -7,15 +7,16 @@ import { Logger } from "../../utils/Logger.mjs"; const { HandlebarsApplicationMixin } = foundry.applications.api; const { ActorSheetV2 } = foundry.applications.sheets; -const { ContextMenu } = foundry.applications.ui; +const { ContextMenu } = foundry.applications.ux; +const { deepClone } = foundry.utils; -export class HeroCraftCardV1 extends GenericAppMixin(HandlebarsApplicationMixin(ActorSheetV2)) { +export class CraftCardV1 extends GenericAppMixin(HandlebarsApplicationMixin(ActorSheetV2)) { // #region Options static DEFAULT_OPTIONS = { classes: [ `ripcrypt--actor`, - `ripcrypt--HeroCraftCardV1`, + `ripcrypt--CraftCardV1`, ], position: { width: `auto`, @@ -34,7 +35,7 @@ export class HeroCraftCardV1 extends GenericAppMixin(HandlebarsApplicationMixin( static PARTS = { content: { - template: filePath(`templates/Apps/HeroCraftCardV1/content.hbs`), + template: filePath(`templates/Apps/CraftCardV1/content.hbs`), }, }; // #endregion @@ -42,7 +43,7 @@ export class HeroCraftCardV1 extends GenericAppMixin(HandlebarsApplicationMixin( // #region Lifecycle async _onRender(context, options) { await super._onRender(context, options); - HeroCraftCardV1._onRender.bind(this)(context, options); + CraftCardV1._onRender.bind(this)(context, options); }; static async _onRender(_context, options) { @@ -79,12 +80,18 @@ export class HeroCraftCardV1 extends GenericAppMixin(HandlebarsApplicationMixin( ctx = await super._preparePartContext(partId, ctx, opts); ctx.actor = this.document; - ctx = await HeroCraftCardV1.prepareCraft(ctx); + ctx = await CraftCardV1.prepareAura(ctx); + ctx = await CraftCardV1.prepareCraft(ctx); Logger.debug(`Context:`, ctx); return ctx; }; + static async prepareAura(ctx) { + ctx.aura = deepClone(ctx.actor.system.aura); + return ctx; + }; + static async prepareCraft(ctx) { ctx.craft = {}; const aspects = Object.values(gameTerms.Aspects); @@ -108,7 +115,8 @@ export class HeroCraftCardV1 extends GenericAppMixin(HandlebarsApplicationMixin( const length = crafts.length; if (length >= limit) { crafts = crafts.slice(0, limit); - } else { + } + else { crafts = crafts .concat(Array(limit - length).fill(null)) .slice(0, limit); diff --git a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs b/module/Apps/ActorSheets/SkillsCardV1.mjs similarity index 68% rename from module/Apps/ActorSheets/HeroSkillsCardV1.mjs rename to module/Apps/ActorSheets/SkillsCardV1.mjs index b0e83fb..b0231f3 100644 --- a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs +++ b/module/Apps/ActorSheets/SkillsCardV1.mjs @@ -1,21 +1,25 @@ 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 { ItemFlags } from "../../flags/item.mjs"; import { localizer } from "../../utils/Localizer.mjs"; import { Logger } from "../../utils/Logger.mjs"; +import { PopoverEventManager } from "../../utils/PopoverEventManager.mjs"; const { HandlebarsApplicationMixin } = foundry.applications.api; const { ActorSheetV2 } = foundry.applications.sheets; -const { ContextMenu } = foundry.applications.ui; +const { ContextMenu } = foundry.applications.ux; +const { deepClone } = foundry.utils; -export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin(ActorSheetV2)) { +export class SkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin(ActorSheetV2)) { // #region Options static DEFAULT_OPTIONS = { classes: [ `ripcrypt--actor`, - `ripcrypt--HeroSkillsCardV1`, + `ripcrypt--SkillsCardV1`, ], position: { width: `auto`, @@ -34,7 +38,7 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin static PARTS = { content: { - template: filePath(`templates/Apps/HeroSkillsCardV1/content.hbs`), + template: filePath(`templates/Apps/SkillsCardV1/content.hbs`), }, }; // #endregion @@ -42,7 +46,8 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin // #region Lifecycle async _onRender(context, options) { await super._onRender(context, options); - HeroSkillsCardV1._onRender.bind(this)(context, options); + SkillsCardV1._onRender.bind(this)(context, options); + SkillsCardV1._createPopoverListeners.bind(this)(); }; static async _onRender(_context, options) { @@ -75,13 +80,27 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin ); }; + /** @this {SkillsCardV1} */ + static async _createPopoverListeners() { + const ammoInfoIcon = this.element.querySelector(`.ammo-info-icon`); + const idPrefix = this.actor.uuid; + + const manager = new PopoverEventManager(`${idPrefix}.ammo-info-icon`, ammoInfoIcon, AmmoTracker); + this._popoverManagers.set(`.ammo-info-icon`, manager); + this._hookIDs.set(Hooks.on(`prepare${manager.id}Context`, (ctx) => { + ctx.ammos = this.actor.itemTypes.ammo; + }), `prepare${manager.id}Context`); + }; + async _preparePartContext(partId, ctx, opts) { ctx = await super._preparePartContext(partId, ctx, opts); ctx.actor = this.document; - ctx = await HeroSkillsCardV1.prepareGear(ctx); - ctx = await HeroSkillsCardV1.prepareAmmo(ctx); - ctx = await HeroSkillsCardV1.prepareSkills(ctx); + ctx = await SkillsCardV1.prepareGear(ctx); + ctx = await SkillsCardV1.prepareAmmo(ctx); + ctx = await SkillsCardV1.prepareSkills(ctx); + + ctx.aura = deepClone(ctx.actor.system.aura); Logger.debug(`Context:`, ctx); return ctx; @@ -106,7 +125,7 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin }; if (ctx.gear.length < limit) { - for (let i = ctx.gear.length - 1; i <= limit; i++) { + for (let i = ctx.gear.length; i < limit; i++) { ctx.gear.push({ index: ctx.gear.length, uuid: ``, @@ -120,7 +139,24 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin }; static async prepareAmmo(ctx) { - ctx.ammo = 0; + let total = 0; + let favouriteCount = 0; + ctx.favouriteAmmo = new Array(3).fill(null); + + for (const ammo of ctx.actor.itemTypes.ammo) { + total += ammo.system.quantity; + + if (favouriteCount < 3 && ammo.getFlag(game.system.id, ItemFlags.FAVOURITE)) { + ctx.favouriteAmmo[favouriteCount] = { + uuid: ammo.uuid, + name: ammo.name, + quantity: ammo.system.quantity, + }; + favouriteCount++; + }; + }; + + ctx.ammo = total; return ctx; }; @@ -158,7 +194,8 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin const length = ctx.skills[ability].length; if (length >= limit) { ctx.skills[ability] = ctx.skills[ability].slice(0, limit); - } else { + } + else { ctx.skills[ability] = ctx.skills[ability] .concat(Array(limit - length).fill(null)) .slice(0, limit); diff --git a/module/Apps/ActorSheets/HeroSummaryCardV1.mjs b/module/Apps/ActorSheets/StatsCardV1.mjs similarity index 85% rename from module/Apps/ActorSheets/HeroSummaryCardV1.mjs rename to module/Apps/ActorSheets/StatsCardV1.mjs index 0dc19bd..cd44ead 100644 --- a/module/Apps/ActorSheets/HeroSummaryCardV1.mjs +++ b/module/Apps/ActorSheets/StatsCardV1.mjs @@ -1,4 +1,5 @@ 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"; @@ -7,15 +8,15 @@ import { Logger } from "../../utils/Logger.mjs"; const { HandlebarsApplicationMixin } = foundry.applications.api; const { ActorSheetV2 } = foundry.applications.sheets; -const { ContextMenu } = foundry.applications.ui; +const { ContextMenu } = foundry.applications.ux; -export class HeroSummaryCardV1 extends GenericAppMixin(HandlebarsApplicationMixin(ActorSheetV2)) { +export class StatsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin(ActorSheetV2)) { // #region Options static DEFAULT_OPTIONS = { classes: [ `ripcrypt--actor`, - `ripcrypt--HeroSummaryCardV1`, + `ripcrypt--StatsCardV1`, ], position: { width: `auto`, @@ -25,6 +26,7 @@ export class HeroSummaryCardV1 extends GenericAppMixin(HandlebarsApplicationMixi resizable: false, }, actions: { + rollForHaste: DelveDiceHUD.rollForHaste, }, form: { submitOnChange: true, @@ -34,7 +36,7 @@ export class HeroSummaryCardV1 extends GenericAppMixin(HandlebarsApplicationMixi static PARTS = { content: { - template: filePath(`templates/Apps/HeroSummaryCardV1/content.hbs`), + template: filePath(`templates/Apps/StatsCardV1/content.hbs`), }, }; // #endregion @@ -42,7 +44,7 @@ export class HeroSummaryCardV1 extends GenericAppMixin(HandlebarsApplicationMixi // #region Lifecycle async _onRender(context, options) { await super._onRender(context, options); - HeroSummaryCardV1._onRender.bind(this)(context, options); + StatsCardV1._onRender.bind(this)(context, options); }; static async _onRender(context, options) { @@ -79,13 +81,13 @@ export class HeroSummaryCardV1 extends GenericAppMixin(HandlebarsApplicationMixi ctx = await super._preparePartContext(partId, ctx, opts); ctx.actor = this.document; - ctx = await HeroSummaryCardV1.prepareGuts(ctx); - ctx = await HeroSummaryCardV1.prepareWeapons(ctx); - ctx = await HeroSummaryCardV1.prepareArmor(ctx); - ctx = await HeroSummaryCardV1.prepareFatePath(ctx); - ctx = await HeroSummaryCardV1.prepareAbilityRow(ctx); - ctx = await HeroSummaryCardV1.prepareSpeed(ctx); - ctx = await HeroSummaryCardV1.prepareLevelData(ctx); + ctx = await StatsCardV1.prepareGuts(ctx); + ctx = await StatsCardV1.prepareWeapons(ctx); + ctx = await StatsCardV1.prepareArmor(ctx); + ctx = await StatsCardV1.prepareFatePath(ctx); + ctx = await StatsCardV1.prepareAbilityRow(ctx); + ctx = await StatsCardV1.prepareSpeed(ctx); + ctx = await StatsCardV1.prepareLevelData(ctx); Logger.debug(`Context:`, ctx); return ctx; @@ -117,7 +119,7 @@ export class HeroSummaryCardV1 extends GenericAppMixin(HandlebarsApplicationMixi ctx.fate.options = [ { label: `RipCrypt.common.empty`, v: `` }, ...Object.values(gameTerms.FatePath) - .map(v => ({ label: `RipCrypt.common.path.${v}`, value: v })), + .map(v => ({ label: `RipCrypt.common.ordinals.${v}.full`, value: v })), ]; return ctx; }; diff --git a/module/Apps/DelveDiceHUD.mjs b/module/Apps/DelveDiceHUD.mjs new file mode 100644 index 0000000..9599fa1 --- /dev/null +++ b/module/Apps/DelveDiceHUD.mjs @@ -0,0 +1,305 @@ +import { distanceBetweenFates, nextFate, previousFate } from "../utils/fates.mjs"; +import { filePath } from "../consts.mjs"; +import { gameTerms } from "../gameTerms.mjs"; +import { localizer } from "../utils/Localizer.mjs"; +import { Logger } from "../utils/Logger.mjs"; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; +const { ContextMenu } = foundry.applications.ux; +const { Roll } = foundry.dice; +const { FatePath } = gameTerms; + +const CompassRotations = { + [FatePath.NORTH]: -90, + [FatePath.EAST]: 0, + [FatePath.SOUTH]: 90, + [FatePath.WEST]: 180, +}; + +const conditions = [ + { label: `RipCrypt.common.difficulties.easy`, value: 4 }, + { label: `RipCrypt.common.difficulties.normal`, value: 5 }, + { label: `RipCrypt.common.difficulties.tough`, value: 6 }, + { label: `RipCrypt.common.difficulties.hard`, value: 7 }, +]; + +export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { + // #region Options + static DEFAULT_OPTIONS = { + id: `ripcrypt-delve-dice`, + tag: `aside`, + classes: [ + `ripcrypt`, + `ripcrypt--DelveDiceHUD`, + `hud`, + ], + window: { + frame: false, + positioned: false, + }, + actions: { + tourDelta: this.#tourDelta, + setFate: this.#setFate, + }, + }; + + static PARTS = { + previousTour: { + template: filePath(`templates/Apps/DelveDiceHUD/tour/previous.hbs`), + }, + difficulty: { + template: filePath(`templates/Apps/DelveDiceHUD/difficulty.hbs`), + }, + fateCompass: { + template: filePath(`templates/Apps/DelveDiceHUD/fateCompass.hbs`), + }, + sandsOfFate: { + template: filePath(`templates/Apps/DelveDiceHUD/tour/current.hbs`), + }, + nextTour: { + template: filePath(`templates/Apps/DelveDiceHUD/tour/next.hbs`), + }, + }; + // #endregion + + // #region Instance Data + /** + * The current number of degrees the compass pointer should be rotated, this + * is not stored in the DB since we only care about the initial rotation on + * reload, which is derived from the current fate. + * @type {Number} + */ + _rotation; + + constructor(...args) { + super(...args); + this._sandsOfFate = game.settings.get(`ripcrypt`, `sandsOfFate`); + this._currentFate = game.settings.get(`ripcrypt`, `currentFate`); + this._rotation = CompassRotations[this._currentFate]; + this._difficulty = game.settings.get(`ripcrypt`, `dc`); + }; + // #endregion + + // #region Lifecycle + /** + * Injects the element into the Foundry UI in the top middle + */ + _insertElement(element) { + const existing = document.getElementById(element.id); + if (existing) { + existing.replaceWith(element); + } + else { + const parent = document.getElementById(`ui-top`); + parent.prepend(element); + }; + }; + + async _onRender(context, options) { + await super._onRender(context, options); + + // Shortcut because users can't edit + if (!game.user.isGM) { return }; + + new ContextMenu( + this.element, + `#delve-difficulty`, + [ + ...conditions.map(condition => ({ + name: localizer(condition.label), + callback: DelveDiceHUD.#setDifficulty.bind(this, condition.value), + })), + { + name: localizer(`RipCrypt.common.difficulties.random`), + callback: () => { + const condition = conditions[Math.floor(Math.random() * conditions.length)]; + DelveDiceHUD.#setDifficulty.bind(this)(condition.value); + }, + }, + ], + { jQuery: false, fixed: true }, + ); + }; + + async _preparePartContext(partId, ctx, opts) { + ctx = await super._preparePartContext(partId, ctx, opts); + ctx.meta ??= {}; + + ctx.meta.editable = game.user.isGM; + + switch (partId) { + case `sandsOfFate`: { + ctx.sandsOfFate = this._sandsOfFate; + break; + }; + case `difficulty`: { + ctx.dc = this._difficulty; + break; + }; + case `fateCompass`: { + ctx.fate = this._currentFate; + ctx.rotation = `${this._rotation}deg`; + break; + }; + }; + + Logger.log(`${partId} Context`, ctx); + return ctx; + }; + + async animate({ parts = [] } = {}) { + if (parts.includes(`fateCompass`)) { + this.#animateCompassTo(); + }; + + if (parts.includes(`sandsOfFate`)) { + this.#animateSandsTo(); + }; + }; + + #animateCompassTo(newFate) { + if (newFate === this._currentFate) { return }; + + /** @type {HTMLElement|undefined} */ + const pointer = this.element.querySelector(`.compass-pointer`); + if (!pointer) { return }; + + newFate ??= game.settings.get(`ripcrypt`, `currentFate`); + + let distance = distanceBetweenFates(this._currentFate, newFate); + if (distance === 3) { distance = -1 }; + + this._rotation += distance * 90; + + pointer.style.setProperty(`transform`, `rotate(${this._rotation}deg)`); + this._currentFate = newFate; + }; + + #animateSandsTo(newSands) { + /** @type {HTMLElement|undefined} */ + const sands = this.element.querySelector(`.sands-value`); + if (!sands) { return }; + + newSands ??= game.settings.get(`ripcrypt`, `sandsOfFate`); + + sands.innerHTML = newSands; + this._sandsOfFate = newSands; + }; + // #endregion + + // #region Actions + /** @this {DelveDiceHUD} */ + static async #tourDelta(_event, element) { + const delta = parseInt(element.dataset.delta); + await this.sandsOfFateDelta(delta); + + switch (Math.sign(delta)) { + case -1: { + game.settings.set(`ripcrypt`, `currentFate`, nextFate(this._currentFate)); + break; + } + case 1: { + game.settings.set(`ripcrypt`, `currentFate`, previousFate(this._currentFate)); + break; + } + }; + }; + + /** @this {DelveDiceHUD} */ + static async #setFate(_event, element) { + const fate = element.dataset.toFate; + this.#animateCompassTo(fate); + game.settings.set(`ripcrypt`, `currentFate`, fate); + }; + + /** @this {DelveDiceHUD} */ + static async #setDifficulty(value) { + this._difficulty = value; + game.settings.set(`ripcrypt`, `dc`, value); + }; + // #endregion + + // #region Public API + async alertCrypticEvent() { + const alertType = game.settings.get(`ripcrypt`, `onCrypticEvent`); + if (alertType === `nothing`) { return }; + + if ([`both`, `notif`].includes(alertType)) { + ui.notifications.info( + localizer(`RipCrypt.notifs.info.cryptic-event-alert`), + { console: false }, + ); + game.socket.emit(`system.ripcrypt`, { + event: `notify`, + payload: { + message: `RipCrypt.notifs.info.cryptic-event-alert`, + type: `info`, + }, + }); + }; + + if ([`both`, `pause`].includes(alertType) && game.user.isGM) { + game.togglePause(true, { broadcast: true }); + }; + }; + + /** + * Changes the current Sands of Fate by an amount provided, animating the + * @param {number} delta The amount of change + */ + async sandsOfFateDelta(delta) { + const initial = game.settings.get(`ripcrypt`, `sandsOfFateInitial`); + let newSands = this._sandsOfFate + delta; + + if (newSands > initial) { + Logger.info(`Cannot increase the Sands of Fate to a value about the initial`); + return; + }; + + if (newSands === 0) { + newSands = initial; + await this.alertCrypticEvent(); + }; + + this.#animateSandsTo(newSands); + game.settings.set(`ripcrypt`, `sandsOfFate`, newSands); + }; + + /** + * A helper method that rolls the dice required for hasty turns while delving + * and adjusts the Sands of Fate accordingly + */ + static async rollForHaste() { + const shouldUpdateSands = game.settings.get(`ripcrypt`, `allowUpdateSandsSocket`); + if (shouldUpdateSands && game.users.activeGM == null) { + ui.notifications.error(localizer(`RipCrypt.notifs.error.no-active-gm`)); + return; + }; + + const roll = new Roll(`1d8xo=1`); + await roll.evaluate(); + + let delta = 0; + if (roll.dice[0].results[0].exploded) { + delta = -1; + if (roll.dice[0].results[1].result === 1) { + delta = -2; + }; + }; + + roll.toMessage({ flavor: `Haste Check` }); + + // Change the Sands of Fate setting if required + if (delta === 0 || !shouldUpdateSands) { return }; + if (game.user.isActiveGM) { + ui.delveDice.sandsOfFateDelta(delta); + } + else { + game.socket.emit(`system.ripcrypt`, { + event: `updateSands`, + payload: { delta }, + }); + }; + }; + // #endregion +}; diff --git a/module/Apps/DelveTourApp.mjs b/module/Apps/DelveTourApp.mjs deleted file mode 100644 index 8c213ed..0000000 --- a/module/Apps/DelveTourApp.mjs +++ /dev/null @@ -1,104 +0,0 @@ -import { filePath } from "../consts.mjs"; -import { GenericAppMixin } from "./GenericApp.mjs"; -import { Logger } from "../utils/Logger.mjs"; - -const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; - -const conditions = [ - { label: `RipCrypt.common.difficulties.easy`, value: 4 }, - { label: `RipCrypt.common.difficulties.normal`, value: 5 }, - { label: `RipCrypt.common.difficulties.tough`, value: 6 }, - { label: `RipCrypt.common.difficulties.hard`, value: 7 }, -]; - -export class DelveTourApp extends GenericAppMixin(HandlebarsApplicationMixin(ApplicationV2)) { - // #region Options - static DEFAULT_OPTIONS = { - classes: [ - `ripcrypt--CryptApp`, - ], - window: { - title: `Delve Tour`, - frame: true, - positioned: true, - resizable: false, - minimizable: false, - }, - position: { - width: `auto`, - }, - actions: { - randomCondition: this.#randomCondition, - }, - }; - - static PARTS = { - turnCount: { - template: filePath(`templates/Apps/CryptApp/turnCount.hbs`), - }, - delveConditions: { - template: filePath(`templates/Apps/CryptApp/delveConditions.hbs`), - }, - fate: { - template: filePath(`templates/Apps/CryptApp/fate.hbs`), - }, - }; - // #endregion - - // #region Lifecycle - async _renderFrame(options) { - const frame = await super._renderFrame(options); - this.window.close.remove(); // Prevent closing - return frame; - }; - - async _onRender(context, options) { - await super._onRender(context, options); - - // Shortcut because users can't edit - if (!game.user.isGM) { return }; - - // Add event listener for the dropdown - if (options.parts.includes(`delveConditions`)) { - const select = this.element.querySelector(`#${this.id}-difficulty`); - select.addEventListener(`change`, async (ev) => { - const newDifficulty = parseInt(ev.target.value); - if (!Number.isNaN(newDifficulty)) { - await game.settings.set(`ripcrypt`, `dc`, newDifficulty); - }; - this.render({ parts: [`delveConditions`] }); - }); - }; - }; - - async _preparePartContext(partId, ctx, opts) { - ctx = await super._preparePartContext(partId, ctx, opts); - - ctx.meta.editable = game.user.isGM; - - switch (partId) { - case `delveConditions`: { - ctx = this._prepareDifficulty(ctx); - break; - }; - }; - - Logger.log(`${partId} Context`, ctx); - return ctx; - }; - - _prepareDifficulty(ctx) { - ctx.options = conditions; - ctx.difficulty = game.settings.get(`ripcrypt`, `dc`); - return ctx; - }; - // #endregion - - // #region Actions - static async #randomCondition() { - const dc = conditions[Math.floor(Math.random() * conditions.length)]; - await game.settings.set(`ripcrypt`, `dc`, dc.value); - await this.render({ parts: [`delveConditions`] }); - }; - // #endregion -}; diff --git a/module/Apps/GenericApp.mjs b/module/Apps/GenericApp.mjs index 5f3a75b..0be30e4 100644 --- a/module/Apps/GenericApp.mjs +++ b/module/Apps/GenericApp.mjs @@ -1,10 +1,11 @@ -import { createItemFromElement, deleteItemFromElement, editItemFromElement } from "./utils.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 + * A mixin that takes the class from HandlebarsApplicationMixin and combines it + * with utility functions / data that is used across all RipCrypt applications */ export function GenericAppMixin(HandlebarsApp) { class GenericRipCryptApp extends HandlebarsApp { @@ -31,6 +32,13 @@ export function GenericAppMixin(HandlebarsApp) { }; // #endregion + // #region Instance Data + /** @type {Map} */ + _popoverManagers = new Map(); + /** @type {Map} */ + _hookIDs = new Map(); + // #endregion + // #region Lifecycle /** * @override @@ -38,13 +46,43 @@ export function GenericAppMixin(HandlebarsApp) { * top after being re-rendered as normal */ async render(options = {}, _options = {}) { - super.render(options, _options); + await super.render(options, _options); const instance = foundry.applications.instances.get(this.id); if (instance !== undefined && options.orBringToFront) { instance.bringToFront(); }; }; + /** @override */ + async _onRender(...args) { + await super._onRender(...args); + + /* + Rendering each of the popover managers associated with this app allows us + to have them be dynamic and update when their parent application is rerendered, + this could eventually be something we can move into the Document's apps + collection so Foundry auto-rerenders it, but because it isn't actually + associated with the Document (as it's dependendant on the Application), I + decided that it would be best to do my own handling for it. + */ + for (const manager of this._popoverManagers.values()) { + manager.render(); + }; + + /* + Foreign update listeners so that we can easily update items that may not + be this document itself, but are useful to be able to be edited from this + sheet. Primarily useful for editing the Actors' Item collection, or an Items' + ActiveEffect collection. + */ + this.element.querySelectorAll(`input[data-foreign-update-on]`).forEach(el => { + const events = el.dataset.foreignUpdateOn.split(`,`); + for (const event of events) { + el.addEventListener(event, updateForeignDocumentFromEvent); + }; + }); + }; + async _preparePartContext(partId, ctx, opts) { ctx = await super._preparePartContext(partId, ctx, opts); delete ctx.document; @@ -54,12 +92,29 @@ export function GenericAppMixin(HandlebarsApp) { ctx.meta.idp = this.document?.uuid ?? this.id; if (this.document) { ctx.meta.limited = this.document.limited; - ctx.meta.editable = ctx.editable; - } + ctx.meta.editable = this.isEditable || game.user.isGM; + ctx.meta.embedded = this.document.isEmbedded; + }; delete ctx.editable; return ctx; }; + + _tearDown(options) { + // Clear all popovers associated with the app + for (const manager of this._popoverManagers.values()) { + manager.destroy(); + }; + this._popoverManagers.clear(); + + // Remove any hooks added for this app + for (const [id, hook] of this._hookIDs.entries()) { + Hooks.off(hook, id); + }; + this._hookIDs.clear(); + + super._tearDown(options); + }; // #endregion // #region Actions diff --git a/module/Apps/ItemSheets/AllItemSheetV1.mjs b/module/Apps/ItemSheets/AllItemSheetV1.mjs index 3ac26ba..0608fde 100644 --- a/module/Apps/ItemSheets/AllItemSheetV1.mjs +++ b/module/Apps/ItemSheets/AllItemSheetV1.mjs @@ -61,7 +61,7 @@ export class AllItemSheetV1 extends GenericAppMixin(HandlebarsApplicationMixin(I await super._processSubmitData(...args); if (this.document.system.forceRerender) { - await this.render(false); + await this.render(); }; }; // #endregion diff --git a/module/Apps/ItemSheets/ArmourSheet.mjs b/module/Apps/ItemSheets/ArmourSheet.mjs new file mode 100644 index 0000000..7570413 --- /dev/null +++ b/module/Apps/ItemSheets/ArmourSheet.mjs @@ -0,0 +1,135 @@ +import { filePath } from "../../consts.mjs"; +import { gameTerms } from "../../gameTerms.mjs"; +import { GenericAppMixin } from "../GenericApp.mjs"; + +const { HandlebarsApplicationMixin } = foundry.applications.api; +const { ItemSheetV2 } = foundry.applications.sheets; +const { getProperty, hasProperty, setProperty } = foundry.utils; + +export class ArmourSheet extends GenericAppMixin(HandlebarsApplicationMixin(ItemSheetV2)) { + + // #region Options + static DEFAULT_OPTIONS = { + classes: [ + `ripcrypt--item`, + `ArmourSheet`, + ], + position: { + width: `auto`, + height: `auto`, + }, + window: { + resizable: false, + }, + form: { + submitOnChange: true, + closeOnSubmit: false, + }, + }; + + static PARTS = { + header: { + template: filePath(`templates/Apps/partials/item-header.hbs`), + }, + content: { + template: filePath(`templates/Apps/ArmourSheet/content.hbs`), + }, + }; + // #endregion + + // #region Lifecycle + async _onRender() { + // remove the flag if it exists when we render the sheet + delete this.document?.system?.forceRerender; + }; + + /** + * Used to make it so that items that don't get updated because of the + * _preUpdate hook removing/changing the data submitted, can still get + * re-rendered when the diff is empty. If the document does get updated, + * this rerendering does not happen. + * + * @override + */ + async _processSubmitData(...args) { + await super._processSubmitData(...args); + + if (this.document.system.forceRerender) { + await this.render(); + }; + }; + + /** + * Customize how form data is extracted into an expanded object. + * @param {SubmitEvent|null} event The originating form submission event + * @param {HTMLFormElement} form The form element that was submitted + * @param {FormDataExtended} formData Processed data for the submitted form + * @returns {object} An expanded object of processed form data + * @throws {Error} Subclasses may throw validation errors here to prevent form submission + * @protected + */ + _processFormData(event, form, formData) { + const data = super._processFormData(event, form, formData); + + if (hasProperty(data, `system.location`)) { + let locations = getProperty(data, `system.location`); + locations = locations.filter(value => value != null); + setProperty(data, `system.location`, locations); + }; + + return data; + }; + // #endregion + + // #region Data Prep + async _preparePartContext(partId, _, opts) { + const ctx = await super._preparePartContext(partId, {}, opts); + + ctx.item = this.document; + ctx.system = this.document.system; + + switch (partId) { + case `content`: { + this._prepareContentContext(ctx, opts); + break; + }; + }; + + return ctx; + }; + + async _prepareContentContext(ctx) { + ctx.weights = [ + { + label: `RipCrypt.common.empty`, + value: null, + }, + ...Object.values(gameTerms.WeightRatings).map(opt => ({ + label: `RipCrypt.common.weightRatings.${opt}`, + value: opt, + })), + ]; + + ctx.accesses = [ + { + label: `RipCrypt.common.empty`, + value: ``, + }, + ...gameTerms.Access.map(opt => ({ + label: `RipCrypt.common.accessLevels.${opt}`, + value: opt, + })), + ]; + + ctx.protects = { + head: this.document.system.location.has(gameTerms.Anatomy.HEAD), + body: this.document.system.location.has(gameTerms.Anatomy.BODY), + arms: this.document.system.location.has(gameTerms.Anatomy.ARMS), + legs: this.document.system.location.has(gameTerms.Anatomy.LEGS), + }; + }; + // #endregion + + // #region Actions + // #endregion +}; diff --git a/module/Apps/components/ArmourSummary.mjs b/module/Apps/components/ArmourSummary.mjs new file mode 100644 index 0000000..15ddf16 --- /dev/null +++ b/module/Apps/components/ArmourSummary.mjs @@ -0,0 +1,56 @@ +import { filePath } from "../../consts.mjs"; +import { StyledShadowElement } from "./mixins/StyledShadowElement.mjs"; + +const { renderTemplate } = foundry.applications.handlebars; + +export class ArmourSummary extends StyledShadowElement(HTMLElement) { + static elementName = `armour-summary`; + static formAssociated = false; + + /* Stuff for the mixin to use */ + static _stylePath = `css/components/armour-summary.css`; + #container; + + get type() { + return this.getAttribute(`type`) ?? `hero`; + }; + + set type(newValue) { + this.setAttribute(`type`, newValue); + }; + + _mounted = false; + async connectedCallback() { + super.connectedCallback(); + if (this._mounted) { return }; + + /* + This converts all of the double-dash prefixed properties on the element to + CSS variables so that they don't all need to be provided by doing style="" + */ + for (const attrVar of this.attributes) { + if (attrVar.name?.startsWith(`var:`)) { + const prop = attrVar.name.replace(`var:`, ``); + this.style.setProperty(`--` + prop, attrVar.value); + }; + }; + + this.#container = document.createElement(`div`); + this.#container.classList = `person`; + + this.#container.innerHTML = await renderTemplate( + filePath(`templates/components/armour-summary.hbs`), + { type: this.type }, + ); + + this._shadow.appendChild(this.#container); + + this._mounted = true; + }; + + disconnectedCallback() { + super.disconnectedCallback(); + if (!this._mounted) { return }; + this._mounted = false; + }; +}; diff --git a/module/Apps/elements/Icon.mjs b/module/Apps/components/Icon.mjs similarity index 100% rename from module/Apps/elements/Icon.mjs rename to module/Apps/components/Icon.mjs diff --git a/module/Apps/elements/RipCryptBorder.mjs b/module/Apps/components/RipCryptBorder.mjs similarity index 100% rename from module/Apps/elements/RipCryptBorder.mjs rename to module/Apps/components/RipCryptBorder.mjs diff --git a/module/Apps/elements/_index.mjs b/module/Apps/components/_index.mjs similarity index 91% rename from module/Apps/elements/_index.mjs rename to module/Apps/components/_index.mjs index 3568481..8400462 100644 --- a/module/Apps/elements/_index.mjs +++ b/module/Apps/components/_index.mjs @@ -1,9 +1,11 @@ +import { ArmourSummary } from "./ArmourSummary.mjs"; import { Logger } from "../../utils/Logger.mjs"; import { RipCryptBorder } from "./RipCryptBorder.mjs"; import { RipCryptIcon } from "./Icon.mjs"; import { RipCryptSVGLoader } from "./svgLoader.mjs"; const components = [ + ArmourSummary, RipCryptIcon, RipCryptSVGLoader, RipCryptBorder, diff --git a/module/Apps/elements/mixins/StyledShadowElement.mjs b/module/Apps/components/mixins/StyledShadowElement.mjs similarity index 98% rename from module/Apps/elements/mixins/StyledShadowElement.mjs rename to module/Apps/components/mixins/StyledShadowElement.mjs index e5ef291..37f81d1 100644 --- a/module/Apps/elements/mixins/StyledShadowElement.mjs +++ b/module/Apps/components/mixins/StyledShadowElement.mjs @@ -51,7 +51,8 @@ export function StyledShadowElement(Base) { const stylePath = this.constructor._stylePath; if (this.constructor._styles.has(stylePath)) { this._style.innerHTML = this.constructor._styles.get(stylePath); - } else { + } + else { fetch(`./systems/${game.system.id}/templates/${stylePath}`) .then(r => r.text()) .then(t => { diff --git a/module/Apps/elements/svgLoader.mjs b/module/Apps/components/svgLoader.mjs similarity index 100% rename from module/Apps/elements/svgLoader.mjs rename to module/Apps/components/svgLoader.mjs diff --git a/module/Apps/popovers/AmmoTracker.mjs b/module/Apps/popovers/AmmoTracker.mjs new file mode 100644 index 0000000..458e783 --- /dev/null +++ b/module/Apps/popovers/AmmoTracker.mjs @@ -0,0 +1,96 @@ +import { filePath } from "../../consts.mjs"; +import { GenericPopoverMixin } from "./GenericPopoverMixin.mjs"; +import { ItemFlags } from "../../flags/item.mjs"; +import { localizer } from "../../utils/Localizer.mjs"; +import { Logger } from "../../utils/Logger.mjs"; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin(ApplicationV2)) { + // #region Options + static DEFAULT_OPTIONS = { + classes: [ + `ripcrypt`, + ], + window: { + title: `RipCrypt.app-titles.AmmoTracker`, + contentClasses: [ + `ripcrypt--AmmoTracker`, + ], + }, + actions: { + favourite: this.#favourite, + unfavourite: this.#unfavourite, + }, + }; + + static PARTS = { + ammoList: { + template: filePath(`templates/Apps/popovers/AmmoTracker/ammoList.hbs`), + }, + }; + // #endregion + + // #region Instance Data + _favouriteCount = 0; + // #endregion + + // #region Lifecycle + async _preparePartContext(partId, data) { + const ctx = { + meta: { idp: this.id }, + partId, + }; + + let favouriteCount = 0; + ctx.ammos = data.ammos.map(ammo => { + const favourite = ammo.getFlag(game.system.id, ItemFlags.FAVOURITE) ?? false; + if (favourite) { favouriteCount++ }; + + return { + ammo, + favourite, + }; + }); + + this._favouriteCount = favouriteCount; + ctx.atFavouriteLimit = favouriteCount >= 3; + return ctx; + }; + // #endregion + + // #region Actions + static async #favourite(_, el) { + const targetEl = el.closest(`[data-item-id]`); + if (!targetEl) { + Logger.warn(`Cannot find a parent element with data-item-id`); + return; + }; + + if (this._favouriteCount > 3) { + ui.notifications.error(localizer(`RipCrypt.notifs.error.at-favourite-limit`)); + return; + }; + + const data = targetEl.dataset; + const item = await fromUuid(data.itemId); + if (!item) { return }; + + item.setFlag(game.system.id, ItemFlags.FAVOURITE, true); + }; + + static async #unfavourite(_, el) { + const targetEl = el.closest(`[data-item-id]`); + if (!targetEl) { + Logger.warn(`Cannot find a parent element with data-item-id`); + return; + }; + + const data = targetEl.dataset; + const item = await fromUuid(data.itemId); + if (!item) { return }; + + item.unsetFlag(game.system.id, ItemFlags.FAVOURITE); + }; + // #endregion +}; diff --git a/module/Apps/popovers/GenericPopoverMixin.mjs b/module/Apps/popovers/GenericPopoverMixin.mjs new file mode 100644 index 0000000..271bb9a --- /dev/null +++ b/module/Apps/popovers/GenericPopoverMixin.mjs @@ -0,0 +1,188 @@ +import { updateForeignDocumentFromEvent } from "../utils.mjs"; + +const { ApplicationV2 } = foundry.applications.api; + +/** + * This mixin provides the ability to designate an Application as a "popover", + * which means that it will spawn near the x/y coordinates provided it won't + * overflow the bounds of the screen. This also implements a _preparePartContext + * in order to allow the parent application passing new data into the popover + * whenever it rerenders; how the popover handles this data is up to the + * specific implementation. + */ +export function GenericPopoverMixin(HandlebarsApp) { + class GenericRipCryptPopover extends HandlebarsApp { + static DEFAULT_OPTIONS = { + id: `popover-{id}`, + classes: [ + `popover`, + ], + window: { + frame: false, + positioned: true, + resizable: false, + minimizable: false, + }, + actions: {}, + }; + + popover = {}; + constructor({ popover, ...options}) { + + // For when the caller doesn't provide anything, we want this to behave + // like a normal Application instance. + popover.framed ??= true; + popover.locked ??= false; + + if (popover.framed) { + options.window ??= {}; + options.window.frame = true; + options.window.minimizable = true; + } + + options.classes ??= []; + options.classes.push(popover.framed ? `framed` : `frameless`); + + super(options); + this.popover = popover; + }; + + toggleLock() { + this.popover.locked = !this.popover.locked; + this.classList.toggle(`locked`, this.popover.locked); + }; + + /** + * This render utility is intended in order to make the popovers able to be + * used in both framed and frameless mode, making sure that the content classes + * from the framed mode get shunted onto the frameless Application's root + * element. + */ + async _onFirstRender(...args) { + await super._onFirstRender(...args); + + const hasContentClasses = this.options?.window?.contentClasses?.length > 0; + if (!this.popover.framed && hasContentClasses) { + this.classList.add(...this.options.window.contentClasses); + }; + }; + + async _onRender(...args) { + await super._onRender(...args); + + /* + Foreign update listeners so that we can easily update items that may not + be this document itself, but are useful to be able to be edited from this + sheet. Primarily useful for editing the Actors' Item collection, or an Items' + ActiveEffect collection. + */ + this.element.querySelectorAll(`input[data-foreign-update-on]`).forEach(el => { + const events = el.dataset.foreignUpdateOn.split(`,`); + for (const event of events) { + el.addEventListener(event, updateForeignDocumentFromEvent); + }; + }); + }; + + async close(options = {}) { + // prevent locked popovers from being closed + if (this.popover.locked && !options.force) { return }; + + if (!this.popover.framed) { + options.animate = false; + }; + return super.close(options); + }; + + /** + * @override + * Custom implementation in order to make it show up approximately where I + * want it to when being created. + * + * Most of this implementation is identical to the ApplicationV2 + * implementation, the biggest difference is how targetLeft and targetTop + * are calculated. + */ + _updatePosition(position) { + if (!this.element) { return position }; + if (this.popover.framed) { return super._updatePosition(position) }; + + const el = this.element; + let {width, height, left, top, scale} = position; + scale ??= 1.0; + const computedStyle = getComputedStyle(el); + let minWidth = ApplicationV2.parseCSSDimension(computedStyle.minWidth, el.parentElement.offsetWidth) || 0; + let maxWidth = ApplicationV2.parseCSSDimension(computedStyle.maxWidth, el.parentElement.offsetWidth) || Infinity; + let minHeight = ApplicationV2.parseCSSDimension(computedStyle.minHeight, el.parentElement.offsetHeight) || 0; + let maxHeight = ApplicationV2.parseCSSDimension(computedStyle.maxHeight, el.parentElement.offsetHeight) || Infinity; + let bounds = el.getBoundingClientRect(); + const {clientWidth, clientHeight} = document.documentElement; + + // Explicit width + const autoWidth = width === `auto`; + if ( !autoWidth ) { + const targetWidth = Number(width || bounds.width); + minWidth = parseInt(minWidth) || 0; + maxWidth = parseInt(maxWidth) || (clientWidth / scale); + width = Math.clamp(targetWidth, minWidth, maxWidth); + } + + // Explicit height + const autoHeight = height === `auto`; + if ( !autoHeight ) { + const targetHeight = Number(height || bounds.height); + minHeight = parseInt(minHeight) || 0; + maxHeight = parseInt(maxHeight) || (clientHeight / scale); + height = Math.clamp(targetHeight, minHeight, maxHeight); + } + + // Implicit height + if ( autoHeight ) { + Object.assign(el.style, {width: `${width}px`, height: ``}); + bounds = el.getBoundingClientRect(); + height = bounds.height; + } + + // Implicit width + if ( autoWidth ) { + Object.assign(el.style, {height: `${height}px`, width: ``}); + bounds = el.getBoundingClientRect(); + width = bounds.width; + } + + // Left Offset + const scaledWidth = width * scale; + const targetLeft = left ?? (this.popover.x - Math.floor( scaledWidth / 2 )); + const maxLeft = Math.max(clientWidth - scaledWidth, 0); + left = Math.clamp(targetLeft, 0, maxLeft); + + // Top Offset + const scaledHeight = height * scale; + const targetTop = top ?? (this.popover.y - scaledHeight); + const maxTop = Math.max(clientHeight - scaledHeight, 0); + top = Math.clamp(targetTop, 0, maxTop); + + // Scale + scale ??= 1.0; + return { + width: autoWidth ? `auto` : width, + height: autoHeight ? `auto` : height, + left, + top, + scale, + }; + }; + + /** + * This is here in order allow things that are not this Application + * to provide / augment the context data for the lifecycle of the app. + */ + async _prepareContext(_partId, _context, options) { + const context = {}; + Hooks.callAll(`prepare${this.constructor.name}Context`, context, options); + Hooks.callAll(`prepare${this.popover.managerId}Context`, context, options); + return context; + }; + }; + return GenericRipCryptPopover; +}; diff --git a/module/Apps/sidebar/CombatTracker.mjs b/module/Apps/sidebar/CombatTracker.mjs index 8cffd77..d976867 100644 --- a/module/Apps/sidebar/CombatTracker.mjs +++ b/module/Apps/sidebar/CombatTracker.mjs @@ -1,6 +1,38 @@ const { CombatTracker } = foundry.applications.sidebar.tabs; +function createButtonInnerHTML() { + const whoFirst = game.settings.get(`ripcrypt`, `whoFirst`); + let icon = `evil`; + let ariaLabel = `Geists go first, click to make heroes go first`; + + if (whoFirst === `friendly`) { + icon = `hero`; + ariaLabel = `Heroes go first, click to make geists go first`; + }; + + return ``; +}; + +function createButtonTooltip() { + const whoFirst = game.settings.get(`ripcrypt`, `whoFirst`); + if (whoFirst === `friendly`) { + return `Heroes currently go first`; + }; + return `Geists currently go first`; +}; + export class RipCryptCombatTracker extends CombatTracker { + + static DEFAULT_OPTIONS = { + actions: { + toggleFirst: this.#toggleFirst, + }, + }; + /** * Changes the way the combat tracker renders combatant rows to account for * multiple combatants being in the same combat "group", thus all going at the @@ -28,10 +60,30 @@ export class RipCryptCombatTracker extends CombatTracker { async _onRender(...args) { await super._onRender(...args); + const spacer = document.createElement(`div`); + spacer.classList.add(`spacer`); + + const button = document.createElement(`button`); + button.classList.add(`inline-control`, `combat-control`, `icon`); + button.type = `button`; + button.dataset.tooltip = createButtonTooltip(); + button.dataset.action = `toggleFirst`; + button.innerHTML = createButtonInnerHTML(); + button.disabled = !game.user.isGM; + // Purge the combat controls that I don't want to exist because they don't // make sense in the system. - this.element?.querySelector(`[data-action="resetAll"]`)?.remove(); - this.element?.querySelector(`[data-action="rollNPC"]`)?.remove(); - this.element?.querySelector(`[data-action="rollAll"]`)?.remove(); + this.element?.querySelector(`[data-action="rollNPC"]`)?.replaceWith(spacer.cloneNode(true)); + this.element?.querySelector(`[data-action="rollAll"]`)?.replaceWith(button.cloneNode(true)); + }; + + static async #toggleFirst(_event, element) { + game.tooltip.deactivate(); + const whoFirst = game.settings.get(`ripcrypt`, `whoFirst`); + const otherFirst = whoFirst === `friendly` ? `hostile` : `friendly`; + await game.settings.set(`ripcrypt`, `whoFirst`, otherFirst); + element.innerHTML = createButtonInnerHTML(); + element.dataset.tooltip = createButtonTooltip(); + game.tooltip.activate(element); }; }; diff --git a/module/Apps/utils.mjs b/module/Apps/utils.mjs index 1baee18..47f8f01 100644 --- a/module/Apps/utils.mjs +++ b/module/Apps/utils.mjs @@ -11,7 +11,7 @@ export async function createItemFromElement(target, { parent } = {}) { const type = data.defaultItemType; await Item.createDialog( { type }, - { parent }, + { parent, showEquipPrompt: false }, { types, folders: [], @@ -40,5 +40,26 @@ export async function deleteItemFromElement(target) { const itemId = itemEl.dataset.itemId; if (!itemId) { return }; const item = await fromUuid(itemId); - item.delete(); + item.deleteDialog(); +}; + +/** + * Updates a document using the UUID, expects there to be the following + * dataset attributes: + * - "data-foreign-uuid" : The UUID of the document to update + * - "data-foreign-name" : The dot-separated path of the value to update + * + * @param {Event} event + */ +export async function updateForeignDocumentFromEvent(event) { + const target = event.currentTarget; + const data = target.dataset; + const document = await fromUuid(data.foreignUuid); + + let value = target.value; + switch (target.type) { + case `checkbox`: value = target.checked; break; + }; + + await document?.update({ [data.foreignName]: value }); }; diff --git a/module/api.mjs b/module/api.mjs index 7b04ea8..4938519 100644 --- a/module/api.mjs +++ b/module/api.mjs @@ -1,12 +1,18 @@ // App imports +import { AmmoTracker } from "./Apps/popovers/AmmoTracker.mjs"; import { CombinedHeroSheet } from "./Apps/ActorSheets/CombinedHeroSheet.mjs"; import { DicePool } from "./Apps/DicePool.mjs"; -import { HeroSkillsCardV1 } from "./Apps/ActorSheets/HeroSkillsCardV1.mjs"; -import { HeroSummaryCardV1 } from "./Apps/ActorSheets/HeroSummaryCardV1.mjs"; import { RichEditor } from "./Apps/RichEditor.mjs"; +import { SkillsCardV1 } from "./Apps/ActorSheets/SkillsCardV1.mjs"; +import { StatsCardV1 } from "./Apps/ActorSheets/StatsCardV1.mjs"; // Util imports +import { distanceBetweenFates, nextFate, previousFate } from "./utils/fates.mjs"; import { documentSorter } from "./consts.mjs"; +import { rankToInteger } from "./utils/rank.mjs"; + +// Misc Imports +import { ItemFlags } from "./flags/item.mjs"; const { deepFreeze } = foundry.utils; @@ -16,15 +22,21 @@ Object.defineProperty( { value: deepFreeze({ Apps: { + AmmoTracker, DicePool, CombinedHeroSheet, - HeroSummaryCardV1, - HeroSkillsCardV1, + StatsCardV1, + SkillsCardV1, RichEditor, }, utils: { documentSorter, + distanceBetweenFates, + nextFate, + previousFate, + rankToInteger, }, + ItemFlags, }), writable: false, }, diff --git a/module/consts.mjs b/module/consts.mjs index 84d8740..ee88a31 100644 --- a/module/consts.mjs +++ b/module/consts.mjs @@ -42,9 +42,11 @@ export function toBoolean(val) { export function documentSorter(a, b) { if (!a && !b) { return 0; - } else if (!a) { + } + else if (!a) { return 1; - } else if (!b) { + } + else if (!b) { return -1; }; @@ -54,3 +56,14 @@ export function documentSorter(a, b) { }; return Math.sign(a.name.localeCompare(b.name)); }; + +// MARK: getTooltipDelay +/** + * Retrieves the configured minimum delay between the user hovering an element + * and a tooltip showing up. Used for the pseudo-tooltip Applications that I use. + * + * @returns The number of milliseconds for the timeout + */ +export function getTooltipDelay() { + return game.tooltip.constructor.TOOLTIP_ACTIVATION_MS; +}; diff --git a/module/data/Actor/Entity.mjs b/module/data/Actor/Entity.mjs new file mode 100644 index 0000000..89f2b7f --- /dev/null +++ b/module/data/Actor/Entity.mjs @@ -0,0 +1,201 @@ +import { derivedMaximumBar } from "../helpers.mjs"; +import { gameTerms } from "../../gameTerms.mjs"; +import { rankToInteger } from "../../utils/rank.mjs"; +import { sumReduce } from "../../utils/sumReduce.mjs"; + +const { fields } = foundry.data; + +export class EntityData extends foundry.abstract.TypeDataModel { + + // MARK: Token Attrs + static get trackableAttributes() { + return { + bar: [ + `guts`, + ], + value: [ + `ability.grit`, + `ability.gait`, + `ability.grip`, + `ability.glim`, + `level.glory`, + `level.step`, + `level.rank`, + `coin.gold`, + `coin.silver`, + `coin.copper`, + ], + }; + }; + + // MARK: Schema + static defineSchema() { + return { + ability: new fields.SchemaField({ + grit: new fields.NumberField({ + min: 0, + initial: 1, + integer: true, + required: true, + nullable: false, + }), + gait: new fields.NumberField({ + min: 0, + initial: 1, + integer: true, + required: true, + nullable: false, + }), + grip: new fields.NumberField({ + min: 0, + initial: 1, + integer: true, + required: true, + nullable: false, + }), + glim: new fields.NumberField({ + min: 0, + initial: 1, + integer: true, + required: true, + nullable: false, + }), + }), + guts: derivedMaximumBar(0, 5), + coin: new fields.SchemaField({ + gold: new fields.NumberField({ + initial: 5, + integer: true, + required: true, + nullable: false, + }), + silver: new fields.NumberField({ + initial: 0, + integer: true, + required: true, + nullable: false, + }), + copper: new fields.NumberField({ + initial: 0, + integer: true, + required: true, + nullable: false, + }), + }), + fate: new fields.StringField({ + initial: ``, + blank: true, + trim: true, + nullable: false, + choices: () => { + return Object.values(gameTerms.FatePath).concat(``); + }, + }), + level: new fields.SchemaField({ + glory: new fields.NumberField({ + min: 0, + initial: 0, + integer: true, + required: true, + nullable: false, + }), + step: new fields.NumberField({ + min: 1, + initial: 1, + max: 3, + integer: true, + required: true, + nullable: false, + }), + rank: new fields.StringField({ + initial: gameTerms.Rank.NOVICE, + required: true, + nullable: false, + blank: false, + trim: true, + choices: Object.values(gameTerms.Rank), + }), + }), + }; + }; + + // MARK: Base Data + prepareBaseData() { + super.prepareBaseData(); + + // Calculate the person's base Crafting aura + const rank = rankToInteger(this.level.rank); + this.aura = { + normal: ( rank + 1 ) * 2, + heavy: ( rank + 2 ) * 2, + }; + + this.guts.max = 0; + + // The limitations imposed on things like inventory spaces and equipped + // weapon count + this.limit = { + weapons: 4, + equipment: 12, + skills: 4, + }; + }; + + // MARK: Derived Data + prepareDerivedData() { + super.prepareDerivedData(); + + this.guts.max += Object.values(this.ability).reduce(sumReduce); + + // Movement speeds + this.speed = { + move: this.ability.gait + 3, + run: (this.ability.gait + 3) * 2, + }; + }; + + // #region Getters + get equippedWeapons() { + const weapons = this.parent.itemTypes.weapon; + return weapons.filter(w => w.system.equipped); + }; + + get equippedArmour() { + const armours = this.parent.itemTypes.armour; + const slots = Object.fromEntries( + Object.values(gameTerms.Anatomy).map(v => [v, null]), + ); + for (const armour of armours) { + if (!armour.system.equipped) { continue }; + for (const locationTag of [...armour.system.location.values()]) { + const location = locationTag.toLowerCase(); + slots[location] = armour; + }; + }; + return slots; + }; + + get equippedShield() { + const shields = this.parent.itemTypes.shield; + return shields.find(item => item.system.equipped); + }; + + get defense() { + const defenses = {}; + const armour = this.equippedArmour; + for (const slot in armour) { + defenses[slot] = armour[slot]?.system.protection ?? 0; + }; + + const shield = this.equippedShield; + if (shield) { + for (const location of [...shield.system.location.values()]) { + const slot = location.toLowerCase(); + defenses[slot] += shield.system.protection; + }; + }; + + return defenses; + }; + // #endregion +}; diff --git a/module/data/Actor/Geist.mjs b/module/data/Actor/Geist.mjs new file mode 100644 index 0000000..440392a --- /dev/null +++ b/module/data/Actor/Geist.mjs @@ -0,0 +1,3 @@ +import { EntityData } from "./Entity.mjs"; + +export class GeistData extends EntityData {}; diff --git a/module/data/Actor/Hero.mjs b/module/data/Actor/Hero.mjs index ab5d3d5..6e6c364 100644 --- a/module/data/Actor/Hero.mjs +++ b/module/data/Actor/Hero.mjs @@ -1,191 +1,3 @@ -import { gameTerms } from "../../gameTerms.mjs"; -import { sumReduce } from "../../utils/sumReduce.mjs"; +import { EntityData } from "./Entity.mjs"; -const { fields } = foundry.data; - -export class HeroData extends foundry.abstract.TypeDataModel { - - // MARK: Token Attrs - static get trackableAttributes() { - return { - bar: [ - `guts`, - ], - value: [ - `ability.grit`, - `ability.gait`, - `ability.grip`, - `ability.glim`, - `level.glory`, - `level.step`, - `level.rank`, - ], - }; - }; - - // MARK: Schema - static defineSchema() { - return { - ability: new fields.SchemaField({ - grit: new fields.NumberField({ - min: 0, - initial: 1, - integer: true, - required: true, - nullable: false, - }), - gait: new fields.NumberField({ - min: 0, - initial: 1, - integer: true, - required: true, - nullable: false, - }), - grip: new fields.NumberField({ - min: 0, - initial: 1, - integer: true, - required: true, - nullable: false, - }), - glim: new fields.NumberField({ - min: 0, - initial: 1, - integer: true, - required: true, - nullable: false, - }), - }), - guts: new fields.SchemaField({ - value: new fields.NumberField({ - min: 0, - initial: 5, - integer: true, - nullable: false, - }), - }), - coin: new fields.SchemaField({ - gold: new fields.NumberField({ - initial: 5, - integer: true, - required: true, - nullable: false, - }), - silver: new fields.NumberField({ - initial: 0, - integer: true, - required: true, - nullable: false, - }), - copper: new fields.NumberField({ - initial: 0, - integer: true, - required: true, - nullable: false, - }), - }), - fate: new fields.StringField({ - initial: ``, - blank: true, - trim: true, - nullable: false, - choices: () => { - return Object.values(gameTerms.FatePath).concat(``); - }, - }), - level: new fields.SchemaField({ - glory: new fields.NumberField({ - min: 0, - initial: 0, - integer: true, - required: true, - nullable: false, - }), - step: new fields.NumberField({ - min: 1, - initial: 1, - max: 3, - integer: true, - required: true, - nullable: false, - }), - rank: new fields.StringField({ - initial: gameTerms.Rank.NOVICE, - required: true, - nullable: false, - blank: false, - trim: true, - choices: Object.values(gameTerms.Rank), - }), - }), - }; - }; - - // MARK: Base Data - prepareBaseData() { - super.prepareBaseData(); - - this.guts.max = 0; - - // The limitations imposed on things like inventory spaces and equipped - // weapon count - this.limit = { - weapons: 4, - equipment: 12, - skills: 4, - }; - }; - - // MARK: Derived Data - prepareDerivedData() { - super.prepareDerivedData(); - - this.guts.max += Object.values(this.ability).reduce(sumReduce); - - // Movement speeds - this.speed = { - move: this.ability.gait + 3, - run: (this.ability.gait + 3) * 2, - }; - }; - - // #region Getters - get equippedArmour() { - const armours = this.parent.itemTypes.armour; - const slots = Object.fromEntries( - Object.values(gameTerms.Anatomy).map(v => [v, null]), - ); - for (const armour of armours) { - if (!armour.system.equipped) { continue }; - for (const locationTag of [...armour.system.location.values()]) { - const location = locationTag.toLowerCase(); - slots[location] = armour; - }; - }; - return slots; - }; - - get equippedShield() { - const shields = this.parent.itemTypes.shield; - return shields.find(item => item.system.equipped); - }; - - get defense() { - const defenses = {}; - const armour = this.equippedArmour; - for (const slot in armour) { - defenses[slot] = armour[slot]?.system.protection ?? 0; - }; - - const shield = this.equippedShield; - if (shield) { - for (const location of [...shield.system.location.values()]) { - const slot = location.toLowerCase(); - defenses[slot] += shield.system.protection; - }; - }; - - return defenses; - }; - // #endregion -}; +export class HeroData extends EntityData {}; diff --git a/module/data/Item/Ammo.mjs b/module/data/Item/Ammo.mjs index a3aa54b..0493eb1 100644 --- a/module/data/Item/Ammo.mjs +++ b/module/data/Item/Ammo.mjs @@ -44,6 +44,14 @@ export class AmmoData extends CommonItemData { })), ], }, + { + id: `cost`, + type: `cost`, + label: `RipCrypt.common.cost`, + gold: this.cost.gold, + silver: this.cost.silver, + copper: this.cost.copper, + }, ]; return fields; }; diff --git a/module/data/Item/Armour.mjs b/module/data/Item/Armour.mjs index 5d56510..f2a1b0a 100644 --- a/module/data/Item/Armour.mjs +++ b/module/data/Item/Armour.mjs @@ -4,12 +4,13 @@ import { localizer } from "../../utils/Localizer.mjs"; import { Logger } from "../../utils/Logger.mjs"; import { requiredInteger } from "../helpers.mjs"; +const { diffObject, getProperty, setProperty } = foundry.utils; +const { DialogV2 } = foundry.applications.api; const { fields } = foundry.data; -const { hasProperty, diffObject, mergeObject } = foundry.utils; /** Used for Armour and Shields */ export class ArmourData extends CommonItemData { - // MARK: Schema + // #region Schema static defineSchema() { return { ...super.defineSchema(), @@ -19,16 +20,16 @@ export class ArmourData extends CommonItemData { blank: false, trim: true, nullable: false, + required: true, options: Object.values(gameTerms.Anatomy), }), { nullable: false, - required: true, + initial: [], }, ), equipped: new fields.BooleanField({ initial: false, - required: true, nullable: false, }), weight: new fields.StringField({ @@ -39,27 +40,30 @@ export class ArmourData extends CommonItemData { }), }; }; - - // MARK: Base Data - prepareBaseData() { - super.prepareBaseData(); - }; - - // MARK: Derived Data - prepareDerivedData() { - super.prepareDerivedData(); - }; + // #endregion Schema // #region Lifecycle + async _preCreate(item, options) { + const showEquipPrompt = options.showEquipPrompt ?? true; + if (showEquipPrompt && this.parent.isEmbedded && this._canEquip()) { + const shouldEquip = await DialogV2.confirm({ + window: { title: `Equip Item?` }, + content: `Do you want to equip ${item.name}?`, + }); + if (shouldEquip) { + this.updateSource({ "equipped": true }); + }; + }; + }; + async _preUpdate(changes, options, user) { - // return false if (options.force && game.settings.get(`ripcrypt`, `devMode`)) { return }; // Ensure changes is a diffed object const diff = diffObject(this.parent._source, changes); let valid = await super._preUpdate(changes, options, user); - if (hasProperty(diff, `system.equipped`) && !this._canEquip()) { + if (getProperty(diff, `system.equipped`) && !this._canEquip()) { ui.notifications.error( localizer( `RipCrypt.notifs.error.cannot-equip`, @@ -69,9 +73,7 @@ export class ArmourData extends CommonItemData { ); // Don't stop the update, but don't allow changing the equipped status - mergeObject(changes, { - "system.equipped": false, - }); + setProperty(changes, `system.equipped`, false); // Set a flag so that we can tell the sheet that it needs to rerender this.forceRerender = true; @@ -79,8 +81,13 @@ export class ArmourData extends CommonItemData { return valid; }; + // #endregion Lifecycle - /** Used to tell the preUpdate logic whether or not to prevent the */ + // #region Helpers + /** + * Used to tell the preUpdate logic whether or not to prevent the item from + * being equipped or not. + */ _canEquip() { const parent = this.parent; if (!parent.isEmbedded || !(parent.parent instanceof Actor)) { @@ -94,7 +101,7 @@ export class ArmourData extends CommonItemData { }; const slots = parent.parent.system.equippedArmour ?? {}; - Logger.debug(`slots`, slots); + for (const locationTag of this.location) { if (slots[locationTag.toLowerCase()] != null) { Logger.error(`Unable to equip multiple items in the same slot`); @@ -103,89 +110,9 @@ export class ArmourData extends CommonItemData { }; return true; }; - // #endregion - // #region Getters get locationString() { return [...this.location].join(`, `); }; - // #endregion - - // #region Sheet Data - getFormFields(_ctx) { - const fields = [ - { - id: `quantity`, - type: `integer`, - label: `RipCrypt.common.quantity`, - path: `system.quantity`, - value: this.quantity, - min: 0, - }, - { - id: `access`, - type: `dropdown`, - label: `RipCrypt.common.access`, - path: `system.access`, - value: this.access, - limited: false, - options: [ - { - label: `RipCrypt.common.empty`, - value: ``, - }, - ...gameTerms.Access.map(opt => ({ - label: `RipCrypt.common.accessLevels.${opt}`, - value: opt, - })), - ], - }, - { - id: `weight`, - type: `dropdown`, - label: `RipCrypt.common.weightRating`, - path: `system.weight`, - value: this.weight, - options: [ - { - label: `RipCrypt.common.empty`, - value: null, - }, - ...Object.values(gameTerms.WeightRatings).map(opt => ({ - label: `RipCrypt.common.weightRatings.${opt}`, - value: opt, - })), - ], - }, - { - id: `location`, - type: `string-set`, - label: `RipCrypt.common.location`, - placeholder: `RipCrypt.Apps.location-placeholder`, - path: `system.location`, - value: this.locationString, - }, - { - id: `protection`, - type: `integer`, - label: `RipCrypt.common.protection`, - value: this.protection, - path: `system.protection`, - min: 0, - }, - ]; - - if (this.parent.isEmbedded) { - fields.push({ - id: `equipped`, - type: `boolean`, - label: `RipCrypt.common.equipped`, - value: this.equipped, - path: `system.equipped`, - }); - }; - - return fields; - }; - // #endregion + // #endregion Helpers }; diff --git a/module/data/Item/Common.mjs b/module/data/Item/Common.mjs index fe60d96..bf37c0e 100644 --- a/module/data/Item/Common.mjs +++ b/module/data/Item/Common.mjs @@ -1,5 +1,5 @@ +import { optionalInteger, requiredInteger } from "../helpers.mjs"; import { gameTerms } from "../../gameTerms.mjs"; -import { requiredInteger } from "../helpers.mjs"; const { fields } = foundry.data; @@ -14,6 +14,11 @@ export class CommonItemData extends foundry.abstract.TypeDataModel { trim: true, choices: gameTerms.Access, }), + cost: new fields.SchemaField({ + gold: optionalInteger(), + silver: optionalInteger(), + copper: optionalInteger(), + }), }; }; diff --git a/module/data/Item/Good.mjs b/module/data/Item/Good.mjs index 0194609..9af246b 100644 --- a/module/data/Item/Good.mjs +++ b/module/data/Item/Good.mjs @@ -59,6 +59,14 @@ export class GoodData extends CommonItemData { })), ], }, + { + id: `cost`, + type: `cost`, + label: `RipCrypt.common.cost`, + gold: this.cost.gold, + silver: this.cost.silver, + copper: this.cost.copper, + }, { id: `description`, type: `prosemirror`, diff --git a/module/data/Item/Weapon.mjs b/module/data/Item/Weapon.mjs index 5242f25..ec35f29 100644 --- a/module/data/Item/Weapon.mjs +++ b/module/data/Item/Weapon.mjs @@ -2,12 +2,14 @@ import { barAttribute, optionalInteger, requiredInteger } from "../helpers.mjs"; import { CommonItemData } from "./Common.mjs"; import { gameTerms } from "../../gameTerms.mjs"; import { localizer } from "../../utils/Localizer.mjs"; +import { Logger } from "../../utils/Logger.mjs"; +const { diffObject, getProperty, setProperty } = foundry.utils; +const { DialogV2 } = foundry.applications.api; const { fields } = foundry.data; -const { hasProperty, mergeObject } = foundry.utils; export class WeaponData extends CommonItemData { - // MARK: Schema + // #region Schema static defineSchema() { return { ...super.defineSchema(), @@ -41,39 +43,69 @@ export class WeaponData extends CommonItemData { }), }; }; - - // MARK: Base Data - prepareBaseData() { - super.prepareBaseData(); - }; - - // MARK: Derived Data - prepareDerivedData() { - super.prepareDerivedData(); - }; + // #endregion Schema // #region Lifecycle + async _preCreate(item, options) { + const showEquipPrompt = options.showEquipPrompt ?? true; + if (showEquipPrompt && this.parent.isEmbedded && this._canEquip()) { + const shouldEquip = await DialogV2.confirm({ + window: { title: `Equip Item?` }, + content: `Do you want to equip ${item.name}?`, + }); + if (shouldEquip) { + this.updateSource({ "equipped": true }); + }; + }; + }; + + /** + * + * @param {*} changes The expanded object that was used for the update + * @param {*} options + * @param {*} user + * @returns + */ async _preUpdate(changes, options, user) { if (options.force && game.settings.get(`ripcrypt`, `devMode`)) { return }; + + const diff = diffObject(this.parent._source, changes); let valid = super._preUpdate(changes, options, user); - if (hasProperty(changes, `system.equipped`) && !this.parent.isEmbedded) { + if (getProperty(diff, `system.equipped`) && !this._canEquip()) { ui.notifications.error(localizer( - `RipCrypt.notifs.error.cannot-equip-not-embedded`, + `RipCrypt.notifs.error.cannot-equip`, { itemType: `@TYPES.Item.${this.parent.type}` }, )); - mergeObject( - changes, - { "-=system.equipped": null }, - { inplace: true, performDeletions: true }, - ); - return false; + + // Don't stop the update, but don't allow changing the equipped status + setProperty(changes, `system.equipped`, false); + + // Set a flag so that we can tell the sheet that it needs to rerender + this.forceRerender = true; }; return valid; }; - // #endregion + // #endregion Lifecycle + + // #region Helpers + /** + * Used to tell the preUpdate logic whether or not to prevent the item from + * being equipped or not. + */ + _canEquip() { + const parent = this.parent; + if (!parent.isEmbedded || !(parent.parent instanceof Actor)) { + Logger.error(`Unable to equip item when it's not embedded`); + return false; + }; + const actor = this.parent.parent.system; + if (actor.equippedWeapons?.length >= actor.limit.weapons) { + return false; + }; + return true; + }; - // #region Getters get traitString() { return [...this.traits].join(`, `); }; @@ -84,7 +116,7 @@ export class WeaponData extends CommonItemData { }; return String(this.range.short ?? this.range.long ?? ``); }; - // #endregion + // #endregion Helpers // #region Sheet Data async getFormFields(_ctx) { @@ -115,6 +147,14 @@ export class WeaponData extends CommonItemData { })), ], }, + { + id: `cost`, + type: `cost`, + label: `RipCrypt.common.cost`, + gold: this.cost.gold, + silver: this.cost.silver, + copper: this.cost.copper, + }, { id: `weight`, type: `dropdown`, @@ -159,7 +199,8 @@ export class WeaponData extends CommonItemData { value: this.range.long, }, }); - } else { + } + else { fields.push({ id: `short-range`, type: `integer`, @@ -217,5 +258,5 @@ export class WeaponData extends CommonItemData { return fields; }; - // #endregion + // #endregion Sheet Data }; diff --git a/module/data/helpers.mjs b/module/data/helpers.mjs index 2321503..e7c705f 100644 --- a/module/data/helpers.mjs +++ b/module/data/helpers.mjs @@ -19,6 +19,17 @@ export function barAttribute(min, initial, max = undefined) { }); }; +export function derivedMaximumBar(min, initial) { + return new fields.SchemaField({ + value: new fields.NumberField({ + min, + initial, + integer: true, + nullable: false, + }), + }); +}; + export function optionalInteger({min, initial = null, max} = {}) { return new fields.NumberField({ min, diff --git a/module/dice/CryptDie.mjs b/module/dice/CryptDie.mjs index 9f304d2..4da7ce7 100644 --- a/module/dice/CryptDie.mjs +++ b/module/dice/CryptDie.mjs @@ -38,7 +38,8 @@ export class CryptDie extends Die { if (almostCrypted) { this.ripCryptState = `crypted`; break; - } else { + } + else { almostCrypted = true; } } diff --git a/module/documents/combat.mjs b/module/documents/combat.mjs index fa76361..f2aeb4b 100644 --- a/module/documents/combat.mjs +++ b/module/documents/combat.mjs @@ -7,7 +7,7 @@ Resources: export class RipCryptCombat extends Combat { - get groups() { + get customGroups() { let groups = new Map(); for (const combatant of this.combatants) { @@ -16,7 +16,8 @@ export class RipCryptCombat extends Combat { if (groups.has(groupKey)) { groups.get(groupKey).push(combatant); - } else { + } + else { groups.set(groupKey, [combatant]); }; }; @@ -129,8 +130,9 @@ export class RipCryptCombat extends Combat { const currentToken = this.combatant?.token?._object; if (!tokenGroup && currentToken) { currentToken.renderFlags.set({refreshTurnMarker: true}); - } else { - const group = this.groups.get(tokenGroup) ?? []; + } + else { + const group = this.customGroups.get(tokenGroup) ?? []; for (const combatant of group) { combatant.token?._object?.renderFlags.set({ refreshTurnMarker: true }); } diff --git a/module/documents/combatant.mjs b/module/documents/combatant.mjs index 6de765b..128f042 100644 --- a/module/documents/combatant.mjs +++ b/module/documents/combatant.mjs @@ -1,4 +1,4 @@ -import { distanceBetweenFates } from "../utils/distanceBetweenFates.mjs"; +import { distanceBetweenFates } from "../utils/fates.mjs"; export class RipCryptCombatant extends Combatant { @@ -28,7 +28,8 @@ export class RipCryptCombatant extends Combatant { const disposition = this.disposition; if (disposition === `unknown`) { total += 0.25; - } else if (whoFirst !== disposition) { + } + else if (whoFirst !== disposition) { total += 0.5; }; }; diff --git a/module/documents/token.mjs b/module/documents/token.mjs index c4913fe..9492339 100644 --- a/module/documents/token.mjs +++ b/module/documents/token.mjs @@ -26,7 +26,8 @@ export class RipCryptToken extends Token { }; canvas.tokens.turnMarkers.add(this); this.turnMarker.draw(); - } else if (this.turnMarker) { + } + else if (this.turnMarker) { canvas.tokens.turnMarkers.delete(this); this.turnMarker.destroy(); this.turnMarker = null; diff --git a/module/flags/item.mjs b/module/flags/item.mjs new file mode 100644 index 0000000..de53d3b --- /dev/null +++ b/module/flags/item.mjs @@ -0,0 +1,4 @@ +export const ItemFlags = Object.freeze({ + /** The boolean value to indicate if an item is considered favourited/starred or not */ + FAVOURITE: `favourited`, +}); diff --git a/module/gameTerms.mjs b/module/gameTerms.mjs index 5bcbe71..86507ad 100644 --- a/module/gameTerms.mjs +++ b/module/gameTerms.mjs @@ -37,6 +37,7 @@ export const gameTerms = Object.preventExtensions({ }), /** The types of items that contribute to the gear limit */ gearItemTypes: new Set([ + `ammo`, `armour`, `weapon`, `shield`, diff --git a/module/handlebarHelpers/inputs/currency.mjs b/module/handlebarHelpers/inputs/currency.mjs new file mode 100644 index 0000000..12b6ca8 --- /dev/null +++ b/module/handlebarHelpers/inputs/currency.mjs @@ -0,0 +1,33 @@ +import { groupInput } from "./groupInput.mjs"; + +export function costInput(input, data) { + return groupInput({ + title: input.label, + fields: [ + { + id: input.id + `-gold`, + type: `integer`, + label: `RipCrypt.common.currency.gold`, + value: input.gold, + path: `system.cost.gold`, + limited: input.limited, + }, + { + id: input.id + `-silver`, + type: `integer`, + label: `RipCrypt.common.currency.silver`, + value: input.silver, + path: `system.cost.silver`, + limited: input.limited, + }, + { + id: input.id + `-copper`, + type: `integer`, + label: `RipCrypt.common.currency.copper`, + value: input.copper, + path: `system.cost.copper`, + limited: input.limited, + }, + ], + }, data); +}; diff --git a/module/handlebarHelpers/inputs/formFields.mjs b/module/handlebarHelpers/inputs/formFields.mjs index 518b054..d8cd18e 100644 --- a/module/handlebarHelpers/inputs/formFields.mjs +++ b/module/handlebarHelpers/inputs/formFields.mjs @@ -1,5 +1,6 @@ import { barInput } from "./barInput.mjs"; import { booleanInput } from "./booleanInput.mjs"; +import { costInput } from "./currency.mjs"; import { dropdownInput } from "./dropdownInput.mjs"; import { groupInput } from "./groupInput.mjs"; import { numberInput } from "./numberInput.mjs"; @@ -18,6 +19,7 @@ const inputTypes = { boolean: booleanInput, group: groupInput, text: textInput, + cost: costInput, }; const typesToSanitize = new Set([ `string`, `number` ]); diff --git a/module/handlebarHelpers/inputs/groupInput.mjs b/module/handlebarHelpers/inputs/groupInput.mjs index d6a5c45..59f26e8 100644 --- a/module/handlebarHelpers/inputs/groupInput.mjs +++ b/module/handlebarHelpers/inputs/groupInput.mjs @@ -16,7 +16,7 @@ export function groupInput(input, data) { data-input-type="group" var:border-color="${input.borderColor ?? `var(--accent-1)`}" var:vertical-displacement="${input.verticalDisplacement ?? `12px`}" - var:padding-top="${input.paddingTop ?? `16px`}" + var:padding-top="${input.paddingTop ?? `20px`}" >
${title}
diff --git a/module/hooks/init.mjs b/module/hooks/init.mjs index 812c6e2..28c4fd9 100644 --- a/module/hooks/init.mjs +++ b/module/hooks/init.mjs @@ -1,15 +1,18 @@ // Applications import { AllItemSheetV1 } from "../Apps/ItemSheets/AllItemSheetV1.mjs"; +import { ArmourSheet } from "../Apps/ItemSheets/ArmourSheet.mjs"; import { CombinedHeroSheet } from "../Apps/ActorSheets/CombinedHeroSheet.mjs"; -import { DelveTourApp } from "../Apps/DelveTourApp.mjs"; -import { HeroSkillsCardV1 } from "../Apps/ActorSheets/HeroSkillsCardV1.mjs"; -import { HeroSummaryCardV1 } from "../Apps/ActorSheets/HeroSummaryCardV1.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"; // Data Models import { AmmoData } from "../data/Item/Ammo.mjs"; import { ArmourData } from "../data/Item/Armour.mjs"; import { CraftData } from "../data/Item/Craft.mjs"; +import { GeistData } from "../data/Actor/Geist.mjs"; import { GoodData } from "../data/Item/Good.mjs"; import { HeroData } from "../data/Actor/Hero.mjs"; import { ShieldData } from "../data/Item/Shield.mjs"; @@ -28,17 +31,20 @@ import { RipCryptToken } from "../documents/token.mjs"; // Misc import helpers from "../handlebarHelpers/_index.mjs"; import { Logger } from "../utils/Logger.mjs"; -import { registerCustomComponents } from "../Apps/elements/_index.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"; import { registerWorldSettings } from "../settings/worldSettings.mjs"; +const { Items, Actors } = foundry.documents.collections; + Hooks.once(`init`, () => { Logger.log(`Initializing`); CONFIG.Combat.initiative.decimals = 2; - CONFIG.ui.crypt = DelveTourApp; + CONFIG.ui.delveDice = DelveDiceHUD; // #region Settings registerMetaSettings(); @@ -49,7 +55,8 @@ Hooks.once(`init`, () => { // #region Datamodels CONFIG.Actor.dataModels.hero = HeroData; - CONFIG.Item.dataModels.ammo = AmmoData, + CONFIG.Actor.dataModels.geist = GeistData; + CONFIG.Item.dataModels.ammo = AmmoData; CONFIG.Item.dataModels.armour = ArmourData; CONFIG.Item.dataModels.craft = CraftData; CONFIG.Item.dataModels.good = GoodData; @@ -68,12 +75,6 @@ Hooks.once(`init`, () => { // #endregion // #region Sheets - // Unregister core sheets - /* eslint-disable no-undef */ - Items.unregisterSheet(`core`, ItemSheet); - Actors.unregisterSheet(`core`, ActorSheet); - /* eslint-enabled no-undef */ - // #region Actors Actors.registerSheet(game.system.id, CombinedHeroSheet, { makeDefault: true, @@ -81,15 +82,26 @@ Hooks.once(`init`, () => { label: `RipCrypt.sheet-names.CombinedHeroSheet`, themes: CombinedHeroSheet.themes, }); - Actors.registerSheet(game.system.id, HeroSummaryCardV1, { + Actors.registerSheet(game.system.id, StatsCardV1, { types: [`hero`], - label: `RipCrypt.sheet-names.HeroSummaryCardV1`, - themes: HeroSummaryCardV1.themes, + label: `RipCrypt.sheet-names.StatsCardV1`, + themes: StatsCardV1.themes, }); - Actors.registerSheet(game.system.id, HeroSkillsCardV1, { - types: [`hero`], - label: `RipCrypt.sheet-names.HeroSkillsCardV1`, - themes: HeroSkillsCardV1.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`], + label: `RipCrypt.sheet-names.SkillsCardV1`, + themes: SkillsCardV1.themes, + }); + Actors.registerSheet(game.system.id, CraftCardV1, { + types: [`hero`, `geist`], + label: `RipCrypt.sheet-names.CraftCardV1`, + themes: CraftCardV1.themes, }); // #endregion @@ -99,6 +111,16 @@ Hooks.once(`init`, () => { label: `RipCrypt.sheet-names.AllItemsSheetV1`, themes: AllItemSheetV1.themes, }); + + Items.registerSheet(game.system.id, ArmourSheet, { + makeDefault: true, + types: [`armour`, `shield`], + label: `RipCrypt.sheet-names.ArmourSheet`, + themes: ArmourSheet.themes, + }); + Items.unregisterSheet(game.system.id, AllItemSheetV1, { + types: [`armour`, `shield`], + }); // #endregion // #endregion @@ -106,6 +128,7 @@ Hooks.once(`init`, () => { CONFIG.Actor.trackableAttributes.hero = HeroData.trackableAttributes; // #endregion + registerSockets(); registerCustomComponents(); Handlebars.registerHelper(helpers); }); diff --git a/module/hooks/ready.mjs b/module/hooks/ready.mjs index c71bc25..e77e0e7 100644 --- a/module/hooks/ready.mjs +++ b/module/hooks/ready.mjs @@ -6,22 +6,21 @@ Hooks.once(`ready`, () => { let defaultTab = game.settings.get(`ripcrypt`, `defaultTab`); if (defaultTab) { - if (!ui.sidebar?.TABS?.[defaultTab]) { - Logger.error(`Couldn't find a sidebar tab with ID:`, defaultTab); - } else { + try { Logger.debug(`Switching sidebar tab to:`, defaultTab); - ui.sidebar.activateTab(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() }; + if (game.paused) { game.togglePause(false, { broadcast: true }) }; }; - if (game.settings.get(`ripcrypt`, `showDelveTour`)) { - ui.crypt.render({ force: true }); - }; + ui.delveDice.render({ force: true }); // MARK: 1-time updates if (!game.settings.get(`ripcrypt`, `firstLoadFinished`)) { @@ -30,7 +29,7 @@ Hooks.once(`ready`, () => { combatConfig.turnMarker.src = filePath(`assets/turn-marker.png`); combatConfig.turnMarker.animation = `spinPulse`; game.settings.set(`core`, `combatTrackerConfig`, combatConfig); - } + }; game.settings.set(`ripcrypt`, `firstLoadFinished`, true); }); diff --git a/module/settings/metaSettings.mjs b/module/settings/metaSettings.mjs index 7694088..5acdbe9 100644 --- a/module/settings/metaSettings.mjs +++ b/module/settings/metaSettings.mjs @@ -1,23 +1,42 @@ +import { gameTerms } from "../gameTerms.mjs"; + +const { StringField } = foundry.data.fields; +const { FatePath } = gameTerms; + export function registerMetaSettings() { game.settings.register(`ripcrypt`, `dc`, { scope: `world`, type: Number, + default: 5, config: false, requiresReload: false, onChange: () => { - ui.crypt.render({ parts: [ `delveConditions` ]}); + ui.delveDice.render({ parts: [`difficulty`] }); + }, + }); + + game.settings.register(`ripcrypt`, `sandsOfFate`, { + scope: `world`, + type: Number, + default: 8, + config: false, + requiresReload: false, + onChange: async () => { + ui.delveDice.animate({ parts: [`sandsOfFate`] }); }, }); game.settings.register(`ripcrypt`, `currentFate`, { scope: `world`, - type: String, + type: new StringField({ + blank: false, + nullable: false, + initial: FatePath.NORTH, + }), config: false, requiresReload: false, onChange: async () => { - await ui.crypt.render({ parts: [ `fate` ] }); - await game.combat.setupTurns(); - await ui.combat.render({ parts: [ `tracker` ] }); + ui.delveDice.animate({ parts: [`fateCompass`] }); }, }); @@ -26,9 +45,9 @@ export function registerMetaSettings() { type: String, config: false, requiresReload: false, - initial: `friendly`, + default: `friendly`, onChange: async () => { - await game.combat.setupTurns(); + await game.combat?.setupTurns(); await ui.combat.render({ parts: [ `tracker` ] }); }, }); @@ -36,7 +55,7 @@ export function registerMetaSettings() { game.settings.register(`ripcrypt`, `firstLoadFinished`, { scope: `world`, type: Boolean, - initial: false, + default: false, requiresReload: false, }); }; diff --git a/module/settings/userSettings.mjs b/module/settings/userSettings.mjs index 6dca4c4..400ec7c 100644 --- a/module/settings/userSettings.mjs +++ b/module/settings/userSettings.mjs @@ -1,20 +1,20 @@ export function registerUserSettings() { - const userScope = game.release.generation >= 13 ? `user` : `client`; - + /* ! Non-Functional game.settings.register(`ripcrypt`, `abbrAccess`, { name: `RipCrypt.setting.abbrAccess.name`, hint: `RipCrypt.setting.abbrAccess.hint`, - scope: userScope, + scope: `user`, type: Boolean, config: true, default: false, requiresReload: false, }); + */ game.settings.register(`ripcrypt`, `condensedRange`, { name: `RipCrypt.setting.condensedRange.name`, hint: `RipCrypt.setting.condensedRange.hint`, - scope: userScope, + scope: `user`, type: Boolean, config: true, default: true, diff --git a/module/settings/worldSettings.mjs b/module/settings/worldSettings.mjs index d7c426a..ac1643c 100644 --- a/module/settings/worldSettings.mjs +++ b/module/settings/worldSettings.mjs @@ -1,10 +1,52 @@ +const { NumberField, StringField } = foundry.data.fields; + export function registerWorldSettings() { - game.settings.register(`ripcrypt`, `showDelveTour`, { - name: `Delve Tour Popup`, + game.settings.register(`ripcrypt`, `sandsOfFateInitial`, { + name: `RipCrypt.setting.sandsOfFateInitial.name`, + hint: `RipCrypt.setting.sandsOfFateInitial.hint`, scope: `world`, - type: Boolean, config: true, - default: true, requiresReload: false, + type: new NumberField({ + required: true, + min: 1, + step: 1, + max: 10, + initial: 8, + }), + onChange: async (newInitialSands) => { + const currentSands = game.settings.get(`ripcrypt`, `sandsOfFate`); + if (newInitialSands <= currentSands) { + game.settings.set(`ripcrypt`, `sandsOfFate`, newInitialSands); + }; + }, + }); + + game.settings.register(`ripcrypt`, `onCrypticEvent`, { + name: `RipCrypt.setting.onCrypticEvent.name`, + hint: `RipCrypt.setting.onCrypticEvent.hint`, + scope: `world`, + config: true, + requiresReload: false, + type: new StringField({ + required: true, + initial: `notif`, + choices: { + "notif": `RipCrypt.setting.onCrypticEvent.options.notif`, + "pause": `RipCrypt.setting.onCrypticEvent.options.pause`, + "both": `RipCrypt.setting.onCrypticEvent.options.both`, + "nothing": `RipCrypt.setting.onCrypticEvent.options.nothing`, + }, + }), + }); + + game.settings.register(`ripcrypt`, `allowUpdateSandsSocket`, { + name: `RipCrypt.setting.allowUpdateSandsSocket.name`, + hint: `RipCrypt.setting.allowUpdateSandsSocket.hint`, + scope: `world`, + config: true, + requiresReload: false, + type: Boolean, + default: true, }); }; diff --git a/module/sockets/_index.mjs b/module/sockets/_index.mjs new file mode 100644 index 0000000..bcb2b5e --- /dev/null +++ b/module/sockets/_index.mjs @@ -0,0 +1,29 @@ +import { localizer } from "../utils/Localizer.mjs"; +import { Logger } from "../utils/Logger.mjs"; +import { notify } from "./notify.mjs"; +import { updateSands } from "./updateSands.mjs"; + +const events = { + notify, + updateSands, +}; + +export function registerSockets() { + Logger.info(`Setting up socket listener`); + + game.socket.on(`system.ripcrypt`, (data, userID) => { + const { event, payload } = data ?? {}; + if (event == null || payload === undefined) { + ui.notifications.error(localizer(`RipCrypt.notifs.error.invalid-socket`)); + return; + }; + + if (events[event] == null) { + ui.notifications.error(localizer(`RipCrypt.notifs.error.unknown-socket-event`, { event })); + return; + }; + + const user = game.users.get(userID); + events[event](payload, user); + }); +}; diff --git a/module/sockets/notify.mjs b/module/sockets/notify.mjs new file mode 100644 index 0000000..072e5af --- /dev/null +++ b/module/sockets/notify.mjs @@ -0,0 +1,56 @@ +import { localizer } from "../utils/Localizer.mjs"; + +export function notify(payload) { + // #region Payload Validity + const { + message, + users = [], + type = `info`, + permanent = false, + } = payload; + + if (!message) { + ui.notifications.error(localizer( + `RipCrypt.notifs.error.malformed-socket-payload`, + { + event: `notify`, + details: `A message must be provided`, + }, + )); + return; + }; + + if (users && !Array.isArray(users)) { + ui.notifications.error(localizer( + `RipCrypt.notifs.error.malformed-socket-payload`, + { + event: `notify`, + details: `"users" must be an array of user IDs`, + }, + )); + return; + }; + + if (![`info`, `error`, `success`].includes(type)) { + ui.notifications.error(localizer( + `RipCrypt.notifs.error.malformed-socket-payload`, + { + event: `notify`, + details: `An invalid notification type was provided.`, + }, + )); + return; + } + // #endregion Payload Validity + + // Act + if (users.length === 0 || users.includes(game.user.id)) { + ui.notifications[type]?.( + localizer(message), + { + console: false, + permanent, + }, + ); + }; +}; diff --git a/module/sockets/updateSands.mjs b/module/sockets/updateSands.mjs new file mode 100644 index 0000000..8560b6d --- /dev/null +++ b/module/sockets/updateSands.mjs @@ -0,0 +1,38 @@ +import { clamp } from "../utils/clamp.mjs"; +import { localizer } from "../utils/Localizer.mjs"; + +export function updateSands(payload) { + if (!game.user.isActiveGM) { return }; + if (!game.settings.get(game.system.id, `allowUpdateSandsSocket`)) { return }; + + // Assert payload validity + const { value, delta } = payload; + if (value == null && delta == null) { + ui.notifications.error(localizer( + `RipCrypt.notifs.error.malformed-socket-payload`, + { + event: `updateSands`, + details: `Either value or delta must be provided`, + }, + )); + return; + }; + + // Take action + if (value != null) { + const initial = game.settings.get(game.system.id, `sandsOfFateInitial`); + let sands = clamp(0, value, initial); + if (sands === 0) { + ui.delveDice.alertCrypticEvent(); + sands = initial; + }; + game.settings.set( + game.system.id, + `sandsOfFate`, + sands, + ); + } + else if (delta != null) { + ui.delveDice.sandsOfFateDelta(delta); + }; +}; diff --git a/module/utils/PopoverEventManager.mjs b/module/utils/PopoverEventManager.mjs new file mode 100644 index 0000000..07e4793 --- /dev/null +++ b/module/utils/PopoverEventManager.mjs @@ -0,0 +1,184 @@ +import { getTooltipDelay } from "../consts.mjs"; +import { Logger } from "./Logger.mjs"; + +export class PopoverEventManager { + #options; + #id; + + get id() { + return this.#id; + }; + + /** @type {Map} */ + static #existing = new Map(); + + /** + * @param {HTMLElement} element The element to attach the listeners to. + * @param {GenericPopoverMixin} popoverClass The class reference that represents the popover app + */ + constructor(id, element, popoverClass, options = {}) { + id = `${id}-${popoverClass.name}`; + this.#id = id; + + if (PopoverEventManager.#existing.has(id)) { + const manager = PopoverEventManager.#existing.get(id); + manager.#addListeners(element); + return manager; + }; + + options.managerId = id; + options.locked ??= false; + options.lockable ??= true; + + this.#options = options; + this.#element = element; + this.#class = popoverClass; + + this.#addListeners(element); + PopoverEventManager.#existing.set(id, this); + }; + + /** + * @param {HTMLElement} element + */ + #addListeners(element) { + element.addEventListener(`pointerenter`, this.#pointerEnterHandler.bind(this)); + element.addEventListener(`pointerout`, this.#pointerOutHandler.bind(this)); + element.addEventListener(`click`, this.#clickHandler.bind(this)); + + if (this.#options.lockable) { + element.addEventListener(`pointerup`, this.#pointerUpHandler.bind(this)); + }; + }; + + destroy() { + this.close(); + this.#element.removeEventListener(`pointerenter`, this.#pointerEnterHandler); + this.#element.removeEventListener(`pointerout`, this.#pointerOutHandler); + this.#element.removeEventListener(`click`, this.#clickHandler); + if (this.#options.lockable) { + this.#element.removeEventListener(`pointerup`, this.#pointerUpHandler); + }; + this.#stopOpen(); + this.#stopClose(); + }; + + close() { + this.#frameless?.close({ force: true }); + this.#framed?.close({ force: true }); + }; + + #stopOpen() { + if (this.#openTimeout != null) { + clearTimeout(this.#openTimeout); + this.#openTimeout = null; + }; + }; + + #stopClose() { + if (this.#closeTimeout != null) { + clearTimeout(this.#closeTimeout); + this.#closeTimeout = null; + } + }; + + get rendered() { + return Boolean(this.#frameless?.rendered || this.#framed?.rendered); + }; + + render(options) { + if (this.#framed?.rendered) { + this.#framed.render(options); + }; + if (this.#frameless?.rendered) { + this.#frameless.render(options); + }; + }; + + #element; + #class; + #openTimeout = null; + #closeTimeout = null; + + #frameless; + #framed; + + #construct(options) { + options.popover ??= {}; + options.popover.managerId = this.#id; + + return new this.#class(options); + }; + + #clickHandler() { + Logger.debug(`click event handler`); + // Cleanup for the frameless lifecycle + this.#stopOpen(); + this.#stopClose(); + this.#frameless?.close({ force: true }); + + if (!this.#framed) { + this.#framed = this.#construct({ popover: { ...this.#options, framed: true } }); + } + this.#framed?.render({ force: true }); + }; + + #pointerEnterHandler(event) { + this.#stopClose(); + + const pos = event.target.getBoundingClientRect(); + const x = pos.x + Math.floor(pos.width / 2); + const y = pos.y; + + this.#openTimeout = setTimeout( + () => { + this.#openTimeout = null; + + // When we have the framed version rendered, we might as well just focus + // it instead of rendering a new application + if (this.#framed?.rendered) { + this.#framed.bringToFront(); + return; + }; + + // When the frameless is already rendered, we should just move it to the + // new location instead of spawning a new one + if (this.#frameless?.rendered) { + const { width, height } = this.#frameless.element.getBoundingClientRect(); + const top = y - height; + const left = x - Math.floor(width / 2); + this.#frameless.setPosition({ left, top }); + return; + } + + this.#frameless = this.#construct({ + popover: { + ...this.#options, + framed: false, + x, y, + }, + }); + this.#frameless?.render({ force: true }); + }, + getTooltipDelay(), + ); + }; + + #pointerOutHandler() { + this.#stopOpen(); + + this.#closeTimeout = setTimeout( + () => { + this.#closeTimeout = null; + this.#frameless?.close(); + }, + getTooltipDelay(), + ); + }; + + #pointerUpHandler(event) { + if (event.button !== 1 || !this.#frameless?.rendered || Tour.tourInProgress) { return }; + event.preventDefault(); + this.#frameless.toggleLock(); + }; +}; diff --git a/module/utils/clamp.mjs b/module/utils/clamp.mjs new file mode 100644 index 0000000..94cac4e --- /dev/null +++ b/module/utils/clamp.mjs @@ -0,0 +1,3 @@ +export function clamp(min, ideal, max) { + return Math.max(min, Math.min(ideal, max)); +}; diff --git a/module/utils/distanceBetweenFates.mjs b/module/utils/fates.mjs similarity index 50% rename from module/utils/distanceBetweenFates.mjs rename to module/utils/fates.mjs index 4eaaddd..d1fb6dd 100644 --- a/module/utils/distanceBetweenFates.mjs +++ b/module/utils/fates.mjs @@ -14,14 +14,38 @@ export function distanceBetweenFates(start, end) { return undefined; }; - if (isOppositeFates(start, end)) { + if (start === end) { + return 0; + }; + + if (isOppositeFates(start, end) || isOppositeFates(end, start)) { return 2; }; let isForward = start === FatePath.SOUTH && end === FatePath.WEST; isForward ||= start === FatePath.NORTH && end === FatePath.EAST; + isForward ||= start === FatePath.WEST && end === FatePath.NORTH; + isForward ||= start === FatePath.EAST && end === FatePath.SOUTH; if (isForward) { return 1; }; return 3; }; + +const fateOrder = [ + FatePath.WEST, // to make the .find not integer overflow + FatePath.NORTH, + FatePath.EAST, + FatePath.SOUTH, + FatePath.WEST, +]; + +export function nextFate(fate) { + const fateIndex = fateOrder.findIndex(f => f === fate); + return fateOrder[fateIndex + 1]; +}; + +export function previousFate(fate) { + const fateIndex = fateOrder.lastIndexOf(fate); + return fateOrder[fateIndex - 1]; +}; diff --git a/module/utils/rank.mjs b/module/utils/rank.mjs new file mode 100644 index 0000000..81f6b59 --- /dev/null +++ b/module/utils/rank.mjs @@ -0,0 +1,13 @@ +import { gameTerms } from "../gameTerms.mjs"; + +/** + * Converts a rank's name into an integer form for use in mathematical calculations + * that rely on rank. + * + * @param {Novice|Adept|Expert|Master} rankName The rank to convert into an integer + * @returns An integer between 1 and 4 + */ +export function rankToInteger(rankName) { + return Object.values(gameTerms.Rank) + .findIndex(r => r === rankName) + 1; +}; diff --git a/package-lock.json b/package-lock.json index 9384e26..af1a200 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "@eslint/js": "^9.16.0", "@foundryvtt/foundryvtt-cli": "^1.0.3", "@stylistic/eslint-plugin": "^2.12.0", + "dotenv": "^17.2.3", "eslint": "^9.16.0" } }, @@ -775,6 +776,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", diff --git a/package.json b/package.json index 9b335a7..5fa77f1 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,13 @@ "@eslint/js": "^9.16.0", "@foundryvtt/foundryvtt-cli": "^1.0.3", "@stylistic/eslint-plugin": "^2.12.0", + "dotenv": "^17.2.3", "eslint": "^9.16.0" }, "scripts": { + "data:build": "node scripts/buildCompendia.mjs", + "data:extract": "node scripts/extractCompendia.mjs", + "link": "node scripts/linkFoundry.mjs", "lint": "eslint --fix", "lint:nofix": "eslint" } diff --git a/packs/protection/_source/Armour_pZxc6QLgVWfnZlf7.json b/packs/protection/_source/Armour_pZxc6QLgVWfnZlf7.json new file mode 100644 index 0000000..6f80750 --- /dev/null +++ b/packs/protection/_source/Armour_pZxc6QLgVWfnZlf7.json @@ -0,0 +1,23 @@ +{ + "type": "Item", + "folder": null, + "name": "Armour", + "color": "#04262a", + "sorting": "m", + "_id": "pZxc6QLgVWfnZlf7", + "description": "", + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759994081362, + "modifiedTime": 1759994081362, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!folders!pZxc6QLgVWfnZlf7" +} diff --git a/packs/protection/_source/Breastplate_KQ6uyTPUOHuMTxDF.json b/packs/protection/_source/Breastplate_KQ6uyTPUOHuMTxDF.json new file mode 100644 index 0000000..75dd4a1 --- /dev/null +++ b/packs/protection/_source/Breastplate_KQ6uyTPUOHuMTxDF.json @@ -0,0 +1,41 @@ +{ + "folder": "BsNUpCnwmlhOWBhZ", + "name": "Breastplate", + "type": "armour", + "_id": "KQ6uyTPUOHuMTxDF", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 90, + "copper": null + }, + "protection": 3, + "location": [ + "body" + ], + "equipped": false, + "weight": "heavy", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759994459142, + "modifiedTime": 1759994468351, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!KQ6uyTPUOHuMTxDF" +} diff --git a/packs/protection/_source/Heavy_BsNUpCnwmlhOWBhZ.json b/packs/protection/_source/Heavy_BsNUpCnwmlhOWBhZ.json new file mode 100644 index 0000000..225a22c --- /dev/null +++ b/packs/protection/_source/Heavy_BsNUpCnwmlhOWBhZ.json @@ -0,0 +1,21 @@ +{ + "type": "Item", + "folder": "pZxc6QLgVWfnZlf7", + "name": "Heavy", + "color": "#06393f", + "sorting": "a", + "_id": "BsNUpCnwmlhOWBhZ", + "description": "", + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "lastModifiedBy": null + }, + "_key": "!folders!BsNUpCnwmlhOWBhZ" +} diff --git a/packs/protection/_source/Heavy_Shields_uUrCwjxV6Ihisb6V.json b/packs/protection/_source/Heavy_Shields_uUrCwjxV6Ihisb6V.json new file mode 100644 index 0000000..69d4a46 --- /dev/null +++ b/packs/protection/_source/Heavy_Shields_uUrCwjxV6Ihisb6V.json @@ -0,0 +1,44 @@ +{ + "folder": "RXPJBkzVxFnoT3Tm", + "name": "Heavy Shields", + "type": "shield", + "_id": "uUrCwjxV6Ihisb6V", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 50, + "copper": null + }, + "protection": 1, + "location": [ + "head", + "body", + "arms", + "legs" + ], + "equipped": false, + "weight": "heavy", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759994801184, + "modifiedTime": 1759994810086, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!uUrCwjxV6Ihisb6V" +} diff --git a/packs/protection/_source/Leather_Cap_JMkV8kMnCXhW5KDh.json b/packs/protection/_source/Leather_Cap_JMkV8kMnCXhW5KDh.json new file mode 100644 index 0000000..c7db6f2 --- /dev/null +++ b/packs/protection/_source/Leather_Cap_JMkV8kMnCXhW5KDh.json @@ -0,0 +1,41 @@ +{ + "folder": "HRwiz1c1ZcQyPu4z", + "name": "Leather Cap", + "type": "armour", + "_id": "JMkV8kMnCXhW5KDh", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 10, + "copper": null + }, + "protection": 1, + "location": [ + "head" + ], + "equipped": false, + "weight": "light", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759994118194, + "modifiedTime": 1759994130845, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!JMkV8kMnCXhW5KDh" +} diff --git a/packs/protection/_source/Leather__Hide_Bracers_nz4DXXR4iU9CeMRA.json b/packs/protection/_source/Leather__Hide_Bracers_nz4DXXR4iU9CeMRA.json new file mode 100644 index 0000000..509e0e7 --- /dev/null +++ b/packs/protection/_source/Leather__Hide_Bracers_nz4DXXR4iU9CeMRA.json @@ -0,0 +1,41 @@ +{ + "folder": "HRwiz1c1ZcQyPu4z", + "name": "Leather, Hide Bracers", + "type": "armour", + "_id": "nz4DXXR4iU9CeMRA", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 10, + "copper": null + }, + "protection": 1, + "location": [ + "arms" + ], + "equipped": false, + "weight": "light", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759994170968, + "modifiedTime": 1759994180395, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!nz4DXXR4iU9CeMRA" +} diff --git a/packs/protection/_source/Leather__Hide_Jacket_zMyxSJ6VpaH3ddOO.json b/packs/protection/_source/Leather__Hide_Jacket_zMyxSJ6VpaH3ddOO.json new file mode 100644 index 0000000..f382b54 --- /dev/null +++ b/packs/protection/_source/Leather__Hide_Jacket_zMyxSJ6VpaH3ddOO.json @@ -0,0 +1,41 @@ +{ + "folder": "HRwiz1c1ZcQyPu4z", + "name": "Leather, Hide Jacket", + "type": "armour", + "_id": "zMyxSJ6VpaH3ddOO", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 20, + "copper": null + }, + "protection": 1, + "location": [ + "body" + ], + "equipped": false, + "weight": "light", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759994151324, + "modifiedTime": 1759994160761, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!zMyxSJ6VpaH3ddOO" +} diff --git a/packs/protection/_source/Leather__Hide_Leggings_14Omu9q2sMxW8GWB.json b/packs/protection/_source/Leather__Hide_Leggings_14Omu9q2sMxW8GWB.json new file mode 100644 index 0000000..8c9a9c3 --- /dev/null +++ b/packs/protection/_source/Leather__Hide_Leggings_14Omu9q2sMxW8GWB.json @@ -0,0 +1,41 @@ +{ + "folder": "HRwiz1c1ZcQyPu4z", + "name": "Leather, Hide Leggings", + "type": "armour", + "_id": "14Omu9q2sMxW8GWB", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 20, + "copper": null + }, + "protection": 1, + "location": [ + "legs" + ], + "equipped": false, + "weight": "light", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759994190989, + "modifiedTime": 1759994198011, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!14Omu9q2sMxW8GWB" +} diff --git a/packs/protection/_source/Light_HRwiz1c1ZcQyPu4z.json b/packs/protection/_source/Light_HRwiz1c1ZcQyPu4z.json new file mode 100644 index 0000000..bb9b437 --- /dev/null +++ b/packs/protection/_source/Light_HRwiz1c1ZcQyPu4z.json @@ -0,0 +1,21 @@ +{ + "type": "Item", + "folder": "pZxc6QLgVWfnZlf7", + "name": "Light", + "color": "#06393f", + "sorting": "a", + "_id": "HRwiz1c1ZcQyPu4z", + "description": "", + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "lastModifiedBy": null + }, + "_key": "!folders!HRwiz1c1ZcQyPu4z" +} diff --git a/packs/protection/_source/Light_Shields_a6vPAa25z8L9t79K.json b/packs/protection/_source/Light_Shields_a6vPAa25z8L9t79K.json new file mode 100644 index 0000000..e04bc0c --- /dev/null +++ b/packs/protection/_source/Light_Shields_a6vPAa25z8L9t79K.json @@ -0,0 +1,42 @@ +{ + "folder": "RXPJBkzVxFnoT3Tm", + "name": "Light Shields", + "type": "shield", + "_id": "a6vPAa25z8L9t79K", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 20, + "copper": null + }, + "protection": 1, + "location": [ + "head", + "arms" + ], + "equipped": false, + "weight": "light", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759994549164, + "modifiedTime": 1759994761998, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!a6vPAa25z8L9t79K" +} diff --git a/packs/protection/_source/Mail__Link__Scale_Coat_Sr40RFsPr2M0bTKK.json b/packs/protection/_source/Mail__Link__Scale_Coat_Sr40RFsPr2M0bTKK.json new file mode 100644 index 0000000..1c008b2 --- /dev/null +++ b/packs/protection/_source/Mail__Link__Scale_Coat_Sr40RFsPr2M0bTKK.json @@ -0,0 +1,42 @@ +{ + "folder": "cKN149ZGLqfyt0oi", + "name": "Mail, Link, Scale Coat", + "type": "armour", + "_id": "Sr40RFsPr2M0bTKK", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 180, + "copper": null + }, + "protection": 2, + "location": [ + "body", + "arms" + ], + "equipped": false, + "weight": "modest", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759994257751, + "modifiedTime": 1759994294312, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!Sr40RFsPr2M0bTKK" +} diff --git a/packs/protection/_source/Mail__Link__Scale_Coif_HfG5Doxf7576Jgbt.json b/packs/protection/_source/Mail__Link__Scale_Coif_HfG5Doxf7576Jgbt.json new file mode 100644 index 0000000..708afd3 --- /dev/null +++ b/packs/protection/_source/Mail__Link__Scale_Coif_HfG5Doxf7576Jgbt.json @@ -0,0 +1,41 @@ +{ + "folder": "cKN149ZGLqfyt0oi", + "name": "Mail, Link, Scale Coif", + "type": "armour", + "_id": "HfG5Doxf7576Jgbt", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 180, + "copper": null + }, + "protection": 2, + "location": [ + "head" + ], + "equipped": false, + "weight": "modest", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759994210701, + "modifiedTime": 1759994221462, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!HfG5Doxf7576Jgbt" +} diff --git a/packs/protection/_source/Mail__Link__Scale_Leggings_YBpElIVQ534pm3Mf.json b/packs/protection/_source/Mail__Link__Scale_Leggings_YBpElIVQ534pm3Mf.json new file mode 100644 index 0000000..e1d45ce --- /dev/null +++ b/packs/protection/_source/Mail__Link__Scale_Leggings_YBpElIVQ534pm3Mf.json @@ -0,0 +1,41 @@ +{ + "folder": "cKN149ZGLqfyt0oi", + "name": "Mail, Link, Scale Leggings", + "type": "armour", + "_id": "YBpElIVQ534pm3Mf", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 200, + "copper": null + }, + "protection": 2, + "location": [ + "legs" + ], + "equipped": false, + "weight": null, + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759994280754, + "modifiedTime": 1759994424980, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!YBpElIVQ534pm3Mf" +} diff --git a/packs/protection/_source/Mail__Link__Scale_Shirt_wab6Bo8ngar4mBCN.json b/packs/protection/_source/Mail__Link__Scale_Shirt_wab6Bo8ngar4mBCN.json new file mode 100644 index 0000000..766f79f --- /dev/null +++ b/packs/protection/_source/Mail__Link__Scale_Shirt_wab6Bo8ngar4mBCN.json @@ -0,0 +1,41 @@ +{ + "folder": "cKN149ZGLqfyt0oi", + "name": "Mail, Link, Scale Shirt", + "type": "armour", + "_id": "wab6Bo8ngar4mBCN", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 90, + "copper": null + }, + "protection": 2, + "location": [ + "body" + ], + "equipped": false, + "weight": "modest", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759994235204, + "modifiedTime": 1759994246578, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!wab6Bo8ngar4mBCN" +} diff --git a/packs/protection/_source/Modest_Shields_fyL8LZ8jpEQbjpM2.json b/packs/protection/_source/Modest_Shields_fyL8LZ8jpEQbjpM2.json new file mode 100644 index 0000000..5f40713 --- /dev/null +++ b/packs/protection/_source/Modest_Shields_fyL8LZ8jpEQbjpM2.json @@ -0,0 +1,43 @@ +{ + "folder": "RXPJBkzVxFnoT3Tm", + "name": "Modest Shields", + "type": "shield", + "_id": "fyL8LZ8jpEQbjpM2", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 30, + "copper": null + }, + "protection": 1, + "location": [ + "head", + "body", + "arms" + ], + "equipped": false, + "weight": "modest", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759994777609, + "modifiedTime": 1759994784898, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!fyL8LZ8jpEQbjpM2" +} diff --git a/packs/protection/_source/Modest_cKN149ZGLqfyt0oi.json b/packs/protection/_source/Modest_cKN149ZGLqfyt0oi.json new file mode 100644 index 0000000..0df0b4f --- /dev/null +++ b/packs/protection/_source/Modest_cKN149ZGLqfyt0oi.json @@ -0,0 +1,21 @@ +{ + "type": "Item", + "folder": "pZxc6QLgVWfnZlf7", + "name": "Modest", + "color": "#06393f", + "sorting": "a", + "_id": "cKN149ZGLqfyt0oi", + "description": "", + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "lastModifiedBy": null + }, + "_key": "!folders!cKN149ZGLqfyt0oi" +} diff --git a/packs/protection/_source/Plate_Bracers_e8JRJn5Blw3UrvnW.json b/packs/protection/_source/Plate_Bracers_e8JRJn5Blw3UrvnW.json new file mode 100644 index 0000000..b14cd65 --- /dev/null +++ b/packs/protection/_source/Plate_Bracers_e8JRJn5Blw3UrvnW.json @@ -0,0 +1,42 @@ +{ + "folder": "BsNUpCnwmlhOWBhZ", + "name": "Plate Bracers", + "type": "armour", + "_id": "e8JRJn5Blw3UrvnW", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 180, + "copper": null + }, + "protection": 1, + "location": [ + "body", + "arms" + ], + "equipped": false, + "weight": "heavy", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759994478040, + "modifiedTime": 1759994486947, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!e8JRJn5Blw3UrvnW" +} diff --git a/packs/protection/_source/Plate_Leggings_v1y4RKGad2IXOu5e.json b/packs/protection/_source/Plate_Leggings_v1y4RKGad2IXOu5e.json new file mode 100644 index 0000000..ea89641 --- /dev/null +++ b/packs/protection/_source/Plate_Leggings_v1y4RKGad2IXOu5e.json @@ -0,0 +1,41 @@ +{ + "folder": "BsNUpCnwmlhOWBhZ", + "name": "Plate Leggings", + "type": "armour", + "_id": "v1y4RKGad2IXOu5e", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 200, + "copper": null + }, + "protection": 3, + "location": [ + "legs" + ], + "equipped": false, + "weight": "heavy", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759994497119, + "modifiedTime": 1759994506514, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!v1y4RKGad2IXOu5e" +} diff --git a/packs/protection/_source/Ring_Coif__Helm_Z4NTsrX63JNjjZ8Z.json b/packs/protection/_source/Ring_Coif__Helm_Z4NTsrX63JNjjZ8Z.json new file mode 100644 index 0000000..072f340 --- /dev/null +++ b/packs/protection/_source/Ring_Coif__Helm_Z4NTsrX63JNjjZ8Z.json @@ -0,0 +1,41 @@ +{ + "folder": "BsNUpCnwmlhOWBhZ", + "name": "Ring Coif, Helm", + "type": "armour", + "_id": "Z4NTsrX63JNjjZ8Z", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 440, + "copper": null + }, + "protection": 3, + "location": [ + "head" + ], + "equipped": false, + "weight": "heavy", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759994438779, + "modifiedTime": 1759994448846, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!Z4NTsrX63JNjjZ8Z" +} diff --git a/packs/protection/_source/Shields_RXPJBkzVxFnoT3Tm.json b/packs/protection/_source/Shields_RXPJBkzVxFnoT3Tm.json new file mode 100644 index 0000000..f561577 --- /dev/null +++ b/packs/protection/_source/Shields_RXPJBkzVxFnoT3Tm.json @@ -0,0 +1,23 @@ +{ + "type": "Item", + "folder": null, + "name": "Shields", + "color": "#04262a", + "sorting": "m", + "_id": "RXPJBkzVxFnoT3Tm", + "description": "", + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759994538745, + "modifiedTime": 1759994538745, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!folders!RXPJBkzVxFnoT3Tm" +} diff --git a/packs/weapons/_source/Ammo_gvNPXXRBx2eGIzcU.json b/packs/weapons/_source/Ammo_gvNPXXRBx2eGIzcU.json new file mode 100644 index 0000000..f25006d --- /dev/null +++ b/packs/weapons/_source/Ammo_gvNPXXRBx2eGIzcU.json @@ -0,0 +1,23 @@ +{ + "type": "Item", + "folder": null, + "name": "Ammo", + "color": "#04262a", + "sorting": "a", + "_id": "gvNPXXRBx2eGIzcU", + "description": "", + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993925940, + "modifiedTime": 1759993925940, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!folders!gvNPXXRBx2eGIzcU" +} diff --git a/packs/weapons/_source/Arming_Sword_xXUItaoHTQ2QiaX4.json b/packs/weapons/_source/Arming_Sword_xXUItaoHTQ2QiaX4.json new file mode 100644 index 0000000..07dc4b5 --- /dev/null +++ b/packs/weapons/_source/Arming_Sword_xXUItaoHTQ2QiaX4.json @@ -0,0 +1,47 @@ +{ + "folder": "dBAI76CApXH8qqjx", + "name": "Arming Sword", + "type": "weapon", + "_id": "xXUItaoHTQ2QiaX4", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 80, + "copper": null + }, + "traits": [], + "range": { + "short": null, + "long": null + }, + "damage": 2, + "wear": { + "value": 4, + "max": 4 + }, + "equipped": false, + "weight": "modest", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759992689272, + "modifiedTime": 1759992708712, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!xXUItaoHTQ2QiaX4" +} diff --git a/packs/weapons/_source/Arrow_gN9JbmouUI7eOrSj.json b/packs/weapons/_source/Arrow_gN9JbmouUI7eOrSj.json new file mode 100644 index 0000000..5538980 --- /dev/null +++ b/packs/weapons/_source/Arrow_gN9JbmouUI7eOrSj.json @@ -0,0 +1,35 @@ +{ + "folder": "gvNPXXRBx2eGIzcU", + "name": "Arrow", + "type": "ammo", + "_id": "gN9JbmouUI7eOrSj", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 12, + "cost": { + "gold": null, + "silver": 3, + "copper": null + }, + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993937035, + "modifiedTime": 1759993944077, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!gN9JbmouUI7eOrSj" +} diff --git a/packs/weapons/_source/Axe__Hammer__Pick_cr35WzuPGDojuOuJ.json b/packs/weapons/_source/Axe__Hammer__Pick_cr35WzuPGDojuOuJ.json new file mode 100644 index 0000000..b36ef0b --- /dev/null +++ b/packs/weapons/_source/Axe__Hammer__Pick_cr35WzuPGDojuOuJ.json @@ -0,0 +1,49 @@ +{ + "folder": "mmd8siMKSLyOeILo", + "name": "Axe, Hammer, Pick", + "type": "weapon", + "_id": "cr35WzuPGDojuOuJ", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 10, + "copper": null + }, + "traits": [ + "Thrown" + ], + "range": { + "short": null, + "long": null + }, + "damage": 0, + "wear": { + "value": 2, + "max": 2 + }, + "equipped": false, + "weight": "light", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759991980652, + "modifiedTime": 1759992328546, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!cr35WzuPGDojuOuJ" +} diff --git a/packs/weapons/_source/Battleaxe__Warhammer_otIFI9TIDPWnT3cq.json b/packs/weapons/_source/Battleaxe__Warhammer_otIFI9TIDPWnT3cq.json new file mode 100644 index 0000000..d4249cb --- /dev/null +++ b/packs/weapons/_source/Battleaxe__Warhammer_otIFI9TIDPWnT3cq.json @@ -0,0 +1,47 @@ +{ + "folder": "dBAI76CApXH8qqjx", + "name": "Battleaxe, Warhammer", + "type": "weapon", + "_id": "otIFI9TIDPWnT3cq", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 40, + "copper": null + }, + "traits": [], + "range": { + "short": null, + "long": null + }, + "damage": 3, + "wear": { + "value": 3, + "max": 3 + }, + "equipped": false, + "weight": "modest", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759992730036, + "modifiedTime": 1759992748112, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!otIFI9TIDPWnT3cq" +} diff --git a/packs/weapons/_source/Black_Powder_c86ht86Z9vOEBtNH.json b/packs/weapons/_source/Black_Powder_c86ht86Z9vOEBtNH.json new file mode 100644 index 0000000..ef90dd2 --- /dev/null +++ b/packs/weapons/_source/Black_Powder_c86ht86Z9vOEBtNH.json @@ -0,0 +1,35 @@ +{ + "folder": "gvNPXXRBx2eGIzcU", + "name": "Black Powder", + "type": "ammo", + "_id": "c86ht86Z9vOEBtNH", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 12, + "cost": { + "gold": null, + "silver": 40, + "copper": null + }, + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759994005587, + "modifiedTime": 1759994010244, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!c86ht86Z9vOEBtNH" +} diff --git a/packs/weapons/_source/Blowgun_Darts_FvtiEaQhJumPsCwb.json b/packs/weapons/_source/Blowgun_Darts_FvtiEaQhJumPsCwb.json new file mode 100644 index 0000000..9c307f4 --- /dev/null +++ b/packs/weapons/_source/Blowgun_Darts_FvtiEaQhJumPsCwb.json @@ -0,0 +1,35 @@ +{ + "folder": "gvNPXXRBx2eGIzcU", + "name": "Blowgun Darts", + "type": "ammo", + "_id": "FvtiEaQhJumPsCwb", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 12, + "cost": { + "gold": null, + "silver": 2, + "copper": null + }, + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993963869, + "modifiedTime": 1759993969644, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!FvtiEaQhJumPsCwb" +} diff --git a/packs/weapons/_source/Blowgun__Sling_VrG2xer1quhjwUag.json b/packs/weapons/_source/Blowgun__Sling_VrG2xer1quhjwUag.json new file mode 100644 index 0000000..2394a78 --- /dev/null +++ b/packs/weapons/_source/Blowgun__Sling_VrG2xer1quhjwUag.json @@ -0,0 +1,49 @@ +{ + "folder": "vPyj2cK1j66Zyrul", + "name": "Blowgun, Sling", + "type": "weapon", + "_id": "VrG2xer1quhjwUag", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 10, + "copper": null + }, + "traits": [ + "Ammo" + ], + "range": { + "short": 5, + "long": 10 + }, + "damage": 1, + "wear": { + "value": 1, + "max": 1 + }, + "equipped": false, + "weight": "light", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993449825, + "modifiedTime": 1759993468278, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!VrG2xer1quhjwUag" +} diff --git a/packs/weapons/_source/Broadsword_I9QaJTU6O2E9WzUS.json b/packs/weapons/_source/Broadsword_I9QaJTU6O2E9WzUS.json new file mode 100644 index 0000000..7802c4f --- /dev/null +++ b/packs/weapons/_source/Broadsword_I9QaJTU6O2E9WzUS.json @@ -0,0 +1,47 @@ +{ + "folder": "dBAI76CApXH8qqjx", + "name": "Broadsword", + "type": "weapon", + "_id": "I9QaJTU6O2E9WzUS", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 100, + "copper": null + }, + "traits": [], + "range": { + "short": null, + "long": null + }, + "damage": 3, + "wear": { + "value": 3, + "max": 3 + }, + "equipped": false, + "weight": "modest", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759992797603, + "modifiedTime": 1759992815028, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!I9QaJTU6O2E9WzUS" +} diff --git a/packs/weapons/_source/Club_NlDJVbXeXRfoCZWp.json b/packs/weapons/_source/Club_NlDJVbXeXRfoCZWp.json new file mode 100644 index 0000000..b174c2b --- /dev/null +++ b/packs/weapons/_source/Club_NlDJVbXeXRfoCZWp.json @@ -0,0 +1,47 @@ +{ + "folder": "mmd8siMKSLyOeILo", + "name": "Club", + "type": "weapon", + "_id": "NlDJVbXeXRfoCZWp", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 2, + "copper": null + }, + "traits": [], + "range": { + "short": null, + "long": null + }, + "damage": 1, + "wear": { + "value": 2, + "max": 2 + }, + "equipped": false, + "weight": "light", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759992392474, + "modifiedTime": 1759992619109, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!NlDJVbXeXRfoCZWp" +} diff --git a/packs/weapons/_source/Crossbow_BNoYUrlpDk6oBeJt.json b/packs/weapons/_source/Crossbow_BNoYUrlpDk6oBeJt.json new file mode 100644 index 0000000..d03ae6d --- /dev/null +++ b/packs/weapons/_source/Crossbow_BNoYUrlpDk6oBeJt.json @@ -0,0 +1,49 @@ +{ + "folder": "8NNF9jBjpmPpmw1B", + "name": "Crossbow", + "type": "weapon", + "_id": "BNoYUrlpDk6oBeJt", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 90, + "copper": null + }, + "traits": [ + "Reload" + ], + "range": { + "short": 10, + "long": 25 + }, + "damage": 3, + "wear": { + "value": 3, + "max": 3 + }, + "equipped": false, + "weight": "modest", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993621847, + "modifiedTime": 1759993694978, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!BNoYUrlpDk6oBeJt" +} diff --git a/packs/weapons/_source/Crossbow_Bolts_7cmLLV6o2pPAyyAg.json b/packs/weapons/_source/Crossbow_Bolts_7cmLLV6o2pPAyyAg.json new file mode 100644 index 0000000..1dea670 --- /dev/null +++ b/packs/weapons/_source/Crossbow_Bolts_7cmLLV6o2pPAyyAg.json @@ -0,0 +1,35 @@ +{ + "folder": "gvNPXXRBx2eGIzcU", + "name": "Crossbow Bolts", + "type": "ammo", + "_id": "7cmLLV6o2pPAyyAg", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 12, + "cost": { + "gold": null, + "silver": 3, + "copper": null + }, + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993977460, + "modifiedTime": 1759993982911, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!7cmLLV6o2pPAyyAg" +} diff --git a/packs/weapons/_source/Cutlass__Saber__Scimitar_kSWrbdKdYIRxkWka.json b/packs/weapons/_source/Cutlass__Saber__Scimitar_kSWrbdKdYIRxkWka.json new file mode 100644 index 0000000..d391acc --- /dev/null +++ b/packs/weapons/_source/Cutlass__Saber__Scimitar_kSWrbdKdYIRxkWka.json @@ -0,0 +1,49 @@ +{ + "folder": "dBAI76CApXH8qqjx", + "name": "Cutlass, Saber, Scimitar", + "type": "weapon", + "_id": "kSWrbdKdYIRxkWka", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 120, + "copper": null + }, + "traits": [ + "Agile" + ], + "range": { + "short": null, + "long": null + }, + "damage": 2, + "wear": { + "value": 2, + "max": 2 + }, + "equipped": false, + "weight": "modest", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759992861725, + "modifiedTime": 1759992882913, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!kSWrbdKdYIRxkWka" +} diff --git a/packs/weapons/_source/Dagger_q8z2HptFaPmeHU9n.json b/packs/weapons/_source/Dagger_q8z2HptFaPmeHU9n.json new file mode 100644 index 0000000..8ded77a --- /dev/null +++ b/packs/weapons/_source/Dagger_q8z2HptFaPmeHU9n.json @@ -0,0 +1,50 @@ +{ + "folder": "mmd8siMKSLyOeILo", + "name": "Dagger", + "type": "weapon", + "_id": "q8z2HptFaPmeHU9n", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 20, + "copper": null + }, + "traits": [ + "Agile", + "Thrown" + ], + "range": { + "short": null, + "long": null + }, + "damage": 1, + "wear": { + "value": 1, + "max": 1 + }, + "equipped": false, + "weight": "light", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759992606052, + "modifiedTime": 1759992639628, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!q8z2HptFaPmeHU9n" +} diff --git a/packs/weapons/_source/Darts_rDxS6EJzg2zvSpxR.json b/packs/weapons/_source/Darts_rDxS6EJzg2zvSpxR.json new file mode 100644 index 0000000..6caf1c7 --- /dev/null +++ b/packs/weapons/_source/Darts_rDxS6EJzg2zvSpxR.json @@ -0,0 +1,49 @@ +{ + "folder": "vPyj2cK1j66Zyrul", + "name": "Darts", + "type": "weapon", + "_id": "rDxS6EJzg2zvSpxR", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 6, + "cost": { + "gold": null, + "silver": 30, + "copper": null + }, + "traits": [ + "Thrown" + ], + "range": { + "short": 3, + "long": 6 + }, + "damage": 1, + "wear": { + "value": 1, + "max": 1 + }, + "equipped": false, + "weight": "light", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993477954, + "modifiedTime": 1759993498878, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!rDxS6EJzg2zvSpxR" +} diff --git a/packs/weapons/_source/Flintlock_Pistol_1vxM6KoEPrQ7pjcg.json b/packs/weapons/_source/Flintlock_Pistol_1vxM6KoEPrQ7pjcg.json new file mode 100644 index 0000000..868057d --- /dev/null +++ b/packs/weapons/_source/Flintlock_Pistol_1vxM6KoEPrQ7pjcg.json @@ -0,0 +1,50 @@ +{ + "folder": "vPyj2cK1j66Zyrul", + "name": "Flintlock Pistol", + "type": "weapon", + "_id": "1vxM6KoEPrQ7pjcg", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 260, + "copper": null + }, + "traits": [ + "Reload", + "Loud" + ], + "range": { + "short": 5, + "long": 10 + }, + "damage": 2, + "wear": { + "value": 2, + "max": 2 + }, + "equipped": false, + "weight": "light", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993514814, + "modifiedTime": 1759993538727, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!1vxM6KoEPrQ7pjcg" +} diff --git a/packs/weapons/_source/Flintlock_Rifle_xUGUgnjJsKUPgPpX.json b/packs/weapons/_source/Flintlock_Rifle_xUGUgnjJsKUPgPpX.json new file mode 100644 index 0000000..13105fc --- /dev/null +++ b/packs/weapons/_source/Flintlock_Rifle_xUGUgnjJsKUPgPpX.json @@ -0,0 +1,50 @@ +{ + "folder": "IkSGLBUzPI9Jbcj7", + "name": "Flintlock Rifle", + "type": "weapon", + "_id": "xUGUgnjJsKUPgPpX", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 640, + "copper": null + }, + "traits": [ + "Reload", + "Loud" + ], + "range": { + "short": 15, + "long": 30 + }, + "damage": 4, + "wear": { + "value": 3, + "max": 3 + }, + "equipped": false, + "weight": "heavy", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993769479, + "modifiedTime": 1759993794528, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!xUGUgnjJsKUPgPpX" +} diff --git a/packs/weapons/_source/Great_Arrows_FQ5VBjR0LdXf8Nh2.json b/packs/weapons/_source/Great_Arrows_FQ5VBjR0LdXf8Nh2.json new file mode 100644 index 0000000..6f80b11 --- /dev/null +++ b/packs/weapons/_source/Great_Arrows_FQ5VBjR0LdXf8Nh2.json @@ -0,0 +1,35 @@ +{ + "folder": "gvNPXXRBx2eGIzcU", + "name": "Great Arrows", + "type": "ammo", + "_id": "FQ5VBjR0LdXf8Nh2", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 12, + "cost": { + "gold": null, + "silver": 12, + "copper": null + }, + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993988701, + "modifiedTime": 1759993993428, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!FQ5VBjR0LdXf8Nh2" +} diff --git a/packs/weapons/_source/Greataxe__Maul__Sword_Pfm448hGPVSuyyrd.json b/packs/weapons/_source/Greataxe__Maul__Sword_Pfm448hGPVSuyyrd.json new file mode 100644 index 0000000..ccba1ec --- /dev/null +++ b/packs/weapons/_source/Greataxe__Maul__Sword_Pfm448hGPVSuyyrd.json @@ -0,0 +1,49 @@ +{ + "folder": "3tp9cwpArQNOpkAY", + "name": "Greataxe, Maul, Sword", + "type": "weapon", + "_id": "Pfm448hGPVSuyyrd", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 120, + "copper": null + }, + "traits": [ + "Long" + ], + "range": { + "short": null, + "long": null + }, + "damage": 4, + "wear": { + "value": 4, + "max": 4 + }, + "equipped": false, + "weight": "heavy", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993233713, + "modifiedTime": 1759993258414, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!Pfm448hGPVSuyyrd" +} diff --git a/packs/weapons/_source/Greatbow_N80F8sq9SaHrXBvS.json b/packs/weapons/_source/Greatbow_N80F8sq9SaHrXBvS.json new file mode 100644 index 0000000..a823775 --- /dev/null +++ b/packs/weapons/_source/Greatbow_N80F8sq9SaHrXBvS.json @@ -0,0 +1,49 @@ +{ + "folder": "IkSGLBUzPI9Jbcj7", + "name": "Greatbow", + "type": "weapon", + "_id": "N80F8sq9SaHrXBvS", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 320, + "copper": null + }, + "traits": [ + "Ammo" + ], + "range": { + "short": 20, + "long": 40 + }, + "damage": 4, + "wear": { + "value": 4, + "max": 4 + }, + "equipped": false, + "weight": "heavy", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993806102, + "modifiedTime": 1759993839512, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!N80F8sq9SaHrXBvS" +} diff --git a/packs/weapons/_source/Greatclub__Staff_Ct9iNF9KPMGStSQo.json b/packs/weapons/_source/Greatclub__Staff_Ct9iNF9KPMGStSQo.json new file mode 100644 index 0000000..181532c --- /dev/null +++ b/packs/weapons/_source/Greatclub__Staff_Ct9iNF9KPMGStSQo.json @@ -0,0 +1,49 @@ +{ + "folder": "3tp9cwpArQNOpkAY", + "name": "Greatclub, Staff", + "type": "weapon", + "_id": "Ct9iNF9KPMGStSQo", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 30, + "copper": null + }, + "traits": [ + "Long" + ], + "range": { + "short": null, + "long": null + }, + "damage": 4, + "wear": { + "value": 4, + "max": 4 + }, + "equipped": false, + "weight": "heavy", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993276032, + "modifiedTime": 1759993292511, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!Ct9iNF9KPMGStSQo" +} diff --git a/packs/weapons/_source/Hand_Crossbow_5sUeNom6dn6MaoAE.json b/packs/weapons/_source/Hand_Crossbow_5sUeNom6dn6MaoAE.json new file mode 100644 index 0000000..0dc3807 --- /dev/null +++ b/packs/weapons/_source/Hand_Crossbow_5sUeNom6dn6MaoAE.json @@ -0,0 +1,49 @@ +{ + "folder": "vPyj2cK1j66Zyrul", + "name": "Hand Crossbow", + "type": "weapon", + "_id": "5sUeNom6dn6MaoAE", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 120, + "copper": null + }, + "traits": [ + "Reload" + ], + "range": { + "short": 5, + "long": 10 + }, + "damage": 2, + "wear": { + "value": 1, + "max": 1 + }, + "equipped": false, + "weight": "light", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993551322, + "modifiedTime": 1759993570977, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!5sUeNom6dn6MaoAE" +} diff --git a/packs/weapons/_source/Hand_Weapons_3mr6aZe43z7YBysA.json b/packs/weapons/_source/Hand_Weapons_3mr6aZe43z7YBysA.json new file mode 100644 index 0000000..cc22c9f --- /dev/null +++ b/packs/weapons/_source/Hand_Weapons_3mr6aZe43z7YBysA.json @@ -0,0 +1,23 @@ +{ + "type": "Item", + "folder": null, + "name": "Hand Weapons", + "color": "#04262a", + "sorting": "m", + "_id": "3mr6aZe43z7YBysA", + "description": "", + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759992349332, + "modifiedTime": 1759993205214, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!folders!3mr6aZe43z7YBysA" +} diff --git a/packs/weapons/_source/Heavy_3tp9cwpArQNOpkAY.json b/packs/weapons/_source/Heavy_3tp9cwpArQNOpkAY.json new file mode 100644 index 0000000..729d6f9 --- /dev/null +++ b/packs/weapons/_source/Heavy_3tp9cwpArQNOpkAY.json @@ -0,0 +1,21 @@ +{ + "type": "Item", + "folder": "3mr6aZe43z7YBysA", + "name": "Heavy", + "color": "#06393f", + "sorting": "a", + "_id": "3tp9cwpArQNOpkAY", + "description": "", + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "lastModifiedBy": null + }, + "_key": "!folders!3tp9cwpArQNOpkAY" +} diff --git a/packs/weapons/_source/Heavy_Crossbow_xJzHTrYsJVL2WsSF.json b/packs/weapons/_source/Heavy_Crossbow_xJzHTrYsJVL2WsSF.json new file mode 100644 index 0000000..bd06196 --- /dev/null +++ b/packs/weapons/_source/Heavy_Crossbow_xJzHTrYsJVL2WsSF.json @@ -0,0 +1,49 @@ +{ + "folder": "IkSGLBUzPI9Jbcj7", + "name": "Heavy Crossbow", + "type": "weapon", + "_id": "xJzHTrYsJVL2WsSF", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 200, + "copper": null + }, + "traits": [ + "Ammo" + ], + "range": { + "short": 20, + "long": 40 + }, + "damage": 4, + "wear": { + "value": 4, + "max": 4 + }, + "equipped": false, + "weight": "heavy", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993848517, + "modifiedTime": 1759993869078, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!xJzHTrYsJVL2WsSF" +} diff --git a/packs/weapons/_source/Heavy_IkSGLBUzPI9Jbcj7.json b/packs/weapons/_source/Heavy_IkSGLBUzPI9Jbcj7.json new file mode 100644 index 0000000..b7585e5 --- /dev/null +++ b/packs/weapons/_source/Heavy_IkSGLBUzPI9Jbcj7.json @@ -0,0 +1,21 @@ +{ + "type": "Item", + "folder": "sjc6X9bKf7BY04Ar", + "name": "Heavy", + "color": "#06393f", + "sorting": "a", + "_id": "IkSGLBUzPI9Jbcj7", + "description": "", + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "lastModifiedBy": null + }, + "_key": "!folders!IkSGLBUzPI9Jbcj7" +} diff --git a/packs/weapons/_source/Knife_fv5D0xOJVpOwnyTn.json b/packs/weapons/_source/Knife_fv5D0xOJVpOwnyTn.json new file mode 100644 index 0000000..04d325c --- /dev/null +++ b/packs/weapons/_source/Knife_fv5D0xOJVpOwnyTn.json @@ -0,0 +1,49 @@ +{ + "folder": "mmd8siMKSLyOeILo", + "name": "Knife", + "type": "weapon", + "_id": "fv5D0xOJVpOwnyTn", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 6, + "copper": null + }, + "traits": [ + "Thrown" + ], + "range": { + "short": null, + "long": null + }, + "damage": 1, + "wear": { + "value": 1, + "max": 1 + }, + "equipped": false, + "weight": "light", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759992653763, + "modifiedTime": 1759992669428, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!fv5D0xOJVpOwnyTn" +} diff --git a/packs/weapons/_source/Light_mmd8siMKSLyOeILo.json b/packs/weapons/_source/Light_mmd8siMKSLyOeILo.json new file mode 100644 index 0000000..f243f42 --- /dev/null +++ b/packs/weapons/_source/Light_mmd8siMKSLyOeILo.json @@ -0,0 +1,23 @@ +{ + "type": "Item", + "folder": "3mr6aZe43z7YBysA", + "name": "Light", + "color": "#06393f", + "sorting": "a", + "_id": "mmd8siMKSLyOeILo", + "description": "", + "sort": -100000, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759991969340, + "modifiedTime": 1759993210807, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!folders!mmd8siMKSLyOeILo" +} diff --git a/packs/weapons/_source/Light_vPyj2cK1j66Zyrul.json b/packs/weapons/_source/Light_vPyj2cK1j66Zyrul.json new file mode 100644 index 0000000..fb4cbf3 --- /dev/null +++ b/packs/weapons/_source/Light_vPyj2cK1j66Zyrul.json @@ -0,0 +1,21 @@ +{ + "type": "Item", + "folder": "sjc6X9bKf7BY04Ar", + "name": "Light", + "color": "#06393f", + "sorting": "a", + "_id": "vPyj2cK1j66Zyrul", + "description": "", + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "lastModifiedBy": null + }, + "_key": "!folders!vPyj2cK1j66Zyrul" +} diff --git a/packs/weapons/_source/Long_Rifle_dn1eja68NNuxB8K1.json b/packs/weapons/_source/Long_Rifle_dn1eja68NNuxB8K1.json new file mode 100644 index 0000000..d20d8c3 --- /dev/null +++ b/packs/weapons/_source/Long_Rifle_dn1eja68NNuxB8K1.json @@ -0,0 +1,50 @@ +{ + "folder": "IkSGLBUzPI9Jbcj7", + "name": "Long Rifle", + "type": "weapon", + "_id": "dn1eja68NNuxB8K1", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 840, + "copper": null + }, + "traits": [ + "Reload", + "Loud" + ], + "range": { + "short": 20, + "long": 40 + }, + "damage": 4, + "wear": { + "value": 3, + "max": 3 + }, + "equipped": false, + "weight": "heavy", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993878663, + "modifiedTime": 1759993902478, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!dn1eja68NNuxB8K1" +} diff --git a/packs/weapons/_source/Longbow_oyVNU8XgMiZI0Uxr.json b/packs/weapons/_source/Longbow_oyVNU8XgMiZI0Uxr.json new file mode 100644 index 0000000..57ee032 --- /dev/null +++ b/packs/weapons/_source/Longbow_oyVNU8XgMiZI0Uxr.json @@ -0,0 +1,49 @@ +{ + "folder": "8NNF9jBjpmPpmw1B", + "name": "Longbow", + "type": "weapon", + "_id": "oyVNU8XgMiZI0Uxr", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 80, + "copper": null + }, + "traits": [ + "Ammo" + ], + "range": { + "short": 9, + "long": 30 + }, + "damage": 3, + "wear": { + "value": 3, + "max": 3 + }, + "equipped": false, + "weight": "modest", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993703627, + "modifiedTime": 1759993727378, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!oyVNU8XgMiZI0Uxr" +} diff --git a/packs/weapons/_source/Longsword_7ezV0MrGA0duoGbl.json b/packs/weapons/_source/Longsword_7ezV0MrGA0duoGbl.json new file mode 100644 index 0000000..7ca740d --- /dev/null +++ b/packs/weapons/_source/Longsword_7ezV0MrGA0duoGbl.json @@ -0,0 +1,49 @@ +{ + "folder": "dBAI76CApXH8qqjx", + "name": "Longsword", + "type": "weapon", + "_id": "7ezV0MrGA0duoGbl", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 220, + "copper": null + }, + "traits": [ + "Able" + ], + "range": { + "short": null, + "long": null + }, + "damage": 3, + "wear": { + "value": 3, + "max": 3 + }, + "equipped": false, + "weight": "modest", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759992894140, + "modifiedTime": 1759992913378, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!7ezV0MrGA0duoGbl" +} diff --git a/packs/weapons/_source/Modest_8NNF9jBjpmPpmw1B.json b/packs/weapons/_source/Modest_8NNF9jBjpmPpmw1B.json new file mode 100644 index 0000000..ca9314d --- /dev/null +++ b/packs/weapons/_source/Modest_8NNF9jBjpmPpmw1B.json @@ -0,0 +1,21 @@ +{ + "type": "Item", + "folder": "sjc6X9bKf7BY04Ar", + "name": "Modest", + "color": "#06393f", + "sorting": "a", + "_id": "8NNF9jBjpmPpmw1B", + "description": "", + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "lastModifiedBy": null + }, + "_key": "!folders!8NNF9jBjpmPpmw1B" +} diff --git a/packs/weapons/_source/Modest_dBAI76CApXH8qqjx.json b/packs/weapons/_source/Modest_dBAI76CApXH8qqjx.json new file mode 100644 index 0000000..0828e40 --- /dev/null +++ b/packs/weapons/_source/Modest_dBAI76CApXH8qqjx.json @@ -0,0 +1,21 @@ +{ + "type": "Item", + "folder": "3mr6aZe43z7YBysA", + "name": "Modest", + "color": "#06393f", + "sorting": "a", + "_id": "dBAI76CApXH8qqjx", + "description": "", + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "lastModifiedBy": null + }, + "_key": "!folders!dBAI76CApXH8qqjx" +} diff --git a/packs/weapons/_source/Poleaxe__Glaive__Halberd_j6C9IyebpKsk7M6R.json b/packs/weapons/_source/Poleaxe__Glaive__Halberd_j6C9IyebpKsk7M6R.json new file mode 100644 index 0000000..28cf30c --- /dev/null +++ b/packs/weapons/_source/Poleaxe__Glaive__Halberd_j6C9IyebpKsk7M6R.json @@ -0,0 +1,49 @@ +{ + "folder": "3tp9cwpArQNOpkAY", + "name": "Poleaxe, Glaive, Halberd", + "type": "weapon", + "_id": "j6C9IyebpKsk7M6R", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 60, + "copper": null + }, + "traits": [ + "Long" + ], + "range": { + "short": null, + "long": null + }, + "damage": 4, + "wear": { + "value": 4, + "max": 4 + }, + "equipped": false, + "weight": "heavy", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993308439, + "modifiedTime": 1759993324411, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!j6C9IyebpKsk7M6R" +} diff --git a/packs/weapons/_source/Quarterstaff_0ebCxTylmQa5UJvF.json b/packs/weapons/_source/Quarterstaff_0ebCxTylmQa5UJvF.json new file mode 100644 index 0000000..63aaffe --- /dev/null +++ b/packs/weapons/_source/Quarterstaff_0ebCxTylmQa5UJvF.json @@ -0,0 +1,49 @@ +{ + "folder": "dBAI76CApXH8qqjx", + "name": "Quarterstaff", + "type": "weapon", + "_id": "0ebCxTylmQa5UJvF", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 20, + "copper": null + }, + "traits": [ + "Able" + ], + "range": { + "short": null, + "long": null + }, + "damage": 2, + "wear": { + "value": 3, + "max": 3 + }, + "equipped": false, + "weight": "modest", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759992932299, + "modifiedTime": 1759992950112, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!0ebCxTylmQa5UJvF" +} diff --git a/packs/weapons/_source/Ranged_Weapons_sjc6X9bKf7BY04Ar.json b/packs/weapons/_source/Ranged_Weapons_sjc6X9bKf7BY04Ar.json new file mode 100644 index 0000000..ff8a606 --- /dev/null +++ b/packs/weapons/_source/Ranged_Weapons_sjc6X9bKf7BY04Ar.json @@ -0,0 +1,23 @@ +{ + "type": "Item", + "folder": null, + "name": "Ranged Weapons", + "color": "#04262a", + "sorting": "m", + "_id": "sjc6X9bKf7BY04Ar", + "description": "", + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993402367, + "modifiedTime": 1759993402367, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!folders!sjc6X9bKf7BY04Ar" +} diff --git a/packs/weapons/_source/Rapier__Foil_cgEQKXKnSShCRQaq.json b/packs/weapons/_source/Rapier__Foil_cgEQKXKnSShCRQaq.json new file mode 100644 index 0000000..b2a5f0c --- /dev/null +++ b/packs/weapons/_source/Rapier__Foil_cgEQKXKnSShCRQaq.json @@ -0,0 +1,50 @@ +{ + "folder": "dBAI76CApXH8qqjx", + "name": "Rapier, Foil", + "type": "weapon", + "_id": "cgEQKXKnSShCRQaq", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 120, + "copper": null + }, + "traits": [ + "Agile", + "Long" + ], + "range": { + "short": null, + "long": null + }, + "damage": 2, + "wear": { + "value": 2, + "max": 2 + }, + "equipped": false, + "weight": "modest", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759992972763, + "modifiedTime": 1759992992895, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!cgEQKXKnSShCRQaq" +} diff --git a/packs/weapons/_source/Scythe_PnBWkLNzmQjXbB94.json b/packs/weapons/_source/Scythe_PnBWkLNzmQjXbB94.json new file mode 100644 index 0000000..e999370 --- /dev/null +++ b/packs/weapons/_source/Scythe_PnBWkLNzmQjXbB94.json @@ -0,0 +1,49 @@ +{ + "folder": "3tp9cwpArQNOpkAY", + "name": "Scythe", + "type": "weapon", + "_id": "PnBWkLNzmQjXbB94", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 80, + "copper": null + }, + "traits": [ + "Long" + ], + "range": { + "short": null, + "long": null + }, + "damage": 4, + "wear": { + "value": 4, + "max": 4 + }, + "equipped": false, + "weight": "heavy", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993335901, + "modifiedTime": 1759993351161, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!PnBWkLNzmQjXbB94" +} diff --git a/packs/weapons/_source/Shortbow_qmnDO3TrKoeW7DZB.json b/packs/weapons/_source/Shortbow_qmnDO3TrKoeW7DZB.json new file mode 100644 index 0000000..484b4ab --- /dev/null +++ b/packs/weapons/_source/Shortbow_qmnDO3TrKoeW7DZB.json @@ -0,0 +1,49 @@ +{ + "folder": "8NNF9jBjpmPpmw1B", + "name": "Shortbow", + "type": "weapon", + "_id": "qmnDO3TrKoeW7DZB", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 50, + "copper": null + }, + "traits": [ + "Ammo" + ], + "range": { + "short": 6, + "long": 20 + }, + "damage": 2, + "wear": { + "value": 3, + "max": 3 + }, + "equipped": false, + "weight": "modest", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993734661, + "modifiedTime": 1759993756094, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!qmnDO3TrKoeW7DZB" +} diff --git a/packs/weapons/_source/Shortsword_UulalWrMRtSX5KxZ.json b/packs/weapons/_source/Shortsword_UulalWrMRtSX5KxZ.json new file mode 100644 index 0000000..06322d5 --- /dev/null +++ b/packs/weapons/_source/Shortsword_UulalWrMRtSX5KxZ.json @@ -0,0 +1,47 @@ +{ + "folder": "dBAI76CApXH8qqjx", + "name": "Shortsword", + "type": "weapon", + "_id": "UulalWrMRtSX5KxZ", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 60, + "copper": null + }, + "traits": [], + "range": { + "short": null, + "long": null + }, + "damage": 2, + "wear": { + "value": 3, + "max": 3 + }, + "equipped": false, + "weight": "modest", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993008835, + "modifiedTime": 1759993028879, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!UulalWrMRtSX5KxZ" +} diff --git a/packs/weapons/_source/Shot_Q5i7hMOPgb9oXmh5.json b/packs/weapons/_source/Shot_Q5i7hMOPgb9oXmh5.json new file mode 100644 index 0000000..5338692 --- /dev/null +++ b/packs/weapons/_source/Shot_Q5i7hMOPgb9oXmh5.json @@ -0,0 +1,35 @@ +{ + "folder": "gvNPXXRBx2eGIzcU", + "name": "Shot", + "type": "ammo", + "_id": "Q5i7hMOPgb9oXmh5", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 12, + "cost": { + "gold": null, + "silver": 10, + "copper": null + }, + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759994014714, + "modifiedTime": 1759994020511, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!Q5i7hMOPgb9oXmh5" +} diff --git a/packs/weapons/_source/Sickle_o9saXoHahbxTxt4h.json b/packs/weapons/_source/Sickle_o9saXoHahbxTxt4h.json new file mode 100644 index 0000000..f0c8fe8 --- /dev/null +++ b/packs/weapons/_source/Sickle_o9saXoHahbxTxt4h.json @@ -0,0 +1,47 @@ +{ + "folder": "dBAI76CApXH8qqjx", + "name": "Sickle", + "type": "weapon", + "_id": "o9saXoHahbxTxt4h", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 80, + "copper": null + }, + "traits": [], + "range": { + "short": null, + "long": null + }, + "damage": 3, + "wear": { + "value": 2, + "max": 2 + }, + "equipped": false, + "weight": "modest", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993051839, + "modifiedTime": 1759993069744, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!o9saXoHahbxTxt4h" +} diff --git a/packs/weapons/_source/Spear__Javelin__Pike_bTOoWxHeLSY9JWNY.json b/packs/weapons/_source/Spear__Javelin__Pike_bTOoWxHeLSY9JWNY.json new file mode 100644 index 0000000..477bc8b --- /dev/null +++ b/packs/weapons/_source/Spear__Javelin__Pike_bTOoWxHeLSY9JWNY.json @@ -0,0 +1,50 @@ +{ + "folder": "dBAI76CApXH8qqjx", + "name": "Spear, Javelin, Pike", + "type": "weapon", + "_id": "bTOoWxHeLSY9JWNY", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 30, + "copper": null + }, + "traits": [ + "Agile", + "Thrown" + ], + "range": { + "short": 5, + "long": 10 + }, + "damage": 2, + "wear": { + "value": 3, + "max": 3 + }, + "equipped": false, + "weight": "modest", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993081111, + "modifiedTime": 1759993110561, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!bTOoWxHeLSY9JWNY" +} diff --git a/packs/weapons/_source/Throwing_Axe_n2j1gxnn3WnUPag6.json b/packs/weapons/_source/Throwing_Axe_n2j1gxnn3WnUPag6.json new file mode 100644 index 0000000..b193d2a --- /dev/null +++ b/packs/weapons/_source/Throwing_Axe_n2j1gxnn3WnUPag6.json @@ -0,0 +1,49 @@ +{ + "folder": "vPyj2cK1j66Zyrul", + "name": "Throwing Axe", + "type": "weapon", + "_id": "n2j1gxnn3WnUPag6", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 30, + "copper": null + }, + "traits": [ + "Thrown" + ], + "range": { + "short": 2, + "long": 4 + }, + "damage": 1, + "wear": { + "value": 3, + "max": 3 + }, + "equipped": false, + "weight": "light", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993592390, + "modifiedTime": 1759993609878, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!n2j1gxnn3WnUPag6" +} diff --git a/packs/weapons/_source/Warclub__Flail__Mace_WMiQdSVgM8z84MEv.json b/packs/weapons/_source/Warclub__Flail__Mace_WMiQdSVgM8z84MEv.json new file mode 100644 index 0000000..0f102f8 --- /dev/null +++ b/packs/weapons/_source/Warclub__Flail__Mace_WMiQdSVgM8z84MEv.json @@ -0,0 +1,47 @@ +{ + "folder": "dBAI76CApXH8qqjx", + "name": "Warclub, Flail, Mace", + "type": "weapon", + "_id": "WMiQdSVgM8z84MEv", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 80, + "copper": null + }, + "traits": [], + "range": { + "short": null, + "long": null + }, + "damage": 2, + "wear": { + "value": 3, + "max": 3 + }, + "equipped": false, + "weight": "modest", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993125576, + "modifiedTime": 1759993138211, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!WMiQdSVgM8z84MEv" +} diff --git a/packs/weapons/_source/Whip_vVF6LZSFi0pNB95E.json b/packs/weapons/_source/Whip_vVF6LZSFi0pNB95E.json new file mode 100644 index 0000000..7380fb8 --- /dev/null +++ b/packs/weapons/_source/Whip_vVF6LZSFi0pNB95E.json @@ -0,0 +1,50 @@ +{ + "folder": "dBAI76CApXH8qqjx", + "name": "Whip", + "type": "weapon", + "_id": "vVF6LZSFi0pNB95E", + "img": "icons/svg/item-bag.svg", + "system": { + "quantity": 1, + "cost": { + "gold": null, + "silver": 20, + "copper": null + }, + "traits": [ + "Agile", + "Long" + ], + "range": { + "short": null, + "long": null + }, + "damage": 1, + "wear": { + "value": 3, + "max": 3 + }, + "equipped": false, + "weight": "modest", + "access": "" + }, + "effects": [], + "sort": 0, + "ownership": { + "default": 0, + "9x9FgB0YTeCJJUDK": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.350", + "systemId": "ripcrypt", + "systemVersion": "0.2.0", + "createdTime": 1759993155738, + "modifiedTime": 1759993176128, + "lastModifiedBy": "9x9FgB0YTeCJJUDK" + }, + "_key": "!items!vVF6LZSFi0pNB95E" +} diff --git a/scripts/buildCompendia.mjs b/scripts/buildCompendia.mjs new file mode 100644 index 0000000..88ab4e4 --- /dev/null +++ b/scripts/buildCompendia.mjs @@ -0,0 +1,36 @@ +import { existsSync } from "fs"; +import { readFile } from "fs/promises"; +import { join } from "path"; +import { compilePack } from "@foundryvtt/foundryvtt-cli"; +import { pathToFileURL } from "url"; + +export async function buildCompendia() { + const manifest = JSON.parse(await readFile(`./system.json`, `utf-8`)); + + if (!manifest.packs || manifest.packs.length === 0) { + console.log(`No compendium packs defined`); + process.exit(0); + }; + console.log(`Packing compendia`); + + for (const compendium of manifest.packs) { + console.debug(`Packing ${compendium.label} (${compendium.name})`); + let src = join(process.cwd(), compendium.path, `_source`); + if (!existsSync(src)) { + console.warn(`${compendium.path} doesn't exist, skipping.`) + continue; + }; + await compilePack( + src, + join(process.cwd(), compendium.path), + { recursive: true }, + ); + console.debug(`Finished packing compendium: ${compendium.name}`); + }; + + console.log(`Finished packing all compendia`) +}; + +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + buildCompendia(); +}; diff --git a/scripts/extractCompendia.mjs b/scripts/extractCompendia.mjs new file mode 100644 index 0000000..0730b38 --- /dev/null +++ b/scripts/extractCompendia.mjs @@ -0,0 +1,31 @@ +import { readFile } from "fs/promises"; +import { join } from "path"; +import { extractPack } from "@foundryvtt/foundryvtt-cli"; +import { pathToFileURL } from "url"; + +export async function extractCompendia() { + const manifest = JSON.parse(await readFile(`./system.json`, `utf-8`)); + + if (!manifest.packs || manifest.packs.length === 0) { + console.log(`No compendium packs defined`); + process.exit(0); + }; + console.log(`Extracting compendia`); + + for (const compendium of manifest.packs) { + console.debug(`Unpacking ${compendium.label} (${compendium.name})`); + let src = join(process.cwd(), compendium.path, `_source`); + await extractPack( + join(process.cwd(), compendium.path), + src, + { recursive: true }, + ); + console.debug(`Finished unpacking compendium: ${compendium.name}`); + }; + + console.log(`Finished unpacking all compendia`); +}; + +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + extractCompendia(); +}; diff --git a/scripts/linkFoundry.mjs b/scripts/linkFoundry.mjs new file mode 100644 index 0000000..3ee94c5 --- /dev/null +++ b/scripts/linkFoundry.mjs @@ -0,0 +1,47 @@ +import { existsSync } from "fs"; +import { symlink, unlink } from "fs/promises"; +import { join } from "path"; +import { config } from "dotenv"; + +config({ quiet: true }); + +const root = process.env.FOUNDRY_ROOT; + +// Early exit +if (!root) { + console.error(`Must provide a FOUNDRY_ROOT environment variable`); + process.exit(1); +}; + +// Assert Foundry exists +if (!existsSync(root)) { + console.error(`Foundry root not found.`); + process.exit(1); +}; + +// Removing existing symlink +if (existsSync(`foundry`)) { + console.log(`Attempting to unlink foundry instance`); + try { + await unlink(`foundry`); + } catch { + console.error(`Failed to unlink foundry folder.`); + process.exit(1); + }; +}; + +// Account for if the root is pointing at an Electron install +let targetRoot = root; +if (existsSync(join(root, `resources`, `app`))) { + console.log(`Switching to use the "${root}/resources/app" directory`); + targetRoot = join(root, `resources`, `app`); +}; + +// Create symlink +console.log(`Linking foundry source into folder`); +try { + await symlink(targetRoot, `foundry`); +} catch (e) { + console.error(e); + process.exit(1); +}; diff --git a/system.json b/system.json index 9924bfe..d4b61ed 100644 --- a/system.json +++ b/system.json @@ -1,25 +1,24 @@ { "id": "ripcrypt", "title": "RipCrypt", - "description": "", - "version": "0.0.1", + "description": "A dungeon sprint RPG. Faster than an arrow to the eye. Smoother than a clean blade. Compact with consequences.", + "version": "0.2.0", "compatibility": { "minimum": 13, - "verified": 13, + "verified": "13.350", "maximum": 13 }, "authors": [ - { - "name": "Oliver Akins", - "url": "https://oliver.akins.me" - } + { "name": "Oliver" } ], "esmodules": [ "module/main.mjs" ], "styles": [ - "templates/css/common.css", - "templates/Apps/apps.css" + { + "src": "templates/css/main.css", + "layer": "system" + } ], "languages": [ { @@ -28,11 +27,12 @@ "path": "langs/en-ca.json" } ], - "url": "https://github.com/Oliver-Akins/Foundry-RipCrypt", - "manifest": "https://github.com/Oliver-Akins/Foundry-RipCrypt/releases/latest/download/module.json", - "download": "https://github.com/Oliver-Akins/Foundry-RipCrypt/releases/latest/download/release.zip", + "url": "https://github.com/Eldritch-Oliver/Foundry-RipCrypt", + "manifest": "https://github.com/Eldritch-Oliver/Foundry-RipCrypt/releases/latest/download/module.json", + "download": "#{DOWNLOAD}#", "readme": "README.md", "bugs": "", + "socket": true, "flags": { "hotReload": { "extensions": ["css", "hbs", "json", "mjs", "svg"], @@ -41,7 +41,8 @@ }, "documentTypes": { "Actor": { - "hero": {} + "hero": {}, + "geist": {} }, "Item": { "ammo": {}, @@ -52,5 +53,74 @@ "skill": {}, "weapon": {} } - } -} \ No newline at end of file + }, + "packs": [ + { + "name": "protection", + "label": "Armour & Shields", + "system": "ripcrypt", + "path": "packs/protection", + "type": "Item", + "ownership": { + "PLAYER": "OBSERVER", + "ASSISTANT": "OWNER" + } + }, + { + "name": "weapons", + "label": "Weapons & Ammo", + "system": "ripcrypt", + "path": "packs/weapons", + "type": "Item", + "ownership": { + "PLAYER": "OBSERVER", + "ASSISTANT": "OWNER" + } + }, + { + "name": "skills", + "label": "Skills", + "system": "ripcrypt", + "path": "packs/skills", + "type": "Item", + "ownership": { + "PLAYER": "OBSERVER", + "ASSISTANT": "OWNER" + } + }, + { + "name": "geist", + "label": "Geist", + "system": "ripcrypt", + "path": "packs/geist", + "type": "Actor", + "ownership": { + "PLAYER": "NONE", + "ASSISTANT": "OWNER" + } + } + ], + "packFolders": [ + { + "name": "RipCrypt Sprint Start", + "color": "#04262a", + "sorting": "m", + "folders": [ + { + "name": "Character Options", + "color": "#06393f", + "sorting": "m", + "folders": [], + "packs": [ + "protection", + "weapons", + "skills" + ] + } + ], + "packs": [ + "geist" + ] + } + ] +} diff --git a/templates/Apps/AllItemSheetV1/content.hbs b/templates/Apps/AllItemSheetV1/content.hbs index 0fb9599..523c483 100644 --- a/templates/Apps/AllItemSheetV1/content.hbs +++ b/templates/Apps/AllItemSheetV1/content.hbs @@ -1,6 +1,6 @@
{{#if meta.editable}} - + +
+ + + + + {{#if meta.embedded}} + + + {{/if}} + + + {{ rc-i18n "RipCrypt.common.cost" }} + +
+ + + + + + +
+
+
+
+
+ + {{ rc-i18n "RipCrypt.common.location" }} + + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
diff --git a/templates/Apps/ArmourSheet/style.css b/templates/Apps/ArmourSheet/style.css new file mode 100644 index 0000000..24535ce --- /dev/null +++ b/templates/Apps/ArmourSheet/style.css @@ -0,0 +1,135 @@ +.ripcrypt.ArmourSheet > .window-content { + --input-height: 1rem; + --input-underline: none; + --col-gap: 8px; + --row-gap: 8px; + + --string-tags-tag-text: var(--header-text); + --string-tags-tag-background: var(--header-background); + --string-tags-add-text: white; + --string-tags-add-background: var(--accent-1); + --string-tags-input-text: white; + --string-tags-input-background: var(--accent-2); + + --input-text: white; + --input-background: var(--accent-2); + --button-text: white; + --button-background: var(--accent-2); + + --pill-width: 100%; + --pill-border-radius: 4px; + + display: flex; + flex-direction: column; + gap: 8px; + + padding: 8px; + background: var(--base-background); + color: var(--base-text); + + hr { + background: var(--accent-1); + grid-column: 1 / -1; + height: 1px; + width: 90%; + margin: 0 auto; + + &.vertical { + grid-column: unset; + height: 100%; + } + } + + label, .label { + display: flex; + align-items: center; + box-sizing: border-box; + + padding: 2px 4px; + text-transform: uppercase; + font-size: var(--font-size-14); + overflow: hidden; + text-overflow: ellipsis; + font-weight: bold; + } + + button, input, select, .value { + border-radius: 4px; + padding: 2px 4px; + } + input[type="checkbox"] { + justify-self: end; + padding: 0; + } + .value { + border: 2px solid var(--accent-2); + } + + .contents { + display: grid; + grid-template-columns: 300px 1px 118px; + gap: 8px; + + > .contents__left { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); + column-gap: var(--col-gap); + row-gap: var(--row-gap); + } + + > .contents__right { + display: flex; + flex-direction: column; + gap: 8px; + } + } + + .section-pill { + background: var(--section-header-background); + color: var(--section-header-text); + padding: 0 4px; + border-radius: 999px; + text-align: right; + } + + rc-border { + grid-column: 1 / -1; + + > .content { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); + column-gap: var(--col-gap); + row-gap: var(--row-gap); + } + + .label { + background: purple; + } + } + + .center { + text-align: center; + } + + .compass { + --size: 35px; + width: var(--size); + height: var(--size); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + border: 2px solid var(--accent-1); + border-radius: 50%; + font-size: 1.1rem; + position: relative; + background: var(--base-background); + + > .value { + background: none; + width: 70%; + text-align: center; + padding: 0; + } + } +} diff --git a/templates/Apps/CombinedHeroSheet/crafts.css b/templates/Apps/CombinedHeroSheet/crafts.css index d2d89d1..3e803da 100644 --- a/templates/Apps/CombinedHeroSheet/crafts.css +++ b/templates/Apps/CombinedHeroSheet/crafts.css @@ -4,10 +4,12 @@ grid-template-columns: repeat(3, minmax(0, 3fr)); grid-template-rows: repeat(5, minmax(0, 1fr)); grid-auto-flow: column; + padding: 8px; .col-header { background: var(--section-header-background); color: var(--section-header-text); + border-radius: 999px; } label, .label { diff --git a/templates/Apps/CombinedHeroSheet/style.css b/templates/Apps/CombinedHeroSheet/style.css index a3f6a6a..c458b72 100644 --- a/templates/Apps/CombinedHeroSheet/style.css +++ b/templates/Apps/CombinedHeroSheet/style.css @@ -4,6 +4,7 @@ > .window-content { gap: 4px; background: var(--base-background); + overflow-y: auto; } .HeroSkillsCardV1 { diff --git a/templates/Apps/CraftCardV1/content.hbs b/templates/Apps/CraftCardV1/content.hbs new file mode 100644 index 0000000..d5d10ed --- /dev/null +++ b/templates/Apps/CraftCardV1/content.hbs @@ -0,0 +1,101 @@ +
+
+ {{rc-i18n "RipCrypt.common.glimcraft"}} +
+
+
10
+
8
+
6
+
4
+ +
+
+ +
+ {{aura.normal}} + + {{aura.heavy}} +
+
+
+
+ +
+ {{rc-i18n "RipCrypt.common.aspectNames.focus"}} + {{rc-i18n "RipCrypt.common.details"}} +
+
    + {{#each craft.focus as | craft |}} + {{#if craft}} +
  1. + {{ craft.name }} + {{#if craft.use}} + + {{/if}} +
  2. + {{else}} +
  3. + {{/if}} + {{/each}} +
+ +
+ {{rc-i18n "RipCrypt.common.aspectNames.flect"}} + {{rc-i18n "RipCrypt.common.details"}} +
+
    + {{#each craft.flect as | craft |}} + {{#if craft}} +
  1. + {{ craft.name }} + {{#if craft.use}} + + {{/if}} +
  2. + {{else}} +
  3. + {{/if}} + {{/each}} +
+ +
+ {{rc-i18n "RipCrypt.common.aspectNames.fract"}} + {{rc-i18n "RipCrypt.common.details"}} +
+
    + {{#each craft.fract as | craft |}} + {{#if craft}} +
  1. + {{ craft.name }} + {{#if craft.use}} + + {{/if}} +
  2. + {{else}} +
  3. + {{/if}} + {{/each}} +
+
diff --git a/templates/Apps/CraftCardV1/style.css b/templates/Apps/CraftCardV1/style.css new file mode 100644 index 0000000..ae85b05 --- /dev/null +++ b/templates/Apps/CraftCardV1/style.css @@ -0,0 +1,137 @@ +.ripcrypt .CraftCardV1 { + --col-gap: 8px; + + display: grid; + column-gap: var(--col-gap); + grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-rows: repeat(15, minmax(0, 1fr)); + + background: var(--base-background); + color: var(--base-text); + + .col-header { + display: flex; + flex-direction: row; + align-items: center; + background: var(--section-header-background); + color: var(--section-header-text); + padding: 2px 4px; + border-radius: 999px; + } + + label, .label { + box-sizing: border-box; + padding: 2px 4px; + text-transform: uppercase; + font-size: var(--font-size-14); + overflow: hidden; + text-overflow: ellipsis; + font-weight: bold; + } + + .aura-container { + grid-column: 1 / -1; + grid-row: 2 / span 4; + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + grid-template-rows: minmax(0, 1fr); + position: relative; + } + + .circle-fragment, .full-circle { + display: flex; + justify-content: center; + align-items: center; + } + + .circle-fragment { + border-top-left-radius: 24% 100%; + border-bottom-left-radius: 25% 100%; + border-left: 2px dashed var(--accent-3); + margin-right: -5%; + } + + .full-circle { + border: 2px dashed var(--accent-3); + flex-grow: 0; + border-radius: 999px; + width: 80%; + aspect-ratio: 1; + align-self: center; + justify-self: center; + grid-row: 1; + grid-column: 4; + } + + .caster-silhouette { + grid-column: 4 / span 4; + grid-row: 1; + position: absolute; + left: 2rem; + width: 70%; + bottom: -10px; + } + + .aura-values { + grid-row: 1; + grid-column: -3 / -1; + display: flex; + justify-content: center; + align-items: center; + z-index: 3; + + .dual-pill { + border-radius: 999px; + background: var(--accent-1); + display: flex; + flex-direction: row; + gap: 0.25rem; + align-items: center; + padding-left: 8px; + margin-left: 1rem; + margin-bottom: 1.2rem; + } + + .values { + border-radius: 999px; + margin: 2px; + background: var(--base-background); + color: var(--base-text); + padding: 0.125rem 0.5rem; + display: flex; + flex-direction: row; + gap: 0.5rem; + --slash-color: var(--accent-1); + } + } + + .craft-list { + display: grid; + grid-template-rows: subgrid; + + > :nth-child(even) { + background: var(--alt-row-background); + color: var(--alt-row-text); + } + } + + span.name { + flex-grow: 1; + } + + [data-aspect="focus"] { --row: 6; --col: 1; } + [data-aspect="flect"] { --row: 6; --col: 2; } + [data-aspect="fract"] { --row: 11; --col: 1; } + + [data-aspect] { + &.aspect-header { + z-index: 1; + grid-row: var(--row); + grid-column: var(--col); + } + &.craft-list { + grid-row: calc(var(--row) + 1) / span 4; + grid-column: var(--col); + } + } +} diff --git a/templates/Apps/CryptApp/delveConditions.hbs b/templates/Apps/CryptApp/delveConditions.hbs deleted file mode 100644 index 4e0f00d..0000000 --- a/templates/Apps/CryptApp/delveConditions.hbs +++ /dev/null @@ -1,29 +0,0 @@ -
- -
- Difficulty -
-
- {{ difficulty }} - {{#if meta.editable}} -
- - -
- {{/if}} -
-
-
diff --git a/templates/Apps/CryptApp/fate.hbs b/templates/Apps/CryptApp/fate.hbs deleted file mode 100644 index c1a5ba6..0000000 --- a/templates/Apps/CryptApp/fate.hbs +++ /dev/null @@ -1,12 +0,0 @@ -
- -
- Fate -
-
- N -
-
-
diff --git a/templates/Apps/CryptApp/style.css b/templates/Apps/CryptApp/style.css deleted file mode 100644 index 9d6c772..0000000 --- a/templates/Apps/CryptApp/style.css +++ /dev/null @@ -1,18 +0,0 @@ -.ripcrypt.ripcrypt--CryptApp { - max-width: initial; - min-width: initial; - - > .window-header .window-title { - text-align: center; - } - - > .window-content { - background: var(--base-background); - padding: 4px; - }; - - .row { - display: flex; - flex-direction: row; - } -} diff --git a/templates/Apps/CryptApp/turnCount.hbs b/templates/Apps/CryptApp/turnCount.hbs deleted file mode 100644 index 0653413..0000000 --- a/templates/Apps/CryptApp/turnCount.hbs +++ /dev/null @@ -1,12 +0,0 @@ -
- -
- Turn -
-
- 1 -
-
-
diff --git a/templates/Apps/DelveDiceHUD/difficulty.hbs b/templates/Apps/DelveDiceHUD/difficulty.hbs new file mode 100644 index 0000000..bf5704f --- /dev/null +++ b/templates/Apps/DelveDiceHUD/difficulty.hbs @@ -0,0 +1,14 @@ +
+
+ {{dc}} + +
+
diff --git a/templates/Apps/DelveDiceHUD/fateCompass.hbs b/templates/Apps/DelveDiceHUD/fateCompass.hbs new file mode 100644 index 0000000..983e6bf --- /dev/null +++ b/templates/Apps/DelveDiceHUD/fateCompass.hbs @@ -0,0 +1,64 @@ +
+
+
+ + {{#if meta.editable}} + + + + + {{else}} + {{rc-i18n "RipCrypt.common.ordinals.North.abbv"}} + {{rc-i18n "RipCrypt.common.ordinals.West.abbv"}} + {{rc-i18n "RipCrypt.common.ordinals.East.abbv"}} + {{rc-i18n "RipCrypt.common.ordinals.South.abbv"}} + {{/if}} +
+
+
diff --git a/templates/Apps/DelveDiceHUD/style.css b/templates/Apps/DelveDiceHUD/style.css new file mode 100644 index 0000000..55128c2 --- /dev/null +++ b/templates/Apps/DelveDiceHUD/style.css @@ -0,0 +1,88 @@ +#ripcrypt-delve-dice { + display: grid; + grid-template-columns: max-content 2rem 90px 2rem max-content; + gap: 8px; + padding: 4px 1.5rem; + background: var(--DelveDice-background); + align-items: center; + justify-items: center; + pointer-events: all; + + border-radius: 0 0 999px 999px; + + button { + &:hover { + cursor: pointer; + } + } + + #fate-compass { + width: 100%; + height: 100%; + overflow: visible; + position: relative; + + .compass-container { + position: absolute; + background: var(--DelveDice-background); + border-radius: 0 0 999px 999px; + padding: 4px; + width: 100%; + } + + .compass { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-template-rows: repeat(3, minmax(0, 1fr)); + grid-template-areas: + ". N ." + "W A E" + ". S ."; + align-items: center; + justify-items: center; + background: var(--accent-2); + border-radius: 999px; + aspect-ratio: 1; + } + + .compass-pointer { + grid-area: A; + transition: 500ms transform; + transform: rotate(-90deg); /* North by default */ + } + } + + #the-hourglass, + #delve-difficulty { + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + position: relative; + + .icon-container { + position: absolute; + width: 34px; + display: grid; + padding: 4px 0; + background: var(--accent-1); + border-radius: 8px; + + > * { + grid-row: 1 / -1; + grid-column: 1 / -1; + } + + span { + font-size: 1.25rem; + z-index: 2; + align-self: center; + justify-self: center; + } + + rc-svg { + inset: 4px; + } + } + } +} diff --git a/templates/Apps/DelveDiceHUD/tour/current.hbs b/templates/Apps/DelveDiceHUD/tour/current.hbs new file mode 100644 index 0000000..8c802ce --- /dev/null +++ b/templates/Apps/DelveDiceHUD/tour/current.hbs @@ -0,0 +1,16 @@ +
+
+ + {{sandsOfFate}} + + +
+
diff --git a/templates/Apps/DelveDiceHUD/tour/next.hbs b/templates/Apps/DelveDiceHUD/tour/next.hbs new file mode 100644 index 0000000..5e102f0 --- /dev/null +++ b/templates/Apps/DelveDiceHUD/tour/next.hbs @@ -0,0 +1,22 @@ +
+ {{#if meta.editable}} + + {{else}} + {{!-- This is here to prevent height collapsing --}} + ​ + {{/if}} +
diff --git a/templates/Apps/DelveDiceHUD/tour/previous.hbs b/templates/Apps/DelveDiceHUD/tour/previous.hbs new file mode 100644 index 0000000..b8c59b7 --- /dev/null +++ b/templates/Apps/DelveDiceHUD/tour/previous.hbs @@ -0,0 +1,23 @@ +
+ {{#if meta.editable}} + + {{else}} + {{!-- This is here to prevent height collapsing --}} + ​ + {{/if}} +
+ diff --git a/templates/Apps/DicePool/drag.hbs b/templates/Apps/DicePool/drag.hbs index 0fb4a0f..11f1974 100644 --- a/templates/Apps/DicePool/drag.hbs +++ b/templates/Apps/DicePool/drag.hbs @@ -5,7 +5,7 @@
{{ rc-i18n "RipCrypt.common.drag" }}
-
+
-
+
    {{#each skills.grit as | skill |}} @@ -23,6 +36,19 @@
    {{ rc-i18n "RipCrypt.Apps.gait-skills" }} +
      {{#each skills.gait as | skill |}} @@ -45,6 +71,19 @@
      {{ rc-i18n "RipCrypt.Apps.grip-skills" }} +
        {{#each skills.grip as | skill |}} @@ -67,6 +106,19 @@
        {{ rc-i18n "RipCrypt.Apps.glim-skills" }} +
          {{#each skills.glim as | skill |}} @@ -89,6 +141,21 @@
          {{ rc-i18n "RipCrypt.common.gear" }} + +
          {{ rc-i18n "RipCrypt.common.slot" }}
            @@ -104,18 +171,9 @@ {{/each}}
          -
          -
          - {{ rc-i18n "RipCrypt.common.ammo"}} -
          -
          - {{ ammo }} -
          -
          - {{!-- * Currencies --}}
          -
          +
          @@ -126,7 +184,7 @@ value="0" >
          -
          +
          @@ -137,7 +195,7 @@ value="0" >
          -
          +
          @@ -149,4 +207,63 @@ >
          -
          \ No newline at end of file + + {{!-- * Ammo Summary & Stars --}} +
            +
          • +
            + +
            + {{ rc-i18n "RipCrypt.common.ammo"}} +
            +
            + {{ ammo }} +
            +
            +
          • + {{#each favouriteAmmo as | data |}} +
          • + {{#if data}} +
            +
            + {{data.name}} +
            + +
            + {{else}} +
            + {{ rc-i18n "RipCrypt.Apps.starred-ammo-placeholder" }} +
            + {{/if}} +
          • + {{/each}} +
          + + {{!-- * Aura Size --}} + {{#if aura}} +
          + +
          + {{aura.normal}} + + {{aura.heavy}} +
          +
          + {{/if}} +
          diff --git a/templates/Apps/HeroSkillsCardV1/style.css b/templates/Apps/SkillsCardV1/style.css similarity index 60% rename from templates/Apps/HeroSkillsCardV1/style.css rename to templates/Apps/SkillsCardV1/style.css index 9c092ca..c1815ce 100644 --- a/templates/Apps/HeroSkillsCardV1/style.css +++ b/templates/Apps/SkillsCardV1/style.css @@ -1,4 +1,4 @@ -.ripcrypt .HeroSkillsCardV1 { +.ripcrypt .SkillsCardV1 { /* Foundry Variable Tweaks */ --input-height: 1rem; @@ -6,8 +6,9 @@ display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); - grid-template-rows: repeat(13, minmax(0, 1fr)); + grid-template-rows: repeat(14, minmax(0, 1fr)); column-gap: var(--col-gap); + row-gap: var(--row-gap); background: var(--base-background); color: var(--base-text); @@ -33,8 +34,9 @@ .list-header { display: flex; - justify-content: space-between; + gap: 4px; align-items: center; + border-radius: 999px; } .skill-list { display: grid; @@ -66,20 +68,20 @@ .grip-skills-header { grid-column: 1 / span 1; - grid-row: 6 / span 1; + grid-row: 7 / span 1; } .grip-skills { grid-column: 1 / span 1; - grid-row: 7 / span 4; + grid-row: 8 / span 4; } .glim-skills-header { grid-column: 2 / span 1; - grid-row: 6 / span 1; + grid-row: 7 / span 1; } .glim-skills { grid-column: 2 / span 1; - grid-row: 7 / span 4; + grid-row: 8 / span 4; } .gear-list { @@ -98,28 +100,83 @@ } } + .ammo-list { + grid-column: 1 / span 2; + grid-row: 13 / span 2; + display: grid; + grid-template-columns: subgrid; + grid-template-rows: subgrid; + list-style-type: none; + padding: 0; + } + .currencies { grid-column: 1 / span 2; - grid-row: 13 / span 1; + grid-row: 12 / span 1; display: grid; column-gap: var(--col-gap); grid-template-columns: repeat(3, minmax(0, 1fr)); } - .half-pill { + .aura-size { + grid-column: 3; + grid-row: 14; + } + + .pill { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); align-items: center; background: var(--section-header-background); - border-radius: 0 999px 999px 0; + border-radius: 999px; color: var(--section-header-text); + padding: 2px 0 2px 4px; --input-background: var(--base-background); --input-text: var(--base-text); - .input { - margin: 2px; + &.with-icon { + grid-template-columns: min-content minmax(0, 1.5fr) minmax(0, 1fr); + gap: 4px; + } + + label, .label { + padding: 0; + white-space: nowrap; + text-overflow: ellipsis; + } + + input, .input { + margin: 0 2px 0 0; border-radius: 999px; text-align: center; } } + + .dual-pill { + border-radius: 999px; + background: var(--accent-1); + display: grid; + grid-template-columns: 2fr 1fr; + gap: 0.25rem; + align-items: center; + padding-left: 8px; + + .values { + border-radius: 999px; + margin: 2px; + background: var(--base-background); + color: var(--base-text); + padding: 0.125rem 0.5rem; + display: flex; + flex-direction: row; + justify-content: space-evenly; + gap: 0.5rem; + --slash-color: var(--accent-1); + } + + .value { + flex-grow: 1; + text-align: center; + } + } } diff --git a/templates/Apps/HeroSummaryCardV1/content.hbs b/templates/Apps/StatsCardV1/content.hbs similarity index 74% rename from templates/Apps/HeroSummaryCardV1/content.hbs rename to templates/Apps/StatsCardV1/content.hbs index 23b84c8..d4c75e2 100644 --- a/templates/Apps/HeroSummaryCardV1/content.hbs +++ b/templates/Apps/StatsCardV1/content.hbs @@ -1,13 +1,10 @@ -
          +
          {{!-- * Header --}}
          Logo Image
          -
          +
          + +
          {{!-- * Armour --}}
          -
          +
          {{ rc-i18n "RipCrypt.common.armour" }}
          - + {{#if (eq actor.type "hero")}} + + {{else}} + + {{/if}} {{#each armours as | slot |}}
          @@ -56,41 +72,41 @@
          • - {{ rc-i18n "RipCrypt.common.anatomy.head" }} + {{ rc-i18n "RipCrypt.common.anatomy.head" }} {{ armours.head.name }}
          • - {{ rc-i18n "RipCrypt.common.anatomy.body" }} + {{ rc-i18n "RipCrypt.common.anatomy.body" }} {{ armours.body.name }}
          • - {{ rc-i18n "RipCrypt.common.anatomy.arms" }} + {{ rc-i18n "RipCrypt.common.anatomy.arms" }} {{ armours.arms.name }}
          • - {{ rc-i18n "RipCrypt.common.anatomy.legs" }} + {{ rc-i18n "RipCrypt.common.anatomy.legs" }} {{ armours.legs.name }}
          • - {{ rc-i18n "RipCrypt.common.shield" }} + {{ rc-i18n "RipCrypt.common.shield" }} {{ shield.name }}
          @@ -98,11 +114,11 @@ {{!-- * Fate & Advancement --}}
          -