NPCs and Daemons

Two classes from the default bootstrap let you add autonomous behaviour to a world:

  • $daemon — an invisible scheduler that fires a verb on a configurable interval. Use it for ambient effects, periodic housekeeping, broadcasts, or anything that should happen “in the background” with no player attention.

  • $npc — an actor the parser sees as a player. Inherits from both $player (so look, tell, gender, and parser identity work) and $daemon (so it ticks on a schedule). $wanderer is a small subclass that demonstrates the pattern.

Both ship with two wizard convenience commands — @daemon and @npc — that cover the common lifecycle operations.

When to reach for which

Use a $daemon when nothing needs to be visible in the room and the behaviour is “tick a verb every N seconds.” Examples: announce a chime on the hour, sweep stale guest accounts, restock a vendor’s inventory.

Use a $npc (or a subclass of it) when you want something players can look at, talk to, or address as a target — and that also acts on its own. The class costs you a Player row and parser dispatch overhead, so don’t reach for it when a daemon would do.

Daemon lifecycle

Every $daemon carries five properties that change over its life:

Property

Purpose

interval

seconds between ticks; default 60

target

the Object the daemon acts on or speaks to (your subclass uses this however it wants)

periodic_task_id

PK of the live django_celery_beat.PeriodicTask, or None if disabled

tick_count

total ticks since last reset

last_tick_at

ISO-8601 timestamp of the most recent tick

Create

@daemon is wizard-only. A daemon is just an Object whose class chain includes $daemon:

@create $daemon called "Town Crier"
@daemon list

For richer behaviour, define your own subclass with verbs:

@create $daemon called "Generic Town Crier"
@eval _.town_crier = lookup("Generic Town Crier")

Then attach an on_tick verb to your subclass — the dispatcher fires that verb on each tick:

#!moo verb on_tick --on $town_crier

# pylint: disable=return-outside-function,undefined-variable

target = this.get_property("target")
if target is not None:
    target.tell(f"The town crier rings the hour: {this.tick_count} bells.")

Enable, disable, trigger

@daemon enable Town Crier
@daemon trigger Town Crier   # fires once now, synchronously, for testing
@daemon disable Town Crier
@daemon list                 # show all daemons with status

enable creates a django_celery_beat.PeriodicTask through invoke() (periodic=True) and records the PK on the daemon. disable calls cancel_scheduled_task() and clears the pointer. Both are idempotent.

trigger skips the schedule and calls this.tick() directly, which does the bookkeeping (tick_count += 1, last_tick_at = now) and then your on_tick. Use it while developing the verb.

Recycle

@daemon kill Town Crier calls disable() and then delete(). The inherited $daemon.recycle verb also fires disable(), so direct obj.delete() won’t leak a PeriodicTask.

NPC lifecycle

$npc is a $player and a $daemon. Its on_tick calls this.act() — the personality hook. Subclasses override act to decide what to do each cycle. The base act is a no-op.

Create

@npc create Cat

This calls create(name, parents=[$npc], location=context.player.location) and immediately calls ensure_player_record() so the parser sees is_player() == True. The NPC is not connected, so any tell() to it silently drops.

To base an NPC on a custom subclass, use from:

@npc create Crow from $wanderer

For full programmatic control from a bootstrap or verb:

from moo.sdk import create, ensure_player_record, lookup

cat = create("Cat", parents=[lookup("$npc")], location=lookup("The Garden"))
ensure_player_record(cat)

$npc.initialize also calls ensure_player_record as a safety net — direct create() calls don’t need to do it themselves, but the explicit call in @npc create is intentional because some downstream code reads is_player() synchronously and the initialize verb fires on a separate task.

Schedule

Once created, an NPC is just a daemon — start it like any other:

@daemon enable Cat
@daemon trigger Cat       # fire act() once now
@daemon disable Cat
@daemon kill Cat          # disable, drop Player row, delete Object

$npc.recycle calls remove_player_record() and then disable(). The explicit disable() call is needed because passthrough() from $npc.recycle reaches $root_class.recycle through the $player branch and never visits $daemon.

Authoring an act verb

act runs in the daemon’s task context. The NPC is this. The context.parser is None (no player command triggered this). context.player is the NPC itself.

#!moo verb act --on $cat

# pylint: disable=return-outside-function,undefined-variable

import random

if not this.location:
    return

choices = ["The cat stretches.", "The cat washes one paw.", "The cat yawns."]
this.location.announce_all_but(this, random.choice(choices))

announce_all_but(this, msg) is the canonical broadcast call for “everyone in this room except me.” It uses tell() so gag-list filtering and paranoia tracking apply.

Wanderer: a worked example

$wanderer is shipped as a complete NPC subclass. It carries three extra properties:

Property

Default

Purpose

wander_rooms

[]

List of room PKs the wanderer may visit

wander_leave_msg

"%N wanders off."

Broadcast in the room being left

wander_arrive_msg

"%N wanders in."

Broadcast in the room being entered

The act override picks a random room from wander_rooms (excluding the current location), runs both messages through pronoun_sub with %N set to the wanderer’s name, and teleports through moveto.

Set destinations through the @npc destinations wizard subcommand:

@npc create Crow from $wanderer
@npc destinations Crow #20 #21 #22 #23
@daemon enable Crow

@npc destinations Crow with no PK list prints the current destinations.

When the parser dispatches verbs on a daemon or NPC

Daemon ticks never run through the parser. context.parser is None inside on_tick, tick, or act. If your verb wants to fall back to parser-style argument lookup, you must guard:

if context.parser and context.parser.has_dobj_str():
    target_name = context.parser.get_dobj_str()
else:
    target_name = args[0] if args else None

$npc is parser-visible — players can look at it, give it things, or whisper to it. Verb dispatch follows the usual caller → inventory → location → dobj → pobj order. If you want the NPC to respond to direct address (hello Cat), add the verb to the NPC’s class with --dspec this.

Where to look for more

  • moo/bootstrap/default/verbs/daemon/ — base class implementation

  • moo/bootstrap/default/verbs/npc/$npc-specific extensions (including the moveto override that resolves the multi-parent ambiguity)

  • moo/bootstrap/default/verbs/wanderer/act.py — the canonical worked subclass

  • moo/bootstrap/default/tests/test_daemon.py and test_npc.py — test patterns for time-based verbs

  • Objects in the DjangoMOO Database — the class hierarchy table

  • SDK Functionsinvoke(), cancel_scheduled_task(), get_scheduled_task_info(), ensure_player_record(), remove_player_record()