Skip to main content
GameDev.net gamedev.net
🔒 Locked

Decorators - for everything!

Started by Ezbez Aug 27, 2007 at 7:07 PM 12 replies 2.5k views
Original Post
Ezbez
Ezbez
So I finally really got to work on this Workshop. I am implementing both classes and races as decorators (I so do like that pattern) of Characters. So a human wizard would be a Human who's component is a Wizard whose component is a Player (though I might rename that class). This makes some things, such as multi-classing, really easy, almost built-in. Progress is going fine, but I have one issue. If the Player needs to access the, say, Con modifier for the *entire* thing, it can't. It can only access it's own con modifier. For example, if the Fortitude Save property is accessed, the class decorators contribute their saves to the total, and the Player contributes the Constitution modifier. But since it's Constitution could be affected by a race decorator, among other things, the Player somehow needs to access the Constitution of the entire chain of Characters. To counter this I thought of letting each Character (Player and the decorators derive from this) have a reference to the starting Character. I can think of two ways to do this; making it a doubly linked list (sorta), or having them each store a reference to the first Character. I'm not sure I like either of them, but they'd work. For the doubly linked list, I'd just give them each a reference to the decorator before it. For the other method, I'd have to have some way of updating the reference if it changes. I don't have any good ideas on how to do that updating. Another possibility would be having a separate object 'outside' of the chain of Characters. Each of the Characters has a reference to it, and it would have a reference to the first character. It would, in essence, own the chain. What would you do?
Ezbez
Ezbez
I don't fully understand.

Would the race decorator the attributes of a Player rather than the actual player? If so, how would you deal with races that give other, non-attribute bonuses to the player (eg: a Gnome who gets some bonus spells)?

Here, let me see if I can diagram out what I'm thinking you're doing:
.____________________________________.|Player   .________________________. ||         |Race   .____________.   | ||         |       |Attributes  |   | ||         |       .____________.   | ||         |                        | ||         |       .____________.   | ||         |       |SpellList   |   | ||         |       .____________.   | ||         .________________________. |.____________________________________.


Where Player 'owns' Race and Race 'decorates' Attributes and SpellList? Where would a class fit in?
SamLowry
SamLowry
The Decorator DP is very nice but comes with its limitations: decorator N can only work on the information given by decorator N-1 in the chain.

The specifications of how the class/race/... system works does not really fit nicely with this. Some "internal" decorators need access to result from "outer" decorators, etc.

[opinion]
Personally, I would make a separate class hierarchy of modifier "decorators" and have each component (race, class, spells, ...) add modifier-objects to this chain. As it is sometimes important to add effects at a certain place in the chain (e.g. does a spell add 10% to the base strength or the modified strength?), I'd associate a "priority number" to each modifier. So, I'd have a linked list of modifiers sorted by priority. To compute the value of some statistic, you'd ask every modifier in turn how it modifies the statistics and pass the result onto the next modifier in the list. (I haven't read the specifications thoroughly, so it might very well be that another approach would be needed, it's just an example. My point is that "formalizing" the entire thing into a separate system could help.)

BTW, I would go for the linked list approach. The decorator way can work too here, but that be equivalent with taking the modifiers and the linked-list functionality and force them into one abstraction: decorators keep a reference to their "inner component", which can be seen as keeping a link to the next item in a linked list.

The advantage of using a separate class hierarchy (i.e. modifier objects) is that these can be shared by the actual modifying entities (i.e. race, character class, spells). I'd have a "StrengthModifier" which can be used by the Barbarian class (assuming Barbarians have higher-than-average strength), Orc race, and the uhm... SpinachSpell.
[/opinion]
Telastyn
Telastyn
Yes, to be a little more accurate I do use something more akin to SamLowry's description. The design is more like:

    Apply Race -----|                                     VStrength: [base][ + racial][ + somebonus]


The race isn't a decorator, it's an object which defines a transition. An operator. Characters can gain the race or lose the race which adds/removes the appropriate bonuses.


Pardon the hackishness and the little cheating, but this is the module that handles almost all of the numerical values within a Character:

using System;using System.Collections.Generic;using System.Text;namespace DnD {    public class CumulativeValue{        public event Game.PropertyChange<GameObj, int> OnChange = NoopPropertyChange<GameObj, int>.Common;        public readonly string Name;        public readonly GameObj Owner;        public readonly int? MinConstraint = null;        public readonly int? MaxConstraint = null;        protected List<Modifier> Parts = new List<Modifier>();        public int Value {            get {                int rtn = 0;                foreach (Modifier Bit in Parts) {                    if (Bit.Classification == Modifier.Flavor.None || Bit.Classification == Modifier.Flavor.None) {                        rtn += Bit.Value(this);                    } else {                        bool AlreadyDone = false;                        for (int x = 0; x < Parts.IndexOf(Bit); ++x) {                            if (Bit.Classification == Parts[x].Classification) {                                AlreadyDone = true;                                x = Parts.IndexOf(Bit);                            }                        }                        if (!AlreadyDone) {                            rtn += MaxModifier(Bit.Classification).Value(this);                        }                    }                }                if (MinConstraint.HasValue && rtn < MinConstraint.Value) { rtn = MinConstraint.Value; }                if (MaxConstraint.HasValue && rtn > MaxConstraint.Value) { rtn = MaxConstraint.Value; }                return (rtn);            }        }        public Modifier MaxModifier(Modifier.Flavor constraint) {            if(constraint == Modifier.Flavor.None){ return(null);}            Modifier rtn = null;            for (int x = 0; x < Parts.Count; ++x) {                if (Parts[x].Classification == constraint) {                    if (rtn == null) {                        rtn = Parts[x];                    } else {                        if (Math.Abs(rtn.Value(this)) < Math.Abs(Parts[x].Value(this))) {                            rtn = Parts[x];                        }                    }                }            }            return (rtn);        }        protected void DynamicChangeCatcher(DynamicModifier src, int from, int to){            OnChange(Owner,Value-to+from,Value);        }        internal void Add(Modifier what) {            int val = Value;            Parts.Add(what);            if (what is DynamicModifier) {                (what as DynamicModifier).OnChange += DynamicChangeCatcher;            }            if (val != Value) {                OnChange(Owner, val, Value);            }        }        internal bool Remove(Modifier what) {            int val = Value;            if (Parts.Remove(what)) {                if (what is DynamicModifier) {                    (what as DynamicModifier).OnChange -= DynamicChangeCatcher;                    (what as DynamicModifier).Cleanup();                }                if (val != Value) {                    OnChange(Owner, val, Value);                }                return (true);            }            return (false);        }        internal bool Remove(GameObj src) {            bool rtn = false;            for (int x = 0; x < Parts.Count; ) {                if (Parts[x].Source == src) {                    Remove(Parts[x]);                    rtn = true;                } else {                    ++x;                }            }            return (rtn);        }        public List<Modifier> Details {            get {                return (new List<Modifier>(Parts));            }        }        public List<Modifier> EffectiveDetails {            get {                List<Modifier> rtn = new List<Modifier>();                for (int x = 0; x < Parts.Count; ++x) {                    if (Parts[x].Classification == Modifier.Flavor.None || Parts[x].Classification==Modifier.Flavor.Dodge) {                        rtn.Add(Parts[x]);                    } else {                        Modifier tmp = MaxModifier(Parts[x].Classification);                        if (!rtn.Contains(tmp)) {                            rtn.Add(tmp);                        }                    }                }                return (rtn);            }        }        public CumulativeValue( string name, GameObj owner, int? min, int? max){            Name = name;            Owner = owner;            MinConstraint = min;            MaxConstraint = max;        }        public CumulativeValue(string name, GameObj owner) : this(name, owner, null, null) { }        public override string  ToString(){                       string buffer = "";            if (Name.Length > 15) {                buffer = string.Format("{0}\t{1} = ", Name, Value);            } else if (Name.Length > 7) {                buffer = string.Format("{0}\t\t{1} =", Name, Value);            } else {                buffer = string.Format("{0}\t\t\t{1} =", Name, Value);            }            bool plus = false;            foreach (Modifier Bit in Parts) {                if (plus) {                    buffer += " + ";                } else {                    buffer += " ";                    plus = true;                }                buffer += string.Format("{0} ({1}[{2}])", Bit.Value(this), Bit.Classification, Bit.Source.ID);            }                        return (buffer);        }    }    public abstract class Modifier {        public abstract int Value(CumulativeValue context);        public readonly GameObj Source;        public readonly Flavor Classification;        public enum Flavor{            None,            Base,            Racial,            Strength,            Intelligence,            Wisdom,            Dexterity,            Constitution,            Charisma,            Feat,            Dodge,            Size        }        public static Flavor AbilityToFlavor(Character.Ability what) {            switch (what) {                case Character.Ability.Strength:                    return (Flavor.Strength);                case Character.Ability.Intelligence:                    return (Flavor.Intelligence);                case Character.Ability.Wisdom:                    return (Flavor.Wisdom);                case Character.Ability.Dexterity:                    return (Flavor.Dexterity);                case Character.Ability.Constitution:                    return (Flavor.Constitution);                case Character.Ability.Charisma:                    return (Flavor.Charisma);            }            return (Flavor.None);        }        protected Modifier(GameObj src, Flavor classification) { Source = src; Classification = classification; }    }    public class StaticModifier : Modifier {        protected int internalValue;        public override int Value(CumulativeValue context) { return (internalValue); }        public StaticModifier(GameObj src, int val, Flavor classification) : base(src, classification) { internalValue = val; }    }    public abstract class DynamicModifier : Modifier {        public delegate void ChangeType(DynamicModifier src, int from, int to);        public event ChangeType OnChange = delegate(DynamicModifier src, int from, int to) { };        protected void TriggerChange(int from, int to){ OnChange(this,from,to);}        protected int ChangeCount {            get {                return (OnChange.GetInvocationList().GetLength(0));            }        }        public virtual void Cleanup() { }        protected DynamicModifier(GameObj src, Flavor classifiction) : base(src, classifiction) { }    }    public class VariableModifier : DynamicModifier {        protected int internalValue = 0;        public override int Value(CumulativeValue context) {            return (internalValue);        }        public void SetValue(int to) {            int tmp = internalValue;            internalValue = to;            TriggerChange(tmp, to);        }        internal VariableModifier(GameObj src, Flavor classification) : base(src, classification) { }    }    public class StatModifier : DynamicModifier {        public readonly CumulativeValue Stat;        public override int Value(CumulativeValue context) {            return (Stat.Value / 2 - 5);        }        protected void  ChangeCatcher(GameObj src, int from, int to){            if(Character.StatValueToModifier(from)!=Character.StatValueToModifier(to)){                TriggerChange(Character.StatValueToModifier(from),Character.StatValueToModifier(to));            }        }        public override void Cleanup() {            if (ChangeCount == 0) {                Stat.OnChange -= ChangeCatcher;            }        }        public StatModifier(GameObj src, Flavor classification, CumulativeValue inStat)            : base(src, classification) {            Stat = inStat;            Stat.OnChange += ChangeCatcher;        }    }}


[Edited by - Telastyn on August 28, 2007 5:16:53 PM]
Ezbez
Ezbez
I'm beginning to see why decorators aren't so great in the way that I'm using them. For classes, and race it's no big deal. But when you start adding in feats, spell effects, diseases, abilities, magical items, and everything else it starts seeming like madness. I'd have to update all references to the character every time that a new effect was added or removed, among other things.

My new thoughts:

All Feats, spells, and whatnot have an Effect. Each Effect is Apply()ed to the target Character1, which lets them apply their modifiers to all the properties of the Character. Each property (by this I mean attributes, saves, attack bonuses, etc.) will have a list of possible modifiers (eg: Racial, Armor, Natural, Size, Misc) and each of those modifiers will itself be a list 2. When an Effect is applied, it can add an integer to the a specific modifier. When the Effect goes away, it removes the integer from the list. The total value of the property is the sum of the highest value in each modifier list plus the sum of all in the Misc list plus an optional base value (eg: 10 for armor). All modifiers that need to stack go in the Misc list (it's special!) and all ones that overlap go in their typical list3.

How's it sound? Is it improvement? I think it's a lot more like what you guys use now.

1. How can I deal with future spells that will affect items, locations, or other things? I'd start off by making all possible targets derive from an Object, but I then have no way of, well, doing much to it. Visitor pattern?

2. Is this overkill? I can't think of any other way of reverting back to the next highest of a specific modifier. When my Natural Armor +3 spell wears off, I want my Natural Armor +2 Amulet to kick into affect, but I don't want them to stack.

3. There are modifiers that need to stack, right? I can't think of any (nor find any in the Does it Stack? articles linked to in another thread here), but I swear that there are a few. Either way, having a Misc modifier that stacks might be a good idea for those odd, rare bonuses that two things in all of DnD give.
Telastyn
Telastyn
Quote:
Original post by Ezbez
My new thoughts:

All Feats, spells, and whatnot have an Effect. Each Effect is Apply()ed to the target Character1, which lets them apply their modifiers to all the properties of the Character. Each property (by this I mean attributes, saves, attack bonuses, etc.) will have a list of possible modifiers (eg: Racial, Armor, Natural, Size, Misc) and each of those modifiers will itself be a list 2. When an Effect is applied, it can add an integer to the a specific modifier. When the Effect goes away, it removes the integer from the list. The total value of the property is the sum of the highest value in each modifier list plus the sum of all in the Misc list plus an optional base value (eg: 10 for armor). All modifiers that need to stack go in the Misc list (it's special!) and all ones that overlap go in their typical list3.

How's it sound? Is it improvement? I think it's a lot more like what you guys use now.


Sounds quite similar. There are though many ways to do something. One thing to beware of is the "it's special" or similar exceptions to a design. They're usually a good indicator that the design could use improvement. Sometimes that is impossible though...

Quote:

1. How can I deal with future spells that will affect items, locations, or other things? I'd start off by making all possible targets derive from an Object, but I then have no way of, well, doing much to it. Visitor pattern?


Possibly. At this point I'm just doing icky casting to get from the generic target into what the spell expects.

Quote:

2. Is this overkill? I can't think of any other way of reverting back to the next highest of a specific modifier. When my Natural Armor +3 spell wears off, I want my Natural Armor +2 Amulet to kick into affect, but I don't want them to stack.


For a simple learning exercise? Perhaps. Even for not learning it might be.

Quote:

3. There are modifiers that need to stack, right? I can't think of any (nor find any in the Does it Stack? articles linked to in another thread here), but I swear that there are a few. Either way, having a Misc modifier that stacks might be a good idea for those odd, rare bonuses that two things in all of DnD give.


Here right at the top of the page. In my code above, I just make an exception in the property that totals the Value.

Ezbez
Ezbez
Quote:
Original post by Telastyn
For a simple learning exercise? Perhaps. Even for not learning it might be.


So should I just have them all stack? It would be the easiest other thing that I can think of.

Quote:
Here right at the top of the page. In my code above, I just make an exception in the property that totals the Value.


Great! Thanks.
SiCrane
SiCrane
I took a different tack. All numeric values are associated with a string that describe them. Then modifiers contain their target name and bonus type, and the modifiers the all get saved in giant dictionary the character holds. And since modifiers are numeric values, they are also associated with a string, which means they can be target of modifiers, etc. Relevant bits of code:
  internal delegate int ValueDelegate(Character ch);    enum BonusType {    Unnamed,    Ability,    Alchemical,    Armor,    Circumstance,    Competence,    Deflection,    Dodge,    Enhancement,    Insight,    Luck,    Morale,    NaturalArmor,    Profane,    Racial,    Resistance,    Sacred,    Shield,    Size      }    internal class Value {    private ValueDelegate base_value;    internal string name;        internal Value(string name, int v) {      this.name = name;      base_value = delegate(Character ch) { return v; };    }    internal Value(string name, Value v) {      this.name = name;      base_value = delegate(Character ch) { return v.Val(ch); };    }    internal Value(string name, ValueDelegate v) {      this.name = name;      base_value = v;    }        internal int Val(Character ch) {      int [] bonus_types = new int[Enum.GetValues(typeof(BonusType)).Length];      for (int i = 0; i < bonus_types.Length; i++) {        bonus_types = 0;      }          foreach (Modifier m in ch.modifiers.Values) {        if (m.target == this.name) {          int modifier_value = m.Val(ch);          switch (m.type) {            case BonusType.Unnamed:            case BonusType.Circumstance:            case BonusType.Dodge:              bonus_types[(int)m.type] += modifier_value;              break;            default:              if (bonus_types[(int)m.type] < modifier_value)                bonus_types[(int)m.type] = modifier_value;              break;          }        }      }      int accumulator = base_value(ch);      foreach (int i in bonus_types) {        accumulator += i;      }          return accumulator;    }    internal int RawValue(Character ch) {      return base_value(ch);    }  }    internal class Modifier : Value {    internal Modifier(string name,                       int v,                       BonusType type,                       string source,                       string target)      : base(name, v) {      this.type = type;      this.source = source;      this.target = target;    }    internal Modifier(string name,                      Value v,                      BonusType type,                      string source,                      string target)      : base(name, v) {      this.type = type;      this.source = source;      this.target = target;    }    internal Modifier(string name,                      ValueDelegate v,                      BonusType type,                      string source,                      string target)      : base(name, v) {      this.type = type;      this.source = source;      this.target = target;    }      internal BonusType type;    internal string source;    internal string target;  }    internal class Character {    internal Dictionary<string, Value> values = new Dictionary<string,Value>();    internal Dictionary<string, Modifier> modifiers = new Dictionary<string,Modifier>();    private void add_value(Value v) {      values.Add(v.name, v);    }        private void add_modifier(Modifier m) {      values.Add(m.name, m);      modifiers.Add(m.name, m);    }  }

Since modifier sources are also tracked, you can remove them when the source ends, such as when the spell that grants a bonus to natural armor goes away. That won't change the existing natural armor bonus from a magic item, since the source would be different. Though, I believe technically in the 3.5 rules, the natural armor bonus is a intrinsic value of the character and an amulet of natural armor adds an enhancement bonus to the natural armor bonus.
Ezbez
Ezbez
Can you explain the purpose of ValueDelegates? I think I understand the rest.

I really like how Modifiers are also values. That would be a smart thing for me to do. This thing is starting to piece together.
SiCrane
SiCrane
The point behind value delegates is that values come in a few different flavors: some are just straight numbers. For example, the enhancement bonus to hit from a masterwork weapon is always +1. However, some values depend on other values. For example, the bonus to hit in a melee attack from strength depends on the strength modifier.

So for the first modifier, I'd generate it in code something like:
add_modifier(new Modifier("masterwork enhancement bonus",                          +1,                          BonusType.Enhancement,                          "masterwork weapon",                          "melee weapon attack bonus"));

The +1 propagates up the function call chain until it hits the Value constructor, where a delegate is constructed that always returns 1.

For the second case, since it operates as a function of another value, I can't just plug in a +1 or +3. In this case I want it to depend on a function of strength.
add_modifier(new Modifier("strength bonus to melee attack",                          delegate(Character ch) {                            return Rules.ability_modifier(ch.values["strength"].Val(ch));                          },                          BonusType.Ability,                          "intrinsic property",                          "melee attack bonus"));

Of course, there are some values that are of intermediate complexity.
add_value(new Value("melee weapon attack bonus", values["melee attack bonus"]));

In this case I create a new value that depends on an existing value. This propagates to the second Value constructor which just calls Val() on the value. So that a "melee weapon attack bonus" is equal to the "melee attack bonus" plus or minus whatever other modifiers that only apply to the "melee weapon attack bonus" such as the "masterwork enhancement bonus" or possibly the "Weapon Focus bonus", etc.

Ezbez
Ezbez
Thanks a bunch! I dissected your code, SiCrane, and found that I really liked it's approach, especially since I don't need to derive a base class for pretty much every single modifier I need. I had to read up a bit on delegates, but they make sense to me since I studied in the Scheme workshop for a while.

So I hope that you don't mind that my code now looks suspiciously like yours. But don't worry I wrote it from scratch myself; I'm not just a copy-and-paste monkey. I'm a read, understand, and rewrite monkey. [grin]
SiCrane
SiCrane
Anonymous delegates are probably my favorite addition to C# 2.0.

Keep in mind that sometimes you might want to derive from Value or Modifier if you want to access a value that normally gets squirreled away inside an anonymous delegate. For example, for the attribute gain for every fourth level you may want to increase the base value of an attribute. (Alternately, you can do what I did, and create a "base_str" value and a "str" value, and level gains create modifiers that affect "base_str" and things like magic items create modifiers that affect "str".)

But once you wrap your head around the Value/Modifier pair, consider this guy:
  internal delegate bool ConditionDelegate(Character ch, string [] descriptors);  internal class ConditionalModifier : Value {    internal ConditionalModifier(string name,                                 ValueDelegate value_delegate,                                 BonusType type,                                 string source,                                 string target,                                 ConditionDelegate condition_delegate)      : base(name, value_delegate) {      this.type = type;      this.source = source;      this.target = target;      this.condition = condition_delegate;    }        internal ConditionalModifier(string name,                                 int v,                                 BonusType type,                                 string source,                                 string target,                                 string condition)      : this(name,             delegate(Character ch) {               return v;             },             type,             source,             target,             delegate(Character ch, string [] descriptors) {               foreach (string s in descriptors) {                 if (s == condition) return true;               }               return false;             }) {}    // other constructors omitted    internal BonusType type;    internal string source;    internal string target;    internal ConditionDelegate condition;  }

Ezbez
Ezbez
Okay I'll look at that part in a bit.

For what I called 'built-in' values (eg: the attributes, BAB, saves) I decided to give the Character class an actual internal int that stores them (I actually called them baseStr and the like). Then, there's a Value that just accesses them. Basically, the same thing as you, but base_str is an int not a Value.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.