Skip to content

Instantly share code, notes, and snippets.

@SplenectomY
Created January 1, 2015 09:17
Show Gist options
  • Save SplenectomY/1a8858b8a713a06db669 to your computer and use it in GitHub Desktop.
Save SplenectomY/1a8858b8a713a06db669 to your computer and use it in GitHub Desktop.
Inventory manager v. 0.0.2 demo
// VERSION INFO
var InventoryManager_Version = "0.0.2";
// These values must be set for the script to work properly.
// Once a player has joined your campaign,
// their ID will show up in the API Output Console
// when the script fires up.
player_list = [
player_1_id = "-JeTLkLt_UFt7Boc1DsQ", /* Coille Flitterleaf */
player_2_id = "-JeT529rL-wvu8yT8Q9N", /* Tanion of House Victaria */
player_3_id = "-JeXs7g5ADZiaVjYOdIg", /* Oldstuff McTuggins */
player_4_id = "-JeIO7XaKP76ZS22z0x3", /* Xanneiros Lothammer */
//player_5_id = "-JeIO7XaKP76ZS22z0x3", /* Sampleton Testificate */
]
total_players = player_list.length
// Follow the instruction at
// https://wiki.roll20.net/API:Objects#imgsrc_and_avatar_property_restrictions
// to get a URL from your library.
// This image is the background for your individual inventories.
// YOU MUST CHANGE THIS. You can't use mine. Sorry. :(
inv_img = "https://s3.amazonaws.com/files.d20.io/images/6927416/y5kI_BBCDJrxGCU8FttwEg/thumb.png?1420069394"
coin = {
wgt: 0.02, /* in lbs. Default is per 5e rules */
vol: 0.0002, /* in cubic feet. (5,000 per ft3) */
}
temperature = "F" /* Change to C if celsius is desired */
// Anywhere I'm able, I use the 5e rules for these.
// Modify if desired. These are in lbs and cubic feet.
storage_items = [
backpack = {
name: "Backpack",
max_wgt: 30,
max_vol: 1,
},
quiver = {
name: "Quiver",
max_wgt: "",
max_vol: 0.1,
},
bolt_case = {
name: "Bolt case",
max_wgt: "",
max_vol: 0.15,
},
pouch = {
name: "Pouch",
max_wgt: 6,
max_vol: .2,
},
bag_holding = {
name: "Bag of Holding",
max_wgt: 500,
max_vol: 64,
},
handy_haversack_side1 = {
name: "Side 1",
max_wgt: 20,
max_vol: 2,
},
handy_haversack_side2 = {
name: "Side 2",
max_wgt: 20,
max_vol: 2,
},
handy_haversack = {
name: "Heward's Handy Haversack",
max_wgt: 80,
max_vol: 8,
},
// I have 6 pockets, because thats the max
// I would give my characters on any item.
// Add more if you wish.
pocket_1 = {
name: "Pocket",
max_wgt: 3,
max_vol: .1,
},
pocket_2 = {
name: "Pocket",
max_wgt: 3,
max_vol: .1,
},
pocket_3 = {
name: "Pocket",
max_wgt: 3,
max_vol: .1,
},
pocket_4 = {
name: "Pocket",
max_wgt: 3,
max_vol: .1,
},
pocket_5 = {
name: "Pocket",
max_wgt: 3,
max_vol: .1,
},
pocket_6 = {
name: "Pocket",
max_wgt: 3,
max_vol: .1,
},
]
// This variable is defined for certain functionality,
// namely when an inventory is removed (i.e. a player
// takes his or her backpack off, and the cooresponding
// section dissapears with all its contents). This
// operation requires the script to run multiple times
extra_run = false /*DO NOT MODIFY THIS VARIABLE*/
queued_obj_creation = []
wait_timer = 0
// This is the meat and potatoes of the script
function inventory_manager() {
if(wait_timer > 0) {wait_timer--}
else {
// Collects relevant objects (graphics)
// and puts them in an array for use
objects = filterObjs(function(obj) {
if(obj.get("type") == "graphic") return true
else return false
});
// These are variables that must be defined
// every time the function runs
warmth = []
hosp = []
hosp_average = 0
equipped = []
for(index = total_players; index > 0; index--) {
warmth[index - 1] = 0
hosp[index - 1] = 0
equipped[index -1] = []
}
// This array stores the vast majority
// of important information, including
// player sheet bounds and properties,
// and sub inventory bounds and props.
inv = []
/***************************/
/***** STEP 1 out of 4 *****/
/***************************/
// First, recognize and identify all main player areas.
// These are the "player sheets" on the page
// in your campaign. All other "sub" inventory
// areas (backpacks, quivers) will be within
// this sheet. This sheet should be on the
// map layer for convenience, but it doesn't
// have to be. It should also be large enough
// to hold spaces for their sub inventories.
_.each(objects, function(obj) {
// Find main areas (worksheets). These are graphic
// objects with "main" in the GM notes.
// Corresponding player number is the bar3 value
gmnotes = obj.get("gmnotes")
if(gmnotes.indexOf("main") !== -1) {
inv_owner = obj.get("bar3_value")
if(!obj.get("bar1_value")) {obj.set("bar1_value", 0)}
// Now we set the bounding area for that player
// [left,top,right,bottom] based on the sheet's
// location and size, as well as other properties.
inv[inv.length] = {
type:'main',
id:obj.get("_id"),
owner:inv_owner,
weight:0,
volume:0,
inv_gmnotes:obj.get("gmnotes"),
left:Math.floor(obj.get("left") - Math.floor(obj.get("width") / 2)),
top:Math.floor(obj.get("top") - Math.floor(obj.get("height") / 2)),
right:Math.floor(obj.get("left") + Math.floor(obj.get("width") / 2)),
bottom:Math.floor(obj.get("top") + Math.floor(obj.get("height") / 2)),
}
}
});
/***************************/
/***** STEP 2 out of 4 *****/
/***************************/
// Now, recognize and identify all "sub" inventories,
// such as backpacks, quivers and pouches. These are given
// .owner properties that match the .owner property
// of the player sheet they are located inside of.
// These must have the words "inv" for sub inventories
// or "worn" for worn slots in the graphic's GM notes.
// If you want your players to have a "drop" area, which
// will put items out of their control until the GM gives them
// back, put "drop" in the GM notes.
_.each(objects, function(obj) {
// Check the GM notes
gmnotes = obj.get("gmnotes")
if( gmnotes.indexOf("inv") !== -1
|| gmnotes.indexOf("worn") !== -1
|| gmnotes.indexOf("drop") !== -1
) {
// Find out what player sheet it's in
for(index = 0; index < inv.length; index++) {
if( inv[index].type == 'main'
&& obj.get("left") > inv[index].left
&& obj.get("top") > inv[index].top
&& obj.get("left") < inv[index].right
&& obj.get("top") < inv[index].bottom
) {
// And now set its properties, borrowing
// the .owner property from the player sheet
// that it sits inside of.
inv_owner = inv[index].owner
inv[inv.length] = {
type:'sub',
id:obj.get("_id"),
owner:inv_owner,
weight:0,
volume:0,
layer:obj.get("layer"),
inv_gmnotes:obj.get("gmnotes"),
left:Math.floor(obj.get("left") - Math.floor(obj.get("width") / 2)),
top:Math.floor(obj.get("top") - Math.floor(obj.get("height") / 2)),
right:Math.floor(obj.get("left") + Math.floor(obj.get("width") / 2)),
bottom:Math.floor(obj.get("top") + Math.floor(obj.get("height") / 2)),
//bar1_rem:obj.get("bar1_max") - obj.get("bar1_value"), /* currently unused */
//bar2_rem:obj.get("bar2_max") - obj.get("bar2_value"), /* currently unused */
}
}
}
}
});
/***************************/
/***** STEP 3 out of 4 *****/
/***************************/
// Now, we process the "item" objects (swords, potions, etc.)
// adding their weights (bar_1) and volumes (bar_2) to the containers'
// corresponding properties (.weight, .volume).
// Weight is measured in lbs, volume in cubic feet.
// Items with special properties (unstowable, warmth, hosp, coin)
// are processed in this section as well, with special code.
_.each(objects, function(obj) {
gmnotes = obj.get("gmnotes")
// Find all "item" objects through process of elimination.
// Check GM notes for special qualities
if( gmnotes.indexOf("main") == -1
&& gmnotes.indexOf("inv") == -1
&& gmnotes.indexOf("worn") == -1
&& gmnotes.indexOf("drop") == -1
&& gmnotes.indexOf("status") == -1
&& gmnotes.indexOf("status2") == -1
) {
// These aren't particularly important
// but it forces items to show their names,
// which I use as flavor text (i.e. "Enchanted +1")
obj.set("showplayers_name",true)
obj.set("showname",true)
// More flavor stuff.
obj.set("playersedit_bar1",false)
obj.set("playersedit_bar2",false)
if(gmnotes.indexOf("liquid") !== -1) {
obj.set("playersedit_bar3",true)
obj.set("showplayers_bar3",true)
}
else {
obj.set("playersedit_bar3",false)
obj.set("showplayers_bar3",false)
}
obj.set("showplayers_bar1",false)
obj.set("showplayers_bar2",false)
// Begin processing each "item" against each and
// every inventory space on the page ...
// (I WISH THERE WAS A WAY I COULD MAKE THIS MORE EFFICIENT)
for(index = 0; index < inv.length; index++) {
// First, check to see where the item is at.
// Technically, this checks each and every
// inventory space to see if it holds
// the current item being processed.
if( obj.get("left") > inv[index].left
&& obj.get("top") > inv[index].top
&& obj.get("left") < inv[index].right
&& obj.get("top") < inv[index].bottom
) {
// Assign ownership of graphic items based on
// what player sheet they are located on.
if(inv[index].type == 'main') {
owner_id = inv[index].owner
for(index2 = total_players; index2 > 0; index2--) {
if(owner_id == index2) {obj.set("controlledby",player_list[index2 - 1])}
}
}
// This is complicated.
// Basically, if an "item" is on top of any "sub"
// inventory that has been hidden (due to another
// part of the script that removes inventories
// if you aren't wearing it, like taking off a
// backpack), this part of the script will
// hide that item. From a contextual point of
// view, if you take your backpack off and throw it
// on the ground, you lose access to all of those items
// until you put it back on.
// Items and inventories are "hidden" by being moved
// to the GM layer.
if(inv[index].layer == "gmlayer") {
obj.set("layer","gmlayer")
}
// Conversely, if the inventory comes back, this tells
// the items to also come back EXCEPT when they
// have been moved to the "map" layer.
// (which means they're on a "drop" inventory)
else if( inv[index].type == "sub"
&& inv[index].inv_gmnotes != "drop"
&& inv[index].layer == "objects"
|| inv[index].layer == "map"
) {
obj.set("layer","objects")
}
// As mentioned above, items moved to any "drop" inventory
// are moved to the map layer, so only the GM
// can give that item back to the player when he or she
// "picks it up" in-game.
if(inv[index].inv_gmnotes == "drop") {obj.set("layer","map")}
// If any item has "unstowable" in its GM notes,
// it cannot be placed in any "inv" type inventory,
// such as backpacks, pouches, pockets, quivers.
// This code kicks it out of the box by moving
// it directly underneath it.
// For example, I don't let my players put
// greatswords or greataxes in their backpacks.
gmnotes2 = inv[index].inv_gmnotes
if( gmnotes2.indexOf("inv") !== -1
&& gmnotes2.indexOf("worn") == -1
&& gmnotes.indexOf("unstowable") !== -1
) {
obj.set("top",inv[index].bottom + 35)
}
// Finally, the calculations.
else {
// This is a failsafe. Any items that don't have a default
// weight and volume are given values of 0. This keeps
// the script from causing "undefined" errors.
if(!obj.get("bar1_value")) {obj.set("bar1_value",0)}
if(!obj.get("bar2_value")) {obj.set("bar2_value",0)}
// This will exclude items that are hidden or dropped.
if(obj.get("layer") == "objects") {
// Graphics that have "coin" in thier GM notes
// are handled differently. It's too much
// to ask my players to figure out their
// coin weight and volume, so this
// does that automatically. They can
// adjust bar3 to specify the amount of coins
// the graphic represents.
// The GM notes for each should also have
// the type of coin in there. For example:
// "coin copper"
if(gmnotes.indexOf("coins") !== -1) {
// Using the variables set at the beginning,
// we set the weight and volume of each coin
// graphic. In 5e, all types of coins
// weigh the same.
obj.set("bar1_value",Math.round((parseFloat(obj.get("bar3_value") * coin.wgt) * 1000) / 1000))
obj.set("bar2_value",Math.round((parseFloat(obj.get("bar3_value") * coin.vol) * 1000) / 1000))
// Next, we adjust the names of each graphic.
// This makes life easy for at-a-glance
// calculations.
if(gmnotes.indexOf("copper") !== -1) {
obj.set("name","Cp: " + obj.get("bar3_value"))
}
if(gmnotes.indexOf("silver") !== -1) {
obj.set("name","Sp: " + obj.get("bar3_value"))
}
if(gmnotes.indexOf("gold") !== -1) {
obj.set("name","Gp: " + obj.get("bar3_value"))
}
if(gmnotes.indexOf("electrum") !== -1) {
obj.set("name","Ep: " + obj.get("bar3_value"))
}
if(gmnotes.indexOf("platinum") !== -1) {
obj.set("name","Pp: " + obj.get("bar3_value"))
}
}
// Adjust all inventory .weight and .volume properties
// based on contained items' bar1 and bar2 values.
inv[index].weight = inv[index].weight + parseFloat(obj.get("bar1_value"))
inv[index].volume = inv[index].volume + parseFloat(obj.get("bar2_value"))
}
}
// Any item with "warmth" followed by a number in its GM notes
// will add warmth to a player if it's put in a "worn" inventory.
// By default, this number x 10 = how much warmer (in Fahrenheit)
// this item will make you. Hot items, like travelers clothes or
// plate armor, should have a value of 4, and light items, like
// common clothes, should be "warmth 2". Hats or breastplates
// might give you "warmth 1". Use your judgment.
warmth_owner = inv[index].owner - 1
if( inv[index].inv_gmnotes == 'worn'
&& gmnotes.indexOf("warmth") !== -1
) {
warmth[warmth_owner] = warmth[warmth_owner] + parseInt(gmnotes.replace("warmth%20", ""))
}
// I use a system in my campaign called hospitality.
// Items such as tents, mess kits, bedrolls, blankets
// pillows, etc. will give you a benefit during rests.
// It's up to you to decide what the benefits do.
// To enable, just add the word "hosp" to any item object's
// GM notes. Any hospitality item will add +1 to the effect.
if(inv[index].type == 'main'
&& obj.get("layer") == "objects"
&& gmnotes.indexOf("hosp") !== -1
) {
hosp[warmth_owner] = hosp[warmth_owner] + 1
}
// This part of the script creates new inventory areas
// based on what a player equips for the first time.
// An important requirement of this is having certain
// words in such an item's GM notes. For example,
// by defeault any item with the word "backpack"
// in the GM notes will trigger this code.
// All applicable types of worn inventories
// are listed in the storage_items
// array at the beginning of the script.
// By default, new inventory areas are created
// 350 pixels to the right of the equipped item.
//
// In a more technical sense, what REALLY happens
// is this: This script will replace the keyword
// in the GM notes with a special, unique key
// ("storage_" + obj.get("_id))
// and likewise, the created inventory's key
// will match in the form of:
// ("inv_" + obj.get("_id)).
// This will come into play later in
// the script; if the "storage_" key isn't
// in a "worn" inventory, the "inv" inventory
// will become hidden, as will its contents.
if(gmnotes2.indexOf("worn") !== -1) {
obj_left = parseInt(obj.get("left"))
obj_top = parseInt(obj.get("top"))
for(i = 0; i < storage_items.length; i++) {
if(gmnotes.indexOf(storage_items[i].name) !== -1) {
queued_obj_creation[queued_obj_creation.length] = [
obj.get("_pageid"),
storage_items[i].name,
obj.get("left"),
obj.get("top"),
obj.get("controlledby"),
obj.get("_id"),
storage_items[i].max_wgt,
storage_items[i].max_vol,
]
obj.set("gmnotes", gmnotes.replace(storage_items[i].name,"storage_" + obj.get("_id")))
}
}
}
// Log any worn "storage_" items into an array.
// This array is sorted by player number
// and will be used later to hide unequipped
// inventories.
if( gmnotes2.indexOf("worn") !== -1
&& gmnotes.indexOf("storage_" + obj.get("_id")) !== -1
) {
equipped[warmth_owner][equipped[warmth_owner].length] = "inv_" + obj.get("_id")
}
}
}
}
});
// This code calculates Average Hospitality,
// which is a stat I use to give the party
// benefits from shared items (two person tent).
for(index2 = 0; index2 < hosp.length; index2++) {
hosp_average = hosp_average + hosp[index2]
}
hosp_average = Math.floor(hosp_average / total_players)
/***************************/
/***** STEP 4 out of 4 *****/
/***************************/
// The final section is the "print" code.
// This makes all inventories reflect their
// weights and volume capacities, as well as some
// other fancy bells and whistles, such as
// status icons, conglomerate currency calculation
// and more.
_.each(objects, function(obj) {
gmnotes = obj.get("gmnotes")
// But first, we have to make sure that any
// "unequipped" inventories are hidden.
for(index = 0; index < inv.length; index++) {
// This default value is checked agains
// each inventory space.
is_it_equipped = false
if(inv[index].id == obj.get("_id")) {
if(gmnotes.indexOf("inv") !== -1) {
item_owner = inv[index].owner - 1
for(i = 0; i < equipped[item_owner].length; i++){
if(gmnotes.indexOf(equipped[item_owner][i]) !== -1) {
is_it_equipped = true
}
}
if(is_it_equipped == true) {
obj.set("layer","objects")
if(extra_run == true) {}
else {extra_run = true}
}
else if (is_it_equipped == false) {
obj.set("layer","gmlayer")
if(extra_run == true) {}
else {extra_run = true}
}
}
// Now we apply values to bar1 and bar2 of each inventory.
// Values are rounded to the nearest thousandth.
obj.set("bar1_value", Math.round(inv[index].weight * 1000) / 1000)
obj.set("bar2_value", Math.round(inv[index].volume * 1000) / 1000)
// These are flavor. When an inventory fills up in either weight
// or volume (if either is applicable), a large red X appears,
// which informs a player that they need to unload a bit.
if(obj.get("bar1_value") > obj.get("bar1_max") && obj.get("bar1_max") > 0) {
obj.set({
statusmarkers: "dead",
});
}
else if(obj.get("bar2_value") > obj.get("bar2_max") && obj.get("bar2_max") > 0) {
obj.set({
statusmarkers: "dead",
});
}
else {
obj.set({
statusmarkers:"!"
});
}
}
// The following code is for two special objects on each sheet.
// One has "status" in the GM notes and the other has "status2".
// The former displays weight and "profile", which is a special
// value that calculates the amount of items (in cubic feet)
// that a character is carrying or wearing. I subtract this
// value from stealth rolls and other relevant checks, giving
// players incentive to not be pack rats or drop their gear
// during fights.
if(obj.get("gmnotes") == 'status'
&& inv[index].type == 'main'
&& obj.get("bar3_value") == inv[index].owner
) {
// These values don't need to be that precise.
// So they are only rounded to the nearest tenth.
obj.set("bar1_value", Math.round(inv[index].weight * 10) / 10)
obj.set("bar2_value", Math.round(inv[index].volume * 10) / 10)
// Then the "name" of the status graphic is adjusted.
obj.set("name",'Weight: ' + obj.get("bar1_value") + " | Profile: " + obj.get("bar2_value"))
}
if(obj.get("gmnotes") == 'status2'
&& inv[index].type == 'main'
&& obj.get("bar3_value") == inv[index].owner
) {
warmth_owner = inv[index].owner - 1
// Temperature values are changed per user preference
if(temperature == "F") {
obj.set("bar1_value", warmth[warmth_owner] * 10)
}
else if (temperature == "C") {
obj.set("bar1_value", Math.floor(((warmth[warmth_owner] * 10) - 32) * (5 / 9)))
}
obj.set("bar2_value", hosp[warmth_owner])
// "status2" graphic's "name" is adjusted.
obj.set("name",'Warmth: ' + obj.get("bar1_value") + "°" + temperature + " | Hosp: " + obj.get("bar2_value") + " (" + hosp_average + ")")
}
}
});
}
}
function create_inv(g_page_id, g_name, g_left, g_top, g_controlledby, g_id, g_wgt, g_vol) {
createObj("graphic",{
name: g_name,
imgsrc: inv_img,
pageid: g_page_id,
controlledby: g_controlledby,
left: g_left + 350,
top: g_top,
width: 70,
height: 70,
bar1_value: 0,
bar1_max: g_wgt,
bar2_value: 0,
bar2_max: g_vol,
layer: "objects",
gmnotes: "inv inv_" + g_id,
});
}
on("ready",function(){
inventory_manager()
// This part of the script collects player ids
// and prints them to the Output Console
players = filterObjs(function(obj) {
if(obj.get("type") == "player") return true
else return false
});
log('************* PLAYER IDs *************')
_.each(players, function(obj) {
log(obj.get("_displayname") + ": " + obj.get("_id"))
});
log('**************************************')
});
on("change:graphic:lastmove", function(obj) {
if (queued_obj_creation.length == 0) {
inventory_manager()
}
for(i = 0; i < queued_obj_creation.length; i++) {
create_inv(
queued_obj_creation[i][0],
queued_obj_creation[i][1],
queued_obj_creation[i][2],
queued_obj_creation[i][3],
queued_obj_creation[i][4],
queued_obj_creation[i][5],
queued_obj_creation[i][6],
queued_obj_creation[i][7]
)
wait_timer = 3
}
queued_obj_creation = []
// This also handles any extra runs
if(extra_run == true
&& queued_obj_creation.length == 0
) {
inventory_manager()
inventory_manager()
extra_run = false
}
});
on("change:graphic", function(obj) {
if (queued_obj_creation.length == 0) {
inventory_manager()
}
// This also handles any extra runs
if(extra_run == true
&& queued_obj_creation.length == 0
) {
inventory_manager()
inventory_manager()
extra_run = false
}
});
// NOTE ON ITEMS
// bar1 (green) = weight
// bar2 (blue) = volume
// bar3 (red) = hp for items, time remaining for torches and such
// or amount of coins in a particular graphic
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment