diff --git a/CHANGELOG.md b/CHANGELOG.md index e3d9bbc..8d65b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,11 @@ +# v.0.2.0 + +[BREAKING] - New macro, please replace. + +* Support for currency items +* Support for random coin items +* Support for random treasure table items. + # v.0.1.0 + * Initial version for internal testing \ No newline at end of file diff --git a/README.md b/README.md index 4d06f5d..bebdbd9 100644 --- a/README.md +++ b/README.md @@ -21,20 +21,53 @@ And the Modules: Create a macro with permission for all your players with the following content: -Macro - LOOTING +Macro [NEW MACRO] - LOOTING -`let actions = new InnocentiLoot.Loot();` +`(async () => { +let actions = new InnocentiLoot.Loot(); +await actions.Check(); +})();` ## How to Use the module basically works looking for a items in the token inventory of npc with hp 0, can be looted using the target with a macro, unless they are: classes, spells, feats, natural weapons, siege weapons, vehicle equipment and natural equipment. -Items also have a percentage (configurable) chance of not being in good use and not being moved (and deleted) to the character sheet. +### Loot Currency end Cois +From version 0.2 the module adapts a new type of loot for currencys. + +a loot type item that has the end of its name, in brackets, the abbreviation of the currencies in force will be considered coins of the same type. for example: + +* "Silver (sp)" will convert your quantity into silver coins. +* "Golden (gp)" will convert your quantity into golden coins. +* "Bubble (sp)" will convert your quantity into silver coins. + +A valid abbreviation must be in lower case and in parentheses at the end of the item name. +Alternatively you can make the item also generate a random quantity (eg. 3d6) of the item using the item's "source" field. + +For the quantity, the module will initially consider if the item's source field has a valid roll, otherwise it will consider the item's quantity field. + + +In the example above this item will generate 3d6 gold coins for the player who loot it. + +### Loot Items Rolltables. +From version 0.2 the module adapts a new type of loot for random items. +This type of item does not consider the module settings for the percentages of looting items, all items drawn will be added. +a random table loot is recognized by the name that must begin with 'Table:' followed by the name of the rollable table to be drawn, eg: + +'Table: My Random Tresures' + +will draw new items from the "My Random Tresures" rollable table add them to the loot list. + +The table should only contain items and/or other item tables. +Items can be in the items folder in your world or in any item compendium. + + +## Module Settings +Items also have a percentage (configurable in module settings) chance of not being in good use and not being moved (and deleted) to the character sheet. ## Future Features -* Looting Gold. +* Support for Module Better Tables * Pickpocket in lives npcs -* Sort Golds in loot and robbery (maybe in rolltables) ## Support If you like this module and would like to help or found a bug or request new features call me on discord @Innocenti#1455 or create a issue here. diff --git a/img/item-coin.png b/img/item-coin.png new file mode 100644 index 0000000..fe33c67 Binary files /dev/null and b/img/item-coin.png differ diff --git a/lang/en.json b/lang/en.json index 9840211..a5fea48 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1,4 +1,8 @@ { + "Looting.MsgChat": { + "looting": "Looting", + "Currency": "Currency" + }, "Looting.Errors": { "noSelect": "Select a token", "noToken": "No token is targeted", @@ -16,6 +20,8 @@ "removeItem": "Remove Item from token", "removeItemHint": "If enabled it removes items from the token (does not remove from the actor unless the token is linked)", "interactDistance": "Maximum distance to interact (in grids)", - "interactDistanceHint": "the maximum distance for players to interact with the lock in number of grids" + "interactDistanceHint": "the maximum distance for players to interact with the lock in number of grids", + "lootSystem": "Loot System", + "lootSystemHint": "Choose one of the ways to perform the loot (Soon)" } } \ No newline at end of file diff --git a/lang/pt-BR.json b/lang/pt-BR.json index 7c6c6e6..557ea83 100644 --- a/lang/pt-BR.json +++ b/lang/pt-BR.json @@ -1,21 +1,27 @@ { + "Looting.MsgChat": { + "looting": "Pilhando", + "Currency": "Moedas" + }, "Looting.Errors": { "noSelect": "Selecione um Token", "noToken": "Nenhum token esta marcado como alvo", - "novalidLoot": " não é um baú válido", - "invalidDistance": "A distancia minima para esta interação é: {dist} quadros", - "invalidCheck": "{token} já foi pilhado" + "novalidLoot": " não é um baú válido", + "invalidDistance": "A distancia minima para esta interação é: {dist} quadros", + "invalidCheck": "{token} já foi pilhado" }, "Looting.Settings": { "percentWeapon": "Pilhagem de Armas (%)", - "percentWeaponHint": "Porcentagem de chance das armas no inventório do NPC não servirem para pilhagem (Foram danificadas)", + "percentWeaponHint": "Porcentagem de chance das armas no inventório do NPC não servirem para pilhagem (Foram danificadas)", "percentEquipment": "Pilhagem de equipamento (%)", - "percentEquipmentHint": "Porcentagem de chance das armas no inventório do NPC não servirem para pilhagem (Foram danificadas)", + "percentEquipmentHint": "Porcentagem de chance das armas no inventório do NPC não servirem para pilhagem (Foram danificadas)", "percentConsumable": "Pilhagem de consumivel (%)", - "percentConsumableHint": "Porcentagem de chance das armas no inventório do NPC não servirem para pilhagem (Foram danificadas)", - "interactDistance": "Distância máxima para interagir (em grade)", - "interactDistanceHint": "A distância mimáxima para os jogadores interagirem com a trava em unidades de grade do mapa", + "percentConsumableHint": "Porcentagem de chance das armas no inventório do NPC não servirem para pilhagem (Foram danificadas)", + "interactDistance": "Distância máxima para interagir (em grade)", + "interactDistanceHint": "A distância mimáxima para os jogadores interagirem com a trava em unidades de grade do mapa", "removeItem": "Remover os Itens", - "removeItemHint": "Se ativado remove os itens do token (não remove do ator a menos que o token estiver linkado)" + "removeItemHint": "Se ativado remove os itens do token (não remove do ator a menos que o token estiver linkado)", + "lootSystem": "Sistema de Pilhagem", + "lootSystemHint": "Escolha uma das formas para realizar a pilhagem (em breve)" } } \ No newline at end of file diff --git a/module.json b/module.json index 7686ede..3e9a521 100644 --- a/module.json +++ b/module.json @@ -3,7 +3,7 @@ "title": "Innocenti Looting", "description": "
Modules for loot from monsters and Npcs
", "author": "Renato innocenti", - "version": "0.1.1", + "version": "0.2.0", "minimumCoreVersion": "0.7.0", "compatibleCoreVersion": "0.7.9", "systems": [ "dnd5e" ], diff --git a/packs/looting-macro.db b/packs/looting-macro.db index b573e07..516e2b9 100644 --- a/packs/looting-macro.db +++ b/packs/looting-macro.db @@ -1 +1 @@ -{"name":"Looting","permission":{"default":0,"KrgPitW1Ezq59fcV":3},"type":"script","flags":{"furnace":{"runAsGM":false},"core":{"sourceId":"Macro.xF8VDI28j7UzkSoG"}},"scope":"global","command":"let actions = new InnocentiLoot.Loot();","author":"KrgPitW1Ezq59fcV","img":"icons/containers/bags/coinpouch-gold-red.webp","actorIds":[],"_id":"Nh2PYfEvqiw4lK59"} +{"name":"Looting","permission":{"default":0,"oYLBQUHyYQcwtcvC":3},"type":"script","flags":{"furnace":{"runAsGM":false},"core":{"sourceId":"Macro.xF8VDI28j7UzkSoG"}},"scope":"global","command":"(async () => {\nlet actions = new InnocentiLoot.Loot();\nawait actions.Check();\n})();","author":"oYLBQUHyYQcwtcvC","img":"icons/containers/bags/coinpouch-gold-red.webp","actorIds":[],"_id":"1cW8KDJr83nOynwW"} diff --git a/scripts/ActionLoot.js b/scripts/ActionLoot.js index 1811872..9429523 100644 --- a/scripts/ActionLoot.js +++ b/scripts/ActionLoot.js @@ -1,137 +1,245 @@ -// JavaScript source code -import { SETTINGS } from './settings.js'; -import { GMActions } from './gmactions.js'; -import { PickPocket } from './pickpocket.js'; -export class ActionLoot { - constructor() { - // - if (canvas.tokens.controlled.length === 0) { - return ui.notifications.error(game.i18n.localize('Looting.Errors.noSelect')); - } - if (!game.user.targets.values().next().value) { - return ui.notifications.warn(game.i18n.localize('Looting.Errors.noToken')); - } - this.actor = canvas.tokens.controlled[0].actor; - this.targets = game.user.targets; - this.data = { - tokenid: canvas.tokens.controlled[0].id, - targetid: false, - looting: false, - ppocket: false, - currentItems: false - } - this.Check(); - } - // check targets - async Check() { - this.targets.forEach(entity => { - if (entity.id == canvas.tokens.controlled[0].id) return; - if (this.CheckDistance(entity) != true) return; - this.data.targetid = entity.id; - if (entity.data.actorData.data.attributes.hp.value <= 0 && !entity.isPC) { - // Morto - lootiar - if (entity.getFlag(SETTINGS.MODULE_NAME, SETTINGS.LOOT)) return ui.notifications.warn(game.i18n.format("Looting.Errors.invalidCheck", { token: entity.name })); // já foi lootiado. - this.LootNPC(entity.actor, this.actor); - } else { - // vivo - Roubar - if (entity.actor.getFlag(SETTINGS.MODULE_LOOT_SHEET, SETTINGS.LOOT_SHEET)) return; // não é um bau ou mercador. - //this.AttempPickpocket(entity.actor, this.actor); - } - if (game.user.isGM) { - let gmaction = new GMActions(this.data); - gmaction.Init(); - } else { - game.socket.emit(`module.${SETTINGS.MODULE_NAME}`, this.data); - } - }); - - } - - AttempPickpocket(target, actor) { - // criar um dialogo para verificar se o jogador quer mesmo fzer o pickpocket - let d = new Dialog({ - title: "PickPocket", - content: "O alvo ainda está conciente e pode reagir, Você tem certeza que deseja roubar os alvos?
", - buttons: { - one: { - icon: '', - label: "Sim", - callback: () => this.PickPocket(target, actor) - }, - two: { - icon: '', - label: "Não", - callback: () => console.log("Cancel Pickpoket") - } - }, - default: "two", - //render: html => console.log("Register interactivity in the rendered dialog"), - //close: html => console.log("This always is logged no matter which option is chosen") - }); - d.render(true); - } - - PickPocket(target, tokenactor) { - this.data.ppocket = true; - this.loots = this.LootItemList(target.actor); - let pickpocket = new PickPocket(this.loots, target, tokenactor); - } - - LootNPC(target, tokenactor) { - this.data.looting = true; - // Faz uma lista com possibilidade de perda do item. - let loots = this.LootItemList(target.items); - // Cria os itens no token do usuário - tokenactor.createEmbeddedEntity("OwnedItem", loots); - this.ResultChat("Looting", loots, target.name); - if (game.settings.get(SETTINGS.MODULE_NAME, "removeItem")) { - let items = this.LootItemList(target.items, true); - this.data.currentItems = items.map(i => i._id); - } - } - - LootItemList(actoritems, check = false) { - return actoritems.filter(item => { - if (item == null || item == undefined) return; - if (item.type == "class" || item.type == "spell" || item.type == "feat") return; - // weapon equipment consumable - if (item.type === "weapon") { - if (item.data.data.weaponType == "siege" || item.data.data.weaponType == "natural") return; - if (!check && (Math.floor(Math.random() * 100) + 1) <= game.settings.get(SETTINGS.MODULE_NAME, "perWeapons")) return; - } - if (item.type === "equipment") { - if (item.data.data.equipmentType == "vehicle" || item.data.data.equipmentType == "natural") return; - if (!check && (Math.floor(Math.random() * 100) + 1) <= game.settings.get(SETTINGS.MODULE_NAME, "perEquipment")) return; - } - if (item.type === "consumable") { - if (!check && (Math.floor(Math.random() * 100) + 1) <= game.settings.get(SETTINGS.MODULE_NAME, "perConsumable")) return; - } - return item; - }); - } - ResultChat(titleChat, items, targetName) { - let title = titleChat + '- ' + targetName; - let table_content = ``; - for (let item of items) { - table_content += `O alvo ainda está conciente e pode reagir, Você tem certeza que deseja roubar os alvos?
", + buttons: { + one: { + icon: '', + label: "Sim", + callback: () => this.PickPocket(target, actor) + }, + two: { + icon: '', + label: "Não", + callback: () => console.log("Cancel Pickpoket") + } + }, + default: "two", + //render: html => console.log("Register interactivity in the rendered dialog"), + //close: html => console.log("This always is logged no matter which option is chosen") + }); + d.render(true); + } + + PickPocket(target, tokenactor) { + this.data.ppocket = true; + this.loots = this.InventoryChancesLoot(target.actor); + let pickpocket = new PickPocket(this.loots, target, tokenactor); + } + + async LootNPC(target, tokenactor) { + this.data.looting = true; + this.data.currency = duplicate(tokenactor.data.data.currency); + // tipos de loot + if (game.settings.get(SETTINGS.MODULE_NAME, "lootSystem") == "mode1") { + this.loots = await this.InventoryChancesLoot(target.items); + for (var coin in this.lootCurrency) { + this.data.currency[coin] = this.data.currency[coin] + this.lootCurrency[coin]; + } + } else if (game.settings.get(SETTINGS.MODULE_NAME, "lootSystem") == "mode2") { + + } else if (game.settings.get(SETTINGS.MODULE_NAME, "lootSystem") == "mode3") { + + } + await tokenactor.createEmbeddedEntity("OwnedItem", this.loots); + await tokenactor.update({ "data.currency": this.data.currency }); + } + + async AttempItemRemove(target) { + if (game.settings.get(SETTINGS.MODULE_NAME, "removeItem")) { + let items = await this.FilterInventory(target.items); + this.data.currentItems = items.map(i => i._id); + } + } + + async FilterInventory(items) { + let filtro = await items.filter(item => { + if (item == null || item == undefined) return; + if (item.type == "class" || item.type == "spell" || item.type == "feat") return; + if (item.type === "weapon" && (item.data.data.weaponType == "siege" || item.data.data.weaponType == "natural")) return; + if (item.type === "equipment" && (item.data.data.equipmentType == "vehicle" || item.data.data.equipmentType == "natural")) return; + return item; + }); + return filtro; + + } + + async InventoryChancesLoot(actoritems, check = false) { + let tables=[]; + let ac = await actoritems.filter(item => { + if (item == null || item == undefined) return; + if (item.type == "class" || item.type == "spell" || item.type == "feat") return; + // weapon equipment consumable + if (item.type === "weapon") { + if (item.data.data.weaponType == "siege" || item.data.data.weaponType == "natural") return; + if (!check && (Math.floor(Math.random() * 100) + 1) <= game.settings.get(SETTINGS.MODULE_NAME, "perWeapons")) return; + } + if (item.type === "equipment") { + if (item.data.data.equipmentType == "vehicle" || item.data.data.equipmentType == "natural") return; + if (!check && (Math.floor(Math.random() * 100) + 1) <= game.settings.get(SETTINGS.MODULE_NAME, "perEquipment")) return; + } + if (item.type === "consumable") { + if (!check && (Math.floor(Math.random() * 100) + 1) <= game.settings.get(SETTINGS.MODULE_NAME, "perConsumable")) return; + } + if (item.type === "loot") { + if (this.ConvertItens2Coins(item)) return; + let matches = item.name.match(/Table:([\w\s\S]+)/gis); + if (matches) { + let t = matches[0].split('Table:'); + tables.push(t[1].trim()); + return; + } + } + return item; + }); + for (let tableroll of tables) { + let item = await this.ConvertItems2TableLoot(tableroll); + if (item) { + ac.push(...item) + } + } + //console.log("All Loot", ac, this.lootCurrency); + return ac; + } + + + async ConvertItems2TableLoot(tableroll) { + let nItems = []; + let table = game.tables.getName(tableroll); + if (this.betterTables && this.betterTables.active) { + //Bettertable + let re = await table.roll(); + let result = await re.results; + for (let r of result) { + let packs = game.packs.get(r.collection); + let entity = (packs) ? await packs.getEntity(r.resultId) : game.items.get(r.resultId); + if (this.ConvertItens2Coins(entity)) return; + nItems.push(entity); + } + } else { + //Vanilla + let re = await table.roll(); + let result = await re.results; + for (let r of result) { + let packs = game.packs.get(r.collection); + let entity = (packs) ? await packs.getEntity(r.resultId) : game.items.get(r.resultId); + if (this.ConvertItens2Coins(entity)) return; + nItems.push(entity); + } + } + // console.log("items", nItems); + return nItems; + } + + ConvertItens2Coins(item) { + let matches = item.name.match(/\([a-z]{1,2}\)$/gs); + if (matches) { + let coin = matches[0].substring(1, matches[0].length - 1); + if (!this.lootCurrency[`${coin}`]) this.lootCurrency[`${coin}`] = 0; + if (item.data.data.source.match(/[dkfhxo]{1}[0-9\s\+\-\*\/]+/gs)) { + let r = new Roll(item.data.data.source); + this.lootCurrency[`${coin}`] += r.evaluate().total; + } else { + this.lootCurrency[`${coin}`] += item.data.data.quantity; + } + Object.keys(this.lootCurrency).sort() + return true; + } + return false; + } + + ResultChat(titleChat, items, targetName, currency) { + let title = titleChat + '- ' + targetName; + let table_content = ``; + console.log(currency) + //console.log("result chat", items); + for (let item of items) { + table_content += `${coins}