Module: Lich::DragonRealms::DRC

Defined in:
documented/dragonrealms/commons/common.rb

Overview

Namespace for DragonRealms common utilities and helpers.

DRC (DragonRealms Common) provides shared methods for game interaction, item management, combat, music, and character state across Lich scripts.

Defined Under Namespace

Classes: Item

Constant Summary collapse

XML_TAG_PATTERN =

Pattern for XML tags

/<[^>]+>/.freeze
WAIT_RESPONSE_PATTERN =

Pattern for game wait/roundtime responses in bput

/(?:\.\.\.wait |Wait |\.\.\. wait )(?<seconds>[0-9]+)/.freeze
COLLECT_MESSAGES =

Collect command response messages

[
  'As you rummage around',
  'believe you would probably have better luck trying to find a dragon',
  'if you had a bit more luck',
  'The room is too cluttered',
  'one hand free to properly collect',
  'You are sure you knew',
  'You begin to forage around,',
  'You begin scanning the area before you',
  'You begin exploring the area, searching for',
  'You find something dead and lifeless',
  'You cannot collect anything',
  'you fail to find anything',
  'You forage around but are unable to find anything',
  'You manage to collect a pile',
  'You survey the area and realize that any collecting efforts would be futile',
  'You wander around and poke your fingers',
  'You forage around for a while and manage to stir up a small mound of fire ants!'
].freeze
RETREAT_ESCAPE_MESSAGES =

Retreat command response patterns

[
  /You are already as far away as you can get/,
  /You retreat from combat/,
  /You sneak back out of combat/,
  /Retreat to where/,
  /There's no place to retreat to/
].freeze
RETREAT_MESSAGES =

Response patterns indicating the game reacted to a retreat or movement command.

Used by #retreat to detect whether the character moved during combat or attempted to move. Matches indicate the game acknowledged the action, though not necessarily that the character successfully retreated.

Returns:

  • (Array<Regexp>)
[
  /retreat/,
  /sneak/,
  /grip on you/,
  /grip remains solid/,
  /You try to back/,
  /You must stand first/,
  /You stop advancing/,
  /You are already/
].freeze
ASSESS_TEACH_TEACHER_PATTERN =

Assess teach parsing patterns

/(?<teacher>.*) is teaching a class on (?<skill>.*) which is still open to new students/.freeze
ASSESS_TEACH_SKILL_FILTER_PATTERN =

Pattern extracting the filtered skill name from assess teach output.

Matches lines showing a teacher's skill with a comparison clause, capturing the skill name in the filtered_skill group.

Examples:

"arcane magic (compared to what you already know) elemental magic".match(ASSESS_TEACH_SKILL_FILTER_PATTERN)[:filtered_skill]
#=> "elemental magic"

Returns:

  • (Regexp)

See Also:

/.* \(compared to what you already know\) (?<filtered_skill>.*)/.freeze
COMMON_RANGED_WEAPONS_PATTERN =

Common ranged weapon nouns

/^(bow|shortbow|longbow|crossbow|stonebow|latchbow|slurbow|lockbow|pelletbow|arbalest|sling|slingshot|blowgun)$/i.freeze
RACIAL_RANGED_WEAPONS_PATTERN =
/^(jranoki|uku'uan|uku'uanstaho|chunenguti|hhr'ibu|guti|mahil|taisgwelduan|chyeb|sverfil|tangara|alaer|kari|wami|usus|srigos|href|vrope|falocisana|stof|dzelt)$/i.freeze
FLAVOR_TEXT_PATTERN =

Flavor text pattern for item descriptions https://regex101.com/r/4lGY6u/13

/\s?\b(?:(?:colorfully and )?(?:artfully|artistically|attractively|beautifully|bl?ack-|cleverly|clumsily|crudely|deeply|delicately|edged|elaborately|faintly|flamboyantly|front-|fully|gracefully|heavily|held|intricately|lavishly|masterfully|plentifully|prominantly|roughly|securely|sewn|shabbily|shadow-|simply|somberly|skillfully|sloppily|starkly|stitched|tied and|tightly|well-)\s?)?(?:accented|accentuated|acid-etched|adorned|affixed|appliqued|assembled|attached|augmented|awash|backed|back-laced|balanced|banded|batiked|beaded|bearded|bearing|bedazzled|bedecked|bejeweled|beset|bestrewn|blazoned|bordered|bound|braided|branded|brocaded|bristling|brushed|buckled|burned|buttoned|caked|camouflaged|capped|carved|caught|centered|chased|chiseled|cinched|circled|clasped|cloaked|closed|coated|cobbled together|coiled|colored|composed|concealed|connected|constructed|countoured|covered|crafted|crested|crisscrossed|crowded|crowned|cuffed|cut|dangling|dappled|decked|decorated|deformed|depicting|designed|detailed|discolored|displaying|divided|done|dotted|draped|drawn|dressed|drizzled|dusted|edged|elaborately|embedded|embell?ished|emblazed|emblazoned|embossed|embroidered(?: all over| painstakingly)?|enameled(?: across)?|encircled|encrusted|engraved|engulfed|enhanced|entwined|equipped|etched|fashioned(?: so)?|fastened|feathered|featuring|festooned|fettered|filed|filled|firestained|fit|fitted|fixed|flecked|fletched|forged|formed|framed|fringed|frosted|full|gathered|gleaming|glimmering|glittering|goldworked|growing|gypsy-set|hafted|hand-tooled|hanging|heavily(?:-beaded| covered)?|held fast|hemmed|hewn|hideously|highlighted|hilted|honed|hung|impressed|incised|ingeniously repurposed|inscribed|inlaid|inset|interlaced|interspersed|interwoven|jeweled|joined|laced(?: up)?|lacquered|laden|layered|limned|lined|linked|looped|knotted|made|marbled|marked|marred|meshed|mosaicked|mottled|mounted|oiled|oozing|outlined|ornamented|overlai(?:d|n)|padded|painted|paired|patched|pattern-welded|patterned|pinned|plumed|polished|printed|reinforced|reminiscent|rendered|revealing|riddled|ridged|rimed|ringed|riveted|sashed|scarred|scattered|scorched|sculpted|sealed|seamed|secured|securely|set|sewn|shaped|shimmering|shod|shot|shrouded|side-laced|slashed|slung|smeared|smudged|spangled|speckled|spiraled|splatter-dyed|splattered|spotted|sprinkled|stacked|surmounted|surrounded|suspended|stained|stamped|starred|stenciled|stippled|stitched(?: together)?|strapped|streaked|strengthened|strewn|striated|striped|strung|studded|swathed|swirled|tailored|tangled|tapered|tethered|textured|threaded|tied|tightly|tinged|tinted|tipped|tooled|topped|traced|trimmed|twined|veined|vivified|washed|webbed|weighted|whorled|worked|worn|woven|wrapped|wreathed|wrought)?\b ["]?\b(?:a hand-tooled|across|along|an|around|atop|bearing|belted|bright streaks|dangling|designed|detailing|down (?:each leg|one side)|dyed (?:a|and|deep|of|in|night|rust|shimmering|the|to|with)|engravings|entitled|errant pieces|featuring|flaunting|frescoed|from|Gnomish Pride|(?:encased |quartered )?in(?: the)?|into|labeled|leading|like|lining|matching|(?<!stick|slice|chunk|flask|hunk|series|set|pair|piece) of|on|out|overlayed gleaming silver|resembling|shades of color|sporting|surrounding|that|the|through|tinged somber black|titled|to|upon|WAR MONGER|with|within|\b(?:at|bearing|(?:accented |held |secured )?by|carrying|clutching|colored|cradling|dangling|depicting|(?:prominently )?displaying|embossed|etched|featuring|for(?:ming)?|holding|(?<!slice |chunk |flask |hunk |series |set |pair |piece )of|over|patterned|striped|suspending|textured|that)\b \b(?:a (?:band|beaded|brass|cascade|cluster|coral|crown|dead|.+ (?:ingot|boulder|stone|rock|nugget)|fierce|fanged|fringe|glowing|golden|grinning|howling|large|lotus|mosaic|pair|poorly|rainbow|roaring|row|silver(?:y|weave)?|small|snarling|spray|tailored|thick|tiny|trio|turquoise|yellowed)|(?:squared )?agonite (?:links|decorated)|alternating|an|(?:purple |blue )?and|ash|beaded fringe|blackened (?:steel(?: accents| bearing| with|$)|ironwood)|blue (?:gold|steel)|burnished golden|cascading layers|carved ivory|chain-lined|chitinous|(?:deep red|dull black|pale blue) cloth|cloudberry blossoms|colorful tightly|cotton candy|crimson steel|crisscrossed|curious design|curved|crystaline charm|dark (?:blue|green|grey|metals|windsteel) (?:and|exuding|glaes|hues|khor'vela|muracite|pennon|with)|dark supple|deepest|deeply blending|delicate|dusky (?:dreamweave|green-grey)|ebonwood$|emblazoned|enamel?led (?:steel|bronze)|etched|fine(?:-grained| black| crushed)|finely wrought|flame-kissed|forest|fused-together|fuzzy grey|gauze atop|gilded steel|glass eyeballs|glistening green|golden oak|grey fur|hammered|haralun|has|heavy (?:grey|pearl|silver)|horn|Ilithi cedar|inky black|interlocking silver|interwoven|iridescent|jagged interlocking plates|(?:soft dark|supple|thick|woven) (?:bolts|leather)|lightweight|long swaths|lustrous|kertig ravens|made|metal cogs|mirror-finished|mottled|multiple woods|naphtha|oak|oblong sanguine|one|onyx buttons|opposing images|overlapping|pale cerulean|pallid links|pastel-hued|pins|pitted (?:black iron|steel)|plush velvet|polished (?:bronze|hemlock|steel)|raccoon tails|ram's horns|rat pelts|raw|red and blue|rich (?:purple|golden)|riveted bindings|roughened|rowan|sanguine thornweave|scattered star|scorch marks|sculpted|shadows|shark cartilage|shifting (?:celadon|shades)|shipboard|(?:braided |cobalt |deep black |desert-tan |dusky red Taisidon |ebony |exquisite spider|fine leaf-green |flowing night|glimmering ebony |heavy |marigold |pale gold marquisette and virid |rich copper |spiral-braided |steel|unadorned black Musparan )?silk(?:cress)?|(?:coiled |shimmering )?silver(?:steel| and |y)?|sirese blue spun glitter|six crossed|slender|small bones|smoothly interlocking|snow leopard|soft brushed|somber black|sprawled|sun-bleached|steel links|stones|strips of|sunny yellow|teardrop plates|telothian|the|tiny (?:golden|indurium|scales|skull)|tightly braided|tomiek|torn|twists|two|undyed|vibrant multicolored|viscous|waves of|weighted|well-cured|white ironwood|windstorm gossamer|wintry faeweave|woven diamondwood))\b.*/.freeze
CANNOT_STAND_PATTERN =

Game responses to STAND that mean standing cannot currently succeed, so fix_standing must stop instead of looping forever (issue #3668). These are already in the STAND match list, but matching one does not change posture, so without this guard the loop spams STAND endlessly (e.g. while unconscious, plummeting, held, or overburdened).

/unconscious|plummeting to your death|prevents you from standing|don't seem to be able to move|overburdened and cannot|weight of all your possessions|no room to do much of anything/.freeze
DEFAULT_BOX_SUBSTITUTIONS =

Post-match box name rewrites (applied via gsub after a box is matched). "ironwood" -> "iron" because the game parser wants the shortened noun. Players extend this via the custom_box_substitutions setting; see box_list_to_adj_and_noun.

Returns:

  • (Array<Array(String, String)>)

    ordered [from, to] literal pairs

[%w[ironwood iron]].freeze
DEFAULT_SCROLL_SUBSTITUTIONS_PRE =

Item-specific scroll rewrites applied before SCROLL_KEYWORD_COLLAPSE. These full game descriptions contain keywords the collapse would otherwise mangle (e.g. "icy blue vellum scroll" -> "icy scroll", not "icy blue vellum"), or must be caught before the collapse can run. Order matters and is preserved. Players extend this list via the custom_scroll_substitutions setting; see scroll_list_to_adj_and_noun.

Returns:

  • (Array<Array(String, String)>)

    ordered [from, to] literal pairs

[
  ['large midnight-blue scale torn with symbols', 'midnight-blue scale'],
  ['icy blue vellum scroll', 'icy scroll'],
  ['green vellum scroll', 'green scroll'],
  ['fetid antelope vellum', 'antelope vellum'],
  ['papyrus roll', 'papyrus.roll'],
  ['pallid red scroll', 'pallid scroll']
].freeze
DEFAULT_SCROLL_SUBSTITUTIONS_POST =

Adjective-pair scroll rewrites applied after SCROLL_KEYWORD_COLLAPSE, reducing already-collapsed forms (e.g. "stormy grey" -> "stormy"). Order matters and is preserved. Not player-extensible (these operate on the collapsed noun, not the raw description).

Returns:

  • (Array<Array(String, String)>)

    ordered [from, to] literal pairs

[
  ['crumpled paper', 'crumpled'],
  ['pale ricepaper', 'pale'],
  ['stormy grey', 'stormy'],
  ['mossy green', 'mossy'],
  ['dark purple', 'dark'],
  ['vibrant red', 'vibrant'],
  ['bright green', 'bright'],
  ['icy blue', 'blue'],
  ['pearl-white silk', 'silk'],
  ['ghostly white', 'white'],
  ['crinkled violet', 'crinkled'],
  ['drawing paper', 'drawing']
].freeze
SCROLL_KEYWORD_COLLAPSE =

Structural collapse: reduce " <flavor...>" to " " by keeping the noun keyword and dropping trailing flavor. Sandwiched between the pre and post substitution passes.

Returns:

  • (Regexp)
/\s(bark|leaf|ostracon|papyrus|parchment|roll|scroll|tablet|vellum|manuscript)\s.*/.freeze

Class Method Summary collapse

Class Method Details

.assess_teachHash<String, String>

Parses the ASSESS TEACH command output and returns active teachers and their skills.

Issues ASSESS TEACH and collects lines until roundtime. Filters out lines indicating no one is teaching or the character is teaching. Returns a hash mapping teacher names to their skill (filtered to the comparison skill if present).

Examples:

DRC.assess_teach #=> { "Master Trainer" => "melee", "Sage" => "scholarship" }

Returns:

  • (Hash<String, String>)

    teacher name => skill being taught, or {} if no teachers

See Also:

  • #parse_assess_teach_lines


801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
# File 'documented/dragonrealms/commons/common.rb', line 801

def assess_teach
  lines = Lich::Util.issue_command(
    'assess teach',
    /is teaching a class|No one seems to be teaching|You are teaching a class/,
    /Roundtime/,
    usexml: false,
    quiet: true,
    include_end: false
  )
  waitrt?
  return {} if lines.nil?
  return {} if lines.any? { |l| l.match?(/No one seems to be teaching|You are teaching a class/) }

  parse_assess_teach_lines(lines.map(&:strip).reject(&:empty?))
end

.atmo(text, make_bold = true) ⇒ Object

Sends a message to the atmospherics window. By default the message is bold. DEPRECATED in favor of log_window



1471
1472
1473
# File 'documented/dragonrealms/commons/common.rb', line 1471

def atmo(text, make_bold = true)
  log_window(text, "atmospherics", make_bold)
end

.beepObject

windows only I believe.



734
735
736
# File 'documented/dragonrealms/commons/common.rb', line 734

def beep
  echo("\a")
end

.bold(text) ⇒ Object

Helper function to wrap text in the necessary markup to make it render as bold in a frontend client. Used by atmo and log_window methods.



1496
1497
1498
1499
1500
# File 'documented/dragonrealms/commons/common.rb', line 1496

def bold(text)
  prefix = Frontend.supports_gsl? ? "\034GSL\r\n " : "<pushBold\/>"
  suffix = Frontend.supports_gsl? ? "\034GSM\r\n " : "<popBold\/>"
  "#{prefix}#{text}#{suffix}"
end

.box_list_to_adj_and_noun(list) ⇒ Array<String>

Take a game formatted list of boxes "a reinforced wooden strongbox and a plain ironwood crate" and return ["wooden strongbox", "iron crate"].

The recognized wood and container words are BOX_WOODS and BOX_CONTAINERS merged with the player's custom_box_woods / custom_box_containers settings, so a player can teach Lich about a box material or container it does not yet know without a Lich release. The global $box_regex (built from the same defaults) is left untouched for third-party scripts. Post-match rewrites come from DEFAULT_BOX_SUBSTITUTIONS merged with custom_box_substitutions.

Examples:

box_list_to_adj_and_noun('an ironwood crate') #=> ['iron crate']

Parameters:

  • list (String)

    game-formatted box list (e.g. from rummage /B)

Returns:

  • (Array<String>)

    gettable box adjective+noun names

See Also:



474
475
476
477
478
479
480
481
482
483
484
# File 'documented/dragonrealms/commons/common.rb', line 474

def box_list_to_adj_and_noun(list)
  woods = CustomSubstitutions.resolve(:custom_box_woods, BOX_WOODS, type: :names)
  containers = CustomSubstitutions.resolve(:custom_box_containers, BOX_CONTAINERS, type: :names)
  substitutions = CustomSubstitutions.resolve(:custom_box_substitutions, DEFAULT_BOX_SUBSTITUTIONS, type: :pairs)
  box_regex = /((?:#{woods.map { |wood| Regexp.escape(wood) }.join('|')}) (?:#{containers.map { |container| Regexp.escape(container) }.join('|')}))/
  list.strip
      .split(box_regex)
      .reject(&:empty?)
      .select { |item| item =~ box_regex }
      .map { |box| substitutions.reduce(box) { |current, (from, to)| current.gsub(from, to) } }
end

.bput(message, *matches) ⇒ Object

Like fput but better because will wait for RT before performing command and do smart retries. Will wait for matching text up to 15 seconds then timeout. Also recovers from some limited failures wherein we want to simply fix the issue and retry the bput, like when we're prone and need to be standing. Complex handling should be done within the calling script.



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'documented/dragonrealms/commons/common.rb', line 126

def bput(message, *matches)
  options = (matches.shift if matches.first.is_a?(Hash)) || {}
  options['timeout'] ||= 15
  options['ignore_rt'] ||= false

  timeout = options['timeout']
  ignore_rt = options['ignore_rt']
  suppress = options['suppress_no_match']

  if options['debug']
    echo "bput.message=#{message}"
    echo "bput.options=#{options}"
    echo "bput.matches=#{matches}"
  end

  waitrt? unless ignore_rt
  log = []
  matches.flatten!
  matches.map! { |item| item.is_a?(Regexp) ? item : /#{item}/i }
  clear
  put message
  timer = Time.now
  while (response = get?) || (Time.now - timer < timeout)

    if response.nil?
      pause 0.1
      next
    end

    log += [response]

    case response
    when /^For some strange reason you are unable to do that\.  The world somehow seems frozen in place/
      # Zadraes - 13:32 It's a "You're in an area actively being updated" message
      pause 1
      put message
      timer = Time.now
      next
    when WAIT_RESPONSE_PATTERN
      unless ignore_rt
        wait_match = response.match(WAIT_RESPONSE_PATTERN)
        pause(wait_match[:seconds].to_i - 0.5)
        waitrt?
        put message
        timer = Time.now
      end
      next
    when /Sorry, you may only type ahead/
      pause 1
      put message
      timer = Time.now
      next
    when /^You can't do that while you are asleep./
      put 'wake'
      put message
      timer = Time.now
      next
    when /^You are a bit too busy performing to do that/, /^You should stop playing before you do that/
      put 'stop play'
      put message
      timer = Time.now
      next
    when /would give away your hiding place/
      release_invisibility
      put 'unhide'
      put message
      timer = Time.now
      next
    when /^You don't seem to be able to move to do that/
      next unless matches.include?(response)
    when /^You are still stunned/
      pause 0.5 while stunned?
      pause 0.5
      put message
      timer = Time.now
      next
    when /^You can't do that while entangled in a web/
      pause 0.5 while webbed?
      pause 0.5
      put message
      timer = Time.now
      next
    when /^You must be standing/, /^You should stand up first/, /^You'll need to stand up first/, /^You can't do that while (sitting|kneeling|lying)/, /^You should be sitting up/, /^You really should be standing to play/, /^After failing to draw a breath for what feels like forever/
      fix_standing
      waitrt?
      put message
      timer = Time.now
      next
    end

    matches.each do |match|
      if (result = response.match(match))
        return result.to_a.first
      end
    end
  end

  unless suppress
    Lich::Messaging.msg("bold", "DRC: No match was found after #{timeout} seconds for command '#{message}'")
    Lich::Messaging.msg("bold", "DRC: Messages seen: #{log.length}")
    log.reverse.each { |logged_response| Lich::Messaging.msg("bold", "DRC: > #{logged_response}") }
    Lich::Messaging.msg("bold", "DRC: Checked against: #{matches}")
  end

  ''
end

.can_see_sky?Boolean

Checks whether the character can see the sky at the current location.

Issues the WEATHER command and interprets the response: characters indoors without sky access return false; outdoor locations and indoors with visible sky (windows, skylights) return true.

Examples:

DRC.can_see_sky? #=> true (in an outdoor area or room with skylight)

Returns:

  • (Boolean)

    true if sky is visible, false if indoors with no sky view



286
287
288
289
290
291
292
293
294
295
# File 'documented/dragonrealms/commons/common.rb', line 286

def can_see_sky?
  # If you are indoors and not able to see the sky.
  inside_no_sky = "That's a bit hard to do while inside."
  # If you are indoors but able to see the sky (e.g. a window or skylight).
  inside_yes_sky = "You glance outside"
  # If you are outdoors.
  outside = "You glance up at the sky"
  # Can we see the sky?
  bput("weather", inside_no_sky, inside_yes_sky, outside) != inside_no_sky
end

.check_encumbrance(refresh = true) ⇒ Object

Checks the character's current encumbrance level.

If refresh is true, issues the ENCUMBRANCE command and parses the result; otherwise uses the cached Lich::DragonRealms::DRStats.encumbrance value. Returns the encumbrance level (e.g., 'lightly', 'heavily') mapped via $ENC_MAP to a game constant.

Examples:

DRC.check_encumbrance #=> (queries game and returns current level)

Parameters:

  • refresh (Boolean) (defaults to: true)

    whether to re-issue the command (default: true)

Returns:

  • (Object)

    the encumbrance level constant



950
951
952
953
954
955
956
957
958
959
# File 'documented/dragonrealms/commons/common.rb', line 950

def check_encumbrance(refresh = true)
  encumbrance = DRStats.encumbrance
  if refresh
    encumbrance_pattern = /(?:Encumbrance)\s:\s(?<encumbrance>.*)/
    result = bput('encumbrance', encumbrance_pattern)
    enc_match = result.match(encumbrance_pattern)
    encumbrance = enc_match[:encumbrance] if enc_match
  end
  $ENC_MAP[encumbrance]
end

.clean_instrument(settings, worn = true) ⇒ Boolean

Cleans an instrument using a chamois cloth via repeated wipe and wring cycles.

Gets the cloth and instrument (removing it first if worn). Loops wiping and wringing until the cloth is dry. Then loops cleaning until the instrument is no longer dirty. Re-wears the instrument if it was worn. Returns false if the cloth or instrument cannot be retrieved, and sends a beep alert. Re-stands if needed.

Examples:

DRC.clean_instrument(settings) #=> true

Parameters:

  • settings (OpenStruct)

    settings with cleaning_cloth, instrument, and worn_instrument

  • worn (Boolean) (defaults to: true)

    whether the instrument is worn (default: true)

Returns:

  • (Boolean)

    true if cleaning succeeded, false if cloth/instrument could not be obtained

See Also:

  • #stop_playing


1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
# File 'documented/dragonrealms/commons/common.rb', line 1142

def clean_instrument(settings, worn = true)
  cloth = settings.cleaning_cloth
  instrument = worn ? settings.worn_instrument : settings.instrument

  unless DRCI.get_item?(cloth)
    Lich::Messaging.msg("bold", "DRC: You have no chamois cloth -- this could cause problems with playing an instrument!")
    DRC.beep
    return false
  end
  DRC.stop_playing

  if worn
    unless DRCI.remove_item?(instrument)
      Lich::Messaging.msg("bold", "DRC: Could not remove #{instrument}, putting away cloth, and not trying to clean.")
      DRCI.stow_item?(cloth)
      DRC.beep
      return false
    end
  else
    unless DRCI.get_item?(instrument)
      Lich::Messaging.msg("bold", "DRC: Could not get #{instrument}, putting away cloth, and not trying to clean.")
      DRCI.stow_item?(cloth)
      DRC.beep
      return false
    end
  end

  loop do
    case DRC.bput("wipe my #{instrument} with my #{cloth}", 'Roundtime', 'not in need of drying', 'You should be sitting up')
    when 'not in need of drying'
      break
    when 'You should be sitting up'
      DRC.fix_standing
      next
    end
    pause 1
    waitrt?

    until /you wring a dry/i =~ DRC.bput("wring my #{cloth}", 'You wring a dry', 'You wring out')
      pause 1
      waitrt?
    end
  end

  until /not in need of cleaning/i =~ DRC.bput("clean my #{instrument} with my #{cloth}", 'Roundtime', 'not in need of cleaning')
    pause 1
    waitrt?
  end

  DRCI.wear_item?(instrument) if worn
  DRCI.stow_item?(cloth)
  true
end

.collect(item, practice = true) ⇒ void

This method returns an undefined value.

Attempts to collect an item, optionally in practice mode.

Issues the COLLECT command with optional 'practice' flag. If the room is too cluttered, attempts to kick piles and retry. Waits for roundtime before returning.

Examples:

DRC.collect('stone')

Parameters:

  • item (String)

    the item to collect

  • practice (Boolean) (defaults to: true)

    whether to collect in practice mode (default: true)



341
342
343
344
345
346
347
348
349
350
351
# File 'documented/dragonrealms/commons/common.rb', line 341

def collect(item, practice = true)
  practicing = "practice" if practice

  case bput("collect #{item} #{practicing}", COLLECT_MESSAGES)
  when 'The room is too cluttered'
    return unless kick_pile?

    collect(item)
  end
  waitrt?
end

.do_tune(instrument, tuning = "") ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Recursively tunes an instrument by comparing game feedback and adjusting sharp/flat.

Issues TUNE MY INSTRUMENT [TUNING] and reads the response. If tuned, returns true. If flat, recursively calls with "sharp"; if sharp, calls with "flat". If the character must stand first, calls #fix_standing and retries. Returns false if the instrument is not in hand.

Examples:

DRC.do_tune("lute") #=> true (if successfully tuned)

Parameters:

  • instrument (String)

    the instrument name

  • tuning (String) (defaults to: "")

    optional tuning direction ("sharp" or "flat") (default: "")

Returns:

  • (Boolean)

    true if in tune, false if not in hand



1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
# File 'documented/dragonrealms/commons/common.rb', line 1255

def do_tune(instrument, tuning = "")
  unless instrument && DRCI.in_hands?(instrument)
    Lich::Messaging.msg("bold", "DRC: No instrument found in hands. Not trying to tune.")
    DRC.beep
    return false
  end

  case DRC.bput("tune my #{instrument} #{tuning}",
                /^You should be sitting up/,
                /After a moment, you .* flat/,
                /After a moment, you .* sharp/,
                /After a moment, you .* tune/)
  when /After a moment, you .* tune/
    Lich::Messaging.msg("plain", "DRC: Instrument tuned.")
    return true
  when /After a moment, you .* flat/
    DRC.do_tune(instrument, "sharp")
  when /After a moment, you .* sharp/
    DRC.do_tune(instrument, "flat")
  when /^You should be sitting up/
    DRC.fix_standing
    DRC.do_tune(instrument)
  end
end

.fix_dr_bullshit(string) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Simplifies item names by dropping middle descriptive text, keeping only first and last words.

Handles the game's verbose item naming by reducing multi-word names to a gettable form. Special case: removes ' and chain' from 'ball and chain'. Returns the string unchanged if it has 2 or fewer words, or if no match-and-last pattern is found.

Examples:

DRC.fix_dr_bullshit("a gleaming dark iron sword") #=> "a sword"

Parameters:

  • string (String)

    the item name

Returns:

  • (String)

    the simplified name



872
873
874
875
876
877
878
879
880
881
# File 'documented/dragonrealms/commons/common.rb', line 872

def fix_dr_bullshit(string)
  return string if string.split.length <= 2

  string = string.sub(' and chain', '') if string =~ /ball and chain/

  match = string.match(/(?<first>\S+) .* (?<last>\S+)/)
  return string unless match

  "#{match[:first]} #{match[:last]}"
end

.fix_standingvoid

This method returns an undefined value.

Issues STAND until the character is standing, giving up when the game reports a state from which standing cannot currently succeed. Without the CANNOT_STAND_PATTERN guard these states loop forever spamming STAND, because matching the message never makes standing? true (issue #3668: safe-room spamming STAND while unconscious).



744
745
746
747
748
749
750
751
# File 'documented/dragonrealms/commons/common.rb', line 744

def fix_standing
  loop do
    break if standing?

    result = bput('stand', 'You stand', 'You are so unbalanced', 'As you stand', 'You are already', 'weight of all your possessions', 'You are overburdened and cannot', 'You\'re unconscious', 'You swim back up into a vertical position', "You don't seem to be able to move to do that", 'prevents you from standing', 'You\'re plummeting to your death', 'There\'s no room to do much of anything here')
    break if result =~ CANNOT_STAND_PATTERN
  end
end

.forage?(item, tries = 5) ⇒ Boolean

Attempts to forage for an item, retrying up to the specified number of times.

Compares hand contents before and after each forage attempt to confirm success. Handles cluttered rooms by attempting to kick piles, and handles full hands by stowing the right hand. Returns false if the room is too cluttered to recover, if foraging efforts are futile, or if stowing fails.

Examples:

DRC.forage?('herb') #=> true (if item was found)

Parameters:

  • item (String)

    the item to forage for

  • tries (Integer) (defaults to: 5)

    maximum number of forage attempts (default: 5)

Returns:

  • (Boolean)

    true if an item was foraged, false if unsuccessful



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'documented/dragonrealms/commons/common.rb', line 309

def forage?(item, tries = 5)
  snapshot = "#{right_hand}#{left_hand}"
  while snapshot == "#{right_hand}#{left_hand}"
    tries > 0 ? tries -= 1 : (return false)
    case bput("forage #{item}", 'Roundtime', 'The room is too cluttered to find anything here', 'You really need to have at least one hand free to forage properly', 'You survey the area and realize that any foraging efforts would be futile')
    when 'The room is too cluttered to find anything here'
      return false unless kick_pile?
    when 'You survey the area and realize that any foraging efforts would be futile'
      return false
    when 'You really need to have at least one hand free to forage properly'
      Lich::Messaging.msg("bold", "DRC: Hands not emptied properly. Stowing right hand...")
      unless DRCI.stow_hand('right')
        Lich::Messaging.msg("bold", "DRC: Failed to stow right hand, cannot forage.")
        return false
      end
    end
    waitrt?
  end
  true
end

.get_gems(container) ⇒ Array<String>

Returns gems from a rummage of the container.

Convenience method that calls #rummage with the 'G' parameter.

Parameters:

  • container (String)

    the container name

Returns:

  • (Array<String>)

    list of gem nouns

See Also:

  • #rummage


427
428
429
# File 'documented/dragonrealms/commons/common.rb', line 427

def get_gems(container)
  rummage('G', container)
end

.get_materials(container) ⇒ Array<String>

Returns materials from a rummage of the container.

Convenience method that calls #rummage with the 'M' parameter.

Parameters:

  • container (String)

    the container name

Returns:

  • (Array<String>)

    list of material nouns

See Also:

  • #rummage


438
439
440
# File 'documented/dragonrealms/commons/common.rb', line 438

def get_materials(container)
  rummage('M', container)
end

.get_noun(long_name) ⇒ String?

Extracts the gettable noun from an item's long name.

Removes flavor text using #remove_flavor_text, then scans for the last sequence of alphanumeric characters and hyphens/apostrophes (typically the noun). Returns nil or an empty string if no noun is found.

Examples:

DRC.get_noun("a blue gem-studded sword") #=> "sword"

Parameters:

  • long_name (String)

    the full item name (e.g., "a blue gem-studded sword")

Returns:

  • (String, nil)

    the extracted noun, or nil if not found

See Also:

  • #remove_flavor_text


578
579
580
# File 'documented/dragonrealms/commons/common.rb', line 578

def get_noun(long_name)
  remove_flavor_text(long_name).strip.scan(/[a-z\-']+$/i).first
end

.get_skins(container) ⇒ Array<String>

Returns skins from a rummage of the container.

Convenience method that calls #rummage with the 'S' parameter.

Parameters:

  • container (String)

    the container name

Returns:

  • (Array<String>)

    list of skin nouns

See Also:

  • #rummage


416
417
418
# File 'documented/dragonrealms/commons/common.rb', line 416

def get_skins(container)
  rummage('S', container)
end

.get_town_name(text) ⇒ Object

Looks up the canonical name of the town based on the given text. Utility to help identify the canonical town name based on arbitrary text. For example, "Theren" for "Therenborough" and "Haven" for "Riverhaven". It also handles missing apostrophes and the occasional space between names like "merkresh" or "Mer'Kresh" or "ainghazal" or "Ain Ghazal". Returns nil if unable to find a match.



723
724
725
726
727
728
729
730
731
# File 'documented/dragonrealms/commons/common.rb', line 723

def get_town_name(text)
  towns = $HOMETOWN_REGEX_MAP.select { |_town, regex| regex =~ text }.keys
  if towns.length > 1
    Lich::Messaging.msg("bold", "DRC: Found multiple towns that match '#{text}': #{towns}")
    Lich::Messaging.msg("bold", "DRC: Using first town that matched: #{towns.first}")
    Lich::Messaging.msg("bold", "DRC: To avoid ambiguity, please use the town's full name: https://elanthipedia.play.net/Category:Cities")
  end
  towns.first
end

.hide?(hide_type = 'hide') ⇒ Boolean

Attempts to hide, with fallback handling for common errors.

Issues the hide command (HIDE, STALK, or custom). Handles cases where the character is playing music (stops it first), is stalking (stops stalking first), or needs more time. Stops after hiding? returns true. Does not throw; returns the result of hiding? even if the command failed.

Examples:

DRC.hide? #=> true (if successfully hidden)

Parameters:

  • hide_type (String) (defaults to: 'hide')

    the hide command variant (default: 'hide')

Returns:

  • (Boolean)

    true if the character is hiding after the attempt



842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
# File 'documented/dragonrealms/commons/common.rb', line 842

def hide?(hide_type = 'hide')
  unless hiding?
    case bput(hide_type, 'Roundtime', 'too busy performing', 'can\'t see any place to hide yourself', 'Stalk what', 'You\'re already stalking', 'Stalking is an inherently stealthy', 'You haven\'t had enough time', 'You search but find no place to hide')
    when 'too busy performing'
      bput('stop play', 'You stop playing', 'In the name of')
      return hide?(hide_type)
    when "You're already stalking"
      put 'stop stalk'
      return hide?(hide_type)
    when 'You haven\'t had enough time'
      pause 1
      return hide?(hide_type)
    end
    pause
    waitrt?
  end
  hiding?
end

.kick_pile?(item = 'pile') ⇒ Boolean

Attempts to kick a pile out of the way.

Verifies standing, then checks for a pile in the room using Lich::DragonRealms::DRRoom.room_objs. Returns true only if the character successfully moved the pile by running and kicking; other kick responses (item not found, futile kick) return false.

Examples:

DRC.kick_pile? #=> true (if pile was kicked successfully)

Parameters:

  • item (String) (defaults to: 'pile')

    the pile or object to kick (default: 'pile')

Returns:

  • (Boolean)

    true if kicked with a running attack, false otherwise



363
364
365
366
367
# File 'documented/dragonrealms/commons/common.rb', line 363

def kick_pile?(item = 'pile')
  fix_standing
  return unless DRRoom.room_objs.any? { |room_obj| room_obj.match?(/pile/) }
  bput("kick #{item}", 'I could not find', 'take a step back and run up to', 'Now what did the .* ever do to you', 'You lean back and kick your feet,') == 'take a step back and run up to'
end

.left_handString?

Returns the name of the item in the character's left hand, or nil if empty.

Applies #fix_dr_bullshit to the game's item name to simplify verbose descriptions.

Examples:

DRC.left_hand #=> "sword" (or nil)

Returns:

  • (String, nil)

    the item name, or nil if hand is empty



890
891
892
# File 'documented/dragonrealms/commons/common.rb', line 890

def left_hand
  GameObj.left_hand.name == 'Empty' ? nil : fix_dr_bullshit(GameObj.left_hand.name)
end

.left_hand_nounString?

Returns the noun of the item in the character's left hand, or nil if empty.

Returns:

  • (String, nil)

    the item noun, or nil if hand is empty



908
909
910
# File 'documented/dragonrealms/commons/common.rb', line 908

def left_hand_noun
  GameObj.left_hand == 'Empty' ? nil : GameObj.left_hand.noun
end

.list_to_array(list) ⇒ Object

Take a game formatted list "an arrow, silver coins and a deobar strongbox" And return an array ["an arrow", "silver coins", "a deobar strongbox"] is this ever useful compared to the list_to_nouns?



445
446
447
# File 'documented/dragonrealms/commons/common.rb', line 445

def list_to_array(list)
  list.strip.split(/(?:,|(?:, |\s)?and\s?)(?:\s?<pushBold\/>\s?)?(?=\s\ba\b|\s\ban\b|\s\bsome\b|\s\bthe\b)/i).reject(&:empty?)
end

.list_to_nouns(list) ⇒ Object

Take a game formatted list "an arrow, silver coins and a deobar strongbox" And return an array of nouns ["arrow", "coins", "strongbox"]



560
561
562
563
564
565
# File 'documented/dragonrealms/commons/common.rb', line 560

def list_to_nouns(list)
  list_to_array(list)
    .map { |long_name| get_noun(long_name) }
    .compact
    .reject { |noun| noun == '' }
end

.listen?(teacher, observe_flag = false) ⇒ Boolean

Attempts to listen to a teacher, checking skill appropriateness for the character.

Listens to a teacher for the skill they are teaching. For Barbarians and Thieves, blocks listening to certain magic schools and combat skills; for Barbarians only, also blocks Utility. Returns true if listening succeeded (either newly started or already listening), or if the skill was inappropriate so the listener disengaged. Returns false if the teacher was not found, is not teaching, has left, or teaching is incompatible with the character's class.

Examples:

DRC.listen?("Master Trainer") #=> true

Parameters:

  • teacher (String)

    the teacher's name

  • observe_flag (Boolean) (defaults to: false)

    whether to use 'observe' instead of plain listen (default: false)

Returns:

  • (Boolean)

    true if listening is/was active and appropriate, false otherwise



767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
# File 'documented/dragonrealms/commons/common.rb', line 767

def listen?(teacher, observe_flag = false)
  return false if teacher.nil?
  return false if teacher.empty?

  bad_classes = %w[Thievery Sorcery]
  bad_classes += ['Life Magic', 'Holy Magic', 'Lunar Magic', 'Elemental Magic', 'Arcane Magic', 'Targeted Magic', 'Arcana', 'Attunement'] if DRStats.barbarian? || DRStats.thief?
  bad_classes += ['Utility'] if DRStats.barbarian?

  observe = observe_flag ? 'observe' : ''

  result = bput("listen to #{teacher} #{observe}", 'begin to listen to \w+ teach the .* skill', 'already listening', 'could not find who', 'You have no idea', 'isn\'t teaching a class', 'don\'t have the appropriate training', 'Your teacher appears to have left', 'isn\'t teaching you anymore', 'experience differs too much from your own', 'but you don\'t see any harm in listening', 'invitation if you wish to join this class', 'You cannot concentrate to listen to .* while in combat')
  if (skill_match = result.match(/begin to listen to \w+ teach the (?<skill>.*) skill/))
    return true if bad_classes.grep(/#{skill_match[:skill]}/i).empty?

    bput('stop listening', 'You stop listening')
  elsif result == 'already listening'
    return true
  elsif result == "but you don't see any harm in listening"
    bput('stop listening', 'You stop listening')
  end

  false
end

.log_window(text, window_name, make_bold = true, create_window = false, pre_clear_window = false) ⇒ Object

Sends a message to the specified window. Replaces the deprecated atmo method. By default the message is bold. Creates window upon request. Pre-clears window upon request.



1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
# File 'documented/dragonrealms/commons/common.rb', line 1477

def log_window(text, window_name, make_bold = true, create_window = false, pre_clear_window = false)
  if create_window
    _respond("<streamWindow id=\"#{window_name}\" title=\"#{window_name}\" location=\"center\" save=\"true\" />")
    _respond("<exposeStream id=\"#{window_name}\"/>")
  end

  if pre_clear_window
    _respond("<clearStream id=\"#{window_name}\"/>\r\n")
  end

  _respond(
    "<pushStream id=\"#{window_name}\"/>" + (make_bold ? bold(text) : text),
    "<popStream id=\"#{window_name}\" /><prompt time=\"#{XMLData.server_time.to_i}\">&gt;</prompt>"
  )
end

.message(text, make_bold = true) ⇒ Object

Sends a message to the game window. By default the message is bold. Delegates to Lich::Messaging.msg for consistent multi-frontend support.



1505
1506
1507
# File 'documented/dragonrealms/commons/common.rb', line 1505

def message(text, make_bold = true)
  Lich::Messaging.msg(make_bold ? "bold" : "plain", text, encode: false)
end

.parse_assess_teach_lines(lines) ⇒ Object

Pure parsing method for assess teach output (unit-testable)



818
819
820
821
822
823
824
825
826
827
828
829
# File 'documented/dragonrealms/commons/common.rb', line 818

def parse_assess_teach_lines(lines)
  lines.each_with_object({}) do |line, hash|
    match = line.match(ASSESS_TEACH_TEACHER_PATTERN)
    next unless match

    teacher = match[:teacher]
    skill = match[:skill]
    skill_filter = skill.match(ASSESS_TEACH_SKILL_FILTER_PATTERN)
    skill = skill_filter[:filtered_skill] if skill_filter
    hash[teacher] = skill
  end
end

.pause_allBoolean

Pauses all other scripts (except the current one) and locks the pause state.

Acquires $pause_all_lock. Already-paused scripts are tracked in @pause_all_no_unpause so they are not unpaused by #unpause_all. Waits 1 second before returning. Returns false if the lock is already held.

Returns:

  • (Boolean)

    true if all other scripts were paused, false if lock could not be acquired

See Also:

  • #unpause_all


1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
# File 'documented/dragonrealms/commons/common.rb', line 1288

def pause_all
  return false unless $pause_all_lock.try_lock

  @pause_all_no_unpause = []

  Script.running.find_all(&:paused?).each do |script|
    @pause_all_no_unpause << script
  end

  Script.running.find_all do |script|
    !script.paused? &&
      !script.no_pause_all &&
      script != Script.current
  end
        .each(&:pause)

  pause 1
  true
end

.play_song?(settings, song_list, worn = true, skip_clean = false, climbing = false, skip_tuning = false) ⇒ Boolean

Plays a song with smart retry logic and instrument maintenance.

Attempts to play songs from song_list (an array of [song_name, next_song] tuples). Tracks state in UserVars.song and UserVars.climbing_song per instrument. Detects instrument changes and resets song state. Handles errors by: tuning if out of tune, cleaning if dirty (if rank >= 20), waking/standing/stopping other actions, getting/wearing the instrument if needed, or returning false for unrecoverable states. On success, advances to the next song in the list unless at the end. Skips cleaning/tuning if the respective flag is true or if rank < 20.

Examples:

DRC.play_song?(settings, [["waltz", "foxtrot"], ["foxtrot", "waltz"]]) #=> true (if waltz plays)

Parameters:

  • settings (OpenStruct)

    settings with instrument, worn_instrument, and cleaning_cloth

  • song_list (Hash, Array)

    list of songs as [[name, next], [next, next2], ...]

  • worn (Boolean) (defaults to: true)

    whether the instrument is worn (vs. held); get/wear/remove as needed (default: true)

  • skip_clean (Boolean) (defaults to: false)

    skip the cleaning phase if dirty (default: false)

  • climbing (Boolean) (defaults to: false)

    use climbing song state instead of regular song (default: false)

  • skip_tuning (Boolean) (defaults to: false)

    skip the tuning phase if out of tune (default: false)

Returns:

  • (Boolean)

    true if the song started successfully or an error was unrecoverable, false if the character cannot play

See Also:

  • #stop_playing
  • #clean_instrument
  • #tune_instrument


1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
# File 'documented/dragonrealms/commons/common.rb', line 1040

def play_song?(settings, song_list, worn = true, skip_clean = false, climbing = false, skip_tuning = false)
  instrument = worn ? settings.worn_instrument : settings.instrument

  if UserVars.instrument.nil?
    Lich::Messaging.msg("plain", "DRC: No previous instrument setting detected. Cleaning stored song data.")
    UserVars.song = nil
    UserVars.climbing_song = nil
    UserVars.instrument = instrument
  elsif UserVars.instrument != instrument
    Lich::Messaging.msg("plain", "DRC: New instrument #{instrument} detected; old instrument: #{UserVars.instrument}. Resetting stored song data.")
    UserVars.song = nil
    UserVars.climbing_song = nil
    UserVars.instrument = instrument
  end
  UserVars.song = song_list.first.first unless UserVars.song
  UserVars.climbing_song = song_list.first.first unless UserVars.climbing_song
  song_to_play = climbing ? UserVars.climbing_song : UserVars.song
  play_command = "play #{song_to_play}"
  if instrument
    play_command = play_command + " on my #{instrument}"
  end
  fput('release ecry') if DRSpells.active_spells["Eillie's Cry"].to_i > 0
  result = bput(play_command, 'too damaged to play', 'dirtiness may affect your performance', 'slightest hint of difficulty', 'fumble slightly', /Your .+ is submerged in the water/, 'You begin a', 'You struggle to begin', 'You\'re already playing a song', 'You effortlessly begin', 'You begin some', 'You cannot play', 'Play on what instrument', 'Are you sure that\'s the right instrument', 'now isn\'t the best time to be playing', 'Perhaps you should find somewhere drier before trying to play', 'You should stop practicing', /^You really need to drain/, /Your .* tuning is off, and may hinder your performance/)
  case result
  when 'Play on what instrument', 'Are you sure that\'s the right instrument'
    unless DRCI.get_item?(instrument)
      Lich::Messaging.msg("bold", "DRC: Failed to get #{instrument}.")
      return false
    end
    if worn && !DRCI.wear_item?(instrument)
      Lich::Messaging.msg("bold", "DRC: Failed to wear #{instrument}.")
      return false
    end
    play_song?(settings, song_list, worn, skip_clean, climbing, skip_tuning)
  when 'now isn\'t the best time to be playing', 'Perhaps you should find somewhere drier before trying to play', 'You should stop practicing'
    false
  when 'You\'re already playing a song'
    fput('stop play')
    play_song?(settings, song_list, worn, skip_clean, climbing, skip_tuning)
  when 'You cannot play'
    wait_for_script_to_complete('safe-room')
  when /Your .* tuning is off, and may hinder your performance/
    Lich::Messaging.msg("bold", "DRC: Instrument out of tune. Attempting to tune it.")
    return true if DRSkill.getrank('Performance') < 20
    return true if skip_tuning
    return true unless DRC.tune_instrument(settings)

    play_song?(settings, song_list, worn, skip_clean, climbing, skip_tuning)
  when 'dirtiness may affect your performance', /^You really need to drain/
    return true if DRSkill.getrank('Performance') < 20
    return true if skip_clean
    return true unless clean_instrument(settings, worn)

    play_song?(settings, song_list, worn, skip_clean, climbing, skip_tuning)
  when 'slightest hint of difficulty', 'fumble slightly'
    true
  when 'You begin a', 'You effortlessly begin', 'You begin some'
    return true if song_to_play == song_list.to_a.last.last
    # Ignore difficulty messages if we have an offset
    return true if climbing && UserVars.climbing_song_offset

    stop_playing
    UserVars.climbing_song = song_list[UserVars.climbing_song] || song_list.first.first if climbing
    UserVars.song = song_list[UserVars.song] || song_list.first.first unless climbing
    play_song?(settings, song_list, worn, skip_clean, climbing, skip_tuning)
  when 'You struggle to begin'
    return true if song_to_play == song_list.first.first
    # Ignore difficulty messages if we have an offset
    return true if climbing && UserVars.climbing_song_offset

    stop_playing
    UserVars.climbing_song = song_list.first.first if climbing
    UserVars.song = song_list.first.first unless climbing
    play_song?(settings, song_list, worn, skip_clean, climbing, skip_tuning)
  else
    false
  end
end

.release_invisibilityvoid

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Releases all active invisibility spells and Khri silence/Vanish meditations.

Queries active spells for invisibility properties and releases each one by abbreviation. Also handles Khri silence and Vanish (Thief only) via their specific commands. Does not throw if meditations are not active or if the character is not invisible.



927
928
929
930
931
932
933
934
935
936
937
938
# File 'documented/dragonrealms/commons/common.rb', line 927

def release_invisibility
  get_data('spells')
    .spell_data
    .select { |_name, properties| properties['invisibility'] }
    .select { |name, _properties| DRSpells.active_spells.keys.include?(name) }
    .map { |_name, properties| properties['abbrev'] }
    .each { |abbrev| fput("release #{abbrev}") }

  # handle khri silence as it's not part of base-spells data, and method of ending it differs from spells
  bput('khri stop silence', 'You attempt to relax') if DRSpells.active_spells.keys.include?('Khri Silence')
  bput('khri stop vanish', /^You would need to start Vanish/, /^Your control over the limited subversion of reality falters/, /^You are not trained in the Vanish meditation/) if (DRStats.guild == "Thief" && invisible?)
end

.remove_flavor_text(item) ⇒ String

Strips descriptive flavor text ("... adorned with ...") from an item name, leaving the gettable noun phrase.

Applies the built-in FLAVOR_TEXT_PATTERN first, then any player-defined custom_flavor_text_patterns (regular expressions) for flavor the built-in pattern misses -- letting a player strip a new flavor phrasing without a Lich release. User patterns are compiled with a per-pattern timeout and validated/guarded by CustomSubstitutions; an invalid or runaway pattern is reported and skipped, never raising here.

Examples:

remove_flavor_text('a sword adorned with rubies of deep crimson') #=> 'a sword'

Parameters:

  • item (String)

    the item long name

Returns:

  • (String)

    the item name with flavor text removed

See Also:



598
599
600
601
# File 'documented/dragonrealms/commons/common.rb', line 598

def remove_flavor_text(item)
  custom_patterns = CustomSubstitutions.resolve(:custom_flavor_text_patterns, [], type: :regexes)
  CustomSubstitutions.apply_regexes(item.sub(FLAVOR_TEXT_PATTERN, ''), custom_patterns)
end

.retreat(ignored_npcs = []) ⇒ Boolean

Retreats from combat if there are hostiles in the room (excluding ignored NPCs).

Returns immediately if no hostiles are present. Otherwise, loops calling RETREAT until either the character successfully escapes (detected by RETREAT_ESCAPE_MESSAGES) or encounters an error (handled by calling #fix_standing). Halts the loop once escape is confirmed.

Examples:

DRC.retreat(['mentor']) #=> true (if escaped without counting mentor as hostile)

Parameters:

  • ignored_npcs (Array<String>) (defaults to: [])

    NPCs to exclude from hostility check (default: [])

Returns:

  • (Boolean)

    true if successfully retreated, nil/false otherwise



972
973
974
975
976
977
978
979
980
981
982
983
# File 'documented/dragonrealms/commons/common.rb', line 972

def retreat(ignored_npcs = [])
  return if (DRRoom.npcs - ignored_npcs).empty?

  loop do
    case DRC.bput("retreat", *RETREAT_ESCAPE_MESSAGES, *RETREAT_MESSAGES)
    when *RETREAT_ESCAPE_MESSAGES
      return true
    else
      DRC.fix_standing
    end
  end
end

.right_handString?

Returns the name of the item in the character's right hand, or nil if empty.

Applies #fix_dr_bullshit to the game's item name to simplify verbose descriptions.

Examples:

DRC.right_hand #=> "shield" (or nil)

Returns:

  • (String, nil)

    the item name, or nil if hand is empty



901
902
903
# File 'documented/dragonrealms/commons/common.rb', line 901

def right_hand
  GameObj.right_hand.name == 'Empty' ? nil : fix_dr_bullshit(GameObj.right_hand.name)
end

.right_hand_nounString?

Returns the noun of the item in the character's right hand, or nil if empty.

Returns:

  • (String, nil)

    the item noun, or nil if hand is empty



915
916
917
# File 'documented/dragonrealms/commons/common.rb', line 915

def right_hand_noun
  GameObj.right_hand == 'Empty' ? nil : GameObj.right_hand.noun
end

.rummage(parameter, container) ⇒ Array<String>

Rummages a container for items matching a category and returns parsed results.

Issues RUMMAGE /PARAMETER MY CONTAINER and parses the item list. The parameter determines how results are parsed: 'B' for boxes (adjective+noun via #box_list_to_adj_and_noun), 'SC' for scrolls (via #scroll_list_to_adj_and_noun), or any other value for generic nouns (via #list_to_nouns). Handles invisibility by releasing it and retrying. Returns an empty array if the container is closed, empty, doesn't exist, or the action is futile.

Examples:

DRC.rummage('S', 'backpack') #=> ["silk scroll", "parchment"]

Parameters:

  • parameter (String)

    search category: 'B' for boxes, 'SC' for scrolls, or other

  • container (String)

    the container name (without 'my')

Returns:

  • (Array<String>)

    parsed item names, or an empty array if nothing found

See Also:

  • #box_list_to_adj_and_noun
  • #scroll_list_to_adj_and_noun
  • #list_to_nouns


387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'documented/dragonrealms/commons/common.rb', line 387

def rummage(parameter, container)
  result = DRC.bput("rummage /#{parameter} my #{container}", 'but there is nothing in there like that\.', 'looking for .* and see .*', 'While it\'s closed', 'I don\'t know what you are referring to', 'You feel about', 'That would accomplish nothing')

  case result
  when 'You feel about'
    release_invisibility
    return rummage(parameter, container)
  when 'but there is nothing in there like that.', 'While it\'s closed', 'I don\'t know what you are referring to', 'That would accomplish nothing'
    return []
  end

  text = result.match(/looking for .* and see (.*)\.$/).to_a[1]
  case parameter
  when 'B'
    box_list_to_adj_and_noun(text)
  when 'SC'
    scroll_list_to_adj_and_noun(text)
  else
    list_to_nouns(text)
  end
end

.safe_pause_listArray<String>, false

Safely pauses all other scripts while protecting the caller from deadlock.

Acquires $safe_pause_lock and sets the lock holder's ignore_pause flag so it cannot be paused (preventing deadlock if another script tries to pause the lock holder). Complementary to #safe_unpause_list, which restores the flag and releases the lock. Returns false if the lock is already held.

Returns:

  • (Array<String>, false)

    names of paused scripts, or false if lock could not be acquired

See Also:

  • #safe_unpause_list


1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
# File 'documented/dragonrealms/commons/common.rb', line 1379

def safe_pause_list
  return false unless $safe_pause_lock.try_lock

  # Pausing is cooperative: a paused script is a live thread that keeps
  # every mutex it holds (Ruby frees a mutex on thread death, not on
  # pause). So a script that gets pause_script'd while holding
  # $safe_pause_lock never releases it, and every peer spins forever on
  # try_lock -> false. Make the lock-holder immune to pause for as long as
  # it owns the lock, so this deadlock cannot form. Save the prior value
  # (and the holder itself) so we restore exactly what was there for a
  # caller that was already ignoring pauses, e.g. mid-travel.
  @safe_pause_holder = Script.self
  @safe_pause_prev_ignore_pause = @safe_pause_holder&.ignore_pause
  @safe_pause_holder&.ignore_pause = true

  paused_script_list = []
  Script.running.find_all { |s| !s.paused? && !s.no_pause_all && s.name != Script.self.name }.each do |s|
    s.pause
    paused_script_list << s.name
  end
  Lich::Messaging.msg("plain", "DRC: Pausing #{paused_script_list} to run #{Script.self.name}")
  return paused_script_list
end

.safe_unpause_list(scripts_to_unpause) ⇒ Boolean

Safely unpauses the specified scripts and releases the pause lock.

Unpauses only scripts in scripts_to_unpause. Restores the lock holder's prior ignore_pause state before releasing $safe_pause_lock to prevent leaving the script permanently unpausable. Sends completion messages. Returns false if the lock is not held.

Parameters:

  • scripts_to_unpause (Array<String>)

    script names to unpause

Returns:

  • (Boolean)

    true if unpausing succeeded, false if lock is not held

See Also:

  • #safe_pause_list


1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
# File 'documented/dragonrealms/commons/common.rb', line 1413

def safe_unpause_list(scripts_to_unpause)
  return false unless $safe_pause_lock.owned?

  if scripts_to_unpause.empty?
    Lich::Messaging.msg("plain", "DRC: #{Script.self.name} has finished.")
  else
    Lich::Messaging.msg("plain", "DRC: Unpausing #{scripts_to_unpause}, #{Script.self.name} has finished.")
    Script.running.find_all { |s| s.paused? && !s.no_pause_all && scripts_to_unpause.include?(s.name) }.each(&:unpause)
  end
  # Restore the holder's pre-lock pause immunity before releasing the lock,
  # so we never leave a script permanently unpausable.
  @safe_pause_holder&.ignore_pause = @safe_pause_prev_ignore_pause
  @safe_pause_holder = nil
  @safe_pause_prev_ignore_pause = nil
  $safe_pause_lock.unlock
end

.scroll_list_to_adj_and_noun(list) ⇒ Array<String>

Converts a game rummage scroll list into gettable adjective+noun forms.

Pipeline per entry: strip the leading article, strip "labeled with...", apply the pre-collapse literal substitutions (DEFAULT_SCROLL_SUBSTITUTIONS_PRE merged with the player's custom_scroll_substitutions), apply the SCROLL_KEYWORD_COLLAPSE, then apply the post-collapse substitutions (DEFAULT_SCROLL_SUBSTITUTIONS_POST).

Examples:

scroll_list_to_adj_and_noun(' an icy blue parchment') #=> ['blue parchment']

Parameters:

  • list (String)

    game-formatted scroll list (e.g. from rummage /SC)

Returns:

  • (Array<String>)

    gettable scroll names

See Also:



546
547
548
549
550
551
552
553
554
555
556
# File 'documented/dragonrealms/commons/common.rb', line 546

def scroll_list_to_adj_and_noun(list)
  pre_substitutions = CustomSubstitutions.resolve(:custom_scroll_substitutions, DEFAULT_SCROLL_SUBSTITUTIONS_PRE, type: :pairs)
  list_to_array(list).map do |entry|
    without_article = entry
                      .sub(/(an|some|a(?: piece of)?)\s/, '')
                      .sub(/\slabeled with.*/, '')
    with_pre = pre_substitutions.reduce(without_article) { |text, (from, to)| text.sub(from, to) }
    collapsed = with_pre.sub(SCROLL_KEYWORD_COLLAPSE, ' \1')
    DEFAULT_SCROLL_SUBSTITUTIONS_POST.reduce(collapsed) { |text, (from, to)| text.sub(from, to) }.strip
  end
end

.set_stance(skill) ⇒ void

This method returns an undefined value.

Sets the combat stance distribution (evasion/parry/shield) by skill focus.

Calculates stance points based on guild and Defending skill rank. Paladins use a divisor of 50, Barbarians/Rangers/Traders/Commoners use 60, others use 70. Distributes 80 + (Defending rank / divisor) points: 100 to the chosen skill, remaining to secondary, and overflow (if any) to tertiary.

Examples:

DRC.set_stance('parry') #=> (issues STANCE SET with parry-favoring distribution)

Parameters:

  • skill (String)

    the skill to focus: 'evasion', 'parry', or 'shield'



1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
# File 'documented/dragonrealms/commons/common.rb', line 1441

def set_stance(skill)
  div = if DRStats.guild == 'Paladin'
          50
        elsif %w[Barbarian Ranger Trader Commoner].include?(DRStats.guild)
          60
        else
          70
        end

  points = 80 + DRSkill.getrank('Defending') / div
  secondary = points > 100 ? 100 : points
  tertiary = points > 100 ? points - 100 : 0

  stance = case skill.downcase
           when 'evasion'
             "100 #{secondary} #{tertiary}"
           when 'parry'
             "#{secondary} 100 #{tertiary}"
           when 'shield'
             "#{secondary} #{tertiary} 100"
           else
             "100 #{secondary} #{tertiary}"
           end

  DRC.bput("stance set #{stance}", /Setting your/)
end

.smart_pause_allArray<String>

Pauses all other unpaused scripts and returns their names for later resumption.

Does not use a lock. Sends a message to plain output listing paused scripts. Complementary to #unpause_all_list; the caller is responsible for tracking the returned list and passing it to unpause.

Examples:

paused = DRC.smart_pause_all
# ... do work ...
DRC.unpause_all_list(paused)

Returns:

  • (Array<String>)

    names of paused scripts

See Also:

  • #unpause_all_list


1341
1342
1343
1344
1345
1346
1347
1348
1349
# File 'documented/dragonrealms/commons/common.rb', line 1341

def smart_pause_all
  paused_script_list = []
  Script.running.find_all { |s| !s.paused? && !s.no_pause_all && s.name != Script.self.name }.each do |s|
    s.pause
    paused_script_list << s.name
  end
  Lich::Messaging.msg("plain", "DRC: Pausing #{paused_script_list} to run #{Script.self.name}")
  return paused_script_list
end

.stop_playingvoid

This method returns an undefined value.

Stops the current song.

Issues STOP PLAY and waits for a response. Handles the case where the character is not playing.



1125
1126
1127
# File 'documented/dragonrealms/commons/common.rb', line 1125

def stop_playing
  bput('stop play', 'You stop playing your song', 'In the name of', "But you're not performing")
end

.strip_xml(lines) ⇒ Array<String>

Strips XML tags and decodes common HTML entities from game output lines.

Parameters:

  • lines (Array<String>)

    Array of raw game output lines

Returns:

  • (Array<String>)

    Array of non-empty, trimmed strings with XML removed



114
115
116
117
# File 'documented/dragonrealms/commons/common.rb', line 114

def strip_xml(lines)
  lines.map { |line| line.gsub(XML_TAG_PATTERN, '').gsub('&gt;', '>').gsub('&lt;', '<').strip }
       .reject(&:empty?)
end

.text2num(text_num) ⇒ Integer?

Converts spoken number words to an integer.

Parses a text string like 'one hundred twenty-three' into its numeric value. Handles multipliers (hundred, thousand) and hyphens/spaces as separators. Returns nil and logs a message if an unknown word is encountered.

Examples:

DRC.text2num("twenty-five") #=> 25
DRC.text2num("one hundred") #=> 100

Parameters:

  • text_num (String)

    the spoken number (e.g., "fifty", "two hundred thirty-five")

Returns:

  • (Integer, nil)

    the numeric value, or nil if an unknown word is found



996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
# File 'documented/dragonrealms/commons/common.rb', line 996

def text2num(text_num)
  text_num = text_num.tr('-', ' ')
  split_words = text_num.split(' ')
  g = 0

  split_words.each do |word|
    x = $NUM_MAP.fetch(word, nil)
    if word.eql?('hundred') && (g != 0)
      g *= 100
    elsif word.eql?('thousand') && (g != 0)
      g *= 1000
    elsif x.nil?
      Lich::Messaging.msg("bold", "DRC: Unknown number word '#{word}' in '#{text_num}'")
      return nil
    else
      g += x
    end
  end

  g
end

.tune_instrument(settings) ⇒ Boolean

Tunes an instrument by iteratively adjusting flat/sharp until in tune.

Stops any playing, verifies the instrument is in hand, and removes/gets it if needed. Delegates to #do_tune for the actual tuning loop. Re-wears the instrument if it was worn. Returns false if the instrument cannot be obtained or both hands are not free.

Examples:

DRC.tune_instrument(settings) #=> true

Parameters:

  • settings (OpenStruct)

    settings with instrument and worn_instrument

Returns:

  • (Boolean)

    true if tuning succeeded, false if the instrument is not available or both hands are full

See Also:

  • #do_tune


1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
# File 'documented/dragonrealms/commons/common.rb', line 1207

def tune_instrument(settings)
  instrument = settings.worn_instrument || settings.instrument

  unless instrument
    Lich::Messaging.msg("bold", "DRC: Neither worn_instrument nor instrument set. Doing nothing.")
    return false
  end

  DRC.stop_playing

  unless (DRC.left_hand.nil? && DRC.right_hand.nil?) || DRCI.in_hands?(instrument)
    Lich::Messaging.msg("bold", "DRC: Need two free hands. Not tuning now.")
    return false
  end

  if settings.worn_instrument
    unless DRCI.remove_item?(instrument) || DRCI.in_hands?(instrument)
      Lich::Messaging.msg("bold", "DRC: Could not remove #{instrument}. Not trying to tune.")
      DRC.beep
      return false
    end
  else
    unless DRCI.get_item?(instrument) || DRCI.in_hands?(instrument)
      Lich::Messaging.msg("bold", "DRC: Could not get #{instrument}. Not trying to tune.")
      DRC.beep
      return false
    end
  end
  DRC.do_tune(instrument)
  waitrt?
  pause 1
  DRCI.wear_item?(instrument) if settings.worn_instrument
  true
end

.unpause_allBoolean

Unpauses scripts that were paused by #pause_all, excluding those already paused before.

Releases $pause_all_lock after unpausing. Returns false if the lock is not held.

Returns:

  • (Boolean)

    true if unpausing succeeded, false if lock is not held

See Also:

  • #pause_all


1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
# File 'documented/dragonrealms/commons/common.rb', line 1314

def unpause_all
  return false unless $pause_all_lock.owned?

  Script.running.find_all do |script|
    script.paused? &&
      !@pause_all_no_unpause.include?(script)
  end
        .each(&:unpause)

  @pause_all_no_unpause = []
  $pause_all_lock.unlock

  true
end

.unpause_all_list(scripts_to_unpause) ⇒ void

This method returns an undefined value.

Unpauses the specified scripts and sends a completion message.

Unpauses only scripts in scripts_to_unpause (allowing caller control). Sends different messages depending on whether any scripts are being unpaused.

Examples:

DRC.unpause_all_list(["hunting", "mining"])

Parameters:

  • scripts_to_unpause (Array<String>)

    script names to unpause

See Also:

  • #smart_pause_all


1361
1362
1363
1364
1365
1366
1367
1368
# File 'documented/dragonrealms/commons/common.rb', line 1361

def unpause_all_list(scripts_to_unpause)
  if scripts_to_unpause.empty?
    Lich::Messaging.msg("plain", "DRC: #{Script.self.name} has finished.")
  else
    Lich::Messaging.msg("plain", "DRC: Unpausing #{scripts_to_unpause}, #{Script.self.name} has finished.")
    Script.running.find_all { |s| s.paused? && !s.no_pause_all && scripts_to_unpause.include?(s.name) }.each(&:unpause)
  end
end

.verify_script(script_names) ⇒ Boolean

Checks that each script name in the list exists as a loadable script.

Sends a message to bold output for each missing script. Does not raise or stop execution; the caller must check the return value to decide whether to proceed.

Examples:

DRC.verify_script(['hunting', 'mining']) #=> true (if both exist)
DRC.verify_script('invalid-name') #=> false

Parameters:

  • script_names (String, Array<String>)

    one script name or an array of names

Returns:

  • (Boolean)

    true if all scripts exist, false if any are missing



244
245
246
247
248
249
250
251
252
253
254
# File 'documented/dragonrealms/commons/common.rb', line 244

def verify_script(script_names)
  script_names = [script_names] unless script_names.is_a?(Array)
  state = true
  script_names
    .reject { |name| Script.exists?(name) }
    .each do |name|
      Lich::Messaging.msg("bold", "DRC: Failed to find a script named '#{name}'")
      state = false
    end
  state
end

.wait_for_script_to_complete(name, args = [], flags = {}) ⇒ Object

Starts a script with the given arguments and blocks until it completes.

Verifies the script exists before starting. Waits 2 seconds after starting, then polls Script.running until the script is no longer in the list.

Examples:

DRC.wait_for_script_to_complete('hunting', ['ogre'])

Parameters:

  • name (String)

    the script name

  • args (Array) (defaults to: [])

    command-line arguments to pass to the script; strings with spaces are auto-quoted

  • flags (Hash) (defaults to: {})

    start flags (e.g., no-pause, quiet)

Returns:

  • (Object)

    the script handle, or nil if verification failed



267
268
269
270
271
272
273
274
275
# File 'documented/dragonrealms/commons/common.rb', line 267

def wait_for_script_to_complete(name, args = [], flags = {})
  verify_script(name)
  script_handle = start_script(name, args.map { |arg| arg.to_s =~ /\s/ ? "\"#{arg}\"" : arg }, flags)
  if script_handle
    pause 2
    pause 0.5 while Script.running.include?(script_handle)
  end
  script_handle
end