Modding:Tutorial/Custom Beings

From DoomRL Wiki

Jump to: navigation, search

In the following tutorial you will learn the basics of being objects. Beings are anything in the game that can act independently on the map, without the need to specifically interact with with it. The player object is also a being, and so all of the topics mentioned here will also apply to the player (with some exceptions). We will learn how to create new beings, as well as how to apply various modifications in order to produce special and unique varieties.

Contents

Prototype

The following is a complete list of the being prototype table:

Beings{
    name            = "evil marine",                --required field
    name_plural     = "evil marines",               --defaults to name with an appended 's'
    id              = "badclone",                   --defaults to lowercased first word of 'name'
    ascii           = "@",                          --required field
    color           = DARKGRAY,                     --required field
    sound_id        = "soldier",                    --checked if 'id' has no sound bindings
    desc            = "It's you, but evil."         --required field
    kill_desc       = "fragged by your evil clone", --defaults to ""
    kill_desc_melee = "mauled by your evil clone",  --defaults to ""
    weight          = 1,                            --required field
    danger          = 12,                           --required field
    minLev          = 20,                           --required field
    maxLev          = 100,                          --defaults to 200
    corpse          = true,                         --defaults to true
    HP              = 100,                          --defaults to 10
    armor           = 2,                            --defaults to 0
    res_bullet      = 0,                            --defaults to 0
    res_melee       = 0,                            --defaults to 0
    res_shrapnel    = 0,                            --defaults to 0
    res_acid        = 0,                            --defaults to 0
    res_fire        = 0,                            --defaults to 0
    res_plasma      = 0,                            --defaults to 0
    weapon          = "bazooka",                    --defaults to "" (no weapon)
    toDam           = 5,                            --defaults to 0
    toHit           = 5,                            --defaults to 0
    toHitMelee      = 0,                            --defaults to 0
    attackchance    = 60,                           --defaults to 75
    vision          = 11,                           --defaults to 9
    speed           = 100,                          --defaults to 100
    XP              = 2000,                         --defaults to 3*'danger'^2+20
    ai_type         = "cyberdemon_ai",              --defaults to ""
    group           = 0,                            --defaults to 0
    flags           = {},                           --defaults to {}
    bulk            = 100,                          --defaults to 100
    sprite          = 0,                            --required field, set to 0
    overlay         = {0,0,0,0},                    --defaults to {0,0,0,0}
}
  • name is what the being appears to be in-game (e.g., using the 'look' command).
  • name_plural is what the game uses when a pluralized form of the name is required (e.g., in the kills section of the mortem).
  • id is the being identifier, to be used in lua whenever you want to call the item prototype.
  • ascii is the ASCII character used to represent the being in the console.
  • color is one of 16 (4-bit) colors that you can use to distinguish the being on the map.
  • sound_id is what you want the being's sound bindings to map to. If the being's id is not registered for sound bindings, it will use this key instead (useful when mapping several beings to a single set of sounds). (Note that in the current version, registering sound bindings for custom beings requires direct manipulation of the prototype. This will be explained later on.)
  • desc is the description of the enemy, as seen when using the 'more' command while looking.
  • kill_desc is what shows at the top of a mortem if the player was killed by this being's ranged attack.
  • kill_desc_melee is what shows at the top of a mortem if the player was killed by this being's melee attack.
  • weight affects the frequency that the being will appear, for random monster generation purposes.
  • danger is the capacity by which the being can be added to the map, for random monster generation purposes.
  • minLev is the earliest floor on which the being can appear, for random monster generation purposes.
  • maxLev is the latest floor on which the being can appear, for random monster generation purposes. weight, danger, minLev, and maxLev are only important on levels that randomly generate beings using Level.flood_monsters().
  • corpse determines if the being will generate a corpse upon death. Alternatively, you can choose a cell (by referencing its id here) that the being will leave upon death.
  • HP determines how much damage the being can take before it dies.
  • armor determines how much damage the being absorbs before HP is subtracted, modified by the damage type of the source. Note that, without BF_HARDY, armor can only reduce damage to 1.
  • res_[damage_type] sets the internal resistance of the being for damage_type. Internal resistance affects both torso and foot sources. Note that amount of damage absorbed by resistance is calculated before any damage absorption due to armor, and that 100% resistance can reduce damage to 0 (though anything less will not).
  • weapon sets the being's default weapon (in its weapon equipment slot). This can either be an id for a separate item prototype, or it can be inlined into the being prototype. If inlined, the following is true by default for the item prototype:
    • type is set to ITEMTYPE_NRANGED
    • id is automatically assigned (although no lua-based id (sid) will exist)
    • weight and sprite are set to 0
    • IF_NODROP and IF_NOAMMO are added to the flags table
  • toDam is the damage modifier to the being's natural melee attack. It deals 1d3 damage unmodified.
  • toHit is the accuracy modifier to all of the being's attacks.
  • toHitMelee is the accuracy modifier to the being's melee attack. toHit and toHitMelee can have negative values (e.g., in order to set melee to-hit to 0 while keeping ranged to-hit at some positive value).
  • attackchance is the chance that the being will attack on a given turn. This can be overridden with custom AI.
  • vision is the distance in which the being can see, assuming no cells that block vision are present.
  • speed is the speed of any of the being's internal actions (e.g., movement and attacking). Values greater than 100 result in a faster being, while values less than 100 result in a slower being. The maximum value this can be set to is 255.
  • XP determines how much experience the player receives upon killing the enemy. Only the player can benefit from experience.
  • ai_type sets the AI of the being. There are a number of pre-defined AI for the base monsters, but it is possible to define your own.
  • group assigns the being to a particular group of other beings that it will not attack. Beings of different groups, by default, will attack each other. In the base game, all monsters are in group 0, while the player is in group 1.
  • flags defines what being flags you give the being.
  • bulk is an unused parameter, so you can ignore it entirely.
  • sprite is what will eventually be the graphical tile of the being. Set to 0 and ignore it for now.
  • overlay, like sprite, is based on there being graphical tiles in DoomRL. You can ignore this key entirely for the time being.

Properties

Several of the being object properties are identical to their prototype counterparts, others differ only by name. The comparisons are given here:

  • The following properties are the same as the prototype keys:
    • attackchance
    • vision
    • speed
    • armor
  • The following properties have a different name than a prototype key, but function exactly the same as the key:
    • The key sound_id is broken up into the following properties:
      • soundact is heard occasionally whenever the being acts
      • soundhit is heard whenever the being takes damage
      • sounddie is heard whenever the being dies
      • soundattack is heard whenever the being uses a ranged attack
      • soundmelee is heard whenever the being uses a melee attack
      • soundhoof is heard whenever the being moves (often disabled on most beings)
    • hpmax is the property of HP
    • tohit is the property of toHit
    • todam is the property of toDam
    • tohitmelee is the property of toHitMelee
    • expvalue is the property of XP
  • There are also a number of properties that are instantiated by default without automatic customization. They are as follows:
    • proto is the being's prototype. Don't change this unless you're planning on doing something really hacky.
    • hp is the being's current health. It defaults to the HP prototype key, but changes whenever the being takes damage.
    • scount is the being's speed count, or energy. For each turn that passes, all beings' scount is increased 100 (modified by each's speed); whenever a particular being takes an action, its scount is decreased by the amount of game seconds it takes for the action, multiplied by 1000. (For custom actions, scount must be modified manually in order for the action to take any time.)
    • todamall is a damage modifer applied to all of the being's attack (melee and ranged). (When the player invests in Son of a Bitch, this property is modified.)
    • Three properties affect the time in which particular actions are taken, given as a percentage relative to speed:
      • movetime affects how much time it takes for the being to move: for the player, this also corresponds to an increased dodge chance. (When the player invests in Hellrunner, this property is modified.)
      • reloadtime affects how much time it takes for the being to reload its weapon. (When the player invests in Reloader, this property is modified.
      • firetime affect how much time it takes for the being to attack. (When the player invests in Finesse, this property is modified.)
    • bodybonus reduces knockback by the number given, and any value greater than 0 will prevent health decay above 100%. (When the player invests in Badass, this property is modified.)
    • pistolbonus lowers firetime and increases damage whenever the being is using a weapon with the item flag IF_PISTOL. (When the player invests in Son of a Gun, this property is modified.)
    • rapidbonus modifies the number of shots fired from a weapon whose "shots" property is greater than one. (When the player invests in Triggerhappy, this property is modified.)
    • techbonus increases the number of mods that can be added to the being's equipment, as well as what tier of assemblies the being can create. (When the player invests in Whizkid, this property is modified.)
  • Finally, there are the properties that beings inherit from things. (Some of the thing properties are readonly, meaning they can't be changed once the instantiated object is initialized. These will be bolded for clarification.)
    • The following properties are the same as the prototype keys:
      • color
      • id
      • name
      • sprite
      • res_bullet
      • res_shrapnel
      • res_melee
      • res_acid
      • res_fire
      • res_plasma
    • The following properties have a different name than a prototype key, but function exactly the same as the key:
      • picture is the property of ascii
      • nameplural is the property of name_plural
    • The following properties are automatically given by default:
      • sid is a string identifier, similar to id. This is what is used dominantly by modders, and is set based on the being's id key.
      • uid is a unique identifier across all instantiated objects. This is often used to save the state of a game.
      • x is the x-coordinate of the being on the map.
      • y is the y-coordinate of the being on the map. x and y make up a being's coordinate indirectly.
      • __ptr is a pointer to the object in the engine: you'll never have to worry about this property.

Engine Hooks

Beings come with three hooks:

  • OnCreate(being) is called when being is allocated to memory (i.e., created but not necessarily placed on the map). Use this function to add or modify properties, or potentially cause special cases to occur depending on the circumstances of the creation.
  • OnAction(being) is called whenever being takes an action. This is where a great deal of code used to be added when a modder wanted to manipulate the being's special abilities and/or AI, but is now mostly used as a means to have abilities that should be activated on every action. For more complicated setups, the modder should create a special AI for the being (explained in a separate tutorial).
  • OnDie(being,overkill) is called whenever being dies. The overkill parameter is a boolean that only triggers OnDie() if being was gibbed on death. (In this way you can set up two separate OnDie() hooks to the same being prototype.)

Basic Examples

There are quite a few things you can do with beings. The following examples will go through the more common uses for beings: you should feel free to work with any of these as a base for your own designs.

Dummy

Dummies are beings that cannot be killed but cannot kill, either. They are mainly used as targets, potentially as a trigger or simply something to shoot at. In spite of this simple purpose, there is a lot you can do with dummies. First, let's add the prototype fields:

Beings{
    name   = "dummy",
    id     = "dummy",
    ascii  = "I",
    color  = WHITE,
    desc   = "Nothing but a punching bag."
    sprite = 0,
    weight = 0,
    danger = 0,
    minLev = 0,
    speed  = 0,
    flags  = {BF_INV},
}

Since the dummy isn't going to be attacking at all, we can ignore a great deal of keys, and unless we want the dummy to be able to randomly spawn with other enemies, we set the generation parameters to 0. The important factors to this prototype involve the being's speed and flags:

  • speed is set to 0, so the dummy will never have any turn whatsoever.
    • If you want the dummy to attack but not move, you can set the BF_SESSILE flag (which does exactly that).
    • If, instead, you want the dummy to move but not attack, you can set the BF_NOMELEE flag (which prevents melee attacks). To avoid ranged attacks, just don't give the dummy a ranged weapon with which to attack.
  • "BF_INV" is the flag set when a player is invulnerable, which prevents any damage from being taken regardless of its source (even going so far as to include a nuclear explosion!).
    • If you want to prevent only certain types of damage, you can alternatively set any of the six resistance keys to 100, allowing for complete immunity against that type of damage. Note that there is no resistsance for piercing attacks, nor would this dummy ever withstand a nuclear explosion.
    • If you want the dummy to be able to take damage but not die, you can set the HP to a ridiculous amount, such as 1000, and use a hook to set its hp to hpmax. This should prevent death in all but the most extreme cases. (I don't know the limit on HP, but 1000 will actually work.) Note that, if the dummy's speed is set to zero, the OnAction() hook won't trigger.

Now we can place the dummy on the map and go to town. However, this dummy can still be pushed around if it takes enough damage, and there's a good chance we don't want to chase this thing all across the map. There are two ways to add some knockback prevention:

Items{
    name = "dummysuit"
    weight = 0,
    sprite = 0,
    type = ITEMTYPE_ARMOR,
    desc = "for dummy",
    knockmod = -100,
    flags = {IF_NODROP},
}
 
Beings{
    name   = "dummy",
    ....
    OnCreate = function(self)
        self.eq.armor = item.new("dummysuit")
    end
}

The first method is to create a custom item, either armor or boots, that provides 100% knockback reduction, and then place the armor/boots in the dummy's armor/boots equipment slot on creation. The "IF_NODROP" flag is there so that, no matter what kind of dummy you create, it will never let the armor drop (allowing the player to wear it).

Beings{
    name   = "dummy",
    ....
    OnCreate = function(self)
        self.bodybonus = 100
    end
}

The second method is to alter the dummy's bodybonus property on creation, setting it to a very high amount so that only extreme amounts of damage will ever cause movement. bodybonus works like the Badass trait, so each point will prevent X damage from causing knockback, where X depends on the damage type (usually 12, 7 for fire). (Note that bodybonus accepts only ShortInt values, which range from -128 to 127.) This isn't as guaranteed as the first method, but requires fewer lines of code to execute and doesn't add a new item definition.

Now that we have an indestructible, immovable being, we can try making it useful. One fairly simple possibility is to have it spawn with an item and have it act as a decoy for other enemies to attack while you run from them. Here is one way to set it up:

Items{
    name = "hologram projector",
    id = "holo",
    level = 10,
    weight = 30,
    sprite = 0,
    desc = "Creates a virtual replica of yourself. These hellish buffoons won't know the difference!"
    type = ITEMTYPE_PACK,
 
    OnUse = function()
        local p = player:get_position()
        local a = area.around(p,1)
        Level.area_summon(a,"decoy",1)
        return(true)
    end,
}
 
Beings{
    name   = "decoy",
    id     = "decoy",
    ascii  = "@",
    color  = LIGHTGRAY,
    desc   = "It looks just like you!",
    soundID = "soldier",
    sprite = 0,
    weight = 0,
    danger = 0,
    minLev = 0,
    group =  1,
    speed  = 40,
    flags  = {BF_INV},
 
    OnCreate = function(self)
        self.bodybonus = 100
        self.scount = 4000
    end,
 
    OnAction = function(self)
        self:kill()
    end,
}

The item, called holo, uses Level.area_summon() to spawn a single dummy being adjacent to the player. The area is found by first getting the coordinate of the player using player:get_position(), and then using that coordinate to create a square centered around that coordinate with area.around().

The being, called decoy, is very similar to our original dummy, although a few things have changed in order to make it work properly:

  • First, we set speed to a non-zero but small number. This will serve as the decoy's timer.
  • Next, we change the decoy's scount on creation to 4000. Since each turn increases scount by speed, and beings take actions when their scount is 5000, we can calculate that (5000-4000)/40 = 25, which is the amount of game seconds that the being will exist before taking an action.
  • Finally, we modify the decoy's OnAction() hook to remove itself from the map using the being:kill() method. This is used instead of the Level.clear_being() method so that it doesn't interfere with 100% completions (since any being added to the map will add one to the total enemies on the map).
  • In addition to the changes above, we also set group to 1, same as the player's, so that all enemies will attack the decoy just as they would the player. (To be fair, I don't know how well this works with the enemy AI, but this is the only way for this to work short of very specific AI modifications.)

The end result is that we have an item that causes a decoy to spawn next to the player, which lasts for 25 turns and will be attacked by enemies before disappearing. It should work in theory, although the extent to which enemies will attack a pacifist decoy aren't well-known. It is likely that you'll need not to attack enemies yourself to get the best use out of it.

Multi-form Beings

A multi-form being is one that changes while you are fighting it. This can come in a variety of forms (HP loss, actions taken, level-specific triggers) and can cause a variety of changes (stat modifications, weapon changes, new abilities). They are most often created for the purpose of "boss fights" or some similarly final combat in the level or game. Let's begin by setting up the prototype for a multi-form being:

Beings{
    name            = "mecha-baron",
    name_plural     = "mecha-barons",
    id              = "mechbaron",
    ascii           = "H",
    sprite          = 0,
    color           = LIGHTMAGENTA,
    sound_id        = "arachno",
    desc            = "Looks like someone found one of the gunner 'bots."
    kill_desc       = "gunned down by a mecha-baron",
    kill_desc_melee = "smashed to dust by a mecha-baron",
    weight          = 1
    danger          = 15,
    minLev          = 20,
    corpse          = "baron",
    HP              = 140,
    armor           = 0,
    res_bullet      = 25,
    res_melee       = 50,
    res_shrapnel    = 25,
    weapon          = {
        damage = "1d10",
        damagetype = DAMAGE_BULLET,
        fire = 12,
        shots = 10,
        soundID = "chaingun",
        missile = "chaingun",
    }
    toDam           = 15,
    toHit           = 1,
    toHitMelee      = -1,
    attackchance    = 40,
    vision          = 9,
    speed           = 80,
    ai_type         = "melee_ranged_ai",
    flags           = {},
}

The idea behind this multi-form being is that a Baron of hell is inside some kind of vehicle (typically known as a "mech") which augments its fighting power. It has the equivalent of an assault cannon for the attack and never runs out of ammo, though is slightly less accurate to compensate), and a remarkable amount of health and resistances. However, the baron operating this machine isn't quite as skilled a trained marine or the mech's own AI would be, so it's fairly slow at the same time.

Now let's add a second form:

Beings{
    name            = "mecha-baron",
    ....
 
    OnCreate = function(self)
        self:add_property("rogue",false)
    end,
 
    OnAction = function(self)
        if self.hp < 70 and not self.rogue then
            self.hp = 70
            self.name = "rogue mech"
            self.color = DARKGRAY
            self.weapon.fire = 10
            self.tohit = 3
            self.speed = 100
            self.group = 2
            self.rogue = true
            self:msg("Pilot dead: massacre mode activated.")
        end
    end,
}

The idea behind the second form is that the player managed to kill the baron inside of the vehicle, but now that it no longer has a living pilot, the mech begins shooting everything around. In order to implement this form, we add a trigger property called rogue which is set to false upon creation. Once the mecha-baron's HP dips below 70 while rogue is false, we set the HP back to 70, change some properties like name, color, and combat stats, then set rogue to true (so that it doesn't trigger again). group is also set to a value different from normal enemies and the player, which causes the being to now attack and be attacked by everything. Finally, we get a message indicating that this has occured (assuming the player is in sight of the mecha-baron when this happens).

Note that, when the rogue mech finally dies, it will drop a baron corpse: the mech itself cannot be revived, but the mangled body inside of it can, which is why we set this. If you wanted to be creative, you could drop the corpse when the baron "dies", although you should be careful about where it's dropped and if it should be dropped (for instance, corpses typically don't replace fluid or door tiles).

Special Abilities

Even in the base game, there are a variety of special abilities that enemies use. Typically, these are handled either through flags or the OnAction() hook.

While there is really only one flag that takes care of in-game abilities for enemies (the first one here), there are plenty that correspond to benefits and disadvantages given to the player.

  • Immunity to environmental damage, namely walking on acid and lava tiles, is set with the BF_ENVIROSAFE flag. This is also the same flag used with the player activates an envirosuit pack. Custom hazard cells must check for this flag during the OnEnter() hook for it to apply (at which point the tile can deal whatever damage you want it to).
  • Berserk boosts are handled with the BF_BERSERK flag (not to be confused with BF_BERSERKER).
  • Invulnerability to all damage comes from the BF_INV, as described earlier.
  • Increased ammo capacity is gained using BF_BACKPACK. This only works at a +40% bonus: if you want full control (and, to some extent, better handling of ammo in your inventory when you get this effect) you'll want to design a custom script that changes ammomax of all ITEMTYPE_AMMO items, then systematically rearrange the player's inventory when the effect activates.
  • Regeneration can be handled through the BF_REGENERATE flag, which works once per second (rather than action). However, the benefit only applies for up to 20 HP.
  • Class bonuses have a few flags as well:
    • BF_POWERBONUS increases all base-game powerup durations by 50%. If you want to use both base and custom powerups, you'll have to check for this flag on any customs during the OnUse() flag that applies an effect (at which point you can change the duration to whatever you want). When modding with only custom powerups, this flag is superfluous.
    • BF_STAIRSENSE counts all tiles with the CF_STAIRS flag as permanently visible.
    • BF_INSTAUSE reduces all time spent using ITEMTYPE_PACK items to a single turn, rather than the default 1.0s (10 turns).
    • BF_MAPEXPERT is what makes Computer Maps act like Tracking Maps. For modding, this is mostly superfluous.
  • Trait bonuses that don't include a simple change in stats (such the bonus properties) are typically given through flags:
    • Juggler uses the BF_QUICKSWAP flag
    • Berserker uses the BF_BERSERKER flag (attack-based benefits only apply to melee hits)
    • Dualgunner uses the BF_DUALGUN flag (benefits only apply if both weapons in the weapon and prepared slots have the IF_PISTOL flag)
    • Dodgemaster uses the BF_MASTERDODGE flag
    • Intuition uses several flags:
      • BF_POWERSENSE reveals powerups
      • BF_BEINGSENSE reveals enemies (using the intuition color/symbol from config.lua)
      • BF_LEVERSENSE1 and BF_LEVERSENSE2 provide more detailed information about levers. Custom levers can be made to take advantage of these flags, if you so choose to.
    • Shottyman uses the BF_SHOTTYMAN flag (benefits only apply to items with the IF_SHOTGUN flag)
    • Vampyre uses the BF_VAMPYRE flag (benefits only apply to melee kills)
    • Army of the Dead uses the BF_ARMYDEAD flag. (benefits only apply to weapons that use DAMAGE_SHRAPNEL)
    • Survivalist uses two flags:
      • BF_HARDY allows protection and resistances to reduce damage taken to zero. On Survivalist, this only works 50% of the time, but the BF_HARDY flag is hard-coded to work in every case.
      • BF_MEDPLUS allows med-packs to heal above 100%. Dependings on how you create your own health supplies, this may be a superfluous flag.
    • Blademaster uses the BF_CLEAVE flag (benefits only apply to melee kills)
    • Gun Kata uses the BF_GUNKATA flag (benefits only apply to weapons with the IF_PISTOL flag)
    • Sharpshooter uses the BF_PISTOLMAX flag (benefits only apply to weapons with the IF_PISTOL flag)
    • Fireangel uses the BF_FIREANGEL flag
    • Scavenger uses the BF_SCAVENGER flag. Since non-player beings don't know how to unload items, this is useless on any being but the player.
  • Challenge perks can be arranged through some flags:

"Smart" Beings

Normally monsters act solely to kill the player. There are, however, some flags that allow beings to act more intelligently while they move around, without needing to define your own AI:

  • "BF_USESITEMS" allows the being to pick up items with the "IF_AIHEALPACK" flag. Armor will be immediately equipped on pickup, but the being will not carry more than one armor at a time. Consumables will be used when the being is below 50% of their health.
  • "BF_OPENDOORS" allows the being to open doors, if their path would require them to move into a door.
  • "BF_DISTANCE" is a flag that causes the being, as opposed to normal movement, not to seek engagement with its attacker. Useful for enemies that don't have much of a melee weapon but a powerful ranged weapon.
  • "BF_CHARGE" allows the being to enter hazardous tiles when moving around. Useful for enemies that you set immune to hazardous damage (through "BF_ENVIROSAFE").
  • "BF_HUNTING" sets the being to precisely target the player, regardless of where the player is. Note that this flag does not necessarily pick the best path, so beings can stll become "stuck". Unlike other flags here, BF_HUNTING works with lua-scripted AI.

These flags are still somewhat limited with regards to being intelligence, but they cover a variety of needs without having to hassle with more advanced setups.

Personal tools