Pyrel dev log, part 4

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • Kinematics
    Apprentice
    • Feb 2013
    • 72

    Ok, completed two more commits:
    1) Changed 'type' to 'variantName'. May be able to remove it entirely later on, but this at least gives us a more sensical name.
    2) Complete rewrite of position handling, to include positionDistance. This actually simplifies the naming code a fair bit.

    Affixes should specify a base affixType (optional). If so, it will default to a position and positionDistance as given by that affix type; however the affix itself can override either value. If there's no affixType, and no specified positionDistance, distance will default to 100. Position itself defaults to None, which means it won't show up at all if not specified.

    Removed the position value from all affixes in affix.txt that matched the value of the given affixType (ie: remove redundancy).

    Aside: Elvenkind is actually listed as an Arcane affixType, so wouldn't have conflicted, but the general intent remains. May want to change them to Make, anyway.

    Flavors get inserted as if they were affixes of distance 101.

    Working through things, the previous code would probably have broken when dealing with affixes without an affixType.

    Comment

    • Magnate
      Angband Devteam member
      • May 2007
      • 5110

      Originally posted by Kinematics
      Working through things, the previous code would probably have broken when dealing with affixes without an affixType.
      I don't think affixType should be optional anyway!
      "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

        Found a flaw in my groupby code. Need to do some rethinking to be sure it behaves as I expect.

        Comment

        • Kinematics
          Apprentice
          • Feb 2013
          • 72

          Bit of revision on the groupby stuff. Nothing major, just making sure it works cleanly.

          Comment

          • Kinematics
            Apprentice
            • Feb 2013
            • 72

            A little expansion on ideas brought up in irc (somewhat divergent from the naming work) --

            Assuming item templates specify affix info, a guess as to some basic values:

            {"templateName": "item",
            "type": "item",
            "categories": ["item"],
            "nameInfo": {"proc": "default name"},
            "display": {"ascii": {"color": [192, 192, 192]}},
            "minAffixes": 0,
            "maxAffixes": -1},

            // No defined max on number of affixes for any item, by default. Min is 0

            {"templateName": "junk",
            "type": "junk",
            "templates": "item",
            "categories": ["junk"],
            "display": {"ascii": {"symbol": "~"},
            "flags": ["EASY_KNOW"]},
            "maxAffixes": 0},

            // Anything deriving from junk isn't allowed any affixes at all.

            {"templateName": "magic item",
            "type": "magic item",
            "templates": "item",
            "categories": ["magic item"],
            "display": {"ascii": {"color": "flavor"}},
            "flags": ["FLAVORED"],
            "minAffixes": 1,
            "maxAffixes": 1},

            // Magic items must be provided 1 and only 1 affix

            {"templateName": "ring",
            "type": "ring",
            "templates": "magic item",
            "categories": ["ring"],
            "nameInfo": {"baseNoun": "ring~", "flavorTypes": ["Stones", "Metals", "Miscellaneous"]},
            "display": {"ascii": {"symbol": "="}},
            "equipSlots": ["finger"],
            "stats": {"weight": 0.2},
            "flags": ["HATES_ELEC"],
            "maxAffixes": 2},

            // Further override, so rings could have 2 affixes instead of just 1.

            Assume that "effect" would be a new affixType (eg: "Cure Light Wounds"). Effects would have a default position of suffix and positionDistance of 1.

            Then the affixes could be set up with something like:

            "name": "Fire Bolt"
            "affixType": "effect"
            "allowCategories": ["wand", "rod"]
            "procs": [~ onUse proc info]

            "name": "Cure Light Wounds"
            "affixType": "effect"
            "allowCategories": ["potion", "staff"]
            "procs": [~ onUse proc info], [~ onBreak proc info]

            "name": "Aggravate Monster"
            "affixType": "effect"
            "allowCategories": ["scroll", "staff"]
            "procs": [~ onUse proc info]

            "name": "Aggravation"
            "affixType": "arcane" (or whatever; this particular affix doesn't have an affixType defined)
            "allowCategories": ["ring", "amulet"]
            "procs": [~ onEquip proc info]

            Comment

            • Magnate
              Angband Devteam member
              • May 2007
              • 5110

              Close enough. Any affix can have any number of proc entries, so there's no need to use the affixType "effect" (which would not fit with the other affix origin types). But otherwise I see it working out pretty much like that.
              "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

                The main point of an affixType of effect was to allow for a default naming position and positionDistance. Other than that, the types just provide default allocator rules; not sure whether that would be relevant.

                Comment

                • Magnate
                  Angband Devteam member
                  • May 2007
                  • 5110

                  Originally posted by Kinematics
                  The main point of an affixType of effect was to allow for a default naming position and positionDistance. Other than that, the types just provide default allocator rules; not sure whether that would be relevant.
                  It would. For naming details it's better to do those on a per-effect basis I think.
                  "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

                    Posting some notes I've been writing up....

                    Usage Overview (short documentation):


                    Creating and adding a new item to the dats:

                    1) Determine its object type. Inherit the appropriate template(s) for the type.

                    2) Determine its name. The noun goes under nameInfo's baseNoun, and any loosely-coupled modifier to the noun goes under nameInfo's modifierNoun.

                    Loose coupling: [set of] [gloves], because you can have a [set of] [leather] [gloves], or a [set of] [patched] [gloves], etc. Similarly for something like a [large] [shield], where you could eventually modify it to something like a [large] [metal] [shield].

                    Tight coupling: [seeker arrow], since you could have a [maple] [seeker arrow], but it doesn't make sense to have a [seeker] [maple] [arrow]. Thus it would all remain under the baseNoun field.

                    Determine it's name variant, if any. For example, an Amulet of Charisma would have:
                    - templates: "amulet"
                    - nameInfo: "variantName": "Charisma"

                    Obviously the name is expected to reflect what the item does, but there is no intrinsic link. The mods and other data are specified in separate fields; I could change the 'type' to "Beauty", or "the Movie Star" or whatever, and it would have no effect on the stats of the item itself. That's why it's part of the nameInfo struct, and not put in as a data value.

                    3) Add a category to describe the current item, more narrowly specific than its parent template. For actual items, this is often the same as the item's name, though with some adjustments and simplifications.

                    EG:

                    "Set of Leather Gloves"
                    - templates: "hand armor" (thus inheriting [item], [armor] and [hand armor] categories)
                    - categories: "gloves"

                    "Suit of Augmented Chain Mail"
                    - templates: "hard armor"
                    - categories: "chain mail", "augmented chain mail"


                    4) Fill in other data normally (mods, stats, procs, etc).



                    Templates:

                    Templates can inherit from other templates, but can only inherit from templates that are listed before them in the templates file. That is, anything a template inherits from has to have already been defined before the template can use it, or it will generate an error when it does the lookup. I didn't see any reason to try to create a more complex system.


                    Template restrictions:

                    Currently you can't inherit from another item. For example, if the only template was "head armor", and you had objects of "helm" and "crown", you couldn't have other items that inherited from "helm" or "crown", but only from "head armor". By making "helm" and "crown" templates, though, objects can inherit from those.

                    On the other hand, templates can inherit from other templates (though only templates earlier than them in the source data file).

                    The reason for this is that the 'templates' field can only refer to something stored in the TEMPLATE_NAME_MAP, which is only loaded from object_templates.txt. object.txt is by definition not templates, so it's impossible to inherit from them because they are by definition not templates.

                    While there is a little temptation to just make objects arbitrarily inheritable, keeping them separate is actually not that large a burden. There might be a little duplication, but it's probably best to keep things separate.



                    Plurals:

                    There are two decorations used for modifierNoun and baseNoun to indicate how to pluralize the noun form.

                    1) ~ added in place of an 's'. "amulet~" converts to "amulet" when singular and "amulets" when plural. Note that you can have multiple ~ in the name, such as for "main~ gauche~", which pluralizes to "mains gauches".

                    2) : separating two forms, the first being the singular form and the second being the plural. This allows handling of every irregular noun form where a simple 's' is insufficient. EG: "staff:staves" returns either "staff" or "staves".

                    3) No extra decoration. In this case, the same word is returned regardless of singular or plural. EG: "set~ of", "gauntlets". When pluralized, "set~ of" returns "sets of", while "gauntlets" remains "gauntlets".

                    Comment

                    • Kinematics
                      Apprentice
                      • Feb 2013
                      • 72

                      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), 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.

                      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.

                      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.

                      Comment

                      • Kinematics
                        Apprentice
                        • Feb 2013
                        • 72

                        There's been a lot of "it can be done using procs" for various aspects of how the game would work. I'd like to look a little deeper into the proc system to see exactly how it will behave.

                        given a proc name, maps to proc class name.
                        class is initialized with trigger event, params, and level
                        when triggered, calls procClass.trigger(whatever params)


                        For items:

                        basic interactions:

                        onPickup
                        onDrop
                        onEquip
                        onUnequip
                        onUse


                        Some procs are intended to simply 'do' something. Others are designed to calculate and return a value or values.
                        Code:
                        "procs": [{"name": "temporary stat mod", "statName": "speed", "modAmount": 1, "duration": "10+d10", "triggerCondition": "item use"}],
                        
                        base item template might have:
                        "procs": [{"name": "add weight", "triggerCondition": "onPickup"},
                                  {"name": "add weight", "triggerCondition": "onCreate"},
                                  {"name": "remove weight", "triggerCondition": "onDrop"},
                                  {"name": "remove weight", "triggerCondition": "onOverflow"},
                                  {"name": "remove weight", "triggerCondition": "onThrow"},
                                  {"name": "remove weight", "triggerCondition": "onDestroy"},
                                  {"name": "remove weight", "triggerCondition": "onBreak"},
                        ]
                        A curse might add the proc:
                        Code:
                                  {"name": "cancel action", "triggerCondition": "onUnequip"},
                        Looks like we need some means of interrupting what was intended. In fact a lot of procs may intend to interrupt or change the results of attempting something. Should probably have the two stages of event handling: onTriggering and onTriggered, for calling before attempting to do something, then calling to actually do it.

                        Why separate it? A couple reasons:
                        1) Separate the logic out, so you know where to look to determine if a process should continue or not.
                        2) With multiple potential procs on an item, we may need to know whether any of them want to block before we allow all of them to proceed. For example, if some procs modify the character stats when a certain event happens, but there's some status that prevents that event from taking place (eg: auto-regeneration, but using an ability that blocks auto-regen), you don't want to allow some of the stats to be modified each time before it reaches the cancel stage.


                        Any triggering() should take a dict parameter that includes a "cancel" member. Set it to True to prevent execution of the action (more flexible; we don't have to return a value this way). Can fit that into the base proc class:

                        Code:
                        class Proc():
                            ## Preparing to trigger the proc.  Default to letting this pass through.
                            def triggering(self, eventArgs = {}, *args):
                                pass
                            ## Trigger the Proc, with any appropriate parameters.
                            def trigger(self, *args):
                                raise RuntimeError("Proc of type [%s] didn't implement its trigger function." % type(self))
                        Some proc that cancels an action:
                        Code:
                        CancelProc(proc.Proc):
                            def triggering(self, eventArgs):
                                eventArgs["cancel"] = True
                            def trigger(self)
                                stuff~~
                        How might it be implemented?
                        Code:
                        class item:
                            ## Respond to being unequipped.
                            def onUnequip(self, user, gameMap):
                                eventArgs = self.triggeringProcs(user, gameMap, 'item removal')
                                if not eventArgs["cancel"]:
                                    self.triggerProcs(user, gameMap, 'item removal')
                                    user.removeStats(self.stats)
                        
                        
                            ## Trigger procs that have the specified trigger.
                            def triggeringProcs(self, user, gameMap, trigger, *args, **kwargs):
                                eventArgs = {"cancel": False}
                                for proc in self.procs:
                                    if proc.triggerCondition == trigger:
                                        proc.triggering(self, user, gameMap, eventArgs, *args, **kwargs)
                                return eventArgs
                        or
                        Code:
                            ## Trigger procs that have the specified trigger.
                            def triggerProcs(self, user, gameMap, trigger, *args, **kwargs):
                                triggerProcs = [p for p in self.procs if proc.triggerCondition == trigger]
                                eventArgs = {"cancel": False}
                                for proc in triggerProcs:
                                    proc.triggering(eventArgs, self, user, gameMap, *args, **kwargs)
                                if not eventArgs["cancel"]:
                                    for proc in triggerProcs:
                                        proc.trigger(self, user, gameMap, *args, **kwargs)
                        Possibly pass eventArgs over to trigger() as well? In case the pre-stage code wants to pass a value to the main stage code.

                        However that leaves trailing actions. The onUnequip function on item calls the trigger and then removes the stats, without first finding out if it was actually removed.

                        In this case we might actually want to return a value: True if successfully triggered, False if not. However I'm not sure if that might not interfere with procs that return a value normally. Except that if you're calling procs that return a value, you wouldn't go this route anyway; if multiple procs returned a value, you'd lose all but the last one.

                        So it should be safe to do that, and we can write it as:

                        Code:
                            ## Trigger procs that have the specified trigger.
                            def triggerProcs(self, user, gameMap, trigger, *args, **kwargs):
                                triggerProcs = [p for p in self.procs if proc.triggerCondition == trigger]
                                eventArgs = {"cancel": False}
                                for proc in triggerProcs:
                                    proc.triggering(eventArgs, self, user, gameMap, *args, **kwargs)
                                if eventArgs["cancel"]:
                                    return False
                                else:
                                    for proc in triggerProcs:
                                        proc.trigger(self, user, gameMap, *args, **kwargs)
                                    return True
                        and then:

                        Code:
                        class item:
                            ## Respond to being unequipped.
                            def onUnequip(self, user, gameMap):
                                if self.triggerProcs(user, gameMap, 'item removal'):
                                    user.removeStats(self.stats)

                        Of course this all depends on the logic that this function was called immediately on the attempt to remove the item, and that the logic for preventing the attempt isn't being handled elsewhere. I'd expect you'd want to try to keep such things localized, particularly in a proc-based system where you have no idea what the procs might try to do.

                        You could, I suppose, put the logic for attempting the action at a higher level, and only call the item's onUnequip when you're sure it will pass, but it seems an awkward separation. Either you need an entire extra proc class just to handle the attempt logic, or you call triggering() from a different code level and have to keep the two parts of code in sync.

                        The main problem seems to be the class construction of the procs themselves. Basically, a given proc can only handle exactly one trigger. I can't add a single proc class for a curse, as I need to handle both "on remove curse"- and "on remove item"-type triggers (and maybe "on apply curse" to prevent doubling up on procs).

                        Hmm. That could be handled if the trigger field was a list instead of a string. The only change would be how you interact with it.

                        Code:
                                triggerProcs = [p for p in self.procs if proc.triggerCondition == trigger]
                        to
                                triggerProcs = [p for p in self.procs if proc.triggerCondition in triggers]
                        Though it requires passing the trigger in as a parameter for triggering() and trigger(), as well.

                        I think that would be useful for simplifying proc creation. That way a proc class encompasses an entire idea rather than just a single reaction.

                        Comment

                        • Derakon
                          Prophet
                          • Dec 2009
                          • 9022

                          I'd like to keep the creation of procs as simple as possible. They're where newbie developers are most likely to get their feet wet with programming, and it's thus vitally important that simple new behaviors be able to be added with as little fuss as possible. 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.

                          My first design for the Proc system collected Procs together into Effects -- each Effect was an ordered sequence of Procs. The Effect would call each Proc in turn, and based on the return value, would either continue calling Procs or would stop prematurely. This ended up getting jettisoned mostly because it made the entire process of defining Procs for use to be very tedious, since typically each Effect consisted of a single Proc but they both had to be defined in the data files.

                          My gut instinct regarding your suggestion to allow Procs to cancel events is that we should separate that into two separate trigger conditions. The first would be when we check to see if an action is allowed, and the second is for when we actually do the event. So e.g. onAttemptUnequip and onUnequip. The former takes "filter"-style Procs that return booleans declaring if the action can continue (much like the existing Procs for regulating the generation of unique monsters), while the latter takes more standard Procs that just do things. I suspect that trying to combine things into "triggering" and "triggered" functions under the same Proc would result in having many Procs that only define one or the other function, leaving the counterpart as a no-op. And the code in the engine wouldn't really be any simpler either.

                          tl;dr Procs should be as simple and "atomic" as possible. If we want complex behaviors, then we should ensure there are lots of potential trigger conditions, not increase the basic complexity of Procs.

                          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.

                          (Though having proc triggers for each of those events is worthwhile, so you can have e.g. wands that explode when destroyed, or bear traps that cripple thieves when stolen)

                          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.

                          Comment

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