Starting out first, the most important thing when designing anything is consistency. What seems to be one of the most common implementations of combat, according to the developer forums, is something gross like this:
mob
proc
deathCheck(mob/killer)
if(health < 1)
world << "[killer] killed [src]."
if(client)
loc = locate(1,1,1) // etc...
else
del src // lol
verb
punch(mob/target in oview(1))
target.health -= rand(1,100)
target.deathCheck(src)
There are a couple of problems with this implementation. Mostly, it's redundant. Every time we reduce the enemy's health, we have to manually call deathCheck for them. Not to mention that if in the future you wanted to add something like damage reduction, you end up having to do a lot of extra work on top of what we're already doing. Instead we should sum up all of this stuff in one single function. Since all of them are directly related and there is probably no situation, or very few, where the exact same process isn't going to be repeated for every instance of damage taking, a single function approach will suit us just fine.
mob
proc
getHurt(damage, mob/attacker)
health -= damage
if(health < 1)
// death stuff
Now instead of lowing their health and doing a death check every time, we'll just call mob.getHurt() when we want a mob to take damage, and eventually die. Since everything is confined to the one function, changing how we take damage, and how we die, or what defines death, just takes a couple of lines. For example, if we want to add something like "damage resistance," we could just do something like:
mob
proc
getHurt(damage, mob/attacker)
var/damageResistance = getDamageResistance() // some value representing how much damage needs to be mitigated, as a percentage, like 60% (0.6).
if(damageResistance)
damage *= 1-damageResistance // reduce the damage by the given percentage. 1 - 0.6 = 0.4, 40% of what we originally had.
health -= damage
if(health<1)
// etc...
Now, in the spirit of modularity (removing as much redundant code, and making things that we want to repeat a lot, maybe in other places in code, into individual functions), we can end up with quite a few nifty functions that, maybe right now we don't have much use for, but later on could be pretty nice to have around.
Edited to include the variables we use, added isDying(), which determines if we should die now (health<1, etc...), and isDead() now checks if we're currently dead (added a death-state for mobs). Also added live() for making creatures "live" again, which is used by respawn().
mob
var
health = 100
maxHealth = 100
// boolean indicating we've died
dead = FALSE
proc
// take some hurt.
// optional attacker can be provided, for when someone is killing us.
getHurt(damage, mob/attacker)
if(isDead()) // nothing to be done if we're already dead.
return
health -= damage
if(isDying())
die(attacker)
// should we die? can we even die? let's check.
isDying()
//if(isModerator())
// return FALSE
return (health < 1) // by default, <1 should be impending death for all
// are we currently dead?
// this is for games where death does not instantly result in returning to the game.
// you could have an in-between state where the character is a ghost and has to find
// their body, or they go to hell or heaven, or something fancy like that.
isDead()
return (dead == TRUE)
// do some dying.
die(mob/killer)
dead = TRUE // we're dead now.
//deathAnnounce(src, killer || null)
//deathCry()
if(client)
//dropCorpse()
respawn()
else
//killer.getKillExperience(src) -- instead of putting the formula here, we can do it over there... another useful trick
//dropCorpse()
del src
// if we have anything special we need to do to make this character
// become "alive" again, do it here. like losing our halo, restoring our
// normal icon (maybe we became a ghost?), or anything like that. might also
// be wise to restore our health here instead of respawn().
live()
dead = FALSE
//restoreHealth()
// throw us to our graveyard.
// unlike live(), respawn() is meant to restore us to life
// and then move us to a proper respawn point.
respawn()
live() // we're back alive now. woo.
//Move(getGraveyard())
Now we have a nifty way of determing if people should die, and a way to instantly kill someone. Now the act of dying is not dependant on the player losing their health; since we've separated the function of dying from simply getting hurt, we can, with absolutely certainty, kill someone with one function call. Ignoring any type of damage mitigation, any type of inability to die (immortals, zombies, whatever). Pretty cool, right? Now you can add like, a slay command that doesn't have to do like... damage(mob, 99999999999999999999999999999999). Which is the most important command in a game, right?
We've also partitioned the act of respawning, which doesn't have much use right now, but it does separate the act of respawning from dying, which will make our code a lot easier to read, where everyone can tell what every line of code is doing based on just the name of the function being called.
Now we have a framework that we can build on. It is consistent with itself, and there is never a time during combat when we are going to be unsure about the status of the combatants. They die when they are supposed to, and we can kill them if we need to. Or move them back to the graveyard with full health. Or whatever.</1>