Pyrel dev log, part 5

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • Magnate
    Angband Devteam member
    • May 2007
    • 5110

    Pyrel dev log, part 5

    Originally posted by Kinematics
    Was doing some thinking regarding the affixType, and why it'd be wanted (it's replacing the hackish handling of tightly coupling the item variant to the item type)
    No, I don't think so - at least, that's not why I originally created affixTypes. The idea of affixTypes is to make themed items and affix genera work better. So themes are flavourful (arcane, holy, etc.) rather than code-friendly. I think it could be dangerous to view affixType as the replacement for coupling item to item type.
    and realized a change to make magic items purely affix-based introduces another problem: how do you associate them with a specific flavor?

    The flavors are currently tied to the factory, and thus can always be put on the same item variant each time it's created since there's one factory per variant. However if you get rid of all the ring variants and only have a single ring object (and thus only a single ring factory), you now need to add an extra mapping between the affix chosen and the flavor that goes with it. Not difficult, but something to be aware of.
    Yes, this is a fair point. As you say, not impossible, but something to bear in mind during the testing of this approach.
    Also, allocator rules:

    {"index": 261,
    "templates": "scroll",
    "nameInfo": {"variantName": "Teleportation"},
    "allocatorRules": [{"commonness": 40, "minDepth": 10, "maxDepth": -1, "piling": {"chance": 25, "pileSize": "2d1"}}]},

    {"index": 420,
    "templates": "staff",
    "nameInfo": {"variantName": "Teleportation"},
    "allocatorRules": [{"commonness": 50, "minDepth": 20, "maxDepth": -1}]},

    {"index": 247,
    "templates": "amulet",
    "nameInfo": {"variantName": "Teleportation"},
    "flags": ["TELEPORT", "EASY_KNOW"],
    "allocatorRules": [{"commonness": 20, "minDepth": 10, "maxDepth": 40}]},

    {"index": 216,
    "templates": "ring",
    "nameInfo": {"variantName": "Teleportation"},
    "mods": [{"bonus": "2", "flags": ["SPEED"]}],
    "flags": ["TELEPORT", "EASY_KNOW"],
    "allocatorRules": [{"commonness": 20, "minDepth": 5, "maxDepth": 50}]},


    For the same affix name, every one of them has different rarities and depth ranges, plus, scrolls need a piling value. Most of it is easy enough, since the same fields are available in item allocators and affix allocators, but you'd have to add piling rules into affix allocator rules, which seems a little odd, though necessary if you divorce the item allocation from the variants.
    You're right that that is odd - and worth thinking about. But the different rarities and depths are already catered for in the affix handling: for the same affix you can specify different allocator rules for different item categories.
    The other issue is the mods and flags per item. I'm not sure those are easily translatable to the affix environment, since the affix is typically just a single effect that you can specify different rules for when it shows up on different items. The above, however, are two that are effects, and two that are flags, and one that has a secondary mod attached.
    Affixes provide one thing each, whether that's a mod (pval), a flag (binary) or an effect (activation). So the ring and amulet have the binary flag for uncontrolled random teleportation (one affix), and the scroll and staff have the activation for teleportation (a different affix). To be done neatly, these affixes would need different name (I'd go for "of Random Teleportation" for the first).

    The ring would need two affixes to be generated - the random teleportation affix and the speed affix. This is already achievable via themes - if a ring gets teleportation, it automatically gets +2 speed. Theme names are independent of affix names, so that could solve the naming problem too (if we let teleportation amulets also get +2 speed ...).

    Thanks for the thoughts - keep them coming.
    "Been away so long I hardly knew the place, gee it's good to be back home" - The Beatles
  • Derakon
    Prophet
    • Dec 2009
    • 9022

    #2
    Thinking about this whole affix-drives-the-name thing, I really think we are overcomplicating magical item creation. Just because rings and amulets and potions and scrolls and so on could be handled by the affix system doesn't mean that this is the right way to do it. In particular, adding a new magical item becomes more complicated under such a scheme:

    * You need to understand not just the object data files but also the affix files.
    * Per what you just said, Magnate, you would likely also need to understand the theme system.
    * Adding any new affix makes all of the other affixes marginally less common (conflation of base object and affix allocation rates).

    All of this so, what, we can avoid having a second naming function? Is it really worth it? What would be so bad about having an alternate naming function for flavored items that just says "[Flavor] BaseType of SubType"?

    I think we've gotten a bit too embedded in the problem; step back a bit, and consider whether the solution being proposed is really the right step forward.

    Also, Magnate, use a better reader.

    Comment

    • Nick
      Vanilla maintainer
      • Apr 2007
      • 9637

      #3
      Originally posted by Derakon
      Also, Magnate, use a better reader.
      The internet reached perfection twenty years ago, and it's been downhill since then
      One for the Dark Lord on his dark throne
      In the Land of Mordor where the Shadows lie.

      Comment

      • Kinematics
        Apprentice
        • Feb 2013
        • 72

        #4
        Originally posted by Magnate
        Originally posted by Kinematics
        Was doing some thinking regarding the affixType, and why it'd be wanted (it's replacing the hackish handling of tightly coupling the item variant to the item type)
        No, I don't think so - at least, that's not why I originally created affixTypes.
        It's not why affixTypes were originally created, but it's a significant element of how the naming code is using them. And I was noting that variant names are the 'hackish' implementation, and that switching solely to affixes would be a cleaner approach. However, for that to work, the affixType has to cooperate.

        I agree with Derakon, though, that this seems to be heading in the direction of making item definition far more complicated.


        Another example issue with the difference between the affix and the item:
        Code:
        {"index": 190,
        "templates": "ring",
        "nameInfo": {"variantName": "Strength"},
        "mods": [{"bonus": "1+M5", "flags": ["STR"]}],
        "flags": ["SUST_STR", ""],
        "allocatorRules": [{"commonness": 50, "minDepth": 30, "maxDepth": -1}]},
        This would allow a ring with a strength bonus between +1 and +6.

        Code:
          {
            "index": 183, 
            "mods": [
              {
                "bonus": "d2", 
                "minimum": "1", 
                "flags": [
                  "STR"
                ]
              }
            ], 
            "allocatorRules": [..snip..], 
            "name": "Strength",
            "affixType": "physical",
            "genus": "the Body"
          },
        The affix gives a bonus between 1 and 2.

        Unless you want to create yet another name for a slight difference in the value generation, you'd need to modify the affix structure so that its properties could be specified per item type it got applied to (eg: 1d2 for armor, 1+M5 for rings).
        Last edited by Kinematics; March 11, 2013, 22:32.

        Comment

        • Kinematics
          Apprentice
          • Feb 2013
          • 72

          #5
          Originally posted by Derakon
          While there's certainly the potential for making them more complicated, we need to make certain that simple procs only need simple definitions and simple bits of code to work.
          Simple is good, but we don't want it to be so simple that it's awkward to get even mildly complex tasks done. When you want to do less, it shouldn't get in the way, but when you want to do more, it should be easy to grow. Procs seem to be intended to take on a huge portion of the workload for game behavior, so I'm trying to see what sorts of behaviors are easy or difficult to apply.

          Regarding your specific example of handling the weight of the player's pack via procs, while you could do that I don't think it's a very good idea. There's so many potential ways for an item to no longer be in the pack (dropped, equipped, consumed, destroyed, stolen, etc.), and every single one would need to invoke the correct proc trigger. That's an awful lot of triggers for a single proc definition. Moreover, every single item in the game is going to have the same result for them weight-wise (i.e. add/remove weight to/from the player), so we don't really gain anything by "proc-izing" the behavior. This is one bit of game logic that I'm happy to let the engine handle. Procs could, however, handle what to do when the player is overweight.
          Well, it wasn't so much written as "how should it be done?", as "what sorts of triggers could possibly be needed and how/when would they be used?". As you say, there's lots of different ways for an item to be affected; I was trying to get a better grasp of what all those might be, primarily to see what implications it would have for how the procs need to work.

          Finally, I'm not averse to allowing a Proc definition to have multiple trigger conditions, but recognize that the trigger condition also determines the parameters that the trigger() function receives. E.g. an "item use" trigger provides the item, the user, and the GameMap, while a "creature death" trigger would provide the creature, the killer, and the GameMap. Probably there would be many trigger conditions that pass the same parameter sets, but there'd be nothing stopping you from creating a Proc whose trigger() function would crash when triggered the "wrong way". I think we'd need to resolve this issue before we can consider multiple-trigger Procs to be a finished feature.
          Good point, on the different parameter lists. I need to give this some more thought.

          Comment

          • Derakon
            Prophet
            • Dec 2009
            • 9022

            #6
            Originally posted by Kinematics
            Simple is good, but we don't want it to be so simple that it's awkward to get even mildly complex tasks done. When you want to do less, it shouldn't get in the way, but when you want to do more, it should be easy to grow. Procs seem to be intended to take on a huge portion of the workload for game behavior, so I'm trying to see what sorts of behaviors are easy or difficult to apply.
            This is certainly true. At the same time I don't think we should expect to do all game logic via Procs. I'd say that if what you want to do can't be reasonably encapsulated by a single Proc, then either you should move that behavior into the engine, or you should add a new trigger to enable the Proc properly.

            Comment

            • Kinematics
              Apprentice
              • Feb 2013
              • 72

              #7
              It would be easy to have a function in the base Proc class that wraps the distinction between single and multiple triggers, so the external code wouldn't need to know about that. However with different parameters you'd have to also do some kind of branching function, like:

              Code:
              class myProc(proc.Proc):
                  def trigger(self, trigger, *args):
                      if trigger == "condition 1":
                          self.trigger1(args)
                      elif trigger == "condition 2":
                          self.trigger2(args)
                          
                  def trigger1(self, *args):
                      ~stuff
              
                  def trigger2(self, *args):
                      ~stuff
              And to make it generically consistent you'd have to call every proc with the trigger that triggered it as the first parameter, which many/most procs wouldn't care about.

              You could get around some of that with an internal mapping dict, but that makes it more difficult to define simple stuff. Unless....

              [Note: this may not actually work; writing down my thought process as I work through details]

              Add in a util curry class (since Python doesn't have it natively), then you could add something like this to the base class (subclasses don't have to touch it at all, except maybe triggerMaps):

              Code:
              class proc():
                  init
                      self.triggerMaps = {}
                      
                  def getTriggerFunction(triggerCondition):
                      # Validate argument
                      if not isinstance(triggerCondition, basestr):
                          return None
                      # If only provided a single string as a trigger condition
                      if isinstance(self.triggerConditions, basestr):
                          if triggerCondition == self.triggerConditions:
                              return self.trigger
                      # If provided a list of strings as trigger conditions,
                      # create a curried version of the function to return.
                      elif isinstance(self.triggerConditions, list):
                          if triggerCondition in self.triggerConditions:
                              return curry(self.trigger, triggerCondition)
                      # If provided a dict of trigger conditions, return the
                      # function defined for the provided triggerCondition
                      # (function value kept in class-defined triggerMaps).
                      elif isinstance(self.triggerConditions, dict):
                          if triggerCondition in self.triggerConditions:
                              return self.triggerMaps[triggerConditions[triggerCondition]]
                      # If none of the above are true, return None.
                      return None
              Then you could define a proc with any of

              Code:
                        {"name": "remove weight", "triggerCondition": "onDrop"},
                        {"name": "remove weight", "triggerCondition": ["onDrop", "onWaggle"]},
                        {"name": "remove weight", "triggerCondition": {"onDrop": "wiggle"}},
              And it would be called from the item class like so:

              Code:
                  ## Trigger procs that have the specified trigger.
                  def triggerProcs(self, user, gameMap, trigger, *args, **kwargs):
                      triggerProcs = [p.getFunction(trigger) for p in self.procs]
                      for proc in triggerProcs:
                          if proc:
                              proc(self, user, gameMap, *args, **kwargs)
              Note: doesn't include the pre-action/post-action separation here.

              The call works the same regardless of the proc trigger condition type.

              Unfortunately, while the -call- works, the code execution point has problems. To allow for lists of trigger actions, and assuming we want to allow the proc to know what triggered it, you'd need both

              def trigger(self, *args):
              and
              def trigger(self, triggerCondition, *args):

              defined, since you don't know if/when a list will ever be used on the calling side. I think we can bypass this, though.

              First: If triggerMaps is empty, then we don't care what trigger condition called us; we'll always use the standard trigger() function. As such, the trigger condition value is irrelevant.
              Second: If triggerMaps has values, we can use that to map the list value to a specific function, while not needing to know explicitly what value was passed in. So again, we can get away without needing to define a triggerCondition parameter on trigger().

              Code:
              class proc():
                  init
                      self.triggerMaps = {}
                      
                  def getTriggerFunction(triggerCondition):
                      # Validate argument
                      if not isinstance(triggerCondition, basestr):
                          return None
                      # If only provided a single string as a trigger condition
                      if isinstance(self.triggerConditions, basestr):
                          if triggerCondition == self.triggerConditions:
                              return self.trigger
                      # If provided a list of strings as possible trigger conditions,
                      # use the function pointed to via triggerMaps.
                      elif isinstance(self.triggerConditions, list):
                          if triggerCondition in self.triggerConditions:
                              if self.triggerMaps:
                                  if triggerCondition in self.triggerMaps:
                                      return self.triggerMaps[triggerCondition]
                                  elif "*" in self.triggerMaps:
                                      return self.triggerMaps["*"]
                                  else:
                                      return self.trigger
                              else:
                                  return self.trigger
                      # If provided a dict of trigger conditions, return the
                      # function defined for the provided triggerCondition
                      # (function value kept in class-defined triggerMaps).
                      elif isinstance(self.triggerConditions, dict):
                          if triggerCondition in self.triggerConditions:
                              mappedTrigger = triggerConditions[triggerCondition]
                              if self.triggerMaps:
                                  if mappedTrigger in self.triggerMaps:
                                      return self.triggerMaps[mappedTrigger]
                                  elif "*" is self.triggerMaps:
                                      return self.triggerMaps["*"]
                                  else:
                                      return self.trigger
                              else:
                                  return self.trigger
                      # If none of the above are true, return None.
                      return None

              Now in the object (or whatever) definition, you can provide a specific trigger condition, a list of trigger conditions, or a mapping of trigger conditions, and any one would work.

              If the proc only defines a trigger() function, it can be called if the proc was defined with either a simple string for a trigger condition, or a list. If it's a list, then any item in the list will call trigger(), and no distinction will be made based on the actual trigger condition.

              If the proc defines a triggerMap, then creating the proc with either a list or a dict will cause it to try to look up the value in the map to determine which function to actually call for each available trigger condition (the dict maps the triggerCondition argument to another value first). If it's listed explicitly, it calls the specified function; if the triggerMap includes a "*" (general glob indicator), it will call that for any condition not otherwise specified. Otherwise it will simply call the default trigger() function.


              That would allow any degree of simplicity or complexity on the calling side of things. What about different numbers of parameters, though?

              Well, any given called proc has to, by definition, handle the expected parameters for a given event. If a "get name" event is going to call all procs with arguments of an item and a list of callbacks, then every proc that is intended to be used for a "get name" event must have a function signature that can handle that.

              The the "get name" proc was altered so that it could handle "get item name" and "get creature name" events, and (for the sake of example) if if the creature event passed in the creature, its position, and a list of callbacks, then you'd need to handle those differences.

              The triggerMap could map it as: {"get item name": self.getItemName, "get creature name": self.getCreatureName}. Then when a "get creature name" event happened, it would return a function that had a signature that could return appropriate values.

              Then you could do something like add a petrification curse that created a proc of {"name": "get name", "triggerCondition": {"get creature name": "get item name"}}, mapping a request for a creature's name to the "get item name" function instead, making it look like a real item. (Could maybe do similarly with mimics.)

              That of course leads to the question of overriding a proc instead of just supplementing it, since by default such an additional "get name" function would be called in addition to the existing one, leading to general brokenness.


              So we still need to handled triggering vs trigger, and how to override (temporarily, without removing) an existing proc. Will get to that in the next post.
              Last edited by Kinematics; March 11, 2013, 23:54.

              Comment

              • Kinematics
                Apprentice
                • Feb 2013
                • 72

                #8
                Quick revision: the triggerMaps code can be simplified, since it's really just a repetition of the same logic.

                Code:
                class proc():
                    init
                        self.triggerMaps = {}
                        
                    def getTriggerFunction(triggerCondition):
                        # Validate argument
                        if not isinstance(triggerCondition, basestr):
                            return None
                            
                        useTrigger = None
                        
                        # If initialized with a simple string, that's all we
                        # need to check.
                        if isinstance(self.triggerConditions, basestr):
                            if triggerCondition == self.triggerConditions:
                                useTrigger = triggerCondition
                        # If initialized with a list of strings, check if the
                        # triggerCondition is in that list.
                        elif isinstance(self.triggerConditions, list):
                            if triggerCondition in self.triggerConditions:
                                useTrigger = triggerCondition
                        # If initialized with a dict of trigger conditions, check
                        # if the triggerCondition is in the keys; if so, use
                        # the value mapping.
                        elif isinstance(self.triggerConditions, dict):
                            if triggerCondition in self.triggerConditions:
                                useTrigger = triggerConditions[triggerCondition]
                                
                        # Whatever we ended up with as the trigger, check to
                        # see if it's present in triggerMaps; if so, use the
                        # specified function.  Otherwise just use the default.
                        if useTrigger:
                            if useTrigger in self.triggerMaps:
                                return self.triggerMaps[useTrigger]
                            else:
                                return self.trigger
                        else:
                            # If none of the above are true, return None.
                            return None
                Last edited by Kinematics; March 12, 2013, 01:21. Reason: name glob is redundant

                Comment

                • Magnate
                  Angband Devteam member
                  • May 2007
                  • 5110

                  #9
                  Originally posted by Derakon
                  Thinking about this whole affix-drives-the-name thing, I really think we are overcomplicating magical item creation. Just because rings and amulets and potions and scrolls and so on could be handled by the affix system doesn't mean that this is the right way to do it. In particular, adding a new magical item becomes more complicated under such a scheme:

                  * You need to understand not just the object data files but also the affix files.
                  * Per what you just said, Magnate, you would likely also need to understand the theme system.
                  * Adding any new affix makes all of the other affixes marginally less common (conflation of base object and affix allocation rates).

                  All of this so, what, we can avoid having a second naming function?
                  No. I have wanted to move rings and amulets to affix-based generation for as long as I've thought about affixes. This is just a handy incentive to get started. It is true that this doesn't necessarily mean that consumables also need to move to the same system, but I can see advantages beyond the naming issue.

                  I don't think understanding affixes and themes will be that complicated in the end. I reckon I'll be able to write a simple guide to adding a new item in a paragraph. Better still, I'll be able to write a guide to adding a whole new TYPE of item in two paragraphs - something which would take pages in V and quite a bit in Pyrel as it stands.

                  Adding a new affix only makes "marginally less common" those affixes which are legal for the same item at the same depth, so I don't think it's a huge issue.
                  "Been away so long I hardly knew the place, gee it's good to be back home" - The Beatles

                  Comment

                  • Kinematics
                    Apprentice
                    • Feb 2013
                    • 72

                    #10
                    pre-event/post-event handling:

                    I think the triggerMap setup allows fairly simple access to pre-event handling.

                    triggerMap = {"prepare to cast": self.prepareToCast, "cast": self.trigger}

                    Then you can add proc definitions

                    item 1: {"name": "casting", "triggerCondition": "cast"},
                    item 2: {"name": "casting", "triggerCondition": ["prepare to cast", "cast"]},
                    item 3: {"name": "casting", "triggerCondition": ["boom"]},
                    item 4: {"name": "casting", "triggerCondition": {"preboom": "prepare to cast", "boom": "cast"}},

                    Any proc can then (if applicable) encapsulate logic for pre-event stuff without any single proc instance needing to use it, and without needing no-op functions, simply by including that event type in its trigger map.

                    Aside: note that even though (or because) "boom" in item 3 isn't in the trigger map, it will still get mapped to the proc's trigger() function. In item 4, it explicitly maps some other events to the proc's functions.
                    Last edited by Kinematics; March 12, 2013, 01:42.

                    Comment

                    • Kinematics
                      Apprentice
                      • Feb 2013
                      • 72

                      #11
                      The ability to override an existing proc, though, is more difficult. Replacing a proc entirely is easy, but suspending it and putting it back when the current one goes away takes a little more finesse.

                      The standard logic for processing procs is to iterate over the list and allow each one to execute. However you may sometimes only want a single one to execute (eg: only use the single highest resistance value; the previous example of making a creature look like an item; etc).

                      It could be done if you abstract out the proc management instead of leaving it as a simple list that other code can directly manipulate.

                      Than you could have:

                      Code:
                      funcs = item.myProcs.getProcFunctions(trigger)
                      for f in funcs:
                          f(~params)
                      
                      procs.getProcFunctions(trigger):
                          validProcFuncs = [p.getFunction(trigger) for p in self.procs]
                          validProcFuncs = [p for p in validProcFuncs if p]
                          maxPriority = max(validProcFuncs, key = lambda p: p.priority)
                          return [p for p in validProcFuncs if p.priority == maxPriority]
                      It inherently introduces another layer of abstraction to do this, though it could be argued as a good thing, even without the priority-handling code.

                      However even that allows for multiple returns of the highest priority procs, so we're not out of the woods yet.

                      Note: we actually already have a situation where this comes into play with normal vs unique items. We get around it right now because the procs aren't actually entered as procs in the object data; only the name of the proc is listed in the nameInfo struct, and then the proc is determined from that. However, logically, nameInfo shouldn't have a "proc" entry, and the proc should be explicitly entered as such in the item record.

                      Or suppose we had a mimic that got hit with petrification, thus leading to two procs at priority 1 (as opposed to the default 0). We're back at the situation of two procs activating when we only want to allow one. You can somewhat dance around that by varying the priority values, but I think it would be better to handle it explicitly.

                      We can probably assume that, in such a situation, we always want the most recently applied, highest priority effect. Or, in computing terms, we want this to be a stack. Except we also want it sorted by priority.

                      We can also control the addition and removal of procs more precisely if we have an abstracted class for handling that.
                      Code:
                      procs.addProc(proc):
                          nextHigherPriorityIndex = next((p for p, q in enumerate(self.procs) if q > proc.priority), None)
                          if nextHigherPriorityIndex:
                              self.procs.insert(proc, nextHigherPriorityIndex)
                          else
                              self.procs.append(proc)
                      Then the procs will be listed in order of both least-recent to most-recent, and low-priority to high-priority. You could then check if the top of the stack (last one in the list) considers itself 'singular'. If so, only return that item. Otherwise, return the list as normal.

                      However that seems more like something you'd need to know the logic of at the calling code level (eg: when wanting to get the item name) rather than internally to the procs, so probably better to have the encapsulating class provide two functions: getAllProcs, or getTopProc.

                      Comment

                      • Derakon
                        Prophet
                        • Dec 2009
                        • 9022

                        #12
                        Rather than try to teach each Proc how to handle being overridden and come up with a sensible way to order the Procs (which makes defining each individual Proc slightly more complicated), perhaps instead we could have meta-Procs that operate on the Proc list immediately prior to triggering. We would add the following atttributes to the base Proc class:

                        * isMetaProc: a boolean, determines if the Proc is invoked normally or as part of the self-examination process (see below)
                        * isActive: a boolean, if False then the Proc is not triggered.

                        These would default to False and True respectively, if not specified otherwise in the data file.

                        When we go to trigger Procs for a given trigger condition, first we look up all the MetaProcs with that trigger. Each of them is called (in undetermined order) with the list of Procs as an additional argument to its trigger function, and is allowed to mutate the list as it sees fit. Procs can be disabled by modifying their isActive boolean. If you want to modify a Proc's behavior, then you can disable it and insert a new Proc that does something different. Et cetera. Once all of the MetaProcs have been invoked, the normal Procs in the now-modified list are invoked.

                        (Note that a MetaProc can e.g. create a new Proc, and also spawn a timered event to remove it from the list later without requiring the MetaProc's trigger to be called again. This should suffice to handle any bookkeeping cleanup.)

                        Of course this system does not allow for recursion -- MetaMetaProcs. In practice I doubt this will be needed, but if it is then we can just replace the isMetaProc boolean with a metaLayer integer, and invoke Procs in order based on that value. Still, eliminate complexity where it is not needed, hence why we start with booleans.

                        Comment

                        • Derakon
                          Prophet
                          • Dec 2009
                          • 9022

                          #13
                          Originally posted by Magnate
                          No. I have wanted to move rings and amulets to affix-based generation for as long as I've thought about affixes. This is just a handy incentive to get started. It is true that this doesn't necessarily mean that consumables also need to move to the same system, but I can see advantages beyond the naming issue.
                          Could you list some of those advantages? I can see much more of a case for rings/amulets than for the other flavored item types -- affixes make much more sense for equipment than for consumables IMO, especially with the attendant stacking issues.

                          I think the thing that bugs me most about this is that the player treats each ring and amulet flavor as its own separate type of item. Players don't think "Ring that has the Speed affix", they think "Rings of Speed". There will be inevitable confusion when they go to the backend and discover that it doesn't work that way. Perhaps I'm making a big deal over nothing -- it would help to see your couple-of-paragraphs description of how new items would be created under this system.

                          Comment

                          • Kinematics
                            Apprentice
                            • Feb 2013
                            • 72

                            #14
                            Went to write this procs wrapper class, and found that it seemed to work better a bit different than I originally thought. The class as a whole is pretty simple.

                            This moves the code to run a proc's triggers into the procs class, exposing just two functions: runAll() and runTop(). runTop() encapsulates the logic of running the most recently added, highest priority proc.

                            runAll() returns a list of all the results, while runTop() just returns the result of the function that was run.

                            Code:
                            class Procs(list):
                                # Add a proc to the procs list.
                                # Inserted according to priority value, and with most
                                # recent entry last.
                                def addProc(self, proc):
                                    nextHigherPriorityIndex = next((p for p, q in enumerate(self) if q > proc.priority), None)
                                    if nextHigherPriorityIndex:
                                        self.insert(proc, nextHigherPriorityIndex)
                                    else:
                                        self.append(proc)
                            
                            
                                # Get the first proc that matches a provided predicate
                                def getProc(self, predicate):
                                    proc = next((q for p, q in enumerate(self) if predicate(q)), None)
                                    return proc
                            
                            
                                # Get all procs that match a provided predicate
                                def getProcs(self, predicate):
                                    return [p for p in self if predicate(p)]
                            
                            
                                # Remove the specified proc.
                                def removeProc(self, proc):
                                    self.remove(proc)
                            
                            
                                # Run all procs that match the provided trigger, using the
                                # specified arguments list.
                                # Returns a list of the results of all proc calls.
                                def runAll(self, trigger, *args):
                                    results = []
                                    for proc in self:
                                        procFunc = proc.triggerFunction(trigger)
                                        if procFunc:
                                            results.append(procFunc(args))
                                    return results
                            
                            
                                # Run the most recent, highest priority proc that matches
                                # the provided trigger.
                                # Returns the result of the proc.
                                def runTop(self, trigger, *args):
                                    for proc in self[::-1]:
                                        procFunc = proc.triggerFunction(trigger)
                                        if procFunc:
                                            return procFunc(args)

                            On your ideas:

                            onActive would be a simple way to switch other procs off and only have a single one run, however I'm not sure how it would determine which other proc to turn back on if/when the active one is removed.

                            isMetaProc allows for some significant power, but this time I'm the one saying, do you really need it?

                            Though I suppose my Procs class is like a tiny version of that. It handles maintaining an order for procs to run in (so higher priority procs will always run after lower priority ones, and recent ones after older ones), and allows you to select a mode for running procs (one or all). I can't think of anything offhand that would require moving to the next stage of allowing complete free-form rearranging of proc run order, or cherry-picking which ones to run.

                            Although.. I think I added a bit of that into the stats code, with the filter option (eg: get all temporary stat mods). Though you can do something a bit similar with Procs.getProcs(predicate). Or add a simple runProc() function to go with the getProc/getProcs functions to allow custom runs.


                            Code:
                                # Run the specified proc, if it's one of our procs.
                                def runProc(self, proc, trigger, *args):
                                    if proc in self:
                                        procFunc = proc.triggerFunction(trigger)
                                        if procFunc:
                                            return procFunc(args)
                            
                            
                                # Run all the procs in the provided lists that are
                                # one of our procs.
                                def runProcs(self, procList, trigger, *args):
                                    results = []
                                    for proc in procList:
                                        if proc in self:
                                            procFunc = proc.triggerFunction(trigger)
                                            if procFunc:
                                                results.append(procFunc(args))
                                    return results

                            Done this way, you can do meta procs without a special flag. Just pass in the procs class as one of the parameters to another proc.

                            Comment

                            • Kinematics
                              Apprentice
                              • Feb 2013
                              • 72

                              #15
                              Minor aside:

                              Magnate, if you're going to continue to break this up into multiple threads, please be sure that the first post has links to the previous threads, and that the last post has a link to the next thread.

                              Comment

                              Working...
                              😀
                              😂
                              🥰
                              😘
                              🤢
                              😎
                              😞
                              😡
                              👍
                              👎