diff --git a/README.md b/README.md index fa32f0d..08728ca 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,12 @@ ![Download Count](https://img.shields.io/github/downloads/AngryBeaver/beavers-crafting/total?color=bright-green) ## Work in progress -! Carefull structure will probably change until i finalize this module, + +! 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 ! + ## Features ### Loot subtype Recipe ![img.png](pictures/newItem.png) @@ -17,11 +20,21 @@ Default is "Create New Item" obviously you need to adapt if you have a different ### Configure subtype Recipe ![img.png](pictures/configure.png) -- cost: you may add costs to the crafting process -- ingredients: you add items that will get consumed while crafting -- skill: you may add a skill that is required in the crafting process - - your may check that all costs are payed in the crafting process no matter of success or fail. -- results: you may add items as a result of a successfull crafting process of this recipe. +#### cost: +you may add costs to the crafting process +#### Ingredients: + + +You may add Items via drag and drop as Ingredients. +#### skill: +you may add a skill that is required in the crafting process. +you can enable that costs and ingredients are also payed when the check failed. +#### results: +![img.png](pictures/rollTable.png) + +You may add Items or RollTable via drag and drop as result. +The result is the outcome of a successfull crafting process. +If you add a RollTable you will get quantity amount of rolls on that table not one roll quantity of times. ### Crafting ![img.png](pictures/crafting.png) @@ -41,11 +54,16 @@ or throu recipe compendium You will see a chat message with your result +## latest features: +### 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 -I want to create recipes with "any" weapon or mushroom therefor i may need some new fields in recipe or a new subtype ingredient -### results should include rollTables -I want to create a random potion with random ingredients. +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 +### results should include rollTables (done 0.2.x) +I want to create a random potion. ### macro I want to be able to add macros to recipes. giving them more flexibility e.g. get damage on certain recipes where you failed your check. @@ -69,6 +87,5 @@ Actor Items will get merged to stacks in the crafting process. (only those that match ingredients or results) - ## Credits Copy organizational structur from midi-qol (gulpfile,package.json,tsconcig.json) \ No newline at end of file diff --git a/lang/en.json b/lang/en.json index 1e3efa7..3fb892f 100644 --- a/lang/en.json +++ b/lang/en.json @@ -20,7 +20,12 @@ }, "crafting-app": { "title": "Beaver's Recipe Compendium", - "button": "Craft" + "button": "Craft", + "errors": { + "tableNotFound": "RollTable not found for : ", + "tableNotValid": "RollTable return invalid Object : ", + "tableItemNotFound": "Item not found in RollTable for :" + } }, "settings": { "craftById": { diff --git a/module.json b/module.json index e1c405b..fe4d822 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.1.0", + "version": "0.2.0", "authors": [ { "name": "angryBeaver", diff --git a/package.json b/package.json index cfb142f..e5cfed5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "beavers-crafting", "title": "Beaver's Crafting", - "version": "0.1.0", + "version": "0.2.0", "description": "Crafting", "devDir": "C:\\Users\\User\\AppData\\Local\\FoundryVTT\\Data\\modules", "main": "src/main.js", diff --git a/pictures/rollTable.png b/pictures/rollTable.png new file mode 100644 index 0000000..66c035e Binary files /dev/null and b/pictures/rollTable.png differ diff --git a/src/Crafting.ts b/src/Crafting.ts index e743fa4..0c147f0 100644 --- a/src/Crafting.ts +++ b/src/Crafting.ts @@ -1,7 +1,8 @@ -import {Recipe} from "./Recipe.js"; +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"; export class Crafting { recipe: Recipe; @@ -14,57 +15,46 @@ export class Crafting { this.actor = actor; this.item = item; } - static fromOwned(item):Crafting{ + + static fromOwned(item): Crafting { return new Crafting(item.parent, item); } - static async from(actorId, itemId):Promise { + static async from(actorId, itemId): Promise { const actor = await fromUuid("Actor." + actorId) const item = await fromUuid("Item." + itemId); return new Crafting(actor, item); } - async craft():Promise { + async craft(): Promise { const result = await this.checkSkill(); - RecipeCompendium.validateRecipeToItemList(this.recipe,this.actor.items,result); + RecipeCompendium.validateRecipeToItemList(this.recipe, this.actor.items, result); this.checkCurrency(result); - this.addResults(result); + await this.addResults(result); await this.updateActor(result); await this._sendToChat(result); - console.log(result); return result; } - //if you have to comment it, its not clean code ! - addResults(result?:Result):Result { + async checkSkill(result?: Result): Promise { if (!result) result = new DefaultResult(); if (result.hasErrors) return result; - for (const [k, component] of Object.entries(this.recipe.results)) { - const itemChange = RecipeCompendium.findComponentInList(this.actor.items,component); - if (itemChange.toUpdate["system.quantity"] == 0) { // actor does not have item - result.changes.items.toCreate.push(component); //add that item - } else { // actor does have item - const updates = result.changes.items.toUpdate - .filter(x => x._id === itemChange.toUpdate._id); - if (updates.length > 0) { //crafting already updated that item - updates.forEach(x => x["system.quantity"] = x["system.quantity"] + component.quantity) //reupdate it - } else { //crafting does not update that item - if (result.changes.items.toDelete.includes(itemChange.toUpdate._id)) { //crafting deleted that item - result.changes.items.toCreate.push(component); //add that item // now i delete it then create it again. - } else { //on actor but not yet touched - itemChange.toUpdate["system.quantity"] = itemChange.toUpdate["system.quantity"] + component.quantity - result.changes.items.toUpdate.push(itemChange.toUpdate); - result.changes.items.toDelete.push(...itemChange.toDelete); - - } - } + if (this.recipe.skill) { + this.roll = await this.actor.rollSkill(this.recipe.skill.name, {"chatMessage": false}); + result.skill = { + name: this.recipe.skill.name, + difference: this.roll.total - this.recipe.skill.dc, + total: this.roll.total + } + if (this.roll.total < this.recipe.skill.dc) { + result.hasErrors = true; } } return result; } //simple stupid functional but not performant (yagni) - checkCurrency(result?:Result):Result { + checkCurrency(result?: Result): Result { if (!result) result = new DefaultResult(); result.changes.currencies = this.actor.system.currency; if (this.recipe.currency) { @@ -78,40 +68,22 @@ export class Crafting { return result; } - async checkSkill(result?:Result):Promise { + async addResults(result?: Result): Promise { if (!result) result = new DefaultResult(); if (result.hasErrors) return result; - if (this.recipe.skill) { - this.roll = await this.actor.rollSkill(this.recipe.skill.name, {"chatMessage": false}); - result.skill = { - name:this.recipe.skill.name, - difference:this.roll.total-this.recipe.skill.dc, - total:this.roll.total - } - if (this.roll.total < this.recipe.skill.dc) { - result.hasErrors = true; - } + const components = await this._getResultComponents(result); + for (const component of components) { + this._addComponentToResult(result, component); } return result; } - async _sendToChat(result:Result) { - let content = await renderTemplate(`modules/${Settings.NAMESPACE}/templates/crafting-chat.hbs`, - {recipe: this.recipe, result: result, roll: this.roll}) - content = await TextEditor.enrichHTML(content); - ChatMessage.create({ - content: content, - speaker: {actor: this.actor.id}, - }) - } - - - async updateActor(result:Result) { + async updateActor(result: Result) { if (!result) result = new DefaultResult(); - if (result.hasErrors && (!this.recipe.skill?.consume || !result.skill)) return; + if (result.hasException || (result.hasErrors && (!this.recipe.skill?.consume || !result.skill))) return; await this.actor.updateEmbeddedDocuments("Item", result.changes.items.toUpdate); await this.actor.deleteEmbeddedDocuments("Item", result.changes.items.toDelete); - const createItems:any[] = []; + const createItems: any[] = []; for (const component of result.changes.items.toCreate) { if (component.uuid) { const item = await fromUuid(component.uuid); @@ -132,8 +104,53 @@ export class Crafting { return result; } - processId() { - return foundry.utils.randomID(); + async _sendToChat(result: Result) { + let content = await renderTemplate(`modules/${Settings.NAMESPACE}/templates/crafting-chat.hbs`, + {recipe: this.recipe, result: result, roll: this.roll}) + content = TextEditor.enrichHTML(content); + await ChatMessage.create({ + content: content, + speaker: {actor: this.actor.id}, + }) + } + + //if you have to comment it, its not clean code ! + _addComponentToResult(result: Result, component: Component) { + const itemChange = RecipeCompendium.findComponentInList(this.actor.items, component); + if(result.results[component.uuid]){ + DefaultComponent.inc(result.results[component.uuid]) + }else{ + result.results[component.uuid] = component; + } + if (itemChange.toUpdate["system.quantity"] == 0) { // actor does not have item + result.changes.items.toCreate.push(component); //add that item + } else { // actor does have item + const updates = result.changes.items.toUpdate + .filter(x => x._id === itemChange.toUpdate._id); + if (updates.length > 0) { //crafting already updated that item + updates.forEach(x => x["system.quantity"] = x["system.quantity"] + component.quantity) //reupdate it + } else { //crafting does not update that item + if (result.changes.items.toDelete.includes(itemChange.toUpdate._id)) { //crafting deleted that item + result.changes.items.toCreate.push(component); //add that item // now i delete it then create it again. + } else { //on actor but not yet touched + itemChange.toUpdate["system.quantity"] = itemChange.toUpdate["system.quantity"] + component.quantity + result.changes.items.toUpdate.push(itemChange.toUpdate); + result.changes.items.toDelete.push(...itemChange.toDelete); + + } + } + } } + async _getResultComponents(result: Result): Promise { + const items = Object.values(this.recipe.results).filter(component => component.type === "Item"); + const tables = Object.values(this.recipe.results).filter(component => component.type === "RollTable"); + for (const component of tables) { + items.push(...await rollTableToComponents(component, result)); + if (result.hasErrors) return []; + } + return items; + } + + } diff --git a/src/Recipe.ts b/src/Recipe.ts index 44ecb50..5aa2058 100644 --- a/src/Recipe.ts +++ b/src/Recipe.ts @@ -56,27 +56,27 @@ export class Recipe implements RecipeStoreData{ return {...this.results,...this._trash.results} } - addIngredient(entity,uuid) { - if(!this.ingredients[entity.id]){ - this.ingredients[entity.id] = new DefaultComponent(entity,uuid); + addIngredient(entity,uuid,type) { + if(!this.ingredients[uuid]){ + this.ingredients[uuid] = new DefaultComponent(entity,uuid,type); }else{ - DefaultComponent.inc(this.ingredients[entity.id]) + DefaultComponent.inc(this.ingredients[uuid]) } } - removeIngredient(id){ - delete this.ingredients[id]; - this._trash.ingredients["-="+id] = null; + removeIngredient(uuid){ + delete this.ingredients[uuid]; + this._trash.ingredients["-="+uuid] = null; } - addResult(entity,uuid) { - if(!this.results[entity.id]){ - this.results[entity.id] = new DefaultComponent(entity,uuid); + addResult(entity,uuid,type) { + if(!this.results[uuid]){ + this.results[uuid] = new DefaultComponent(entity,uuid,type); }else{ - DefaultComponent.inc(this.results[entity.id]) + DefaultComponent.inc(this.results[uuid]) } } - removeResults(id) { - delete this.results[id]; - this._trash.results["-=" + id] = null; + removeResults(uuid) { + delete this.results[uuid]; + this._trash.results["-=" + uuid] = null; } addSkill() { this.skill = new DefaultSkill(); @@ -103,13 +103,15 @@ export class DefaultComponent implements Component { quantity: number; sourceId: string; uuid: string; + type: string; - constructor(entity,uuid) { + constructor(entity, uuid, type) { this.id = entity.id; this.uuid = uuid; + this.type = type; this.name = entity.name; this.img = entity.img; - this.quantity = entity.system.quantity; + this.quantity = entity.system?.quantity || 1; this.sourceId = entity.flags.core?.sourceId; } diff --git a/src/RecipeCompendium.ts b/src/RecipeCompendium.ts index 449e1ac..b23abb2 100644 --- a/src/RecipeCompendium.ts +++ b/src/RecipeCompendium.ts @@ -121,6 +121,8 @@ export class DefaultResult implements Result { }, currencies: {}, }; + results = {}; hasErrors = false; + hasException:false; isAvailable = true; } \ No newline at end of file diff --git a/src/RecipeSheet.js b/src/RecipeSheet.ts similarity index 80% rename from src/RecipeSheet.js rename to src/RecipeSheet.ts index 94116d2..dbfaf27 100644 --- a/src/RecipeSheet.js +++ b/src/RecipeSheet.ts @@ -4,9 +4,16 @@ import {getCurrencies, getSkills} from "./systems/dnd5e.js" import {RecipeCompendium} from "./RecipeCompendium.js"; import {getDataFrom} from "./apps/CraftingApp.js"; -const recipeSheets = []; +const recipeSheets: { [key: string]: RecipeSheet } = {}; export class RecipeSheet { + app; + item; + editable:boolean; + recipe:Recipe; + recipeElement?; + + static bind(app, html, data) { if(RecipeCompendium.isRecipe(app.item)){ if(!recipeSheets[app.id]){ @@ -94,22 +101,27 @@ export class RecipeSheet { async _onDrop(e) { - const isIngredient = !$(e.target).parents(".beavers-crafting .recipe .ingredients").length ==0; - const isResult = !$(e.target).parents(".beavers-crafting .recipe .results").length ==0; + const isIngredient = $(e.target).parents(".beavers-crafting .recipe .ingredients").length !==0; + const isResult = $(e.target).parents(".beavers-crafting .recipe .results").length !==0; if(!isIngredient && !isResult){ return; } const data = getDataFrom(e); - if(!data || data.type !== "Item") return; - const entity = await fromUuid(data.uuid); - if(entity) { - if(isIngredient){ - this.recipe.addIngredient(entity,data.uuid); - } - if(isResult){ - this.recipe.addResult(entity,data.uuid); + if(data && + (data.type === "Item" || + (data.type === "RollTable" && isResult) + ) + ) { + const entity = await fromUuid(data.uuid); + if (entity) { + if (isIngredient) { + this.recipe.addIngredient(entity, data.uuid,data.type); + } + if (isResult) { + this.recipe.addResult(entity, data.uuid, data.type); + } + this.update(); } - this.update(); } } diff --git a/src/helpers/RollTableToComponent.js b/src/helpers/RollTableToComponent.js new file mode 100644 index 0000000..0e04223 --- /dev/null +++ b/src/helpers/RollTableToComponent.js @@ -0,0 +1,33 @@ +import {DefaultComponent} from "../Recipe.js"; + +export async function rollTableToComponents(component,result) { + const table = await fromUuid(component.uuid); + let components = []; + if (!table) { + ui.notifications.error(game.i18n.localize(`beaversCrafting.crafting-app.errors.tableNotFound`) + component.name); + result.hasErrors = true; + result.hasException = true; + return []; + } + for (let x = 0; x < component.quantity; x++) { + const object = await table.roll(); + for (const r of object.results) { + if (r.documentCollection !== "Item") { + ui.notifications.error(game.i18n.localize(`beaversCrafting.crafting-app.errors.tableNotValid`) + r.name); + result.hasErrors = true; + result.hasException = true; + return []; + }else{ + const item = await fromUuid("Item."+r.documentId); + if(!item){ + ui.notifications.error(game.i18n.localize(`beaversCrafting.crafting-app.errors.tableItemNotFound`) + r.name); + result.hasErrors = true; + result.hasException = true; + return []; + } + components.push(new DefaultComponent(item, item.uuid, r.documentCollection)); + } + } + } + return components; +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index fda4549..190c427 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,6 +10,7 @@ interface Result { } ingredients: { [key: string]: IngredientResult } + results: { [key: string]: Component } currencies: boolean; skill?:{ name: string, @@ -17,6 +18,7 @@ interface Result { difference: number, }; hasErrors:boolean; + hasException:boolean; isAvailable:boolean; } @@ -29,6 +31,7 @@ interface IngredientResult { interface Component { id:string; uuid:string; + type:string; sourceId:string; name:string; img:string; diff --git a/templates/crafting-chat.hbs b/templates/crafting-chat.hbs index 1e3fa76..25ce93d 100644 --- a/templates/crafting-chat.hbs +++ b/templates/crafting-chat.hbs @@ -62,14 +62,15 @@
{{localize "beaversCrafting.recipe.results"}}
{{#unless result.hasErrors}} - {{#each recipe.results}} + {{#each result.results}}
-
-
- {{this.quantity}} -
+
+
+

{{this.quantity}}

+
+
-
{{/each}} {{/unless}}