Skip to content

Monsters

Almost every monster in Oxidus is a data file. You write what it is called, what level it is, and what it drops, and the game builds you a working creature that fights, dies, and hands out loot. There is no LPC to write.

The whole lib contains exactly one hand-coded NPC, and seventeen data-driven ones. That ratio is the recommendation.

Drop a .lpml file in d/mobs/:

d/mobs/marsh_toad.lpml
{
type: "monster",
name: "marsh toad",
short: "a bloated marsh toad",
long: "A toad the size of a dog, slick with brackish water and breathing "
"in slow, wet heaves.",
id: ["toad", "marsh toad"],
level: [3, 5],
race: "amphibian",
weapon name: "tongue",
weapon type: "bludgeoning",
}

That is a complete, working monster. It has hit points, it fights back, it can be killed, and it leaves a corpse.

FieldWhat it does
typeRequired. Picks the base class — see Types below
nameWhat the game calls it internally
shortThe one-line description in a room
longWhat a player sees when they look at it
idWhat a player can type to refer to it
levelA number, or [low, high] for a range rolled per spawn
genderA string, a list to pick from, or a weighted mapping
raceMostly flavour — see Race
weapon nameWhat its attacks are called. Defaults to fist
weapon typeThe damage type it deals. Defaults to bludgeoning
damageBase damage. Leave it out and the level decides
attack speedSeconds between swings
plus attack speedAdjusts the default — positive makes it faster
lootSee Loot
loot chanceDefault drop chance for the loot list
coinsSee Loot
simple procsSee Procs
proc chanceHow often a proc fires at all

type names a base class in std/mobs/. Four exist:

TypeFor
monsterThe generic base. Use this unless one of the others fits
mammalFurred things
rodentSmall scurrying things
insectInsects

A type with no matching file in std/mobs/ means the monster does not compile at all, so check the name.

The single most useful thing to know here: a monster’s skills come from its level. You do not give a monster combat skills, defence skills, or anything else. Set the level and it is done.

level: 3,

A level 3 monster fights like a level 3 monster. That is deliberate — monsters exist to be level-appropriate opposition, so authoring a skill tree for each one would be a great deal of typing to arrive somewhere the level already describes. See Skills.

A range rolls fresh on every spawn, which is usually what you want:

level: [3, 5],

Both ends are included, so [3, 5] gives 3, 4, or 5. An area’s spawn.lpml rolls its range the same way.

Two fields, both optional. Give the monster a list of things it might drop and a chance that applies to each:

loot: [
"/obj/loot/toad_skin.loot",
"/obj/loot/swamp_gland.loot",
],
loot chance: 60.0,
coins: {
copper: [12, 100.0],
silver: [2, 25.0],
},

Coins read as [amount, chance]. Each entry is rolled on its own, so the toad above always drops twelve copper and drops two silver a quarter of the time.

For per-item chances, use a mapping instead of a list, or nest pairs inside it:

loot: {
"/obj/loot/toad_skin.loot": 80.0,
"/obj/loot/rare_gizzard.loot": 5.0,
},

The items themselves are data files too. See Loot.

A proc is a special attack a monster sometimes uses instead of swinging normally. Give it a tag, the message players see, and a damage type:

simple procs: [
{
tag: "gore",
messages: [
"{{630}}$N $vgore $t with $p short tusks.{{res}}",
],
damage_type: "piercing",
attack_type: "melee",
cooldown: 8,
weight: 100,
severity: "normal",
},
],

The message uses action tokens — $N is the monster, $t is the target, $v conjugates the verb so each reader sees it correctly, and $p is a possessive. Give two messages and the first goes to the target while the second goes to everyone else. The action-messages skill has the full token list.

severity scales the damage, cooldown is seconds before it can fire again, and weight decides how often it is picked when several are eligible. The proc still has to hit — it rolls against the target’s defences like any other attack.

race takes any string:

race: "amphibian",

If a matching module exists in std/modules/race/, the monster gets that race’s body parts and recovery rates. If not — and for every animal race in the lib today, there is not — the name is simply recorded and nothing else happens. No error, no missing behaviour.

So on a monster, race is mostly identity. It becomes load-bearing only when you write a module for it. The ones that exist are the playable races: human, elf, dwarf, gnome, orc, troll.

There is no central spawner. Areas do it themselves, which keeps the pacing of an area in the area that owns it. The forest is the pattern worth copying: a weighted pool in a data file, and rooms that draw from it.

d/forest/spawn.lpml
[
{ path: "mob/crimson_fox", level: [5, 8], weight: 4 },
{ path: "mob/shadow_wolf", level: [8, 10], weight: 2 },
]
object mob = add_inventory(file);
mob->set_level(level);
mob->simple_action("$N $vemerge from the undergrowth.");

Note the path: mob/<name>, with no leading slash and no extension.

File paths in FluffOS resolve from the mudlib root, so mob/crimson_fox is /mob/crimson_fox — and because mob is the first directory in it, the request goes to the monster compiler, which looks up d/mobs/crimson_fox.lpml by filename. There is no /mob/ directory on disk; the path is purely a routing instruction. That is what lets any area draw from the shared monster pool without copying anything.

new("mob/crimson_fox"); // builds from d/mobs/crimson_fox.lpml

A .mob extension routes there too, and is the way to reach the compiler from a path that does not start with mob/. Either form works; the areas in the lib all use the short one.

Also note the ordering — set_level() is called on the new monster, after creation. That is correct and deliberate, and it matters:

Some things a data file cannot express — a monster that talks back, guards something, or runs its own logic. Then you write one:

inherit STD_NPC;
void setup() {
set_name("the gatekeeper");
set_short("a scarred gatekeeper");
set_id(({ "gatekeeper", "keeper" }));
set_level(12.0);
}

Everything from the data path is available here — add_loot(), add_coin(), set_weapon_type(), and the rest are ordinary functions. The data file is a convenient front end to exactly these calls.

d/village/manor/mon/arcanist.lpc is the lib’s only example, and worth reading before you write your own.

  • Monsters stop thinking in empty rooms, once they are healthy. A monster stops its heartbeat only when the room is empty and it is back to full health — so a wounded one heals up first, then goes quiet. After that: no recovery, no AI, no boon expiry. Anything that must run whether or not somebody is watching belongs in a daemon, not on a monster.
  • set_level() before custom skills. See the caution above.
  • A monster remembers who attacked it, by name. Walk back in and it comes for you — through your death, revival, and relog. See Combat.
  • type must match a file in std/mobs/ or the monster silently fails to compile.
  • Monsters do not persist. Nothing about one is saved. A reboot brings back a fresh one with no memory.
FileRole
d/mobs/*.lpmlThe monster data files
std/mobs/monster.lpcReads the data and builds the monster
std/mobs/mammal.lpc, rodent.lpc, insect.lpcThe other type bases
std/living/npc.lpcNPC behaviour — heartbeat, death, set_level()
std/living/proc.lpcSimple procs
std/living/race.lpcset_race()
adm/daemons/modules/virtual/mob.lpcTurns a mob/<name> path into a monster
d/forest/A worked example of area spawning