-
Notifications
You must be signed in to change notification settings - Fork 37
Creature Module Developer 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
CreatureBaseextraction (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.
- Mental model: three layers
- File map
- Which game is loaded
-
Game-agnostic layer —
Lich::Common::CreatureBase - DragonRealms-specific layer
- GemStone-specific layer
- XML parser integration (the feed plumbing)
- The debug system (
$creature_debug) - Registry lifecycle & capacity
- Agnostic vs DR vs GS — comparison matrix
- Gotchas, edge cases & known issues
- Cheat sheets & inline
;esnippets
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:
-
CreatureBaseis a mixin module, not a data holder. EchoingLich::Common::CreatureBaseshows you the module/its constants — never a list of creatures. All roster/registry state lives in per-class instance variables on the includingCreatureInstanceclass. -
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 thevalid_target?rule differ per game.
| 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 |
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.
File: lib/common/creature/creature_base.rb. Everything in this section is
shared by both games verbatim.
module Lich::Common::CreatureBase
def self.included(base)
base.extend(ClassMethods) # class-level registry/roster/queries
base.include(InstanceMethods) # per-instance status tracking
end
endA 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).
All constants are frozen and live on Lich::Common::CreatureBase.
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
}.freezeTransient 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'
}.freezeRelationship/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
}.freezeCRTR_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).
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.freezeA filter name outside this set is "unknown" and matches nothing — even when
negated with a not_ prefix (see §4.5).
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 registerSo 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.
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)
endNote the asymmetry: statuses are timer-eligible (they may auto-expire before
the next snapshot); classification flags are always-sent booleans (unseen ⇒
false).
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 asflag_active?(key) != negate. -
Unknown filters match nothing — by design. A filter whose base name (after stripping
not_) isn't inKNOWN_FLAG_NAMEScollapses 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.)
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.
Unlike GemStone's single structured room-object tag, DR delivers creature info across three streams:
-
<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. → drivesCreature.sync(creates instances id-first). -
room-objs bold text (
<pushBold/>a jeol moradu<popBold/>) — names only, noexistid. Consumed for the stream-order name backfill (zip Nth bold name to Nth crtrStatus id, at the next<prompt>, only when counts match). -
the
assesscombat stream (<d cmd='look #NNN'>A jeol moradu</d> (1: …) is behind you at melee range) — the only feed that ties anexistid to a name, plus assess number, relation, range, target. → drivesCreature.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.
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.
| 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.
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. |
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 viacrtr_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 lastassess, and they go stale. Pollassessbefore relying on one; checkenriched?/enriched_atfor 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.
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.
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 matchingGameObj.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'sStatus.muckled?. Deliberately narrow: only statuses that prevent acting (webbed, dead,stunned,sleeping,immobilized,rooted) — excludes penalty-only (disoriented), positional (prone/kneeling/sitting/flying/hovering), andcalm.
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.
All in lib/gemstone/creature.rb, all GS-only:
-
SpecialAbility—name,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)andmatch(field, str). UsesPLACEHOLDER_MAP(Pronoun/pronoun/direction/weapon) andPlaceholderTemplate. -
DefenseAttributes— ASG + per-attack/per-CS TDs (melee,ranged,bolt,udf, and the*_tdwarding values), immunities, defensive spells/abilities. Range strings like"10..20"are parsed toRangewithout eval. -
PlaceholderTemplate— compiles message templates to display strings (to_display) or regexes (to_regex, cached) and matches (match).
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. |
print_damage_report(**options) |
Delegates to CreatureInstance.print_damage_report. |
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.
File: lib/common/xmlparser.rb. This is where the raw server stream becomes
registry state. The dispatcher is agnostic but branches by game.
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.
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_statusbecause 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 nextclear_room(it'd sync but never reappear intargets/in_room).
- room-marking happens on the bolded
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)
endDR 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 promptOn 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.)
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_RELATIONregex matchingflanking/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)
endXMLData.assess_creatures returns just the creature entries
(@assess.reject { |e| e[:self] || e[:pc] }).
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>): ….
-
Birth:
register(directly in GS via the parser; viasync/feed_assessin DR). Room-marking always happens; instance creation is gated onauto_register?and capacity. -
Room presence: tracked separately in
@current_room_ids, cleared on nav / room refresh and rebuilt from the next feed.targets/in_roomread 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 inin_roomwhile the room still reports it. It's excluded fromtargets(viavalid_target?). Once it drops out of the room feed it leaves the roster; the instance lingers inalluntil aged out. -
Eviction:
cleanup_old(max_age_seconds)bycreated_at. Auto-invoked with escalating thresholds whenregisterhitsfull?(see §4.3). -
Capacity:
max_sizedefault 1000;full?register refuses (returns nil) after exhausting cleanup. -
ID recycling: the server reuses
existids. 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.
| 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 ( |
| External populator | — | XML parser | XML parser + Combat::Tracker
|
-
CreatureBaseholds no data.;e echo Lich::Common::CreatureBaseshows a module, not creatures. Use the game facade's.in_room/.all. -
Creature.damage_report/print_damage_reportare dangling delegations. The GS facade callsCreatureInstance.damage_report(**options)/CreatureInstance.print_damage_report(**options), but no such class methods are defined inlib/gemstone/creature.rb(or anywhere inlib/). Calling them will raiseNoMethodErrorunless aCombat::Tracker-side reopening provides them at runtime. Treat as unsupported / verify before use. -
configureresets omitted options.configure(max_size: 2000)silently resetsauto_registerback totrue. 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 toKNOWN_FLAG_NAMES. -
sync_crtr_statusclears 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,targetare pull snapshots. Re-assessand checkenriched?/enriched_atbefore 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 sayhostile="1"whilefriendly?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 == nilbriefly. -
current_room_idsis 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?andmuckled?check both; a filter:deadchecks only the crtrStatus classification flag. -
add_injuryraises on bad body parts. OnlyBODY_PARTSare valid. -
ID recycling. Don't assume an
existid maps to one creature forever. - Registry can refuse registration when full — presence is still marked, but the creature won't appear in queries.
;e <ruby>executes Ruby inline; userespond/putsto print to your client. SwapLich::DragonRealms::CreatureforLich::Gemstone::Creatureon GS.
# 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# 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"- 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
[].