-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit d7870f1
Showing
18 changed files
with
589 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
|
||
images/icon.png | ||
module-releases_template.txt | ||
workspace.code-workspace | ||
git_tags.txt | ||
template/debug.log |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2021 Daniel Böttner | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
[![Github All Releases](https://img.shields.io/github/downloads/DanielBoettner/fvtt-loot-populator-npc-5e/total.svg)]() | ||
|
||
# Loot Populator NPC 5E | ||
|
||
This module allows you to enable automatic population of loot on NPCs in 5e. | ||
|
||
The module is heavily inspired by [LootSheetNPC5e](https://github.com/jopeek/fvtt-loot-sheet-npc-5e) and also is only really useful when using this in tandem. As LootSheetNPC5e adds the capability and permission handling for players to actually loot items from token/actors. | ||
|
||
### Features | ||
|
||
Allows you to have automated random loot on NPCs when dropping them on the scene. | ||
|
||
### Compatibility: | ||
- Tested with FVTT v0.7.9 and the DND5E system only. | ||
|
||
### Installation Instructions | ||
|
||
To install a module, follow these instructions: | ||
|
||
1. Start FVTT and browse to the Game Modules tab in the Configuration and Setup menu | ||
2. Select the Install Module button and enter the following URL: https://raw.githubusercontent.com/DanielBoettner/fvtt-loot-populator-npc-5e/master/module.json | ||
3. Click Install and wait for installation to complete | ||
|
||
### Feedback | ||
|
||
If you have any suggestions or feedback, please submit an issue on GitHub or contact me on Discord (JackPrince#0494). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import {populateLoot} from '../scripts/populateLoot.js'; | ||
|
||
export let initHooks = () => { | ||
Hooks.on('createToken', (scene, data, options, userId) => { | ||
|
||
if(! game.settings.get("lootpopulatornpc5e","autoPopulateTokens")) | ||
return; | ||
|
||
const actor = game.actors.get(data.actorId); | ||
|
||
if (!actor || (data.actorLink)) // Don't for linked token | ||
return data; | ||
|
||
populateLoot.generateLoot(scene, data); | ||
}); | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import {initHooks} from './hooks/onCreateToken.js'; | ||
|
||
Hooks.once('init', () => { | ||
initHooks(); | ||
}); | ||
|
||
Hooks.once('ready', () => { | ||
game.settings.register("lootpopulatornpc5e", "autoPopulateTokens", { | ||
name: "Auto populate tokens with loot", | ||
hint: "If an actor has a rolltable assigned to it, should the token be populated with the Loot?", | ||
scope: "world", | ||
config: true, | ||
default: false, | ||
type: Boolean | ||
}); | ||
|
||
let MyRolltables = Object.assign(...game.tables.entities.map(table => ({[table.name]: table.name}))); | ||
game.settings.register("lootpopulatornpc5e", "fallbackRolltable", { | ||
name: "fallbackRolltable", | ||
hint: "If no lootsheet rolltable is assigned to an actor, this will be used as a fallback table.", | ||
scope: "world", | ||
config: true, | ||
default: '', | ||
type: String, | ||
choices: MyRolltables | ||
}); | ||
|
||
game.settings.register("lootpopulatornpc5e", "fallbackShopQty", { | ||
name: "Shop quantity", | ||
hint: "If no lootsheet shop quantity is assigned to an actor, this will be used as a fallback shop quantity.", | ||
scope: "world", | ||
config: true, | ||
default: '1d2', | ||
type: String | ||
}); | ||
|
||
game.settings.register("lootpopulatornpc5e", "fallbackItemQty", { | ||
name: "Item quantity", | ||
hint: "If no lootsheet item quantity is assigned to an actor, this will be used as a fallback item quantity.", | ||
scope: "world", | ||
config: true, | ||
default: '1d2', | ||
type: String | ||
}); | ||
|
||
game.settings.register("lootpopulatornpc5e", "fallbackItemQtyLimit", { | ||
name: "Item quantity limit", | ||
hint: "If no lootsheet item quantity limit is assigned to an actor, this will be used as a fallback item quantity limit.", | ||
scope: "world", | ||
config: true, | ||
default: '1d2', | ||
type: String | ||
}); | ||
|
||
game.settings.register("lootpopulatornpc5e", "reduceUpdateVerbosity", { | ||
name: "Reduce Update Shop Verbosity", | ||
hint: "If enabled, no notifications will be created every time an item is added.", | ||
scope: "world", | ||
config: true, | ||
default: true, | ||
type: Boolean | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{ | ||
"name": "lootpopulatornpc5e", | ||
"title": "LootPopulator NPC 5e", | ||
"description": "A module to auto populate loot on NPCs", | ||
"version": "0.0.1", | ||
"minimumCoreVersion": "0.7.9", | ||
"compatibleCoreVersion": "0.7.9", | ||
"author": "Daniel Böttner", | ||
"systems": ["dnd5e"], | ||
"includes": [ | ||
"./hooks/**", | ||
"./scripts/**" | ||
], | ||
"esmodules": [ | ||
"/lootpopulatornpc5e.js" | ||
], | ||
"styles": [], | ||
"socket": false, | ||
"url": "https://github.com/DanielBoettner/fvtt-loot-populator-npc-5e", | ||
"manifest": "https://github.com/DanielBoettner/fvtt-loot-populator-npc-5e/master/module.json", | ||
"download": "https://github.com/DanielBoettner/fvtt-loot-populator-npc-5e/archive/master.zip" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
import Item5e from "../../../systems/dnd5e/module/item/entity.js"; | ||
|
||
export class populateLoot { | ||
constructor() { | ||
return this; | ||
} | ||
|
||
static async generateLoot(scene, data) { | ||
//instead of the main actor we want/need the actor of the token. | ||
const tokenId = data._id; | ||
const token = canvas.tokens.get(tokenId); | ||
const actor = token.actor; | ||
|
||
const ls5e_moduleNamespace = "lootsheetnpc5e"; | ||
const moduleNamespace = "lootpopulatornpc5e"; | ||
const rolltableName = actor.getFlag(ls5e_moduleNamespace, "rolltable") || game.settings.get(moduleNamespace,"fallbackRolltable"); | ||
const shopQtyFormula = actor.getFlag(ls5e_moduleNamespace, "shopQty") || game.settings.get(moduleNamespace,"fallbackShopQty") || "1"; | ||
const itemQtyFormula = actor.getFlag(ls5e_moduleNamespace, "itemQty") || game.settings.get(moduleNamespace,"fallbackItemQty") || "1"; | ||
const itemQtyLimit = actor.getFlag(ls5e_moduleNamespace, "itemQtyLimit") || game.settings.get(moduleNamespace,"fallbackItemQtyLimit") || "0"; | ||
const itemOnlyOnce = actor.getFlag(ls5e_moduleNamespace, "itemOnlyOnce") || false; | ||
const reducedVerbosity = game.settings.get(moduleNamespace, "reduceUpdateVerbosity") || true; | ||
let shopQtyRoll = new Roll(shopQtyFormula); | ||
|
||
shopQtyRoll.roll(); | ||
|
||
if (!rolltableName) { | ||
return; | ||
} | ||
|
||
let rolltable = game.tables.getName(rolltableName); | ||
|
||
if (!rolltable) { | ||
return ui.notifications.error(`No Rollable Table found with name "${rolltableName}".`); | ||
} | ||
|
||
if (itemOnlyOnce) { | ||
if (rolltable.results.length < shopQtyRoll.total) { | ||
return ui.notifications.error(`Cannot create a loot with ${shopQtyRoll.total} unqiue entries if the rolltable only contains ${rolltable.results.length} items`); | ||
} | ||
} | ||
|
||
if (!itemOnlyOnce) { | ||
for (let i = 0; i < shopQtyRoll.total; i++) { | ||
const rollResult = rolltable.roll(); | ||
let newItem = null; | ||
|
||
if (rollResult.results[0].collection === "Item") { | ||
newItem = game.items.get(rollResult.results[0].resultId); | ||
} else { | ||
const items = game.packs.get(rollResult.results[0].collection); | ||
newItem = await items.getEntity(rollResult.results[0].resultId); | ||
} | ||
|
||
if (!newItem || newItem === null) { | ||
return; | ||
} | ||
|
||
if (newItem.type === "spell") { | ||
newItem = await Item5e.createScrollFromSpell(newItem) | ||
} | ||
|
||
let itemQtyRoll = new Roll(itemQtyFormula); | ||
itemQtyRoll.roll(); | ||
|
||
console.log(`Loot Populator 5e| Adding ${itemQtyRoll.total} x ${newItem.name}`) | ||
|
||
let existingItem = actor.items.find(item => item.data.name == newItem.name); | ||
|
||
if (existingItem === null) { | ||
await actor.createEmbeddedEntity("OwnedItem", newItem); | ||
console.log(`Loot Populator 5e | ${newItem.name} does not exist.`); | ||
existingItem = await actor.items.find(item => item.data.name == newItem.name); | ||
|
||
if (itemQtyLimit > 0 && Number(itemQtyLimit) < Number(itemQtyRoll.total)) { | ||
await existingItem.update({ "data.quantity": itemQtyLimit }); | ||
if (!reducedVerbosity) ui.notifications.info(`Added new ${itemQtyLimit} x ${newItem.name}.`); | ||
} else { | ||
await existingItem.update({ "data.quantity": itemQtyRoll.total }); | ||
if (!reducedVerbosity) ui.notifications.info(`Added new ${itemQtyRoll.total} x ${newItem.name}.`); | ||
} | ||
} else { | ||
console.log(`Loot Populator 5e | Item ${newItem.name} exists.`); | ||
|
||
let newQty = Number(existingItem.data.data.quantity) + Number(itemQtyRoll.total); | ||
|
||
if (itemQtyLimit > 0 && Number(itemQtyLimit) === Number(existingItem.data.data.quantity)) { | ||
if (!reducedVerbosity) ui.notifications.info(`${newItem.name} already at maximum quantity (${itemQtyLimit}).`); | ||
} | ||
else if (itemQtyLimit > 0 && Number(itemQtyLimit) < Number(newQty)) { | ||
//console.log("Exceeds existing quantity, limiting"); | ||
await existingItem.update({ "data.quantity": itemQtyLimit }); | ||
|
||
if (!reducedVerbosity) ui.notifications.info(`Added additional quantity to ${newItem.name} to the specified maximum of ${itemQtyLimit}.`); | ||
} else { | ||
await existingItem.update({ "data.quantity": newQty }); | ||
if (!reducedVerbosity) ui.notifications.info(`Added additional ${itemQtyRoll.total} quantity to ${newItem.name}.`); | ||
} | ||
} | ||
} | ||
} else { | ||
// Get a list which contains indexes of all possible results | ||
|
||
const rolltableIndexes = [] | ||
|
||
// Add one entry for each weight an item has | ||
for (let index in [...Array(rolltable.results.length).keys()]) { | ||
let numberOfEntries = rolltable.data.results[index].weight | ||
for (let i = 0; i < numberOfEntries; i++) { | ||
rolltableIndexes.push(index); | ||
} | ||
} | ||
|
||
// Shuffle the list of indexes | ||
var currentIndex = rolltableIndexes.length, temporaryValue, randomIndex; | ||
|
||
// While there remain elements to shuffle... | ||
while (0 !== currentIndex) { | ||
|
||
// Pick a remaining element... | ||
randomIndex = Math.floor(Math.random() * currentIndex); | ||
currentIndex -= 1; | ||
|
||
// And swap it with the current element. | ||
temporaryValue = rolltableIndexes[currentIndex]; | ||
rolltableIndexes[currentIndex] = rolltableIndexes[randomIndex]; | ||
rolltableIndexes[randomIndex] = temporaryValue; | ||
} | ||
|
||
// console.log(`Rollables: ${rolltableIndexes}`) | ||
|
||
let indexesToUse = []; | ||
let numberOfAdditionalItems = 0; | ||
// Get the first N entries from our shuffled list. Those are the indexes of the items in the roll table we want to add | ||
// But because we added multiple entries per index to account for weighting, we need to increase our list length until we got enough unique items | ||
while (true) | ||
{ | ||
let usedEntries = rolltableIndexes.slice(0, shopQtyRoll.total + numberOfAdditionalItems); | ||
// console.log(`Distinct: ${usedEntries}`); | ||
let distinctEntris = [...new Set(usedEntries)]; | ||
|
||
if (distinctEntris.length < shopQtyRoll.total) { | ||
numberOfAdditionalItems++; | ||
// console.log(`numberOfAdditionalItems: ${numberOfAdditionalItems}`); | ||
continue; | ||
} | ||
|
||
indexesToUse = distinctEntris | ||
// console.log(`indexesToUse: ${indexesToUse}`) | ||
break; | ||
} | ||
|
||
for (const index of indexesToUse) | ||
{ | ||
let itemQtyRoll = new Roll(itemQtyFormula); | ||
itemQtyRoll.roll(); | ||
|
||
let newItem = null | ||
|
||
if (rolltable.results[index].collection === "Item") { | ||
newItem = game.items.get(rolltable.results[index].resultId); | ||
} | ||
else { | ||
//Try to find it in the compendium | ||
const items = game.packs.get(rolltable.results[index].collection); | ||
newItem = await items.getEntity(rolltable.results[index].resultId); | ||
} | ||
if (!newItem || newItem === null) { | ||
return ui.notifications.error(`No item found "${rolltable.results[index].resultId}".`); | ||
} | ||
|
||
if (newItem.type === "spell") { | ||
newItem = await Item5e.createScrollFromSpell(newItem) | ||
} | ||
|
||
await item.createEmbeddedEntity("OwnedItem", newItem); | ||
let existingItem = actor.items.find(item => item.data.name == newItem.name); | ||
|
||
if (itemQtyLimit > 0 && Number(itemQtyLimit) < Number(itemQtyRoll.total)) { | ||
await existingItem.update({ "data.quantity": itemQtyLimit }); | ||
if (!reducedVerbosity) ui.notifications.info(`Added new ${itemQtyLimit} x ${newItem.name}.`); | ||
} else { | ||
await existingItem.update({ "data.quantity": itemQtyRoll.total }); | ||
if (!reducedVerbosity) ui.notifications.info(`Added new ${itemQtyRoll.total} x ${newItem.name}.`); | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.