Skip to content

Commit

Permalink
feat: Use default item quantity path in unknown systems (#155)
Browse files Browse the repository at this point in the history
  • Loading branch information
misterpotts authored Sep 17, 2023
1 parent 4e93bb5 commit 905e59f
Show file tree
Hide file tree
Showing 12 changed files with 209 additions and 56 deletions.
2 changes: 1 addition & 1 deletion docs/_config.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
title: Fabricate 0.9.10
title: Fabricate 0.9.11
email: [email protected]
description: >-
End user documentation for the Foundry Virtual Tabletop (VTT) Module, "Fabricate".
Expand Down
8 changes: 5 additions & 3 deletions docs/api/crafting.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,11 @@ The examples below illustrate how to use the crafting API to craft recipes and s

### Setting the game system item quantity property path

Fabricate treats items in unknown game systems as having a quantity of 1.
You'll need to configure this property if your game system of choice supports item quantities and is not known to Fabricate.
You can do this by making Fabricate aware of the item quantity property path for your game system.
By default, Fabricate looks for item quantity information at the path `system.quantity` in an item's data.
If no value is found at this path, or the path is not valid, Fabricate treats all items in your game world as **having a quantity of 1**.

You'll need to configure this property if your game system of choice supports item quantities and the item data path is not known to Fabricate.
You can do this by making Fabricate aware of the item quantity property path for your world's game system.
It's really easy to do this, just call `CraftingAPI#setGameSystemItemQuantityPropertyPath`, passing in the game system ID and the property path to use.

<details markdown="block">
Expand Down
5 changes: 4 additions & 1 deletion docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,9 @@ Fabricate knows how to read and write item quantities for the following game sys

- D&D 5th Edition
- Pathfinder 2nd Edition
- Any other game system that uses `system.quantity` in item document data

Fabricate needs to be told where to find item quantity information for your world's game system.
By default, Fabricate looks for item quantity information at the path `system.quantity` in an item's data.
If no value is found at this path, or the path is not valid, Fabricate treats all items in your game world as **having a quantity of 1**.
If your world's game system uses a different path, you can tell Fabricate where to find it by changing the `game.fabricate.api.crafting.itemQuantityPropertyPath` setting.
See the [example in the Crafting API documentation](/fabricate/api/crafting#setting-the-game-system-item-quantity-property-path) for details.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fabricate",
"version": "0.9.10",
"version": "0.9.11",
"description": "A system-agnostic, flexible crafting module for FoundryVT",
"main": "index.js",
"type": "module",
Expand Down
1 change: 1 addition & 0 deletions src/scripts/Properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const Properties = {
key: "modelVersion",
targetValue: SettingVersion.V3
},
defaultItemQuantityPropertyPath: "system.quantity"
}
};

Expand Down
18 changes: 13 additions & 5 deletions src/scripts/actor/InventoryFactory.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import {CraftingInventory, Inventory} from "./Inventory";
import {DefaultObjectUtility, ObjectUtility} from "../foundry/ObjectUtility";
import {Component} from "../crafting/component/Component";
import {ItemDataManager, PropertyPathAwareItemDataManager, SingletonItemDataManager} from "./ItemDataManager";
import {
ItemDataManager,
OptimisticItemDataManagerFactory,
PropertyPathAwareItemDataManager
} from "./ItemDataManager";
import {LocalizationService} from "../../applications/common/LocalizationService";

interface InventoryFactory {
Expand All @@ -25,18 +29,22 @@ class DefaultInventoryFactory implements InventoryFactory {
private readonly _localizationService: LocalizationService;
private readonly _objectUtility: ObjectUtility;
private readonly _gameSystemItemQuantityPropertyPaths: Map<string, string>;
private readonly _optimisticItemDataManagerFactory: OptimisticItemDataManagerFactory;

constructor({
localizationService,
objectUtility = new DefaultObjectUtility(),
localizationService,
gameSystemItemQuantityPropertyPaths = DefaultInventoryFactory._KNOWN_GAME_SYSTEM_ITEM_QUANTITY_PROPERTY_PATHS,
optimisticItemDataManagerFactory = new OptimisticItemDataManagerFactory({ objectUtils: objectUtility }),
}: {
localizationService: LocalizationService;
objectUtility?: ObjectUtility;
localizationService: LocalizationService;
gameSystemItemQuantityPropertyPaths?: Map<string, string>;
optimisticItemDataManagerFactory?: OptimisticItemDataManagerFactory;
}) {
this._localizationService = localizationService;
this._objectUtility = objectUtility;
this._localizationService = localizationService;
this._optimisticItemDataManagerFactory = optimisticItemDataManagerFactory;
this._gameSystemItemQuantityPropertyPaths = gameSystemItemQuantityPropertyPaths;
}

Expand All @@ -57,7 +65,7 @@ class DefaultInventoryFactory implements InventoryFactory {
propertyPath: this._gameSystemItemQuantityPropertyPaths.get(gameSystemId),
});
} else {
itemDataManager = new SingletonItemDataManager({ objectUtils: this._objectUtility } );
itemDataManager = this._optimisticItemDataManagerFactory.make();
}

return new CraftingInventory({
Expand Down
104 changes: 101 additions & 3 deletions src/scripts/actor/ItemDataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {Component} from "../crafting/component/Component";
import {Unit} from "../common/Unit";
import {FabricateItemData} from "../foundry/DocumentManager";
import {InventoryContentsNotFoundError} from "../error/InventoryContentsNotFoundError";
import Properties from "../Properties";

interface ItemDataManager {

Expand All @@ -13,7 +14,7 @@ interface ItemDataManager {
* @param item - The item to count the quantity of.
* @returns The quantity of the specified item.
*/
count(item: any): number;
count(item: Item): number;

/**
* Prepares the additions to be made to the inventory as item update or create operations.
Expand All @@ -39,6 +40,8 @@ interface ItemDataManager {

}

export { ItemDataManager }

class SingletonItemDataManager implements ItemDataManager {

private readonly _objectUtils: ObjectUtility;
Expand Down Expand Up @@ -95,6 +98,8 @@ class SingletonItemDataManager implements ItemDataManager {

}

export { SingletonItemDataManager }

class PropertyPathAwareItemDataManager implements ItemDataManager {

private readonly _objectUtils: ObjectUtility;
Expand All @@ -111,7 +116,11 @@ class PropertyPathAwareItemDataManager implements ItemDataManager {
this._propertyPath = propertyPath;
}

count(item: any): number {
get propertyPath(): string {
return this._propertyPath;
}

count(item: Item): number {
const quantity = this._objectUtils.getPropertyValue(this._propertyPath, item);
if (typeof quantity !== "number") {
throw new Error(`Expected a number, but found ${quantity}`);
Expand Down Expand Up @@ -210,4 +219,93 @@ class PropertyPathAwareItemDataManager implements ItemDataManager {

}

export { ItemDataManager, SingletonItemDataManager, PropertyPathAwareItemDataManager }
export { PropertyPathAwareItemDataManager }

/**
* An item data manager that attempts to read the quantity of an item from a property path on the item. If that fails,
* it falls back to a singleton item data manager (treating all items as having a quantity of 1).
*/
class OptimisticItemDataManager implements ItemDataManager {

private readonly _singletonItemDataManager: SingletonItemDataManager;
private readonly _propertyPathAwareItemDataManager: PropertyPathAwareItemDataManager;

constructor({
singletonItemDataManager,
propertyPathAwareItemDataManager,
}: {
singletonItemDataManager: SingletonItemDataManager;
propertyPathAwareItemDataManager: PropertyPathAwareItemDataManager;
}) {
this._singletonItemDataManager = singletonItemDataManager;
this._propertyPathAwareItemDataManager = propertyPathAwareItemDataManager;
}

count(item: Item): number {
try {
return this._propertyPathAwareItemDataManager.count(item);
} catch (e) {
const cause: Error = e instanceof Error ? e : typeof e === "string" ? new Error(e) : new Error("An unknown error occurred");
console.warn(`${Properties.module.id} | Unable to read quantity from item using default property path "${this._propertyPathAwareItemDataManager.propertyPath}". Caused by: ${cause.message}`);
return this._singletonItemDataManager.count();
}
}

prepareAdditions(components: Combination<Component>, activeEffects: ActiveEffect[], ownedItemsByComponentId: Map<string, any[]>): {
updates: any[];
creates: any[]
} {
try {
return this._propertyPathAwareItemDataManager.prepareAdditions(components, activeEffects, ownedItemsByComponentId);
} catch (e) {
const cause: Error = e instanceof Error ? e : typeof e === "string" ? new Error(e) : new Error("An unknown error occurred");
console.warn(`${Properties.module.id} | Unable to prepare additions using default property path "${this._propertyPathAwareItemDataManager.propertyPath}". Caused by: ${cause.message}`);
return this._singletonItemDataManager.prepareAdditions(components, activeEffects);
}
}

prepareRemovals(components: Combination<Component>, ownedItemsByComponentId: Map<string, any[]>): {
updates: any[];
deletes: any[]
} {
try {
return this._propertyPathAwareItemDataManager.prepareRemovals(components, ownedItemsByComponentId);
} catch (e) {
const cause: Error = e instanceof Error ? e : typeof e === "string" ? new Error(e) : new Error("An unknown error occurred");
console.warn(`${Properties.module.id} | Unable to prepare removals using default property path "${this._propertyPathAwareItemDataManager.propertyPath}". Caused by: ${cause.message}`);
return this._singletonItemDataManager.prepareRemovals(components, ownedItemsByComponentId);
}
}

}

export { OptimisticItemDataManager }

class OptimisticItemDataManagerFactory {

private readonly _objectUtils: ObjectUtility;
private readonly _defaultItemQuantityPropertyPath: string;

constructor({
objectUtils = new DefaultObjectUtility(),
defaultItemQuantityPropertyPath = Properties.settings.defaultItemQuantityPropertyPath
}: {
objectUtils?: ObjectUtility;
defaultItemQuantityPropertyPath?: string;
} = {}) {
this._objectUtils = objectUtils;
this._defaultItemQuantityPropertyPath = defaultItemQuantityPropertyPath;
}

make(): OptimisticItemDataManager {
const singletonItemDataManager = new SingletonItemDataManager({ objectUtils: this._objectUtils });
const propertyPathAwareItemDataManager = new PropertyPathAwareItemDataManager({
objectUtils: this._objectUtils,
propertyPath: this._defaultItemQuantityPropertyPath
});
return new OptimisticItemDataManager({ singletonItemDataManager, propertyPathAwareItemDataManager });
}

}

export { OptimisticItemDataManagerFactory }
7 changes: 4 additions & 3 deletions test/CraftingAPI.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {allTestRecipes} from "./test_data/TestRecipes";
import {allTestComponents, testComponentFour, testComponentThree} from "./test_data/TestCraftingComponents";
import {allTestEssences} from "./test_data/TestEssences";
import {testCraftingSystemOne} from "./test_data/TestCrafingSystem";
import {BaseActor} from "@league-of-foundry-developers/foundry-vtt-types/src/foundry/common/documents.mjs";
import {StubActorFactory} from "./stubs/StubActorFactory";
import {DefaultCombination} from "../src/scripts/common/Combination";
import {
Expand All @@ -27,7 +26,9 @@ describe("Crafting API", () => {

test("should salvage a component with one salvage option", async () => {

const stubActor = new StubActorFactory().make(DefaultCombination.of(testComponentFour));
const stubActor = new StubActorFactory().make({
ownedComponents: DefaultCombination.of(testComponentFour)
});

const underTest = make(new Map([ [stubActor.id, stubActor] ]));
const result = await underTest.salvageComponent({
Expand Down Expand Up @@ -76,7 +77,7 @@ describe("Crafting API", () => {

});

function make(stubActors: Map<string, BaseActor> = new Map()): CraftingAPI {
function make(stubActors: Map<string, Actor> = new Map()): CraftingAPI {
const stubLocalizationService = new StubLocalizationService();
return new DefaultCraftingAPI({
recipeAPI: new StubRecipeAPI({
Expand Down
Loading

0 comments on commit 905e59f

Please sign in to comment.