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,
x: 5, y: 5, items: [], coins: 0, map_id: 1, experience: 0
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
\{:ok, User.get(:items)}
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
In the next tutorial we’ll take a look at writing tests and we’ll polish our game a little bit.