Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielBoettner committed May 21, 2021
0 parents commit d7870f1
Show file tree
Hide file tree
Showing 18 changed files with 589 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
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
21 changes: 21 additions & 0 deletions LICENSE.md
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.
26 changes: 26 additions & 0 deletions README.md
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).
16 changes: 16 additions & 0 deletions hooks/onCreateToken.js
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);
});
}
Binary file added icons/Scroll0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/Scroll1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/Scroll2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/Scroll3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/Scroll4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/Scroll5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/Scroll6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/Scroll7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/Scroll8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/Scroll9.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
63 changes: 63 additions & 0 deletions lootpopulatornpc5e.js
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
});
});
22 changes: 22 additions & 0 deletions module.json
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"
}
188 changes: 188 additions & 0 deletions scripts/populateLoot.js
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}.`);
}
}
}
}
}
Loading

0 comments on commit d7870f1

Please sign in to comment.