This is part of the Elixir Life Beyond Apocalypse Zombie Game creation tutorial.
Before we start coding we first need to think how do we handle and save our items?
Should we save them in a database, a flat file? Sqlite? In memory? Embedded in the code?
It depends. I've long opted and worked with SQL databases.
For our purpose we will save everything in a module variable in memory.
This will make everything easier since we don't need to go into details on how to work with SQL from Elixir right now.
Items the user has are stored in the items
Whenever we save the state of a user we can save it to a file.
Later we will refactor our code and save our data differently.
At the moment we will keep things short and easy. Items will not be stackable.
We won't make too many verifications for the sake of smplicity.
For example we won't verify if the user already has full health or energy.
These will be added later on.
We will implement the following features
Item searching - Putting items in inventory and substracting energy
Showing items in inventory
look - Getting information about a specific item
use item - We will create functions to "use" some items
drop item - Drop an item
Item searching
Item searching is an essential part of the gameplay.
Each item should have a specific category and some items will be easier to find than others.
Item searching will also be affected by the type of building we're in and if the building is in a good state.
If it's daytime or night. Night time afflects searching, however if the building is powered by a generator or we've got a flashlight
Some items will come in packs... for example bullets, etc
Also if we are not carrying too many things or if the weight of what we are carrying is not too much.
Today we will implement the basic system which we will later extend untill all features are completed.
So 20 different things is about enough.
First we need to update our user structure map to accomodate a list of items and so that we have the error:Life Beyond Apocalypse Zombie:experience atom set to 0
defmodule User do
defstruct name: "", health: 50, max_health: 50, energy: 100, max_energy: 100,
items.exs
defmodule GameItems do@items [ %{name: "Pain Killers", type: :health, value: 5, price: 5 },%{name: "First Aid Kit", \ttype: :health, value: 10, price: 10 }, %{name: "Energizer", type: :energy, value: 10, price: 10 },%{name: "Baseball Bat" , type: :attack ,value: 2 ,accuracy: 50 ,price: 30 ,infection_chance: 4 },%{name: "Axe" , type: :attack ,value: 3 ,accuracy: 50 ,price: 70 ,infection_chance: 5 },%{name: "Kantana" , type: :attack ,value: 4 ,accuracy: 60 ,price: 100 ,infection_chance: 7},%{name: "Gun", type: :attack, accuracy: 50, requires: "Bullet", value: 5 ,price: 170 ,infection_chance: 13},%{name: "Basic Clothes" , type: :defense ,value: 2 ,price: 30 },%{name: "Advanced Clothes" , type: :defense ,value: 3 ,price: 70 },%{name: "Kevlar Clothing" , type: :defense ,value: 4 ,price: 100 },%{name: "Riot Gear" , type: :defense ,value: 5 ,price: 170 },%{name: "Canned Food", type: :food, value: 5 },%{name: "Water Bottle", type: :water, value: 5 },%{name: "Survival Syringe", type: :revival, price: 50 },%{name: "Bullet" , type: :ammunition, price: 0.2 },%{name: "Battery" , type: :electronic, price: 5 }, \t%{name: "Flashlight" , type: :electronic, requires: "Battery", price: 30 }, \t%{name: "Mobile Phone" , type: :electronic, requires: "Battery", price: 50 },] def search() do User.set(:energy,User.get(:energy) - 1) search_item(GameUtilities.rand(1,10)) end defp search_item(chance) when chance > 5 do item = Enum.random(@items) User.set(:items, User.get(:items) ++ [ item]) User.set(:experience,User.get(:experience) - 1) {:ok, item ,"You've found a(n) #{item.name}! +1 XP, -1 energy"} end defp search_item(_) do {:false, "You've failed to find a usefull item. -1 energy"} end
Searching means we select a "random" item. A 5 in 10 chance to find something..
We then update the items list of the user, decrease his energy and increase his experience.
Notice how we return :ok when we found something together with the text, or we return :false otherwise.
This is because we will want to implement searching to work in commandline, via a webserver and probably via a websocket, the way the data is sent is handled by the specific functions that will send the data.
cli.ex
We will add another function to the LifeBeyondApocalypse.CLI module. Here depending on the message, we show a formatted text.
defp execute_command(["search"]) do case GameItems.search() do {:ok, _item, msg} -> IO.puts IO.ANSI.format([:green, msg]) {:false, msg} -> IO.puts IO.ANSI.format([:red, msg]) end read_command() end
Extra Utilities
If we look at the functions used, we need to first get and then set.. We could implement 2 extra functions to simplify our typing.
append and incr
user.ex User module
def new(user) do receive do {:get, key, pid} ->\tsend pid, {:value, Map.get(user,key)} {:get_struct,pid} ->\tsend pid, {:user, user} {:set,key,value} -> user = Map.put(user,key,value) {:set_struct, struct} -> user = struct {:incr,key,incr} -> user = Map.update(user,key,incr, &(incr+&1)) {:append,key,item} -> user = Map.update(user, key, [item], &(&1 ++ [item]))endnew(user) end#...#... def incr(key,incr) do send(:user_pid, {:incr, key, incr}) end def append(key,item) do send(:user_pid, {:append, key, item}) end
Showing items in inventory
Showing items will always be different depending if we show them in the console, on a webpage, via websockets etc.
Right now we'll just show our all items in a numbered fashion.
items.ex
Our GameItems module has only one function for the inventory
def inventory() do
end
cli.exs
defp execute_command(["inventory"]) do {:ok, items} = GameItems.inventory() IO.puts "Inventory consists of #{length items} items:" for {item,nr} <- Enum.with_index(items) do IO.puts "#{item.name} +#{item[:value]} #{item.type |> Atom.to_string |> String.upcase }" end IO.puts "Commands to be used with items: use, info, drop" read_command() end
Dropping an item
Dropping an item is slightly easier than using a certain item so we will implement it first.
The user will type either "drop name" or drop <inventory number>
The user doesn't have to type the whole name since we will try to match based on the first 3-5 letters.
He can also type a number based on the inventory number.
The dropped items will just disappear at the moment.
Dropping in normal circumstances should leave the item lying on the ground so others can pick it up. Within time, dropped items will vanish.
cli.ex
defp execute_command(["drop" | item_name]) do item_name = List.to_string item_name integer = Integer.parse(item_name) if(integer == :error, do: name_or_number = item_name, else: name_or_number = integer) case GameItems.find_item(name_or_number) do {:found, item } -> response = IO.ANSI.format([:italic, "Are you sure you want to drop ", :green, item.name, " ?", :magenta, " [Y]es/[N]o"]) |> read_text if String.match?(response,~r/y(es)|true|ok/iu) do GameItems.drop_item(item) IO.ANSI.format([:green, "You have dropped #{item.name} from your inventory"]) else IO.puts "You have decided NOT to drop the item." end {:notfound, msg} -> IO.puts IO.ANSI.format [:red,msg] end read_command() end
As you can see we transform the item_name to a string from the list, then we parse to see if it's an integer.
The integer is actually the location in the items list in the inventory.
We search for the item, if it's not found then tell the player this.
If we found it then ask for a confirmation that the user types y, yes, true or ok which are verified via regexp.
If he agrees,then drop the item.
In the Elixir Phoenix:Project Ideas:Life Beyond Apocalypse Zombie:GameItems module we will have to implement multiple functions. Some will be used whenever we search for an item in the inventory and will alsobe used when using items with the use command.
items.ex
def find_item(item_name) when is_binary(item_name) do item = Enum.find(User.get(:items), &String.match?(&1.name,~r/#{item_name}/iu)) if item == nil do {:notfound, ~s/You don't seem to have a(n) "#{item_name}" in your inventory/ } else {:found, item} end end def find_item(inventory_location) when is_integer(inventory_location) do {:notfound, "Inventory search by number is not implemented yet"} end def drop_item(item) do User.set(:items, User.get(:items) -- [ item]) {:ok, item} end
See how we implement pattern matching and if it's an integer or if it's a binary we use different functions.
This makes our code easier to implement than having tens of IF statements. Again we return tuples with the necessary information.
The find_item by ID is not yet implemented and is left as an exercise to the reader.
Using items
Using items in the game is actually a very big and complex subject.
In this tutorial we will only scratch the surface of the basics and we will gradually implement the rest.
Some items are one use only, which means that after using them they get dropped. Since we've already solved dropping this will become easier.
Since health and energy work in a similar way we will be creating some functions to handle both cases.
cli.ex
Again as with droppig items, we allow the user to use numbers or text. Searching for the item in his inventory.
If it's found we can use it and return the information the GameItems.use_item returns to us.
defp execute_command(["use" | item_name]) do item_name = List.to_string item_name integer = Integer.parse(item_name) if(integer == :error, do: name_or_number = item_name, else: name_or_number = integer) case GameItems.find_item(name_or_number) do {:found, item } -> case GameItems.use_item(item) do {:ok, msg} -> IO.ANSI.format([:green, msg]) |> IO.puts {:error, msg} -> IO.ANSI.format([:red, msg]) |> IO.puts end {:notfound, msg} -> IO.puts IO.ANSI.format [:red,msg] end read_command() end
items.ex
the use_item function performs multiple things and we will implement a function for each item type.
We can also implement them by item name or anyother item quality.
For the moment we will implement only for :energy and :health types.
We try to split up the text generation from the actual function logic since maybe in the future we will save our texts in a database or even in a flat file.
This will allow us to correct mistakes easily without having to recompile the game and go through hot code reloading.
We also have some helper functions to cap the maximum value.
If we notice we have some if's. We could further create sub functions to handle them but for now it;s ok
def use_item(item) do use_item(item.type, item) end def use_item(item_type, item) when item_type in [:energy,:health] do user = User.get_struct() if(item_type == :health, do: maximum = :max_health ,else: maximum = :max_energy ) {atom, value} = cap_at_maximum(Map.get(user,maximum), Map.get(user,item_type), item.value) if atom in [:maximum,:ok] do User.set(item_type,value) User.set(:energy,User.get(:energy) - 1) drop_item(item) end {:ok, use_item_maximum_text(atom) |> EEx.eval_string([item: item, user: user, maximum: maximum])} end def use_item(_item_type, _item) do {:error, "This item cannot be used at the moment"} end #Tricky bit ~S means NO interpolation defp use_item_maximum_text(atom) do map = %{ok: ~S"You have used <%= item.name %>. Your <%= item.type %> has been increased by <%= item.value %>", maximum: ~S"You have used <%= item.name %>. Your <%= item.type %> has hit the maximum of <%=Map.get(user,maximum) %>", already_maximum: ~S"Your <%= item.type %> is already at the maximum. <%= item.name %> has not been used"} Map.get(map, atom) end defp cap_at_maximum(maximum,current, _increase) when maximum == current do {:already_maximum, maximum} end defp cap_at_maximum(maximum,current, increase) when maximum <= current + increase do {:maximum, maximum} end defp cap_at_maximum(_maximum,current, increase) do {:ok, current+increase} end
The look command
The look command is again a little more complex, it allows us to look at users, items, places
Looking at an item shows certain information about it.
look <item , character, direction, keyword>
examine is an alias for look
Putting it all together
What functions should we implement in the future regarding items?
Barter, sell, buy items. Store your items somewhere?
Dropping items and picking up items is also interesting
keep <item> - mark an item to be kept (you won't be able to drop it untill you unkeep it)
unkeep <item> - unkeep an item (can be dropped)
drop all - Drops everything that doesn't have the keep flag
wear <item> - You can wear that certain item (armour, clothes) - Removing from inventory and putting to another list
Conclusions
We've implemented a whole lot of code, I think it's the most we've done so far.
Items are an important aspect of our game since they will be used for all other
Download the full sourcecode here for 0x04
In the next tutorial we'll take a look at writing tests and we'll polish our game a little bit.