02MORPG: Making an MORPG with BYOND

by Ter13
Learn to make a MORPG with BYOND with no existing knowledge
ID:2157180
 
If you have been following along in order, you'll now have a simple project where you can walk into monsters to kill them and they will attempt to run at you and stand there like idiots. This is actually a really good foundation for a MORPG. We need to make killing monsters worthwhile though first. Let's add lootable items that monsters can drop when they are killed.




3.1: Phat Loot

  As with all systems we work on, we really want to outline our goals before jumping in. Our goal at the moment is to define an inventory for players that can hold a set number of items. Monsters will generate items when killed that will appear in a special "loot" menu, and players will have a set amount of time to claim those generated loot items before they are destroyed.

item
parent_type = /obj //shorthand to ensure we don't have to type /obj/item everywhere. We can just type /item and still inherit from /obj
var
tmp //temporary (not saved) variables
loot_duration //stores the number of ticks this item stays lootable
expire_time = 0 //stores the time this item will expire
proc //these are commands
take() //called when the item should attempt to be taken

discard() //called when the item should attempt to be discarded

drop() //called when the item should attempt to be dropped

proc //these are hooks
Loot(mob/player/p,expiration=600) //called to put the item in a loot panel for a specific player

Taken(mob/player/p) //called when an item is successfully taken

Discarded(mob/player/p) //called when an item is successfully discarded

Dropped(mob/player/p) //called when an item is successfully dropped

New() //called when an item is created
if(istype(loc,/mob/player)) //if the item is created in a player
Taken(loc) //note that it has been taken


  The above structure is the basis of our entire looting system. At least on the item end of things. Of course, we also need these items to be able to interact with the player. Let's add some code to interact with items for the player too.

mob
player
var
item_slots = 20 //the max number of items that the player can have
tmp
list/loot //stores items that are pending looting
proc
LootTicker()
set waitfor = 0


  So let's get started implementing item functions. Let's start with the looting functions, as that's where the player will first start to interact with items.

proc
//this is a global helper function to convert tick values into human-readable values
tick2time(expiration)
if(expiration<0) //if expiration is less than 0 ticks
return "0s"
else if(expiration<600) //if expiration is less than 1 minute
return "[round(expiration/10)]s" //round down the expiration ticks into seconds
else if(expiration<36000) //if expiration is less than 1 hour
return "[round(expiration/600)]m" //round down the expiration ticks into minutes
else if(expiration<864000) //if expiration is less than 1 day
return "[round(expiration/36000)]h" //round down the expiration ticks into hours
else if(expiration<1#INF) //if expiration is less than infinite
return "[round(expiration/864000)]d" //round down the expiration ticks into days
else //expiration is infinite
return null //leave a blank timer

item
proc
//call to add this item to the player's loot list.
Loot(mob/player/p,expiration=600)
expire_time = expiration+world.time //set the time this item expires in the future

//add the item to the player's loot list
if(!p.loot)
p.loot = list(src) //create the loot list if it doesn't exist
else
p.loot += src //add the item to the loot list since it exists
//add a visual notifier to the object that shows how long until the item expires.
suffix = tick2time(expiration) //convert the expiration to a time string
verbs.Add(/item/proc/take,/item/proc/discard) //add some verbs to the item. This will be explained below

return 1

mob
player
proc
//maintains items in the loot list over time. The loot ticker removes expired items and updates their suffix values to show the player how long until that item expires accurately.
LootTicker()
set waitfor = 0 //make sure the function doesn't block processing while waiting
var/time, showing //set up some local variables

while(client) //loop as long as the client is logged in
time = world.time //store the current time to save processing doing lookup traversal
showing = client.statobj==src&&client.statpanel=="Loot (L)" //we can save a lot of processing by short-handing a conditional expression
for(var/item/i in loot) //iterate over every item in the loot list
if(time>=i.expire_time) //if the item has expired
loot -= i //remove it from the loot list
if(!loot.len) //destroy the loot list if it is no longer in use
loot = null
else if(showing) //if the item isn't expired and the loot menu is showing
i.suffix = tick2time(i.expire_time-time) //calculate the time until expiration in ticks and convert it to a time string
sleep(world.tick_lag) //sleep for a single tick


&emsp Now that we have our basic loot functionality working, we can start working on interacting with items. There was a section of the above code that mentioned verbs. We should probably delve into that a little bit before we move on to the next task.

  Verbs are very similar to procs. In fact, a proc can be a verb. When a verb is defined on an object, players can right click on that object to bring up a simple popup menu that allows the player to trigger the verb. It's a very easy, built-in method of interacting with objects in the world. Verbs also show up in menu panels, but it is an ugly approach that I personally do not recommend. We're going to be working with several procs that should be considered verbs in the next batch of code we're adding to items.

item
proc
take()
set src in world //set the verb visibility in relation to src
set category = null //set the verb category to hide from menu panels (only shows up in right-click menu)
if(!loc) //if the item isn't already in a container
var/mob/player/m = usr //casting the user of the verb to a player type
if(m.contents.len>=m.item_slots) //check if the user can pick up this item
return 0
Move(m,forced=1) //forcibly move the item into the user's contents
expire_time = 0 //clear the expire_time
Taken(m) //notify the item it has been successfully taken
return 1
return 0

discard()
set src in world
set category = null
if(!loc)
var/mob/player/m = usr //cast the user into the expected type
if(m.loot) //if the user has a loot list
m.loot -= src //subtract this item from that loot list
if(!m.loot.len) //destroy the list if no longer occupied
m.loot = null
Discarded(m) //tell this item it has been discarded
return 1
return 0

drop()
set src in usr //only valid when in user's contents
set category = null
if(loc==usr) //check to make sure the user can drop this item
if(alert("Are you sure you want to drop [src]?\nIt will be destroyed permanently.","Confirm","Yes","No")=="No") //use a popup alert window to ask the player for permission to destroy the item.
return //the player didn't want to destroy the item, so we stop the function.
Move(null,forced=1) //move to nowhere
Dropped(usr) //tell the item is has been dropped
return 1
return 0

proc
Taken(mob/player/p)
suffix = null
verbs.Remove(/item/proc/take,item/proc/discard)
verbs += /item/proc/drop

Discarded(mob/player/p)
verbs.Remove(/item/proc/take,/item/proc/discard)

Dropped(mob/player/p)
verbs -= /item/proc/drop


  Those procs are treated like verbs. The verbs list of an object keeps track of what commands are available on the object and the src settings help to ensure when verbs are shown based on the position of the object in the world relative to the player. What these three functions do, in addition to moving items in and out of the loot list and inventory, is make sure that the commands available to the object match the state that the object is in. You can't drop an item you don't have, and you can't take an item you already have. These verbs are being used to make sure that context is respected by items and their commands.

  Usr is a new local variable we've never seen before. usr refers to the last player to trigger a verb within a callchain. BYOND is a multiplayer environment and as such, it assumes that you may not always know exactly who is going to be doing what when. Verbs are behavior that live on an object, and in some cases, the src setting will infer that the person that used the verb isn't the object itself. src always refers to the object the proc belongs to. usr always refers to the player that performed the action that resulted in the current proc running. Usr is typecast as a root /mob. That's why we performed that "casting" business up above. Because we're assuming that the mob that performed an action is always a /mob/player, we have to tell the compiler that we're working with a /mob/player to access behavior and properties defined under /mob/player without running into compiler errors.

  Use of usr is in almost all cases, best avoided. If there is an instance where usr and src mean the same thing, you should always prefer src. Where usr and src are different, you must be aware of the usr/src differences before you use either. usr can also have no value in some cases. When a function is called not originating from a player action, usr will generally be empty. It's often said that usr is only valid in verbs, but that is not completely true. usr is valid in lots of places that aren't verbs, but generally the majority of procs should not use usr.

  Last, we need to make sure that the player can actually SEE incoming loot items and interact with their inventory. BYOND offers an inefficient, less-than-customizable, but very easy to use form of providing menus to a user called statpanels. Statpanels aren't pretty and they aren't very efficient, but they are very fast to write and don't require a good grasp of UI design to use. In the early phases of designing a game, it's usually best to use the lowest-effort means of getting input and output from and to a player, so statpanels are definitely the approach we're going to take despite their weaknesses. However, don't think that this is the only way to represent information to and get input from the player in BYOND. BYOND is a very flexible and highly customizable game engine. For now, we are using them despite their flaws in preparation for a better system when we have the skills to tackle it.






3.2: Statpanels

  We should talk about what statpanels do before we proceed. Statpanels are a series of tabs that show a vertical table of data. Every few ticks, the client calls the client's Stat() function, which in turn calls the Stat() function on the client's statobj reference. The object stored in statobj will then go about the process of building the statpanels. There are two major functions that we use in Stat(). statpanel(), and stat(). statpanel() tells the client to create a new tab and route all stat() calls to that tab or in some cases, check whether a tab is currently the one tab that is showing on the client. stat() sends information to the statpanel that last had statpanel() called. statpanel() can also send a list of information to a statpanel all at once to populate numerous elements much more quickly.

  The default statobj that the client picks is actually the default mob that the client is given on login. statobj can be changed at any time, allowing you to create entirely different output for the player without having to do a lot of work yourself. Since the default statobj for the client is the mob they are connected to, and our world's default mob type for connecting clients is /mob/player, we should override Stat() on /mob/player.

mob
player
Login() //called by default when the player is connected to by a client
LootTicker() //start the loot ticker on player login
..() //call the default action

Stat()
if(usr==src) //if client.statobj is equal to src. In other words, we're looking at our own stats
if(statpanel("Pack (I)")) //if the selected panel is this one
statpanel("Pack (I)",contents) //send all items in the player's contents to the statpanel as output
stat(null,null) //send a blank line
stat(null,"[contents.len] / [item_slots]") //show how many slots are taken up by items
if(loot&&loot.len&&statpanel("Loot (L)")) //only attempt to show the loot panel when there is loot and check if the loot panel is the active statpanel.
statpanel("Loot (L)",loot) //send all the loot items to the statpanel.


  When statpanel() is called with only the first argument, it returns 1 or 0 without generating any output to the panel. The return value is 1 if the player is looking at that panel right now, and 0 if the player is looking at another panel. When programming, we want to keep our code very tight and not do anything that isn't necessary. By checking if a panel is currently selected, we can reduce the amount of data that is sent to the player. There's no point in sending data to a player that they won't be able to see, so we should always avoid it to make our game faster.

  Calling statpanel() doesn't always create a statpanel. It will only create the statpanel the first time that it is called with a unique name as the first argument. Subsequent calls to statpanel() will forward all stat() calls to the specified panel, or if a second argument is specified, it will add a line to that statpanel just like a stat() call would.

Let's take a minute to look at all the code from the last two sections put together:

proc
tick2time(expiration)
if(expiration<0)
return "0s"
else if(expiration<600)
return "[round(expiration/10)]s"
else if(expiration<36000)
return "[round(expiration/600)]m"
else if(expiration<864000)
return "[round(expiration/36000)]h"
else if(expiration<1#INF)
return "[round(expiration/864000)]d"
else
return null

item
parent_type = /obj
var
tmp
expire_time = 0
loot_duration
proc
take()
set src in world
set category = null
if(!loc)
var/mob/player/m = usr
if(m.contents.len>=m.item_slots)
return 0
Move(usr,forced=1)
expire_time = 0
Taken(usr)
return 1
return 0

discard()
set src in world
set category = null
if(!loc)
var/mob/player/m = usr
m.loot -= src
Discarded(usr)
return 1
return 0

drop()
set src in usr
set category = null
if(loc==usr)
if(alert("Are you sure you want to drop [src]?\n It will be destroyed permanently.","Confirm","Yes","No")=="No")
return
Move(null,forced=1)
Dropped(usr)
return 1
return 0
proc
Loot(mob/player/p,expiration=600)
expire_time = expiration+world.time
p.loot += src
suffix = tick2time(expiration)
verbs.Add(/item/proc/take,/item/proc/discard)
return 1

Taken(mob/player/p)
suffix = null
verbs.Remove(/item/proc/take,/item/proc/discard)
verbs += /item/proc/drop

Discarded(mob/player/p)
verbs.Remove(/item/proc/take,/item/proc/discard)

Dropped(mob/player/p)
verbs -= /item/proc/drop

New()
if(istype(loc,/mob/player))
Taken(loc)

mob
player
var
item_slots = 20
tmp/list/loot = list()
proc
LootTicker()
set waitfor = 0
var/time, showing
while(client)
time = world.time
showing = client.statobj==src&&client.statpanel=="Loot (L)"
for(var/item/i in loot)
if(time>=i.expire_time)
loot -= i
else if(showing)
i.suffix = tick2time(i.expire_time-time)
sleep(world.tick_lag)

Login()
LootTicker()
..()

Stat()
if(usr==src)
if(statpanel("Pack (I)"))
statpanel("Pack (I)",contents)
stat(null,null)
stat(null,"[contents.len] / [item_slots]")
if(loot&&loot.len&&statpanel("Loot (L)"))
statpanel("Loot (L)",loot)





  We still can't really test any of this until monsters actually generate loot. Let's implement some temporary code that will help us test these features.

mob
player
Kill(mob/victim)
if(istype(victim,/mob/monster))
var/mob/monster/m = victim
var/list/loot = m.GenerateLoot(src) //generate a list of lootable items
if(loot&&loot.len) //if the monster created anything to be lootable
for(var/item/i in loot) //loop over all the items
i.Loot(src,i.loot_duration) //loot each item
monster
proc
GenerateLoot(mob/player/p)
return list(new/item()) //return a list containing a single item (This bit is temporary)


  Now we can compile our project and get testing! Right click on any items in the loot list to bring up the verb popup menu and select take or discard to take the item or discard the item. Keep killing enemies until your inventory is full! Watch as the items slowly tick down and eventually disappear from the loot list.
Looking forward to tutorial #4.