diff --git a/FAQ.md b/FAQ.md new file mode 100644 index 0000000..66f1f01 --- /dev/null +++ b/FAQ.md @@ -0,0 +1,81 @@ +### Q: What is the difference between the ZomboTropolis remake and Urbandead/Quarantine2019? + +*(Note - I will here forth refer to ZomboTropolis as ZT, Urbandead as UD, and Quarantine2019 as Q)* + +Multiple major and distinct differences. + +**Platform:** ZT is going to be an app for mobile. Browser based games are dying, any remake needs to address this. The app market has a huge playerbase that keeps growing, and for games such as this, the complexity of getting a decent user interface for mobile is pretty doable. Push notifications also present a major advantage over browsers when it comes to in game events. + +**Graphics:** ZT is going to be using sprites for the characters, locations, and skills from free art assets. (Space Station 13, game-icons.net, etc.) This is a __MUST HAVE__ for an app on the app store. Without graphics there would be no playerbase! (although the hardcore vets will still play regardless) + +**Gameplay for UD:** +UD was a simple game, with basic math calculations and combat. Survivors had items, safehouses, and equipment. Zombies had abilities that were rather... plain. The skills for both sides complemented the above rather well, but none of it was groundbreaking. Neither side in the game could truly win. When one side started to win, the code in the game would nerf the other side. What gave this game so much life was the *community*. They were so active and fun to play with and it gave the game a rich and immersive history. The map for the game was huge! There were endless suburbs and buildings, not to mention distinct landmarks such as malls, churches, and mansions that were fun to discover and explore. One problem with a map this size was population. If population for the game dipped to low (and it did) then large portions of the map became ghost towns. Kevan (the developer) did test out new maps with permadeath enabled. This briefly revitalized the playerbase, although each of these maps are now over with. + +**Gameplay for Q:** +Q was a remake of UD with the intention of adding more complexity, features, and a end goal for both sides. With Q, you could actually win the game for your side! By playing through a round based system, zombies could either eliminate all humans, or humans could research the cure to win the game. Maps for Q were much smaller and tailored to the size of the population. One notable thing that Q did was to split skills into classes. Certain zombies were given abilities that others did not have, and vice-versa for humans. While this was a great idea, I believe it was executed poorly. Some skills were worthless, others were unbalanced. Some classes only had one utility they were good for and nothing else. (which made gameplay dull and repetitive for that class) Other features and systems seemed to be designed without proper planning. Staircases and multi-level buildings served no purpose, infection was unrealistic and unpractical, and resource points for buildings was just plain weird. It had it's flaws but I respected what the developers of Q were trying to do. UD had been neglected with updates, and Q was trying to branch off into a much better version. And to be fair, Q did come up with some really great stuff! Power plant buildings that powered entire suburbs, computers could network with ISP buildings, items had durability, etc. etc. Q only had a fraction of the population that UD had, so their rounds and maps were rather quick and small. Still lots of fun to play back in the day though! + +**Gameplay for ZT:** +ZT seeks to take both of these games a step further and add roguelike features and complexity to the code. All skills, abilities, and items in the game use dice rolls and dice modifiers to function. This has been implemented in a (mostly) balanced fashion. + +Just like UD, there is a persistent world, but with rubber banding enabled that stretches the map based on population size. Respawns for the game are always enabled, but a player will lose all their items, abilities, and skills upon permadeath. Permadeath is triggered for humans upon death, and zombies upon starvation. The goal is to follow a [prey/predator model](http://www.tiem.utk.edu/~gross/bioed/bealsmodules/pred-prey.gph1.gif) of graphing. + +Leveling is based on time. Players are no longer forced to heal, fight, or repair things to gain experience. The longer a player's character survives in the game, the more experience they earn to unlock skills. The skill system is setup to exponentially increase the skill cost after prior skills have been purchased. With enough time, a player can even unlock a special class with it's own special abilities to enhance a certain playstyle. + +Items and equipment in the game possess a condition value that will enhance or degrade its functionality. Each class can see relevant condition data based on the type of object. (military class can see weapon conditions, engineering class can see equipment and barricade conditions, etc.) Subsequent uses of an item or equipment eventually decreases the condition, yielding lower dice rolls or performance results. For instance, a generator that is "pristine" condition will use fuel more efficiently than the "ruined" condition. Additionally, searching in buildings when they are ruined yields items of lower quality. (including lowered search rates) + +A lot more features have been added to ZT than in Q and UD. A quick list as follows, + ++ Hunter zombie function as scouts. They can hide in unlit buildings, track players scent, free-run from ruined buildings into unruined buildings, move faster than other zombies (movement cost 1 AP), blood vision (x-ray) to see wounded characters from outside. ++ Brute zombies function as tanks. They can maim humans (reduce their hp permanently), generate organic armor from corpses, drag prey out into the streets, and destroy barricades/equipment/armor more effectively ++ Hive zombies function as support. They can ruin buildings, corrode human inventory with acid, deliver infection with bites, and communicate with all zombies via the hivemind. + ++ Engineers are builders. They can repair ruins, barricade to higher levels, and install equipment faster. ++ Military are fighters. They can master any weapon, maim zombies, and give a bonus to safehouse defenses. ++ Researchers are healers. They can heal efficiently, scan zombies, use terminals (gives info about surroundings), and create antidotes. + +While humans generally do not have restrictions to their actions, certain actions will greatly benefit from skills from one class. + +### Q: Roguelike?! With permadeath, dice and stuff? + +Yes. If you die (as a human) or starve (as a zombie) then it's game over. Fortunately, respawning is easy and you won't be completely useless with a new character. With ZT, your goal is to see how long you can survive, and if I have developed the game correctly, this should be hard for both sides. + +Regarding dice, I felt like both UD and Q suffered from using what I call 'basic' math. Weapons and items always did `x` amount of effect. That wasn't realistic and pretty boring for gameplay. So by adding dice it would allow ZT to be more dynamic! Weapons in better condition do more damage or have better accuracy. Skills either boost accuracy or grant rerolls to weapon attacks that successfully land. This is a powerful system that works well and allows for interesting possibilities. + +### Q: What about Player Killing? (PK'ing) + +Player killing is going to be disabled for both sides. So a human cannot kill other humans, and vice-versa for zombies. It would be too game breaking to have this feature enabled with permadeath functioning. + +Additionally: + +* It is not possible for humans to attack or destroy equipment in buildings. +* Humans can enter buildings regardless of barricade levels (thus no overcade griefing) + +To enable these things would be the trivial task of removing a few lines of code. This may get added as a feature to a new map (such as Monroeville), or in game event (a dark fog), or a map subsection (sewers). Don't expect it to be a normal occurrence in game though. + +### Q: Will this be free? What is the monetization plan? + +Free for freedom! Yes, it will be free. The monetization plan I have in mind is to have an occasional ad inserted into the game. (probably after AP is used) Another source of funding is to have cosmetic items in game that can be unlocked via purchasing tokens or surviving for `x` amount of days. + +Money will not give a person an in game advantage. This will keep the game true to it's predecessors! :) + +### Q: When will the game be playable/finished? + +I'm aiming for development to be finished around Spring 2018. The app should launch shortly after that time frame. + +### Q: How long has this been in development? + +I have been developing ZT for a few years off and on in my free time. This last year I have made tremendous progress, and the game is now close to being finished. I have worked on other smaller projects, but this game has been my most ambitious yet. + +### Q: What about modding? + +I will be looking at modding ZT to other themes since my API and source code is pretty robust. It shouldn't be that hard to switch it to something like say, Ninjas vs Samurais, heh. + +### Q: How can I help? + +I need help with the following: + ++ Playtesters in a few months ++ Setting up a ZT game wiki ++ People to bounce ideas off + +I am **not** looking for other coders or artists to contribute at this time. After launch this may change. diff --git a/README.md b/README.md index dc8b0c9..b17f2ed 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ ZomboTropolis ============= +Join the [Discord Server](https://discord.gg/cnHUgMt) + An **unfinished** zombie survival MMO using Corona and Lua. Play as either a zombie or a human in a fight for survival. The game is set to replicate a [prey-predator model](http://www.tiem.utk.edu/~gross/bioed/bealsmodules/pred-prey.gph1.gif) of population. Human Survivor diff --git a/code/item/item.lua b/code/item/item.lua index 1efaeaa..cc55f08 100644 --- a/code/item/item.lua +++ b/code/item/item.lua @@ -37,7 +37,8 @@ function Item:isReloadable() return self.reload or false end function Item:isSingleUse() return self.DURABILITY == 0 end -function Item:failDurabilityCheck(player) +function Item:failDurabilityCheck(player, degrade_multiplier) + local degrade_multiplier = multiplier or 1 local durability -- need to add a Item.DURABILITY_SKILL for items that are not weapons and check them here with weapons @@ -47,7 +48,7 @@ function Item:failDurabilityCheck(player) durability = player.skills:check(self.weapon.MASTER_SKILL) and math.floor(self.DURABILITY*1.2 + 0.5) or durability end end - return dice.roll(durability or self.DURABILITY) <= 1 + return dice.roll(durability or self.DURABILITY) <= degrade_multiplier end function Item:updateCondition(num) diff --git a/code/item/items.lua b/code/item/items.lua index beedb17..d22fa23 100644 --- a/code/item/items.lua +++ b/code/item/items.lua @@ -1,19 +1,24 @@ local Crowbar, Bat, Sledge, Knife, Katanna = unpack(require('code.item.items.melee_weaponry')) local Pistol, Magnum, Shotgun, Rifle, Flare, Molotov = unpack(require('code.item.items.ranged_weaponry')) local FAK, Bandage, Syringe, Vaccine, Antidote = unpack(require('code.item.items.medical')) -local Generator, Transmitter, Terminal, Fuel, Barricade, Toolbox = unpack(require('code.item.items.equipment')) +local Generator, Transmitter, Terminal = unpack(require('code.item.items.machinery')) +local Fuel, Barricade, Toolbox = unpack(require('code.item.items.tools')) local Radio, GPS, Flashlight, Sampler = unpack(require('code.item.items.gadget')) local Book, Bottle, Newspaper = unpack(require('code.item.items.junk')) local Magazine, Shell, Clip, Quiver = unpack(require('code.item.items.ammo')) local Leather, Firesuit = unpack(require('code.item.items.armor')) local Items = { - -- WEAPONRY - Crowbar, Bat, Sledge, Knife, Katanna, Pistol, Magnum, Shotgun, Rifle, Flare, Molotov, + -- MELEE_WEAPONRY + Crowbar, Bat, Sledge, Knife, Katanna, + -- RANGED_WEAPONRY + Pistol, Magnum, Shotgun, Rifle, Flare, Molotov, -- MEDICAL FAK, Bandage, Syringe, Vaccine, Antidote, - -- EQUIPMENT - Generator, Transmitter, Terminal, Fuel, Barricade, Toolbox, + -- MACHINERY + Generator, Transmitter, Terminal, + -- TOOLS + Fuel, Barricade, Toolbox, -- GADGET Radio, GPS, Flashlight, Sampler, --'cellphone', 'sampler' -- JUNK diff --git a/code/item/items/armor.lua b/code/item/items/armor.lua index f17c5f1..1c88570 100644 --- a/code/item/items/armor.lua +++ b/code/item/items/armor.lua @@ -1,56 +1,62 @@ local class = require('code.libs.middleclass') local Item = require('code.item.item') +local IsArmor = require('code.item.mixin.is_armor') local broadcastEvent = require('code.server.event') string.replace = require('code.libs.replace') -local Leather = class('Leather', Item) +------------------------------------------------------------------- -Leather.FULL_NAME = 'leather jacket' -Leather.DURABILITY = 0 -Leather.CATEGORY = 'military' -Leather.ap = {cost = 1} +local Armor = class('Armor', Item):include(IsArmor) -function Leather:activate(player) - player.armor:equip('leather', self.condition) +Armor.ap = {cost = 1} -- default AP cost for armor + +function Armor:activate(player) + if player.equipment:isActive('armor') then -- remove old armor and put into inventory + local old_armor = player.equipment.armor + player.inventory:insert(old_armor) + end + player.equipment:add('armor', self) -------------------------------------------- ----------- M E S S A G E -------------- -------------------------------------------- - local msg = 'You equip a leather jacket.' - + local msg = 'You equip a {armor}.' + msg = msg:replace(self) -- This should work? Needs to be tested + -------------------------------------------- --------- B R O A D C A S T ------------ -------------------------------------------- - local event = {'leather', player} - player.log:insert(msg, event) + local event = {'armor', player} + player.log:insert(msg, event) end ------------------------------------------------------------------- -local Firesuit = class('Firesuit', Item) +local Leather = class('Leather', Armor) + +Leather.FULL_NAME = 'leather jacket' +Leather.DURABILITY = 32 +Leather.CATEGORY = 'military' + +Leather.armor = {} +Leather.armor.resistance = {blunt=1} + +------------------------------------------------------------------- + +local Firesuit = class('Firesuit', Armor) Firesuit.FULL_NAME = 'firesuit' -Firesuit.DURABILITY = 0 +Firesuit.DURABILITY = 4 Firesuit.CATEGORY = 'military' -Firesuit.ap = {cost = 1} -function Firesuit:activate(player) - player.armor:equip('firesuit', self.condition) - - -------------------------------------------- - ----------- M E S S A G E -------------- - -------------------------------------------- - - local msg = 'You equip a firesuit.' - - -------------------------------------------- - --------- B R O A D C A S T ------------ - -------------------------------------------- - - local event = {'firesuit', player} - player.log:insert(msg, event) -end +Firesuit.armor = {} +Firesuit.armor.resistance = { + {acid=1}, + {acid=2}, + {acid=3}, + {acid=4}, +} return {Leather, Firesuit} \ No newline at end of file diff --git a/code/item/items/equipment.lua b/code/item/items/equipment.lua deleted file mode 100644 index 66906bf..0000000 --- a/code/item/items/equipment.lua +++ /dev/null @@ -1,249 +0,0 @@ -local class = require('code.libs.middleclass') -local Item = require('code.item.item') -local broadcastEvent = require('code.server.event') -string.replace = require('code.libs.replace') -local dice = require('code.libs.dice') - -local Generator = class('Generator', Item) - -Generator.FULL_NAME = 'generator' -Generator.WEIGHT = 25 -Generator.DURABILITY = 0 -Generator.CATEGORY = 'engineering' -Generator.ap = {cost = 10, modifier = {tech = -2, power_tech = -4}} - -function Generator:client_criteria(player) - local p_tile = player:getTile() - assert(player:isStaged('inside'), 'Must be inside building to install generator') - assert(not p_tile.generator:isPresent(), 'There is no room for a second generator') -end - -Generator.server_criteria = Generator.client_criteria - -function Generator:activate(player) - local building_tile = player:getTile() - building_tile:insert('generator', self.condition) - - -------------------------------------------- - ----------- M E S S A G E -------------- - -------------------------------------------- - - local self_msg = 'You install a generator.' - local msg = '{player} installs a generator.' - msg = msg:replace(player) - - -------------------------------------------- - --------- B R O A D C A S T ------------ - -------------------------------------------- - - local event = {'generator', player} - player:broadcastEvent(msg, self_msg, event) -end - -------------------------------------------------------------------- - -local Transmitter = class('Transmitter', Item) - -Transmitter.FULL_NAME = 'transmitter' -Transmitter.WEIGHT = 25 -Transmitter.DURABILITY = 0 -Transmitter.CATEGORY = 'engineering' -Transmitter.ap = {cost = 10, modifier = {tech = -2, radio_tech = -4}} - -function Transmitter:client_criteria(player) - local p_tile = player:getTile() - assert(player:isStaged('inside'), 'Must be inside building to install transmitter') - assert(not p_tile.transmitter:isPresent(), 'There is no room for a second transmitter') -end - -Transmitter.server_criteria = Transmitter.client_criteria - -function Transmitter:activate(player) - local building_tile = player:getTile() - building_tile:insert('transmitter', self.condition) - - -------------------------------------------- - ----------- M E S S A G E -------------- - -------------------------------------------- - - local self_msg = 'You install a transmitter.' - local msg = '{player} installs a transmitter.' - msg = msg:replace(player) - - -------------------------------------------- - --------- B R O A D C A S T ------------ - -------------------------------------------- - - local event = {'transmitter', player} - player:broadcastEvent(msg, self_msg, event) -end - -------------------------------------------------------------------- - -local Terminal = class('Terminal', Item) - -Terminal.FULL_NAME = 'terminal' -Terminal.WEIGHT = 25 -Terminal.DURABILITY = 0 -Terminal.CATEGORY = 'engineering' -Terminal.ap = {cost = 10, modifier = {tech = -2, computer_tech = -4}} - -function Terminal:client_criteria(player) - local p_tile = player:getTile() - assert(player:isStaged('inside'), 'Must be inside building to install terminal') - assert(not p_tile.terminal:isPresent(), 'There is no room for a second terminal') -end - -Terminal.server_criteria = Terminal.client_criteria - -function Terminal:activate(player) - local building_tile = player:getTile() - building_tile:insert('terminal', self.condition) - - -------------------------------------------- - ----------- M E S S A G E -------------- - -------------------------------------------- - - local self_msg = 'You install a terminal.' - local msg = '{player} installs a terminal.' - msg = msg:replace(player) - - -------------------------------------------- - --------- B R O A D C A S T ------------ - -------------------------------------------- - - local event = {'terminal', player} - player:broadcastEvent(msg, self_msg, event) -end - -------------------------------------------------------------------- - -local Fuel = class('Fuel', Item) - -Fuel.FULL_NAME = 'fuel tank' -Fuel.WEIGHT = 10 -Fuel.DURABILITY = 0 -Fuel.CATEGORY = 'engineering' -Fuel.ap = {cost = 1} - -function Fuel:client_criteria(player) - local p_tile = player:getTile() - assert(player:isStaged('inside'), 'Must be inside building to refuel') - assert(p_tile.generator:isPresent(), 'Missing nearby generator to refuel') -end - -Fuel.server_criteria = Fuel.client_criteria - -function Fuel:activate(player) - local building_tile = player:getTile() - building_tile.generator:refuel() - - -------------------------------------------- - ----------- M E S S A G E -------------- - -------------------------------------------- - - local self_msg = 'You refuel the generator.' - local msg = '{player} refuels the generator.' - msg = msg:replace(player) - - -------------------------------------------- - --------- B R O A D C A S T ------------ - -------------------------------------------- - - local event = {'fuel', player} - player:broadcastEvent(msg, self_msg, event) -end - -------------------------------------------------------------------- - -local Barricade = class('Barricade', Item) - -Barricade.FULL_NAME = 'barricade' -Barricade.WEIGHT = 7 -Barricade.DURABILITY = 0 -Barricade.CATEGORY = 'engineering' -Barricade.ap = {cost = 1} - -function Barricade:client_criteria(player) - local p_tile = player:getTile() - assert(player:isStaged('inside'), 'Must be inside building to barricade') - assert(p_tile.barricade:roomForFortification(), 'There is no room available for fortifications') - assert(p_tile.barricade:canPlayerFortify(player), 'Unable to make stronger fortification without required skills') - assert(not p_tile.integrity:isState('ruined'), 'Unable to make fortifications in a ruined building') -end - -Barricade.server_criteria = Barricade.client_criteria - -function Barricade:activate(player) - local building_tile = player:getTile() - local did_zombies_interfere = building_tile.barricade:didZombiesIntervene(player) - - if not did_zombies_interfere then building_tile.barricade:fortify(player, self.condition) end - - -------------------------------------------- - ----------- M E S S A G E -------------- - -------------------------------------------- - - local msg = not did_zombies_interfere and 'You fortify the building with a barricade.' or 'You start to fortify the building, but a zombie lurches towards you.' - - -------------------------------------------- - --------- B R O A D C A S T ------------ - -------------------------------------------- - - local event = {'barricade', player, did_zombies_interfere} - player.log:insert(msg, event) - - return did_zombies_interefere -- not sure if there is a better way to deal with returning the result (needed for the item:updateDurability() code) [only used by syringes and barricades] -end - -------------------------------------------------------------------- - -local Toolbox = class('Toolbox', Item) - -Toolbox.FULL_NAME = 'toolbox' -Toolbox.WEIGHT = 15 -Toolbox.DURABILITY = 10 -Toolbox.CATEGORY = 'engineering' -Toolbox.ap = {cost = 10, modifier = {repair = -2, repair_adv = -3}} --{cost= 5, modifier={repair= -1, repair_adv = -1}} - -function Toolbox:client_criteria(player) - assert(player:isStaged('inside'), 'Must be inside building to repair') - local p_tile = player:getTile() - local can_repair_building = p_tile.integrity:canModify(player) - assert(can_repair_building, 'Unable to repair building in current state') -end - -Toolbox.server_criteria = Toolbox.client_criteria - -local toolbox_dice = {'3d2-2', '3d2-1', '3d2', '3d2+1'} - -function Toolbox:activate(player) - local repair_dice = dice:new(toolbox_dice[self.condition]) - if player.skills:check('repair') then repair_dice = repair_dice / 1 end - if player.skills:check('repair_adv') then repair_dice = repair_dice ^ 3 end - - local building = player:getTile() - building.integrity:updateHP(repair_dice:roll() ) - local integrity_state = building.integrity:getState() - - -------------------------------------------- - ----------- M E S S A G E -------------- - -------------------------------------------- - - local self_msg = 'You repair the building {is_finished}.' - local msg = '{player} repairs the building {is_finished}.' - local names = {player=player, is_finished=integrity_state == 'intact' and 'completely' or ''} - self_msg = self_msg:replace(names) - msg = msg:replace(names) - - -------------------------------------------- - --------- B R O A D C A S T ------------ - -------------------------------------------- - - local event = {'toolbox', integrity_state} - player:broadcastEvent(msg, self_msg, event) -end - -------------------------------------------------------------------- - -return {Generator, Transmitter, Terminal, Fuel, Barricade, Toolbox} \ No newline at end of file diff --git a/code/item/items/machinery.lua b/code/item/items/machinery.lua new file mode 100644 index 0000000..815084d --- /dev/null +++ b/code/item/items/machinery.lua @@ -0,0 +1,121 @@ +local class = require('code.libs.middleclass') +local Item = require('code.item.item') +local broadcastEvent = require('code.server.event') +string.replace = require('code.libs.replace') +local dice = require('code.libs.dice') + +local Generator = class('Generator', Item) + +Generator.FULL_NAME = 'generator' +Generator.WEIGHT = 25 +Generator.DURABILITY = 0 +Generator.CATEGORY = 'engineering' +Generator.ap = {cost = 10, modifier = {tech = -2, power_tech = -4}} + +function Generator:client_criteria(player) + local p_tile = player:getTile() + assert(player:isStaged('inside'), 'Must be inside building to install generator') + assert(not p_tile.generator:isPresent(), 'There is no room for a second generator') +end + +Generator.server_criteria = Generator.client_criteria + +function Generator:activate(player) + local building_tile = player:getTile() + building_tile:insert('generator', self.condition) + + -------------------------------------------- + ----------- M E S S A G E -------------- + -------------------------------------------- + + local self_msg = 'You install a generator.' + local msg = '{player} installs a generator.' + msg = msg:replace(player) + + -------------------------------------------- + --------- B R O A D C A S T ------------ + -------------------------------------------- + + local event = {'generator', player} + player:broadcastEvent(msg, self_msg, event) +end + +------------------------------------------------------------------- + +local Transmitter = class('Transmitter', Item) + +Transmitter.FULL_NAME = 'transmitter' +Transmitter.WEIGHT = 25 +Transmitter.DURABILITY = 0 +Transmitter.CATEGORY = 'engineering' +Transmitter.ap = {cost = 10, modifier = {tech = -2, radio_tech = -4}} + +function Transmitter:client_criteria(player) + local p_tile = player:getTile() + assert(player:isStaged('inside'), 'Must be inside building to install transmitter') + assert(not p_tile.transmitter:isPresent(), 'There is no room for a second transmitter') +end + +Transmitter.server_criteria = Transmitter.client_criteria + +function Transmitter:activate(player) + local building_tile = player:getTile() + building_tile:insert('transmitter', self.condition) + + -------------------------------------------- + ----------- M E S S A G E -------------- + -------------------------------------------- + + local self_msg = 'You install a transmitter.' + local msg = '{player} installs a transmitter.' + msg = msg:replace(player) + + -------------------------------------------- + --------- B R O A D C A S T ------------ + -------------------------------------------- + + local event = {'transmitter', player} + player:broadcastEvent(msg, self_msg, event) +end + +------------------------------------------------------------------- + +local Terminal = class('Terminal', Item) + +Terminal.FULL_NAME = 'terminal' +Terminal.WEIGHT = 25 +Terminal.DURABILITY = 0 +Terminal.CATEGORY = 'engineering' +Terminal.ap = {cost = 10, modifier = {tech = -2, computer_tech = -4}} + +function Terminal:client_criteria(player) + local p_tile = player:getTile() + assert(player:isStaged('inside'), 'Must be inside building to install terminal') + assert(not p_tile.terminal:isPresent(), 'There is no room for a second terminal') +end + +Terminal.server_criteria = Terminal.client_criteria + +function Terminal:activate(player) + local building_tile = player:getTile() + building_tile:insert('terminal', self.condition) + + -------------------------------------------- + ----------- M E S S A G E -------------- + -------------------------------------------- + + local self_msg = 'You install a terminal.' + local msg = '{player} installs a terminal.' + msg = msg:replace(player) + + -------------------------------------------- + --------- B R O A D C A S T ------------ + -------------------------------------------- + + local event = {'terminal', player} + player:broadcastEvent(msg, self_msg, event) +end + +------------------------------------------------------------------- + +return {Generator, Transmitter, Terminal} \ No newline at end of file diff --git a/code/item/items/tools.lua b/code/item/items/tools.lua new file mode 100644 index 0000000..87b9a93 --- /dev/null +++ b/code/item/items/tools.lua @@ -0,0 +1,164 @@ +local class = require('code.libs.middleclass') +local Item = require('code.item.item') +local broadcastEvent = require('code.server.event') +string.replace = require('code.libs.replace') +local dice = require('code.libs.dice') + +------------------------------------------------------------------- + +local Fuel = class('Fuel', Item) + +Fuel.FULL_NAME = 'fuel tank' +Fuel.WEIGHT = 10 +Fuel.DURABILITY = 0 +Fuel.CATEGORY = 'engineering' +Fuel.ap = {cost = 1} + +function Fuel:client_criteria(player) + local p_tile = player:getTile() + assert(player:isStaged('inside'), 'Must be inside building to refuel') + assert(p_tile.generator:isPresent(), 'Missing nearby generator to refuel') +end + +Fuel.server_criteria = Fuel.client_criteria + +function Fuel:activate(player) + local building_tile = player:getTile() + building_tile.generator:refuel() + + -------------------------------------------- + ----------- M E S S A G E -------------- + -------------------------------------------- + + local self_msg = 'You refuel the generator.' + local msg = '{player} refuels the generator.' + msg = msg:replace(player) + + -------------------------------------------- + --------- B R O A D C A S T ------------ + -------------------------------------------- + + local event = {'fuel', player} + player:broadcastEvent(msg, self_msg, event) +end + +------------------------------------------------------------------- + +local Barricade = class('Barricade', Item) + +Barricade.FULL_NAME = 'barricade' +Barricade.WEIGHT = 7 +Barricade.DURABILITY = 0 +Barricade.CATEGORY = 'engineering' +Barricade.ap = {cost = 1} + +function Barricade:client_criteria(player) + local p_tile = player:getTile() + assert(player:isStaged('inside'), 'Must be inside building to barricade') + assert(p_tile.barricade:roomForFortification(), 'There is no room available for fortifications') + assert(p_tile.barricade:canPlayerFortify(player), 'Unable to make stronger fortification without required skills') + assert(not p_tile.integrity:isState('ruined'), 'Unable to make fortifications in a ruined building') +end + +Barricade.server_criteria = Barricade.client_criteria + +function Barricade:activate(player) + local building_tile = player:getTile() + local did_zombies_interfere = building_tile.barricade:didZombiesIntervene(player) + + if not did_zombies_interfere then building_tile.barricade:fortify(player, self.condition) end + + -------------------------------------------- + ----------- M E S S A G E -------------- + -------------------------------------------- + + local msg = not did_zombies_interfere and 'You fortify the building with a barricade.' or 'You start to fortify the building, but a zombie lurches towards you.' + + -------------------------------------------- + --------- B R O A D C A S T ------------ + -------------------------------------------- + + local event = {'barricade', player, did_zombies_interfere} + player.log:insert(msg, event) + + return did_zombies_interefere -- not sure if there is a better way to deal with returning the result (needed for the item:updateDurability() code) [only used by syringes and barricades] +end + +------------------------------------------------------------------- + +local Toolbox = class('Toolbox', Item) + +Toolbox.FULL_NAME = 'toolbox' +Toolbox.WEIGHT = 15 +Toolbox.DURABILITY = 1 +Toolbox.CATEGORY = 'engineering' +Toolbox.ap = {cost = 5, modifier = {repair = -1, repair_adv = -2}} + +function Toolbox:client_criteria(player) + local p_building = player:getTile() + assert(p_building:isBuilding(), 'No building nearby to repair') + assert(player:isStaged('inside'), 'Must be inside building to repair') + + -- integrity code + assert(not p_building.integrity:isState('intact'), 'Cannot repair building that has full integrity') + if p_building.integrity:isState('ruined') then + local n_zombies = p_building:countPlayers('zombie', 'inside') + assert(player.skills:check('renovate'), 'Must have "renovate" skill to repair ruins') + assert(n_zombies == 0, 'Cannot repair building with zombies present') + end + + --[[ Other targets to check (machines, doors, etc.) + -- machine code + assert(p_building:isPresent('damaged machines'), 'No damaged machines are present to repair') + + -- door code + assert(p_building.door:isDamaged(), 'No damaged door to repair') + --]] +end + +function Toolbox.server_criteria(player) --, target) + local p_building = player:getTile() + assert(p_building:isBuilding(), 'No building nearby to repair') + assert(player:isStaged('inside'), 'Must be inside building to repair') + + assert(not p_building.integrity:isState('intact'), 'Cannot repair building that has full integrity') + if p_building.integrity:isState('ruined') then + local n_zombies = p_building:countPlayers('zombie', 'inside') + assert(player.skills:check('renovate'), 'Must have "renovate" skill to repair ruins') + assert(n_zombies == 0, 'Cannot repair building with zombies present') + end +--[[ Need a better system to identify targets + if target == 'building' then + elseif target == 'door' then + assert(p_building.door:isDamaged(), 'No damaged door to repair') + else -- target is a machine + assert(p_building:isPresent('damaged machines'), 'No damaged machines are present to repair') + end +--]] +end + +function Toolbox:activate(player, target) + local building = player:getTile() + building.integrity:updateHP(1) + + -------------------------------------------- + ----------- M E S S A G E -------------- + -------------------------------------------- + + local self_msg = 'You repair the building {is_finished}.' + local msg = '{player} repairs the building {is_finished}.' + local names = {player=player, is_finished=building.integrity:isState('intact') and 'completely' or ''} + self_msg = self_msg:replace(names) + msg = msg:replace(names) + + -------------------------------------------- + --------- B R O A D C A S T ------------ + -------------------------------------------- + + local event = {'toolbox'} + player:broadcastEvent(msg, self_msg, event) +end + +------------------------------------------------------------------- + +return {Fuel, Barricade, Toolbox} \ No newline at end of file diff --git a/code/item/mixin/is_armor.lua b/code/item/mixin/is_armor.lua new file mode 100644 index 0000000..334fb4c --- /dev/null +++ b/code/item/mixin/is_armor.lua @@ -0,0 +1,17 @@ +local IsArmor = {} + +function IsArmor:updateArmorDurability(degrade_multiplier) -- god this method name is horrible, think of something better + local failed_durability_test = self:failDurabilityCheck(player, degrade_multiplier) + local condition + + if failed_durability_test then condition = self:updateCondition(-1) end + + return condition +end + +function IsArmor:getProtection(damage_type) + local resistance, condition = self.armor.RESISTANCE, self.condition + return (resistance[condition] and resistance[condition][damage_type]) or resistance[damage_type] or 0 +end + +return IsArmor \ No newline at end of file diff --git a/code/location/tile/building/building.lua b/code/location/tile/building/building.lua index 234c4cb..abdc23e 100644 --- a/code/location/tile/building/building.lua +++ b/code/location/tile/building/building.lua @@ -106,14 +106,21 @@ end -- function Building:getPos() return (NO NEED?) function Building:isPresent(setting) - if setting == 'equipment' then - for machine, i in pairs(equipment.subclasses) do + if setting == 'machines' then + for Machine in ipairs(Machines) do + machine = string.lower(tostring(Machine)) if self[machine] then return true end end + return false + elseif setting == 'powered machines' then + return self:isPresent('machines') and self:isPowered() + elseif setting == 'damaged machines' then + for Machine in ipairs(Machines) do + machine = string.lower(tostring(Machine)) + if self[machine] and self[machine]:isDamaged() then return true end + end return false - elseif setting == 'powered equipment' then - return self:isPresent('equipment') and self:isPowered() - else -- individual equipment + else -- individual machine local machine = setting return self[machine] end diff --git a/code/location/tile/building/door.lua b/code/location/tile/building/door.lua index 5ab9569..bdcf908 100644 --- a/code/location/tile/building/door.lua +++ b/code/location/tile/building/door.lua @@ -2,24 +2,14 @@ local class = require('code.libs.middleclass') local Barrier = require('code.location.tile.building.barrier') local Door = class('Door', Barrier) -local default_hp, max_hp = 3, 3 - -local door_desc = {[0] = 'destroyed', [1] = 'smashed', [2] = 'dented', [3] = 'undamaged'} +local DEFAULT_HP, MAX_HP = 3, 3 function Door:initialize() Barrier.initialize(self) - self.hp = default_hp + self.hp = DEFAULT_HP self.is_open = false - - self.hp_desc = door_desc[default_hp] end - - Door.max_hp = max_hp - -function Door:updateDesc() self.hp_desc = door_desc[self.hp] end - -function Door:getDesc() return self.hp_desc end function Door:repair() self:updateHP(3) end @@ -27,6 +17,8 @@ function Door:toggle() self.is_open = not self.is_open end function Door:isDestroyed() return self.hp == 0 end +function Door:isDamaged() return self.hp == MAX_HP end + function Door:isOpen() return self.is_open end return Door \ No newline at end of file diff --git a/code/location/tile/building/integrity.lua b/code/location/tile/building/integrity.lua index a7ec529..ec3abc9 100644 --- a/code/location/tile/building/integrity.lua +++ b/code/location/tile/building/integrity.lua @@ -2,25 +2,21 @@ local class = require('code.libs.middleclass') local Integrity = class('Integrity') -local BUILDING_MAX_HP = 20 --{15, 20, 30, ???} this is a HP table based on building sizes -local RANSACK_VALUE = BUILDING_MAX_HP - 10 -local MAX_DECAY = -60 - function Integrity:initialize(building) self.building = building - self.hp = BUILDING_MAX_HP -- Update this later to the size of the building (all buildilng are 1 tile sized right now) + self.hp = building.MAX_INTEGRITY end function Integrity:updateHP(num) - self.hp = math.min(math.max(self.hp+num, MAX_DECAY), BUILDING_MAX_HP) + local max_hp, max_decay = self.building.MAX_INTEGRITY, -1*self.building.MAX_INTEGRITY + + self.hp = math.min(math.max(self.hp+num, max_decay), max_hp) - if self.hp >= 0 or self.hp == MAX_DECAY then + if self.hp > 0 or self.hp == max_decay then --remove building from decay list - elseif self.hp < 0 then + elseif self.hp <= 0 then --add building to decay list end - - self:updateDesc() end function Integrity:canModify(player) -- possibly move all or parts of this code to criteria.toolbox and critera.ransack? @@ -44,14 +40,11 @@ end function Integrity:isState(setting) return self:getState() == setting end function Integrity:getState() - local integrity_is_full = self.hp == BUILDING_MAX_HP - local integrity_within_ransack_range = self.hp >= 0 - local integrity_within_ruin_range = self.hp < 0 - return (integrity_is_full and 'intact') or (integrity_within_ransack_range and 'ransacked') or (integrity_within_ruin_range and 'ruined') + local integrity_is_full = self.hp == self.building.MAX_INTEGRITY + local integrity_is_ransacked = self.hp > 0 + return (integrity_is_full and 'intact') or (integrity_is_ransacked and 'ransacked') or 'ruined' end ---function Integrity:getHP() return self.hp end - -function Integrity:updateDesc() end +function Integrity:getHP() return self.hp end return Integrity \ No newline at end of file diff --git a/code/location/tile/building/machine/machine.lua b/code/location/tile/building/machine/machine.lua index 470f0c1..9c4f926 100644 --- a/code/location/tile/building/machine/machine.lua +++ b/code/location/tile/building/machine/machine.lua @@ -22,6 +22,8 @@ end function Machine:getHP() return self.hp end +function Machine:isDamaged() return self.hp ~= MAX_HP end + function Machine:__tostring() return self.name end --[[ diff --git a/code/location/tile/tile.lua b/code/location/tile/tile.lua index 35b97a9..451cc51 100644 --- a/code/location/tile/tile.lua +++ b/code/location/tile/tile.lua @@ -104,6 +104,21 @@ function Tile:getPlayers(setting, filter) return next(players) and players or nil end +function Tile:getCorpses(setting) + local corpses, players = {} + + if setting == 'outside' then players = self.outside_players + elseif setting == 'inside' then players = self.inside_players + else error('Tile:getCorpses setting arg not present') + end + + for player in pairs(players) do + if not player:isStanding() then corpses[#corpses+1] = player end + end + + return next(corpses) and corpses or nil +end + function Tile:isIntegrity(setting) if self:isBuilding() then return self.integrity:getState() == setting else return 'intact' == setting diff --git a/code/player/equipment.lua b/code/player/equipment.lua new file mode 100644 index 0000000..a9ee1f8 --- /dev/null +++ b/code/player/equipment.lua @@ -0,0 +1,19 @@ +local class = require('code.libs.middleclass') +local Equipment = class('Equipment') + +-- clothing slots for (feet, hands, eye, mask, helmet, back, suit) +-- armor slot for (exo_suit) + +function Equipment:initialize(player) + self.player = player +end + +function Equipment:add(section, object) + self[section] = object +end + +function Equipment:remove(section) self[section] = nil end + +function Equipment:isPresent(section) return self[section] end + +return Equipment \ No newline at end of file diff --git a/code/player/human/action/basic.lua b/code/player/human/action/basic.lua index 85c860b..9db9011 100644 --- a/code/player/human/action/basic.lua +++ b/code/player/human/action/basic.lua @@ -45,7 +45,7 @@ function move.activate(player, dir) local y, x = player:getPos() local map = player:getMap() local dir_y, dir_x = getNewPos(y, x, dir) - local GPS_usage + local GPS, GPS_usage, condition if player:isStaged('inside') then map[y][x]:remove(player, 'inside') @@ -56,15 +56,14 @@ function move.activate(player, dir) end else -- player is outside local inventory_has_GPS, inv_ID = player.inventory:search('GPS') + GPS = player.inventory:lookup(inv_ID) if inventory_has_GPS then -- the GPS has a chance to avoid wasting ap on movement local GPS_chance = (player.skils:check('gadgets') and GPS_advanced_chance) or GPS_basic_chance local GPS_usage = GPS_chance >= math.random() -- this is pretty much a hack (if a player's ap is 50 then they will NOT receive the ap) if GPS_usage then player:updateStat('ap', 1) end - - local GPS = player.inventory:lookup(inv_ID) - if GPS:failDurabilityCheck(player) then GPS:updateCondition(-1, player, inv_ID) end + condition = player.inventory:updateDurability(inv_ID) end map[y][x]:remove(player) @@ -81,6 +80,12 @@ function move.activate(player, dir) local self_msg = 'You travel {dir} {with_GPS}.' local names = {dir=compass[dir], with_GPS=GPS_str} self_msg = self_msg:replace(names) + + if condition == 0 then + self_msg = self_msg..'Your '..tostring(GPS)..' is destroyed!' + elseif condition and GPS:isConditionVisible(player) then + self_msg = self_msg..'Your '..tostring(GPS)..' degrades to a '..GPS:getConditionState()..' state.' + end -------------------------------------------- --------- B R O A D C A S T ------------ @@ -126,21 +131,21 @@ function attack.server_criteria(player, target, weapon, inv_ID) assert(player:isSameLocation(target), 'Target has moved out of range') end -local ARMOR_DAMAGE_MOD = 2.5 - function attack.activate(player, target, weapon, inv_ID) local target_class = target:getClassName() local attack, damage, critical = combat(player, target, weapon) - local condition + local armor_condition, condition + local armor if attack then - if target.armor:isPresent() and not weapon:isHarmless() then + if target.equipment:isPresent('armor') and not weapon:isHarmless() then + armor = target.equipment.armor local damage_type = weapon:getDamageType() - local resistance = target.armor:getProtection(damage_type) + local resistance = armor:getProtection(damage_type) damage = damage - resistance -- do we need to add a desc if resistance is working? (ie absorbing damage in battle log?) - local retailation_damage = target.armor:getProtection('damage_melee_attacker') + local retailation_damage = armor:getProtection('damage_melee_attacker') local is_melee_attack = weapon:getStyle() == 'melee' if is_melee_attack and retailation_damage > 0 then local retailation_hp_loss = -1*dice.roll(retailation_damage) @@ -148,8 +153,9 @@ function attack.activate(player, target, weapon, inv_ID) -- insert some type of event? end - local degrade_chance = math.floor(damage/ARMOR_DAMAGE_MOD) + 1 -- might wanna change this later? Damage affects degrade chance? - if target.armor:failDurabilityCheck(degrade_chance) then target.armor:degrade(target) end + local degrade_multiplier = 1 -- player.skills:check() some armor breaking skill? + armor_condition = armor:updateArmorDurability(degrade_multiplier) + if armor_condiiton == 0 then target.equipment:remove('armor') end end if target.skills:check('track') then @@ -194,6 +200,13 @@ function attack.activate(player, target, weapon, inv_ID) self_msg = self_msg..'Your '..tostring(weapon)..' degrades to a '..weapon:getConditionState()..' state.' end + if armor_condition == 0 then + self_msg = self_msg..'Their '..tostring(armor)..' is destroyed!' + target_msg = target_msg..'Your '..tostring(armor)..' is destroyed!' + --elseif armor_condition and armor:isConditionVisible(target) then (should organic armor condition be visible to zombies?) + -- target_msg = target_msg..'Your '..tostring(armor)..' degrades to a '..armor:getConditionState()..' state.' + end + -------------------------------------------- --------- B R O A D C A S T ------------ -------------------------------------------- diff --git a/code/player/human/carcass.lua b/code/player/human/carcass.lua index fb5fe63..6e9b0f6 100644 --- a/code/player/human/carcass.lua +++ b/code/player/human/carcass.lua @@ -1,7 +1,7 @@ local class = require('code.libs.middleclass') local Carcass = class('Carcass') -local MAX_FEEDINGS = 5 +local MAX_FEEDINGS = 4 function Carcass:initialize(player) self.player = player diff --git a/code/player/player.lua b/code/player/player.lua index 7b68b18..a5da1fd 100644 --- a/code/player/player.lua +++ b/code/player/player.lua @@ -4,6 +4,7 @@ local StatusEffect = require('code.player.status_effect.status_effect') local broadcastEvent = require('code.server.event') local catalogAvailableActions = require('code.player.catalog') local chanceToHit = require('code.player.chanceToHit') +local Equipment = require('code.player.equipment') local Player = class('Player') @@ -27,6 +28,7 @@ function Player:initialize(username, map_zone, y, x) --add account name self.ID = self self.log = Log:new() self.status_effect = StatusEffect:new(self) + self.equipment = Equipment:new(self) map_zone[y][x]:insert(self) end diff --git a/code/player/zombie/ability/generic.lua b/code/player/zombie/ability/generic.lua index 84959e2..43db884 100644 --- a/code/player/zombie/ability/generic.lua +++ b/code/player/zombie/ability/generic.lua @@ -236,35 +236,38 @@ end ------------------------------------------------------------------- -local ransack = {name='ransack', ap={cost=5, modifier={ransack = -1, ruin = -2}}} +local ruin = {name='ruin', ap={cost=5, modifier={ruin = -1, ruin_adv = -2}}} -function ransack.client_criteria(player) +function ruin.client_criteria(player) local p_tile = player:getTile() assert(p_tile:isBuilding(), 'No building nearby to ransack') assert(player:isStaged('inside'), 'Player must be inside building to ransack') - - local can_ransack_building = p_tile.integrity:canModify(player) - assert(can_ransack_building, 'Unable to ransack building in current state') -end -function ransack.server_criteria(player) - local p_tile = player:getTile() - assert(p_tile:isBuilding(), 'No building nearby to ransack') - assert(player:isStaged('inside'), 'Player must be inside building to ransack') - - local can_ransack_building = p_tile.integrity:canModify(player) - assert(can_ransack_building, 'Unable to ransack building in current state') + assert(player.skills:check('ruin'), 'Must have "ruin" skill to use ability') -- remove this later when abilities implement required_skill + + + -- integrity code + assert(p_building.integrity:isState('intact'), 'Cannot repair building that has full integrity') + if p_building.integrity:isState('ruined') then + local n_zombies = p_building:countPlayers('zombie', 'inside') + assert(player.skills:check('renovate'), 'Must have "renovate" skill to repair ruins') + assert(n_zombies == 0, 'Cannot repair building with zombies present') + end + + local n_humans = p_tile:countPlayers('human', 'inside') + + local integrity_hp = p_tile.integrity:getHP() + assert(integrity_hp >= 0, 'Cannot ruin building that is already ruined') + assert(integrity_hp == 0 and n_humans == 0, 'Cannot ruin building with humans present') end -function ransack.activate(player) - local ransack_dice = dice:new('2d3') - if player.skills:check('ransack') then ransack_dice = ransack_dice / 1 end - if player.skills:check('ruin') then ransack_dice = ransack_dice ^ 4 end - +ruin.server_criteria = ruin.client_criteria + +function ruin.activate(player) local building = player:getTile() - building.integrity:updateHP(-1 * ransack_dice:roll() ) + building.integrity:updateHP(-1) local integrity_state = building.integrity:getState() - local building_was_ransacked = integrity_state == 'ransacked' --local building_was_ruined = integrity_state == 'ruined' + local building_was_ransacked = integrity_state == 'ransacked' -------------------------------------------- ----------- M E S S A G E -------------- @@ -281,7 +284,7 @@ function ransack.activate(player) --------- B R O A D C A S T ------------ -------------------------------------------- - local event = {'ransack', player, integrity_state} + local event = {'ruin', player, integrity_state} player:broadcastEvent(msg, self_msg, event) end diff --git a/code/player/zombie/action/advanced.lua b/code/player/zombie/action/advanced.lua index 0afe611..4c14909 100644 --- a/code/player/zombie/action/advanced.lua +++ b/code/player/zombie/action/advanced.lua @@ -34,14 +34,14 @@ end local feed = {name='feed', ap={cost=1}} function feed.client_criteria(player) - local p_tile, p_stage = player:getTile(), player:getStage() - local corpse_n = p_tile:countCorpses(p_stage) - assert(corpse_n > 0, 'No available corpses to eat') + local p_tile = player:getTile() + local p_stage = player:getStage() + local corpses = p_tile:getCorpses(p_stage) + assert(corpses, 'No available corpses to eat') local edible_corpse_present - local tile_player_group = p_tile:getPlayers(p_stage) - for tile_player in pairs(tile_player_group) do - if not tile_player:isStanding() and tile_player:isMobType('human') and tile_player.carcass:edible(player) then + for corpse in pairs(corpses) do + if corpse:isMobType('human') and corpse.carcass:edible(player) then edible_corpse_present = true break end @@ -50,41 +50,42 @@ function feed.client_criteria(player) end function feed.server_criteria(player) - local p_tile, p_stage = player:getTile(), player:getStage() - local corpse_n = p_tile:countCorpses(p_stage) - assert(corpse_n > 0, 'No available corpses to eat') + local p_tile = player:getTile() + local p_stage = player:getStage() + local corpses = p_tile:getCorpses(p_stage) + assert(corpses, 'No available corpses to eat') local edible_corpse_present - local tile_player_group = p_tile:getPlayers(p_stage) - for tile_player in pairs(tile_player_group) do - if not tile_player:isStanding() and tile_player:isMobType('human') and tile_player.carcass:edible(player) then + for corpse in pairs(corpses) do + if corpse:isMobType('human') and corpse.carcass:edible(player) then edible_corpse_present = true break end end - assert(edible_corpse_present, 'All corpses have been eaten') + assert(edible_corpse_present, 'All corpses have been eaten') end local corpse_effects = { -- First come, first serve! (less xp and decay loss as corpse becomes more devoured) - xp = {'1d10+5', '1d9+3', '1d7+2', '1d5+1', '1d3'}, - satiation = {'1d400+600', '1d400+500', '1d400+400', '1d400+300', '1d400+200'}, - description = {'very fresh', 'fresh', '', 'old', 'very old'} + xp = {'1d10+5', '1d9+3', '1d7+2', '1d5+1'}, + satiation = {'1d400+600', '1d400+500', '1d400+400', '1d400+300'}, + description = {'very fresh', 'fresh', 'old', 'very old'} } function feed.activate(player) - local p_tile, p_stage = player:getTile(), player:getStage() - local tile_player_group = p_tile:getPlayers(p_stage) + local p_tile = player:getTile() + local p_stage = player:getStage() + local corpses = p_tile:getCorpses(p_stage) local target - local lowest_scavenger_num = 5 + local lowest_scavenger_num = 4 -- finds the corpse with the lowest number of scavengers (fresh meat) - for tile_player in pairs(tile_player_group) do + for corpse in pairs(corpses) do - if not tile_player:isStanding() and tile_player:isMobType('human') and tile_player.carcass:edible(player) then - local corpse_scavenger_num = #tile_player.carcass.carnivour_list + if corpse:isMobType('human') and corpse.carcass:edible(player) then + local corpse_scavenger_num = #corpse.carcass.carnivour_list if lowest_scavenger_num > corpse_scavenger_num then - target = tile_player + target = corpse lowest_scavenger_num = corpse_scavenger_num end end diff --git a/code/player/zombie/action/basic.lua b/code/player/zombie/action/basic.lua index 6acc62a..3a64f28 100644 --- a/code/player/zombie/action/basic.lua +++ b/code/player/zombie/action/basic.lua @@ -117,12 +117,11 @@ function attack.server_criteria(player, target, weapon) end end -local ARMOR_DAMAGE_MOD = 2.5 - function attack.activate(player, target, weapon) local target_class = target:getClassName() local attack, damage, critical = combat(player, target, weapon) local caused_infection + local armor_condition, armor if attack then if target_class == 'player' then @@ -140,8 +139,9 @@ function attack.activate(player, target, weapon) -- insert some type of event? end - local degrade_chance = math.floor(damage/ARMOR_DAMAGE_MOD) + 1 -- might wanna change this later? Damage affects degrade chance? - if target.armor:failDurabilityCheck(degrade_chance) then target.armor:degrade(target) end + local degrade_multiplier = player.skills:check('power_claw') and 2 or 1 + armor_condition = armor:updateArmorDurability(degrade_multiplier) + if armor_condiiton == 0 then target.equipment:remove('armor') end end if player.skills:check('track') then @@ -200,7 +200,14 @@ function attack.activate(player, target, weapon) -- infection message to the ZOMBIE only! (human isn't notified until incubation wears off) if caused_infection then self_msg = self_msg .. ' They become infected.' end - + + if armor_condition == 0 then + self_msg = self_msg..'Their '..tostring(armor)..' is destroyed!' + target_msg = target_msg..'Your '..tostring(armor)..' is destroyed!' + elseif armor_condition and armor:isConditionVisible(target) then + target_msg = target_msg..'Your '..tostring(armor)..' degrades to a '..armor:getConditionState()..' state.' + end + -------------------------------------------- --------- B R O A D C A S T ------------ -------------------------------------------- diff --git a/code/player/zombie/organic_armor.lua b/code/player/zombie/organic_armor.lua new file mode 100644 index 0000000..1086bf0 --- /dev/null +++ b/code/player/zombie/organic_armor.lua @@ -0,0 +1,169 @@ +local dice = require('code.libs.dice') +local class = require('code.libs.middleclass') +local Item = require('code.item.item') +local IsArmor = require('code.item.mixin.is_armor') + +------------------------------------------------------------------- + +local OrganicArmor = class('OrganicArmor', Item):include(IsArmor) -- remove Item superclass... do we need the methods? +OrganicArmor.ap = {cost = 5, modifier={armor_adv = -2}} +OrganicArmor.list = {} + +function OrganicArmor:client_criteria(player) + assert(player.skills:check('armor'), 'Must have "armor" skill to create armor') + + local p_tile = player:getTile() + local p_stage = player:getStage() + local corpses = p_tile:getCorpses(p_stage) + assert(corpses, 'No available corpses to eat') + + local edible_corpse_present + for corpse in pairs(corpses) do + if corpse:isMobType('human') and corpse.carcass:edible(player) then + edible_corpse_present = true + break + end + end + assert(edible_corpse_present, 'All corpses have been eaten') +end + +function OrganicArmor:server_criteria(player, armor_type) + assert(player.skills:check('armor'), 'Must have "armor" skill to create armor') + assert(not armor_type or player.skills:check('armor_adv'), 'Must have "armor_adv" skill to select armor') + + local p_tile = player:getTile() + local p_stage = player:getStage() + local corpses = p_tile:getCorpses(p_stage) + assert(corpses, 'No available corpses to eat') + + local edible_corpse_present + for corpse in pairs(corpses) do + if corpse:isMobType('human') and corpse.carcass:edible(player) then + edible_corpse_present = true + break + end + end + assert(edible_corpse_present, 'All corpses have been eaten') +end + +function OrganicArmor:activate(player, armor_type) + local corpses = p_tile:getCorpses(player:getStage()) + local target + local lowest_scavenger_num = 4 + + -- finds the corpse with the lowest number of scavengers (fresh meat) + for corpse in pairs(corpses) do + if corpse:isMobType('human') and corpse.carcass:edible(player) then + local corpse_scavenger_num = #corpse.carcass.carnivour_list + if lowest_scavenger_num > corpse_scavenger_num then + target = corpse + lowest_scavenger_num = corpse_scavenger_num + end + end + end + + local nutrition_LV = target.carcass:devour(player) + local armor_dice = dice:new('1d'..nutrition_LV) + + if player.skills:check('armor_adv') then armor_dice = armor_dice ^ 1 end + local condition = armor_dice:roll() + + armor_type = armor_type or OrganicArmor.list[math.random(1, #OrganicArmor.list)] + armor = OrganicArmor.subclass[armor]:new(condition) -- This should work... + + player.equipment:add('armor', armor) + + -------------------------------------------- + ----------- M E S S A G E -------------- + -------------------------------------------- + + local msg = 'You mutate a corpse and gain a {armor}.' + msg = msg:replace(self) -- This should work? Needs to be tested + + -------------------------------------------- + --------- B R O A D C A S T ------------ + -------------------------------------------- + + local event = {'armor', player} + player.log:insert(msg, event) +end + +------------------------------------------------------------------- + +local Scale = class('Scale', OrganicArmor) +OrganicArmor.list[#OrganicArmor.list+1] = 'Scale' + +Scale.FULL_NAME = 'scale' +Scale.DURABILITY = 8 + +Scale.armor = {} +Scale.armor.resistance = { + {bullet=1, pierce=1}, + {bullet=2, blunt=1, pierce=2}, + {bullet=3, blunt=2, pierce=3}, + {bullet=5, blunt=4, pierce=4}, +} + +------------------------------------------------------------------- + +local Blubber = class('Blubber', OrganicArmor) +OrganicArmor.list[#OrganicArmor.list+1] = 'Blubber' + +Blubber.FULL_NAME = 'blubber' +Blubber.DURABILITY = 16 + +Blubber.armor = {} +Blubber.armor.resistance = { + { blunt=1, pierce=1}, + {bullet=1, blunt=1, pierce=1}, + {bullet=1, blunt=1, pierce=2}, + {bullet=2, blunt=1, pierce=2}, +} + +------------------------- ------------------------------------------ + +local Gel = class('Gel', OrganicArmor) +OrganicArmor.list[#OrganicArmor.list+1] = 'Gel' + +Gel.FULL_NAME = 'gel' +Gel.DURABILITY = 32 + +Gel.armor = {} +Gel.armor.resistance = { + { blunt=1, pierce=1, scorch=4}, + { blunt=1, pierce=1, scorch=4}, + {bullet=1, blunt=1, pierce=1, scorch=4}, + {bullet=1, blunt=1, pierce=1, scorch=4}, +} + +------------------------------------------------------------------- + +local Bone = class('Bone', OrganicArmor) +OrganicArmor.list[#OrganicArmor.list+1] = 'Bone' + +Bone.FULL_NAME = 'bone' +Bone.DURABILITY = 8 + +Bone.armor = {} +Bone.armor.resistance = { + {damage_melee_attacker=1}, + {damage_melee_attacker=1}, + {damage_melee_attacker=1}, + {damage_melee_attacker=2}, +} + +------------------------------------------------------------------- + +--[[ +local Stretch = class('Stretch', OrganicArmor) + +Stretch.FULL_NAME = 'stretch' +Stretch.DURABILITY = 8 +Stretch.armor.resistance = { + {bullet=1} +} +--]] + +------------------------------------------------------------------- + +return {Scale, Blubber, Gel, Bone} -- Stretch,} \ No newline at end of file diff --git a/code/work-in-progress/armor/class.lua b/code/work-in-progress/armor/class.lua deleted file mode 100644 index 2f03b5a..0000000 --- a/code/work-in-progress/armor/class.lua +++ /dev/null @@ -1,36 +0,0 @@ -local dice = require('code.libs.dice') -local item_armor_list = require('code.player.armor.item_list') -local class = require('code.libs.middleclass') - -local armor = class('armor') - -function armor:initialize(player) - self.player = player -end - --- Possibly consider redoing armor for the zombies? Make it similiar to human armor or less complex... if it's made to be similiar to human armor, it will shrink --- down the code in this file quite a bit, no need to differenate between human/zombie mobtypes - -function armor:failDurabilityCheck(degrade_chance) - local player, protection = self.player - - if player:isMobType('human') then return dice.roll(item_armor_list[self.name].durability) <= degrade_chance - elseif player:isMobType('zombie') then return dice.roll(self.durability) <= degrade_chance - end -end - -function armor:getProtection(damage_type) - local player, protection = self.player - - if player:isMobType('human') then return item_armor_list[self.name].resistance[self.condition][damage_type] or 0 - elseif player:isMobType('zombie') then return self.protection[damage_type] or 0 - end -end - -function armor:isPresent() - if player:isMobType('human') then return self.name and true or false - elseif player:isMobType('zombie') then return self.protection and true or false - end -end - -return armor \ No newline at end of file diff --git a/code/work-in-progress/armor/item_class.lua b/code/work-in-progress/armor/item_class.lua deleted file mode 100644 index 09d040f..0000000 --- a/code/work-in-progress/armor/item_class.lua +++ /dev/null @@ -1,32 +0,0 @@ -local class = require('code.libs.middleclass') -local Item = require('code.item.item') -local armor = require('code.player.armor.class') - -local item_armor = class('item_armor', armor) - -function item_armor:initialize(player) - armor.initialize(self, player) -end - -function item_armor:equip(name, condition) - if self:isPresent() then self:remove() end -- unequips the old armor and puts it back into the inventory - self.name, self.condition = name, condition -end - -function item_armor:remove() - local player, armor_type, condition = self.player, self.name, self.condition - local armor_INST = Item[armor_type]:new(condition) - - player.inventory:insert(armor_INST) - self.name, self.condition = nil, nil -end - -function item_armor:degrade(player) - self.condition = self.condition - 1 - if 0 > self.condition then -- armor is destroyed - self.name, self.condition = nil, nil - return -- something to tell that armor is destroyed? - end -end - -return item_armor \ No newline at end of file diff --git a/code/work-in-progress/armor/item_list.lua b/code/work-in-progress/armor/item_list.lua deleted file mode 100644 index 40c3fbe..0000000 --- a/code/work-in-progress/armor/item_list.lua +++ /dev/null @@ -1,31 +0,0 @@ -local armor = {} - ---[[ - full_name = 'insert name' - durability = num (average # of attacks it takes to wear armor out) - resistance = { - condition = {protection=num}, - } - - ** As condition becomes worse armor starts to lose resistance (with the exception of a few armors) ---]] - -armor.leather = {} -armor.leather.durability = 32 -armor.leather.resistance = { - [1] = {blunt=1}, - [2] = {blunt=1}, - [3] = {blunt=1}, - [4] = {blunt=1}, -} - -armor.firesuit = {} -armor.firesuit.durability = 4 -armor.firesuit.resistance = { - [1] = {acid=1}, - [2] = {acid=2}, - [3] = {acid=3}, - [4] = {acid=4}, -} - -return armor \ No newline at end of file diff --git a/code/work-in-progress/armor/organic_class.lua b/code/work-in-progress/armor/organic_class.lua deleted file mode 100644 index 72c72cd..0000000 --- a/code/work-in-progress/armor/organic_class.lua +++ /dev/null @@ -1,83 +0,0 @@ -local class = require('code.libs.middleclass') -local organic_armor_list = require('code.player.armor.organic_list') -local armor = require('code.player.armor.class') - -local organic_armor = class('organic_armor', armor) - -function organic_armor:initialize(player) - armor.initialize(self, player) - self.layers = {} - self.player = player -end - -local LAYER_STACKING_MULTIPLYER = {0, 1/8, 2/8, 4/8} - -local function calculateDurability(layers) - local mean = 0 - for _, armor_type in ipairs(layers) do - local stack = layers[armor_type] - local multiplyer = 1 + LAYER_STACKING_MULTIPLYER[stack] - mean = mean + (organic_armor_list[armor_type].durability * multiplyer) - end - return math.floor(mean/#layers + 0.5) -- returns a rounded integer -end - --- PER LEVEL OF ARMOR -local organic_resistance_values = {bullet=2, blunt=1, pierce=1, scorch=1, damage_melee_attacker=1} - -function organic_armor:equip(armor_name) - self.protection = self.protection or {} - - local resistance = organic_armor_list[armor_name].resistance - if resistance then - local value = organic_resistance_values[resistance] - self.protection[resistance] = (self.protection[resistance] or 0) + value - end - - self.layers[#self.layers+1] = armor_name - self.layers[armor_name] = (self.layers[armor_name] or 0) + 1 - - self.durability = calculateDurability(self.layers) -end - -function organic_armor:degrade() - local armor_name = self.layers[#self.layers] - local resistance = organic_armor_list[armor_name].resistance - - if resistance then - self.protection[resistance] = self.protection[resistance] - organic_resistance_values[resistance] - end - - self.layers[#self.layers] = nil - self.layers[armor_name] = self.layers[armor_name] - 1 - - if #self.layers == 0 then -- armor is destroyed - local player = self.player - player.armor = organic_armor:new(player) - return -- something?? - else - self.durability = calculateDurability(self.layers) - end -end - -local MAX_ORGANIC_LAYERS = 4 - -function organic_armor:hasRoomForLayer() return MAX_ORGANIC_LAYERS > #self.layers end - -function organic_armor:getAvailableArmors() - local list = {} - for armor_type in pairs(organic_armor_list) do - local skill = organic_armor_list[armor_type].required_skill - local cost, ep = self.player:getCost('ep', armor_type), self.player:getStat('ep') - list[armor_type] = (self.player.skills:check(skill) and ep >= cost) or nil - print(armor_type, list[armor_type]) - end - - print() - print('THIS IS OUR ARMOR LIST') - for k,v in pairs(list) do print(k,v) end - - return list -end - -return organic_armor \ No newline at end of file diff --git a/code/work-in-progress/armor/organic_list.lua b/code/work-in-progress/armor/organic_list.lua deleted file mode 100644 index c0d0579..0000000 --- a/code/work-in-progress/armor/organic_list.lua +++ /dev/null @@ -1,46 +0,0 @@ -local armor = {} - ---[[ - full_name = 'insert name' - resistance = bullet/pierce/blunt/scorch/damage_melee_attacker/nil - durability = num (average # of attacks it takes to wear armor out) - required_skill = 'skill name' ---]] - -armor.scale = {} -armor.scale.full_name = 'scale' -armor.scale.resistance = 'pierce' -armor.scale.durability = 16 -armor.scale.required_skill = 'armor' - -armor.blubber = {} -armor.blubber.full_name = 'blubber' -armor.blubber.resistance = 'blunt' -- dampens impact energy -armor.blubber.durability = 16 -armor.blubber.required_skill = 'armor' - -armor.stretch = {} -armor.stretch.full_name = 'stretch' -armor.stretch.resistance = 'bullet' -armor.stretch.durability = 8 -armor.stretch.required_skill = 'ranged_armor' - -armor.gel = {} -armor.gel.full_name = 'gel' -armor.gel.resistance = 'scorch' -armor.gel.durability = 16 -armor.gel.required_skill = 'liquid_armor' - -armor.sticky = {} -armor.sticky.full_name = 'sticky' -armor.sticky.resistance = nil -- sticky armor lacks resistance but greatly improves durability -armor.sticky.durability = 32 -armor.sticky.required_skill = 'liquid_armor' - -armor.bone = {} -armor.bone.full_name = 'bone' -armor.bone.resistance = 'damage_melee_attacker' -armor.bone.durability = 8 -armor.bone.required_skill = 'pain_armor' - -return armor \ No newline at end of file diff --git a/settings.lua b/settings.lua index 5107a9f..c0f9a67 100644 --- a/settings.lua +++ b/settings.lua @@ -1,5 +1,5 @@ local settings = { - _VERSION = 'ZomboTropolis v0.8.0', + _VERSION = 'ZomboTropolis v0.8.1', _AUTHOR = 'Timothy Torres', _URL = 'https://github.com/timothymtorres/ZomboTropolis-Roguelike', _DESCRIPTION = 'A zombie survival roguelike MMORPG.',