Version 2 (modified by magnate, 7 years ago) (diff)

First draft

A Guide to Affixes

a series of 55 commits by magnate


This work was based on some ideas vaguely expressed in

It started off with wanting to address some pet peeves with ego item generation (the hackish OBJECT_XTRA_TYPE_ defines, the rigid nature of the better ego types, etc.). It was also inspired by Eytan Zweig's item prefixes (#587), and grew from there. The basic idea is to allow for a greater variety of magical items and smoother progression from a dagger (1d4) (+0,+0) to a 6d5 Holy Avenger (and so on for other equipment).

What hasn't changed

All the old ego items, are still generatable almost exactly as they used to look. Slay weapons, branded weapons, Defender/HA/Gondolin/Westernesse weapons, Elvenkind armours and boots, Robes of Permanence, etc. At the moment (subject to any rebalancing we may wish to do) they are still generated with the same flags and on the same object types as they used to. (So only Maces can get the Of Disruption suffix, only Scythes can get Of Slicing, only robes can get Of Permanence etc.)

What has changed

I say "almost" because there are two noticeable differences. Some of the high-end items will have all four IGNORE_ flags where they only used to have one or two (e.g. Gondolin). More significantly, most items will have lower to-hit/to-dam values than under the old system - though this can now be addressed purely through editing ego_item.txt and ego_themes.txt

The ego_item type

Ah yes, themes. Let's get down to the code. The ego_item type still exists, and they're still stored in the global e_info[] array, but they're not called egos any more, they're called affixes (because you can have MAX_AFFIXES of them). There are some (IMO) useful changes to the type:

  • The C: line now takes seven parameters rather than three. As well as to_h/to_d/to_a, you can now modify base AC, weight, dice and sides. The first two of these are percentage mods (and are signed so can be negative), the last two are just extra dice and sides (but can also be negative). Full credit to Eytan Zweig for inspiring these.
  • The M: line is assumed to be NO_MINIMUM:NO_MINIMUM:NO_MINIMUM if it's absent. So no worries about accidentally removing armour penalties.
  • The X: line is gone. Rarity is no longer used, and random flags are now done by R: or R2: lines. R: lines specify flag types (sustains, high resists, etc.), while R2: lines allow specification of an exact flag mask. So if you want, you can have an affix that adds one of SUST_STR or SUST_CON but nothing else. You cannot add pval flags this way though - I wrote a spec for a Z: line to do this, but it is difficult to box around MAX_PVALS so I have left this for another day.
  • The T: line now deals with rarity as well as kind legality. For each tval/min-sval/max-sval group, you can specify an alloc_prob and a min and max depth. EGO_TVALS_MAX has been increased accordingly (and could go further). So you can have the same affix appear with different likelihood and at different depths for different items. Acid-resistant shields can be much rarer (or less rare) than acid-resistant cloaks, for example.
  • The T: line also includes a "level" field, which specifies how good the affix is considered for this object group at these depths. So far I've defined four levels: "good", "great", "uber" and "artifact", but we could have more. The last is in case we want to have affixes only found on randarts (e.g. immunities). More on levels in a minute, but one point to note is that you can redefine the same object group here. So:

T:soft armour:0:99:great:10:1 to 25
T:soft armour:0:99:good:25:26 to 100

would mean that this affix is considered "great" at low levels but merely "good" at deeper depths - for soft armour.

Ok, so those are affixes. I stripped out all the compound egos, with multiple attributes, and boiled affixes right down, so that each only provides one or two things. Resist affixes still give the IGNORE_ flag, and the five x3 brands still give both RESIST_ and IGNORE_ flags, but otherwise most affixes only provide one flag. I've added the EyAngband? prefixes, which modify the base properties of armour and weapons (AC, hit/dam, dice/sides) - I left out only the inappropriate race-specific ones (Ey has lots of weird races), and guesstimated on some. (N.B. I was concentrating on implementation, not on balance, so there is a ton of room for tweaking the values of any of the Ey affixes, and any others.) I also stripped out base objects which are obviously other base objects with affixes: Lead-Filled Mace, Mithril Chain/Plate?, Adamantite Plate, Mace of Disruption, Scythe of Slicing. These are all now affixes (but see below for more to do on this).

make_object and make_artifact

Having improved the type and reduced the egos down to simple affixes, I set about changing object generation. First I rewrote kind_is_good to look for OF_GOOD, so that powerful items like DSMs are automatically considered "good" and never "average". This doesn't actually have much impact on the obj_alloc_great table though, since all armour and weapon types were already in it (but see below for more thoughts on this).

Then I sorted out artifact generation, combining special and normal artifacts. This distinction only ever existed because all normal artifacts were only ever generated after the object kind was chosen, and specials were attempted before. Now make_object tries for an artifact first, and make_artifact now creates an allocation table of all possible artifacts (i.e. not yet created, in-depth, out-of-depth checks etc.) before choosing one. If the kind is already chosen (e.g. from specified monster drops) then the allocation table looks a lot smaller because it only contains artifacts legal for that kind.

Please note: artifact generation will need quite a bit of testing, because not only did I unify the generation functions (which will fundamentally change how many are generated), but I also recalibrated the alloc_prob scale to 1000 rather than 1000, and reflected the fact that these are now checked independently of base items. So Bladeturner's old rarity accounted for the fact that its base item was very rare: now it has to be even rarer. I implemented multiple A: lines for artifacts, so we now have finer control over their findability (because they can have different rarities at different depths).

Choosing and applying affixes

So, if we're not an artifact we now set the number and quality of an object's affixes in apply_magic. Note that the call to apply_magic from make_object deliberately sets allow_artifacts to FALSE, because we've already tried for an artifact there. But if it's called directly (for a specified item), then we still get a shot at becoming an artifact.

One important change is that *all* items can now get "good" affixes. This is because apply_magic_weapon/armour() have been removed, and plusses now come from first-grade affixes (Quality, Sharp, Brutal etc.). Forced-great items automatically get access to great affixes, and good items do so 25% of the time. There's also a depth-dependent chance for increment, so uber affixes are (i) only available on good or great drops and (ii) much less likely early on.

The number of affixes is a function of depth (0 to (1 + lev / 25)), plus one or two if good, or three or four if great (minus one if we have OF_GOOD). These numbers can be rebalanced, of course. There might not actually be many "great" drops, so we might want to be more generous to normal or good items, and tone down the great items to only a couple of extra affixes. But note that "normal" drops get much more interesting with depth, so this may not be necessary.

I haven't done a lot of renaming yet, but I've renamed make_ego_item to obj_add_affix because it wasn't called from anywhere else except apply_magic. It checks that we're not trying to add an affix to an artifact, a themed item, or an item with max affixes already. It also does that weird GREAT_EGO level boost, for a one-in-20 chance of a potentially huge level boost (though that doesn't boost the affix level). Importantly, we copy the object, so we don't have to worry about affixes creating broken items - if that happens we just roll back and don't add anything.

We choose which affix to apply in obj_find_affix, which is ego_find_random renamed and rewritten to allow for the new T: lines above. Like make_artifact, it builds an allocation table from the affixes which are legal for this item at this depth and affix level.

We actually apply the affix in ego_apply_magic (which I didn't rename yet 'cos it's called from a few places) - it deals with the extra stuff outlined above (base ac / weight / dice / sides, and random flags) but is otherwise recognisable. We now check minima both before and after application, to ensure that a min_pval of 2 gives correct results when applied to an existing pval of, say, -1. We also check flags at the end, to remove contradictory elemental flags (RES_FOO and VULN_FOO etc.), and to strip lots of mods off ammo (so that we don't have to replicate affixes and themes for ammo). Oh, I fixed #1531 as well.

If we didn't break anything, we look to see if the object can now get a theme.


Without themes, we can have very powerful items, but they're like randarts - random collections of attributes. Themes allow us to decide, during an item's creation, that it's going down a particular path. So I wrote ego_themes.txt, which sets out what these themes are. At the moment they're all recognisable, because they're the high-end/compound egos I removed from ego_item.txt earlier on.

Themes[] are a global array like e_info[], which have N: and D: lines exactly like ego_item.txt. They also have T: lines, but these only have tval, svals and depths - no "level" or "commonness". So far so obvious. obj_find_theme builds an allocation table of legal themes just like make_artifact and obj_find_affix, checking depth and tval/sval.

But there the similarity ends - themes don't have an inherent commonness, they have a number of component affixes, each of which has a weighting. We check the object to see if it has any of these affixes already, and record their weight. Then we multiply by the proportion of total weight to get the actual likelihood of acquiring that theme. These weightings were chosen very carefully, because often only one theme will be available to an object, and we have to have an absolute percent chance of getting it, as well as an allocation table if there are several to choose from. The total weight of the relevant affixes on the item is multiplied by (itself x 4 / total weight of all affixes in the theme) to get the percent chance (in the code we use x8 and use randint0(200) so we're using half-percent granularity).

So here's a worked example: the theme "of Resistance". It has six constituent affixes: the four resists (each weighted 7), Reinforced (for the to_a boost, weighted 2) and Durable (for the IGNORE flags, weighted 4). Durable items contribute to a lot of themes, but usually with very small probability - this is actually the largest weighting of Durable, because it reflects the nature of the theme. So the total weighting of all affixes in this theme is 34.

If we have only one of them, we can't get the theme. You need at least two affixes to get any theme.

If we have two of the resists, we have a total weight of 14, which is multiplied by 56/34 to give a 23% chance of acquiring the theme.

If we have three of the resists, we have weight of 21, which is multiplied by 84/34 to give 51% chance of acquiring the theme.

If we have all four resists, we have 28, multiplied by 112/34 to give a 92% chance.

By contrast, if the two affixes we have are Reinforced and Durable, we have weight of 6, which is multiplied by 24/34 to give a 4% chance of acquiring the theme. Both of them and one resist makes 19% - less useful than having two of the resists.

Another example worth mentioning is Gloves of Power - a theme which has only two affixes. Both have weights of 100, so if we get them, we will automatically get this theme. Similarly lanterns of True Sight, and Blessed weapons.

Blessed weapons have three affixes, but one of them has a weighting of zero (of Dweomercraft, the one which provides the random ability - also on Gondolin and *Slay* Evil weapons, Lothlorien bows etc.). This means it doesn't contribute to the weighting, but it is applied in obj_apply_theme after the theme is chosen. This function simply cycles through all the affixes in the theme and applies all the ones that aren't already on the item. Since you can specify the same affix more than once in a theme (e.g. for extra combat bonuses, or extra random flags), we allow the second and subsequent ones to be applied.

Note that obj_apply theme doesn't actually set the o_ptr->affix for the affixes it applies. This is deliberate: many themes have more than MAX_AFFIXES. Also, once we acquire a theme we're unable to modify the item further (like an artifact), so it doesn't really matter too much. Note also that branding spells *will* (currently) work on non-themed items, providing they have < MAX_AFFIXES. I like this, but others might not.

ID, naming and saving

ID-by-use works reasonably well for affixes, though I had to write object_affix_is_known to check from first principles whether we know all about an affix. The IDENT_ flags don't work because we don't know how many affixes we're trying to know, and I decided against recording o_ptr->known_affixes in favour of working it out on the fly. object_theme_is_known is just a wrapper which makes sure that we know all the affixes in a theme. This is pretty basic but actually seems to work ok - both magical ID and ID-by-use seem to work ok, and the ego knowledge menu shows affixes once they're known (it doesn't talk about the new mods to weight/base AC/dice/sides, but otherwise works ok).

Finally, with noz's help, we sorted out the prefix and suffix names of the object, which are the theme or the best affixes in the absence of a theme (so you can get Emerald weapons of Gondolin, or Broken ones, etc.). There is still some thinking to do here in relation to ID and naming, some of which was discussed on IRC (d_m/fizzix suggested "synthetic" affixes which change the name but no properties - this seems like a good solution, but it would be a shame to lose all the occurrences of the affix names).

The savefile now stores the indeces of the theme (in the old o_ptr->ego->eidx slot) and the affixes. I also took a cue from Gabe and we now record all of MAX_PVALS, MAX_AFFIXES, OF_SIZE and OF_BYTES in the savefile, so if they change we don't have to write a new function. Oh, and we also store o_ptr->extent, which is food/fuel/charges/gold/chest level, fixing #1540.

Bugs, issues and to-do

I need to get rid of remaining references to o_ptr->ego and remove it from the object_type struct. Also renaming ego_stuff to affix_stuff would be helpful - I've been a bit lazy about this, in case the whole thing was rejected.

The knowledge menus need sorting: first they need to show details of the new mods from affixes (base AC / weight / dice / sides - random flags should already work). Then we need a new "theme knowledge" menu about themes - I don't think I have the skill to do this, yet. Also, theme->everseen is not currently set anywhere. Come to think of it, neither is affix->everseen, so I'm not sure how the knowledge menu is still working for affixes!

We need to agree a strategy for naming items with multiple affixes. Personally I favour adopting the position that an object's displayed name does not give you complete information about all its properties, but others may disagree. Also, affixes can be applied more than once (meaningless for flag affixes, but important for hit/dam/ac etc.). I like the idea of Sharp, *Sharp* and Sharp or something, to denote multiple applications of an affix.

We also need to agree a strategy for the names of base objects for armour. Weapons are now all sorted (there are no prefix-like adjectives in weapon names, and no "of"s). But we are significantly limited in the kinds of affixes we can invent for Leather items, for example. And Mithril is already an affix for body armour (from Ey), but we still have Mithril shields/boots/gauntlets etc. IMO it should be an affix available for all armour pieces, but that means re-thinking the names of most base armour items. We could make Ethereal an affix, and Alchemist's. We could remove Elven Cloaks too.

My original intention was that themes were more random, i.e. that not all affixes in a theme would be applied every time. I didn't implement themes like this because I didn't want the outcry of "my Gondolin weapon doesn't have RES_DARK" etc. But I still think it would be good to have more variation.

IMO we should no longer show the base AC or dice of an object, because these are no longer so static - lots of the Ey prefixes change one or the other. This fits nicely with reducing the amount of info available and forcing people to walk over a fetch stuff.

We also need to refine the IDing of affixes and themes, but this needs discussion after both Eddie's patch improvements and rune-based ID. I have defined IDENT_PREFIX and IDENT_SUFFIX, but these aren't used yet (we use obj_affix_known instead).

We need description/flavour text for affixes and themes. (There was very little for any existing ego types anyway, so this has long been the case.)

Finally, of course, there's a ton of balancing tweaking to be done. Some affixes are available on items which weren't before (e.g. of Warding), and others aren't (e.g. of Dweomercraft), purely because of what I was testing when I added them. Mithril shields don't seem to be able to acquire any affixes at all! Doing this balancing means adjusting the stats code to record affixes and themes (it already records all the info).

A bunch of things occurred to me while doing all this stuff (I'll make tickets)

  • the slay cache can now go, as we're not constrained to a small number of slay combinations which are worth caching
  • we could have a low-level code module for generating lookup tables like flag names (currently duplicated in obj-flag.c and init2.c) and tvals (which we could now do from object_base.txt, removing the need for hard-coding - we could also seek to remove tvalsval.h ...)
  • affixes could change the display colour of an object (Ey has this, and fizzix thought of it too)
  • affixes could be used to generate ego jewelry, which allows re-thinking of what non-ego jewelry ought to be ...
  • allocation of kinds could use the alloc_entry struct (presumably it was written before that struct?)
  • items with alloc_prob 0 should not appear in knowledge menus (the old Bronze DSM problem, now occurring with stuff like Adamantite Plate and Maces of Disruption)
  • speaking of Maces of Disruption, we can't currently generate Deathwreaker. It would be interesting to require artifacts to be generated from certain affixes, as well as from certain base items. i.e. You need a mace, with the Of Disruption affix, before you can generate Deathwreaker.
  • we should now seek to remove the INSTA_ART objects from object.txt
  • should maxima really be sparse? z_info->e_max is set not as the number of e_info entries but the index of the highest.