Class: Lich::Gemstone::CreatureTemplate

Inherits:
Object
  • Object
show all
Defined in:
documented/gemstone/creature.rb

Overview

Static creature template data (ID-less reference information)

Constant Summary collapse

BOON_ADJECTIVES =

Adjectives that boon creatures may spawn with, stripped during template name lookups to handle creature names like "adroit goblin" (stored and matched as "goblin").

See Also:

%w[
  adroit afflicted apt barbed belligerent blurry canny combative dazzling deft diseased drab
  dreary ethereal flashy flexile flickering flinty frenzied ghastly ghostly gleaming glittering
  glorious glowing grotesque hardy illustrious indistinct keen lanky luminous lustrous muculent
  nebulous oozing pestilent radiant raging ready resolute robust rune-covered shadowy shielded
  shifting shimmering shining sickly green sinuous slimy sparkling spindly spiny stalwart steadfast stout
  tattoed tattooed tenebrous tough twinkling unflinching unyielding wavering wispy
]
BOON_REGEX =

Clean creature name by removing boon adjectives Optimized to use single compiled regex instead of 50+ sequential matches

/^(#{BOON_ADJECTIVES.join('|')})\s+/i.freeze
@@templates =
{}
@@loaded =
false

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data) ⇒ CreatureTemplate

Returns a new instance of CreatureTemplate.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'documented/gemstone/creature.rb', line 33

def initialize(data)
  @name = data[:name]
  @url = data[:url]
  @picture = data[:picture]
  @level = data[:level]&.to_i
  @family = data[:family]
  @type = data[:type]
  @undead = data[:undead]
  # Tri-state (true/false/nil) - nil means uncatalogued/unknown, not false.
  @blood = data[:blood]
  @bones = data[:bones]
  @limbs = data[:limbs]
  @witherable = data[:witherable]
  @sympathy = data[:sympathy]
  @muggable = data[:muggable]
  @sleepable = data[:sleepable]
  @boss = data[:boss]
  # nil | "pack" | "miniboss" | "boss" - bestiary classification
  # ("boss" = the once-per-30-days uniques). Zone-dependent for a
  # few creatures; the template carries the highest tier.
  @boss_type = data[:boss_type]
  @otherclass = data[:otherclass] || []
  @areas = data[:areas] || []
  @bcs = data[:bcs]
  @max_hp = data[:max_hp]&.to_i || data[:hitpoints]&.to_i
  @speed = data[:speed]
  @height = data[:height].to_i
  @size = data[:size]

  atk = data[:attack_attributes] || {}
  @attack_attributes = OpenStruct.new(
    physical_attacks: atk[:physical_attacks] || [],
    bolt_spells: atk[:bolt_spells] || [],
    warding_spells: normalize_spells(atk[:warding_spells]),
    offensive_spells: normalize_spells(atk[:offensive_spells]),
    maneuvers: atk[:maneuvers] || [],
    special_abilities: (atk[:special_abilities] || []).map { |s| SpecialAbility.new(s) }
  )

  @defense_attributes = DefenseAttributes.new(data[:defense_attributes] || {})
  @treasure = Treasure.new(data[:treasure] || {})
  @messaging = Messaging.new(data[:messaging] || {})
  @special_other = data[:special_other]
  @abilities = data[:abilities] || []
  @alchemy = data[:alchemy] || []
  # Items seen on the creature via LOOK, as plain strings.
  @equipment = data[:equipment] || []
end

Instance Attribute Details

#abilities ⇒ Object (readonly)

Returns the value of attribute abilities.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def abilities
  @abilities
end

#alchemy ⇒ Object (readonly)

Returns the value of attribute alchemy.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def alchemy
  @alchemy
end

#areas ⇒ Object (readonly)

Returns the value of attribute areas.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def areas
  @areas
end

#attack_attributes ⇒ Object (readonly)

Returns the value of attribute attack_attributes.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def attack_attributes
  @attack_attributes
end

#bcs ⇒ Object (readonly)

Returns the value of attribute bcs.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def bcs
  @bcs
end

#boss ⇒ Object (readonly)

Returns the value of attribute boss.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def boss
  @boss
end

#boss_type ⇒ Object (readonly)

Returns the value of attribute boss_type.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def boss_type
  @boss_type
end

#defense_attributes ⇒ Object (readonly)

Returns the value of attribute defense_attributes.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def defense_attributes
  @defense_attributes
end

#equipment ⇒ Object (readonly)

Returns the value of attribute equipment.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def equipment
  @equipment
end

#family ⇒ Object (readonly)

Returns the value of attribute family.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def family
  @family
end

#height ⇒ Object (readonly)

Returns the value of attribute height.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def height
  @height
end

#level ⇒ Object (readonly)

Returns the value of attribute level.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def level
  @level
end

#max_hp ⇒ Object (readonly)

Returns the value of attribute max_hp.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def max_hp
  @max_hp
end

#messaging ⇒ Object (readonly)

Returns the value of attribute messaging.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def messaging
  @messaging
end

#name ⇒ Object (readonly)

Returns the value of attribute name.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def name
  @name
end

#otherclass ⇒ Object (readonly)

Returns the value of attribute otherclass.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def otherclass
  @otherclass
end

#picture ⇒ Object (readonly)

Returns the value of attribute picture.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def picture
  @picture
end

#size ⇒ Object (readonly)

Returns the value of attribute size.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def size
  @size
end

#special_other ⇒ Object (readonly)

Returns the value of attribute special_other.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def special_other
  @special_other
end

#speed ⇒ Object (readonly)

Returns the value of attribute speed.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def speed
  @speed
end

#treasure ⇒ Object (readonly)

Returns the value of attribute treasure.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def treasure
  @treasure
end

#type ⇒ Object (readonly)

Returns the value of attribute type.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def type
  @type
end

#undead ⇒ Object (readonly)

Returns the value of attribute undead.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def undead
  @undead
end

#url ⇒ Object (readonly)

Returns the value of attribute url.



14
15
16
# File 'documented/gemstone/creature.rb', line 14

def url
  @url
end

Class Method Details

.[](name) ⇒ Object

Lookup template by name



167
168
169
170
171
172
173
174
175
176
177
178
# File 'documented/gemstone/creature.rb', line 167

def self.[](name)
  load_all unless @@loaded
  return nil unless name

  # Try exact match first
  template = @@templates[name.downcase]
  return template if template

  # Try with boon adjectives removed
  normalized_name = fix_template_name(name)
  @@templates[normalized_name]
end

.all ⇒ Object

Get all loaded templates



181
182
183
184
# File 'documented/gemstone/creature.rb', line 181

def self.all
  load_all unless @@loaded
  @@templates.values.uniq
end

.at_uid(uid) ⇒ Object

Templates for the creatures found at the given game room uid.



199
200
201
# File 'documented/gemstone/creature.rb', line 199

def self.at_uid(uid)
  all.select { |t| t.found_at_uid?(uid) }
end

.fix_template_name(template_name) ⇒ String

Cleans a creature name by removing a leading boon adjective and lowercasing.

Produces the lookup key for template storage and retrieval: "Adroit Orc" becomes "orc". Uses a precompiled regex for efficiency (50+ adjectives checked in one pass).

Examples:

CreatureTemplate.fix_template_name("Adroit Orc") #=> "orc"
CreatureTemplate.fix_template_name("Orc") #=> "orc"

Parameters:

  • template_name (String) —

    the creature name to normalize

Returns:

  • (String) —

    the normalized name, lowercased and stripped of leading boon adjective

See Also:



146
147
148
149
150
# File 'documented/gemstone/creature.rb', line 146

def self.fix_template_name(template_name)
  name = template_name.dup.downcase
  name.sub!(BOON_REGEX, '')
  name.strip
end

.load_all(dir = File.join(File.dirname(__FILE__), 'creatures')) ⇒ Object

Load all templates from files dir defaults to the real templates directory; overridable so tests can load an isolated fixture directory instead.



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'documented/gemstone/creature.rb', line 85

def self.load_all(dir = File.join(File.dirname(__FILE__), 'creatures'))
  return if @@loaded
  return unless File.directory?(dir)

  template_count = 0
  Dir[File.join(dir, '*.rb')].each do |path|
    next if File.basename(path) == '_creature_template.rb'

    # Filename-derived name, used only as a fallback for files that
    # don't set their own :name - the file's own value takes priority
    # so a name with characters the filename can't represent (a
    # hyphen, an apostrophe) still round-trips correctly. Both the
    # display name and the lookup key come from the same source now;
    # previously both were overwritten from the filename regardless of
    # what the file itself said, silently breaking lookup for any
    # creature whose real name a slugified filename can't represent
    # exactly (e.g. "shield-maiden" -> "shield_maiden" -> "shield maiden").
    fallback_name = File.basename(path, '.rb').tr('_', ' ')

    begin
      # Safer loading with validation
      file_content = File.read(path)
      data = load_template_data(file_content, path)
      next unless data.is_a?(Hash)

      data[:name] = fallback_name if data[:name].to_s.strip.empty?
      normalized_name = fix_template_name(data[:name])
      if @@templates.key?(normalized_name) && $creature_debug
        respond "--- warning: '#{fallback_name}' collides with an already-loaded template on lookup key '#{normalized_name}' - one will silently overwrite the other"
      end
      template = new(data)
      @@templates[normalized_name] = template
      template_count += 1
    rescue StandardError, ScriptError => e
      # ScriptError (SyntaxError's parent) isn't a StandardError, so a
      # single template file with malformed Ruby would otherwise abort
      # load_all entirely instead of just being skipped.
      respond "--- error loading template #{fallback_name}: #{e.message}" if $creature_debug
    end
  end

  @@loaded = true
  respond "--- loaded #{template_count} creature templates" if $creature_debug
end

Instance Method Details

#boundary_rooms ⇒ Object

Rooms bordering the creature's rooms: connected by an edge - in either direction - to a room the creature is found in, without being one themselves (bigshot's perimeter list). The reverse-edge pass scans the map once; the result is memoized.



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'documented/gemstone/creature.rb', line 229

def boundary_rooms
  @boundary_rooms ||= begin
    inside = {}
    rooms.each { |id| inside[id] = true }
    border = {}
    inside.each_key do |id|
      room = Lich::Common::Map[id] or next
      (room.wayto || {}).each_key do |dest|
        d = dest.to_i
        border[d] = true unless inside[d]
      end
    end
    Lich::Common::Map.list.compact.each do |room|
      next if inside[room.id] || border[room.id]

      border[room.id] = true if (room.wayto || {}).keys.any? { |dest| inside[dest.to_i] }
    end
    border.keys.sort
  end
end

#found_at_uid?(uid) ⇒ Boolean

Whether the creature is found at the given game room uid. Checks the ranges directly, so no expansion cost.

Returns:

  • (Boolean)


194
195
196
# File 'documented/gemstone/creature.rb', line 194

def found_at_uid?(uid)
  @areas.any? { |a| Array(a[:uids]).any? { |r| r.cover?(uid) } }
end

#has_blood? ⇒ Boolean?

Returns whether the bestiary template says the creature has blood.

Returns:

  • (Boolean, nil) —

    true or false when catalogued; nil when unknown.



253
254
255
# File 'documented/gemstone/creature.rb', line 253

def has_blood?
  @blood
end

#has_bones? ⇒ Boolean?

Returns whether the bestiary template says the creature has bones.

Returns:

  • (Boolean, nil) —

    true or false when catalogued; nil when unknown.



260
261
262
# File 'documented/gemstone/creature.rb', line 260

def has_bones?
  @bones
end

#has_limbs? ⇒ Boolean?

Returns whether Limb Disruption (708) has limbs to target on this creature ("The X has no limbs left!" on a fresh target = false).

Returns:

  • (Boolean, nil) —

    true or false when catalogued; nil when unknown.



268
269
270
# File 'documented/gemstone/creature.rb', line 268

def has_limbs?
  @limbs
end

#muggable? ⇒ Boolean?

Returns whether the bestiary template says the creature can be mugged.

Returns:

  • (Boolean, nil) —

    true or false when catalogued; nil when unknown.



294
295
296
# File 'documented/gemstone/creature.rb', line 294

def muggable?
  @muggable
end

#rooms ⇒ Object

Every Lich room id the creature is found in, across all areas - ranges combined, deduped, sorted.



212
213
214
# File 'documented/gemstone/creature.rb', line 212

def rooms
  @rooms ||= uids.flat_map { |u| Lich::Common::Map.ids_from_uid(u) }.uniq.sort
end

#rooms_by_area ⇒ Object

name => [Lich room ids] - the per-area display view (eBestiary and friends).



218
219
220
221
222
223
# File 'documented/gemstone/creature.rb', line 218

def rooms_by_area
  @rooms_by_area ||= @areas.to_h do |a|
    ids = Array(a[:uids]).flat_map { |r| r.flat_map { |u| Lich::Common::Map.ids_from_uid(u) } }
    [a[:name], ids.uniq.sort]
  end
end

#sleepable? ⇒ Boolean?

Returns whether the creature can be put to sleep. False comes from the game's own refusal ("does not seem to be affected").

Returns:

  • (Boolean, nil) —

    true or false when catalogued; nil when unknown.



302
303
304
# File 'documented/gemstone/creature.rb', line 302

def sleepable?
  @sleepable
end

#sympathy? ⇒ Boolean?

Returns whether Sympathy (1120) can affect this creature.

Returns:

  • (Boolean, nil) —

    true or false when catalogued; nil when unknown.



287
288
289
# File 'documented/gemstone/creature.rb', line 287

def sympathy?
  @sympathy
end

#uids ⇒ Object

All game uids the creature is found at, expanded from the stored ranges (memoized - ranges stay compact on disk).



188
189
190
# File 'documented/gemstone/creature.rb', line 188

def uids
  @uids ||= @areas.flat_map { |a| Array(a[:uids]).flat_map(&:to_a) }.uniq.sort
end

#witherable? ⇒ Boolean?

Returns whether Wither (1115) has a body to attack on this creature. Not about limbs - the spell strikes all thirteen locations, chest and abdomen most often - and not about corporeality either: it works on non-corporeal undead (wraiths, spectres, lost souls) and fails on the golem and elemental families.

Returns:

  • (Boolean, nil) —

    true or false when catalogued; nil when unknown.



280
281
282
# File 'documented/gemstone/creature.rb', line 280

def witherable?
  @witherable
end