Skip to content

Creature Module Developer Reference

Mahtra edited this page Aug 15, 2026 · 2 revisions

Creature Module — Developer & LLM Reference

Audience: Lich core developers, script authors writing against the runtime creature API, and LLM agents that need a complete, unambiguous model of the module. This is a reference, not a tutorial. If you want a gentle, task-oriented introduction for GemStone, read Using the Creature and Combat Systems in Lich Scripts (GemStone IV). This page documents everything in the creature module and is explicit about what is game-agnostic, what is DragonRealms-specific, and what is GemStone-specific.

Scope note: This page documents the runtime creature layer introduced by the CreatureBase extraction (the shared mixin) and the DragonRealms creature tracking added on top of it (PR #1485). GemStone's bestiary/UCS/HP layer predates the extraction and was refactored onto the shared base at the same time.


Table of contents

  1. Mental model: three layers
  2. File map
  3. Which game is loaded
  4. Game-agnostic layer — Lich::Common::CreatureBase
  5. DragonRealms-specific layer
  6. GemStone-specific layer
  7. XML parser integration (the feed plumbing)
  8. The debug system ($creature_debug)
  9. Registry lifecycle & capacity
  10. Agnostic vs DR vs GS — comparison matrix
  11. Gotchas, edge cases & known issues
  12. Cheat sheets & inline ;e snippets

1. Mental model: three layers

The creature module is deliberately structured in three layers, mirroring the MapBase convention used elsewhere in Lich:

┌─────────────────────────────────────────────────────────────────┐
│  FACADE  (game-specific module)                                   │
│  Lich::Gemstone::Creature      Lich::DragonRealms::Creature       │
│  - thin module of self.* methods; the public API scripts call     │
│  - delegates almost everything to the CreatureInstance class      │
│  - adds the game's feed entry points (GS: none; DR: sync,         │
│    feed_assess) and game-specific stats                           │
└───────────────────────────────┬───────────────────────────────────┘
                                 │ delegates to
┌───────────────────────────────▼───────────────────────────────────┐
│  INSTANCE CLASS  (game-specific class)                             │
│  Lich::Gemstone::CreatureInstance                                 │
│  Lich::DragonRealms::CreatureInstance                             │
│  - one object per live creature, keyed by server `exist` id       │
│  - includes CreatureBase (gets class + instance methods)          │
│  - defines new(id, noun, name), initialize_status_tracking call,  │
│    valid_target?, created_at                                       │
│  - GS adds: HP/injuries/UCS/templates. DR adds: assess backfill.  │
└───────────────────────────────┬───────────────────────────────────┘
                                 │ include Lich::Common::CreatureBase
┌───────────────────────────────▼───────────────────────────────────┐
│  BASE MIXIN  (GAME-AGNOSTIC)                                       │
│  Lich::Common::CreatureBase                                       │
│  - self.included wires ClassMethods (extend) + InstanceMethods    │
│  - id-keyed registry, current-room roster, targets/in_room query  │
│  - <crtrStatus> flag vocabulary + reconciliation, status timers   │
└─────────────────────────────────────────────────────────────────┘

Key architectural facts:

  • CreatureBase is a mixin module, not a data holder. Echoing Lich::Common::CreatureBase shows you the module/its constants — never a list of creatures. All roster/registry state lives in per-class instance variables on the including CreatureInstance class.
  • Each game keeps an independent registry. Because state lives in @instances, @current_room_ids, etc. on the extending class, GemStone and DragonRealms never share a roster even though they run identical code. In practice only one game's classes are ever loaded per session anyway.
  • The <crtrStatus> flag vocabulary is identical across both games. Status reconciliation, the registry, the room roster, and target filtering are all shared. Only the feed shape (how names/ids arrive) and the valid_target? rule differ per game.

2. File map

Path Layer Game
lib/common/creature/creature_base.rb Base mixin Agnostic
lib/dragonrealms/creature.rb Instance class + facade DR
lib/gemstone/creature.rb Instance class + facade + bestiary GS
lib/gemstone/creatures/*.rb Bestiary template data files GS
lib/common/xmlparser.rb Feed plumbing (drives all of the above) Agnostic dispatcher, game-branched
lib/dragonrealms/drinfomon/drvariables.rb DR_BALANCE_VALUES (assess balance parsing) DR
spec/lib/common/creature/creature_base_spec.rb Tests Agnostic
spec/lib/dragonrealms/creature_spec.rb Tests DR
spec/lib/gemstone/creature_spec.rb, spec/lib/gemstone/creature_data_integrity_spec.rb Tests GS

3. Which game is loaded

Only one game's creature classes are loaded per Lich session. Throughout the XML parser the branch is XMLData.game =~ /^GS/ vs XMLData.game =~ /^DR/, and every call into a game facade is guarded with defined?(...):

Lich::Gemstone::Creature.clear_room     if defined?(Lich::Gemstone::Creature)
Lich::DragonRealms::Creature.clear_room if defined?(Lich::DragonRealms::Creature)

Practical consequence for scripts and agents: write against the facade for your game (Lich::DragonRealms::Creature or Lich::Gemstone::Creature), and guard with defined? if the script is meant to run cross-game. Do not reference Lich::Common::CreatureBase directly for data — it holds none.


4. Game-agnostic layer — Lich::Common::CreatureBase

File: lib/common/creature/creature_base.rb. Everything in this section is shared by both games verbatim.

4.1 The included hook and host-class contract

module Lich::Common::CreatureBase
  def self.included(base)
    base.extend(ClassMethods)     # class-level registry/roster/queries
    base.include(InstanceMethods) # per-instance status tracking
  end
end

A host CreatureInstance class that includes CreatureBase must honor this contract, or the shared code raises at runtime:

Requirement Why Used by
Constructor new(id, noun, name) Registry instantiates via this exact positional signature ClassMethods#register
Call initialize_status_tracking from initialize Sets up @status, @status_timestamps, @crtr_flags all InstanceMethods
Expose valid_target? predicate The game supplies its own attackability rule ClassMethods#targets
Expose created_at reader Age-based eviction ClassMethods#cleanup_old
Expose @name / @id ivars Debug headers only InstanceMethods#debug_header

Both games satisfy this (see §5.2 and §6.1).

4.2 Constants

All constants are frozen and live on Lich::Common::CreatureBase.

STATUS_DURATIONS — timed-status lifetimes (seconds)

Maps a canonical status name to an auto-expiry duration. A nil value means the status has a reliable server-sent removal message and therefore must not be auto-expired on a timer.

STATUS_DURATIONS = {
  'breeze'      => 6,   'bind'      => 10,  'web'       => 8,
  'entangle'    => 10,  'hypnotism' => 12,  'calm'      => 15,
  'mass_calm'   => 15,  'sleep'     => 8,
  # nil = has a reliable removal message, no timer:
  'stunned' => nil, 'immobilized' => nil, 'prone' => nil, 'blind' => nil,
  'sunburst' => nil, 'webbed' => nil, 'poisoned' => nil, 'hidden' => nil
}.freeze

CRTR_STATUS_FLAGS<crtrStatus> XML flag → canonical status name

Transient combat statuses. Note the vocabulary reconciliation (e.g. XML immobile → canonical immobilized) so XML- and message-based detection land in the same status entry.

CRTR_STATUS_FLAGS = {
  'immobile' => 'immobilized', 'webbed' => 'webbed', 'sleeping' => 'sleeping',
  'disoriented' => 'disoriented', 'stunned' => 'stunned', 'rooted' => 'rooted',
  'calmed' => 'calm', 'kneeling' => 'kneeling', 'prone' => 'prone',
  'sitting' => 'sitting', 'flying' => 'flying', 'hovering' => 'hovering',
  'hidden' => 'hidden'
}.freeze

CRTR_CLASSIFICATION_FLAGS<crtrStatus> XML flag → classification key (Symbol)

Relationship/classification facts (not transient statuses), stored separately and read via crtr_flag?.

CRTR_CLASSIFICATION_FLAGS = {
  'hostile' => :hostile, 'disengaged' => :disengaged, 'dead' => :dead,
  'sympathetic' => :sympathetic, 'ascended' => :ascended, 'inferior' => :inferior,
  'AscensionBoss' => :ascension_boss, 'MiniBoss' => :mini_boss,
  'challenging' => :challenging, 'rider' => :rider, 'mount' => :mount
}.freeze

ALL_CRTR_FLAGS

CRTR_STATUS_FLAGS.merge(CRTR_CLASSIFICATION_FLAGS) — used for debug snapshots and (in DR) to exclude crtrStatus flag-words from assess-parsed conditions (see §5.5).

KNOWN_FLAG_NAMES

The complete filter vocabulary accepted by targets/in_room:

KNOWN_FLAG_NAMES = (CRTR_STATUS_FLAGS.values +
                    STATUS_DURATIONS.keys +
                    CRTR_CLASSIFICATION_FLAGS.values.map(&:to_s)).uniq.freeze

A filter name outside this set is "unknown" and matches nothing — even when negated with a not_ prefix (see §4.5).

4.3 ClassMethods — registry, roster, queries

These become class methods on the including CreatureInstance (and are re-exposed through the facade). State is held in per-class ivars: @instances (the id => instance hash), @current_room_ids (the live room roster), @max_size, @auto_register.

Method Signature Returns Notes
register register(name, id, noun = nil) instance or nil Marks the id in-room first (always), then registers. Room-marking happens on every call — even when auto-register is off — because the triggering feed event is proof of presence. Returns nil if auto-register is disabled or the registry is full after cleanup.
mark_in_room mark_in_room(id) Boolean true if the id was newly added to the roster.
clear_room clear_room void Empties the room roster only. The persistent registry is untouched.
current_room_ids current_room_ids Array<Integer> Defensive copy (.dup) of the roster — callers can mutate it freely.
[] [](id) instance or nil Lookup by id (id.to_i).
all all Array<instance> Every registered instance (not just in-room).
clear clear void Wipes instances and roster (session reset).
cleanup_old cleanup_old(max_age_seconds = 600) Integer Removes instances whose created_at < now - max_age. Returns count removed. Positional arg (see §6.4 caveat).
configure configure(max_size: 1000, auto_register: true) void Every call resets omitted options to defaults. Pass all options you mean to keep.
auto_register? Boolean Default true.
max_size Integer Default 1000.
size Integer Current instance count.
full? Boolean size >= max_size.
targets targets(*filters) Array<instance> In-room creatures where valid_target? && crtr_flag?(:hostile), then ANDed filters.
in_room in_room(*filters) Array<instance> All in-room creatures (no hostility/validity requirement), then ANDed filters.
debug_on debug_on(level = :changes) the level Sets global $creature_debug.

Private helpers: apply_filters, known_flag?, instances, room_roster.

register capacity escalation. When the registry is full?, register runs progressively more aggressive age-based cleanup before giving up:

[7200, 6300, 5400, 4500, 3600, 2700, 1800, 900].each do |age_threshold|
  cleanup_old(age_threshold)
  break unless full?
end
return nil if full?   # still full after every attempt → refuse to register

So a creature can silently fail to register under sustained pressure. It still gets mark_in_room'd (presence tracked), but no instance is created, so it won't appear in targets/in_room.

4.4 InstanceMethods — per-creature status tracking

Mixed into each instance. Drives the transient status (@status + @status_timestamps) and classification (@crtr_flags) state.

Method Signature Notes
initialize_status_tracking Initializes @status = [], @status_timestamps = {}, @crtr_flags = {}. Host must call this from initialize.
add_status add_status(status, duration = nil) Normalizes to String (symbol or string OK). No-op if already present. Duration falls back to STATUS_DURATIONS; if a duration resolves, an expiry timestamp is stored, else the status has no auto-expiry.
remove_status remove_status(status) Deletes the status and its timestamp.
cleanup_expired_statuses Drops timed statuses past expiry. Called implicitly by the readers below.
has_status? has_status?(status) Runs expiry cleanup first, then membership check.
statuses Copy of currently-active statuses (after expiry).
sync_crtr_status sync_crtr_status(attrs) Full-snapshot reconciliation from a <crtrStatus> tag (see below).
crtr_flag? crtr_flag?(key) Classification flag, Symbol/String key. Always boolean (unseen → false, never nil).
flag_active? flag_active?(key) `has_status?(key)

Private helpers: debug_level (true:changes), debug_header, debug_log, report_crtr_snapshot.

sync_crtr_status is a full snapshot, not a delta. A <crtrStatus> tag reports everything currently active. A missing known flag, or a flag set to "0", means inactive — so absent known flags are cleared, not ignored:

def sync_crtr_status(attrs)
  CRTR_STATUS_FLAGS.each do |xml_name, status|
    if attrs[xml_name] == '1'      then add_status(status)
    elsif @status.include?(status) then remove_status(status)
    end
  end
  CRTR_CLASSIFICATION_FLAGS.each do |xml_name, key|
    @crtr_flags[key] = (attrs[xml_name] == '1')
  end
  report_crtr_snapshot(attrs) if %i[all active].include?(debug_level)
end

Note the asymmetry: statuses are timer-eligible (they may auto-expire before the next snapshot); classification flags are always-sent booleans (unseen ⇒ false).

4.5 Filter semantics (deep dive)

Both targets(*filters) and in_room(*filters) pass filters through apply_filters. Rules:

  • Filters are ANDed. in_room(:hostile, :prone) returns creatures that are both.

  • not_ prefix negates. in_room(:not_dead) returns non-dead. Implemented as flag_active?(key) != negate.

  • Unknown filters match nothing — by design. A filter whose base name (after stripping not_) isn't in KNOWN_FLAG_NAMES collapses the whole result to []:

    return [] unless known_flag?(key)

    This guard is deliberate. Without it, a negated typo (:not_prnoe) would invert "matches nothing" into "matches everything," silently widening a query. Because filters are ANDed, one unknown filter empties the result.

  • A filter matches via flag_active?, so it can name either a status (:prone, :stunned, :webbed, :hidden, …) or a classification key (:hostile, :dead, :mount, …). The two vocabularies are intentionally disjoint.

Accepted filter names = KNOWN_FLAG_NAMES =

  • canonical statuses: immobilized, webbed, sleeping, disoriented, stunned, rooted, calm, kneeling, prone, sitting, flying, hovering, hidden
  • timer statuses: breeze, bind, web, entangle, hypnotism, mass_calm, sleep, blind, sunburst, poisoned
  • classification keys (as strings): hostile, disengaged, dead, sympathetic, ascended, inferior, ascension_boss, mini_boss, challenging, rider, mount

(All usable with a not_ prefix.)


5. DragonRealms-specific layer

File: lib/dragonrealms/creature.rb. Additive: it does not touch DRRoom or the name-string roster the ~120 existing DR scripts rely on. It introduces ID-based creature tracking that DR previously lacked.

5.1 The three DR feeds

Unlike GemStone's single structured room-object tag, DR delivers creature info across three streams:

  1. <crtrStatus exist="…" hostile="1" immobile="1"/> — id-keyed full snapshot of combat status + classification flags. Emitted automatically with room refreshes, but name-less and batched after the room-objs component closes. → drives Creature.sync (creates instances id-first).
  2. room-objs bold text (<pushBold/>a jeol moradu<popBold/>) — names only, no exist id. Consumed for the stream-order name backfill (zip Nth bold name to Nth crtrStatus id, at the next <prompt>, only when counts match).
  3. the assess combat stream (<d cmd='look #NNN'>A jeol moradu</d> (1: …) is behind you at melee range) — the only feed that ties an exist id to a name, plus assess number, relation, range, target. → drives Creature.feed_assess (authoritative name backfill + positional data).

So a DR CreatureInstance is born id-first (flags, no name) from <crtrStatus> and gets its name/position/range backfilled later.

5.2 CreatureInstance (DR) attributes

class Lich::DragonRealms::CreatureInstance; include Lich::Common::CreatureBase. Constructor initialize(id, noun, name) sets all fields to their empty defaults and calls initialize_status_tracking.

Reader Type Source Meaning
id Integer crtrStatus server exist id
noun String / nil derived trailing word of name, e.g. "a jeol moradu""moradu"; drop-in for attack <noun>
name String / nil room-objs or assess display name (accessor: attr_accessor)
created_at Time ctor registration time
assess_number Integer / nil assess 1-based list number
relation String / nil assess e.g. "behind you", "flanking"
assess_status String / nil assess raw parenthetical, e.g. "cursed and solidly balanced"
range Symbol / nil assess :melee, :pole, or :missile
target_id String / nil assess id of who this creature is engaging
target String / nil assess name of who it's engaging ("you", "Holdigor", "a plague spawn")
target_number Integer / nil assess engaged target's assess number
balance String / nil assess a DR_BALANCE_VALUES descriptor ("solidly", "off", …); nil until an assess with a balance phrase
conditions Array assess assess-only afflictions crtrStatus does NOT carry (["cursed"], ["friendly","cursed"]); crtrStatus states live in crtr_flag?/has_status?, not here
enriched_at Time / nil assess when assess enrichment last landed; nil until first assess

Internal ivar @name_from_assess (bool) marks the assess-derived name as authoritative so a later room-objs backfill can't overwrite it.

5.3 CreatureInstance (DR) methods

Method Notes
feed_assess(entry) Backfills from a parsed assess entry (:name, :number, :status, :relation, :range, :target, :target_id, :target_number). Downcases the assess-capitalized name to match room-objs vocabulary, re-derives noun, sets @name_from_assess = true, parses balance/conditions, stamps enriched_at.
apply_room_name(name) Applies a room-objs (stream-order) name + derives noun. No-op if name is nil or @name_from_assess is set (assess wins).
valid_target? DR rule: !crtr_flag?(:dead). DR has no UCS/appendage decoys to exclude.
enriched? !@enriched_at.nil? — whether any assess enrichment has landed.
off_balance? true if balance is worse than "solidly" in DR_BALANCE_VALUES order. false when balance unknown.
condition?(name) @conditions.include?(name.to_s).
cursed? condition?('cursed').
friendly? condition?('friendly')temporary empath manipulation, NOT a permanent classification (see §5.5 / §11).
self.balance_pattern Lazily-built regex matching a DR_BALANCE_VALUES descriptor + balanced?/imbalanced?. Union ordered longest-first so "somewhat off" resolves before "off".
parse_assess_status(status) (private) Splits the parenthetical into [balance, conditions]; drops any word present in ALL_CRTR_FLAGS (crtrStatus tracks those fresh).
derive_noun(name) (private) name.scan(/[A-Za-z'-]+/).last — trailing word, robust to trailing whitespace/punctuation.

DR_BALANCE_VALUES (from lib/dragonrealms/drinfomon/drvariables.rb), worst → best: completely, hopelessly, extremely, very badly, badly, somewhat off, off, slightly off, solidly, nimbly, adeptly, incredibly.

5.4 Creature facade (DR)

module Lich::DragonRealms::Creature — all self.*:

Method Delegates / does
sync(id, flags) DR feed entry point. CreatureInstance.register(nil, id) (id-first, name-less) then sync_crtr_status(flags). Returns instance or nil.
feed_assess(entry) DR feed entry point. Registers by entry[:id] then calls the instance's feed_assess. Returns nil if no id.
[](id) Lookup by id.
targets(*filters) Hostile attackable in-room.
in_room(*filters) All in-room.
clear_room Empty roster.
register(name, id, noun = nil) Manual registration.
configure(**options) Registry config.
stats {instances:, max_size:, auto_register:} (no templates — DR has no bestiary).
clear Wipe instances + roster.
cleanup_old(max_age_seconds = 600) Age eviction (positional).
all Every instance.
debug_on(level = :changes) Sets $creature_debug.

5.5 assess-only conditions vs. crtrStatus flags

DR has two overlapping information sources, and the module keeps them strictly separated by trust model:

  • <crtrStatus> flags are a push source: always sent, always fresh, reconciled as a full snapshot. Read via crtr_flag?(:hostile) / has_status?('immobilized'). States like prone/sleeping/stunned/immobile/ hidden live here.
  • assess fields (range, balance, conditions, relation, target) are a pull snapshot: only current as of the last assess, and they go stale. Poll assess before relying on one; check enriched? / enriched_at for freshness.

parse_assess_status deliberately drops any parenthetical word that also appears in ALL_CRTR_FLAGS (e.g. immobile) from conditions, because that state is tracked fresh via the push source; keeping it would duplicate and could disagree. Only assess-only afflictions (cursed, poisoned, friendly, …) remain in conditions.

friendly? deserves special care: it is a temporary, manipulated state (an Empath ability makes a creature "consider you friend, not foe"). crtrStatus keeps reporting hostile="1" throughout, and it wears off. Do not treat it as a durable friend/foe flag or as equivalent to a summon/pet.


6. GemStone-specific layer

File: lib/gemstone/creature.rb. GemStone predates the DR feed model: its creatures arrive through a single structured room-object tag carrying inline exist id + noun + name, plus an inline (or @pending_crtr_status-deferred) <crtrStatus>. GS adds three big concerns on top of the shared base: HP/ damage/injuries, UCS (Unarmed Combat System) tracking, and the bestiary (CreatureTemplate).

Much of the GS HP/injury/UCS state is populated by the GemStone Combat tracker (Lich::Gemstone::Combat::Tracker), which parses combat text. The creature module stores it; the tracker fills it. See the GemStone Creature & Combat guide for the tracker side.

6.1 CreatureInstance (GS) — HP, injuries, UCS

class Lich::Gemstone::CreatureInstance; include Lich::Common::CreatureBase.

Accessors: id, noun, name, status, injuries, health, damage_taken, created_at, fatal_crit, status_timestamps, ucs_smote, ucs_updated (attr_accessor), plus ucs_position, ucs_tierup (attr_writer, with custom readers that apply TTL).

Constants:

  • BODY_PARTS — 17 valid injury locations (abdomen, back, chest, head, leftArm, leftEye, … rightLeg).
  • UCS_TTL = 120 — UCS data expires after 2 minutes.
  • UCS_SMITE_TTL = 15 — smite (crimson mist) expires after 15 seconds.

Templates:

Method Notes
template Memoized CreatureTemplate[@name].
has_template? !template.nil?.

UCS (Unarmed Combat System):

Method Notes
position_to_tier(pos) "decent"/1 → 1, "good"/2 → 2, "excellent"/3 → 3, else nil.
set_ucs_position(position) Clears @ucs_tierup if tier changed; stamps ucs_updated.
set_ucs_tierup(attack_type) Records tierup vulnerability.
smite! / smote? / clear_smote Crimson-mist tracking; smote? self-expires past UCS_SMITE_TTL.
ucs_expired? true if never updated or older than UCS_TTL.
ucs_position / ucs_tierup (readers) Return nil once ucs_expired?.

HP / injuries / damage:

Method Notes
add_injury(body_part, amount = 1) Raises ArgumentError for a part not in BODY_PARTS.
injured?(location, threshold = 1)
injured_locations(threshold = 1)
add_damage(amount) / reset_damage
mark_fatal_crit! / fatal_crit?
max_hp Template HP → else Combat::Tracker.fallback_hp (guarded) → else 400.
current_hp [max_hp - damage_taken, 0].max.
hp_percent 0–100, rounded to 0.1.
low_hp?(threshold = 25)
dead? current_hp == 0 (HP-based, distinct from the crtrStatus :dead flag).
essential_data Hash snapshot of id/noun/name/status/injuries/health/damage/HP/template/UCS.

Predicates that combine base + GS state:

  • valid_target? (GS rule) — excludes crtrStatus-dead or HP-dead, plus regex decoy/appendage exclusions matching GameObj.targets:
    return false if crtr_flag?(:dead) || dead?
    return false if @name =~ /^animated\b/i && @name !~ /^animated slush/i
    return false if @noun =~ /^(?:arm|appendage|claw|limb|pincer|tentacle)s?$|^(?:palpus|palpi)$/i &&
                    @name !~ /(?:amaranthine|ghostly|grizzled|ancient) kraken tentacle/i
    true
  • muckled? — creature-side analog of the player's Status.muckled?. Deliberately narrow: only statuses that prevent acting (webbed, dead, stunned, sleeping, immobilized, rooted) — excludes penalty-only (disoriented), positional (prone/kneeling/sitting/flying/hovering), and calm.

6.2 CreatureTemplate — the bestiary

Static, ID-less reference data (the GS "bestiary"). Loaded once from lib/gemstone/creatures/*.rb, keyed by normalized name.

Readers: name, url, picture, level, family, type, undead, otherclass, areas, bcs, max_hp, speed, height, size, attack_attributes, defense_attributes, treasure, messaging, special_other, abilities, alchemy. Tri-state predicates (true/false/nil = uncatalogued): has_blood?, has_bones?, muggable?.

Class methods:

Method Notes
load_all(dir = …/creatures) Idempotent (@@loaded guard). Skips _creature_template.rb. Reads each file, evals it (load_template_data via binding.eval, validated as a Hash), normalizes the name, warns on lookup-key collisions (debug only). Rescues StandardError, ScriptError per-file so one bad template can't abort the whole load.
[](name) Auto-loads; exact-match, then fix_template_name (strip boon adjectives) fallback.
all Auto-loads; unique template list.
fix_template_name(name) Downcase + strip leading boon adjective (BOON_REGEX from BOON_ADJECTIVES) + strip.

Name handling is subtle: the file's own :name wins over the filename-derived fallback, so names with characters a slugified filename can't represent (hyphens, apostrophes) still round-trip for lookup.

6.3 Bestiary support classes

All in lib/gemstone/creature.rb, all GS-only:

  • SpecialAbilityname, note.
  • Treasure — booleanized loot facts: has_coins?, has_gems?, has_boxes?, has_skin?, blunt_required?, to_h.
  • Messaging — arrival/flee/death/attack/etc. message templates with {placeholder} support; display(field, subs) and match(field, str). Uses PLACEHOLDER_MAP (Pronoun/pronoun/direction/weapon) and PlaceholderTemplate.
  • DefenseAttributes — ASG + per-attack/per-CS TDs (melee, ranged, bolt, udf, and the *_td warding values), immunities, defensive spells/abilities. Range strings like "10..20" are parsed to Range without eval.
  • PlaceholderTemplate — compiles message templates to display strings (to_display) or regexes (to_regex, cached) and matches (match).

6.4 Creature facade (GS)

module Lich::Gemstone::Creature — same shared surface as DR ([], targets, in_room, clear_room, register, configure, clear, cleanup_old, all, debug_on), plus GS extras:

Method Notes
stats {instances:, templates:, max_size:, auto_register:}includes templates count (DR's does not).
damage_report(**options) Delegates to CreatureInstance.damage_report. ⚠️ See §11 — the target method is not defined in this file.
print_damage_report(**options) Delegates to CreatureInstance.print_damage_report. ⚠️ Same caveat.

cleanup_old is intentionally positional (max_age_seconds = 600) to match the base method and the positional call from Combat::Tracker#cleanup_creatures; a keyword-only signature previously raised ArgumentError on every scheduled cleanup (swallowed by the tracker's rescue), so registry cleanup silently never ran.

GS has no feed entry points on the facade (no sync/feed_assess) — GS creatures are registered directly by the XML parser's room-objs text path (see §7), which calls Creature.register + sync_crtr_status.


7. XML parser integration (the feed plumbing)

File: lib/common/xmlparser.rb. This is where the raw server stream becomes registry state. The dispatcher is agnostic but branches by game.

Room clearing (both games)

On navigation and on a room-objs refresh, the roster is cleared and rebuilt:

Lich::Gemstone::Creature.clear_room     if defined?(Lich::Gemstone::Creature)
Lich::DragonRealms::Creature.clear_room if defined?(Lich::DragonRealms::Creature)

DR then rebuilds the roster from the <crtrStatus> batch that follows the refresh.

<crtrStatus> handling — game-branched

crtr_id = attributes['exist']
crtr_flags = attributes.reject { |k, _| k == 'exist' }
if XMLData.game =~ /^DR/
  Lich::DragonRealms::Creature.sync(crtr_id, crtr_flags)   # apply now, id-first, name-less
  @dr_crtr_ids << crtr_id                                  # remember arrival order
else # GS
  @pending_crtr_status[crtr_id] = crtr_flags               # defer until the <a> text path
end
  • DR: flags are applied immediately (creature created id-first), and the id is queued for stream-order name pairing.
  • GS: flags are stashed in @pending_crtr_status because GS registration
    • room-marking happens on the bolded <a> room-object text path. Applying here and skipping that path would update flags but leave the creature out of the roster after the next clear_room (it'd sync but never reappear in targets/in_room).

GS room-object text path

For a bold <a exist noun> room object whose id is a current target or has pending flags:

creature = Creature.register(text_string, @obj_exist, @obj_noun)
if creature && (pending_flags = @pending_crtr_status.delete(@obj_exist))
  creature.sync_crtr_status(pending_flags)
end

DR name backfill (stream-order, at <prompt>)

DR room-objs bold NPC names carry no <a> tag, so they're captured in stream order (@dr_room_npc_names) and paired to @dr_crtr_ids at the prompt — all-or-nothing, only when the counts match exactly:

if !@dr_crtr_ids.empty? && @dr_crtr_ids.length == @dr_room_npc_names.length
  @dr_crtr_ids.each_with_index { |id, i| Creature[id]&.apply_room_name(@dr_room_npc_names[i]) }
end
@dr_room_npc_names = []; @dr_crtr_ids = []   # reset every prompt

On any mismatch (e.g. a bold room entity that emits no crtrStatus) naming is skipped rather than risk a shifted mis-pair — assess still backfills names by id later. (There is also a future-proof branch in the <a> handler for if DR ever emits GemStone-style <a exist noun> room-objs.)

assess stream parsing (DR name/position authority)

XMLData.parse_assess_line(text, ids) turns one reconstructed assess line into a structured Hash. Supporting constants:

  • ASSESS_RANGES = { 'melee' => :melee, 'pole weapon' => :pole, 'missile' => :missile }
  • ASSESS_RELATION regex matching flanking/facing/behind/advancing on/…
    • target.

The parser: strips the trailing | F face-hint; matches name (number: status) is|are rest at <range> range; extracts an optional target assess number; splits rest into relation + target; normalizes "moving to flank""flanking"; resolves subject/target ids from the ordered <d cmd='look #id'> ids (self has no subject id; negative ids ⇒ PCs). Returns a Hash with :name, :id, :number, :status, :relation, :target, :target_id, :target_number, :range, :self, :pc — or nil for the header/unparseable lines.

At popStream for the assess stream, DR feeds it in (skipping self and PCs):

if XMLData.game =~ /^DR/ && !entry[:self] && !entry[:pc] && defined?(Lich::DragonRealms::Creature)
  Lich::DragonRealms::Creature.feed_assess(entry)
end

XMLData.assess_creatures returns just the creature entries (@assess.reject { |e| e[:self] || e[:pc] }).


8. The debug system ($creature_debug)

A single global toggles live echo of registration/status/flag changes. Creature.debug_on(level) (or CreatureInstance.debug_on) sets it.

Value Effect
false (default) Silent.
true / :changes Report changes only (registration, +/-status, flag transitions).
:all On each <crtrStatus>, report every known flag and its value.
:active On each <crtrStatus>, report only active (true) flags.

report_crtr_snapshot reads straight from the tag attributes, so a snapshot reflects exactly what the feed sent, independent of how the mutation logic applied it. Output goes through respond (visible in your client) prefixed --- <name> (<id>): ….


9. Registry lifecycle & capacity

  • Birth: register (directly in GS via the parser; via sync/feed_assess in DR). Room-marking always happens; instance creation is gated on auto_register? and capacity.
  • Room presence: tracked separately in @current_room_ids, cleared on nav / room refresh and rebuilt from the next feed. targets/in_room read the roster, not the whole registry.
  • Death: a creature confirmed dead (crtr_flag?(:dead), or GS HP-0) stays in the registry and can still show in in_room while the room still reports it. It's excluded from targets (via valid_target?). Once it drops out of the room feed it leaves the roster; the instance lingers in all until aged out.
  • Eviction: cleanup_old(max_age_seconds) by created_at. Auto-invoked with escalating thresholds when register hits full? (see §4.3).
  • Capacity: max_size default 1000; full? register refuses (returns nil) after exhausting cleanup.
  • ID recycling: the server reuses exist ids. A stale cached flag/name could in principle misapply to an unrelated creature that reuses an id — the parser resets its pairing buffers every prompt/refresh to bound this, and reachability logic should not assume an id is unique over time.

10. Agnostic vs DR vs GS — comparison matrix

Concern Agnostic (CreatureBase) DragonRealms GemStone
Registry / roster / []/all/clear/cleanup_old ✅ defines inherits inherits
targets / in_room / filters ✅ defines inherits inherits
<crtrStatus> flag vocab + reconciliation ✅ defines inherits inherits
Status timers (STATUS_DURATIONS) ✅ defines inherits inherits
configure/max_size/auto_register? ✅ defines inherits inherits
Debug ($creature_debug, snapshots) ✅ defines inherits + facade debug_on inherits + facade debug_on
Feed shape 3 streams (crtrStatus id-first + room-objs names + assess) 1 stream (room-objs <a exist noun> + inline/pending crtrStatus)
Facade feed entry points sync, feed_assess none (parser calls register directly)
Name source assess (authoritative) / room-objs (stream-order) inline <a> name
valid_target? rule host-supplied !dead !dead && !HP0 && !animated-decoy && !appendage
Positional/relational data relation, range, target, balance, off_balance? (assess pull)
assess-only conditions (cursed?, friendly?)
HP / damage / injuries
UCS tracking
Bestiary (CreatureTemplate + support classes)
stats extra keys {instances,max_size,auto_register} + templates
damage_report/print_damage_report facade delegates (⚠️ §11)
External populator XML parser XML parser + Combat::Tracker

11. Gotchas, edge cases & known issues

  • CreatureBase holds no data. ;e echo Lich::Common::CreatureBase shows a module, not creatures. Use the game facade's .in_room / .all.
  • Creature.damage_report / print_damage_report are dangling delegations. The GS facade calls CreatureInstance.damage_report(**options) / CreatureInstance.print_damage_report(**options), but no such class methods are defined in lib/gemstone/creature.rb (or anywhere in lib/). Calling them will raise NoMethodError unless a Combat::Tracker-side reopening provides them at runtime. Treat as unsupported / verify before use.
  • configure resets omitted options. configure(max_size: 2000) silently resets auto_register back to true. Always pass every option you want to keep.
  • Unknown filters return []. A typo'd filter (:not_prnoe, :hostle) empties the whole result — it does not raise. Stick to KNOWN_FLAG_NAMES.
  • sync_crtr_status clears absent flags. It's a full snapshot; don't expect a partial tag to be a delta.
  • DR assess fields go stale. range, balance, conditions, relation, target are pull snapshots. Re-assess and check enriched?/enriched_at before relying on them. crtrStatus flags (crtr_flag?, has_status?) are fresh push data.
  • DR friendly? is temporary manipulation, not a durable ally flag. crtrStatus can still say hostile="1" while friendly? is true.
  • DR name pairing is all-or-nothing. If the bold-name count ≠ crtrStatus id count for a refresh, no stream-order names are applied that refresh (assess still names by id later). So a freshly-synced DR creature may have name == nil briefly.
  • current_room_ids is a copy; the roster is not. Mutate the return value freely; never mutate anything the internals hand you elsewhere.
  • GS dead? (HP-0) ≠ crtrStatus :dead. valid_target? and muckled? check both; a filter :dead checks only the crtrStatus classification flag.
  • add_injury raises on bad body parts. Only BODY_PARTS are valid.
  • ID recycling. Don't assume an exist id maps to one creature forever.
  • Registry can refuse registration when full — presence is still marked, but the creature won't appear in queries.

12. Cheat sheets & inline ;e snippets

;e <ruby> executes Ruby inline; use respond/puts to print to your client. Swap Lich::DragonRealms::Creature for Lich::Gemstone::Creature on GS.

DragonRealms

# Everything in the room (live or dead) — the full picture
;e Lich::DragonRealms::Creature.in_room.each { |c| respond "#{c.id} name=#{c.name.inspect} noun=#{c.noun.inspect} dead=#{c.crtr_flag?(:dead)} hostile=#{c.crtr_flag?(:hostile)} range=#{c.range.inspect} balance=#{c.balance.inspect} statuses=#{c.statuses}" }

# Just IDs of dead mobs in the room
;e respond Lich::DragonRealms::Creature.in_room(:dead).map(&:id).inspect

# Just IDs of prone mobs in the room
;e respond Lich::DragonRealms::Creature.in_room(:prone).map(&:id).inspect

# Attackable hostiles only
;e Lich::DragonRealms::Creature.targets.each { |c| respond "#{c.id} #{c.noun} off_balance=#{c.off_balance?}" }

# Registry-wide (not just this room) + stats
;e respond Lich::DragonRealms::Creature.stats.inspect; Lich::DragonRealms::Creature.all.each { |c| respond "#{c.id} #{c.name.inspect} enriched=#{c.enriched?}" }

# Watch it populate live
;e Lich::DragonRealms::Creature.debug_on(:changes)   # :all / :active / false

GemStone

# Everything in the room
;e Lich::Gemstone::Creature.in_room.each { |c| respond "#{c.id} #{c.name} hp=#{c.hp_percent}% dead=#{c.dead?} muckled=#{c.muckled?}" }

# Attackable hostiles, wounded first
;e Lich::Gemstone::Creature.targets.sort_by { |c| c.hp_percent || 100 }.each { |c| respond "#{c.noun} #{c.hp_percent}%" }

# Bestiary lookup
;e t = Lich::Gemstone::CreatureTemplate['kobold']; respond t ? "#{t.name} lvl #{t.level} hp #{t.max_hp} skin=#{t.treasure.has_skin?}" : "no template"

Filter quick reference (both games)

  • Status filters: :immobilized :webbed :sleeping :disoriented :stunned :rooted :calm :kneeling :prone :sitting :flying :hovering :hidden :blind :poisoned :sunburst (plus timer names :breeze :bind :web :entangle :hypnotism :mass_calm :sleep)
  • Classification filters: :hostile :disengaged :dead :sympathetic :ascended :inferior :ascension_boss :mini_boss :challenging :rider :mount
  • Any filter can be negated: :not_dead, :not_prone, …
  • Filters are ANDed; an unknown filter returns [].

Clone this wiki locally