Jump to content
  • Advertisement
Sign in to follow this  
kSquared

Implementing stat modifiers

This topic is 4656 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I recently started a new project, a rewrite of Nethack for the .NET platform. It's definitely not my first major software project, but it is the first time I've made anything approaching an RPG-ish game. Things have been going pretty well, but I've run into my first major design roadblock, and I'm hoping you guys can help out. First, some background on the problem. Players have various attributes (strength, intelligence, hair color, hit points, etc.) that can be modified by external effects or items (being inside a Zone of Wussiness spell area, drinking a potion of dumbness, using a packet of pink hair dye, getting struck by a zombie's energy drain attack, etc.). Anything that can be the originator of such an effect implements IEffect. Different instances of the same IEffect vary only in their magnitude, duration, or size. For instance, being hit by a Strength-draining attack, being subjected to a Zone of Strengthiness spell, or drinking a potion of Schwarzenegger all place the same type of IEffect on you (since all affect the same attribute, Strength). Other than the Activate()/Deactivate()/Pause() methods (which turn the IEffect on/off or suppress it momentarily) IEffects have two more important features -- a list of child IEffects, and a timer:
  • All IEffects have timers; when it expires, the effect disappears. Sometimes the timer fires at certain intervals; for instance, a poison effect might cause damage to you every 5 turns. If the timer is disabled, the IEffect is permanent and lasts until it is removed.
  • They also have a list of child IEffects. If a parent IEffect is activated or deactivated, so are the child IEffects. An example of where this is useful is an area-effect spell; suppose an evil wizard creates a field of brambles that halves everyone's speed. Everyone in the area has their speed reduced by half (those who successfully resist or who are immune to being slowed don't have their IEffects activated, however). The parent IEffect is the field of brambles; the child IEffects are the Speed/2 effects being applied to each affected creature. This is also useful when complex IEffects do more than one thing. For instance, an Poison Fog spell blinds you, causes poison, and reduces your movement speed, each of which requires separate IEffects. Each effect also needs to be tested independently of the rest -- a creature might be immune to blindness but not poison, for instance, so you can't just apply all three at once.
Now, here's my problem: this puts the knowledge of whether or not someone should be subjected to an effect in the hands of the IEffect. For instance, if a player drinks a Strength-draining potion but is immune to Strength drain, the IEffect needs to check if the player is immune before it should be applied. Otherwise a negative modifier is erroneously applied to the player's statistics. It seems messy to put this logic into the IEffect. Instead, it seems cleaner to simply apply all IEffects to which an object is subjected, and then have those objects decide which ones they're affected by. For instance, consider a player who receives an IEffect that causes poison. Under this new design paradigm, he'd asked "are you affected by this?". The player would make an attempt to resist; if it fails, he tells the IEffect he's affected; otherwise he tells it not to affect him. Likewise, if the player were immune to poison (maybe he's a rock golem), he'd say "I'm not affected" without having to make an attempt to resist, and the IEffect doesn't get activated. But this seems "wrong" to me in that it gives control to the recipients of effects. Walls don't get to "decide" whether or not they're lit up by a torchlight, for instance; they just get lit. But this might be too much of a philosophical question, and maybe it really is the right way. So I'm conflicted about which decisions should be made where. Do I need a different abstraction here for this to work right? Any help is appreciated.

Share this post


Link to post
Share on other sites
Advertisement
I tend to think it's the responsibility of the target to hold his buff/resistance/immunity information, and that other effects should query the character to determine whether the new effects will 'stick' to him. This would involve checking immunities, rolling against their resistance, etc. Seems like the best way to me. The effect IMO doesn't and shouldn't contain enough information to determine such a thing. It knows what it does, which should be enough to query the target for whether or not it can effect it.

Share this post


Link to post
Share on other sites
I had implemented once a similar system. My approach was as follows: the effects that apply on an object are not in the responsibility of the object. However, effects interact. This ranges from the simple 'a slow spell cancels out a haste spell' or 'an immune to fire effect prevents burning effects' to the more complex 'for design reasons, auto-turn-1-hp-to-2-mp and auto-turn-1-mp-to-2-hp cannot be used at the same time'.

Objects are given initial, permanent, unremovable effects. For instance, a zombie is permanently under an 'undead' effect and 'ghastly touch' effect. When an effect is added or removed, effects that already apply to an object are checked against this addition or removal and react accordingly.

The new effect would first ask the effect group 'can I be added?' and the effects answer on a continuous Yes-No scale. For instance, a 'strength drain' effect would be told no : 10 by a 'prevent strength drain' effect and no : 15 by an 'undead' effect, and yes: 5 by a 'magic weakness' effect, resulting in a total of no : 20. This also makes for a very simple saving throw system.

When an effect is removed or inserted, all effects are asked if they should be re-inserted. Those that are told yes are set aside, then re-inserted (re-insertion attempts are performed until all combinations have been tried and none work). This allows to solve incompatibilities between effects. For instance, if a priest prays for a dwarf's magic immunity to disappear — 'divine magic resistance decrease' — for the wizard's 'bull strength' spell to work on the dward, and the 'divine magic resistance decrease' disappears first, the 'bull strength' is removed and then re-inserted, which fails because of the 'magic immune' property. However, if another magic immunity lowering spell had been cast on the dwarf, it would allow the bull strength to be re-inserted.

Watch out, this can easily turn into an infinite loop because of all the removals and insertions. It should be resolved using constraints, counting the 'yes' and 'no' for all objects, creating a dependency graph and removing the least accepted connex sections.

Note that a permanent effect may be removed. However, it will be re-inserted (or such an attempt will be made) when the removing effect disappears.

A more interesting example:

A steel golem has the following 'innate', permanent properties: physical: poison immune, surnatural: poison breath, physical: steelskin.

A mage casts physical: rust on the golem. The spell has a default no : -100% because it only works on metal, but physical: steelskin tells it yes : 110% because it is made of metal. The result is yes, so the effect is added (and reduces the hit points). All surnatural: effects are removed by a rust spell (which wears away magic properties), so the innate surnatural : poison breath effect is removed . It is re-inserted (with a yes : 75% because it's innate), but is told no : 100% by the rust effect, so it stays off until the rust is removed (at which point the 'innate' property will try to put it back, and succeed). The physical: poison immune effect stays.

Share this post


Link to post
Share on other sites
This is the setup I've considered, and tested out a little bit, but not as of yet used in a big project, so caveat programmer...

Basically, creatures, buildings, tiles, "whatever", hold a structure of effects. The effects are generally a label [ex: "poison","undeath","+str"], an operator [ex: "immunity", "add", "bestow"], and a value.

Effects are granted by various other objects, such as the creature's race ["i am undead.","elves gain infravision"], items ["+str","gain poison immunity"], tiles ["I'm standing in lava.","this tile is under the spell of darkness"], enchantments , and pretty much anything.

The effect stack is then taken and simplified to a root effect.

object.poison.get{
grant poison + immunity poison ->
immunity poison -> return(false);
}

object.str.get{
grant str 16 [by creature natural str] +
grant str 1 [by weapon] +
sap str 4 [by weakness] ->
grant str 13 -> return(13);
}


This really helps when effects die:

object.poison.get{
grant poison [by poison knife strike] + grant poison [by fog of poison] -> grant poison -> return(true);
}

poison knife strike effect expires...

object.poison.get{
grant poison [by fog of poison] ->
grant poison -> return(true);
}


Too many systems I've seen don't handle that case correctly or as directly as this. C#/.NET is tailored nicely to this sort of setup with the ingrained properties and chained delegates.

Share this post


Link to post
Share on other sites
Thanks for the great comments, guys. Okay, I think I'm going to proceed by using the following setup. I need to pound out some more of the design details before it gets implemented, but here's the basic idea.

  • Anything that can cause a change in an object's attributes must implement IEffect. IEffects have four notable features: (1) a timer that fires when it expires (or alternately, at periodic intervals); (2) parameters to describe how powerful the effect is (is it multiplicative [double damage]? additive [+3 to Strength]? some other function?); (3) valid targets (living creatures [Slay Creature spell], armor [Enchant Armor spell]; lava floors [transmute lava to water]; etc.) for the effect; (4) whether the effect stacks with others of its kind. When an IEffect originates, it applies itself to all valid targets within range, but it only activates if the EffectGroup (see below) to which it is being added allows it to.

  • An EffectGroup manages the interactions between effects. EffectGroups are collections of effects on a single target, responsible for determining the net result of all applied effects. Calling EffectGroup.GetEffects() returns a "reduced" List<IEffect> that contains the result of applying all the effects, in the context of that particular object. For instance, consider a player that has the following:

    • nonstacking Strength bonuses of +4, +1, +3
    • stacking Strength bonuses of +3, +5, and +1
    • nonstacking Dexterity bonuses of +2, +3, +6
    • stacking Dexterity penalties of -2, -3

    The nonstacking Strength bonuses result in a +4 bonus to Strength, and the stacking Strength bonuses result in a +9 bonus to Strength, for a total +9 bonus to Strength. (If the nonstacking Strength bonus had been higher than the stacking bonus, that number would have been used instead.) Similarly, the net Dexterity bonus is +1 (+6 from the nonstacking group, and -5 from the penalties).

    The result of calling GetEffects() would therefore be to return a List<IEffect> containing just two IEffects: +9 Strength, +1 Dexterity. These values are the ones that actually modify the object's attributes. The EffectGroup individually determines if each IEffect should be participating in the GetEffects() result or not. For instance, if the object had some kind of property that prevented it from being affected by Strength bonuses (perhaps it's got IncorporealEffect), none of the Strength IEffects participate in the GetEffects() result.

  • Things that could affect other IEffects are themselves IEffects. If a creature is resistant to poison, this would be modeled as a PoisonEffect with a magnitude of -50. If a magic door is difficult to affect with an Unlock spell (which reduces the level of Lockpick skill required to open it; 0 or less means an unlocked door), it gets a LockpickEffect with a magnitude of +100. Note that if a player and not a door had the same LockpickEffect on it, it would increase its Lockpick skill by 100 points; the same attribute means different things depending on the object's context.

  • Activating or deactivating a parent IEffect also does the same to the children, but not the converse. Suppressing, removing, or in some way adjusting a child IEffect doesn't modify the rest of the children. However, if a creature is not affected by a parent IEffect, it is also not affected by any of the children.

  • IEffects have reference semantics, not value semantics. Two IEffects are equal iff they are the same IEffect. Merely having identical targets, magnitudes, and durations is not sufficient; they must literally be the same. Any IEffect that is compared for equality to anything but itself does not return true.



Whew! I think the model is more feasible now, particularly with the introduction of EffectGroups, which takes the headaches out of managing the interactions between various IEffects.

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!