From b34dd64d6d585040109c0cbae95529b667afc80a Mon Sep 17 00:00:00 2001 From: AngryBeaver Date: Mon, 17 Oct 2022 18:48:19 +0200 Subject: [PATCH] initial anyOf --- README.md | 13 +- css/crafting.css | 35 +++- icons/anyOf.png | Bin 0 -> 2940 bytes lang/en.json | 19 +- module.json | 2 +- package.json | 2 +- src/Crafting.ts | 31 +++- src/Recipe.ts | 54 +++--- src/Settings.js | 22 +-- src/apps/AnyOfSheet.ts | 170 ++++++++++++++++++ src/apps/CraftingApp.ts | 18 +- src/{ => apps}/RecipeCompendium.ts | 75 +++++--- src/{ => apps}/RecipeSheet.ts | 17 +- .../{RollTableToComponent.js => Utility.js} | 13 ++ src/main.js | 24 ++- src/types.ts | 5 + templates/anyof-sheet.hbs | 23 +++ templates/recipe-sheet.hbs | 4 +- 18 files changed, 409 insertions(+), 118 deletions(-) create mode 100644 icons/anyOf.png create mode 100644 src/apps/AnyOfSheet.ts rename src/{ => apps}/RecipeCompendium.ts (58%) rename src/{ => apps}/RecipeSheet.ts (90%) rename src/helpers/{RollTableToComponent.js => Utility.js} (85%) create mode 100644 templates/anyof-sheet.hbs diff --git a/README.md b/README.md index 08728ca..cc287cb 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ! Carefully structure will probably change until i finalize this module with version 1.0.0 !, so do not start creating tones of recipes already ! The future upgrades might break them. -! breaking change release: 0.1.x -> 0.2.x ! +! breaking change release: 0.2.x -> 0.3.x ! ## Features ### Loot subtype Recipe @@ -55,13 +55,22 @@ or throu recipe compendium You will see a chat message with your result ## latest features: +### 0.3.x feature add initial anyOf ingredient +you now can have anyOf ingredients +breaking change 0.2.x -> 0.3.x ### 0.2.x feature add rollTable result you now can produce a random Potion. breaking change 0.1.x -> 0.2.x + ## Upcoming Changes -### "any" of ingredient +### "any" of ingredient (initial 0.3.x) + I want to create recipes with "any" xxx e.g. (weapon,mushroom,etc) therefor i may need some new fields in recipe or a new subtype ingredient +--- +currently it will only take randomly from avaiable items on actor in required quantity. +the precast is wrong it always shows that AnyOf is not available. + ### results should include rollTables (done 0.2.x) I want to create a random potion. ### macro diff --git a/css/crafting.css b/css/crafting.css index 8226127..187ac97 100644 --- a/css/crafting.css +++ b/css/crafting.css @@ -14,6 +14,13 @@ padding-left:2px; } +.beavers-crafting .recipe .left{ + max-width:50%; +} + +.beavers-crafting .recipe .right{ + max-width:50%; +} .beavers-crafting .recipe .attribute{ flex: 0 0 60px; border-left: 1px solid #c9c7b8; @@ -165,12 +172,12 @@ flex:0; } -.beavers-crafting .crafting-app .drop-area .item{ +.beavers-crafting .drop-area .item{ /* Center the content */ width:20px; margin:2px; } -.beavers-crafting .crafting-app .drop-area .item img{ +.beavers-crafting .drop-area .item img{ /* Center the content */ border:0px; } @@ -195,4 +202,28 @@ border-radius: 0.25rem; } +.beavers-crafting .anyOf textarea { + height: calc(100% - 30px); + resize: none; + padding:5px; + width: calc(100% - 20px); + margin:10px; +} + +.sheet-body .beavers-crafting { + height:100%; + flex:1; +} +.beavers-crafting .anyOf{ + height:100%; +} + +.beavers-crafting .anyOf .drop-area{ + margin:0px 5px; + height:34px; +} +.beavers-crafting .anyOf .bottom{ + margin: 0px 5px 10px; + flex:0 0 30px; +} diff --git a/icons/anyOf.png b/icons/anyOf.png new file mode 100644 index 0000000000000000000000000000000000000000..94f19f19ed624004751a8eb8bab3438dd34a6c7f GIT binary patch literal 2940 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 CsX(Rx literal 0 HcmV?d00001 diff --git a/lang/en.json b/lang/en.json index 3fb892f..fd86dac 100644 --- a/lang/en.json +++ b/lang/en.json @@ -11,10 +11,15 @@ "skills": "Skill", "dc": "DC", "results": "Results", - "dropitem": "drop item", + "dropItem": "drop item", "currency": "Cost", "consumeOnFailedSaveHint": "Consumes Cost on failed Skill check" }, + "anyOf": { + "dropItem": "drop item", + "button": "test item", + "error": "an 'anyOf' macro produced an error. See console (F12)" + }, "component": { "warning": "Your controlled Actor does not have an item named " }, @@ -28,17 +33,9 @@ } }, "settings": { - "craftById": { - "name": "craftById", - "hint": "when crafting the actor item has to match in name and sourceId" - }, "createItemTitle": { - "name": "createItemTitle", - "hint": "Enter the title the create item window has so we can detect it and inject our recipe subtype" - }, - "recipeSourceName":{ - "name": "recipeSourceName", - "hint": "Enter the name the source of an item must have to switch to recipe" + "name": "Title of Item Creation Dialog", + "hint": "This Title is needed to detect the Item Creation Dialog to add 'Recipe' and 'AnyOf' Subtype" } } } diff --git a/module.json b/module.json index fe17015..0b7f187 100644 --- a/module.json +++ b/module.json @@ -2,7 +2,7 @@ "title": "Beaver's Crafting System", "description": "A Crafting Module for DnD", "id": "beavers-crafting", - "version": "0.2.1", + "version": "0.3.0", "authors": [ { "name": "angryBeaver", diff --git a/package.json b/package.json index 5caa43d..be1b9fc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "beavers-crafting", "title": "Beaver's Crafting", - "version": "0.2.1", + "version": "0.3.0", "description": "Crafting", "devDir": "C:\\Users\\User\\AppData\\Local\\FoundryVTT\\Data\\modules", "main": "src/main.js", diff --git a/src/Crafting.ts b/src/Crafting.ts index 4340bcd..a683927 100644 --- a/src/Crafting.ts +++ b/src/Crafting.ts @@ -1,8 +1,9 @@ import {DefaultComponent, Recipe} from "./Recipe.js"; import {Exchange} from "./Exchange.js"; import {Settings} from "./Settings.js"; -import {DefaultResult, RecipeCompendium} from "./RecipeCompendium.js"; -import {rollTableToComponents} from "./helpers/RollTableToComponent.js"; +import {DefaultResult, RecipeCompendium} from "./apps/RecipeCompendium.js"; +import {rollTableToComponents} from "./helpers/Utility.js"; +import {AnyOf} from "./apps/AnyOfSheet.js"; export class Crafting { recipe: Recipe; @@ -11,7 +12,7 @@ export class Crafting { roll; constructor(actor, item) { - this.recipe = new Recipe(item) + this.recipe = Recipe.fromItem(item) this.actor = actor; this.item = item; } @@ -28,7 +29,8 @@ export class Crafting { async craft(): Promise { const result = await this.checkSkill(); - RecipeCompendium.validateRecipeToItemList(this.recipe, this.actor.items, result); + await this.evaluateAnyOf(); + RecipeCompendium.validateRecipeToItemList(Object.values(this.recipe.ingredients), this.actor.items, result); this.checkCurrency(result); await this.addResults(result); await this.updateActor(result); @@ -53,6 +55,27 @@ export class Crafting { return result; } + async evaluateAnyOf(){ + const toDelete:string[] = []; + const toAdd:Component[] = []; + for(const [key,component] of Object.entries(this.recipe.ingredients)){ + if(component.type === Settings.ANYOF_SUBTYPE){ + const item = await fromUuid(component.uuid); + const anyOf = new AnyOf(item); + let results = await anyOf.filter(this.actor.items); + results = results.filter(c=>c.quantity >= component.quantity); + if(results.length >= 0) { + const result = results[Math.floor(Math.random() * results.length)]; + result.quantity = component.quantity; + toAdd.push(result); + toDelete.push(key); + } + } + } + toDelete.forEach(k=>this.recipe.removeIngredient(k)); + toAdd.forEach(component=>{this.recipe.addIngredient(component,component.uuid,component.type)}); + } + //simple stupid functional but not performant (yagni) checkCurrency(result?: Result): Result { if (!result) result = new DefaultResult(); diff --git a/src/Recipe.ts b/src/Recipe.ts index bd1a79b..88f3194 100644 --- a/src/Recipe.ts +++ b/src/Recipe.ts @@ -1,5 +1,6 @@ import {Settings} from "./Settings.js"; import {DefaultCurrency} from "./Exchange.js"; +import {sanitizeUuid} from "./helpers/Utility.js"; export class Recipe implements RecipeStoreData{ id:string; @@ -11,14 +12,19 @@ export class Recipe implements RecipeStoreData{ currency?:Currency; _trash:Trash; - constructor(item) { + static fromItem(item):Recipe{ const flags = item.flags[Settings.NAMESPACE]?.recipe; - const data = mergeObject(this.defaultData(), flags || {}, {inplace: false}); - this.id = item.id; - this.name = item.name; - this.img = item.img; - this.ingredients = data.ingredients; - this.results = data.results; + const data = mergeObject({ingredients:{},results:{}}, flags || {}, {inplace: false}); + return new Recipe(item.id,item.name,item.img,data); + } + + + constructor(id,name,img,data:RecipeStoreData){ + this.id = id; + this.name = name; + this.img = img; + this.ingredients = data.ingredients || {} + this.results = data.results || {} this.skill = data.skill; this.currency = data.currency; this._trash = { @@ -27,13 +33,7 @@ export class Recipe implements RecipeStoreData{ }; } - defaultData() { - return { - ingredients: {}, - results: {}, - } - } - serialize() { + serialize():RecipeStoreData { const serialized = { ingredients: this.serializeIngredients(), skill: this.skill, @@ -57,26 +57,28 @@ export class Recipe implements RecipeStoreData{ } addIngredient(entity,uuid,type) { - if(!this.ingredients[uuid]){ - this.ingredients[uuid] = new DefaultComponent(entity,uuid,type); + const uuidS = sanitizeUuid(uuid); + if(!this.ingredients[uuidS]){ + this.ingredients[uuidS] = new DefaultComponent(entity,uuid,type); }else{ - DefaultComponent.inc(this.ingredients[uuid]) + DefaultComponent.inc(this.ingredients[uuidS]) } } - removeIngredient(uuid){ - delete this.ingredients[uuid]; - this._trash.ingredients["-="+uuid] = null; + removeIngredient(uuidS){ + delete this.ingredients[uuidS]; + this._trash.ingredients["-="+uuidS] = null; } addResult(entity,uuid,type) { - if(!this.results[uuid]){ - this.results[uuid] = new DefaultComponent(entity,uuid,type); + const uuidS = sanitizeUuid(uuid); + if(!this.results[uuidS]){ + this.results[uuidS] = new DefaultComponent(entity,uuid,type); }else{ - DefaultComponent.inc(this.results[uuid]) + DefaultComponent.inc(this.results[uuidS]) } } - removeResults(uuid) { - delete this.results[uuid]; - this._trash.results["-=" + uuid] = null; + removeResults(uuidS) { + delete this.results[uuidS]; + this._trash.results["-=" + uuidS] = null; } addSkill() { this.skill = new DefaultSkill(); diff --git a/src/Settings.js b/src/Settings.js index 9641134..aa5f359 100644 --- a/src/Settings.js +++ b/src/Settings.js @@ -1,29 +1,11 @@ export class Settings { static NAMESPACE = "beavers-crafting"; - static CRAFT_BY_ID = "craftById"; static CREATE_ITEM_TITLE = "createItemTitle"; - static RECIPE_SOURCE_NAME = "recipeSourceName"; + static RECIPE_SUBTYPE = "Recipe"; + static ANYOF_SUBTYPE = "AnyOf"; static init() { - game.settings.register(this.NAMESPACE, this.RECIPE_SOURCE_NAME, { - name: game.i18n.localize('beaversCrafting.settings.recipeSourceName.name'), - hint: game.i18n.localize('beaversCrafting.settings.recipeSourceName.hint'), - scope: "world", - config: true, - default: "Recipe", - type: String, - }); - - game.settings.register(this.NAMESPACE, this.CRAFT_BY_ID, { - name: game.i18n.localize('beaversCrafting.settings.craftById.name'), - hint: game.i18n.localize('beaversCrafting.settings.craftById.hint'), - scope: "world", - config: true, - default: true, - type: Boolean, - }); - game.settings.register(this.NAMESPACE, this.CREATE_ITEM_TITLE, { name: game.i18n.localize('beaversCrafting.settings.createItemTitle.name'), hint: game.i18n.localize('beaversCrafting.settings.createItemTitle.hint'), diff --git a/src/apps/AnyOfSheet.ts b/src/apps/AnyOfSheet.ts new file mode 100644 index 0000000..339dc50 --- /dev/null +++ b/src/apps/AnyOfSheet.ts @@ -0,0 +1,170 @@ +import {Settings} from "../Settings.js"; +import {getDataFrom, sanitizeUuid} from "../helpers/Utility.js"; +import {DefaultComponent} from "../Recipe.js"; +import {RecipeCompendium} from "./RecipeCompendium.js"; + +const anyOfSheets: { [key: string]: AnyOfSheet } = {}; + +export class AnyOfSheet { + app; + item; + editable: boolean; + anyOf: AnyOf; + anyOfElement?; + checkItem?; + + + static bind(app, html, data) { + if (isAnyOf(app.item)) { + if (!anyOfSheets[app.id]) { + anyOfSheets[app.id] = new AnyOfSheet(app, data); + } + anyOfSheets[app.id].init(html); + } + } + + constructor(app, data) { + this.app = app; + this.item = app.item; + this.editable = data.editable; + this.addDragDrop(); + + } + + init(html) { + this.anyOf = new AnyOf(this.item); + if (html[0].localName !== "div") { + html = $(html[0].parentElement.parentElement); + } + let exists = html.find(".sheet-body .beavers-crafting"); + if (exists.length != 0) { + return; + } + this.anyOfElement = $('
'); + html.find(".sheet-body").empty(); + html.find(".sheet-body").append(this.anyOfElement); + this.render(); + } + + async render() { + let macroResult:MacroResult = {value:true}; + if (this.checkItem) { + macroResult = await this.anyOf.executeMacro(this.checkItem); + } + let template = await renderTemplate('modules/beavers-crafting/templates/anyof-sheet.hbs', + {anyOf: this.anyOf, editable: this.editable, checkItem: this.checkItem, macroResult: macroResult}); + this.anyOfElement.find('.anyOf').remove(); + this.anyOfElement.append(template); + this.handleEvents(); + } + + handleEvents() { + this.anyOfElement.find('button').click(e => { + return this.render(); + }); + } + + addDragDrop() { + if (this.editable) { + const dragDrop = new DragDrop({ + dropSelector: '.sheet-body', + permissions: { + dragstart: this.app._canDragStart.bind(this.app), + drop: this.app._canDragDrop.bind(this.app) + }, + callbacks: { + dragstart: this.app._onDragStart.bind(this.app), + dragover: this.app._onDragOver.bind(this.app), + drop: this._onDrop.bind(this) + } + }); + this.app._dragDrop.push(dragDrop); + dragDrop.bind(this.app.form); + } + } + + async _onDrop(e) { + const isDropArea = $(e.target).hasClass("drop-area") + if (!isDropArea) { + return; + } + const data = getDataFrom(e); + if (data && (data.type === "Item")) { + this.checkItem = await fromUuid(data.uuid); + } + this.render(); + } + +} + +export function isAnyOf(item) { + // @ts-ignore + return (item?.type === 'loot' && item?.system?.source === Settings.ANYOF_SUBTYPE); +} + +interface AnyOfStoreData { + macro: string +} + +export class AnyOf { + macro; + img; + name; + + constructor(item) { + const flags = item.flags[Settings.NAMESPACE]?.anyOf; + const data = mergeObject(this.defaultData(), flags || {}, {inplace: false}); + this.macro = data.macro; + this.img = item.img; + this.name = item.name; + } + + defaultData() { + return { + macro: "", + } + } + + serialize(): AnyOfStoreData { + const serialized = { + macro: this.macro, + } + return serialized; + } + + async executeMacro(item): Promise> { + const AsyncFunction = (async function () { + }).constructor; + // @ts-ignore + const fn = new AsyncFunction("item", this.macro); + const result = { + value:false, + error: undefined + } + try { + result.value = await fn(item); + } catch (err) { + // @ts-ignore + logger.error(err); + result.error = err; + } + return result; + } + + async filter(itemList): Promise{ + const resultList:Component[] = []; + for(const item of itemList){ + const result = await this.executeMacro(item); + if(result.value){ + const same = resultList.filter(component => RecipeCompendium.isSame(item,component)) + if(same.length > 0){ + same[0].quantity = same[0].quantity + item.system?.quantity; + }else{ + resultList.push(new DefaultComponent(item,item.uuid,"Item")); + } + } + } + return resultList; + } + +} \ No newline at end of file diff --git a/src/apps/CraftingApp.ts b/src/apps/CraftingApp.ts index 4774e0f..9b85c11 100644 --- a/src/apps/CraftingApp.ts +++ b/src/apps/CraftingApp.ts @@ -1,7 +1,7 @@ import {getCurrencies, getSkills} from "../systems/dnd5e.js"; -import {FilterType, RecipeCompendium} from "../RecipeCompendium.js"; +import {FilterType, RecipeCompendium} from "./RecipeCompendium.js"; import {Crafting} from "../Crafting.js"; -import {DefaultComponent} from "../Recipe"; +import {getDataFrom} from "../helpers/Utility.js"; export class CraftingApp extends Application { data: { @@ -46,7 +46,7 @@ export class CraftingApp extends Application { async getData(options = {}) { const data: any = mergeObject(this.data, await super.getData(options)); - let recipes = RecipeCompendium.filterForActor(data.actor, data.filter); + let recipes = await RecipeCompendium.filterForActor(data.actor, data.filter); if(Object.values(data.filterItems).length != 0){ recipes = RecipeCompendium.filterForItems(recipes,Object.values(data.filterItems)); } @@ -61,7 +61,7 @@ export class CraftingApp extends Application { data.content = null; return null; } - data.result = RecipeCompendium.validateRecipeToItemList(data.recipe, this.data.actor.items); + data.result = RecipeCompendium.validateRecipeToItemList(Object.values(data.recipe.ingredients), this.data.actor.items); data.content = await renderTemplate('modules/beavers-crafting/templates/recipe-sheet.hbs', { recipe: data.recipe, @@ -97,7 +97,6 @@ export class CraftingApp extends Application { Crafting.from(this.data.actor.id, this.data.recipe.id) .then(crafting => { return crafting.craft(); - }).then(result => { if (!result.hasErrors) { this.render(); @@ -148,12 +147,3 @@ export class CraftingApp extends Application { } } -export function getDataFrom(e:DragEvent){ - try { - // @ts-ignore - return JSON.parse(e.dataTransfer.getData('text/plain')); - } - catch (err) { - return false; - } -} \ No newline at end of file diff --git a/src/RecipeCompendium.ts b/src/apps/RecipeCompendium.ts similarity index 58% rename from src/RecipeCompendium.ts rename to src/apps/RecipeCompendium.ts index 0844d77..ac0453b 100644 --- a/src/RecipeCompendium.ts +++ b/src/apps/RecipeCompendium.ts @@ -1,6 +1,8 @@ //the firstdraft implementation will be kept simple stupid and not performant at all. -import {Recipe} from "./Recipe.js"; -import {Settings} from "./Settings.js"; +import {Recipe} from "../Recipe.js"; +import {Settings} from "../Settings.js"; +import {AnyOf} from "./AnyOfSheet.js"; +import {sanitizeUuid} from "../helpers/Utility.js"; export class RecipeCompendium { @@ -8,9 +10,9 @@ export class RecipeCompendium { // @ts-ignore return game.items.directory.documents .filter(item => RecipeCompendium.isRecipe(item)) - .map(item => new Recipe(item)); + .map(item => Recipe.fromItem(item)); } - + //todo ANYOF static filterForItems(recipes: Recipe[], items) { return recipes.filter(recipe => { const recipeItemsInItemList = items.filter( @@ -26,27 +28,51 @@ export class RecipeCompendium { }); } - static filterForActor(actor, filter) { - return RecipeCompendium.getAll() - .filter(recipe => { - if (filter == FilterType.all) { - return true; + static async filterForActor(actor, filter) { + const list = RecipeCompendium.getAll(); + const returnList:Recipe[] = []; + for(const recipe of list){ + if (filter == FilterType.all) { + returnList.push(recipe); + }else{ + const listOfAnyOfIngredients = Object.values(recipe.ingredients).filter(component => component.type === Settings.ANYOF_SUBTYPE); + if (await this.isAnyAnyOfInList(listOfAnyOfIngredients, actor.items)) { //isAvailable or usable ! when any item matches anyOf has the given quantity + const listOfIngredientsWithoutAnyOf = Object.values(recipe.ingredients).filter(component => component.type !== Settings.ANYOF_SUBTYPE); + const result = RecipeCompendium.validateRecipeToItemList(listOfIngredientsWithoutAnyOf, actor.items); + if((filter == FilterType.usable && !result.hasErrors) + || (filter == FilterType.available && result.isAvailable)){ + returnList.push(recipe); + } } - const result = RecipeCompendium.validateRecipeToItemList(recipe, actor.items); - return ((filter == FilterType.usable && !result.hasErrors) - || (filter == FilterType.available && result.isAvailable)); - }); + } + } + return returnList; } - static validateRecipeToItemList(recipe: Recipe, listOfItems, result?: Result): Result { + static async isAnyAnyOfInList(listOfAnyOfIngredients: Component[], listOfItems) { + for (const component of listOfAnyOfIngredients) { + if (component.type === Settings.ANYOF_SUBTYPE) { + const item = await fromUuid(component.uuid); + const anyOf = new AnyOf(item); + const results = await anyOf.filter(listOfItems); + if (results.filter(c=>c.quantity >= component.quantity).length == 0) { + return false; + } + } + } + return true; + } + + static validateRecipeToItemList(listOfIngredients: Component[], listOfItems, result ?: Result): Result { if (!result) result = new DefaultResult(); - result.isAvailable = recipe.ingredients.size === 0; - for (const [k, component] of Object.entries(recipe.ingredients)) { + result.isAvailable = listOfIngredients.length === 0; + for (const component of listOfIngredients) { + const key = sanitizeUuid(component.uuid); const itemChange = RecipeCompendium.findComponentInList(listOfItems, component); const remainingQuantity = itemChange.toUpdate["system.quantity"] - component.quantity; const isAvailAble = itemChange.toUpdate["system.quantity"] > 0; - result.ingredients[k] = { + result.ingredients[key] = { component: component, isAvailable: isAvailAble, difference: remainingQuantity, @@ -86,20 +112,22 @@ export class RecipeCompendium { static isSame(item, component: Component) { const isSameName = (item, component) => item.name === component.name; - const isFromSource = (item, component) => item.flags?.core?.sourceId == component.uuid; - const hasSameSource = (item, component) => item.flags?.core?.sourceId == component.sourceId; + const isFromSource = (item, component) => item.flags?.core?.sourceId === component.uuid; + const hasSameSource = (item, component) => item.flags?.core?.sourceId === component.sourceId || item.sourceId === component.sourceId; return isSameName(item, component) && (isFromSource(item, component) || hasSameSource(item, component)); } static isRecipe(item) { // @ts-ignore - return (item?.type === 'loot' && item?.system?.source === game.settings.get(Settings.NAMESPACE, Settings.RECIPE_SOURCE_NAME)); + return (item?.type === 'loot' && item?.system?.source === Settings.RECIPE_SUBTYPE); } } export enum FilterType { - usable, available, all + usable, + available, + all } class DefaultItemChange implements ItemChange { @@ -108,7 +136,8 @@ class DefaultItemChange implements ItemChange { "_id": "", "system.quantity": 0 }; - constructor(component:Component){ + + constructor(component: Component) { this.toUpdate._id = component.id; } @@ -127,6 +156,6 @@ export class DefaultResult implements Result { }; results = {}; hasErrors = false; - hasException:false; + hasException: false; isAvailable = true; } \ No newline at end of file diff --git a/src/RecipeSheet.ts b/src/apps/RecipeSheet.ts similarity index 90% rename from src/RecipeSheet.ts rename to src/apps/RecipeSheet.ts index dbfaf27..09f023a 100644 --- a/src/RecipeSheet.ts +++ b/src/apps/RecipeSheet.ts @@ -1,8 +1,9 @@ -import {Recipe} from "./Recipe.js"; -import {Settings} from "./Settings.js"; -import {getCurrencies, getSkills} from "./systems/dnd5e.js" +import {Recipe} from "../Recipe.js"; +import {Settings} from "../Settings.js"; +import {getCurrencies, getSkills} from "../systems/dnd5e.js" import {RecipeCompendium} from "./RecipeCompendium.js"; -import {getDataFrom} from "./apps/CraftingApp.js"; +import {getDataFrom} from "../helpers/Utility.js"; +import {isAnyOf} from "./AnyOfSheet.js"; const recipeSheets: { [key: string]: RecipeSheet } = {}; @@ -41,7 +42,7 @@ export class RecipeSheet { this.recipeElement = $('
'); html.find(".sheet-body").empty(); html.find(".sheet-body").append(this.recipeElement); - this.recipe = new Recipe(this.item); + this.recipe = Recipe.fromItem(this.item); this.render(); } @@ -115,7 +116,11 @@ export class RecipeSheet { const entity = await fromUuid(data.uuid); if (entity) { if (isIngredient) { - this.recipe.addIngredient(entity, data.uuid,data.type); + let type = data.type; + if(isAnyOf(entity)){ + type = Settings.ANYOF_SUBTYPE; + } + this.recipe.addIngredient(entity, data.uuid,type); } if (isResult) { this.recipe.addResult(entity, data.uuid, data.type); diff --git a/src/helpers/RollTableToComponent.js b/src/helpers/Utility.js similarity index 85% rename from src/helpers/RollTableToComponent.js rename to src/helpers/Utility.js index 0e04223..f14ae44 100644 --- a/src/helpers/RollTableToComponent.js +++ b/src/helpers/Utility.js @@ -30,4 +30,17 @@ export async function rollTableToComponents(component,result) { } } return components; +} + +export function getDataFrom(e){ + try { + return JSON.parse(e.dataTransfer.getData('text/plain')); + } + catch (err) { + return false; + } +} + +export function sanitizeUuid(uuid){ + return uuid.replace(/\./g,'-') } \ No newline at end of file diff --git a/src/main.js b/src/main.js index 1bcf1bc..2f96244 100644 --- a/src/main.js +++ b/src/main.js @@ -1,8 +1,9 @@ import {CraftingApp} from './apps/CraftingApp.js'; -import {RecipeSheet} from './RecipeSheet.js'; +import {RecipeSheet} from './apps/RecipeSheet.js'; import {Settings} from './Settings.js'; import {Crafting} from "./Crafting.js"; -import {RecipeCompendium} from "./RecipeCompendium.js"; +import {RecipeCompendium} from "./apps/RecipeCompendium.js"; +import {AnyOfSheet} from "./apps/AnyOfSheet.js"; Hooks.once('init', async function () { Settings.init(); @@ -29,36 +30,47 @@ Hooks.on(`dnd5e.preUseItem`, (item, config, options) => { } }); -//RecipeSubTypeSheet +//SubTypeSheet Hooks.on(`renderItemSheet5e`, (app, html, data) => { RecipeSheet.bind(app, html, data); + AnyOfSheet.bind(app,html,data); }); -//add RecipeSubtype to create Item + +//add Subtype to create Item Hooks.on("preCreateItem", (doc, createData, options, user) => { if (createData.subtype && createData.subtype === 'recipe' && !foundry.utils.hasProperty(createData, "system.source")) { - doc.updateSource({"system.source": game.settings.get(Settings.NAMESPACE,Settings.RECIPE_SOURCE_NAME),"img":"icons/sundries/scrolls/scroll-worn-tan.webp"}); + doc.updateSource({"system.source": Settings.RECIPE_SUBTYPE,"img":"icons/sundries/scrolls/scroll-worn-tan.webp"}); + } + if (createData.subtype && createData.subtype === 'anyOf' && + !foundry.utils.hasProperty(createData, "system.source")) { + doc.updateSource({"system.source": Settings.ANYOF_SUBTYPE,"img":"modules/beavers-crafting/icons/anyOf.png"}); } }); + //evil Hooks.on("renderDialog", (app, html, content) => { const title = game.settings.get(Settings.NAMESPACE,Settings.CREATE_ITEM_TITLE)||"Create New Item"; + if (app.data.title === title) { if (html[0].localName !== "div") { html = $(html[0].parentElement.parentElement); } html.find("select[name='type']").append(""); + html.find("select[name='type']").append(""); if (html.find("input.subtype").length === 0) { html.find("form").append(''); } - console.log("here"); html.find("select[name='type']").on("change", function () { const name = $(this).find("option:selected").text(); let value = ""; if (name === "📜Recipe📜") { value = "recipe" } + if (name === "❔AnyOf❔") { + value = "anyOf" + } html.find("input.subtype").val(value); }) } diff --git a/src/types.ts b/src/types.ts index 2fa0131..22f7ea1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -56,3 +56,8 @@ interface ItemChange { "system.quantity": number }; } + +interface MacroResult { + value:t, + error?:Error +} \ No newline at end of file diff --git a/templates/anyof-sheet.hbs b/templates/anyof-sheet.hbs new file mode 100644 index 0000000..a530c39 --- /dev/null +++ b/templates/anyof-sheet.hbs @@ -0,0 +1,23 @@ +
+ + {{macroResult.error.message}} +
+
+ {{#if checkItem}} + + {{checkItem.name}} + + {{#if macroResult.value }} + + {{else}} + + {{/if}} + {{else}} + {{localize "beaversCrafting.anyOf.dropItem"}} + {{/if}} +
+ +
+
diff --git a/templates/recipe-sheet.hbs b/templates/recipe-sheet.hbs index 705ef9f..0ba76d5 100644 --- a/templates/recipe-sheet.hbs +++ b/templates/recipe-sheet.hbs @@ -79,7 +79,7 @@ {{/each}} {{#if editable}}
-
{{localize "beaversCrafting.recipe.dropitem"}}
+
{{localize "beaversCrafting.recipe.dropItem"}}
{{/if}} @@ -158,7 +158,7 @@ {{/each}} {{#if editable}}
-
{{localize "beaversCrafting.recipe.dropitem"}}
+
{{localize "beaversCrafting.recipe.dropItem"}}
{{/if}}